diff --git a/.gitignore b/.gitignore index a0ef7b9..b76794e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ build node_modules -.DS_Store \ No newline at end of file +.DS_Store +package-lock.json +dist +docs +coffees +.vscode \ No newline at end of file diff --git a/Makefile b/Makefile index fe6bcce..c6c4b80 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ -BUILDDIR?=build/htdocs/os +BUILDDIR?=/opt/www/htdocs/os +DOCDIR?=/opt/www/htdocs/doc/antos/api BLUE=\033[1;34m NC=\033[0m @@ -11,88 +12,109 @@ ifeq ($(UNAME_S),Darwin) endif -coffees= src/core/core.coffee\ - src/core/api.coffee\ - src/core/settings.coffee\ - src/core/handlers/RemoteHandler.coffee\ - src/core/vfs.coffee\ - src/core/vfs/GoogleDriveHandler.coffee\ - src/core/db.coffee\ - src/core/gui.coffee\ - src/core/BaseModel.coffee\ - src/core/BaseApplication.coffee\ - src/core/BaseService.coffee\ - src/core/BaseEvent.coffee\ - src/core/BaseDialog.coffee\ - src/antos.coffee +javascripts= dist/core/core.js \ + dist/core/settings.js \ + dist/core/handles/RemoteHandle.js \ + dist/core/Announcerment.js \ + dist/core/vfs.js \ + dist/core/db.js \ + dist/core/BaseModel.js \ + dist/core/BaseApplication.js \ + dist/core/BaseService.js \ + dist/core/BaseEvent.js \ + dist/core/BaseDialog.js \ + dist/core/tags/tag.js \ + dist/core/tags/WindowTag.js \ + dist/core/tags/TileLayoutTags.js \ + dist/core/tags/ResizerTag.js \ + dist/core/tags/LabelTag.js \ + dist/core/tags/ButtonTag.js \ + dist/core/tags/ListViewTag.js \ + dist/core/tags/SwitchTag.js \ + dist/core/tags/NSpinnerTag.js \ + dist/core/tags/MenuTag.js \ + dist/core/tags/GridViewTag.js \ + dist/core/tags/TabBarTag.js \ + dist/core/tags/TabContainerTag.js \ + dist/core/tags/TreeViewTag.js \ + dist/core/tags/SliderTag.js \ + dist/core/tags/FloatListTag.js \ + dist/core/tags/CalendarTag.js \ + dist/core/tags/ColorPickerTag.js \ + dist/core/tags/FileViewTag.js \ + dist/core/tags/OverlayTag.js \ + dist/core/tags/AppDockTag.js \ + dist/core/tags/SystemPanelTag.js \ + dist/core/gui.js \ + dist/core/pm.js \ + dist/bootstrap.js +packages = Syslog CodePad Files MarketPlace Setting - - -packages = CoreServices NotePad wTerm ActivityMonitor Files MarkOn MarketPlace Preview Setting - -main: initd build_coffees build_tags build_themes schemes libs build_packages languages +main: initd build_javascripts build_themes libs build_packages languages - cp src/index.html $(BUILDDIR)/ initd: - mkdir -p $(BUILDDIR) -lite: build_coffees build_tags build_themes schemes build_packages +lite: build_javascripts build_themes build_packages #%.js: %.coffee # coffee --compile $< -build_coffees: - @echo "$(BLUE)Building coffee files$(NC)" +build_javascripts: + -rm -rf dist + tsc + @echo "$(BLUE)Bundling javascript files$(NC)" - mkdir $(BUILDDIR)/scripts - rm $(BUILDDIR)/scripts/antos.js - - rm $(BUILDDIR)/scripts/antos.coffee - for f in $(coffees); do (cat "$${f}"; echo) >> $(BUILDDIR)/scripts/antos.coffee; done - coffee --compile $(BUILDDIR)/scripts/antos.coffee - - rm $(BUILDDIR)/scripts/antos.coffee - - + #echo "(function() {" > $(BUILDDIR)/scripts/antos.js + for f in $(javascripts); do \ + (cat "$${f}"; echo) >> dist/antos.js;\ + rm "$${f}";\ + done + cp dist/antos.js $(BUILDDIR)/scripts/ + echo "if(exports){ exports.__esModule = true;exports.OS = OS; }" >> dist/antos.js + rm -r dist/core libs: @echo "$(BLUE)Copy lib files$(NC)" cp -rf src/libs/* $(BUILDDIR)/scripts/ -schemes: - @echo "$(BLUE)Copy schemes files$(NC)" - - mkdir -p $(BUILDDIR)/resources/schemes - cp src/core/schemes/* $(BUILDDIR)/resources/schemes/ - testdata: @echo "$(BLUE)Copy JSON test files$(NC)" - mkdir -p $(BUILDDIR)/resources/jsons cp src/core/handlers/jsons/* $(BUILDDIR)/resources/jsons -build_tags: - @echo "$(BLUE)Building tag files$(NC)" - -mkdir $(BUILDDIR)/resources - -rm $(BUILDDIR)/resources/antos_tags.js - for f in src/core/tags/*; do (cat "$${f}"; echo) >> $(BUILDDIR)/resources/antos_tags.js; done languages: - -mkdir $(BUILDDIR)/resources - -mkdir $(BUILDDIR)/resources/languages + -mkdir -p $(BUILDDIR)/resources + -mkdir -p $(BUILDDIR)/resources/languages cp src/core/languages/*.json $(BUILDDIR)/resources/languages/ genlang: read -r -p "Enter locale: " LOCAL;\ ./src/core/languages/gen.sh ./src ./src/core/languages/$$LOCAL.json -build_themes: antos_themes_build - cp -r src/themes/system $(BUILDDIR)/resources/themes/ +build_themes: antos_light antos_dark + -rm -rf $(BUILDDIR)/resources/themes/system/* + -mkdir -p $(BUILDDIR)/resources/themes/system + cp -r src/themes/system/fonts $(BUILDDIR)/resources/themes/system + cp -r src/themes/system/wp $(BUILDDIR)/resources/themes/system + for f in src/themes/system/*.css; do (cat "$${f}"; echo) >> $(BUILDDIR)/resources/themes/system/system.css;done -antos_themes_build: - @echo "$(BLUE)Building themes name: antos$(NC)" - -rm -rf $(BUILDDIR)/resources/themes/antos/* - -mkdir -p $(BUILDDIR)/resources/themes/antos - for f in src/themes/antos/*.css; do (cat "$${f}"; echo) >> $(BUILDDIR)/resources/themes/antos/antos.css;done - -mkdir -p $(BUILDDIR)/resources/themes/antos/fonts - cp -rf src/themes/antos/fonts/* $(BUILDDIR)/resources/themes/antos/fonts - cp src/themes/antos/wp* $(BUILDDIR)/resources/themes/antos/ +antos_light: + @echo "$(BLUE)Building themes name: antos-light$(NC)" + -rm -rf $(BUILDDIR)/resources/themes/antos_light/* + -mkdir -p $(BUILDDIR)/resources/themes/antos_light + for f in src/themes/antos_light/*.css; do (cat "$${f}"; echo) >> $(BUILDDIR)/resources/themes/antos_light/antos_light.css;done + + +antos_dark: + @echo "$(BLUE)Building themes name: antos-dark$(NC)" + -rm -rf $(BUILDDIR)/resources/themes/antos_dark/* + -mkdir -p $(BUILDDIR)/resources/themes/antos_dark + for f in src/themes/antos_dark/*.css; do (cat "$${f}"; echo) >> $(BUILDDIR)/resources/themes/antos_dark/antos_dark.css;done build_packages: - - mkdir $(BUILDDIR)/packages + - mkdir -p $(BUILDDIR)/packages - for d in $(packages); do ( test -d $(BUILDDIR)/packages/$$d && rm -rf $(BUILDDIR)/packages/$$d/* ); done for d in $(packages); do (cd src/packages/$$d; make);done for d in $(packages); do ( test -d $(BUILDDIR)/packages/$$d || mkdir -p $(BUILDDIR)/packages/$$d && cp -rf src/packages/$$d/build/* $(BUILDDIR)/packages/$$d/);done @@ -104,42 +126,42 @@ package: cd src/packages/$$PKG && make;\ cd ../../../;\ test -d $(BUILDDIR)/packages/$$PKG || mkdir -p $(BUILDDIR)/packages/$$PKG;\ - cp -rf src/packages/$$PKG/build/* $(BUILDDIR)/packages/$$PKG/;\ + cp -rfv src/packages/$$PKG/build/* $(BUILDDIR)/packages/$$PKG/;\ test -d src/packages/$$PKG/build && rm -r src/packages/$$PKG/build; pkgar: read -r -p "Enter package name: " PKG;\ echo $$PKG | make package &&\ - test -f $(BUILDDIR)/packages/$$PKG/main.js && uglifyjs $(BUILDDIR)/packages/$$PKG/main.js --compress --mangle --output $(BUILDDIR)/packages/$$PKG/main.js;\ + test -f $(BUILDDIR)/packages/$$PKG/main.js && terser $(BUILDDIR)/packages/$$PKG/main.js --compress --mangle --output $(BUILDDIR)/packages/$$PKG/main.js;\ test -f $(BUILDDIR)/packages/$$PKG/main.css && uglifycss --output $(BUILDDIR)/packages/$$PKG/main.css $(BUILDDIR)/packages/$$PKG/main.css;\ cd $(BUILDDIR)/packages/$$PKG && zip -r "$$PKG.zip" ./ ; \ cd ../../ && (test -d repo/$$PKG || mkdir repo/$$PKG) && mv packages/$$PKG/"$$PKG.zip" repo/$$PKG && touch repo/$$PKG/$$PKG.md && rm -r packages/$$PKG uglify: - # uglify antos.js - # npm install uglify-es -g - # npm install uglify-js -g - uglifyjs $(BUILDDIR)/scripts/antos.js --compress --mangle --output $(BUILDDIR)/scripts/antos.js + # sudo npm install terser -g + # + terser $(BUILDDIR)/scripts/antos.js --compress --mangle --output $(BUILDDIR)/scripts/antos.js # uglify tags - # npm install riot-cli -g - riot --ext js $(BUILDDIR)/resources/antos_tags.js $(BUILDDIR)/resources/antos_tags.js - uglifyjs $(BUILDDIR)/resources/antos_tags.js --compress --mangle --output $(BUILDDIR)/resources/antos_tags.js - $(GSED) -i 's/resources\/antos_tags.js/scripts\/riot.min.js/g' $(BUILDDIR)/index.html - $(GSED) -i 's/scripts\/riot.compiler.min.js/resources\/antos_tags.js/g' $(BUILDDIR)/index.html - $(GSED) -i 's/type=\"riot\/tag\"/ /g' "$(BUILDDIR)/index.html" # npm install uglifycss -g # uglify the css - uglifycss --output $(BUILDDIR)/resources/themes/antos/antos.css $(BUILDDIR)/resources/themes/antos/antos.css - uglifycss --output $(BUILDDIR)/resources/themes/system/font-awesome.css $(BUILDDIR)/resources/themes/system/font-awesome.css + uglifycss --output $(BUILDDIR)/resources/themes/antos_light/antos_light.css $(BUILDDIR)/resources/themes/antos_light/antos_light.css + uglifycss --output $(BUILDDIR)/resources/themes/antos_dark/antos_dark.css $(BUILDDIR)/resources/themes/antos_dark/antos_dark.css + uglifycss --output $(BUILDDIR)/resources/themes/system/system.css $(BUILDDIR)/resources/themes/system/system.css #uglify each packages for d in $(packages); do\ echo "Uglifying $$d";\ - test -f $(BUILDDIR)/packages/$$d/main.js && uglifyjs $(BUILDDIR)/packages/$$d/main.js --compress --mangle --output $(BUILDDIR)/packages/$$d/main.js;\ + test -f $(BUILDDIR)/packages/$$d/main.js && terser $(BUILDDIR)/packages/$$d/main.js --compress --mangle --output $(BUILDDIR)/packages/$$d/main.js;\ test -f $(BUILDDIR)/packages/$$d/main.css && uglifycss --output $(BUILDDIR)/packages/$$d/main.css $(BUILDDIR)/packages/$$d/main.css;\ done release: main uglify +doc: + ./node_modules/.bin/typedoc --mode file --excludeNotExported --hideGenerator --name "AntOS API" --out $(DOCDIR) + clean: - rm -rf $(BUILDDIR)/* + rm -rf $(BUILDDIR)/resources + rm -rf $(BUILDDIR)/scripts + rm -rf $(BUILDDIR)/packages + rm -rf $(BUILDDIR)/index.html diff --git a/README.md b/README.md index 3ddcb07..385c0d6 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,34 @@ # antOS + +**This version 1.0.0a removes the dependencies on Riot.js by reimplementing the major API for GUI and Announcement system. The entire core API is also rewritten in TypeScript** + [![Build Status](https://travis-ci.org/lxsang/antos.svg?branch=master)](https://travis-ci.org/lxsang/antos) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Flxsang%2Fantos.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Flxsang%2Fantos?ref=badge_shield) -Server or Embedded Linux are often headless, so accessing the resource on these systems is not always obvious. The aim of this project is to develop a client core API that enables desktop like experience to remote server's resource accessing using web technologies. AntOS is based on jQuery and Riot, it is designed to be used along with our [**antd**](https://github.com/lxsang/ant-http) server and Lua based server side app, but it can also be adapted to any server side languages (PHP, etc) and server, by implementing all the system calls API defined in core/handlers/RemoteHandler.coffee. Basically, application design for the web os relies on these system calls to communicating with the server. The API defines the core UI, system calls (to server), Virtual File system, virtual database and the necessary libraries for easing the development of webOS's applications. Applications can be developped with coffee/javascript/css without the need of a server side script. +AntOS is a front-end API that mimics the traditional desktop environment on the web browser. The front-end can connect to a remote server and acts as a virtual desktop environment (VDE). The original purpose of AntOS is to provide visual tools to access and control resource on remote server +and embedded linux environment. With its application API and the provided SDK, AntOS facilitates the +development and deployment of user specific applications. + +This repository contains only the front-end API. To have fully functional VDE, AntOS need to connect +to the corresponding server side API. + +![https://os.lxsang.me/VFS/shared/d4645d65b3e4bb348f1bde0d42598ad9b99367f5](https://os.lxsang.me/VFS/shared/d4645d65b3e4bb348f1bde0d42598ad9b99367f5) -Note that, the development of the project is in early alpha state, so bugs are very welcome :) -The WebOS is tested on recent Firefox, Chrome and Safari, however i did not test it on IE or Edge since i have no Windows device :) ## Demo -A demo of the web desktop is available at my page [https://os.lxsang.me](https://os.lxsang.me) using username: demo and password: demo +A demo of the VDE is available at my page [https://os.iohub.dev](https://os.iohub.dev) using username: demo and password: demo -![Screenshot](screenshot.png "Screenshot") ## AntOS applications [https://github.com/lxsang/antosdk-apps](https://github.com/lxsang/antosdk-apps) - -## Build -Note that this is only the client API, to make it work for your application, you need to implement all the system calls in core/handlers/RemoteHandler.coffee using a server side scripting language (e.g. PHP). I'm planning to release an API documentation which describes what need to be sent and what will be returned for each system call in near future (i'm kind of very busy right now :) ). +## Documentation -I'm a big fan of the Make system, so i use it as a build system for all of my projects. So, to build AntOS: -1. You need to have *make* installed. Then since most of the API is written in Coffee script, you will need it to be installed too. -2. Edit the BUILDDIR variable in the Makefile file to point to where you want to put the built API -3. Type `make` then you are good to go. - -It you have any problem, please contact me or open an issue, i'll try to response ASAP. +- API documentation: [https://doc.iohub.dev/antos/api/](https://doc.iohub.dev/antos/api/) ## Licence -Copyright 2017-2018 Xuan Sang LE +Copyright 2017-2020 Xuan Sang LE AnTOS is is licensed under the GNU General Public License v3.0, see the LICENCE file for more information diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..8bbaf40 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + roots: [''], + transform : { + '^.+\\.ts$': 'ts-jest' + }, + testRegex: '(/tests/test.*|(\\.|/)(test|spec))\\.[tj]s?$', + moduleFileExtensions: ['js', 'ts'], + } \ No newline at end of file diff --git a/src/antos.coffee b/src/antos.coffee deleted file mode 100644 index 70e3290..0000000 --- a/src/antos.coffee +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2017-2018 Xuan Sang LE - -# AnTOS Web desktop is is licensed under the GNU General Public -# License v3.0, see the LICENCE file for more information - -# This program is free software: you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation, either version 3 of -# the License, or (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. - -# You should have received a copy of the GNU General Public License -#along with this program. If not, see https://www.gnu.org/licenses/. -_GUI = self.OS.GUI -_API = self.OS.API -_PM = self.OS.PM -_OS = self.OS -_courrier = self.OS.courrier -this.onload = () -> - ($ document).on 'webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange', ()-> - _GUI.fullscreen = not _GUI.fullscreen - self.OS.boot() \ No newline at end of file diff --git a/src/bootstrap.ts b/src/bootstrap.ts new file mode 100644 index 0000000..ef8204d --- /dev/null +++ b/src/bootstrap.ts @@ -0,0 +1,25 @@ +// Copyright 2017-2018 Xuan Sang LE + +// AnTOS Web desktop is is licensed under the GNU General Public +// License v3.0, see the LICENCE file for more information + +// This program is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. + +// You should have received a copy of the GNU General Public License +//along with this program. If not, see https://www.gnu.org/licenses/. + +Ant.onload = function () { + $(document).on( + "webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange", + () => (Ant.OS.GUI.fullscreen = !Ant.OS.GUI.fullscreen) + ); + return Ant.OS.boot(); +}; \ No newline at end of file diff --git a/src/core/Announcerment.ts b/src/core/Announcerment.ts new file mode 100644 index 0000000..c613fed --- /dev/null +++ b/src/core/Announcerment.ts @@ -0,0 +1,353 @@ +// Copyright 2017-2018 Xuan Sang LE + +// AnTOS Web desktop is is licensed under the GNU General Public +// License v3.0, see the LICENCE file for more information + +// This program is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. + +// You should have received a copy of the GNU General Public License +//along with this program. If not, see https://www.gnu.org/licenses/. + +namespace OS { + export namespace API { + /** + * Observable entry type definition + * + * @export + * @interface ObservableEntryType + */ + export interface ObservableEntryType { + /** + * A Set of callbacks that should be called only once. + * These callbacks will be removed after the first + * occurrence of the corresponding event + * + * @memberof ObservableEntryType + */ + one: Set<(d: any) => void>; + + /** + * A Set of callbacks that should be called + * every time the corresponding event is triggered + * + * @memberof ObservableEntryType + */ + many: Set<(d: any) => void>; + } + + /** + * Announcement listener type definition + * + * @export + * @interface AnnouncerListenerType + */ + export interface AnnouncerListenerType { + [index: number]: { + /** + * The event name + * + * @type {string} + */ + e: string; + + /** + * The event callback + * + */ + f: (d: any) => void; + }[]; + } + + /** + * This class is the based class used in AntOS event + * announcement system. + * It implements the observer pattern using simple + * subscribe/publish mechanism + * @export + * @class Announcer + */ + export class Announcer { + /** + * The observable object that stores event name + * and its corresponding callback in [[ObservableEntryType]] + * + * @type {GenericObject} + * @memberof Announcer + */ + observable: GenericObject; + + /** + * Enable/disable the announcer + * + * @type {boolean} + * @memberof Announcer + */ + enable: boolean; + + /** + *Creates an instance of Announcer. + * @memberof Announcer + */ + constructor() { + this.observable = {}; + this.enable = true; + } + + /** + * Disable the announcer, when this function is called + * all events and their callbacks will be removed + * + * @returns + * @memberof Announcer + */ + disable() { + this.off("*"); + return (this.enable = false); + } + + /** + * Subscribe to an event, the callback will be called + * every time the corresponding event is trigged + * + * @param {string} evtName event name + * @param {(d: any) => void} callback The corresponding callback + * @returns {void} + * @memberof Announcer + */ + on(evtName: string, callback: (d: any) => void): void { + if (!this.enable) { + return; + } + if (!this.observable[evtName]) { + this.observable[evtName] = { + one: new Set(), + many: new Set(), + }; + } + this.observable[evtName].many.add(callback); + } + + /** + * Subscribe to an event, the callback will + * be called only once and then removed from the announcer + * + * @param {string} evtName event name + * @param {(d: any) => void} callback the corresponding callback + * @returns {void} + * @memberof Announcer + */ + one(evtName: string, callback: (d: any) => void): void { + if (!this.enable) { + return; + } + if (!this.observable[evtName]) { + this.observable[evtName] = { + one: new Set(), + many: new Set(), + }; + } + this.observable[evtName].one.add(callback); + } + + /** + * Unsubscribe the callback from an event + * + * @param {string} evtName event name + * @param {(d: any) => void} [callback] the callback to be unsubscribed. + * When the `callback` is `*`, all callbacks related to `evtName` will be + * removed + * @memberof Announcer + */ + off(evtName: string, callback?: (d: any) => void): void { + const fn = (evt: string, cb: (d: any) => void) => { + if (!this.observable[evt]) { + return; + } + if (cb) { + this.observable[evt].one.delete(cb); + return this.observable[evt].many.delete(cb); + } else { + if (this.observable[evt]) { + return delete this.observable[evt]; + } + } + }; + if (evtName === "*") { + for (let k in this.observable) { + fn(k, callback); + } + } else { + fn(evtName, callback); + } + } + + /** + * Trigger an event + * + * @param {string} evtName event name + * @param {*} data data object that will be send to all related callback + * @returns {void} + * @memberof Announcer + */ + trigger(evtName: string, data: any): void { + const trig = (name: string, d: any) => { + const names = [name, "*"]; + for (let evt of names) { + if (!this.observable[evt]) { + continue; + } + this.observable[evt].one.forEach((f) => f(d)); + this.observable[evt].one = new Set(); + this.observable[evt].many.forEach((f) => f(d)); + } + }; + if (evtName === "*") { + for (let k in this.observable) { + const v = this.observable[k]; + if (k !== "*") { + trig(k, data); + } + } + } else { + return trig(evtName, data); + } + } + } + } + /** + * This namespace defines every thing related to the system announcement. + * + * The system announcement provides a global way to communicate between + * processes (applications/services) using the subscribe/publish + * mechanism + */ + export namespace announcer { + /** + * The global announcer object that manages global events + * and callbacks + */ + export var observable: API.Announcer = new API.Announcer(); + /** + * This variable is used to allocate the `id` of all messages + * passing between publishers and subscribers in the + * system announcement + */ + export var quota: 0; + /** + * Placeholder of all global events listeners + */ + export var listeners: API.AnnouncerListenerType = {}; + + /** + * Subscribe to a global event + * + * @export + * @param {string} e event name + * @param {(d: any) => void} f event callback + * @param {GUI.BaseModel} a the process (Application/service) related to the callback + */ + export function on(e: string, f: (d: any) => void, a: BaseModel): void { + if (!announcer.listeners[a.pid]) { + announcer.listeners[a.pid] = []; + } + announcer.listeners[a.pid].push({ e, f }); + announcer.observable.on(e, f); + } + + /** + * Trigger a global event + * + * @export + * @param {string} e event name + * @param {*} d data passing to all related callback + */ + export function trigger(e: string, d: any): void { + announcer.observable.trigger(e, d); + } + + /** + * Report system fail. This will trigger the global `fail` + * event + * + * @export + * @param {(string | FormattedString)} m message string + * @param {Error} e error to be reported + */ + export function osfail(m: string | FormattedString, e: Error): void { + announcer.ostrigger("fail", { m, e }); + } + + /** + * Report system error. This will trigger the global `error` + * event + * + * @export + * @param {(string | FormattedString)} m message string + * @param {Error} e error to be reported + */ + export function oserror(m: string | FormattedString, e: Error): void { + announcer.ostrigger("error", { m, e }); + } + + /** + * Trigger system notification (`info` event) + * + * @export + * @param {(string | FormattedString)} m notification message + */ + export function osinfo(m: string | FormattedString): void { + announcer.ostrigger("info", { m, e: null }); + } + + /** + * trigger a specific global event + * + * @export + * @param {string} e event name + * @param {*} d event data + */ + export function ostrigger(e: string, d: any): void { + announcer.trigger(e, { id: 0, data: d, name: "OS" }); + } + + /** + * Unregister a process (application/service) from + * the global announcement system + * + * @export + * @param {GUI.BaseModel} app reference to the process + * @returns {void} + */ + export function unregister(app: BaseModel): void { + if ( + !announcer.listeners[app.pid] || + !(announcer.listeners[app.pid].length > 0) + ) { + return; + } + for (let i of announcer.listeners[app.pid]) { + announcer.observable.off(i.e, i.f); + } + delete announcer.listeners[app.pid]; + } + + /** + * Allocate message id + * + * @export + * @returns {number} + */ + export function getMID(): number { + quota += 1; + return quota; + } + } +} diff --git a/src/core/BaseApplication.coffee b/src/core/BaseApplication.coffee deleted file mode 100644 index 7bdd83c..0000000 --- a/src/core/BaseApplication.coffee +++ /dev/null @@ -1,132 +0,0 @@ -# Copyright 2017-2018 Xuan Sang LE - -# AnTOS Web desktop is is licensed under the GNU General Public -# License v3.0, see the LICENCE file for more information - -# This program is free software: you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation, either version 3 of -# the License, or (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. - -# You should have received a copy of the GNU General Public License -#along with this program. If not, see https://www.gnu.org/licenses/. -class BaseApplication extends this.OS.GUI.BaseModel - constructor: (name, args) -> - super name, args - if (not _OS.setting.applications[@name]) or (Array.isArray OS.setting.applications[@name]) - _OS.setting.applications[@name] = {} - @setting = _OS.setting.applications[@name] - @keycomb = - ALT: {} - CTRL: {} - SHIFT: {} - META: {} - me = @ - init: -> - me = @ - @off "*" - @on "exit", () -> me.quit() - # first register some base event to the app - @on "focus", () -> - me.sysdock.set "selectedApp", me - me.appmenu.pid = me.pid - me.appmenu.set "items", (me.baseMenu() || []) - me.appmenu.set "onmenuselect", (d) -> - me.trigger("menuselect", d) - me.dialog.show() if me.dialog - @on "hide", () -> - me.sysdock.set "selectedApp", null - me.appmenu.set "items", [] - me.dialog.hide() if me.dialog - @on "menuselect", (d) -> - switch d.e.item.data.dataid - when "#{me.name}-about" then me.openDialog "AboutDialog", ()-> - when "#{me.name}-exit" then me.trigger "exit" - @on "apptitlechange", () -> me.sysdock.update() - @loadScheme() - - loadScheme: () -> - #now load the scheme - path = "#{@meta().path}/scheme.html" - @.render path - - bindKey: (k, f) -> - arr = k.split "-" - return unless arr.length is 2 - fnk = arr[0].toUpperCase() - c = arr[1].toUpperCase() - return unless @keycomb[fnk] - @keycomb[fnk][c] = f - - shortcut: (fnk, c, e) -> - return true unless @keycomb[fnk] - return true unless @keycomb[fnk][c] - @keycomb[fnk][c](e) - return false - - applySetting: (k) -> - applyAllSetting: () -> - @applySetting k for k, v of @setting - registry: (k, v) -> - @setting[k] = v - @publish "appregistry", k - - show: () -> - @trigger "focus" - - blur: () -> - @.appmenu.set "items", [] if @.appmenu and @.pid == @.appmenu.pid - @trigger "blur" - - hide: () -> - @trigger "hide" - - toggle: () -> - @trigger "toggle" - - title: () -> - @scheme.get "apptitle" - - onexit: (evt) -> - @cleanup(evt) - if not evt.prevent - @.appmenu.set "items", [] if @.pid == @.appmenu.pid - ($ @scheme).remove() - meta: () -> _OS.APP[@name].meta - baseMenu: -> - mn = - [{ - text: _OS.APP[@name].meta.name, - child: [ - { text: "__(About)", dataid: "#{@name}-about" }, - { text: "__(Exit)", dataid: "#{@name}-exit" } - ] - }] - mn = mn.concat @menu() || [] - mn - - main: -> - #main program - # implement by subclasses - menu: -> - # implement by subclasses - # to add menu to application - [] - open:-> - #implement by subclasses - data:-> - #implement by subclasses - # to return app data - - cleanup: (e) -> - #implement by subclasses - # to handle the exit event - # use e.preventDefault() to - # discard the quit command -BaseApplication.type = 1 -this.OS.GUI.BaseApplication = BaseApplication \ No newline at end of file diff --git a/src/core/BaseApplication.ts b/src/core/BaseApplication.ts new file mode 100644 index 0000000..3eb0943 --- /dev/null +++ b/src/core/BaseApplication.ts @@ -0,0 +1,443 @@ +// Copyright 2017-2018 Xuan Sang LE + +// AnTOS Web desktop is is licensed under the GNU General Public +// License v3.0, see the LICENCE file for more information + +// This program is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. + +// You should have received a copy of the GNU General Public License +//along with this program. If not, see https://www.gnu.org/licenses/. + +namespace OS { + /** + * This namespace is dedicated to application and service definition. + * When an application is loaded, its prototype definition will be + * inserted to this namespace for reuse lately + */ + export namespace application { + /** + * Abstract prototype of all AntOS applications. + * Any new application definition should extend + * this prototype + * + * @export + * @abstract + * @class BaseApplication + * @extends {BaseModel} + */ + export abstract class BaseApplication extends BaseModel { + /** + * Placeholder of all settings specific to the application. + * The settings stored in this object will be saved to system + * setting when logout and can be reused in the next login session + * + * @type {GenericObject} + * @memberof BaseApplication + */ + setting: GenericObject; + + /** + * Hotkeys (shortcuts) defined for this application + * + * @protected + * @type {GUI.ShortcutType} + * @memberof BaseApplication + */ + protected keycomb: GUI.ShortcutType; + + /** + * Reference to the system dock + * + * @type {GUI.tag.AppDockTag} + * @memberof BaseApplication + */ + sysdock: GUI.tag.AppDockTag; + + /** + * Reference to the system application menu located + * on the system panel + * + * @type {GUI.tag.MenuTag} + * @memberof BaseApplication + */ + appmenu: GUI.tag.MenuTag; + + /** + *Creates an instance of BaseApplication. + * @param {string} name application name + * @param {AppArgumentsType[]} args application arguments + * @memberof BaseApplication + */ + constructor(name: string, args: AppArgumentsType[]) { + super(name, args); + if (!setting.applications[this.name]) { + setting.applications[this.name] = {}; + } + this.setting = setting.applications[this.name]; + this.keycomb = { + ALT: {}, + CTRL: {}, + SHIFT: {}, + META: {}, + }; + this.subscribe("appregistry", (m) => { + if (m.name === this.name) { + this.applySetting(m.data.m); + } + }); + } + + /** + * Init the application, this function is called when the + * application process is created and docked in the application + * dock. + * + * The application UI will be rendered after the execution + * of this function. + * + * @returns {void} + * @memberof BaseApplication + */ + init(): void { + this.off("*"); + this.on("exit", () => this.quit(false)); + // first register some base event to the app + this.on("focus", () => { + this.sysdock.selectedApp = this; + this.appmenu.pid = this.pid; + this.appmenu.items = this.baseMenu() || []; + this.appmenu.onmenuselect = ( + d: GUI.tag.MenuEventData + ): void => { + return this.trigger("menuselect", d); + }; + if (this.dialog) { + return this.dialog.show(); + } + }); + this.on("hide", () => { + this.sysdock.selectedApp = null; + this.appmenu.items = []; + this.appmenu.pid = -1; + if (this.dialog) { + return this.dialog.hide(); + } + }); + this.on("menuselect", (d) => { + switch (d.data.item.data.dataid) { + case `${this.name}-about`: + return this.openDialog("AboutDialog"); + case `${this.name}-exit`: + return this.trigger("exit", undefined); + } + }); + this.on("apptitlechange", () => this.sysdock.update(undefined)); + this.updateLocale(this.systemsetting.system.locale); + return this.loadScheme(); + } + + /** + * Render the application UI by first loading its scheme + * and then mount this scheme to the DOM tree + * + * @protected + * @returns {void} + * @memberof BaseApplication + */ + protected loadScheme(): void { + //now load the scheme + const path = `${this.meta().path}/scheme.html`; + return this.render(path); + } + + /** + * API function to perform an heavy task. + * This function will trigger the global `loading` + * event at the beginning of the task, and the `loaded` + * event after finishing the task + * + * @protected + * @param {Promise} promise the promise on a task to be performed + * @returns {Promise} + * @memberof BaseApplication + */ + protected load(promise: Promise): Promise { + const q = this._api.mid(); + return new Promise(async (resolve, reject) => { + this._api.loading(q, this.name); + try { + await promise; + this._api.loaded(q, this.name, "OK"); + return resolve(); + } catch (e) { + this._api.loaded(q, this.name, "FAIL"); + return reject(__e(e)); + } + }); + } + + /** + * Bind a hotkey to the application, this function + * is used to define application keyboard shortcut + * + * @protected + * @param {string} k the hotkey to bind, should be in the following + * format: `[ALT|SHIFT|CTRL|META]-KEY`, e.g. `CTRL-S` + * @param {(e: JQuery.KeyboardEventBase) => void} f the callback function + * @returns {void} + * @memberof BaseApplication + */ + protected bindKey( + k: string, + f: (e: JQuery.KeyboardEventBase) => void + ): void { + const arr = k.split("-"); + if (arr.length !== 2) { + return; + } + const fnk = arr[0].toUpperCase(); + const c = arr[1].toUpperCase(); + if (!this.keycomb[fnk]) { + return; + } + this.keycomb[fnk][c] = f; + } + + /** + * Update the application local from the system + * locale or application specific locale configuration + * + * @private + * @param {string} name locale name e.g. `en_GB` + * @returns {void} + * @memberof BaseApplication + */ + protected updateLocale(name: string): void { + const meta = this.meta(); + if (!meta || !meta.locales) { + return; + } + if (!meta.locales[name]) { + return; + } + + const result = []; + for (let k in meta.locales[name]) { + const v = meta.locales[name][k]; + result.push((this._api.lang[k] = v)); + } + } + + /** + * Execute the callback subscribed to a + * keyboard shortcut + * + * @param {string} fnk meta or modifier key e.g. `CTRL`, `ALT`, `SHIFT` or `META` + * @param {string} c a regular key + * @param {JQuery.KeyboardEventBase} e JQuery keyboard event + * @returns {boolean} return whether the shortcut is executed + * @memberof BaseApplication + */ + shortcut(fnk: string, c: string, e: JQuery.KeyDownEvent): boolean { + if (!this.keycomb[fnk]) { + return true; + } + if (!this.keycomb[fnk][c]) { + return true; + } + this.keycomb[fnk][c](e); + return false; + } + + /** + * Apply a setting to the application + * + * @protected + * @param {string} k the setting name + * @memberof BaseApplication + */ + protected applySetting(k: string): void {} + + /** + * Apply all settings to the application + * + * @protected + * @memberof BaseApplication + */ + protected applyAllSetting(): void { + for (let k in this.setting) { + const v = this.setting[k]; + this.applySetting(k); + } + } + + /** + * Set a setting value to the application setting + * registry + * + * @protected + * @param {string} k setting name + * @param {*} v setting value + * @returns {void} + * @memberof BaseApplication + */ + protected registry(k: string, v: any): void { + this.setting[k] = v; + return this.publish("appregistry", k); + } + + /** + * Show the appliation + * + * @returns {void} + * @memberof BaseApplication + */ + show(): void { + return this.trigger("focus", undefined); + } + + /** + * Blur the application + * + * @returns {void} + * @memberof BaseApplication + */ + blur(): void { + if (this.appmenu && this.pid === this.appmenu.pid) { + this.appmenu.items = []; + } + return this.trigger("blur", undefined); + } + + /** + * Hide the application + * + * @returns {void} + * @memberof BaseApplication + */ + hide(): void { + return this.trigger("hide", undefined); + } + + /** + * Maximize or restore the application window size + * and its position + * + * @returns {void} + * @memberof BaseApplication + */ + toggle(): void { + return this.trigger("toggle", undefined); + } + + /** + * Get the application title + * + * @returns {(string| FormattedString)} + * @memberof BaseApplication + */ + title(): string | FormattedString { + return (this.scheme as GUI.tag.WindowTag).apptitle; + } + + /** + * Function called when the application exit. + * If the input exit event is prevented, the application + * process will not be killed + * + * + * @protected + * @param {BaseEvent} evt exit event + * @memberof BaseApplication + */ + protected onexit(evt: BaseEvent): void { + this.cleanup(evt); + if (!evt.prevent) { + if (this.pid === this.appmenu.pid) { + this.appmenu.items = []; + } + $(this.scheme).remove(); + } + } + + /** + * Get the application meta-data + * + * @returns {API.PackageMetaType} + * @memberof BaseApplication + */ + meta(): API.PackageMetaType { + return application[this.name].meta; + } + + /** + * Base menu definition. This function + * returns the based menu definition of all applications. + * Other application specific menu entries + * should be defined in [[menu]] function + * + * @protected + * @returns {GUI.BasicItemType[]} + * @memberof BaseApplication + */ + protected baseMenu(): GUI.BasicItemType[] { + let mn: GUI.BasicItemType[] = [ + { + text: application[this.name].meta.name, + nodes: [ + { text: "__(About)", dataid: `${this.name}-about` }, + { text: "__(Exit)", dataid: `${this.name}-exit` }, + ], + }, + ]; + mn = mn.concat(this.menu() || []); + return mn; + } + + /** + * The main application entry that is called after + * the application UI is rendered. This application + * must be implemented by all subclasses + * + * @abstract + * @memberof BaseApplication + */ + abstract main(): void; + + /** + * Application specific menu definition + * + * @protected + * @returns {GUI.BasicItemType[]} + * @memberof BaseApplication + */ + protected menu(): GUI.BasicItemType[] { + // implement by subclasses + // to add menu to application + return []; + } + + /** + * The cleanup function that is called by [[onexit]] function. + * Application need to override this function to perform some + * specific task before exiting or to prevent the application + * to be exited + * + * @protected + * @param {BaseEvent} e + * @memberof BaseApplication + */ + protected cleanup(e: BaseEvent): void {} + } + + BaseApplication.type = ModelType.Application; + } +} diff --git a/src/core/BaseDialog.coffee b/src/core/BaseDialog.coffee deleted file mode 100644 index 7370ed0..0000000 --- a/src/core/BaseDialog.coffee +++ /dev/null @@ -1,339 +0,0 @@ -# Copyright 2017-2018 Xuan Sang LE - -# AnTOS Web desktop is is licensed under the GNU General Public -# License v3.0, see the LICENCE file for more information - -# This program is free software: you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation, either version 3 of -# the License, or (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. - -# You should have received a copy of the GNU General Public License -#along with this program. If not, see https://www.gnu.org/licenses/. -class SubWindow extends this.OS.GUI.BaseModel - constructor: (name) -> - super name, null - @parent = undefined - @modal = false - - quit: () -> - evt = new _GUI.BaseEvent("exit") - @onexit(evt) - if not evt.prevent - delete @.observable - ($ @scheme).remove() if @scheme - @dialog.quit() if @dialog - init: () -> - main: () -> - meta: () -> - @parent.meta() - show: () -> - @trigger 'focus' - ($ @scheme).css "z-index", window._zindex + 2 - hide: () -> - @trigger 'hide' - -SubWindow.type = 3 -this.OS.GUI.SubWindow = SubWindow - -class BaseDialog extends SubWindow - constructor: (name) -> - super name - @handler = undefined - - onexit: (e) -> - @parent.dialog = undefined if @parent - -this.OS.GUI.BaseDialog = BaseDialog -### - this dialog rende a tag as main content - and a list of buttons, the behaviour of - the button is specified by user. The conf - object is in the follow form - { - tag: , - buttons:[ - { - label: 'buton label', - onclick: function(d){...} - }, ... - ] - } -### -class BasicDialog extends BaseDialog - constructor: ( name, @conf, @title) -> - super name - - init: () -> - @title = @name if not @title - html = "" - html += "
" - html += "<#{v.tag} #{v.att} data-id = 'content#{k}'>" for k,v of @conf.tags - html += "
" - html += "" for k,v of @conf.buttons - html += "
" - #render the html - _GUI.htmlToScheme html, @, @host - - main: () -> - @scheme.set "apptitle", @title - @scheme.set "minimizable", false - @scheme.set "resizable", @conf.resizable if @conf.resizable isnt undefined - me = @ - f = (_v) -> () -> _v.onclick me - # bind action to button - ( (me.find "bt#{k}").set "onbtclick", f(v) ) for k, v of @conf.buttons - @conf.filldata @ if @conf.filldata - @conf.xtra @ if @conf.xtra - -this.OS.GUI.BasicDialog = BasicDialog - -class PromptDialog extends BasicDialog - constructor: () -> - super "PromptDialog", { - tags: [ - { tag: "afx-label" }, - { tag: "input", att: "type = 'text' data-height='25'" } - ], - width: 200, - height: 120, - resizable: false, - buttons: [ - { - label: "__(Ok)", - onclick: (d) -> - txt = (d.find "content1").value - return d.quit() if txt is "" - d.handler txt if d.handler - d.quit() - }, - { - label: "__(Cancel)", - onclick: (d) -> d.quit() - } - ], - filldata: (d) -> - return unless d.data - (d.find "content0").set "text", d.data.label - (d.find "content1").value = d.data.value if d.data.value - $(d.find "content1").attr("type", d.data.type) if d.data.type - xtra: (d) -> - $( d.find "content1" ).keyup (e) -> - (d.find "bt0").trigger() if e.which is 13 - } - -this.OS.register "PromptDialog", PromptDialog - -class CalendarDialog extends BasicDialog - constructor: () -> - super "CalendarDialog", { - tags: [{ tag: 'afx-calendar-view' }], - width: 300, - height: 230, - resizable: false, - buttons: [ - { - label: "__(Ok)", - onclick: (d) -> - date = (d.find "content0").get "selectedDate" - if date - d.handler date if d.handler - d.quit() - else - d.notify __("Please select a date") - }, - { - label: "__(Cancel)", - onclick: (d) -> d.quit() - } - ] - } -this.OS.register "CalendarDialog", CalendarDialog - -class ColorPickerDialog extends BasicDialog - constructor: () -> - super "ColorPickerDialog", { - tags: [{ tag: 'afx-color-picker' }, {tag:'div', att: 'data-height="5"' }], - width: 313, - height: 250, - resizable: false, - buttons: [ - { - label: "__(Ok)", - onclick: (d) -> - c = (d.find "content0").get "selectedColor" - if c - d.handler c if d.handler - d.quit() - else - d.notify "Please select a color" - }, - { - label: "__(Cancel)", - onclick: (d) -> d.quit() - } - ] - } -this.OS.register "ColorPickerDialog", ColorPickerDialog - -class InfoDialog extends BasicDialog - constructor: () -> - super "InfoDialog", { - tags: [{ tag: 'afx-grid-view' }], - width: 250, - height: 300, - resizable: true, - buttons: [ { label: "__(Cancel)", onclick: (d) -> d.quit() } ], - filldata: (d) -> - return unless d.data - rows = [] - rows.push [ { value: k }, { value: v } ] for k, v of d.data - (d.find "content0").set "rows", rows - } -this.OS.register "InfoDialog", InfoDialog - - -class YesNoDialog extends BasicDialog - constructor: () -> - super "YesNoDialog", { - tags: [{ tag: "afx-label" }], - width: 300, - height: 100, - resizable: true, - buttons: [ - { - label: "__(Yes)", onclick: (d) -> - d.handler true if d.handler - d.quit() - }, - { - label: "__(No)", onclick: (d) -> - d.handler false if d.handler - d.quit() - } - ], - filldata: (d) -> - return unless d.data - l = d.find "content0" - for k, v of d.data - l.set k, v - } -this.OS.register "YesNoDialog", YesNoDialog - -class SelectionDialog extends BasicDialog - constructor: () -> - super "SelectionDialog", { - tags: [{ tag: "afx-list-view" }], - width: 250, - height: 300, - resizable: false, - buttons: [ - { - label: "__(Ok)", onclick: (d) -> - el = d.find "content0" - it = el.get "selected" - return unless it - d.handler it if d.handler - d.quit() - }, - { label: "__(Cancel)", onclick: (d) -> d.quit() } - ], - filldata: (d) -> - return unless d.data - (d.find "content0").set "items", d.data - xtra: (d) -> - ( d.find "content0" ).set "onlistdbclick", (e) -> - (d.find "bt0").trigger() - - } -this.OS.register "SelectionDialog", SelectionDialog - -class AboutDialog extends BaseDialog - constructor: () -> - super "AboutDialog" - - init: () -> - @render "os://resources/schemes/about.html" - - main: () -> - mt = @meta() - @scheme.set "apptitle", __("About: {0}",mt.name) - (@find "mylabel").set "*", {icon:mt.icon, iconclass:mt.iconclass, text:"#{mt.name}(v#{mt.version})"} - ($ @find "mydesc").html mt.description - # grid data for author info - return unless mt.info - rows = [] - rows.push [ { value: k }, { value: v } ] for k, v of mt.info - (@find "mygrid").set "rows", rows - -this.OS.register "AboutDialog", AboutDialog - -class FileDiaLog extends BaseDialog - constructor: () -> - super "FileDiaLog" - - init: () -> - @render "os://resources/schemes/filedialog.html" - - main: () -> - fileview = @find "fileview" - location = @find "location" - filename = @find "filename" - me = @ - @scheme.set "apptitle", @title - fileview.set "fetch", (e, f) -> - return unless e.child - e.child.path.asFileHandler().read (d) -> - return me.error __("Resource not found: {0}", e.child.path) if d.error - f d.result - setroot = (path) -> - path.asFileHandler().read (d) -> - if(d.error) - return me.error __("Resource not found: {0}", path) - fileview.set "path", path - fileview.set "data", d.result - if not @data or not @data.root - location.set "onlistselect", (e) -> - return unless e and e.data.path - setroot e.data.path - location.set "items", ( i for i in @systemsetting.VFS.mountpoints when i.type isnt "app" ) - location.set "selected", 0 unless location.get "selected" - else - $(location).hide() - @trigger "calibrate" - setroot @data.root - fileview.set "onfileselect", (f) -> - ($ filename).val f.filename if f.type is "file" - (@find "bt-ok").set "onbtclick", (e) -> - f = fileview.get "selectedFile" - return me.notify __("Please select a file/fofler") unless f - return me.notify __("Please select {0} only", me.data.type) if me.data and me.data.type and me.data.type isnt f.type - if me.data and me.data.mimes - #verify the mime - m = false - if f.mime - for v in me.data.mimes - if f.mime.match (new RegExp v, "g") - m = true - break - return me.notify __("Only {0} could be selected", me.data.mimes.join(",")) unless m - d = f.path - d = f.path.asFileHandler().parent() if f.type is "file" - me.handler d, ($ filename).val(), f.path, f if me.handler - #sel = if me.data and me.data.selection then me.data.selection else "file" - #me.handler f, ($ filename).val() if me.handler and ((f.type is sel) or (sel is "*")) - me.quit() - - (@find "bt-cancel").set "onbtclick", (e) -> - me.quit() - if @data and @data.file - ($ filename).css("display", "block").val @data.file.basename or "Untitled" - @trigger "resize" - fileview.set "showhidden", @data.hidden if @data and @data.hidden - -this.OS.register "FileDiaLog", FileDiaLog \ No newline at end of file diff --git a/src/core/BaseDialog.ts b/src/core/BaseDialog.ts new file mode 100644 index 0000000..cffe28b --- /dev/null +++ b/src/core/BaseDialog.ts @@ -0,0 +1,1126 @@ +// Copyright 2017-2018 Xuan Sang LE + +// AnTOS Web desktop is is licensed under the GNU General Public +// License v3.0, see the LICENCE file for more information + +// This program is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. + +// You should have received a copy of the GNU General Public License +//along with this program. If not, see https://www.gnu.org/licenses/. +namespace OS { + export namespace GUI { + /** + * the SubWindow class is the abstract prototype of all + * modal windows or dialogs definition in AntOS + * + * @export + * @abstract + * @class SubWindow + * @extends {BaseModel} + */ + export abstract class SubWindow extends BaseModel { + /** + * Placeholder indicates whether the sub window is in + * modal mode. This value is reserver for future use + * + * @type {boolean} + * @memberof SubWindow + */ + modal: boolean; + + /** + * Reference to the parent of the current sub-window + * + * @type {(BaseModel | typeof GUI)} + * @memberof SubWindow + */ + parent: BaseModel | typeof GUI; + + /** + *Creates an instance of SubWindow. + * @param {string} name SubWindow (class) name + * @memberof SubWindow + */ + constructor(name: string) { + super(name, null); + this.parent = undefined; + this.modal = false; + } + + /** + * Exit the sub-window + * + * @returns {void} + * @memberof SubWindow + */ + quit(): void { + const evt = new BaseEvent("exit", false); + this.onexit(evt); + if (!evt.prevent) { + delete this._observable; + if (this.scheme) { + $(this.scheme).remove(); + } + if (this.dialog) { + return this.dialog.quit(); + } + } + } + + /** + * Init the sub-window, this function is called + * on creation of the sub-window object. It is used + * to render the sub-window UI. + * + * Need to be implemented by subclasses + * + * @abstract + * @memberof SubWindow + */ + abstract init(): void; + + /** + * Main entry point after rendering of the sub-window + * + * @abstract + * @memberof SubWindow + */ + abstract main(): void; + + /** + * Return the parent meta-data of the current + * sub-window + * + * @returns {API.PackageMetaType} + * @memberof SubWindow + */ + meta(): API.PackageMetaType { + const p = this.parent as BaseModel; + if (p && p.meta) { + return p.meta(); + } + } + + /** + * Show the sub-window + * + * @memberof SubWindow + */ + show(): void { + this.trigger("focus"); + $(this.scheme).css("z-index", GUI.zindex + 2); + } + + /** + * Hide the sub-window + * + * @returns {void} + * @memberof SubWindow + */ + hide(): void { + return this.trigger("hide"); + } + } + + SubWindow.type = ModelType.SubWindow; + + /** + * Abstract prototype of all AntOS dialogs widget + * + * @export + * @abstract + * @class BaseDialog + * @extends {SubWindow} + */ + export abstract class BaseDialog extends SubWindow { + /** + * Placeholder for the dialog callback on exit + * + * @memberof BaseDialog + */ + handle: (d: any) => void; + + /** + * Placeholder of the dialog input data + * + * @type {GenericObject} + * @memberof BaseDialog + */ + data: GenericObject; + + /** + *Creates an instance of BaseDialog. + * @param {string} name Dialog (class) name + * @memberof BaseDialog + */ + constructor(name: string) { + super(name); + this.handle = undefined; + } + + /** + * Function called when dialog exits + * + * @protected + * @param {BaseEvent} e + * @returns {void} + * @memberof BaseDialog + */ + protected onexit(e: BaseEvent): void { + if (this.parent) { + return (this.parent.dialog = undefined); + } + } + } + + /** + * A basic dialog renders a dialog widget using the UI + * scheme provided in it constructor or defined in its + * class variable `scheme` + * + * @export + * @class BasicDialog + * @extends {BaseDialog} + */ + export class BasicDialog extends BaseDialog { + /** + * Placeholder for the UI scheme to be rendered. This can + * be either the string definition of the scheme or + * the VFS file handle of the scheme file + * + * @private + * @type {(string | OS.API.VFS.BaseFileHandle)} + * @memberof BasicDialog + */ + private markup: string | OS.API.VFS.BaseFileHandle; + + /** + * If the `markup` variable is not provided, then + * the [[init]] function will find the scheme definition + * in this class variable + * + * @static + * @type {string} + * @memberof BasicDialog + */ + static scheme: string; + + /** + *Creates an instance of BasicDialog. + * @param {string} name dialog name + * @param {(string | OS.API.VFS.BaseFileHandle)} [markup] UI scheme definition + * @memberof BasicDialog + */ + constructor( + name: string, + markup?: string | OS.API.VFS.BaseFileHandle + ) { + super(name); + this.markup = markup; + } + + /** + * Render the dialog using the UI scheme provided by either + * the `markup` instance variable or the `scheme` class variable + * + * @returns {void} + * @memberof BasicDialog + */ + init(): void { + if (this.markup) { + if (typeof this.markup === "string") { + return GUI.htmlToScheme(this.markup, this, this.host); + } else { + // a file handle + return this.render(this.markup.path); + } + } else if ( + GUI.dialogs[this.name] && + GUI.dialogs[this.name].scheme + ) { + const html: string = GUI.dialogs[this.name].scheme; + return GUI.htmlToScheme(html.trim(), this, this.host); + } else { + this.error(__("Unable to find dialog scheme")); + } + } + + /** + * Main entry point for the dialog + * + * @memberof BasicDialog + */ + main(): void { + const win = this.scheme as tag.WindowTag; + if (this.data && this.data.title) { + win.apptitle = this.data.title; + } + win.resizable = false; + win.minimizable = false; + } + } + + /** + * The namespace `dialogs` is dedicated to all Dialog definition + * in AntOS + */ + export namespace dialogs { + /** + * Simple prompt dialog to get user input text. + * The input date of the dialog: + * + * ```typescript + * { + * title: string, // window title + * label: string, // label text + * value: string // user input text + * } + * ``` + * + * The data passing from the dialog to the callback function is + * in the string text of the user input value + * + * @export + * @class PromptDialog + * @extends {BasicDialog} + */ + export class PromptDialog extends BasicDialog { + /** + *Creates an instance of PromptDialog. + * @memberof PromptDialog + */ + constructor() { + super("PromptDialog"); + } + + /** + * Main entry point + * + * @memberof PromptDialog + */ + main(): void { + super.main(); + const $input = $(this.find("txtInput")); + if (this.data && this.data.label) { + (this.find( + "lbl" + ) as tag.LabelTag).text = this.data.label; + } + if (this.data && this.data.value) { + $input.val(this.data.value); + } + + (this.find("btnOk") as tag.ButtonTag).onbtclick = (e) => { + if (this.handle) { + this.handle($input.val()); + } + return this.quit(); + }; + + (this.find("btnCancel") as tag.ButtonTag).onbtclick = ( + e + ) => { + return this.quit(); + }; + + $input.keyup((e) => { + if (e.which !== 13) { + return; + } + if (this.handle) { + this.handle($input.val()); + } + return this.quit(); + }); + + $input.focus(); + } + } + /** + * Scheme definition of the Prompt dialog + */ + PromptDialog.scheme = `\ + + + +
+ +
+ + +
+ +
+ + + + +
+ + +\ + `; + + /** + * A text dialog is similar to a [[PromptDialog]] nut allows + * user to input multi-line text. + * + * Refer to [[PromptDialog]] for the definition of input and callback data + * of the dialog + * + * @export + * @class TextDialog + * @extends {BasicDialog} + */ + export class TextDialog extends BasicDialog { + /** + *Creates an instance of TextDialog. + * @memberof TextDialog + */ + constructor() { + super("TextDialog"); + } + + /** + * Main entry point + * + * @memberof TextDialog + */ + main(): void { + super.main(); + const $input = $(this.find("txtInput")); + if (this.data && this.data.value) { + $input.val(this.data.value); + } + + (this.find("btnOk") as tag.ButtonTag).onbtclick = (e) => { + const value = $input.val(); + if (!value || value === "") { + return; + } + if (this.handle) { + this.handle(value); + } + return this.quit(); + }; + + (this.find("btnCancel") as tag.ButtonTag).onbtclick = ( + e + ): void => { + return this.quit(); + }; + + $input.focus(); + } + } + /** + * Scheme definition + */ + TextDialog.scheme = `\ + + + +
+ +
+ \ +`; + + schemes.login = `\ +
+

Welcome to AntOS, please login

+ + + +
+
\ +`; + } +} diff --git a/src/core/handlers/RemoteHandler.coffee b/src/core/handlers/RemoteHandler.coffee deleted file mode 100644 index a8d0dee..0000000 --- a/src/core/handlers/RemoteHandler.coffee +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright 2017-2018 Xuan Sang LE - -# AnTOS Web desktop is is licensed under the GNU General Public -# License v3.0, see the LICENCE file for more information - -# This program is free software: you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation, either version 3 of -# the License, or (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. - -# You should have received a copy of the GNU General Public License -#along with this program. If not, see https://www.gnu.org/licenses/. -self.OS.API.HOST = self.location.hostname+ (if self.location.port then":#{self.location.port}" else "") -self.OS.API.REST = "#{self.location.protocol}//#{self.OS.API.HOST}" - -_REST = self.OS.API.REST -_HOST = self.OS.API.HOST -self.OS.API.handler = - # get file, require authentification - get: "#{_REST}/VFS/get" - # get shared file with publish - shared: "#{_REST}/VFS/shared" - scandir: (p, c ) -> - path = "#{_REST}/VFS/scandir" - _API.post path, { path: p }, c, (e, s) -> - _courrier.osfail __("Fail to scan directory: {0}", p), e, s - mkdir: (p, c ) -> - path = "#{_REST}/VFS/mkdir" - _API.post path, { path: p }, c, (e, s) -> - _courrier.osfail __("Fail to create directory: {0}", p), e, s - sharefile: (p, pub , c) -> - path = "#{_REST}/VFS/publish" - _API.post path, { path: p , publish: pub }, c, (e, s) -> - _courrier.osfail __("Fail to publish file: {0}", p), e, s - - fileinfo: (p, c) -> - path = "#{_REST}/VFS/fileinfo" - _API.post path, { path: p }, c, (e, s) -> - _courrier.osfail __("Fail to get file meta data: {0}", p), e, s - - readfile: (p, c, t) -> - path = "#{_REST}/VFS/get/" - _API.get path + p, c, (e, s) -> - _courrier.osfail __("Fail to read file: {0}", p), e, s - , t - - move: (s, d, c) -> - path = "#{_REST}/VFS/move" - _API.post path, { src: s, dest: d }, c, (e, s) -> - _courrier.osfail __("Fail to move file: {0} -> {1}", s, d), e, s - - delete: (p , c) -> - path = "#{_REST}/VFS/delete" - _API.post path, { path: p }, c, (e, s) -> - _courrier.osfail __("Fail to delete: {0}", p), e, s - - fileblob: (p, c) -> - path = "#{_REST}/VFS/get/" - _API.blob path + p, c, (e, s) -> - _courrier.osfail "Fail to read file: #{p}", e, s - - packages: (d, c) -> - path = "#{_REST}/system/packages" - _API.post path, d, c, (e, s) -> - _courrier.osfail __("Fail to {0} package", d.command), e, s - - upload: (d, c) -> - path = "#{_REST}/VFS/upload" - _API.upload path, d, c, (e, s) -> - _courrier.osfail __("Fail to upload file to: {0}", d), e, s - - write: (p, d , c) -> - path = "#{_REST}/VFS/write" - _API.post path, { path: p, data: d }, c, (e, s) -> - _courrier.osfail __("Fail to write to file: {0}", p), e, s - - scanapp: (p, c ) -> - path = "#{_REST}/system/application" - - apigateway: (d, ws, c) -> - if ws - path = "#{_HOST}/system/apigateway?ws=1" - proto = if window.location.protocol is "https:" then "wss://" else "ws://" - socket = new WebSocket proto + path - if c then c(socket) - return socket - else - path = "#{_REST}/system/apigateway?ws=0" - _API.post path, d, c, (e, s) -> - _courrier.osfail __("Fail to invoke gateway api"), e, s - - auth: (c) -> - p = "#{_REST}/user/auth" - _API.post p, {}, c, (e, s) -> - console.log e, s - alert __("Resource not found: {0}", p) - login: (d, c) -> - p = "#{_REST}/user/login" - _API.post p, d, c, () -> - alert __("Resource not found: {0}", p) - logout: () -> - p = "#{_REST}/user/logout" - _API.post p, {}, (d) -> - _OS.boot() - , () -> - alert __("Resource not found: {0}", p) - setting: (f) -> - p = "#{_REST}/system/settings" - _API.post p, _OS.setting, (d) -> - _courrier.oserror __("Cannot save system setting"), d.error if d.error - f(d) if f - , (e, s) -> - m = __("Fail to make request: {0}", p) - _courrier.osfail m , e, s - f({ error: m }) if f - - dbquery: (cmd, d, c) -> - path = "#{_REST}/VDB/#{cmd}" - _API.post path, d, c, (e, s) -> - _courrier.osfail __("Fail to query data from database: {0}", path), e, s \ No newline at end of file diff --git a/src/core/handlers/TestHandler.coffee b/src/core/handlers/TestHandler.coffee deleted file mode 100644 index 63135af..0000000 --- a/src/core/handlers/TestHandler.coffee +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2017-2018 Xuan Sang LE - -# AnTOS Web desktop is is licensed under the GNU General Public -# License v3.0, see the LICENCE file for more information - -# This program is free software: you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation, either version 3 of -# the License, or (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. - -# You should have received a copy of the GNU General Public License -#along with this program. If not, see https://www.gnu.org/licenses/. -self.OS.API.handler = - scandir: (p, c ) -> - path = 'resources/jsons/scandir.json' - _API.get path , c, (e, s) -> - _courrier.osfail "System fall: Cannot read #{path}", e, s - \ No newline at end of file diff --git a/src/core/handles/RemoteHandle.ts b/src/core/handles/RemoteHandle.ts new file mode 100644 index 0000000..0702df8 --- /dev/null +++ b/src/core/handles/RemoteHandle.ts @@ -0,0 +1,465 @@ +// Copyright 2017-2020 Xuan Sang LE + +// AnTOS Web desktop is is licensed under the GNU General Public +// License v3.0, see the LICENCE file for more information + +// This program is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. + +// You should have received a copy of the GNU General Public License +//along with this program. If not, see https://www.gnu.org/licenses/. + +namespace OS { + export namespace API { + /** + * Interface for user login data + * + * @export + * @interface UserLoginType + */ + export interface UserLoginType { + /** + * The user credential + * + * @type {string} + * @memberof UserLoginType + */ + username: string; + + /** + * The user password + * + * @type {string} + * @memberof UserLoginType + */ + password: string; + } + + /** + * Interface for a command sent to + * server side package manage, it contains two field: + * + * @export + * @interface PackageCommandType + */ + export interface PackageCommandType { + /** + * Command name, should be: `init`, `cache`, `install`, + * `uninstall` or `list` + * + * @type {string} + * @memberof PackageCommandType + */ + command: string; + + /** + * Parameter object of each command + * + * @type {GenericObject} + * @memberof PackageCommandType + */ + args: GenericObject; + } + + /** + * + * Interface for basic request result returned + * from the server-side. A valid server-side response should + * be in the following format + * ```json + * { + * "error": boolean or string_err, + * "result": JSON result object + * } + * ``` + * + * @export + * @interface RequestResult + */ + export interface RequestResult { + /** + * Indicate whether the response is error + * + * @type {(boolean | string)} + * @memberof RequestResult + */ + error: boolean | string; + + /** + * The response result, this value must be + * set when `error` is false + * + * @type {(string + * | boolean + * | GenericObject + * | any[] + * | FileInfoType + * | FileInfoType[] + * | setting.UserSettingType)} + * @memberof RequestResult + */ + result: + | string + | boolean + | GenericObject + | any[] + | FileInfoType + | FileInfoType[] + | setting.UserSettingType; + } + + let loc: any = { hostname: "localhost", port: "80", protocol: "http" }; + + if (Ant.location) loc = Ant.location; + /** + * The host name of the server-side + */ + export var HOST: string = + loc.hostname + (loc.port ? `:${loc.port}` : ""); + /** + * The base REST URI of the server-side API + */ + export var REST: string = `${loc.protocol}//${HOST}`; + + /** + * The namespace `handle` contains some low level API to + * communicate with the server side API. It is the only + * API layer that communicate directly with the server. + * To make AntOS compatible with any server side API, + * all exported variable unctions defined in the `handle` + * namespace should be re-implemented + */ + export namespace handle { + /** + * Base URI for reading content of VFS file + */ + export var get: string = `${REST}/VFS/get`; + /** + * Base URI for VFS file sharing + */ + export var shared: string = `${REST}/VFS/shared`; + + /** + * Send a request to the server-side API for a directory scanning + * operation + * + * @export + * @param {string} p a VFS file path e.g. home://test/ + * @returns {Promise} A promise on a [[RequestResult]] + * which contains an error or a list of FileInfoType + */ + export function scandir(p: string): Promise { + const path = `${REST}/VFS/scandir`; + return API.post(path, { path: p }); + } + + /** + * + * Send a request to the server-side API for directory creation + * + * @export + * @param {string} p VFS path of the directory to be created + * @returns {Promise} A promise on a RequestResult + * which contains an error or true on success + */ + export function mkdir(p: string): Promise { + const path = `${API.REST}/VFS/mkdir`; + return API.post(path, { path: p }); + } + + /** + * Send a request to the server-side API for sharing/unsharing a VFS file, + * once shared a VFS file will be publicly visible by everyone + * + * @export + * @param {string} p VFS file path to be shared + * @param {boolean} pub flag: share (true) or unshare (false) + * @returns {Promise} A promise on a RequestResult + * which contains an error or true on success + */ + export function sharefile( + p: string, + pub: boolean + ): Promise { + const path = `${API.REST}/VFS/publish`; + return API.post(path, { path: p, publish: pub }); + } + + /** + * Get VFS file meta-data + * + * @export + * @param {string} p VFS file path + * @returns {Promise} A promise on a [[RequestResult]] + * which contains an error or an object of FileInfoType + */ + export function fileinfo(p: string): Promise { + const path = `${API.REST}/VFS/fileinfo`; + return API.post(path, { path: p }); + } + + /** + * Read a VFS file content. There are many ways a VFS file can be read: + * - Read as a raw text content + * - Read as a javascript file, in this case the content of the + * file will be executed + * - Read as JSON object + * + * @export + * @param {string} p path of the VFS file + * @param {string} t return data type: + * - jsonp: the response is an json object + * - script: the response is a javascript code + * - xml, html: the response is a XML/HTML object + * - text: plain text + * + * @returns {Promise} A promise on a [[RequestResult]] + * which contains an error or an object of [[FileInfoType]] + */ + export function readfile(p: string, t: string): Promise { + const path = `${API.REST}/VFS/get/`; + return API.get(path + p, t); + } + + /** + * Move a file to another location on server-side + * + * @export + * @param {string} s VFS source file path + * @param {string} d VFS destination file path + * @returns {Promise} A promise on a [[RequestResult]] + * which contains an error or a success response + */ + export function move(s: string, d: string): Promise { + const path = `${API.REST}/VFS/move`; + return API.post(path, { src: s, dest: d }); + } + + /** + * Delete a VFS file on the server-side + * + * @export + * @param {string} p VFS file path + * @returns {Promise} A promise on a [[RequestResult]] + * which contains an error or a success response + */ + export function remove(p: string): Promise { + const path = `${API.REST}/VFS/delete`; + return API.post(path, { path: p }); + } + + /** + * Read the file as binary data + * + * @export + * @param {string} p VFS file to be read + * @returns {Promise} a Promise on an array buffer + */ + export function fileblob(p: string): Promise { + const path = `${API.REST}/VFS/get/`; + return API.blob(path + p); + } + + /** + * Send a command to the serverside package manager + * + * @export + * @param {PackageCommandType} d a package command of type PackageCommandType + * @returns {Promise} a promise on a [[RequestResult]] + */ + export function packages( + d: PackageCommandType + ): Promise { + const path = `${API.REST}/system/packages`; + return API.post(path, d); + } + + /** + * Upload file to the server via VFS interface + * + * @export + * @param {string} d VFS destination directory path + * @returns {Promise} a promise on a [[RequestResult]] + */ + export function upload(d: string): Promise { + const path = `${API.REST}/VFS/upload`; + return API.upload(path, d); + } + + /** + * Write Base 64 encoded data to a VFS file + * + * @export + * @param {string} p path to the VFS file + * @param {string} d file data encoded in Base 64 + * @returns {Promise} a promise on a [[RequestResult]] + */ + export function write( + p: string, + d: string + ): Promise { + const path = `${API.REST}/VFS/write`; + return API.post(path, { path: p, data: d }); + } + + /** + * An apigateway allows client side to execute a custom server-side + * script and get back the result. This gateway is particularly + * useful in case of performing a task that is not provided by the core + * API + * + * @export + * @param {GenericObject} d execution indication, provided only when ws is `false` + * otherwise, `d` should be written directly to the websocket stream as JSON object. + * Two possible formats of `d`: + * ```text + * execute an server-side script file: + * + * { + * path: [VFS path], + * parameters: [parameters of the server-side script] + * } + * + * or, execute directly a snippet of server-side script: + * + * { code: [server-side script code snippet as string] } + * + * ``` + * + * @param {boolean} ws flag indicate whether to use websocket for the connection + * to the gateway API. In case of streaming data, the websocket is preferred + * @returns {Promise} a promise on the result object (any) + */ + export function apigateway( + d: GenericObject, + ws: boolean + ): Promise { + if (ws) { + return new Promise(function (resolve, reject) { + try { + const path = `${API.HOST}/system/apigateway?ws=1`; + const proto = + window.location.protocol === "https:" + ? "wss://" + : "ws://"; + const socket = new WebSocket(proto + path); + return resolve(socket); + } catch (e) { + return reject(__e(e)); + } + }); + } else { + const path = `${API.REST}/system/apigateway?ws=0`; + return API.post(path, d); + } + } + + /** + * Check if a user is logged in + * + * @export + * @returns {Promise} a promise on a [[RequestResult]] that + * contains an error or a [[UserSettingType]] object + */ + export function auth(): Promise { + const p = `${API.REST}/user/auth`; + return API.post(p, {}); + } + + /** + * Perform a login operation + * + * @export + * @param {UserLoginType} d user data [[UserLoginType]] + * @returns {Promise} a promise on a [[RequestResult]] that + * contains an error or a [[UserSettingType]] object + */ + export function login(d: UserLoginType): Promise { + const p = `${API.REST}/user/login`; + return API.post(p, d); + } + + /** + * Perform a logout operation + * + * @export + * @returns {Promise} a promise on a [[RequestResult]] + */ + export function logout(): Promise { + const p = `${API.REST}/user/logout`; + return API.post(p, {}); + } + + /** + * Save the current user settings + * + * @export + * @returns {Promise} a promise on a [[RequestResult]] + */ + export function setting(): Promise { + const p = `${API.REST}/system/settings`; + return API.post(p, OS.setting); + } + + /** + * This is the low level function of AntOS VDB API. + * It requests the server API to perform some simple + * SQL query. + * + * @export + * @param {string} cmd action to perform: save, delete, get, select + * @param {GenericObject} d data object of the request based on each action: + * - save: + * ``` + * { table: "table name", data: [record data object]} + * ``` + * - get: + * ``` + * { table: "table name", id: [record id]} + * ``` + * - delete: + * ``` + * { table: "table name", id: [record id]} + * or + * { table: "table name", cond: [conditional object]} + * ``` + * - select: + * ``` + * { table: "table name", cond: [conditional object]} + * ``` + * @returns {Promise} a promise of [[RequestResult]] on the + * query data + * + * A conditional object represents a SQL condition statement as an object, + * example: `pid = 10 AND cid = 2 ORDER BY date DESC` + * ``` + * { + * exp: { + * "and": { + * pid: 10, + * cid: 2 + * } + * }, + * order: { + * date: "DESC" + * } + * } + * ``` + */ + export function dbquery( + cmd: string, + d: GenericObject + ): Promise { + const path = `${API.REST}/VDB/${cmd}`; + return API.post(path, d); + } + } + } +} diff --git a/src/core/handles/TestHandle.js b/src/core/handles/TestHandle.js new file mode 100644 index 0000000..523c15e --- /dev/null +++ b/src/core/handles/TestHandle.js @@ -0,0 +1,29 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +// Copyright 2017-2018 Xuan Sang LE + +// AnTOS Web desktop is is licensed under the GNU General Public +// License v3.0, see the LICENCE file for more information + +// This program is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. + +// You should have received a copy of the GNU General Public License +//along with this program. If not, see https://www.gnu.org/licenses/. +Ant.OS.API.handle = { + scandir(p, c ) { + const path = 'resources/jsons/scandir.json'; + return Ant.OS.API.get(path , c, (e, s) => Ant.OS.announcer.osfail(`System fall: Cannot read ${path}`, e, s)); + } +}; + \ No newline at end of file diff --git a/src/core/handlers/jsons/scandir.json b/src/core/handles/jsons/scandir.json similarity index 100% rename from src/core/handlers/jsons/scandir.json rename to src/core/handles/jsons/scandir.json diff --git a/src/core/languages/gen.sh b/src/core/languages/gen.sh old mode 100644 new mode 100755 index 4332516..e0a391a --- a/src/core/languages/gen.sh +++ b/src/core/languages/gen.sh @@ -20,7 +20,7 @@ ord() { LC_CTYPE=C printf '%d' "'$1" } -grep --include=\*.{coffee,tag} -roh "$1" -e '__("[^"]*"' | while read -r line ; do +grep --include=\*.coffee -roh "$1" -e '__("[^"]*"' | while read -r line ; do SUBSTRING=$(echo $line| cut -d'"' -f 2) if test -f "$2" && [ ! -z "$(grep -F "\"$SUBSTRING\":" "$2")" ] then @@ -29,7 +29,7 @@ grep --include=\*.{coffee,tag} -roh "$1" -e '__("[^"]*"' | while read -r line ; echo -e "\t\"$SUBSTRING\":\"$SUBSTRING\"," >> "tmp.json" fi done -grep --include=\*.{coffee,html,tag} -roh "$1" -e '\"__([^\"]*)\"' | while read -r line; do +grep --include=\*.{coffee,html} -roh "$1" -e '\"__([^\"]*)\"' | while read -r line; do len=$(( ${#line} - 6 )) #echo $len #echo $line diff --git a/src/core/languages/vi_VN.json b/src/core/languages/vi_VN.json index 466854a..1524918 100644 --- a/src/core/languages/vi_VN.json +++ b/src/core/languages/vi_VN.json @@ -318,4 +318,79 @@ "Welcome to AntOSDK":"Welcome to AntOSDK", "Your application version is older ({0} < {1})":"Your application version is older ({0} < {1})", "zip file generated in release folder":"zip file generated in release folder" + +, + "Cancels":"Cancels", + "Command Palette":"Command Palette", + "Mount Points":"Mount Points", + "New CodePad extension at":"New CodePad extension at", + "Report":"Report", + "Select extension archive":"Select extension archive", + "System error log":"System error log" + +, + "[^":"[^", + "{0} is not a file":"{0} is not a file", + "Action {0} is unsupported on: {1}":"Action {0} is unsupported on: {1}", + "Application meta data isnt found":"Application meta data isnt found", + "Cannot load extension meta data":"Cannot load extension meta data", + "Cannot load scheme: {0}":"Cannot load scheme: {0}", + "Cannot read folder: {0}":"Cannot read folder: {0}", + "Change language mode":"Change language mode", + "Change theme":"Change theme", + "Command palete":"Command palete", + "Command palette":"Command palette", + "ct:Logout":"ct:Logout", + "ct:Toggle fullscreen":"ct:Toggle fullscreen", + "ct:User: {0}":"ct:User: {0}", + "Current folder is not found":"Current folder is not found", + "Error reported":"Error reported", + "Error saving file {0}: {1}":"Error saving file {0}: {1}", + "Example action":"Example action", + "Extension installed":"Extension installed", + "ExtensionName":"ExtensionName", + "Fail to create: {0}":"Fail to create: {0}", + "Fail to download: {0}":"Fail to download: {0}", + "Fail to publish: {0}":"Fail to publish: {0}", + "Fail to read: {0}":"Fail to read: {0}", + "Fail to rename: {0}":"Fail to rename: {0}", + "Fail to upload: {0}":"Fail to upload: {0}", + "Install extension":"Install extension", + "Invalid library: {0}":"Invalid library: {0}", + "New Extension":"New Extension", + "New project from current folder":"New project from current folder", + "New Project":"New Project", + "No meta-data found":"No meta-data found", + "Open folder":"Open folder", + "Open Folder":"Open Folder", + "Package is generated in release folder":"Package is generated in release folder", + "Please select a day":"Please select a day", + "Please select an item":"Please select an item", + "Please select color":"Please select color", + "Start":"Start", + "The folder is not empty: {0}":"The folder is not empty: {0}", + "Unable to build extension":"Unable to build extension", + "Unable to build project":"Unable to build project", + "Unable to create archive":"Unable to create archive", + "Unable to create extension directories":"Unable to create extension directories", + "Unable to create extension template":"Unable to create extension template", + "Unable to create package archive":"Unable to create package archive", + "Unable to create project directory":"Unable to create project directory", + "Unable to create template files":"Unable to create template files", + "Unable to find action: {0}":"Unable to find action: {0}", + "Unable to find extension: {0}":"Unable to find extension: {0}", + "Unable to install extension":"Unable to install extension", + "unable to load extension: {0}":"unable to load extension: {0}", + "Unable to load libraries":"Unable to load libraries", + "Unable to open: {0}":"Unable to open: {0}", + "Unable to preload extension":"Unable to preload extension", + "Unable to read: {0}":"Unable to read: {0}", + "Unable to read meta-data":"Unable to read meta-data", + "Unable to report error: {0}":"Unable to report error: {0}", + "Unable to run extension":"Unable to run extension", + "Unable to run project":"Unable to run project", + "Unable to save file: {0}":"Unable to save file: {0}", + "Value":"Value", + "Verifying: {0}":"Verifying: {0}", + "VFS unknown handle: {0}":"VFS unknown handle: {0}" } diff --git a/src/core/languages/vi_VN.json.old b/src/core/languages/vi_VN.json.old index c946656..73c6097 100644 --- a/src/core/languages/vi_VN.json.old +++ b/src/core/languages/vi_VN.json.old @@ -267,4 +267,64 @@ , "Graph editor":"Graph editor", "Render":"Render" + +, + "add {0} to zip":"add {0} to zip", + "Add files to build target":"Add files to build target", + "and unsaved project":"and unsaved project", + "Build and Run":"Build and Run", + "Build":"Build", + "Build done":"Build done", + "Build Options":"Build Options", + "Build release":"Build release", + "Cannot create file: {0}":"Cannot create file: {0}", + "Cannot save project: {0}":"Cannot save project: {0}", + "Cannot save the zip file {0} : {1}":"Cannot save the zip file {0} : {1}", + "Coffees":"Coffees", + "Compiled successful":"Compiled successful", + "Copied {0} -> {1}":"Copied {0} -> {1}", + "Copied files":"Copied files", + "Created directory: {0}":"Created directory: {0}", + "Created file: {0}":"Created file: {0}", + "Css":"Css", + "Error when create directory: {0}":"Error when create directory: {0}", + "Generated {0}":"Generated {0}", + "Hide":"Hide", + "Ignore: {0} unsaved files {1}?":"Ignore: {0} unsaved files {1}?", + "Ignore unsaved project ?":"Ignore unsaved project ?", + "Installing...":"Installing...", + "Javascripts":"Javascripts", + "Metadata found...":"Metadata found...", + "New Project at":"New Project at", + "New project":"New project", + "Opening {0}":"Opening {0}", + "Open project":"Open project", + "Open Project":"Open Project", + "Output":"Output", + "Please select {0} only":"Please select {0} only", + "Please select a file/fofler":"Please select a file/fofler", + "Preparing for release":"Preparing for release", + "ProjectName":"ProjectName", + "Project":"Project", + "project saved":"project saved", + "Running {0}...":"Running {0}...", + "Select a file":"Select a file", + "Show":"Show", + "Uninstall: {0}?":"Uninstall: {0}?", + "Unsaved project":"Unsaved project", + "Update":"Update", + "Verifying {0}":"Verifying {0}", + "Version string is in invalid format: {0}":"Version string is in invalid format: {0}", + "Welcome to AntOSDK":"Welcome to AntOSDK", + "Your application version is older ({0} < {1})":"Your application version is older ({0} < {1})", + "zip file generated in release folder":"zip file generated in release folder" + +, + "Cancels":"Cancels", + "Command Palette":"Command Palette", + "Mount Points":"Mount Points", + "New CodePad extension at":"New CodePad extension at", + "Report":"Report", + "Select extension archive":"Select extension archive", + "System error log":"System error log" } diff --git a/src/core/pm.ts b/src/core/pm.ts new file mode 100644 index 0000000..f8dce1f --- /dev/null +++ b/src/core/pm.ts @@ -0,0 +1,155 @@ +namespace OS { + /** + * This namespace dedicated to all operations related to system + * process management + */ + export namespace PM { + /** + * A process is either an instance of an application or a service + */ + export type ProcessType = + | application.BaseApplication + | application.BaseService; + /** + * Alias to all classes that extends [[BaseModel]] + */ + export type ModelTypeClass = { + new (args: AppArgumentsType[]): T; + }; + /** + * Process id allocator, when a new process is created, the value of + * this variable is increased + */ + export var pidalloc: number = 0; + /** + * All running processes is stored in this variables + */ + export var processes: GenericObject = {}; + /** + * Create a new process of application or service + * + * @export + * @param {string} app class name string + * @param {ProcessTypeClass} cls prototype class + * @param {GUI.AppArgumentsType[]} [args] process arguments + * @returns {Promise} a promise on the created process + */ + export function createProcess( + app: string, + cls: ModelTypeClass, + args?: AppArgumentsType[] + ): Promise { + return new Promise(function (resolve, reject) { + let metaclass = (cls as any) as typeof BaseModel; + const f = function () { + //if it is single ton + // and a process is existing + // just return it + let obj: ProcessType; + if ( + metaclass.singleton && + PM.processes[app] && + PM.processes[app].length === 1 + ) { + obj = PM.processes[ + app + ][0] as application.BaseApplication; + obj.show(); + } else { + if (!PM.processes[app]) { + PM.processes[app] = []; + } + obj = new cls(args); + obj.birth = new Date().getTime(); + PM.pidalloc++; + obj.pid = PM.pidalloc; + PM.processes[app].push(obj); + if (metaclass.type === ModelType.Application) { + GUI.dock( + obj as application.BaseApplication, + metaclass.meta + ); + } else { + GUI.attachservice(obj as application.BaseService); + } + } + return obj; + }; + if (metaclass.dependencies) { + const libs = metaclass.dependencies; + return API.require(libs) + .then(() => resolve(f())) + .catch((e: Error) => reject(__e(e))); + } else { + return resolve(f()); + } + }); + } + + /** + * Get the reference to a process using its id + * + * @export + * @param {number} pid + * @returns {BaseModel} + */ + export function appByPid(pid: number): BaseModel { + let app = undefined; + const find = function (l: Array) { + for (let a of l) { + if (a.pid === pid) { + return a; + } + } + }; + for (let k in PM.processes) { + const v = PM.processes[k]; + app = find(v); + if (app) { + break; + } + } + return app; + } + + /** + * Kill a process + * + * @export + * @param {OS.GUI.BaseModel} app reference to the process + * @returns {void} + */ + export function kill(app: BaseModel): void { + if (!app.name || !PM.processes[app.name]) { + return; + } + + const i = PM.processes[app.name].indexOf(app); + if (i >= 0) { + if (application[app.name].type === ModelType.Application) { + GUI.undock(app as application.BaseApplication); + } else { + GUI.detachservice(app as application.BaseService); + } + announcer.unregister(app); + delete PM.processes[app.name][i]; + PM.processes[app.name].splice(i, 1); + } + } + + /** + * Kill all process of an application or service + * + * @export + * @param {string} app process class name + * @param {boolean} force force exit all process + * @returns {void} + */ + export function killAll(app: string, force: boolean): void { + if (!PM.processes[app]) { + return; + } + PM.processes[app].map((a) => a.quit(force)); + } + } +} diff --git a/src/core/schemes/about.html b/src/core/schemes/about.html deleted file mode 100644 index aba8877..0000000 --- a/src/core/schemes/about.html +++ /dev/null @@ -1,11 +0,0 @@ - - -
-

- -

-

-
- -
-
\ No newline at end of file diff --git a/src/core/schemes/dm.html b/src/core/schemes/dm.html deleted file mode 100644 index 52f7a24..0000000 --- a/src/core/schemes/dm.html +++ /dev/null @@ -1,10 +0,0 @@ - - - -
- - - -
- - \ No newline at end of file diff --git a/src/core/schemes/filedialog.html b/src/core/schemes/filedialog.html deleted file mode 100644 index 512c41d..0000000 --- a/src/core/schemes/filedialog.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - -
- - -
-
-
-
\ No newline at end of file diff --git a/src/core/schemes/login.html b/src/core/schemes/login.html deleted file mode 100644 index 23edcbe..0000000 --- a/src/core/schemes/login.html +++ /dev/null @@ -1,78 +0,0 @@ - - -
-

Welcome to AntOS, please identify

- - - - -
\ No newline at end of file diff --git a/src/core/settings.coffee b/src/core/settings.coffee deleted file mode 100644 index 5d096b7..0000000 --- a/src/core/settings.coffee +++ /dev/null @@ -1,91 +0,0 @@ - # Copyright 2017-2018 Xuan Sang LE - -# AnTOS Web desktop is is licensed under the GNU General Public -# License v3.0, see the LICENCE file for more information - -# This program is free software: you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation, either version 3 of -# the License, or (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. - -# You should have received a copy of the GNU General Public License -#along with this program. If not, see https://www.gnu.org/licenses/. - self.OS.systemSetting = (conf) -> - _OS.setting.desktop = conf.desktop if conf.desktop - _OS.setting.applications = conf.applications if conf.applications - _OS.setting.appearance = conf.appearance if conf.appearance - _OS.setting.appearance.wp = { - url: "os://resources/themes/system/wp/wp2.jpg", - size: "cover", - repeat: "repeat" - } unless _OS.setting.appearance.wp - _OS.setting.appearance.wps = [] unless _OS.setting.appearance.wps - _OS.setting.applications = {} unless _OS.setting.applications - _OS.setting.user = conf.user - _OS.setting.VFS = conf.VFS if conf.VFS - _OS.setting.desktop.path = "home://.desktop" unless _OS.setting.desktop.path - _OS.setting.desktop.menu = {} unless _OS.setting.desktop.menu - _OS.setting.VFS.mountpoints = [ - #TODO: multi app try to write to this object, it neet to be cloned - { text: "__(Applications)", path: 'app://', iconclass: "fa fa-adn", type: "app" }, - { text: "__(Home)", path: 'home://', iconclass: "fa fa-home", type: "fs" }, - { text: "__(Desktop)", path: _OS.setting.desktop.path , iconclass: "fa fa-desktop", type: "fs" }, - { text: "__(OS)", path: 'os://', iconclass: "fa fa-inbox", type: "fs" }, - { text: "__(Google Drive)", path: 'gdv://', iconclass: "fa fa-inbox", type: "fs" }, - { text: "__(Shared)", path: 'shared://' , iconclass: "fa fa-share-square", type: "fs" } - ] if not _OS.setting.VFS.mountpoints - - _OS.setting.system = conf.system if conf.system - _OS.setting.system.startup = { - services: [ - "CoreServices/PushNotification", - "CoreServices/UserService", - "CoreServices/Calendar", - "CoreServices/Spotlight" - ], - apps: [] - } if not _OS.setting.system.startup - - _OS.setting.system.pkgpaths = { - user: "home://.packages", - system: "os://packages" - } unless _OS.setting.system.pkgpaths - _OS.setting.system.locale = "en_GB" unless _OS.setting.system.locale - _OS.setting.system.menu = {} unless _OS.setting.system.menu - _OS.setting.system.repositories = [] unless _OS.setting.system.repositories - _OS.setting.appearance.theme = "antos" unless _OS.setting.appearance.theme - - _OS.setting.VFS.gdrive = { - CLIENT_ID: "" - API_KEY: "" - apilink: "https://apis.google.com/js/api.js" - DISCOVERY_DOCS: ["https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"] - SCOPES: 'https://www.googleapis.com/auth/drive' - } unless _OS.setting.VFS.gdrive - - #search for app - _API.onsearch "__(Applications)", (t) -> - ar = [] - term = new RegExp t, "i" - for k, v of _OS.setting.system.packages when v.app - if (v.name.match term) or (v.description and v.description.match term) - v1 = {} - v1[k1] = e for k1, e of v when k1 isnt "selected" - v1.detail = [{ text: v1.path }] - v1.complex = true - ar.push v1 - else if v.mimes - for m in v.mimes - if t.match (new RegExp m, "g") - v1 = {} - v1[k1] = v[k1] for k1, e of v when k1 isnt "selected" - v1.detail = [{ text: v1.path }] - v1.complex = true - ar.push v1 - break - return ar \ No newline at end of file diff --git a/src/core/settings.ts b/src/core/settings.ts new file mode 100644 index 0000000..d87819a --- /dev/null +++ b/src/core/settings.ts @@ -0,0 +1,569 @@ +// Copyright 2017-2018 Xuan Sang LE + +// AnTOS Web desktop is is licensed under the GNU General Public +// License v3.0, see the LICENCE file for more information + +// This program is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. + +// You should have received a copy of the GNU General Public License +//along with this program. If not, see https://www.gnu.org/licenses/. + +namespace OS { + /** + * This namespace is dedicated to everything related to the + * global system settings + */ + export namespace setting { + /** + * User setting type definition + * + * @export + * @interface UserSettingType + */ + export interface UserSettingType { + /** + * User full name + * + * @type {string} + * @memberof UserSettingType + */ + name: string; + + /** + * User name + * + * @type {string} + * @memberof UserSettingType + */ + username: string; + + /** + * User id + * + * @type {number} + * @memberof UserSettingType + */ + id: number; + + /** + * User groups + * + * @type {{ [index: number]: string }} + * @memberof UserSettingType + */ + group?: { [index: number]: string }; + [propName: string]: any; + } + + /** + * Virtual desktop setting data type + * + * @export + * @interface DesktopSettingType + */ + export interface DesktopSettingType { + /** + * Desktop VFS path + * + * @type {string} + * @memberof DesktopSettingType + */ + path: string; + + /** + * Desktop menu, can be added automatically by applications + * + * @type {GUI.BasicItemType[]} + * @memberof DesktopSettingType + */ + menu: GUI.BasicItemType[]; + + /** + * Show desktop hidden files + * + * @type {boolean} + * @memberof DesktopSettingType + */ + showhidden: boolean; + [propName: string]: any; + } + + /** + * Wallpaper setting data type + * + * @export + * @interface WPSettingType + */ + export interface WPSettingType { + /** + * Repeat wallpaper: + * - `repeat` + * - `repeat-x` + * - `repeat-y` + * - `no-repeat` + * + * @type {string} + * @memberof WPSettingType + */ + repeat: string; + + /** + * Wallpaper size + * - `contain` + * - `cover` + * - `auto` + * + * @type {string} + * @memberof WPSettingType + */ + size: string; + + /** + * VFS path to the wallpaper image + * + * @type {string} + * @memberof WPSettingType + */ + url: string; + } + + /** + * Theme setting data type + * + * @export + * @interface ThemeSettingType + */ + export interface ThemeSettingType { + /** + * Theme name, this value is used for looking + * theme file in system asset + * + * @type {string} + * @memberof ThemeSettingType + */ + name: string; + + /** + * Theme user-friendly text + * + * @type {string} + * @memberof ThemeSettingType + */ + text: string; + } + + /** + * Appearance setting data type + * + * @export + * @interface AppearanceSettingType + */ + export interface AppearanceSettingType { + /** + * Current theme name + * + * @type {string} + * @memberof AppearanceSettingType + */ + theme: string; + + /** + * All themes available in the system + * + * @type {ThemeSettingType[]} + * @memberof AppearanceSettingType + */ + themes: ThemeSettingType[]; + + /** + * Current wallpaper setting + * + * @type {WPSettingType} + * @memberof AppearanceSettingType + */ + wp: WPSettingType; + + /** + * All wallpapers available in the system + * + * @type {string[]} + * @memberof AppearanceSettingType + */ + wps: string[]; + } + + /** + * VFS Mount points setting data type + * + * @export + * @interface VFSMountPointSettingType + */ + export interface VFSMountPointSettingType { + /** + * Path to the mount point + * + * @type {string} + * @memberof VFSMountPointSettingType + */ + path: string; + + /** + * User friendly mount point name + * + * @type {string} + * @memberof VFSMountPointSettingType + */ + text: string; + [propName: string]: any; + } + + /** + * VFS setting data type + * + * @export + * @interface VFSSettingType + */ + export interface VFSSettingType { + /** + * mount points setting + * + * @type {VFSMountPointSettingType[]} + * @memberof VFSSettingType + */ + mountpoints: VFSMountPointSettingType[]; + [propName: string]: any; + } + + /** + * Global system setting data type + * + * @export + * @interface SystemSettingType + */ + export interface SystemSettingType { + /** + * System error report URL + * + * @type {string} + * @memberof SystemSettingType + */ + error_report: string; + + /** + * Current system locale e.g. `en_GB` + * + * @type {string} + * @memberof SystemSettingType + */ + locale: string; + + /** + * System menus + * + * @type {API.PackageMetaType[]} + * @memberof API.PackageMetaType + */ + menu: API.PackageMetaType[]; + + /** + * Packages meta-data + * + * @type {{ [index: string]: API.PackageMetaType }} + * @memberof SystemSettingType + */ + packages: { [index: string]: API.PackageMetaType }; + + /** + * Path to the installed packages + * + * @type {{ + * user: string; + * system: string; + * }} + * @memberof SystemSettingType + */ + pkgpaths: { + /** + * User specific packages install location + * + * @type {string} + */ + user: string; + + /** + * System packages install location + * + * @type {string} + */ + system: string; + }; + + /** + * Package repositories setting. + * This configuration is used by [[MarketPlace]] + * for package management + * + * @type {{ + * text: string; + * url: string; + * }[]} + * @memberof SystemSettingType + */ + repositories: { + /** + * Repository name + * + * @type {string} + */ + text: string; + + /** + * Repository uri + * + * @type {string} + */ + url: string; + }[]; + + /** + * Startup applications and services + * + * @type {{ + * apps: string[]; + * services: string[]; + * }} + * @memberof SystemSettingType + */ + startup: { + /** + * List of application names + * + * @type {string[]} + */ + apps: string[]; + + /** + * List of service names + * + * @type {string[]} + */ + services: string[]; + }; + } + /** + * User settings + */ + export var user: UserSettingType; + /** + * Application settings + */ + export var applications: GenericObject = {}; + /** + * Desktop settings + */ + export var desktop: DesktopSettingType; + /** + * Appearance settings + */ + export var appearance: AppearanceSettingType; + /** + * VFS settings + */ + export var VFS: VFSSettingType; + /** + * System settings + */ + export var system: SystemSettingType; + } + + /** + * Reset the system settings to default values + * + * @export + */ + export function resetSetting(): void { + setting.desktop = { + path: "home://.desktop", + menu: [], + showhidden: false, + }; + setting.user = { + name: undefined, + username: undefined, + id: 0, + }; + + setting.appearance = { + theme: "antos_dark", + themes: [ + { + text: "AntOS light", + name: "antos_light", + }, + { + text: "AntOS dark", + name: "antos_dark", + }, + ], + wp: { + url: "os://resources/themes/system/wp/wp3.jpg", + size: "cover", + repeat: "repeat", + }, + wps: [], + }; + + setting.VFS = { + mountpoints: [ + //TODO: multi app try to write to this object, it neet to be cloned + { + text: "__(Applications)", + path: "app://", + iconclass: "fa fa-adn", + type: "app", + }, + { + text: "__(Home)", + path: "home://", + iconclass: "fa fa-home", + type: "fs", + }, + { + text: "__(Desktop)", + path: setting.desktop.path, + iconclass: "fa fa-desktop", + type: "fs", + }, + { + text: "__(OS)", + path: "os://", + iconclass: "fa fa-inbox", + type: "fs", + }, + { + text: "__(Google Drive)", + path: "gdv://", + iconclass: "fa fa-inbox", + type: "fs", + }, + { + text: "__(Shared)", + path: "shared://", + iconclass: "fa fa-share-square", + type: "fs", + }, + ], + }; + + OS.setting.system = { + error_report: "https://os.iohub.dev/report", + locale: "en_GB", + menu: [], + packages: {}, + pkgpaths: { + user: "home://.packages", + system: "os://packages", + }, + repositories: [], + startup: { + apps: [], + services: ["Syslog/PushNotification", "Syslog/Calendar"], + }, + }; + } + + /** + * Apply the input parameter object to system settings. + * This object could be an object loaded from + * setting JSON file saved on the server. + * + * @export + * @param {*} conf + */ + export function systemSetting(conf: any) { + resetSetting(); + if (conf.desktop) { + setting.desktop = conf.desktop; + } + if (conf.applications) { + setting.applications = conf.applications; + } + if (conf.appearance) { + setting.appearance = conf.appearance; + } + if (conf.user) { + setting.user = conf.user; + } + if (conf.VFS) { + setting.VFS = conf.VFS; + } + + if (conf.system) { + setting.system = conf.system; + } + + if (!setting.VFS.gdrive) { + setting.VFS.gdrive = { + CLIENT_ID: "", + API_KEY: "", + apilink: "https://apis.google.com/js/api.js", + DISCOVERY_DOCS: [ + "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest", + ], + SCOPES: "https://www.googleapis.com/auth/drive", + }; + } + } + + // Register handle for application search + API.onsearch("__(Applications)", function (t) { + const ar = []; + const term = new RegExp(t, "i"); + for (let k in setting.system.packages) { + const v = setting.system.packages[k]; + if (v.app) { + var e, k1, v1; + if ( + v.name.match(term) || + (v.description && v.description.match(term)) + ) { + v1 = {}; + for (k1 in v) { + e = v[k1]; + if (k1 !== "selected") { + v1[k1] = e; + } + } + v1.detail = [{ text: v1.path }]; + v1.complex = true; + ar.push(v1); + } else if (v.mimes) { + for (let m of v.mimes) { + if (t.match(new RegExp(m, "g"))) { + v1 = {}; + for (k1 in v) { + e = v[k1]; + if (k1 !== "selected") { + v1[k1] = v[k1]; + } + } + v1.detail = [{ text: v1.path }]; + v1.complex = true; + ar.push(v1); + break; + } + } + } + } + } + return ar; + }); +} diff --git a/src/core/tags/AppDockTag.ts b/src/core/tags/AppDockTag.ts new file mode 100644 index 0000000..912d725 --- /dev/null +++ b/src/core/tags/AppDockTag.ts @@ -0,0 +1,245 @@ +namespace OS { + export namespace GUI { + /** + * + * Interface for an application dock item + * + * @export + * @interface AppDockItemType + */ + export interface AppDockItemType { + /** + * Reference to the application process represented + * by the dock item + * + * @type {application.BaseApplication} + * @memberof AppDockItemType + */ + app: application.BaseApplication; + + /** + * Reference to the DOM element of + * the owner dock item + * + * @type {AFXTag} + * @memberof AppDockItemType + */ + domel?: AFXTag; + [propName: string]: any; + } + + export namespace tag { + /** + * This class define the AntOS system application dock tag + * + * @export + * @class AppDockTag + * @extends {AFXTag} + */ + export class AppDockTag extends AFXTag { + /** + * variable holds the application select event + * callback handle + * + * @private + * @type {TagEventCallback} + * @memberof AppDockTag + */ + private _onappselect: TagEventCallback; + + /** + * Items data of the dock + * + * @private + * @type {AppDockItemType[]} + * @memberof AppDockTag + */ + private _items: AppDockItemType[]; + + /** + * Reference to the currently select application + * process in the dock + * + * @private + * @type {application.BaseApplication} + * @memberof AppDockTag + */ + private _selectedApp: application.BaseApplication; + + /** + *Creates an instance of AppDockTag. + * @memberof AppDockTag + */ + constructor() { + super(); + this._onappselect = (e) => {}; + } + + /** + * Implementation of the abstract function: Update the current tag. + * It do nothing for this tag + * + * @protected + * @param {*} [d] + * @memberof AppDockTag + */ + protected reload(d?: any): void {} + + /** + * Init the tag before mounting + * + * @protected + * @memberof AppDockTag + */ + protected init(): void { + this._items = []; + } + + /** + * The tag layout, it is empty on creation but elements will + * be added automatically to it in operation + * + * @protected + * @returns {TagLayoutType[]} + * @memberof AppDockTag + */ + protected layout(): TagLayoutType[] { + return []; + } + + /** + * getter to get the dock items + * + * @readonly + * @type {AppDockItemType[]} + * @memberof AppDockTag + */ + get items(): AppDockItemType[] { + return this._items; + } + + /** + * Setter: + * + * set the selected application in the dock + * this will trigger two event: + * - `focus`: on the selected application + * - `blur`: on all other applications on the dock + * + * Getter: + * + * Get the current selected application + * on the dock + * + * @memberof AppDockTag + */ + set selectedApp(v: application.BaseApplication) { + this._selectedApp = v; + let el = undefined; + for (let it of this.items) { + it.app.blur(); + $(it.domel).removeClass(); + if (v && v === it.app) { + el = it.domel; + } + } + if (!el) { + return; + } + $(el).addClass("selected"); + ($(Ant.OS.GUI.workspace)[0] as FloatListTag).unselect(); + } + + get selectedApp(): application.BaseApplication { + return this._selectedApp; + } + + /** + * When a new application process is created, this function + * will be called to add new application entry to the dock. + * The added application will becomes the current selected + * application + * + * @param {AppDockItemType} item an application dock item entry + * @memberof AppDockTag + */ + newapp(item: AppDockItemType): void { + this.items.push(item); + const el = $(""); + const bt = el[0] as ButtonTag; + el.appendTo(this); + el[0].uify(this.observable); + bt.set(item); + bt.data = item.app; + el.attr("tooltip", `cr:${item.app.title()}`); + item.domel = bt; + bt.onbtclick = (e) => { + e.id = this.aid; + //e.data.item = item; + this._onappselect(e); + item.app.show(); + }; + this.selectedApp = item.app; + } + + /** + * Delete and application entry from the dock. + * This function will be called when an application + * is exit + * + * @param {BaseApplication} a the application to be removed from the dock + * @memberof AppDockTag + */ + removeapp(a: application.BaseApplication): void { + let i = -1; + const iterable = this.items; + for (let k = 0; k < iterable.length; k++) { + const v = iterable[k]; + if (v.app.pid === a.pid) { + i = k; + break; + } + } + + if (i !== -1) { + delete this.items[i].app; + this.items.splice(i, 1); + $($(this).children()[i]).remove(); + } + } + + /** + * Mount the current dock tag + * + * @protected + * @memberof AppDockTag + */ + protected mount(): void { + this.contextmenuHandle = (e, m) => { + if (e.target === this) { + return; + } + const bt = ($(e.target).closest( + "afx-button" + )[0] as any) as ButtonTag; + const app = bt.data; + m.items = [ + { text: "__(Show)", dataid: "show" }, + { text: "__(Hide)", dataid: "hide" }, + { text: "__(Close)", dataid: "quit" }, + ]; + m.onmenuselect = function (evt) { + const item = evt.data.item.data; + if (app[item.dataid]) { + return app[item.dataid](); + } + }; + return m.show(e); + }; + announcer.trigger("sysdockloaded", undefined); + } + } + define("afx-apps-dock", AppDockTag); + } + } +} diff --git a/src/core/tags/ButtonTag.ts b/src/core/tags/ButtonTag.ts new file mode 100644 index 0000000..fcffd8f --- /dev/null +++ b/src/core/tags/ButtonTag.ts @@ -0,0 +1,198 @@ +namespace OS { + export namespace GUI { + export namespace tag { + /** + * This tag define a basic button and its behavior + * + * @export + * @class ButtonTag + * @extends {AFXTag} + */ + export class ButtonTag extends AFXTag { + /** + * Variable hold the button click callback handle + * + * @private + * @type {TagEventCallback} + * @memberof ButtonTag + */ + private _onbtclick: TagEventCallback; + + /** + * Custom user data + * + * @type {GenericObject} + * @memberof ButtonTag + */ + data: GenericObject; + + /** + *Creates an instance of ButtonTag. + * @memberof ButtonTag + */ + constructor() { + super(); + } + + /** + * Set the click callback handle for the target button + * + * @memberof ButtonTag + */ + set onbtclick(v: TagEventCallback) { + this._onbtclick = v; + } + + /** + * Set the path to the button icon, the path should be + * a VFS file path + * + * @memberof ButtonTag + */ + set icon(v: string) { + $(this).attr("icon", v); + (this.refs.label as LabelTag).icon = v; + } + + /** + * Set the icon class to the button, this property + * allows to style the button icon using CSS + * + * @memberof ButtonTag + */ + set iconclass(v: string) { + $(this).attr("iconclass", v); + (this.refs.label as LabelTag).iconclass = v; + } + + /** + * Setter: Set the text of the button + * + * Getter: Get the current button test + * + * @memberof ButtonTag + */ + set text(v: string | FormattedString) { + (this.refs.label as LabelTag).text = v; + } + + get text(): string | FormattedString { + return (this.refs.label as LabelTag).text; + } + + /** + * Setter: Enable or disable the button + * + * Getter: Get the `enable` property of the button + * + * @memberof ButtonTag + */ + set enable(v: boolean) { + $(this.refs.button).prop("disabled", !v); + } + get enable(): boolean { + return !$(this.refs.button).prop("disabled"); + } + + /** + * Setter: set or remove the attribute `selected` of the button + * + * Getter: check whether the attribute `selected` of the button is set + * + * @memberof ButtonTag + */ + set selected(v: boolean) { + $(this.refs.button).removeClass(); + this.attsw(v, "selected"); + if (v) { + $(this.refs.button).addClass("selected"); + } + } + get selected(): boolean { + return this.hasattr("selected"); + } + + /** + * Setter: activate or deactivate the toggle mode of the button + * + * Getter: Check whether the button is in toggle mode + * + * @memberof ButtonTag + */ + set toggle(v: boolean) { + this.attsw(v, "toggle"); + } + get toggle(): boolean { + return this.hasattr("toggle"); + } + + /** + * Mount the tag + * + * @protected + * @memberof ButtonTag + */ + protected mount() { + $(this.refs.button).click((e) => { + const evt: TagEventType = { + id: this.aid, + data: e, + }; + this._onbtclick(evt); + this.observable.trigger("btclick", evt); + if (this.toggle) { + return (this.selected = !this.selected); + } + }); + } + + /** + * Init the tag before mounting + * + * @protected + * @memberof ButtonTag + */ + protected init(): void { + this.enable = true; + this.toggle = false; + this._onbtclick = (e) => {}; + } + + /** + * Re-calibrate the button, do nothing in this tag + * + * @protected + * @memberof ButtonTag + */ + protected calibrate(): void {} + + /** + * Update the current tag, do nothing in this tag + * + * @param {*} [d] + * @memberof ButtonTag + */ + reload(d?: any): void {} + + /** + * Button layout definition + * + * @protected + * @returns {TagLayoutType[]} + * @memberof ButtonTag + */ + protected layout(): TagLayoutType[] { + return [ + { + el: "Button", + ref: "button", + children: [{ el: "afx-label", ref: "label" }], + }, + ]; + } + } + + define("afx-button", ButtonTag); + } + } +} diff --git a/src/core/tags/CalendarTag.ts b/src/core/tags/CalendarTag.ts new file mode 100644 index 0000000..5596f93 --- /dev/null +++ b/src/core/tags/CalendarTag.ts @@ -0,0 +1,335 @@ +namespace OS { + export namespace GUI { + export namespace tag { + /** + * Tag that define system calendar widget + * + * @export + * @class CalendarTag + * @extends {AFXTag} + */ + export class CalendarTag extends AFXTag { + /** + * The current selected day + * + * @private + * @type {number} + * @memberof CalendarTag + */ + private _day: number; + + /** + * The current selected month + * + * @private + * @type {number} + * @memberof CalendarTag + */ + private _month: number; + + /** + * The current selected year + * + * @private + * @type {number} + * @memberof CalendarTag + */ + private _year: number; + + /** + * The current selected date object + * + * @private + * @type {Date} + * @memberof CalendarTag + */ + private _selectedDate: Date; + + /** + * placeholder for date select event callback + * + * @private + * @type {TagEventCallback} + * @memberof CalendarTag + */ + private _ondateselect: TagEventCallback; + + /** + *Creates an instance of CalendarTag. + * @memberof CalendarTag + */ + constructor() { + super(); + this._day = 0; + this._month = 0; + this._year = 0; + this._ondateselect = (e) => {}; + } + + /** + * Init the tag before mounting + * + * @protected + * @memberof CalendarTag + */ + protected init(): void { + $(this).css("height", "100%"); + $(this.refs.grid).css("width", "100%"); + } + + /** + * Update the current tag, doing nothing in this tag + * + * @protected + * @param {*} [d] any data object + * @memberof CalendarTag + */ + protected reload(d?: any): void {} + + /** + * Get the current selected date in the widget + * + * @readonly + * @type {Date} + * @memberof CalendarTag + */ + get selectedDate(): Date { + return this._selectedDate; + } + + /** + * Set the date select event callback handle for the widget + * + * @memberof CalendarTag + */ + set ondateselect(v: TagEventCallback) { + this._ondateselect = v; + } + + /** + * Mount the current widget to the DOM tree + * + * @protected + * @memberof CalendarTag + */ + protected mount(): void { + $(this.refs.prev).click((e) => this.prevmonth()); + $(this.refs.next).click((e) => this.nextmonth()); + const grid = this.refs.grid as GridViewTag; + grid.header = [ + { text: "__(Sun)" }, + { text: "__(Mon)" }, + { text: "__(Tue)" }, + { text: "__(Wed)" }, + { text: "__(Thu)" }, + { text: "__(Fri)" }, + { text: "__(Sat)" }, + ]; + grid.oncellselect = (e) => { + this.dateselect(e); + }; + + this.observable.on("resize", (e) => this.calibrate()); + this.calibrate(); + this.calendar(null); + } + + /** + * This function triggers the date select event + * + * @private + * @param {TagEventType} e AFX tag event data [[TagEventType]] + * @returns {void} + * @memberof CalendarTag + */ + private dateselect( + e: TagEventType> + ): void { + if (!e.data.item) { + return; + } + const value = e.data.item.data.text; + if (value === "") { + return; + } + const evt = { + id: this.aid, + data: new Date( + this._year, + this._month, + parseInt(value) + ), + }; + this._ondateselect(evt); + this._selectedDate = evt.data; + return this.observable.trigger("dateselect", evt); + } + + /** + * Calibrate the layout of the tag + * + * @protected + * @memberof CalendarTag + */ + protected calibrate(): void { + $(this.refs.grid).css( + "height", + `${$(this).height() - $(this.refs.ctrl).height()}px` + ); + } + + /** + * Display the previous month of the current month + * + * @private + * @memberof CalendarTag + */ + private prevmonth(): void { + this._selectedDate = undefined; + this._month--; + if (this._month < 0) { + this._month = 11; + this._year--; + } + this.calendar(new Date(this._year, this._month, 1)); + } + + /** + * Display the next month of the current month + * + * @private + * @returns + * @memberof CalendarTag + */ + private nextmonth() { + this._selectedDate = undefined; + this._month++; + if (this._month > 11) { + this._month = 0; + this._year++; + } + return this.calendar(new Date(this._year, this._month, 1)); + } + + /** + * Visualize the calendar base on input date + * + * @private + * @param {Date} date + * @memberof CalendarTag + */ + private calendar(date: Date) { + let week_day: number; + let asc: any, end: any; + if (!date) { + date = new Date(); + } + this._day = date.getDate(); + this._month = date.getMonth(); + this._year = date.getFullYear(); + + const now = { + d: new Date().getDate(), + m: new Date().getMonth(), + y: new Date().getFullYear(), + }; + const months = [ + __("January"), + __("February"), + __("March"), + __("April"), + __("May"), + __("June"), + __("July"), + __("August"), + __("September"), + __("October"), + __("November"), + __("December"), + ]; + const this_month = new Date(this._year, this._month, 1); + const next_month = new Date(this._year, this._month + 1, 1); + // Find out when this month starts and ends. + const first_week_day = this_month.getDay(); + const days_in_this_month = Math.round( + (next_month.getTime() - this_month.getTime()) / + (1000 * 60 * 60 * 24) + ); + //self.mtext = months[self.month] + const rows = []; + let row = []; + // Fill the first week of the month with the appropriate number of blanks. + for ( + week_day = 0, end = first_week_day - 1, asc = 0 <= end; + asc ? week_day <= end : week_day >= end; + asc ? week_day++ : week_day-- + ) { + row.push({ text: "" }); + } + week_day = first_week_day; + for ( + let day_counter = 1, + end1 = days_in_this_month, + asc1 = 1 <= end1; + asc1 ? day_counter <= end1 : day_counter >= end1; + asc1 ? day_counter++ : day_counter-- + ) { + week_day %= 7; + if (week_day === 0) { + rows.push(row); + row = []; + } + // Do something different for the current day. + if ( + now.d === day_counter && + this._month === now.m && + this._year === now.y + ) { + row.push({ text: day_counter, selected: true }); + } else { + row.push({ text: day_counter }); + } + week_day++; + } + for ( + let i = 0, end2 = 7 - row.length, asc2 = 0 <= end2; + asc2 ? i <= end2 : i >= end2; + asc2 ? i++ : i-- + ) { + row.push({ text: "" }); + } + rows.push(row); + const grid = this.refs.grid as GridViewTag; + grid.rows = rows; + (this.refs.mlbl as LabelTag).text = `${ + months[this._month] + } ${this._year}`; + } + + /** + * Layout definition of the widget + * + * @protected + * @returns {TagLayoutType[]} + * @memberof CalendarTag + */ + protected layout(): TagLayoutType[] { + return [ + { + el: "div", + ref: "ctrl", + children: [ + { el: "i", class: "prevmonth", ref: "prev" }, + { el: "afx-label", ref: "mlbl" }, + { el: "i", class: "nextmonth", ref: "next" }, + ], + }, + { el: "afx-grid-view", ref: "grid" }, + ]; + } + } + + define("afx-calendar-view", CalendarTag); + } + } +} diff --git a/src/core/tags/ColorPickerTag.ts b/src/core/tags/ColorPickerTag.ts new file mode 100644 index 0000000..0fd7fae --- /dev/null +++ b/src/core/tags/ColorPickerTag.ts @@ -0,0 +1,327 @@ +namespace OS { + export namespace GUI { + /** + * Color type used by AFX API + * + * @export + * @interface ColorType + */ + export interface ColorType { + /** + * Red chanel + * + * @type {number} + * @memberof ColorType + */ + r: number; + + /** + * Green chanel + * + * @type {number} + * @memberof ColorType + */ + g: number; + + /** + * Blue chanel + * + * @type {number} + * @memberof ColorType + */ + b: number; + + /** + * Alpha chanel + * + * @type {number} + * @memberof ColorType + */ + a?: number; + + /** + * color text in CSS format + * + * @type {string} + * @memberof ColorType + */ + text?: string; + + /** + * Color in hex format + * + * @type {string} + * @memberof ColorType + */ + hex?: string; + } + export namespace tag { + /** + * Class definition of Color picker widget + * + * @export + * @class ColorPickerTag + * @extends {AFXTag} + */ + export class ColorPickerTag extends AFXTag { + /** + * The current selected color object + * + * @private + * @type {ColorType} + * @memberof ColorPickerTag + */ + private _selectedColor: ColorType; + + /** + * placeholder for the color select event callback + * + * @private + * @type {TagEventCallback} + * @memberof ColorPickerTag + */ + private _oncolorselect: TagEventCallback; + + /** + * Creates an instance of ColorPickerTag. + * @memberof ColorPickerTag + */ + constructor() { + super(); + this._oncolorselect = (e) => {}; + } + + /** + * Init tag before mounting, do nothing + * + * @protected + * @memberof ColorPickerTag + */ + protected init(): void {} + + /** + * Reload tag, do nothing + * + * @protected + * @param {*} [d] + * @memberof ColorPickerTag + */ + protected reload(d?: any): void {} + + /** + * Get selected color value + * + * @readonly + * @type {ColorType} + * @memberof ColorPickerTag + */ + get selectedColor(): ColorType { + return this._selectedColor; + } + + /** + * Set the color select event handle + * + * @memberof ColorPickerTag + */ + set oncolorselect(v: TagEventCallback) { + this._oncolorselect = v; + } + + /** + * Mount the widget to DOM tree + * + * @protected + * @memberof ColorPickerTag + */ + protected mount(): void { + $(this.refs.wrapper) + .css("width", "310px") + .css("height", "190px") + .css("display", "block") + .css("padding", "3px"); + $(this.refs.palette) + .css("width", "284px") + .css("height", "155px") + .css("float", "left"); + $(this.refs.colorval) + .css("width", "15px") + .css("height", "155px") + .css("text-align", "center") + .css("margin-left", "3px") + .css("display", "block") + .css("float", "left"); + + $(this.refs.inputwrp).css("margin-top", "3px"); + + $(this.refs.hextext) + .css("width", "70px") + .css("margin-left", "3px") + .css("margin-right", "5px"); + + this.build_palette(); + } + + /** + * Build the color palette + * + * @private + * @memberof ColorPickerTag + */ + private build_palette(): void { + const colorctx = ($(this.refs.palette).get( + 0 + ) as HTMLCanvasElement).getContext("2d"); + let gradient = colorctx.createLinearGradient( + 0, + 0, + $(this.refs.palette).width(), + 0 + ); + // fill color + gradient.addColorStop(0, "rgb(255, 0, 0)"); + gradient.addColorStop(0.15, "rgb(255, 0, 255)"); + gradient.addColorStop(0.33, "rgb(0, 0, 255)"); + gradient.addColorStop(0.49, "rgb(0, 255, 255)"); + gradient.addColorStop(0.67, "rgb(0, 255, 0)"); + gradient.addColorStop(0.84, "rgb(255, 255, 0)"); + gradient.addColorStop(1, "rgb(255, 0, 0)"); + gradient.addColorStop(0, "rgb(0, 0, 0)"); + // Apply gradient to canvas + colorctx.fillStyle = gradient; + colorctx.fillRect( + 0, + 0, + colorctx.canvas.width, + colorctx.canvas.height + ); + + // Create semi transparent gradient (white -> trans. -> black) + gradient = colorctx.createLinearGradient( + 0, + 0, + 0, + $(this.refs.palette).width() + ); + gradient.addColorStop(0, "rgba(255, 255, 255, 1)"); + gradient.addColorStop(0.5, "rgba(255, 255, 255, 0)"); + gradient.addColorStop(0.5, "rgba(0, 0, 0, 0)"); + gradient.addColorStop(1, "rgba(0, 0, 0, 1)"); + // Apply gradient to canvas + colorctx.fillStyle = gradient; + colorctx.fillRect( + 0, + 0, + colorctx.canvas.width, + colorctx.canvas.height + ); + // now add mouse move event + const getHex = function (c) { + let s = c.toString(16); + if (s.length === 1) { + s = "0" + s; + } + return s; + }; + + const pick_color = (e) => { + $(this.refs.palette).css("cursor", "crosshair"); + const offset = $(this.refs.palette).offset(); + const x = e.pageX - offset.left; + const y = e.pageY - offset.top; + const color = colorctx.getImageData(x, y, 1, 1); + const data: ColorType = { + r: color.data[0], + g: color.data[1], + b: color.data[2], + text: + "rgb(" + + color.data[0] + + ", " + + color.data[1] + + ", " + + color.data[2] + + ")", + hex: + "#" + + getHex(color.data[0]) + + getHex(color.data[1]) + + getHex(color.data[2]), + }; + return data; + }; + + const mouse_move_h = (e) => { + const data = pick_color(e); + return $(this.refs.colorval).css( + "background-color", + data.text + ); + }; + + $(this.refs.palette).mouseenter((e) => { + return $(this.refs.palette).on( + "mousemove", + mouse_move_h + ); + }); + + $(this.refs.palette).mouseout((e) => { + $(this.refs.palette).unbind("mousemove", mouse_move_h); + if (this.selectedColor) { + return $(this.refs.colorval).css( + "background-color", + this.selectedColor.text + ); + } + }); + + $(this.refs.palette).on("click", (e) => { + const data = pick_color(e); + $(this.refs.rgbtext).html(data.text); + $(this.refs.hextext).val(data.hex); + this._selectedColor = data; + const evt = { id: this.aid, data }; + this._oncolorselect(evt); + return this.observable.trigger("colorselect", data); + }); + } + + /** + * layout definition of the widget + * + * @protected + * @returns {TagLayoutType[]} + * @memberof ColorPickerTag + */ + protected layout(): TagLayoutType[] { + return [ + { + el: "div", + ref: "wrapper", + children: [ + { + el: "canvas", + class: "color-palette", + ref: "palette", + }, + { el: "color-sample", ref: "colorval" }, + { el: "div", class: "afx-clear" }, + { + el: "div", + ref: "inputwrp", + children: [ + { el: "input", ref: "hextext" }, + { el: "span", ref: "rgbtext" }, + ], + }, + ], + }, + ]; + } + } + + define("afx-color-picker", ColorPickerTag); + } + } +} diff --git a/src/core/tags/FileViewTag.ts b/src/core/tags/FileViewTag.ts new file mode 100644 index 0000000..e0eac7a --- /dev/null +++ b/src/core/tags/FileViewTag.ts @@ -0,0 +1,641 @@ +namespace OS { + export namespace GUI { + export namespace tag { + /** + * Definition of system file view widget + * + * @export + * @class FileViewTag + * @extends {AFXTag} + */ + export class FileViewTag extends AFXTag { + /** + * placeholder for file select event callback + * + * @private + * @type {TagEventCallback} + * @memberof FileViewTag + */ + private _onfileselect: TagEventCallback; + + /** + * placeholder for file open event callback + * + * @private + * @type {TagEventCallback} + * @memberof FileViewTag + */ + private _onfileopen: TagEventCallback; + + /** + * Reference to the currently selected file meta-data + * + * @private + * @type {API.FileInfoType} + * @memberof FileViewTag + */ + private _selectedFile: API.FileInfoType; + + /** + * Data placeholder of the current working directory + * + * @private + * @type {API.FileInfoType[]} + * @memberof FileViewTag + */ + private _data: API.FileInfoType[]; + + /** + * The path of the current working directory + * + * @private + * @type {string} + * @memberof FileViewTag + */ + private _path: string; + + /** + * Header definition of the widget grid view + * + * @private + * @type {(GenericObject[])} + * @memberof FileViewTag + */ + private _header: GenericObject[]; + + /** + * placeholder for the user-specified meta-data fetch function + * + * @private + * @memberof FileViewTag + */ + private _fetch: (p: string) => Promise; + + /** + *Creates an instance of FileViewTag. + * @memberof FileViewTag + */ + constructor() { + super(); + } + + /** + * Init the widget before mounting + * + * @protected + * @memberof FileViewTag + */ + protected init(): void { + this.data = []; + this.status = true; + this.showhidden = false; + this.chdir = true; + this.view = "list"; + this._onfileopen = this._onfileselect = (e) => {}; + this._header = [ + { text: "__(File name)" }, + { text: "__(Type)", width: 150 }, + { text: "__(Size)", width: 70 }, + ]; + } + + /** + * Update the current widget, do nothing + * + * @protected + * @param {*} [d] + * @memberof FileViewTag + */ + protected reload(d?: any): void {} + + /** + * set the function that allows to fetch file entries. + * This handle function should return a promise on + * an arry of [[API.FileInfoType]] + * + * @memberof FileViewTag + */ + set fetch(v: (p: string) => Promise) { + this._fetch = v; + } + + /** + * set the callback handle for the file select event. + * The parameter of the callback should be an object + * of type [[TagEventType]] with the data type `T` is [[API.FileInfoType]] + * + * @memberof FileViewTag + */ + set onfileselect(e: TagEventCallback) { + this._onfileselect = e; + } + + /** + set the callback handle for the file open event. + * The parameter of the callback should be an object + * of type [[TagEventType]] with the data type `T` is [[API.FileInfoType]] + * + * @memberof FileViewTag + */ + set onfileopen(e: TagEventCallback) { + this._onfileopen = e; + } + + /** + * Setter: + * + * chang the view of the widget, there are three different views + * - `icon` + * - `list` + * - `tree` + * + * Getter: + * + * Get the current view setting of the widget + * + * @memberof FileViewTag + */ + set view(v: string) { + $(this).attr("view", v); + this.switchView(); + } + get view(): string { + return $(this).attr("view"); + } + + /** + * Setter: + * + * Turn on/off the changing current working directory feature + * of the widget when a directory is double clicked. If enabled, + * the widget will use the configured [[fetch]] function to query + * the content of the selected directory + * + * Getter: + * + * check whether changing current working directory feature + * is enabled + * + * @memberof FileViewTag + */ + set chdir(v: boolean) { + this.attsw(v, "chdir"); + } + get chdir(): boolean { + return this.hasattr("chdir"); + } + + /** + * Setter : Enable or disable the status bar of the widget + * + * Getter: Check whether the status bar is enabled + * + * @memberof FileViewTag + */ + set status(v: boolean) { + this.attsw(v, "status"); + if (v) { + $(this.refs.status).show(); + return; + } + $(this.refs.status).hide(); + } + get status(): boolean { + return this.hasattr("status"); + } + + /** + * Setter: + * + * Allow the widget to show or hide hidden file + * + * Getter: + * + * Check whether the hidden file should be shown in + * the widget + * + * @memberof FileViewTag + */ + set showhidden(v: boolean) { + this.attsw(v, "showhidden"); + if (!this.data) { + return; + } + this.switchView(); + } + get showhidden(): boolean { + return this.hasattr("showhidden"); + } + + /** + * Get the current selected file + * + * @readonly + * @type {API.FileInfoType} + * @memberof FileViewTag + */ + get selectedFile(): API.FileInfoType { + return this._selectedFile; + } + + /** + * Setter: + * + * Set the path of the current working directory. + * When called the widget will refresh the current + * working directory using the configured [[fetch]] + * function + * + * Getter: + * + * Get the path of the current working directory + * + * @memberof FileViewTag + */ + set path(v: string) { + if (!v) { + return; + } + this._path = v; + if (!this._fetch) { + return; + } + this._fetch(v) + .then((data: API.FileInfoType[]) => { + if (!data) { + return; + } + this.data = data; + if (this.status) { + (this.refs.status as LabelTag).text = " "; + } + }) + .catch((e: Error) => + announcer.oserror(e.toString(), e) + ); + } + get path(): string { + return this._path; + } + + /** + * Setter: Set the data of the current working directory + * + * Getter: Get the data of the current working directory + * + * @memberof FileViewTag + */ + set data(v: API.FileInfoType[]) { + if (!v) { + return; + } + this._data = v; + this.refreshData(); + } + get data(): API.FileInfoType[] { + return this._data; + } + + /** + * Set the file drag and drop event handle. This allows application + * to define custom behavior of the event + * + * @memberof FileViewTag + */ + set ondragndrop( + v: TagEventCallback< + DnDEventDataType + > + ) { + (this.refs.treeview as TreeViewTag).ondragndrop = v; + (this.refs.listview as ListViewTag).ondragndrop = v; + } + + /** + * sort file by it type + * + * @private + * @param {API.FileInfoType} a first file meta-data + * @param {API.FileInfoType} b second file meta-data + * @returns {(0|-1|1)} + * @memberof FileViewTag + */ + private sortByType( + a: API.FileInfoType, + b: API.FileInfoType + ): 0 | -1 | 1 { + if (a.type < b.type) { + return -1; + } else if (a.type > b.type) { + return 1; + } else { + return 0; + } + } + + /** + * calibrate the widget layout + * + * @memberof FileViewTag + */ + calibrate(): void { + let h = $(this).outerHeight(); + const w = $(this).width(); + if (this.status) { + h -= $(this.refs.status).height() + 10; + } + $(this.refs.listview).css("height", h + "px"); + $(this.refs.gridview).css("height", h + "px"); + $(this.refs.treecontainer).css("height", h + "px"); + $(this.refs.listview).css("width", w + "px"); + $(this.refs.gridview).css("width", w + "px"); + $(this.refs.treecontainer).css("width", w + "px"); + } + + /** + * Refresh the list view of the widget. This function + * is called when the view of the widget changed to `icon` + * + * @private + * @memberof FileViewTag + */ + private refreshList(): void { + const items = []; + $.each(this.data, (i, v) => { + if (v.filename[0] === "." && !this.showhidden) { + return; + } + v.text = v.filename; + if (v.text.length > 10) { + v.text = v.text.substring(0, 9) + "..."; + } + v.iconclass = v.iconclass ? v.iconclass : v.type; + v.icon = v.icon; + items.push(v); + }); + (this.refs.listview as ListViewTag).data = items; + } + + /** + * Refresh the grid view of the widget, this function is called + * when the view of the widget set to `list` + * + * @private + * @memberof FileViewTag + */ + private refreshGrid(): void { + const rows = []; + $.each(this.data, (i, v) => { + if (v.filename[0] === "." && !this.showhidden) { + return; + } + v.text = v.filename; + v.iconclass = v.iconclass ? v.iconclass : v.type; + const row = [ + v, + { + text: v.mime, + data: v, + }, + { + text: v.size, + data: v, + }, + ]; + return rows.push(row); + }); + (this.refs.gridview as GridViewTag).rows = rows; + } + + /** + * Refresh the Treeview of the widget, this function is called + * when the view of the widget set to `tree` + * + * @private + * @memberof FileViewTag + */ + private refreshTree(): void { + //@treeview.root.set("selectedItem", null) + const tdata: TreeViewDataType = { + text: this.path, + path: this.path, + open: true, + nodes: this.getTreeData(this.data), + }; + (this.refs.treeview as TreeViewTag).data = tdata; + // (this.refs.treeview as TreeViewTag).expandAll(); + } + + /** + * Create the tree data from the list of input + * file meta-data + * + * @private + * @param {API.FileInfoType[]} data list of file meta-data + * @returns {TreeViewDataType[]} + * @memberof FileViewTag + */ + private getTreeData( + data: API.FileInfoType[] + ): TreeViewDataType[] { + const nodes = []; + const me = this; + $.each(data, (i, v) => { + if (v.filename[0] === "." && !this.showhidden) { + return undefined; + } + v.text = v.filename; + if (v.type === "dir") { + v.nodes = []; + v.open = false; + } + v.iconclass = v.iconclass ? v.iconclass : v.type; + v.icon = v.icon; + return nodes.push(v); + }); + return nodes; + } + + /** + * Refresh data of the current widget view + * + * @private + * @returns {void} + * @memberof FileViewTag + */ + private refreshData(): void { + if (!this.data) { + return; + } + this.data.sort(this.sortByType); + switch (this.view) { + case "icon": + return this.refreshList(); + case "list": + return this.refreshGrid(); + default: + return this.refreshTree(); + } + } + + /** + * Switch between three view options + * + * @private + * @memberof FileViewTag + */ + private switchView(): void { + $(this.refs.listview).hide(); + $(this.refs.gridview).hide(); + $(this.refs.treecontainer).hide(); + this._selectedFile = undefined; + switch (this.view) { + case "icon": + $(this.refs.listview).show(); + break; + case "list": + $(this.refs.gridview).show(); + break; + default: + $(this.refs.treecontainer).show(); + } + this.refreshData(); + this.calibrate(); + if (this.status) { + (this.refs.status as LabelTag).text = " "; + } + } + + /** + * This function triggers the file select event + * + * @private + * @param {API.FileInfoType} e selected file meta-data + * @memberof FileViewTag + */ + private fileselect(e: API.FileInfoType): void { + if (e.path === this.path) { + e.type = "dir"; + e.mime = "dir"; + } + if (this.status) { + (this.refs.status as LabelTag).text = __( + "Selected: {0} ({1} bytes)", + e.filename, + e.size ? e.size : "0" + ); + } + const evt = { id: this.aid, data: e }; + this._selectedFile = e; + this._onfileselect(evt); + this.observable.trigger("fileselect", evt); + } + + /** + * This function triggers the file open event + * + * @private + * @param {API.FileInfoType} e selected file meta-data + * @memberof FileViewTag + */ + private filedbclick(e: API.FileInfoType): void { + if (e.path === this.path) { + e.type = "dir"; + e.mime = "dir"; + } + if (e.type === "dir" && this.chdir) { + this.path = e.path; + } else { + const evt = { id: this.aid, data: e }; + this._onfileopen(evt); + this.observable.trigger("fileopen", evt); + } + } + + /** + * Mount the widget in the DOM tree + * + * @protected + * @memberof FileViewTag + */ + protected mount(): void { + this.observable.on("resize", (e) => this.calibrate()); + const tree = this.refs.treeview as TreeViewTag; + tree.fetch = (v) => { + return new Promise((resolve, reject) => { + if (!this._fetch) { + return resolve(undefined); + } + if (!v.data.path) { + return resolve(undefined); + } + return this._fetch(v.data.path) + .then((d: API.FileInfoType[]) => + resolve( + this.getTreeData( + d.sort(this.sortByType) + ) + ) + ) + .catch((e: Error) => reject(__e(e))); + }); + }; + const grid = this.refs.gridview as GridViewTag; + const list = this.refs.listview as ListViewTag; + grid.header = this._header; + tree.dragndrop = true; + list.dragndrop = true; + // even handles + list.onlistselect = (e) => { + this.fileselect(e.data.item.data as API.FileInfoType); + }; + grid.onrowselect = (e) => { + this.fileselect( + $(e.data.item).children()[0] + .data as API.FileInfoType + ); + }; + tree.ontreeselect = (e) => { + this.fileselect(e.data.item.data as API.FileInfoType); + }; + // dblclick + list.onlistdbclick = (e) => { + this.filedbclick(e.data.item.data as API.FileInfoType); + }; + grid.oncelldbclick = (e) => { + this.filedbclick(e.data.item.data as API.FileInfoType); + }; + tree.ontreedbclick = (e) => { + this.filedbclick(e.data.item.data as API.FileInfoType); + }; + this.switchView(); + } + + /** + * Layout definition of the widget + * + * @protected + * @returns {TagLayoutType[]} + * @memberof FileViewTag + */ + protected layout(): TagLayoutType[] { + return [ + { el: "afx-list-view", ref: "listview" }, + { + el: "div", + class: "treecontainer", + ref: "treecontainer", + children: [ + { el: "afx-tree-view", ref: "treeview" }, + ], + }, + { el: "afx-grid-view", ref: "gridview" }, + { el: "afx-label", class: "status", ref: "status" }, + ]; + } + } + + define("afx-file-view", FileViewTag); + } + } +} diff --git a/src/core/tags/FloatListTag.ts b/src/core/tags/FloatListTag.ts new file mode 100644 index 0000000..903bc25 --- /dev/null +++ b/src/core/tags/FloatListTag.ts @@ -0,0 +1,238 @@ +namespace OS { + export namespace GUI { + export namespace tag { + /** + * A float list is a list of items in which each + * item can be moved (drag and drop) freely + * + * @export + * @class FloatListTag + * @extends {ListViewTag} + */ + export class FloatListTag extends ListViewTag { + /** + * Update the current tag, do nothing + * + * @protected + * @param {*} [d] + * @memberof FloatListTag + */ + protected reload(d?: any): void {} + + /** + * Variable that hold the onready callback of + * the tag. This callback will be called after + * the tag is mounted + * + * @private + * @memberof FloatListTag + */ + private _onready: (e: FloatListTag) => void; + + /** + *Creates an instance of FloatListTag. + * @memberof FloatListTag + */ + constructor() { + super(); + } + + /** + * set the onready callback function to the tag. + * This callback will be called after + * the tag is mounted + * + * @memberof FloatListTag + */ + set onready(v: (e: FloatListTag) => void) { + this._onready = v; + } + + /** + * Setter: + * + * Set the direction of the list item layout. + * Two directions are available: + * - `vertical` + * - `horizontal` + * + * This setter acts as a DOM attribute + * + * Getter: + * + * Get the currently set direction of list + * item layout + * + * @memberof FloatListTag + */ + set dir(v: string) { + $(this).attr("dir", v); + this.calibrate(); + } + get dir(): string { + return $(this).attr("dir"); + } + + /** + * Disable the dropdown option in this list + * + * @memberof FloatListTag + */ + set dropdown(v: boolean) {} + + /** + * Disable the list buttons configuration in this + * list + * + * @memberof FloatListTag + */ + set buttons(v: GenericObject[]) {} + + /** + * Disable the `showlist` behavior in this list + * + * @protected + * @param {*} e + * @memberof FloatListTag + */ + protected showlist(e: any) {} + + /** + * Disable the `dropoff` behavior in this list + * + * @protected + * @param {*} e + * @memberof FloatListTag + */ + protected dropoff(e: any) {} + + /** + * Function called when the data of the list + * is changed + * + * @protected + * @memberof FloatListTag + */ + protected ondatachange(): void { + this.calibrate(); + } + + /** + * Mount the list to the DOM tree + * + * @protected + * @returns {void} + * @memberof FloatListTag + */ + protected mount(): void { + $(this.refs.container) + .css("width", "100%") + .css("height", "100%"); + $(this.refs.mlist) + .css("position", "absolute") + .css("display", "block") + .css("width", "100%"); + this.observable.on("resize", (e) => this.calibrate()); + if (this._onready) { + return this._onready(this); + } + } + + /** + * Push an element to the list + * + * @param {GenericObject} v an element data + * @returns + * @memberof FloatListTag + */ + push(v: GenericObject) { + const el = super.push(v); + this.enable_drag(el); + return el; + } + + /** + * Enable drag and drop on the list + * + * @private + * @param {ListViewItemTag} el the list item DOM element + * @memberof FloatListTag + */ + private enable_drag(el: ListViewItemTag): void { + $(el) + .css("user-select", "none") + .css("cursor", "default") + .css("display", "block") + .css("position", "absolute") + .on("mousedown", (evt) => { + const globalof = $(this.refs.mlist).offset(); + evt.preventDefault(); + const offset = $(el).offset(); + offset.top = evt.clientY - offset.top; + offset.left = evt.clientX - offset.left; + const mouse_move = function ( + e: JQuery.MouseEventBase + ) { + let top = e.clientY - offset.top - globalof.top; + let left = + e.clientX - globalof.left - offset.left; + left = left < 0 ? 0 : left; + top = top < 0 ? 0 : top; + return $(el) + .css("top", `${top}px`) + .css("left", `${left}px`); + }; + + var mouse_up = function (e: JQuery.MouseEventBase) { + $(window).unbind("mousemove", mouse_move); + return $(window).unbind("mouseup", mouse_up); + }; + $(window).on("mousemove", mouse_move); + return $(window).on("mouseup", mouse_up); + }); + } + + /** + * Calibrate the view of the list + * + * @memberof FloatListTag + */ + calibrate(): void { + let ctop = 20; + let cleft = 20; + $(this.refs.mlist).css( + "height", + `${$(this.refs.container).height()}px` + ); + const gw = $(this.refs.mlist).width(); + const gh = $(this.refs.mlist).height(); + + $(this.refs.mlist) + .children() + .each((i, e) => { + $(e) + .css("top", `${ctop}px`) + .css("left", `${cleft}px`); + const w = $(e).width(); + const h = $(e).height(); + if (this.dir === "vertical") { + ctop += h + 20; + if (ctop > gh) { + ctop = 20; + cleft += w + 20; + } + } else { + cleft += w + 20; + if (cleft > gw) { + cleft = 20; + ctop += h + 20; + } + } + }); + } + } + + define("afx-float-list", FloatListTag); + } + } +} diff --git a/src/core/tags/GridViewTag.ts b/src/core/tags/GridViewTag.ts new file mode 100644 index 0000000..1336fe0 --- /dev/null +++ b/src/core/tags/GridViewTag.ts @@ -0,0 +1,896 @@ +/** + * Extend the Array interface with some + * property needed by AFX API + * + * @interface Array + * @template T + */ +interface Array { + /** + * Reference to a DOM element created by AFX API, + * this property is used by some AFX tags to refer + * to its child element in it data object + * + * @type {GenericObject} + * @memberof Array + */ + domel?: GenericObject; +} +namespace OS { + export namespace GUI { + export namespace tag { + /** + * A grid Row is a simple element that + * contains a group of grid cell + * + * @export + * @class GridRowTag + * @extends {AFXTag} + */ + export class GridRowTag extends AFXTag { + /** + * Data placeholder for a collection of cell data + * + * @type {GenericObject[]} + * @memberof GridRowTag + */ + data: GenericObject[]; + + /** + *Creates an instance of GridRowTag. + * @memberof GridRowTag + */ + constructor() { + super(); + + this.refs.yield = this; + } + + /** + * Mount the tag, do nothing + * + * @protected + * @memberof GridRowTag + */ + protected mount(): void {} + + /** + * Init the tag before mounting: reset the data placeholder + * + * @protected + * @memberof GridRowTag + */ + protected init(): void { + this.data = []; + } + + /** + * Empty layout + * + * @protected + * @returns {TagLayoutType[]} + * @memberof GridRowTag + */ + protected layout(): TagLayoutType[] { + return []; + } + + /** + * This function does nothing in this tag + * + * @protected + * @memberof GridRowTag + */ + protected calibrate(): void {} + + /** + * This function does nothing in this tag + * + * @protected + * @param {*} [d] + * @memberof GridRowTag + */ + protected reload(d?: any): void {} + } + + /** + * Event data used by grid cell + */ + export type CellEventData = TagEventDataType; + + /** + * Prototype of any grid cell, custom grid cell + * definition should extend and implement this + * abstract prototype + * + * @export + * @abstract + * @class GridCellPrototype + * @extends {AFXTag} + */ + export abstract class GridCellPrototype extends AFXTag { + /** + * placeholder for cell selected event callback + * + * @private + * @type {TagEventCallback} + * @memberof GridCellPrototype + */ + private _oncellselect: TagEventCallback; + + /** + * placeholder for cell double click event callback + * + * @private + * @type {TagEventCallback} + * @memberof GridCellPrototype + */ + private _oncelldbclick: TagEventCallback; + + /** + * Data placeholder of the current cell + * + * @private + * @type {GenericObject} + * @memberof GridCellPrototype + */ + private _data: GenericObject; + + /** + *Creates an instance of GridCellPrototype. + * @memberof GridCellPrototype + */ + constructor() { + super(); + } + + /** + * Set the cell selected event callback + * + * @memberof GridCellPrototype + */ + set oncellselect(v: TagEventCallback) { + this._oncellselect = v; + } + + /** + * Set the cell double click event callback + * + * @memberof GridCellPrototype + */ + set oncelldbclick(v: TagEventCallback) { + this._oncelldbclick = v; + } + + /** + * Setter: + * + * Set the data of the cell, this will trigger + * the [[ondatachange]] function + * + * Getter: + * + * Get the current cell data placeholder + * + * @memberof GridCellPrototype + */ + set data(v: GenericObject) { + if (!v) return; + this._data = v; + this.ondatachange(); + if (!v.selected) { + return; + } + this.selected = v.selected; + } + get data(): GenericObject { + return this._data; + } + + /** + * Setter: + * + * Set/unset the current cell as selected. + * This will trigger the [[cellselect]] + * event + * + * Getter: + * + * Check whether the current cell is selected + * + * @memberof GridCellPrototype + */ + set selected(v: boolean) { + this.attsw(v, "selected"); + if (this._data) this._data.selected = v; + if (!v) { + return; + } + this.cellselect({ id: this.aid, data: this }, false); + } + get selected(): boolean { + return this.hasattr("selected"); + } + + /** + * Update the current cell. This will + * reset the cell data + * + * @protected + * @param {*} d + * @memberof GridCellPrototype + */ + protected reload(d: any): void { + this.data = this.data; + } + + /** + * Mount the current cell to the grid + * + * @protected + * @memberof GridCellPrototype + */ + protected mount(): void { + $(this).attr("class", "afx-grid-cell"); + this.oncelldbclick = this.oncellselect = ( + e: TagEventType + ): void => {}; + this.selected = false; + $(this).css("display", "block"); + $(this).click((e) => { + let evt = { id: this.aid, data: this }; + return this.cellselect(evt, false); + }); + $(this).dblclick((e) => { + let evt = { id: this.aid, data: this }; + return this.cellselect(evt, true); + }); + } + + /** + * This function triggers the cell select + * event + * + * @private + * @param {TagEventType} e + * @param {boolean} flag + * @returns {void} + * @memberof GridCellPrototype + */ + private cellselect( + e: TagEventType, + flag: boolean + ): void { + const evt = { id: this.aid, data: { item: e.data } }; + if (!flag) { + return this._oncellselect(evt); + } + return this._oncelldbclick(evt); + } + + /** + * Abstract function called when the cell data changed. + * This should be implemented by subclasses + * + * @protected + * @abstract + * @memberof GridCellPrototype + */ + protected abstract ondatachange(): void; + } + + /** + * Simple grid cell defines a grid cell with + * an [[LabelTag]] as it cell layout + * + * @export + * @class SimpleGridCellTag + * @extends {GridCellPrototype} + */ + export class SimpleGridCellTag extends GridCellPrototype { + /** + *Creates an instance of SimpleGridCellTag. + * @memberof SimpleGridCellTag + */ + constructor() { + super(); + } + + /** + * Reset the label of the cell with its data + * + * @protected + * @memberof SimpleGridCellTag + */ + protected ondatachange(): void { + (this.refs.cell as LabelTag).set(this.data); + } + + /** + * This function do nothing in this tag + * + * @protected + * @memberof SimpleGridCellTag + */ + protected init(): void {} + + /** + * This function do nothing in this tag + * + * @protected + * @memberof SimpleGridCellTag + */ + protected calibrate(): void {} + + /** + * The layout of the cell with a simple [[LabelTag]] + * + * @returns + * @memberof SimpleGridCellTag + */ + layout() { + return [ + { + el: "afx-label", + ref: "cell", + }, + ]; + } + } + + /** + * A Grid contains a header and a collection grid rows + * which has the same number of cells as the number of + * the header elements + * + * @export + * @class GridViewTag + * @extends {AFXTag} + */ + export class GridViewTag extends AFXTag { + /** + * Grid header definition + * + * @private + * @type {GenericObject[]} + * @memberof GridViewTag + */ + private _header: GenericObject[]; + + /** + * Grid rows data placeholder + * + * @private + * @type {GenericObject[][]} + * @memberof GridViewTag + */ + private _rows: GenericObject[][]; + + /** + * Reference to the current selected row DOM element + * + * @private + * @type {GridRowTag} + * @memberof GridViewTag + */ + private _selectedRow: GridRowTag; + + /** + * A collection of selected grid rows DOM element + * + * @private + * @type {GridRowTag[]} + * @memberof GridViewTag + */ + private _selectedRows: GridRowTag[]; + + /** + * Reference to the current selected cell + * + * @private + * @type {GridCellPrototype} + * @memberof GridViewTag + */ + private _selectedCell: GridCellPrototype; + + /** + * Cell select event callback placeholder + * + * @private + * @type {TagEventCallback} + * @memberof GridViewTag + */ + private _oncellselect: TagEventCallback; + + /** + * Row select event callback placeholder + * + * @private + * @type {TagEventCallback} + * @memberof GridViewTag + */ + private _onrowselect: TagEventCallback; + + /** + * Cell double click event callback placeholder + * + * @private + * @type {TagEventCallback} + * @memberof GridViewTag + */ + private _oncelldbclick: TagEventCallback; + + /** + * Creates an instance of GridViewTag. + * @memberof GridViewTag + */ + constructor() { + super(); + } + + /** + * Init the grid view before mounting. + * Reset all the placeholders to default values + * + * @protected + * @memberof GridViewTag + */ + protected init(): void { + this._header = []; + this.headeritem = "afx-grid-cell"; + this.cellitem = "afx-grid-cell"; + this._selectedCell = undefined; + this._selectedRows = []; + this._selectedRow = undefined; + this._rows = []; + this._oncellselect = this._onrowselect = this._oncelldbclick = ( + e: TagEventType + ): void => {}; + } + + /** + * This function does nothing + * + * @protected + * @param {*} [d] + * @memberof GridViewTag + */ + protected reload(d?: any): void {} + + /** + * set the cell select event callback + * + * @memberof GridViewTag + */ + set oncellselect(v: TagEventCallback) { + this._oncellselect = v; + } + + /** + * set the row select event callback + * + * @memberof GridViewTag + */ + set onrowselect(v: TagEventCallback) { + this._onrowselect = v; + } + + /** + * set the cell double click event callback + * + * @memberof GridViewTag + */ + set oncelldbclick(v: TagEventCallback) { + this._oncelldbclick = v; + } + + /** + * Setter: set the tag name of the header cells + * + * Getter: get the grid header tag name + * + * @memberof GridViewTag + */ + set headeritem(v: string) { + $(this).attr("headeritem", v); + } + get headeritem(): string { + return $(this).attr("headeritem"); + } + + /** + * Setter: set the tag name of the grid cell + * + * Getter: get the tag name of the grid cell + * + * @memberof GridViewTag + */ + set cellitem(v: string) { + $(this).attr("cellitem", v); + } + get cellitem(): string { + return $(this).attr("cellitem"); + } + + /** + * Setter: set the header data + * + * Getter: get the header data placeholder + * + * @type {GenericObject[]} + * @memberof GridViewTag + */ + get header(): GenericObject[] { + return this._header; + } + set header(v: GenericObject[]) { + this._header = v; + if (!v || v.length === 0) { + $(this.refs.header).hide(); + return; + } + $(this.refs.header).empty(); + for (let item of v) { + const el = $(`<${this.headeritem}>`).appendTo( + this.refs.header + ); + const element = el[0] as GridCellPrototype; + element.uify(this.observable); + element.data = item; + item.domel = element; + } + this.calibrate(); + } + + /** + * Get all the selected rows + * + * @readonly + * @type {GridRowTag[]} + * @memberof GridViewTag + */ + get selectedRows(): GridRowTag[] { + return this._selectedRows; + } + + /** + * Get the latest selected row + * + * @readonly + * @type {GridRowTag} + * @memberof GridViewTag + */ + get selectedRow(): GridRowTag { + return this._selectedRow; + } + + /** + * Get the current selected cell + * + * @readonly + * @type {GridCellPrototype} + * @memberof GridViewTag + */ + get selectedCell(): GridCellPrototype { + return this._selectedCell; + } + + /** + * Setter: set the rows data + * + * Getter: get the rows data + * + * @memberof GridViewTag + */ + set rows(rows: GenericObject[][]) { + $(this.refs.grid).empty(); + this._rows = rows; + rows.map((row) => this.push(row, false)); + } + get rows(): GenericObject[][] { + return this._rows; + } + + /** + * Setter: activate deactivate multi-select + * + * Getter: check whether the `multiselect` option is activated + * + * @memberof GridViewTag + */ + set multiselect(v: boolean) { + this.attsw(v, "multiselect"); + } + get multiselect(): boolean { + return this.hasattr("multiselect"); + } + + /** + * Delete a grid rows + * + * @param {GridRowTag} row row DOM element + * @returns {void} + * @memberof GridViewTag + */ + delete(row: GridRowTag): void { + if (!row) { + return; + } + const rowdata = row.data; + const data = this.rows; + if (this.selectedRow === row) { + this._selectedRow = undefined; + } + let parentRow: any = $(this.selectedCell).parent()[0]; + if ((parentRow as GridRowTag) === row) { + this._selectedCell = undefined; + } + const list = this.selectedRows; + if (list.includes(row)) { + list.splice(list.indexOf(row), 1); + } + if (data.includes(rowdata)) { + data.splice(data.indexOf(rowdata), 1); + } + $(row).remove(); + } + + /** + * Push a row to the grid + * + * @param {GenericObject[]} row list of cell data + * @param {boolean} flag indicates where the row is add to beginning or end + * of the row + * @memberof GridViewTags + */ + push(row: GenericObject[], flag: boolean): void { + const rowel = $("").css( + "display", + "contents" + ); + if (flag) { + $(this.refs.grid).prepend(rowel[0]); + if (!this.rows.includes(row)) { + this.rows.unshift(row); + } + } else { + rowel.appendTo(this.refs.grid); + if (!this.rows.includes(row)) { + this.rows.push(row); + } + } + + const el = rowel[0] as GridRowTag; + rowel[0].uify(this.observable); + el.data = row; + row.domel = rowel[0]; + + for (let cell of row) { + let tag = this.cellitem; + if (cell.tag) { + ({ tag } = cell); + } + const el = $(`<${tag}>`).appendTo(rowel); + cell.domel = el[0]; + const element = el[0] as GridCellPrototype; + element.uify(this.observable); + element.oncellselect = (e) => this.cellselect(e, false); + element.oncelldbclick = (e) => this.cellselect(e, true); + element.data = cell; + } + } + + /** + * Unshift a row to the grid + * + * @param {GenericObject[]} row list of cell data in the row + * @memberof GridViewTag + */ + unshift(row: GenericObject[]): void { + this.push(row, true); + } + + /** + * This function triggers the cell select event + * + * @private + * @param {TagEventType} e event contains cell event data + * @param {boolean} flag indicates whether the event is double clicked + * @returns {void} + * @memberof GridViewTag + */ + private cellselect( + e: TagEventType, + flag: boolean + ): void { + e.id = this.aid; + // return if e.data.item is selectedCell and not flag + if (this.selectedCell) { + $(this.selectedCell).attr("class", "afx-grid-cell"); + } + this._selectedCell = e.data.item; + $(e.data.item).addClass("afx-grid-cell-selected"); + if (flag) { + this.observable.trigger("celldbclick", e); + return this._oncelldbclick(e); + } else { + this.observable.trigger("cellselect", e); + this._oncellselect(e); + return this.rowselect(e); + } + } + + /** + * This function triggers the row select event, a cell select + * event will also trigger this event + * + * @param {TagEventType} e + * @returns {void} + * @memberof GridViewTag + */ + private rowselect(e: TagEventType): void { + if (!e.data.item) { + return; + } + const evt = { + id: this.aid, + data: { + item: undefined, + items: [], + }, + }; + const row = ($( + e.data.item + ).parent()[0] as any) as GridRowTag; + if (this.multiselect) { + if (this.selectedRows.includes(row)) { + this.selectedRows.splice( + this.selectedRows.indexOf(row), + 1 + ); + $(row).removeClass(); + } else { + this.selectedRows.push(row); + $(row) + .removeClass() + .addClass("afx-grid-row-selected"); + } + evt.data.items = this.selectedRows; + } else { + if (this.selectedRow === row) { + return; + } + $(this.selectedRow).removeClass(); + this._selectedRows = [row]; + evt.data.item = row; + evt.data.items = [row]; + $(row).removeClass().addClass("afx-grid-row-selected"); + this._selectedRows = [row]; + } + this._selectedRow = row; + this._onrowselect(evt); + return this.observable.trigger("rowselect", evt); + } + + /** + * Check whether the grid has header + * + * @private + * @returns {boolean} + * @memberof GridViewTag + */ + private has_header(): boolean { + const h = this._header; + return h && h.length > 0; + } + + /** + * Calibrate the grid + * + * @protected + * @memberof GridViewTag + */ + protected calibrate(): void { + this.calibrate_header(); + if (this.has_header()) { + $(this.refs.container).css( + "height", + $(this).height() - + $(this.refs.header).height() + + "px" + ); + } else { + $(this.refs.container).css( + "height", + $(this).height() + "px" + ); + } + } + + /** + * Recalculate the size of each header cell, changing + * in header cell size will also resize the entire + * related column + * + * @private + * @returns {void} + * @memberof GridViewTag + */ + private calibrate_header(): void { + if (!this.has_header()) { + return; + } + const colssize = []; + let ocw = 0; + let nauto = 0; + const totalw = $(this).parent().width(); + $.each(this._header, function (i, item) { + if (item.width) { + colssize.push(item.width); + return (ocw += item.width); + } else { + colssize.push(-1); + return nauto++; + } + }); + if (nauto > 0) { + const cellw = Math.round((totalw - ocw) / nauto); + $.each(colssize, function (i, e) { + if (e !== -1) { + return; + } + return (colssize[i] = cellw); + }); + } + let template = ""; + for (let v of colssize) { + template += `${v}px `; + } + $(this.refs.grid).css("grid-template-columns", template); + $(this.refs.header).css("grid-template-columns", template); + } + + /** + * Mount the grid view tag + * + * @protected + * @returns {void} + * @memberof GridViewTag + */ + protected mount(): void { + $(this).css("overflow", "hidden"); + + $(this.refs.grid).css("display", "grid"); + $(this.refs.header).css("display", "grid"); + this.observable.on("resize", (e) => this.calibrate()); + $(this.refs.container) + .css("width", "100%") + .css("overflow-x", "hidden") + .css("overflow-y", "auto"); + return this.calibrate(); + } + + /** + * Layout definition of the grid view + * + * @protected + * @returns {TagLayoutType[]} + * @memberof GridViewTag + */ + protected layout(): TagLayoutType[] { + return [ + { el: "div", ref: "header", class: "grid_row_header" }, + { + el: "div", + ref: "container", + children: [{ el: "div", ref: "grid" }], + }, + ]; + } + } + define("afx-grid-view", GridViewTag); + define("afx-grid-cell", SimpleGridCellTag); + define("afx-grid-row", GridRowTag); + } + } +} diff --git a/src/core/tags/LabelTag.ts b/src/core/tags/LabelTag.ts new file mode 100644 index 0000000..1a87414 --- /dev/null +++ b/src/core/tags/LabelTag.ts @@ -0,0 +1,149 @@ +namespace OS { + export namespace GUI { + export namespace tag { + /** + * This class defines basic AFX label tag. + * A label contains a text and an icon (optional) + * + * @export + * @class LabelTag + * @extends {AFXTag} + */ + export class LabelTag extends AFXTag { + /** + * placeholder of the text to be displayed + * + * @private + * @type {(string | FormattedString)} + * @memberof LabelTag + */ + private _text: string | FormattedString; + + /** + *Creates an instance of LabelTag. + * @memberof LabelTag + */ + constructor() { + super(); + } + + /** + * this implementation does nothing in this tag + * + * @protected + * @memberof LabelTag + */ + protected mount() {} + + /** + * Refresh the text in the label + * + * @protected + * @param {*} d + * @memberof LabelTag + */ + protected reload(d: any): void { + this.text = this.text; + } + + /** + * Reset to default some property value + * + * @protected + * @memberof LabelTag + */ + protected init(): void { + this.icon = undefined; + this.iconclass = undefined; + this.text = undefined; + } + + /** + * This implementation of the function does nothing + * + * @protected + * @memberof LabelTag + */ + protected calibrate(): void {} + + /** + * Set the VFS path of the label icon + * + * @memberof LabelTag + */ + set icon(v: string) { + $(this.refs.i).attr("style", ""); + $(this).attr("icon", v); + if (v) { + $(this.refs.i) + .css("background", `url(${API.handle.get}/${v})`) + .css("background-size", "100% 100%") + .css("background-repeat", "no-repeat"); + $(this.refs.i).show(); + } else { + $(this.refs.i).hide(); + } + } + + /** + * Set the CSS class of the label icon + * + * @memberof LabelTag + */ + set iconclass(v: string) { + $(this).attr("iconclass", v); + $(this.refs.iclass).removeClass(); + if (v) { + $(this.refs.iclass).addClass(v); + $(this.refs.iclass).show(); + } else { + $(this.refs.iclass).hide(); + } + } + + /** + * Setter: Set the text of the label + * + * Getter: Get the text displayed on the label + * + * @memberof LabelTag + */ + set text(v: string | FormattedString) { + this._text = v; + if (v && v !== "") { + $(this.refs.text).show(); + $(this.refs.text).html(v.__()); + } else { + $(this.refs.text).hide(); + } + } + get text(): string | FormattedString { + return this._text; + } + + /** + * Lqbel layout definition + * + * @protected + * @returns {TagLayoutType[]} + * @memberof LabelTag + */ + protected layout(): TagLayoutType[] { + return [ + { + el: "span", + ref: "container", + children: [ + { el: "i", ref: "iclass" }, + { el: "i", ref: "i", class: "icon-style" }, + { el: "i", ref: "text", class: "label-text" }, + ], + }, + ]; + } + } + + define("afx-label", LabelTag); + } + } +} diff --git a/src/core/tags/ListViewTag.ts b/src/core/tags/ListViewTag.ts new file mode 100644 index 0000000..ca611f2 --- /dev/null +++ b/src/core/tags/ListViewTag.ts @@ -0,0 +1,1220 @@ +namespace OS { + export namespace GUI { + export namespace tag { + /** + * List item event data type + */ + export type ListItemEventData = TagEventDataType; + /** + * A list item represent the individual view of an item in the [[ListView]]. + * This class is an abstract prototype class, implementation of any + * list view item should extend it + * + * + * @export + * @abstract + * @class ListViewItemTag + * @extends {AFXTag} + */ + export abstract class ListViewItemTag extends AFXTag { + /** + * Data placeholder for the list item + * + * @private + * @type {GenericObject} + * @memberof ListViewItemTag + */ + private _data: GenericObject; + + /** + * placeholder for the item select event callback + * + * @private + * @type {TagEventCallback} + * @memberof ListViewItemTag + */ + private _onselect: TagEventCallback; + + /** + * Context menu event callback handle + * + * @private + * @type {TagEventCallback} + * @memberof ListViewItemTag + */ + private _onctxmenu: TagEventCallback; + + /** + * Click event callback placeholder + * + * @private + * @type {TagEventCallback} + * @memberof ListViewItemTag + */ + private _onclick: TagEventCallback; + + /** + * Double click event callback handle + * + * @private + * @type {TagEventCallback} + * @memberof ListViewItemTag + */ + private _ondbclick: TagEventCallback; + + /** + * Item close event callback placeholder + * + * @private + * @type {TagEventCallback} + * @memberof ListViewItemTag + */ + private _onclose: TagEventCallback; + + /** + *Creates an instance of ListViewItemTag. + * @memberof ListViewItemTag + */ + constructor() { + super(); + this._onselect = this._onctxmenu = this._onclick = this._ondbclick = this._onclose = ( + e + ) => {}; + } + + /** + * Setter: Turn on/off the `closable` feature of the list item + * + * Getter: Check whether the item is closable + * + * @memberof ListViewItemTag + */ + set closable(v: boolean) { + this.attsw(v, "closable"); + if (v) { + $(this.refs.btcl).show(); + } else { + $(this.refs.btcl).hide(); + } + } + get closable(): boolean { + return this.hasattr("closable"); + } + /** + * Set item select event handle + * + * @memberof ListViewItemTag + */ + set onitemselect(v: TagEventCallback) { + this._onselect = v; + } + + /** + * Setter: select/unselect the current item + * + * Getter: Check whether the current item is selected + * + * @memberof ListViewItemTag + */ + set selected(v: boolean) { + this.attsw(v, "selected"); + $(this.refs.item).removeClass(); + this._data.selected = v; + if (!v) { + return; + } + $(this.refs.item).addClass("selected"); + this._onselect({ id: this.aid, data: this }); + } + get selected(): boolean { + return this.hasattr("selected"); + } + /** + * Set the context menu event handle + * + * @memberof ListViewItemTag + */ + set onctxmenu(v: TagEventCallback) { + this._onctxmenu = v; + } + + /** + * Set the item click event handle + * + * @memberof ListViewItemTag + */ + set onitemclick(v: TagEventCallback) { + this._onclick = v; + } + + /** + * Set the item double click event handle + * + * @memberof ListViewItemTag + */ + set onitemdbclick(v: TagEventCallback) { + this._ondbclick = v; + } + + /** + * set the item close event handle + * + * @memberof ListViewItemTag + */ + set onitemclose(v: TagEventCallback) { + this._onclose = v; + } + + /** + * Mount the tag and bind some events + * + * @protected + * @memberof ListViewItemTag + */ + protected mount(): void { + $(this.refs.item).attr("dataref", "afx-list-item"); + $(this.refs.item).contextmenu((e) => { + this._onctxmenu({ id: this.aid, data: this }); + }); + + $(this.refs.item).click((e) => { + this._onclick({ id: this.aid, data: this }); + }); + + $(this.refs.item).dblclick((e) => { + this._ondbclick({ id: this.aid, data: this }); + }); + $(this.refs.btcl).click((e) => { + this._onclose({ id: this.aid, data: this }); + e.preventDefault(); + e.stopPropagation(); + }); + } + + /** + * Layout definition of the item tag. + * This function define the outer layout of the item. + * Custom inner layout of each item implementation should + * be defined in [[itemlayout]] + * + * @protected + * @returns {TagLayoutType[]} + * @memberof ListViewItemTag + */ + protected layout(): TagLayoutType[] { + return [ + { + el: "li", + ref: "item", + children: [ + this.itemlayout(), + { el: "i", class: "closable", ref: "btcl" }, + ], + }, + ]; + } + + /** + * Setter: + * + * Set the data of the list item. This will + * trigger the [[ondatachange]] function + * + * Getter: + * + * Get the data of the current list item + * + * @memberof ListViewItemTag + */ + set data(v: GenericObject) { + this._data = v; + this.ondatachange(); + } + get data(): GenericObject { + return this._data; + } + + /** + * Any subclass of this class should implement this + * function to provide its custom item layout + * + * @protected + * @abstract + * @returns {TagLayoutType} + * @memberof ListViewItemTag + */ + protected abstract itemlayout(): TagLayoutType; + + /** + * This function is called when the item data is changed. + * It should be implemented in all subclass of this class + * + * @protected + * @abstract + * @memberof ListViewItemTag + */ + protected abstract ondatachange(): void; + } + + /** + * The layout of a simple list item contains only a + * AFX label + * + * @export + * @class SimpleListItemTag + * @extends {ListViewItemTag} + */ + export class SimpleListItemTag extends ListViewItemTag { + /** + *Creates an instance of SimpleListItemTag. + * @memberof SimpleListItemTag + */ + constructor() { + super(); + } + + /** + * Reset some property to default + * + * @protected + * @memberof SimpleListItemTag + */ + protected init(): void { + this.closable = false; + this.data = {}; + } + + /** + * Do nothing + * + * @protected + * @memberof SimpleListItemTag + */ + protected calibrate(): void {} + + /** + * Refresh the inner label when the item data + * is changed + * + * @protected + * @returns {void} + * @memberof SimpleListItemTag + */ + protected ondatachange(): void { + const v = this.data; + if (!v) { + return; + } + const label = this.refs.label as LabelTag; + label.set(v); + if (v.selected) { + this.selected = v.selected; + } + if (v.closable) { + this.closable = v.closable; + } + } + + /** + * Re-render the list item + * + * @protected + * @memberof SimpleListItemTag + */ + protected reload(): void { + this.data = this.data; + } + + /** + * List item custom layout definition + * + * @protected + * @returns {TagLayoutType} + * @memberof SimpleListItemTag + */ + protected itemlayout(): TagLayoutType { + return { el: "afx-label", ref: "label" }; + } + } + + /** + * This tag defines a traditional or a dropdown list widget. + * It contains a collection of list items in which layout + * of each item may be variable + * + * @export + * @class ListViewTag + * @extends {AFXTag} + */ + export class ListViewTag extends AFXTag { + /** + * placeholder of list select event handle + * + * @private + * @type {TagEventCallback} + * @memberof ListViewTag + */ + private _onlistselect: TagEventCallback; + + /** + * placeholder of list double click event handle + * + * @private + * @type {TagEventCallback} + * @memberof ListViewTag + */ + private _onlistdbclick: TagEventCallback; + + /** + * placeholder of list drag and drop event handle + * + * @private + * @type {TagEventCallback>} + * @memberof ListViewTag + */ + private _ondragndrop: TagEventCallback< + DnDEventDataType + >; + + /** + * placeholder of list item close event handle + * + * @private + * @memberof ListViewTag + */ + private _onitemclose: ( + e: TagEventType + ) => boolean; + + /** + * placeholder of drag and drop mouse down event handle + * + * @private + * @memberof ListViewTag + */ + private _onmousedown: (e: JQuery.MouseEventBase) => void; + + /** + * placeholder of drag and drop mouse up event handle + * + * @private + * @memberof ListViewTag + */ + private _onmouseup: (e: JQuery.MouseEventBase) => void; + + /** + * placeholder of drag and drop mouse move event handle + * + * @private + * @memberof ListViewTag + */ + private _onmousemove: (e: JQuery.MouseEventBase) => void; + + /** + * Reference to the latest selected DOM item + * + * @private + * @type {ListViewItemTag} + * @memberof ListViewTag + */ + private _selectedItem: ListViewItemTag; + + /** + * A collection of selected items in the list. + * The maximum size of this collection is 1 if + * the [[multiselect]] feature is disabled + * + * @private + * @type {ListViewItemTag[]} + * @memberof ListViewTag + */ + private _selectedItems: ListViewItemTag[]; + + /** + * Data placeholder of the list + * + * @private + * @type {GenericObject[]} + * @memberof ListViewTag + */ + private _data: GenericObject[]; + + /** + * Event data passing between mouse event when performing + * drag and drop on the list + * + * @private + * @type {{ from: ListViewItemTag; to: ListViewItemTag }} + * @memberof ListViewTag + */ + private _dnd: { from: ListViewItemTag; to: ListViewItemTag }; + + /** + *Creates an instance of ListViewTag. + * @memberof ListViewTag + */ + constructor() { + super(); + this._onlistdbclick = this._onlistselect = this._ondragndrop = ( + e: TagEventType + ) => {}; + this._onitemclose = ( + e: TagEventType + ) => { + return true; + }; + this._onmousedown = this._onmouseup = this._onmousemove = ( + e: JQuery.MouseEventBase + ) => {}; + this._selectedItems = []; + this._selectedItem = undefined; + } + + /** + * Reset the tag's properties to the default values + * + * @protected + * @memberof ListViewTag + */ + protected init(): void { + this.data = []; + this.multiselect = false; + this.dropdown = false; + this.selected = -1; + this.dragndrop = false; + $(this) + .css("display", "flex") + .css("flex-direction", "column"); + this.itemtag = "afx-list-item"; + } + + /** + * This function does nothing + * + * @protected + * @param {*} [d] + * @memberof ListViewTag + */ + protected reload(d?: any): void {} + + /** + * Setter: toggle between dropdown and traditional list + * + * Getter: Check whether the list is dropdown or traditional list + * + * @memberof ListViewTag + */ + set dropdown(v: boolean) { + this.attsw(v, "dropdown"); + $(this.refs.container).removeAttr("style"); + $(this.refs.mlist).removeAttr("style"); + $(this.refs.container).css("flex", 1); + $(this).removeClass("dropdown"); + const drop = (e: any) => { + return this.dropoff(e); + }; + const show = (e: any) => { + return this.showlist(e); + }; + if (v) { + $(this).addClass("dropdown"); + $(this.refs.current).show(); + $(document).on("click", drop); + $(this.refs.current).on("click", show); + $(this.refs.container) + .css("position", "absolute") + .css("display", "inline-block"); + $(this.refs.mlist) + .css("position", "absolute") + .css("display", "none") + .css("top", "100%") + .css("left", "0"); + this.calibrate(); + } else { + $(this.refs.current).hide(); + $(document).off("click", drop); + $(this.refs.current).off("click", show); + } + } + + /** + * Set drag and drop event handle + * + * @memberof ListViewTag + */ + set ondragndrop( + v: TagEventCallback> + ) { + this._ondragndrop = v; + } + + /** + * Set list select event handle + * + * @memberof ListViewTag + */ + set onlistselect(v: TagEventCallback) { + this._onlistselect = v; + } + + /** + * Set double click event handle + * + * @memberof ListViewTag + */ + set onlistdbclick(v: TagEventCallback) { + this._onlistdbclick = v; + } + + /** + * Set item close event handle + * + * @memberof ListViewTag + */ + set onitemclose( + v: (e: TagEventType) => boolean + ) { + this._onitemclose = v; + } + + get dropdown(): boolean { + return this.hasAttribute("dropdown"); + } + + /** + * Setter: + * + * Set the default tag name of list's items. + * If the tag name is not specified in the + * data of a list item, this tag will be used + * + * Getter: + * + * Get the default tag name of list item + * + * @memberof ListViewTag + */ + set itemtag(v: string) { + $(this).attr("itemtag", v); + } + get itemtag(): string { + return $(this).attr("itemtag"); + } + + /** + * Setter: + * + * Turn on/off of the `multiselect` feature + * + * Getter: + * + * Check whether multi-select is allowed + * in this list + * + * @memberof ListViewTag + */ + set multiselect(v: boolean) { + this.attsw(v, "multiselect"); + } + get multiselect() { + if (this.dropdown) { + return false; + } + return this.hasattr("multiselect"); + } + + /** + * Setter: Enable/disable drag and drop event in the list + * + * Getter: Check whether the drag and drop event is enabled + * + * @memberof ListViewTag + */ + set dragndrop(v: boolean) { + this.attsw(v, "dragndrop"); + } + get dragndrop(): boolean { + return this.hasattr("dragndrop"); + } + + /** + * Set the buttons layout of the list. + * Button layout allows to add some custom + * behaviors to the list. + * + * Each button data should define the [[onbtclick]] + * event handle to specify the custom behavior + * + * When the list is configured as dropdown. The buttons + * layout will be disabled + * + * Example of a button data: + * + * ``` + * { + * text: "Button text", + * icon: "home://path/to/icon.png", + * iconclass: "icon-class-name", + * onbtclick: (e) => console.log(e) + * } + * ``` + * + * @memberof ListViewTag + */ + set buttons(v: GenericObject[]) { + if (this.dropdown) { + return; + } + if (!v || !(v.length > 0)) { + return; + } + $(this.refs.btlist).empty(); + for (let item of v) { + $(this.refs.btlist).show(); + const bt = $("").appendTo(this.refs.btlist); + bt[0].uify(this.observable); + (bt[0] as ButtonTag).set(item); + } + } + + /** + * Getter: Get data of the list + * + * Setter: Set data to the list + * + * @type {GenericObject[]} + * @memberof ListViewTag + */ + get data(): GenericObject[] { + return this._data; + } + set data(data: GenericObject[]) { + this._data = data; + this._selectedItem = undefined; + this._selectedItems = []; + $(this.refs.mlist).empty(); + for (let item of data) { + this.push(item, false); + } + $(this.refs.container).off("mousedown", this._onmousedown); + if (this.dragndrop && !this.dropdown) { + $(this.refs.container).on( + "mousedown", + this._onmousedown + ); + } + this.ondatachange(); + } + + /** + * Do nothing + * + * @protected + * @memberof ListViewTag + */ + protected ondatachange(): void {} + + /** + * Setter: Select list item(s) by their indexes + * + * Getter: Get the indexes of all selected items + * + * @memberof ListViewTag + */ + set selected(idx: number | number[]) { + if (!this.data) { + return; + } + const select = (i: number) => { + if (i < 0) { + this.unselect(); + return; + } + const data = this.data; + if (i >= data.length) { + return; + } + const el = data[i].domel as ListViewItemTag; + el.selected = true; + }; + if (Array.isArray(idx)) { + if (this.multiselect) { + for (const i of idx as number[]) { + select(i); + } + } + } else { + select(idx as number); + } + } + + /** + * Get the latest selected item + * + * @readonly + * @type {ListViewItemTag} + * @memberof ListViewTag + */ + get selectedItem(): ListViewItemTag { + return this._selectedItem; + } + + /** + * Get all the selected items + * + * @readonly + * @type {ListViewItemTag[]} + * @memberof ListViewTag + */ + get selectedItems(): ListViewItemTag[] { + return this._selectedItems; + } + + get selected(): number | number[] { + if (this.multiselect) { + return this.selectedItems.map(function ( + it: ListViewItemTag + ) { + return $(it).index(); + }); + } + return $(this.selectedItem).index(); + } + + /** + * Add an item to the beginning of the list + * + * @param {GenericObject} item + * @returns {ListViewItemTag} the added list item element + * @memberof ListViewTag + */ + unshift(item: GenericObject) { + return this.push(item, true); + } + + /** + * check whether the list has data + * + * @private + * @param {GenericObject} v + * @returns + * @memberof ListViewTag + */ + private has_data(v: GenericObject) { + return this.data && this.data.includes(v); + } + + /** + * Add an item to the beginning or end of the list + * + * @param {GenericObject} item list item data + * @param {boolean} [flag] indicates whether to add the item in the beginning of the list + * @returns {ListViewItemTag} the added list item element + * @memberof ListViewTag + */ + push( + item: GenericObject, + flag?: boolean + ): ListViewItemTag { + let tag = this.itemtag; + if (item.tag) tag = item.tag; + const el = $(`<${tag}>`); + if (flag) { + if (!this.has_data(item)) { + this.data.unshift(item); + } + $(this.refs.mlist).prepend(el[0]); + } else { + if (!this.has_data(item)) { + this.data.push(item); + } + el.appendTo(this.refs.mlist); + } + el[0].uify(this.observable); + const element = el[0] as ListViewItemTag; + element.onctxmenu = (e) => { + return this.iclick(e, true); + }; + element.onitemdbclick = (e) => { + this.idbclick(e); + this.iclick(e, false); + }; + element.onitemclick = (e) => { + return this.iclick(e, false); + }; + element.onitemselect = (e) => { + return this.iselect(e); + }; + element.onitemclose = (e) => { + return this.iclose(e); + }; + element.data = item; + item.domel = el[0]; + return element; + } + + /** + * Delete an item + * + * @param {ListViewItemTag} item item DOM element + * @memberof ListViewTag + */ + delete(item: ListViewItemTag): void { + const el = item.data; + const data = this.data; + if (this.selectedItem === item) { + this._selectedItem = undefined; + } + const list = this.selectedItems; + if (list.includes(item)) { + list.splice(list.indexOf(item), 1); + } + if (data.includes(el)) { + data.splice(data.indexOf(el), 1); + } + $(item).remove(); + } + + /** + * Select item next to the currently selected item. + * If there is no item selected, the first item will + * be selected + * + * @returns {void} + * @memberof ListViewTag + */ + selectNext(): void { + if (this.multiselect) { + return; + } + const el = this.selectedItem; + let idx = 0; + if (el) { + idx = $(el).index() + 1; + } + this.selected = idx; + } + + /** + * Select the previous item in the list. + * + * @returns {void} + * @memberof ListViewTag + */ + selectPrev(): void { + if (this.multiselect) { + return; + } + const el = this.selectedItem; + let idx = 0; + if (el) { + idx = $(el).index() - 1; + } + this.selected = idx; + } + + /** + * Unselect all the selected items in the list + * + * @returns {void} + * @memberof ListViewTag + */ + unselect(): void { + for (let v of this.selectedItems) { + v.selected = false; + } + this._selectedItems = []; + this._selectedItem = undefined; + } + + /** + * This function triggers the click event on an item + * + * @private + * @param {TagEventType} e tag event object + * @param {boolean} flag indicates whether this is a double click event + * @returns {void} + * @memberof ListViewTag + */ + private iclick( + e: TagEventType, + flag: boolean + ): void { + if (!e.data) { + return; + } + const list = this.selectedItems; + if (this.multiselect && list.includes(e.data) && !flag) { + list.splice(list.indexOf(e.data), 1); + e.data.selected = false; + return; + } + e.data.selected = true; + } + + /** + * This function triggers the double click event on an item + * + * @private + * @param {TagEventType} e tag event object + * @returns + * @memberof ListViewTag + */ + private idbclick(e: TagEventType) { + const evt: TagEventType = { + id: this.aid, + data: { item: e.data }, + }; + this._onlistdbclick(evt); + return this.observable.trigger("listdbclick", evt); + } + + /** + * This function triggers the list item select event + * + * @private + * @param {TagEventType} e tag event object + * @returns + * @memberof ListViewTag + */ + private iselect(e: TagEventType) { + if (!e.data) { + return; + } + var edata = { item: e.data, items: [] }; + if (this.multiselect) { + if (this.selectedItems.includes(e.data)) { + return; + } + this._selectedItem = e.data; + this.selectedItems.push(e.data); + edata.items = this.selectedItems; + } else { + if (this.selectedItem === e.data) { + return; + } + if (this.selectedItem) { + this.selectedItem.selected = false; + } + this._selectedItem = e.data; + this._selectedItems = [e.data]; + edata.items = [e.data]; + //scroll element + const li = $(e.data).children()[0]; + const offset = $(this.refs.container).offset(); + const top = $(this.refs.container).scrollTop(); + if ( + $(li).offset().top + $(li).height() > + $(this.refs.container).height() + offset.top + ) { + $(this.refs.container).scrollTop( + top + + $(this.refs.container).height() - + $(li).height() + ); + } else if ($(li).offset().top < offset.top) { + $(this.refs.container).scrollTop( + top - + $(this.refs.container).height() + + $(li).height() + ); + } + } + + if (this.dropdown) { + const label = this.refs.drlabel as LabelTag; + label.set(e.data.data); + $(this.refs.mlist).hide(); + } + const evt = { id: this.aid, data: edata }; + this._onlistselect(evt); + return this.observable.trigger("listselect", evt); + } + + /** + * Mount the tag and bind some basic event + * + * @protected + * @returns {void} + * @memberof ListViewTag + */ + protected mount(): void { + this._dnd = { + from: undefined, + to: undefined, + }; + this._onmousedown = (e) => { + let el: any = $(e.target).closest( + "li[dataref='afx-list-item']" + ); + if (el.length === 0) { + return; + } + el = el.parent()[0] as ListViewItemTag; + this._dnd.from = el; + this._dnd.to = undefined; + $(window).on("mouseup", this._onmouseup); + $(window).on("mousemove", this._onmousemove); + }; + + this._onmouseup = (e) => { + $(window).off("mouseup", this._onmouseup); + $(window).off("mousemove", this._onmousemove); + $("#systooltip").hide(); + let el: any = $(e.target).closest( + "li[dataref='afx-list-item']" + ); + if (el.length === 0) { + return; + } + el = el.parent()[0]; + if (el === this._dnd.from) { + return; + } + this._dnd.to = el; + this._ondragndrop({ id: this.aid, data: this._dnd }); + this._dnd = { + from: undefined, + to: undefined, + }; + }; + + this._onmousemove = (e) => { + if (!e) { + return; + } + if (!this._dnd.from) { + return; + } + const data = this._dnd.from.data; + const $label = $("#systooltip"); + const top = e.clientY + 5; + const left = e.clientX + 5; + $label.show(); + const label = $label[0] as LabelTag; + label.set(data); + return $label + .css("top", top + "px") + .css("left", left + "px"); + }; + + $(this.refs.btlist).hide(); + this.observable.on("resize", (e) => this.calibrate()); + return this.calibrate(); + } + + /** + * This function triggers the item close event + * + * @private + * @param {TagEventType} e tag event object + * @returns {void} + * @memberof ListViewTag + */ + private iclose(e: TagEventType): void { + if (!e.data) { + return; + } + const evt = { id: this.aid, data: { item: e.data } }; + const r = this._onitemclose(evt); + if (!r) { + return; + } + this.observable.trigger("itemclose", evt); + return this.delete(e.data); + } + + /** + * Show the dropdown list. + * This function is called only when the list is a dropdown + * list + * + * @protected + * @param {*} e + * @returns {void} + * @memberof ListViewTag + */ + protected showlist(e: any): void { + if (!this.dropdown) { + return; + } + const desktoph = $(Ant.OS.GUI.workspace).height(); + const offset = + $(this).offset().top + $(this.refs.mlist).height(); + if (offset > desktoph) { + $(this.refs.mlist).css( + "top", + `-${$(this.refs.mlist).outerHeight()}px` + ); + } else { + $(this.refs.mlist).css("top", "100%"); + } + $(this.refs.mlist).show(); + } + + /** + * Hide the dropdown list. + * This function is called only when the list is a dropdown + * list + * + * @protected + * @param {*} e + * @memberof ListViewTag + */ + protected dropoff(e: any): void { + if ($(e.target).closest(this.refs.container).length === 0) { + $(this.refs.mlist).hide(); + } + } + + /** + * calibrate the list layout + * + * @protected + * @returns {void} + * @memberof ListViewTag + */ + protected calibrate(): void { + if (!this.dropdown) { + return; + } + const w = `${$(this).width()}px`; + $(this.refs.container).css("width", w); + $(this.refs.current).css("width", w); + $(this.refs.mlist).css("width", w); + } + + /** + * List view layout definition + * + * @protected + * @returns {TagLayoutType[]} + * @memberof ListViewTag + */ + protected layout(): TagLayoutType[] { + return [ + { + el: "div", + class: "list-container", + ref: "container", + children: [ + { + el: "div", + ref: "current", + children: [ + { el: "afx-label", ref: "drlabel" }, + ], + }, + { el: "ul", ref: "mlist" }, + ], + }, + { el: "div", class: "button_container", ref: "btlist" }, + ]; + } + } + + define("afx-list-view", ListViewTag); + define("afx-list-item", SimpleListItemTag); + } + } +} diff --git a/src/core/tags/MenuTag.ts b/src/core/tags/MenuTag.ts new file mode 100644 index 0000000..bb9e9d5 --- /dev/null +++ b/src/core/tags/MenuTag.ts @@ -0,0 +1,826 @@ +namespace OS { + export namespace GUI { + export namespace tag { + /** + * Menu event data interface definition + */ + export type MenuEventData = TagEventDataType; + + /** + * This class defines the abstract prototype of an menu entry. + * Any implementation of menu entry tag should extend this class + * + * @export + * @abstract + * @class MenuEntryTag + * @extends {AFXTag} + */ + export abstract class MenuEntryTag extends AFXTag { + /** + * Data placeholder of the menu entry + * + * @private + * @type {GenericObject} + * @memberof MenuEntryTag + */ + private _data: GenericObject; + + /** + * placeholder of `menu entry select` event handle + * + * @private + * @type {TagEventCallback} + * @memberof MenuEntryTag + */ + private _onmenuselect: TagEventCallback; + + /** + * placeholder of `sub-menu entry select event` handle + * + * @private + * @type {TagEventCallback} + * @memberof MenuEntryTag + */ + private _onchildselect: TagEventCallback; + + /** + * Reference to the parent menu entry of current one + * + * @type {MenuEntryTag} + * @memberof MenuEntryTag + */ + parent: MenuEntryTag; + + /** + * Reference to the root menu entry + * + * @type {MenuTag} + * @memberof MenuEntryTag + */ + root: MenuTag; + + /** + *Creates an instance of MenuEntryTag. + * @memberof MenuEntryTag + */ + constructor() { + super(); + this._onmenuselect = this._onchildselect = ( + e: TagEventType + ): void => {}; + } + + /** + * Init the tag before mounting + * + * @protected + * @memberof MenuEntryTag + */ + protected init(): void { + this.nodes = undefined; + } + /** + * Set the `menu entry select` event handle + * + * @memberof MenuEntryTag + */ + set onmenuselect(v: TagEventCallback) { + this._onmenuselect = v; + } + + /** + * Setter: Set the `sub menu entry select` event handle + * + * Getter: get the current `sub menu entry select` event handle + * + * @memberof MenuEntryTag + */ + set onchildselect(v: TagEventCallback) { + this._onchildselect = v; + } + get onchildselect(): TagEventCallback { + return this._onchildselect; + } + /** + * Setter: Set data to the entry + * + * Getter: Get data of the current menu entry + * + * @memberof MenuEntryTag + */ + set data(data: GenericObject) { + this._data = data; + this.set(data); + } + get data(): GenericObject { + return this._data; + } + + /** + * Check whether the current menu entry has sub-menu + * + * @protected + * @returns {boolean} + * @memberof MenuEntryTag + */ + protected has_nodes(): boolean { + const ch = this.nodes; + return ch && ch.length > 0; + } + + /** + * Check whether the current menu entry is the root entry + * + * @protected + * @returns + * @memberof MenuEntryTag + */ + protected is_root() { + if (this.parent) { + return false; + } else { + return true; + } + } + + /** + * Layout definition of the menu entry + * This function define the outer layout of the menu entry. + * Custom inner layout of each item implementation should + * be defined in [[itemlayout]] + * @protected + * @returns {TagLayoutType[]} + * @memberof MenuEntryTag + */ + protected layout(): TagLayoutType[] { + return [ + { + el: "li", + ref: "container", + children: [ + { + el: "a", + ref: "entry", + children: this.itemlayout(), + }, + { el: "afx-menu", ref: "submenu" }, + ], + }, + ]; + } + + /** + * Setter: Set the sub-menu data + * + * Getter: Get the sub-menu data + * + * @memberof MenuEntryTag + */ + set nodes(v: GenericObject[]) { + $(this.refs.container).removeClass("afx_submenu"); + if (!v || !(v.length > 0)) { + $(this.refs.submenu).hide(); + return; + } + $(this.refs.container).addClass("afx_submenu"); + $(this.refs.submenu).show().attr("style", ""); + const element = this.refs.submenu as MenuTag; + element.parent = this; + element.root = this.root; + element.items = v; + // ensure that the data is in sync + this._data.nodes = v; + if (this.is_root()) { + $(this.refs.container).mouseleave((e) => { + return $(this.refs.submenu).attr("style", ""); + }); + } + } + get nodes(): GenericObject[] { + if (this.data && this.data.nodes) { + return this.data.nodes; + } + return undefined; + } + /** + * Bind some base event to the menu entry + * + * @protected + * @memberof MenuEntryTag + */ + protected mount(): void { + $(this.refs.entry).click((e) => this.select(e)); + } + + /** + * Hide the sub-menu of the current menu entry + * + * @private + * @returns {void} + * @memberof MenuEntryTag + */ + private submenuoff(): void { + const p = this.parent; + if (!p) { + $(this.refs.submenu).attr("style", ""); + return; + } + return p.submenuoff(); + } + + /** + * This function trigger two event: + * - the `onmenuselect` event on the current entry + * - the `onchildselect` event on the parent of the current entry + * + * @protected + * @param {JQuery.ClickEvent} e + * @memberof MenuEntryTag + */ + protected select(e: JQuery.ClickEvent): void { + const evt = { + id: this.aid, + data: { item: this, event: e }, + }; + e.preventDefault(); + if (this.is_root() && this.has_nodes()) { + $(this.refs.submenu).show(); + } else { + this.submenuoff(); + } + this._onmenuselect(evt); + if (this.parent) { + this.parent.onchildselect(evt); + } + if (this.root) { + this.root.onmenuitemselect(evt); + } + } + + /** + * custom inner layout of a menu entry + * + * @protected + * @abstract + * @returns {TagLayoutType[]} + * @memberof MenuEntryTag + */ + protected abstract itemlayout(): TagLayoutType[]; + } + + /** + * This class extends the [[MenuEntryTag]] prototype. It inner layout is + * defined with the following elements: + * - a [[SwitchTag]] acts as checker or radio + * - a [[LabelTag]] to display the content of the menu entry + * - a `span` element that display the keyboard shortcut of the entry + * + * @class SimpleMenuEntryTag + * @extends {MenuEntryTag} + */ + export class SimpleMenuEntryTag extends MenuEntryTag { + /** + *Creates an instance of SimpleMenuEntryTag. + * @memberof SimpleMenuEntryTag + */ + constructor() { + super(); + } + + /** + * Reset some properties to default value + * + * @protected + * @memberof SimpleMenuEntryTag + */ + protected init(): void { + super.init(); + this.switch = false; + this.radio = false; + this.checked = false; + } + + /** + * Do nothing + * + * @protected + * @memberof SimpleMenuEntryTag + */ + protected calibrate(): void {} + + /** + * Do nothing + * + * @protected + * @param {*} [d] + * @memberof SimpleMenuEntryTag + */ + protected reload(d?: any): void {} + + /** + * Setter: Turn on/off the checker feature of the menu entry + * + * Getter: Check whether the checker feature is enabled on this menu entry + * + * @memberof SimpleMenuEntryTag + */ + set switch(v: boolean) { + this.attsw(v, "switch"); + if (this.radio || v) { + $(this.refs.switch).show(); + } else { + $(this.refs.switch).hide(); + } + } + get switch(): boolean { + return this.hasattr("switch"); + } + + /** + * Setter: Turn on/off the radio feature of the menu entry + * + * Getter: Check whether the radio feature is enabled + * + * @memberof SimpleMenuEntryTag + */ + set radio(v: boolean) { + this.attsw(v, "radio"); + if (this.switch || v) { + $(this.refs.switch).show(); + } else { + $(this.refs.switch).hide(); + } + } + get radio(): boolean { + return this.hasattr("radio"); + } + + /** + * Setter: + * + * Toggle the switch on the menu entry, this setter + * only works when the `checker` or `radio` feature is + * enabled + * + * Getter: + * + * Check whether the switch is turned on + * + * @memberof SimpleMenuEntryTag + */ + set checked(v: boolean) { + this.attsw(v, "checked"); + if (this.data) this.data.checked = v; + if (!this.radio && !this.switch) { + return; + } + (this.refs.switch as SwitchTag).swon = v; + } + get checked(): boolean { + return this.hasattr("checked"); + } + + /** + * Set the label icon using a VFS path + * + * @memberof SimpleMenuEntryTag + */ + set icon(v: string) { + $(this.refs.container).removeClass("fix_padding"); + if (!v) { + return; + } + //$(this).attr("icon", v); + const label = this.refs.label as LabelTag; + label.icon = v; + $(this.refs.container).addClass("fix_padding"); + } + + /** + * Set the label CSS icon class + * + * @memberof SimpleMenuEntryTag + */ + set iconclass(v: string) { + if (!v) { + return; + } + const label = this.refs.label as LabelTag; + label.iconclass = v; + } + + /** + * Set the label text + * + * @memberof SimpleMenuEntryTag + */ + set text(v: string) { + if (v === undefined) { + return; + } + const label = this.refs.label as LabelTag; + label.text = v; + } + + /** + * Set the keyboard shortcut text + * + * @memberof SimpleMenuEntryTag + */ + set shortcut(v: string) { + $(this.refs.shortcut).hide(); + if (!v) { + return; + } + $(this.refs.shortcut).show(); + $(this.refs.shortcut).text(v); + } + + /** + * Uncheck all sub-menu items of the current menu entry + * that have the radio feature enabled + * + * @returns {void} + * @memberof SimpleMenuEntryTag + */ + protected reset_radio(): void { + if (!this.has_nodes()) { + return; + } + for (let v of this.nodes) { + if (!v.domel.radio) { + continue; + } + v.domel.checked = false; + } + } + + /** + * Mount the current tag + * + * @protected + * @memberof SimpleMenuEntryTag + */ + protected mount(): void { + super.mount(); + (this.refs.switch as SwitchTag).enable = false; + } + + /** + * Trigger the onmenuselect and onchildselect events + * + * @protected + * @param {JQuery.ClickEvent} e Mouse click event + * @returns {void} + * @memberof SimpleMenuEntryTag + */ + protected select(e: JQuery.ClickEvent): void { + if (this.switch) { + this.checked = !this.checked; + } else if (this.radio) { + const p = this.parent as SimpleMenuEntryTag; + if (p) { + p.reset_radio(); + } + this.checked = !this.checked; + } + return super.select(e); + } + + /** + * Inner item layout of the menu entry + * + * @returns + * @memberof SimpleMenuEntryTag + */ + itemlayout() { + return [ + { el: "afx-switch", ref: "switch" }, + { el: "afx-label", ref: "label" }, + { el: "span", class: "shortcut", ref: "shortcut" }, + ]; + } + } + + /** + * A menu tag contains a collection of menu entries in which each + * entry maybe a leaf entry or may contain a submenu + * + * @export + * @class MenuTag + * @extends {AFXTag} + */ + export class MenuTag extends AFXTag { + /** + * Reference to the parent menu entry of the current value. + * This value is `undefined` in case of the current menu is + * the root menu + * + * @type {MenuEntryTag} + * @memberof MenuTag + */ + parent: MenuEntryTag; + + /** + * Reference to the root menu + * + * @type {MenuTag} + * @memberof MenuTag + */ + root: MenuTag; + + /** + * The `pid` of the application that attached to this menu. + * This value is optional + * + * @type {number} + * @memberof MenuTag + */ + pid?: number; + + /** + * placeholder for menu select event handle + * + * @private + * @type {TagEventCallback} + * @memberof MenuTag + */ + private _onmenuselect: TagEventCallback; + + /** + * Menu data placeholder + * + * @private + * @type {GenericObject[]} + * @memberof MenuTag + */ + private _items: GenericObject[]; + + /** + *Creates an instance of MenuTag. + * @memberof MenuTag + */ + constructor() { + super(); + } + + /** + * Reset some properties to default value + * + * @protected + * @memberof MenuTag + */ + protected init(): void { + this.contentag = "afx-menu-entry"; + this.context = false; + this._items = []; + this._onmenuselect = ( + e: TagEventType + ): void => {}; + } + + /** + * Do nothing + * + * @protected + * @memberof MenuTag + */ + protected calibrate(): void {} + + /** + * Do nothing + * + * @protected + * @param {*} [d] + * @memberof MenuTag + */ + protected reload(d?: any): void {} + + /** + * Setter: Set the menu items data + * + * Getter: Get menu items data + * + * @memberof MenuTag + */ + set items(data: GenericObject[]) { + this._items = data; + $(this.refs.container).empty(); + data.map((item) => this.push(item, false)); + } + get items(): GenericObject[] { + return this._items; + } + + /** + * Setter: Set whether the current menu is a context menu + * + * Getter: Check whether the current menu is a context menu + * + * @memberof MenuTag + */ + set context(v: boolean) { + this.attsw(v, "context"); + $(this.refs.wrapper).removeClass("context"); + if (!v) { + return; + } + $(this.refs.wrapper).addClass("context"); + $(this).hide(); + } + get context(): boolean { + return this.hasattr("context"); + } + + /** + * Set menu select event handle + * + * @memberof MenuTag + */ + set onmenuselect(v: TagEventCallback) { + this._onmenuselect = v; + } + + /** + * Setter: + * + * Set the default tag name of the menu item. + * If the tag is not specified in an item data, + * this value will be used + * + * Getter: + * + * Get the default menu entry tag name + * + * @memberof MenuTag + */ + set contentag(v: string) { + $(this).attr("contentag", v); + } + get contentag(): string { + return $(this).attr("contentag"); + } + + /** + * Get the reference to the function that triggers + * the menu select event + * + * @readonly + * @type {TagEventCallback} + * @memberof MenuTag + */ + get onmenuitemselect(): TagEventCallback { + return this.handleselect; + } + + /** + * This function triggers the menu select event + * + * @private + * @param {TagEventType} e + * @memberof MenuTag + */ + private handleselect(e: TagEventType): void { + if (this.context) { + $(this).hide(); + } + e.id = this.aid; + this._onmenuselect(e); + this.observable.trigger("menuselect", e); + } + + /** + * Show the current menu. This function is called + * only if the current menu is a context menu + * + * @param {JQuery.MouseEventBase} e JQuery mouse event + * @returns {void} + * @memberof MenuTag + */ + show(e: JQuery.MouseEventBase): void { + if (!this.context) { + return; + } + $(this) + .css("top", e.clientY - 15 + "px") + .css("left", e.clientX - 5 + "px") + .show(); + } + + /** + * Test whether the current menu is the root menu + * + * @private + * @returns {boolean} + * @memberof MenuTag + */ + private is_root(): boolean { + return this.root === undefined; + } + + /** + * Mount the menu tag and bind some basic events + * + * @protected + * @returns {void} + * @memberof MenuTag + */ + protected mount(): void { + $(this.refs.container).css("display", "contents"); + if (!this.context) { + return; + } + $(this.refs.wrapper).mouseleave((e) => { + if (!this.is_root()) { + return; + } + return $(this).hide(); + }); + } + + /** + * Add a menu entry to the beginning of the current + * menu + * + * @param {GenericObject} item menu entry data + * @memberof MenuTag + */ + unshift(item: GenericObject): void { + this.push(item, true); + } + + /** + * Delete a menu entry + * + * @param {MenuEntryTag} item reference to the DOM element of an menu entry + * @memberof MenuTag + */ + delete(item: MenuEntryTag): void { + const el = item.data; + const data = this.items; + if (data.includes(el)) { + data.splice(data.indexOf(el), 1); + } + $(item).remove(); + } + + /** + * Add an menu entry to the beginning or end of the menu + * + * @param {GenericObject} item menu entry data + * @param {boolean} flag indicates whether the entry should be added to the beginning of the menu + * @returns {MenuEntryTag} + * @memberof MenuTag + */ + push(item: GenericObject, flag: boolean): MenuEntryTag { + let tag = this.contentag; + if (item.tag) { + tag = item.tag; + } + const el = $(`<${tag}>`); + if (flag) { + $(this.refs.container).prepend(el[0]); + if (!this.items.includes(item)) { + this.items.unshift(item); + } + } else { + el.appendTo(this.refs.container); + if (!this.items.includes(item)) { + this.items.push(item); + } + } + const entry = el[0] as MenuEntryTag; + entry.uify(this.observable); + entry.parent = this.parent; + entry.root = this.parent ? this.parent.root : this; + entry.data = item; + item.domel = entry; + return entry; + } + + /** + * Menu tag layout definition + * + * @returns + * @memberof MenuTag + */ + layout() { + return [ + { + el: "ul", + ref: "wrapper", + children: [ + { el: "li", class: "afx-corner-fix" }, + { el: "div", ref: "container" }, + { el: "li", class: "afx-corner-fix" }, + ], + }, + ]; + } + } + + define("afx-menu", MenuTag); + define("afx-menu-entry", SimpleMenuEntryTag); + } + } +} diff --git a/src/core/tags/NSpinnerTag.ts b/src/core/tags/NSpinnerTag.ts new file mode 100644 index 0000000..2f438ec --- /dev/null +++ b/src/core/tags/NSpinnerTag.ts @@ -0,0 +1,207 @@ +namespace OS { + export namespace GUI { + export namespace tag { + /** + * A simple number sinner tag + * + * @export + * @class NSpinnerTag + * @extends {AFXTag} + */ + export class NSpinnerTag extends AFXTag { + /** + * Placeholder for value change event handle + * + * @private + * @type {TagEventCallback} + * @memberof NSpinnerTag + */ + private _onchange: TagEventCallback; + + /** + * Placeholder for the spinner data + * + * @private + * @type {number} + * @memberof NSpinnerTag + */ + private _value: number; + + /** + * Place holder for the spinner step + * + * @type {number} + * @memberof NSpinnerTag + */ + step: number; + + /** + *Creates an instance of NSpinnerTag. + * @memberof NSpinnerTag + */ + constructor() { + super(); + this._onchange = (e) => {}; + } + + /** + * Init the spinner value to `0` and step to `1` + * + * @protected + * @memberof NSpinnerTag + */ + protected init(): void { + this._value = 0; + this.step = 1; + } + + /** + * Do nothing + * + * @protected + * @param {*} [d] + * @memberof NSpinnerTag + */ + protected reload(d?: any): void {} + + /** + * Set the value change event handle + * + * @memberof NSpinnerTag + */ + set onvaluechange(f: TagEventCallback) { + this._onchange = f; + } + + /** + * Mount the tag and bind basic events + * + * @protected + * @memberof NSpinnerTag + */ + protected mount(): void { + $(this.refs.holder).attr("type", "text"); + $(this.refs.incr).click((e) => { + this.value = this.value + this.step; + }); + + $(this.refs.decr).click((e) => { + this.value = this.value - this.step; + }); + + // @observable.on "calibrate", () -> @calibrate() + this.observable.on("resize", () => this.calibrate()); + + $(this.refs.holder).on("keyup", (e) => { + if (e.keyCode === 13) { + let val = parseInt( + (this.refs.holder as HTMLInputElement).value + ); + if (!isNaN(val)) { + if (val < 0) { + val = this.value; + } + return (this.value = val); + } + } + }); + this.calibrate(); + } + + /** + * Calibrate the layout of the spinner + * + * @memberof NSpinnerTag + */ + calibrate(): void { + $(this.refs.holder).css( + "width", + $(this).width() - 20 + "px" + ); + $(this.refs.holder).css("height", $(this).height() + "px"); + $(this.refs.spinner) + .css("width", "20px") + .css("height", $(this).height() + "px"); + $(this.refs.incr) + .css("height", $(this).height() / 2 - 2 + "px") + .css("position", "relative"); + $(this.refs.decr) + .css("height", $(this).height() / 2 - 2 + "px") + .css("position", "relative"); + $(this.refs.spinner) + .find("li") + .css("display", "block") + .css("text-align", "center") + .css("vertical-align", "middle"); + $(this.refs.spinner) + .find("i") + .css("font-size", "16px") + .css("position", "absolute"); + const fn = function (ie: HTMLElement, pos: string) { + const el = $(ie).find("i"); + el.css( + pos, + ($(ie).height() - el.height()) / 2 + "px" + ).css("left", ($(ie).width() - el.width()) / 2 + "px"); + }; + fn(this.refs.decr, "bottom"); + fn(this.refs.incr, "top"); + } + + /** + * Setter: Set the spinner value + * + * Getter: Get the spinner value + * + * @memberof NSpinnerTag + */ + set value(v: number) { + this._value = v; + $(this.refs.holder).val(this._value); + const evt = { id: this.aid, data: v }; + this._onchange(evt); + this.observable.trigger("nspin", evt); + } + get value(): number { + return this._value; + } + + /** + * Spinner layout definition + * + * @protected + * @returns {TagLayoutType[]} + * @memberof NSpinnerTag + */ + protected layout(): TagLayoutType[] { + return [ + { + el: "input", + ref: "holder", + }, + { + el: "ul", + ref: "spinner", + children: [ + { + el: "li", + class: "incr", + ref: "incr", + children: [{ el: "i" }], + }, + { + el: "li", + class: "decr", + ref: "decr", + children: [{ el: "i" }], + }, + ], + }, + ]; + } + } + + define("afx-nspinner", NSpinnerTag); + } + } +} diff --git a/src/core/tags/OverlayTag.ts b/src/core/tags/OverlayTag.ts new file mode 100644 index 0000000..21e82e6 --- /dev/null +++ b/src/core/tags/OverlayTag.ts @@ -0,0 +1,159 @@ +namespace OS { + export namespace GUI { + export namespace tag { + /** + * An overlay tag is a layout tag that alway stay on top of + * the virtual desktop environment. Tile layout elements ([[VBoxTag]], [[HboxTag]]) + * can be used inside this tag to compose elements + * + * @export + * @class OverlayTag + * @extends {AFXTag} + */ + export class OverlayTag extends AFXTag { + /** + * Tag width placeholder + * + * @private + * @type {string} + * @memberof OverlayTag + */ + private _width: string; + + /** + * Tag height place holder + * + * @private + * @type {string} + * @memberof OverlayTag + */ + private _height: string; + + /** + *Creates an instance of OverlayTag. + * @memberof OverlayTag + */ + constructor() { + super(); + } + //.css "display", "flex" + //.css "flex-direction", "column" + //$(@refs.yield).css "flex", "1" + + /** + * Put the tag on top of the virtual desktop environment + * + * @protected + * @memberof OverlayTag + */ + protected init(): void { + $(this.refs.yield) + .css("position", "relative") + .css("width", "100%") + .css("height", "100%"); + $(this).css("position", "absolute").css("z-index", 1000000); + } + + /** + * Do nothing + * + * @protected + * @param {*} [d] + * @memberof OverlayTag + */ + protected reload(d?: any): void {} + + /** + * Setter: + * + * Set the width of the tag, the tag width should be in form of: + * `100px` of `80%` + * + * Getter: + * + * Get the tag width + * + * @memberof OverlayTag + */ + set width(v: string) { + if (!v) { + return; + } + this._width = v; + this.calibrate(); + } + get width(): string { + return this._width; + } + + /** + * Setter: + * + * Set the tag height, the tag height should be in form of: + * `100px` of `80%` + * + * Getter: + * + * Get the tag height + * + * @memberof OverlayTag + */ + set height(v: string) { + if (!v) { + return; + } + this._height = v; + this.calibrate(); + } + get height(): string { + return this._height; + } + + /** + * Calibrate the element when mounting + * + * @protected + * @returns {void} + * @memberof OverlayTag + */ + protected mount(): void { + return this.calibrate(); + } + + /** + * Calibrate the width and height of the tag + * + * @returns {void} + * @memberof OverlayTag + */ + calibrate(): void { + $(this).css("width", this.width).css("height", this.height); + return this.observable.trigger("resize", { + id: this.aid, + data: { + w: this.width, + h: this.height, + }, + }); + } + + /** + * Layout definition of the tag + * + * @protected + * @returns {TagLayoutType[]} + * @memberof OverlayTag + */ + protected layout(): TagLayoutType[] { + return [ + { + el: "afx-vbox", + ref: "yield", + }, + ]; + } + } + define("afx-overlay", OverlayTag); + } + } +} diff --git a/src/core/tags/ResizerTag.ts b/src/core/tags/ResizerTag.ts new file mode 100644 index 0000000..a21092b --- /dev/null +++ b/src/core/tags/ResizerTag.ts @@ -0,0 +1,239 @@ +namespace OS { + export namespace GUI { + export namespace tag { + /** + * A `resizer` tag is basically used to dynamically resize an element using mouse. + * It is usually put inside a [[TileLayoutTag]] an can be attached to any element. Example: + * + * The resizer tag in the following example will be attached to the first `afx-vbox`, + * and allows to resize this element using mouse + * + * ```xml + * + * ... + * + * ... + * + * ``` + * + * @export + * @class ResizerTag + * @extends {AFXTag} + */ + export class ResizerTag extends AFXTag { + /** + * Reference to the element that this tag is attached to + * + * @private + * @type {*} + * @memberof ResizerTag + */ + private _resizable_el: any; + + /** + * Reference to the parent tag of the current tag. + * The parent tag should be an instance of a [[TileLayoutTag]] + * such as [[VBoxTag]] or [[HBoxTag]] + * + * @private + * @type {*} + * @memberof ResizerTag + */ + private _parent: any; + + /** + * Placeholder of the minimum value that + * the attached element can be resized + * + * @private + * @type {number} + * @memberof ResizerTag + */ + private _minsize: number; + + /** + *Creates an instance of ResizerTag. + * @memberof ResizerTag + */ + constructor() { + super(); + } + + /** + * Set the properties of the tag to default values + * + * @protected + * @memberof ResizerTag + */ + protected init(): void { + this.dir = "hz"; + this._resizable_el = undefined; + this._parent = $(this).parent().parent()[0]; + this._minsize = 0; + } + + /** + * Do nothing + * + * @protected + * @param {*} [d] + * @memberof ResizerTag + */ + protected reload(d?: any): void {} + /** + * Setter: + * + * Set resize direction, two possible values: + * - `hz` - horizontal direction, resize by width + * - `ve` - vertical direction, resize by height + * + * Getter: + * + * Get the resize direction + * + * @memberof ResizerTag + */ + set dir(v: string) { + $(this).attr("dir", v); + } + get dir(): string { + return $(this).attr("dir"); + } + + /** + * Mount the tag to the DOM tree + * + * @protected + * @memberof ResizerTag + */ + protected mount(): void { + let att: string; + $(this).css(" display", "block"); + const tagname = $(this._parent).prop("tagName"); + this._resizable_el = + $(this).prev().length === 1 + ? $(this).prev()[0] + : undefined; + if (tagname === "AFX-HBOX") { + this.dir = "hz"; + $(this).css("cursor", "col-resize"); + $(this).addClass("horizontal"); + if (this._resizable_el) { + att = $(this._resizable_el).attr("min-width"); + if (att) { + this._minsize = parseInt(att); + } + } + } else if (tagname === "AFX-VBOX") { + this.dir = "ve"; + $(this).css("cursor", "row-resize"); + $(this).addClass("vertical"); + if (this._resizable_el) { + att = $(this._resizable_el).attr("min-height"); + if (att) { + this._minsize = parseInt(att); + } + } + } else { + this.dir = "none"; + } + if (this._minsize === 0) { + this._minsize = 10; + } + this.make_draggable(); + } + + /** + * Enable draggable on the element + * + * @private + * @memberof ResizerTag + */ + private make_draggable(): void { + $(this).css("user-select", "none"); + $(this).on("mousedown", (e) => { + e.preventDefault(); + $(window).on("mousemove", (evt) => { + if (!this._resizable_el) { + return; + } + if (this.dir === "hz") { + return this.horizontalResize(evt); + } else if (this.dir === "ve") { + return this.verticalResize(evt); + } + }); + + return $(window).on("mouseup", function (evt) { + $(window).unbind("mousemove", null); + $(window).unbind("mouseup", null); + + return $(window).unbind("mouseup", null); + }); + }); + } + + /** + * Resize the attached element in the horizontal direction (width) + * + * @private + * @param {JQuery.MouseEventBase} e JQuery mouse event + * @returns {void} + * @memberof ResizerTag + */ + private horizontalResize(e: JQuery.MouseEventBase): void { + if (!this._resizable_el) { + return; + } + const offset = $(this._resizable_el).offset(); + let w = Math.round(e.clientX - offset.left); + if (w < this._minsize) { + w = this._minsize; + } + $(this._resizable_el).attr("data-width", w.toString()); + this.observable.trigger("resize", { + id: this.aid, + data: { w }, + }); + } + + /** + * Resize the attached element in the vertical direction (height) + * + * @protected + * @param {JQuery.MouseEventBase} e JQuery mouse event + * @returns {void} + * @memberof ResizerTag + */ + protected verticalResize(e: JQuery.MouseEventBase): void { + if (!this._resizable_el) { + return; + } + const offset = $(this._resizable_el).offset(); + let h = Math.round(e.clientY - offset.top); + if (h < this._minsize) { + h = this._minsize; + } + $(this._resizable_el).attr("data-height", h.toString()); + return this.observable.trigger("resize", { + id: this.aid, + data: { h }, + }); + } + + /** + * Layout definition of the tag, empty layout + * + * @protected + * @returns {TagLayoutType[]} + * @memberof ResizerTag + */ + protected layout(): TagLayoutType[] { + return []; + } + } + + define("afx-resizer", ResizerTag); + } + } +} diff --git a/src/core/tags/SliderTag.ts b/src/core/tags/SliderTag.ts new file mode 100644 index 0000000..699e78d --- /dev/null +++ b/src/core/tags/SliderTag.ts @@ -0,0 +1,275 @@ +namespace OS { + export namespace GUI { + export namespace tag { + /** + * A slider or track bar is a graphical control element with which + * a user may set a value by moving an indicator, usually horizontally + * + * @class SliderTag + * @extends {AFXTag} + */ + class SliderTag extends AFXTag { + /** + * Slider max value placeholder + * + * @private + * @type {number} + * @memberof SliderTag + */ + private _max: number; + + /** + * Current slider value placeholder + * + * @private + * @type {number} + * @memberof SliderTag + */ + private _value: number; + + /** + * Placeholder of the on change event handle + * + * @private + * @type {TagEventCallback} + * @memberof SliderTag + */ + private _onchange: TagEventCallback; + + /** + * Placeholder of the on changing event handle + * + * @private + * @type {TagEventCallback} + * @memberof SliderTag + */ + private _onchanging: TagEventCallback; + + /** + * Creates an instance of SliderTag. + * @memberof SliderTag + */ + constructor() { + super(); + } + + /** + * Init the default value of the slider: + * - `max`: 100 + * - `value`: 0 + * + * @protected + * @memberof SliderTag + */ + protected init(): void { + this.enable = true; + this._max = 100; + this._value = 0; + this._onchange = this._onchanging = () => {}; + } + + /** + * Do nothing + * + * @protected + * @param {*} [d] + * @memberof SliderTag + */ + protected reload(d?: any): void {} + + /** + * Set value change event handle. + * This handle will be triggered when the + * slider indicator is released + * + * @memberof SliderTag + */ + set onvaluechange(f: TagEventCallback) { + this._onchange = f; + } + + /** + * Set value changing event handle. + * This handle is triggered when moving the + * slider indicator + * + * @memberof SliderTag + */ + set onvaluechanging(f: TagEventCallback) { + this._onchanging = f; + } + + /** + * Setter: Enable/disable the slider + * + * Getter: Check whether the slider is enabled + * + * @memberof SliderTag + */ + set enable(v: boolean) { + this.attsw(v, "enable"); + if (v) { + $(this) + .mouseover(() => { + return $(this.refs.point).show(); + }) + .mouseout(() => { + return $(this.refs.point).hide(); + }); + } else { + $(this.refs.point).hide(); + $(this).unbind("mouseover").unbind("mouseout"); + } + } + get enable(): boolean { + return this.hasattr("enable"); + } + + /** + * Setter: Set the slider value + * + * Getter: Get the current slider value + * + * @memberof SliderTag + */ + set value(v: number) { + this._value = v; + this.calibrate(); + } + get value(): number { + return this._value; + } + + /** + * Setter: Set the maximum value of the slider + * + * Getter: Get the maximum value of the slider + * + * @memberof SliderTag + */ + set max(v: number) { + this._max = v; + this.calibrate(); + } + get max(): number { + return this._max; + } + + /** + * Mount the tag and bind some basic events + * + * @protected + * @memberof SliderTag + */ + protected mount(): void { + this.enable_dragging(); + $(this.refs.point).css("position", "absolute"); + $(this.refs.point).hide(); + this.observable.on("resize", (e) => { + return this.calibrate(); + }); + $(this.refs.container).click((e) => { + const offset = $(this.refs.container).offset(); + const left = e.clientX - offset.left; + const maxw = $(this.refs.container).width(); + this.value = (left * this.max) / maxw; + this.calibrate(); + const evt = { id: this.aid, data: this.value }; + this._onchange(evt); + return this._onchanging(evt); + }); + this.calibrate(); + } + + /** + * Calibrate the slide based on its value and max value + * + * @memberof SliderTag + */ + calibrate(): void { + if (this.value > this.max) { + this.value = this.max; + } + $(this.refs.container).css("width", $(this).width() + "px"); + const w = + ($(this.refs.container).width() * this.value) / + this.max; + $(this.refs.prg) + .css("width", w + "px") + .css("height", $(this.refs.container).height() + "px"); + if (this.enable) { + const ow = w - $(this.refs.point).width() / 2; + const top = Math.floor( + ($(this.refs.prg).height() - + $(this.refs.point).height()) / + 2 + ); + $(this.refs.point) + .css("left", ow + "px") + .css("top", top + "px"); + } + } + + /** + * enable dragging on the slider indicator + * + * @private + * @memberof SliderTag + */ + private enable_dragging(): void { + $(this.refs.point) + .css("user-select", "none") + .css("cursor", "default"); + $(this.refs.point).on("mousedown", (e) => { + e.preventDefault(); + const offset = $(this.refs.container).offset(); + $(window).on("mousemove", (e) => { + let left = e.clientX - offset.left; + left = left < 0 ? 0 : left; + const maxw = $(this.refs.container).width(); + left = left > maxw ? maxw : left; + this.value = (left * this.max) / maxw; + this.calibrate(); + return this._onchanging({ + id: this.aid, + data: this.value, + }); + }); + + $(window).on("mouseup", (e) => { + this._onchange({ + id: this.aid, + data: this.value, + }); + $(window).unbind("mousemove", null); + return $(window).unbind("mouseup", null); + }); + }); + } + + /** + * Layout definition + * + * @protected + * @returns {TagLayoutType[]} + * @memberof SliderTag + */ + protected layout(): TagLayoutType[] { + return [ + { + el: "div", + class: "container", + ref: "container", + children: [ + { el: "div", class: "progress", ref: "prg" }, + { el: "div", class: "dragpoint", ref: "point" }, + ], + }, + ]; + } + } + + define("afx-slider", SliderTag); + } + } +} diff --git a/src/core/tags/SwitchTag.ts b/src/core/tags/SwitchTag.ts new file mode 100644 index 0000000..04bcd46 --- /dev/null +++ b/src/core/tags/SwitchTag.ts @@ -0,0 +1,144 @@ +namespace OS { + export namespace GUI { + export namespace tag { + /** + * A switch tag is basically used to visualize an boolean data value. + * + * @export + * @class SwitchTag + * @extends {AFXTag} + */ + export class SwitchTag extends AFXTag { + /** + * Placeholder for the onchange event handle + * + * @private + * @type {TagEventCallback} + * @memberof SwitchTag + */ + private _onchange: TagEventCallback; + + /** + * Setter: Turn on/off the switch + * + * Getter: Check whether the switch is turned on + * + * @memberof SwitchTag + */ + set swon(v: boolean) { + this.attsw(v, "swon"); + $(this.refs.switch).removeClass(); + if (v) { + $(this.refs.switch).addClass("swon"); + } + } + get swon(): boolean { + return this.hasattr("swon"); + } + + /** + * Setter: Enable the switch + * + * Getter: Check whether the switch is enabled + * + * @memberof SwitchTag + */ + set enable(v: boolean) { + this.attsw(v, "enable"); + } + get enable(): boolean { + return this.hasattr("enable"); + } + + /** + * Set the onchange event handle + * + * @memberof SwitchTag + */ + set onswchange(v: TagEventCallback) { + this._onchange = v; + } + + /** + * Mount the tag and bind the click event to the switch + * + * @protected + * @memberof SwitchTag + */ + protected mount(): void { + $(this.refs.switch).click((e) => { + return this.makechange(e); + }); + } + + /** + * This function will turn the switch (on/off) + * and trigger the onchange event + * + * @private + * @param {JQuery.ClickEvent} e + * @returns + * @memberof SwitchTag + */ + private makechange(e: JQuery.ClickEvent) { + if (!this.enable) { + return; + } + this.swon = !this.swon; + const evt = { id: this.aid, data: this.swon }; + this._onchange(evt); + return this.observable.trigger("switch", evt); + } + + /** + * Tag layout definition + * + * @protected + * @returns + * @memberof SwitchTag + */ + protected layout() { + return [ + { + el: "span", + ref: "switch", + }, + ]; + } + + /** + * Init the tag: + * - switch is turn off + * - switch is enabled + * + * @protected + * @memberof SwitchTag + */ + protected init(): void { + this.swon = false; + this.enable = true; + this._onchange = (e) => {}; + } + + /** + * Do nothing + * + * @protected + * @memberof SwitchTag + */ + protected calibrate(): void {} + + /** + * Do nothing + * + * @protected + * @param {*} [d] + * @memberof SwitchTag + */ + protected reload(d?: any): void {} + } + + define("afx-switch", SwitchTag); + } + } +} diff --git a/src/core/tags/SystemPanelTag.ts b/src/core/tags/SystemPanelTag.ts new file mode 100644 index 0000000..3f54b01 --- /dev/null +++ b/src/core/tags/SystemPanelTag.ts @@ -0,0 +1,387 @@ +namespace OS { + export namespace GUI { + export namespace tag { + /** + * A system panel contains the following elements: + * - Spotlight to access to applications menu + * - Current focused application menu + * - System tray for all running services running in background + * + * @export + * @class SystemPanelTag + * @extends {AFXTag} + */ + export class SystemPanelTag extends AFXTag { + /** + * Reference to spotlight data + * + * @private + * @type {(GenericObject)} + * @memberof SystemPanelTag + */ + private _osmenu: GenericObject; + + /** + * Placeholder indicates whether the spotlight is currently shown + * + * @private + * @type {boolean} + * @memberof SystemPanelTag + */ + private _view: boolean; + + /** + * Place holder for a private callback function + * + * @private + * @memberof SystemPanelTag + */ + private _cb: (e: JQuery.MouseEventBase) => void; + + /** + *Creates an instance of SystemPanelTag. + * @memberof SystemPanelTag + */ + constructor() { + super(); + this._osmenu = { + text: __("Start"), + iconclass: "fa fa-circle", + }; + this._view = false; + } + + /** + * Do nothing + * + * @protected + * @memberof SystemPanelTag + */ + protected init(): void {} + + /** + * Do nothing + * + * @protected + * @param {*} [d] + * @memberof SystemPanelTag + */ + protected reload(d?: any): void {} + + /** + * Attach a service to the system tray on the pannel, + * this operation is performed when a service is started + * + * @param {BaseService} s + * @returns + * @memberof SystemPanelTag + */ + attachservice(s: application.BaseService) { + (this.refs.systray as MenuTag).unshift(s); + return s.attach(this.refs.systray); + } + + /** + * Launch the selected application from the spotlight + * applications list + * + * @private + * @returns {void} + * @memberof SystemPanelTag + */ + private open(): void { + const applist = this.refs.applist as ListViewTag; + const el = applist.selectedItem; + if (!el) { + return; + } + if (!el.data || el.data.dataid === "header") { + return; + } + this.toggle(false); + // launch the app or open the file + Ant.OS.GUI.openWith(el.data as AppArgumentsType); + applist.unselect(); + } + + /** + * Perform spotlight search operation on keyboard event + * + * @private + * @param {JQuery.KeyboardEventBase} e + * @returns {void} + * @memberof SystemPanelTag + */ + private search(e: JQuery.KeyboardEventBase): void { + const applist = this.refs.applist as ListViewTag; + switch (e.which) { + case 27: + // escape key + return this.toggle(false); + + case 37: + return e.preventDefault(); + case 38: + applist.selectPrev(); + return e.preventDefault(); + case 39: + return e.preventDefault(); + case 40: + applist.selectNext(); + return e.preventDefault(); + case 13: + e.preventDefault(); + return this.open(); + default: + var text = (this.refs.search as HTMLInputElement) + .value; + if (!(text.length >= 3)) { + return this.refreshAppList(); + } + var result = Ant.OS.API.search(text); + if (result.length === 0) { + return; + } + applist.data = result; + } + } + + /** + * detach a service from the system tray of the panel. + * This function is called when the corresponding running + * service is killed + * + * @param {BaseService} s + * @memberof SystemPanelTag + */ + detachservice(s: application.BaseService): void { + (this.refs.systray as MenuTag).delete( + s.domel as MenuEntryTag + ); + } + + /** + * Layout definition of the panel + * + * @protected + * @returns {TagLayoutType[]} + * @memberof SystemPanelTag + */ + protected layout(): TagLayoutType[] { + return [ + { + el: "div", + ref: "panel", + children: [ + { + el: "afx-menu", + ref: "osmenu", + class: "afx-panel-os-menu", + }, + { + el: "afx-menu", + id: "appmenu", + ref: "appmenu", + class: "afx-panel-os-app", + }, + { + el: "afx-menu", + id: "systray", + ref: "systray", + class: "afx-panel-os-stray", + }, + ], + }, + { + el: "afx-overlay", + id: "start-panel", + ref: "overlay", + children: [ + { + el: "afx-hbox", + height: 30, + children: [ + { + el: "div", + width: 30, + id: "searchicon", + }, + { el: "input", ref: "search" }, + ], + }, + { + el: "afx-list-view", + id: "applist", + ref: "applist", + }, + { + el: "afx-hbox", + id: "btlist", + height: 30, + children: [ + { + el: "afx-button", + ref: "btscreen", + tooltip: __("ct:Toggle fullscreen"), + }, + { + el: "afx-button", + ref: "btuser", + tooltip: __( + "ct:User: {0}", + Ant.OS.setting.user.username + ), + }, + { + el: "afx-button", + ref: "btlogout", + tooltip: __("ct:Logout"), + }, + ], + }, + ], + }, + ]; + } + + /** + * Refresh applications list on the spotlight widget + * from system packages meta-data + * + * @private + * @memberof SystemPanelTag + */ + private refreshAppList(): void { + let k: string, v: API.PackageMetaType; + const list = []; + for (k in Ant.OS.setting.system.packages) { + v = Ant.OS.setting.system.packages[k]; + if (v && v.app) { + list.push(v); + } + } + for (k in Ant.OS.setting.system.menu) { + v = Ant.OS.setting.system.menu[k]; + list.push(v); + } + list.sort(function (a, b) { + if (a.text < b.text) { + return -1; + } else if (a.text > b.text) { + return 1; + } else { + return 0; + } + }); + (this.refs.applist as ListViewTag).data = list; + } + + /** + * Show/hide the spotlight + * + * @private + * @param {boolean} flag + * @memberof SystemPanelTag + */ + private toggle(flag: boolean): void { + this._view = flag; + if (flag) { + $(this.refs.overlay).show(); + this.refreshAppList(); + + this.calibrate(); + $(document).on("click", this._cb); + (this.refs.search as HTMLInputElement).value = ""; + $(this.refs.search).focus(); + } else { + $(this.refs.overlay).hide(); + $(document).unbind("click", this._cb); + } + } + + /** + * Calibrate the spotlight widget + * + * @memberof SystemPanelTag + */ + calibrate(): void { + (this.refs.overlay as OverlayTag).height = `${ + $(window).height() - $(this.refs.panel).height() + }px`; + } + + /** + * Mount the tag bind some basic event + * + * @protected + * @memberof SystemPanelTag + */ + protected mount(): void { + (this.refs.osmenu as MenuTag).items = [this._osmenu]; + this._cb = (e) => { + if ( + !$(e.target).closest($(this.refs.overlay)).length && + !$(e.target).closest(this.refs.osmenu).length + ) { + return this.toggle(false); + } else { + return $(this.refs.search).focus(); + } + }; + $(this.refs.appmenu).css("z-index", 1000000); + $(this.refs.systray).css("z-index", 1000000); + (this.refs.btscreen as ButtonTag).set({ + iconclass: "fa fa-tv", + onbtclick: (e) => { + this.toggle(false); + return Ant.OS.GUI.toggleFullscreen(); + }, + }); + (this.refs.btuser as ButtonTag).set({ + iconclass: "fa fa-user-circle-o", + onbtclick: (e) => { + this.toggle(false); + return Ant.OS.GUI.openDialog( + "InfoDialog", + Ant.OS.setting.user + ); + }, + }); + (this.refs.btlogout as ButtonTag).set({ + iconclass: "fa fa-power-off", + onbtclick: (e) => { + this.toggle(false); + return Ant.OS.exit(); + }, + }); + (this.refs.osmenu as MenuTag).onmenuselect = (e) => { + return this.toggle(true); + }; + + $(this.refs.search).keyup((e) => { + return this.search(e); + }); + + $(this.refs.applist).click((e) => { + return this.open(); + }); + Ant.OS.GUI.bindKey("CTRL- ", (e) => { + if (this._view === false) { + return this.toggle(true); + } else { + return this.toggle(false); + } + }); + Ant.OS.announcer.trigger("syspanelloaded", undefined); + $(this.refs.overlay) + .css("left", 0) + .css("top", `${$(this.refs.panel).height()}px`) + .css("bottom", "0") + .hide(); + } + } + + define("afx-sys-panel", SystemPanelTag); + } + } +} diff --git a/src/core/tags/TabBarTag.ts b/src/core/tags/TabBarTag.ts new file mode 100644 index 0000000..b18d463 --- /dev/null +++ b/src/core/tags/TabBarTag.ts @@ -0,0 +1,204 @@ +namespace OS { + export namespace GUI { + export namespace tag { + /** + * Tag event data type definition + */ + type TabEventData = TagEventDataType; + /** + * a TabBar allows to control a collection of tabs + * + * @export + * @class TabBarTag + * @extends {AFXTag} + */ + export class TabBarTag extends AFXTag { + /** + * Placeholder of currently selected tab index + * + * @private + * @type {number} + * @memberof TabBarTag + */ + private _selected: number; + + /** + * Placeholder of tab close event handle + * + * @private + * @memberof TabBarTag + */ + private _ontabclose: (e: TagEventType) => boolean; + + /** + * Placeholder of tab select event handle + * + * @private + * @type {TagEventCallback} + * @memberof TabBarTag + */ + private _ontabselect: TagEventCallback; + + /** + *Creates an instance of TabBarTag. + * @memberof TabBarTag + */ + constructor() { + super(); + this._ontabclose = (e) => true; + this._ontabselect = (e) => {}; + } + + /** + * Init the tag + * + * @protected + * @memberof TabBarTag + */ + protected init(): void { + this.selected = -1; + } + + /** + * Do nothing + * + * @protected + * @param {*} [d] + * @memberof TabBarTag + */ + protected reload(d?: any): void {} + + /** + * Setter: Enable/disable a tab to be closed + * + * Getter: Check whether tabs can be closed + * + * @memberof TabBarTag + */ + set closable(v: boolean) { + this.attsw(v, "closable"); + } + get closable(): boolean { + return this.hasattr("closable"); + } + + /** + * Add a tab in the end of the tab bar + * + * @param {GenericObject} item tab data + * @memberof TabBarTag + */ + push(item: GenericObject): ListViewItemTag { + item.closable = this.closable; + return (this.refs.list as ListViewTag).push(item); + } + + /** + * Delete a tab + * + * @param {ListViewItemTag} el reference to DOM element of a tab + * @memberof TabBarTag + */ + delete(el: ListViewItemTag) { + (this.refs.list as ListViewTag).delete(el); + } + + /** + * Add a tab to the beginning of the tab bar + * + * @param {GenericObject} item tab data + * @memberof TabBarTag + */ + unshift(item: GenericObject): ListViewItemTag { + item.closable = this.closable; + return (this.refs.list as ListViewTag).unshift(item); + } + + /** + * Setter: Set tabs data + * + * Getter: Get all tabs data + * + * @memberof TabBarTag + */ + set items(v: GenericObject[]) { + for (let i of v) { + i.closable = this.closable; + } + (this.refs.list as ListViewTag).data = v; + } + get items(): GenericObject[] { + return (this.refs.list as ListViewTag).data; + } + + /** + * Setter: Select a tab by its index + * + * Getter: Get the currently selected tab + * + * @memberof TabBarTag + */ + set selected(v: number | number[]) { + (this.refs.list as ListViewTag).selected = v; + } + get selected(): number | number[] { + return (this.refs.list as ListViewTag).selected; + } + + /** + * Set the tab close event handle + * + * @memberof TabBarTag + */ + set ontabclose(v: (e: TagEventType) => boolean) { + this._ontabclose = v; + } + + /** + * Set the tab select event handle + * + * @memberof TabBarTag + */ + set ontabselect(v: TagEventCallback) { + this._ontabselect = v; + } + + /** + * Mount the tab bar and bind some basic events + * + * @protected + * @memberof TabBarTag + */ + protected mount(): void { + $(this.refs.list).css("height", "100%"); + (this.refs.list as ListViewTag).onitemclose = (e) => { + e.id = this.aid; + return this._ontabclose(e); + }; + (this.refs.list as ListViewTag).onlistselect = (e) => { + this._ontabselect(e); + return this.observable.trigger("tabselect", e); + }; + } + + /** + * TabBar layout definition + * + * @protected + * @returns {TagLayoutType[]} + * @memberof TabBarTag + */ + protected layout(): TagLayoutType[] { + return [ + { + el: "afx-list-view", + ref: "list", + }, + ]; + } + } + + define("afx-tab-bar", TabBarTag); + } + } +} diff --git a/src/core/tags/TabContainerTag.ts b/src/core/tags/TabContainerTag.ts new file mode 100644 index 0000000..efd05ab --- /dev/null +++ b/src/core/tags/TabContainerTag.ts @@ -0,0 +1,245 @@ +namespace OS { + export namespace GUI { + /** + * Tab container data type definition + * + * @export + * @interface TabContainerTabType + */ + export interface TabContainerTabType { + /** + * Reference to the DOM element of the current container + * + * @type {HTMLElement} + * @memberof TabContainerTabType + */ + container: HTMLElement; + + [propName: string]: any; + } + export namespace tag { + /** + * A tab container allows to attach each tab on a [[TabBarTag]] + * with a container widget. The attached container widget should be + * composed inside a [[HBoxTag]] + * + * The tab bar in a tab container can be configured to display tabs + * in horizontal (row) or vertical (column) order. Default to vertical order + * + * Once a tab is selected, its attached container will be shown + * + * @export + * @class TabContainerTag + * @extends {AFXTag} + */ + export class TabContainerTag extends AFXTag { + /** + * Reference to the currently selected tab DOM element + * + * @private + * @type {TabContainerTabType} + * @memberof TabContainerTag + */ + private _selectedTab: TabContainerTabType; + + /** + * Placeholder of the tab select event handle + * + * @private + * @type {TagEventCallback} + * @memberof TabContainerTag + */ + private _ontabselect: TagEventCallback; + + /** + *Creates an instance of TabContainerTag. + * @memberof TabContainerTag + */ + constructor() { + super(); + this._ontabselect = (e) => {}; + } + + /** + * Init the tab bar direction to vertical (column) + * + * @protected + * @memberof TabContainerTag + */ + protected init(): void { + this.dir = "column"; // or row + } + + /** + * Do nothing + * + * @protected + * @param {*} [d] + * @memberof TabContainerTag + */ + protected reload(d?: any): void {} + + /** + * Set the tab select event handle + * + * @memberof TabContainerTag + */ + set ontabselect(f: TagEventCallback) { + this._ontabselect = f; + } + + /** + * Setter: + * + * Set the tab bar direction: + * - `row`: horizontal direction + * - `column`: vertical direction + * + * Getter: + * + * Get the tab bar direction + * + * @memberof TabContainerTag + */ + set dir(v: "row" | "column") { + $(this).attr("dir", v); + if (!v) { + return; + } + (this.refs.wrapper as TileLayoutTag).dir = v; + } + get dir(): "row" | "column" { + return $(this).attr("dir") as any; + } + + /** + * Setter: + * + * Select a tab using the its tab data type. + * This will show the attached container to the tab + * + * Getter: + * + * Get the tab data of the currently selected Tab + * + * @memberof TabContainerTag + */ + set selectedTab(v: TabContainerTabType) { + if (!v) { + return; + } + const selected = this._selectedTab; + this._selectedTab = v; + if (selected) { + $(selected.container).hide(); + } + $(v.container).show(); + this.observable.trigger("resize", undefined); + } + get selectedTab(): TabContainerTabType { + return this._selectedTab; + } + + /** + * Set the tab bar width, this function only + * works when the tab bar direction is set to + * `row` + * + * @memberof TabContainerTag + */ + set tabbarwidth(v: number) { + if (!v) { + return; + } + $(this.refs.bar).attr("data-width", `${v}`); + (this.refs.wrapper as TileLayoutTag).calibrate(); + } + + /** + * Set the tab bar height, this function only works + * when the tab bar direction is set to `column` + * + * @memberof TabContainerTag + */ + set tabbarheight(v: number) { + $(this.refs.bar).attr("data-height", `${v}`); + (this.refs.wrapper as TileLayoutTag).calibrate(); + } + + /** + * Mount the tag and bind basic events + * + * @protected + * @memberof TabContainerTag + */ + protected mount(): void { + (this.refs.bar as TabBarTag).ontabselect = (e) => { + const data = (e.data.item as ListViewItemTag) + .data as TabContainerTabType; + this.selectedTab = data; + return this._ontabselect({ data: data, id: this.aid }); + }; + this.observable.one("mounted", (id) => { + $(this.refs.yield) + .children() + .each((i, e) => { + const item = {} as GenericObject; + if ($(e).attr("tabname")) { + item.text = $(e).attr("tabname"); + } + if ($(e).attr("icon")) { + item.icon = $(e).attr("icon"); + } + if ($(e).attr("iconclass")) { + item.iconclass = $(e).attr("iconclass"); + } + item.container = e; + $(e) + .css("width", "100%") + .css("height", "100%") + .hide(); + const el = (this.refs.bar as TabBarTag).push( + item + ); + el.selected = true; + }); + }); + + this.observable.on("resize", (e) => this.calibrate()); + this.calibrate(); + } + + /** + * calibrate the tab container + * + * @memberof TabContainerTag + */ + calibrate(): void { + $(this.refs.wrapper).css("height", `${$(this).height()}px`); + } + + /** + * Layout definition + * + * @protected + * @returns {TagLayoutType[]} + * @memberof TabContainerTag + */ + protected layout(): TagLayoutType[] { + return [ + { + el: "afx-tile", + ref: "wrapper", + children: [ + { el: "afx-tab-bar", ref: "bar" }, + { el: "div", ref: "yield" }, + ], + }, + ]; + } + } + + define("afx-tab-container", TabContainerTag); + } + } +} diff --git a/src/core/tags/TileLayoutTags.ts b/src/core/tags/TileLayoutTags.ts new file mode 100644 index 0000000..c4b1431 --- /dev/null +++ b/src/core/tags/TileLayoutTags.ts @@ -0,0 +1,298 @@ +namespace OS { + export namespace GUI { + export namespace tag { + /** + * A tile layout organize it child elements + * in a fixed horizontal or vertical direction. + * + * The size of each child element is attributed based + * on its configuration of automatically based on the + * remaining space in the layout + * + * + * @export + * @class TileLayoutTag + * @extends {AFXTag} + */ + export class TileLayoutTag extends AFXTag { + /** + *C reates an instance of TileLayoutTag. + * @memberof TileLayoutTag + */ + constructor() { + super(); + } + + /** + * Do nothing + * + * @protected + * @memberof TileLayoutTag + */ + protected init(): void {} + /** + * Do nothing + * + * @protected + * @param {*} [d] + * @memberof TileLayoutTag + */ + protected reload(d?: any): void {} + + /** + * Setter: Set the name of the tile container, should be: `hbox` or `vbox` + * + * Getter: Get the name of the tile container + * + * @memberof TileLayoutTag + */ + set name(v: string) { + if (!v) { + return; + } + $(this).attr("name", v); + $(this.refs.yield) + .removeClass() + .addClass(`afx-${v}-container`); + this.calibrate(); + } + get name(): string { + return $(this).attr("name"); + } + + /** + * Setter: + * + * SET the layout direction, should be: + * - `row`: horizontal direction + * - `column`: vertical direction + * + * Getter: + * + * Get layout direction + * + * @memberof TileLayoutTag + */ + set dir(v: "row" | "column") { + if (!v) { + return; + } + $(this).attr("dir", v); + $(this.refs.yield).css("flex-direction", v); + this.calibrate(); + } + get dir(): "row" | "column" { + return $(this).attr("dir") as any; + } + + /** + * Mount the element + * + * @protected + * @returns {void} + * @memberof TileLayoutTag + */ + protected mount(): void { + $(this).css("display", "block"); + $(this.refs.yield) + .css("display", "flex") + .css("width", "100%") + .css("height", "100%"); + this.observable.on("resize", (e) => this.calibrate()); + return this.calibrate(); + } + + /** + * re-organize the layout + * + * @returns {void} + * @memberof TileLayoutTag + */ + calibrate(): void { + if (this.dir === "row") { + return this.hcalibrate(); + } + if (this.dir === "column") { + return this.vcalibrate(); + } + } + + /** + * Organize the layout in horizontal direction, only work when + * the layout direction set to horizontal + * + * @private + * @returns {void} + * @memberof TileLayoutTag + */ + private hcalibrate(): void { + const auto_width = []; + let ocwidth = 0; + const avaiheight = $(this).height(); + const avaiWidth = $(this).width(); + //$(this.refs.yield).css("height", `${avaiheight}px`); + $(this.refs.yield) + .children() + .each(function (e) { + $(this).css("height", "100%"); + let attv = $(this).attr("data-width"); + let dw = 0; + if (attv && attv !== "grow") { + if (attv[attv.length - 1] === "%") { + dw = + (parseInt(attv.slice(0, -1)) * + avaiWidth) / + 100; + } else { + dw = parseInt(attv); + } + $(this).css("width", `${dw}px`); + ocwidth += dw; + } else { + $(this).css("flex-grow", "1"); + auto_width.push(this); + } + }); + + const csize = (avaiWidth - ocwidth) / auto_width.length; + if (csize > 0) { + $.each(auto_width, (i, v) => + $(v).css("width", `${csize}px`) + ); + } + return this.observable.trigger("hboxchange", { + id: this.aid, + data: { w: avaiWidth, h: avaiheight }, + }); + } + + /** + * Organize the layout in vertical direction, only work when + * the layout direction set to vertical + * + * @private + * @returns {void} + * @memberof TileLayoutTag + */ + private vcalibrate(): void { + const auto_height = []; + let ocheight = 0; + const avaiheight = $(this).height(); + const avaiwidth = $(this).width(); + //$(this.refs.yield).css("height", `${avaiheight}px`); + $(this.refs.yield) + .children() + .each(function (e) { + let dh = 0; + $(this).css("width", "100%"); + let attv = $(this).attr("data-height"); + if (attv && attv !== "grow") { + if (attv[attv.length - 1] === "%") { + dh = + (parseInt(attv.slice(0, -1)) * + avaiheight) / + 100; + } else { + dh = parseInt(attv); + } + $(this).css("height", `${dh}px`); + ocheight += dh; + } else { + $(this).css("flex-grow", "1"); + auto_height.push(this); + } + }); + + const csize = (avaiheight - ocheight) / auto_height.length; + if (csize > 0) { + $.each(auto_height, (i, v) => + $(v).css("height", `${csize}px`) + ); + } + + return this.observable.trigger("vboxchange", { + id: this.aid, + data: { w: avaiwidth, h: avaiheight }, + }); + } + + /** + * Layout definition + * + * @returns + * @memberof TileLayoutTag + */ + layout() { + return [ + { + el: "div", + ref: "yield", + }, + ]; + } + } + + /** + * A HBox organize its child elements in horizontal direction + * + * @export + * @class HBoxTag + * @extends {TileLayoutTag} + */ + export class HBoxTag extends TileLayoutTag { + /** + * Creates an instance of HBoxTag. + * @memberof HBoxTag + */ + constructor() { + super(); + } + + /** + * Mount the tag + * + * @protected + * @memberof HBoxTag + */ + protected mount(): void { + super.mount(); + this.dir = "row"; + this.name = "hbox"; + } + } + + /** + * A VBox organize its child elements in vertical direction + * + * @export + * @class VBoxTag + * @extends {TileLayoutTag} + */ + export class VBoxTag extends TileLayoutTag { + /** + *Creates an instance of VBoxTag. + * @memberof VBoxTag + */ + constructor() { + super(); + } + + /** + * Mount the tag + * + * @protected + * @memberof VBoxTag + */ + protected mount(): void { + super.mount(); + this.dir = "column"; + this.name = "vbox"; + } + } + + define("afx-tile", TileLayoutTag); + define("afx-hbox", HBoxTag); + define("afx-vbox", VBoxTag); + } + } +} diff --git a/src/core/tags/TreeViewTag.ts b/src/core/tags/TreeViewTag.ts new file mode 100644 index 0000000..7b4a5f6 --- /dev/null +++ b/src/core/tags/TreeViewTag.ts @@ -0,0 +1,993 @@ +namespace OS { + export namespace GUI { + export namespace tag { + /** + * Tree view data type definition + * + * @export + * @interface TreeViewDataType + */ + export interface TreeViewDataType { + /** + * The child nodes data of the current tree node + * + * @type {TreeViewDataType[]} + * @memberof TreeViewDataType + */ + nodes?: TreeViewDataType[]; + + /** + * Boolean indicates whether the current node is opened. + * Only work when the current node is not a leaf node + * + * @type {boolean} + * @memberof TreeViewDataType + */ + open?: boolean; + + /** + * The node's path from the root node + * + * @type {string} + * @memberof TreeViewDataType + */ + path?: string; + + /** + * Indicates whether this node should be selected + * + * @type {boolean} + * @memberof TreeViewDataType + */ + selected?: boolean; + [propName: string]: any; + } + /** + * Tree node event data type definition + */ + export type TreeItemEventData = TagEventDataType< + TreeViewItemPrototype + >; + /** + * Abstract prototype of a tree node. All tree node definition should + * extend this class + * + * @class TreeViewItemPrototype + * @extends {AFXTag} + */ + export abstract class TreeViewItemPrototype extends AFXTag { + /** + * Node data placeholder + * + * @private + * @type {TreeViewDataType} + * @memberof TreeViewItemPrototype + */ + private _data: TreeViewDataType; + + /** + * Placeholder for the indent level of the current node from root node + * + * @private + * @type {number} + * @memberof TreeViewItemPrototype + */ + private _indent: number; + + /** + * private event object used by current node event + * + * @private + * @type {TagEventType} + * @memberof TreeViewItemPrototype + */ + private _evt: TagEventType; + + /** + * Reference to the root node + * + * @type {TreeViewTag} + * @memberof TreeViewItemPrototype + */ + treeroot: TreeViewTag; + + /** + * The tree path from the root node + * + * @type {string} + * @memberof TreeViewItemPrototype + */ + treepath: string; + + /** + * Reference to the parent node of the current node + * + * @type {TreeViewTag} + * @memberof TreeViewItemPrototype + */ + parent: TreeViewTag; + + /** + * Placeholder for the `fetch` function of the node. + * This function is used to fetch the child nodes of the + * current nodes. This function should return a promise on + * a list of [[TreeViewDataType]] + * + * @memberof TreeViewItemPrototype + */ + fetch: ( + d: TreeViewItemPrototype + ) => Promise; + /** + *Creates an instance of TreeViewItemPrototype. + * @memberof TreeViewItemPrototype + */ + constructor() { + super(); + } + + /** + * Update the tree, this function + * is used to refresh/expand/collapse the + * current node based on the input parameter + * + * @protected + * @param {*} p string indication, the value should be: + * - `expand`: expand the current node + * - `collapse`: collapse the current node + * - other string: this string is considered as a tree path of a node. If this value + * is the value of current node tree path, the node will be refreshed. Otherwise, nothing + * happens + * @returns {void} + * @memberof TreeViewItemPrototype + */ + protected reload(p: any): void { + if (!p || typeof p !== "string") { + return; + } + switch (p) { + case "expand": + this.open = true; + break; + case "collapse": + this.open = false; + break; + default: + if (p !== this.treepath) { + return; + } + this.open = true; + } + } + + /** + * Setter: + * + * Set the data of the current node. This will trigger the + * [[ondatachange]] function + * + * Getter: + * + * Get the current node's data + * + * @memberof TreeViewItemPrototype + */ + set data(v: TreeViewDataType) { + this._data = v; + if (!v) { + return; + } + this.open = v.open; + if (v.path) { + this.treepath = v.path; + } + this.selected = v.selected; + v.domel = this; + this.ondatachange(); + } + get data(): TreeViewDataType { + return this._data; + } + + /** + * Setter: + * + * Select or unselect the current node. + * This will trigger the item select event + * on the tree root if the parameter is `true` + * + * Getter: + * + * Check whether the current node is selected + * + * @memberof TreeViewItemPrototype + */ + set selected(v: boolean) { + if (!this._data) { + return; + } + this.attsw(v, "selected"); + $(this.refs.wrapper).removeClass(); + this._data.selected = v; + if (v) { + this.treeroot.unselect(); + // set selectedItem but not trigger the update + this.treeroot.itemclick(this._evt); + this._evt.data.dblclick = false; + $(this.refs.wrapper).addClass("afx_tree_item_selected"); + } + } + get selected(): boolean { + return this.hasattr("selected"); + } + + /** + * Setter: + * + * Refresh the current node and expands its sub tree. + * This function only works if the current node is not + * a leaf node + * + * Getter: + * + * Check whether the current node is expanded + * + * @memberof TreeViewItemPrototype + */ + set open(v: boolean) { + if (!this.is_folder()) { + return; + } + this.attsw(v, "open"); + $(this.refs.toggle).removeClass(); + if (v) { + if (this.fetch) { + this.fetch(this) + .then((d: TreeViewDataType[]) => { + if (!d) { + return; + } + return (this.nodes = d); + }) + .catch((e: Error) => + announcer.oserror(e.toString(), e) + ); + } else { + this.nodes = this.nodes; + } + $(this.refs.childnodes).show(); + } else { + $(this.refs.childnodes).hide(); + } + if (v) { + $(this.refs.toggle).addClass( + "afx-tree-view-folder-open" + ); + } else { + $(this.refs.toggle).addClass( + "afx-tree-view-folder-close" + ); + } + } + get open(): boolean { + return this.hasattr("open"); + } + /** + * Setter: Set the current indent level of this node from the root node + * + * Getter: Get the current indent level + * + * @type {number} + * @memberof TreeViewItemPrototype + */ + get indent(): number { + return this._indent; + } + set indent(v: number) { + if (!v) { + return; + } + this._indent = v; + $(this.refs.padding) + .css("display", "inline-block") + .css("height", "1px") + .css("padding", 0) + .css("margin", 0) + .css("background-color", "transparent") + .css("width", v * 15 + "px"); + } + + /** + * Check whether the current node is not a leaf node + * + * @private + * @returns {boolean} + * @memberof TreeViewItemPrototype + */ + private is_folder(): boolean { + if (this.nodes) { + return true; + } else { + return false; + } + } + + /** + * Getter: Get the child nodes data of the current node + * + * Setter: Set the child nodes data of the current node + * + * @type {TreeViewDataType[]} + * @memberof TreeViewItemPrototype + */ + get nodes(): TreeViewDataType[] { + if (!this._data) return undefined; + return this._data.nodes; + } + set nodes(nodes: TreeViewDataType[]) { + if (!nodes || !this.data) { + return; + } + this._data.nodes = nodes; + // return unless @get("nodes") and @get("nodes").length > 0 + $(this.refs.childnodes).empty(); + $(this.refs.wrapper).addClass("afx_folder_item"); + const root = this.treeroot; + const result = []; + for (let v of nodes) { + const el = $("").appendTo( + this.refs.childnodes + ); + el[0].uify(this.observable); + const element = el[0] as TreeViewTag; + element.treeroot = root; + element.indent = this.indent + 1; + element.open = this.open; + element.parent = this.parent; + element.treepath = `${this.treepath}/${element.aid}`; + element.fetch = this.fetch; + element.data = v; + } + } + + /** + * Init the tag with default properties data + * + * @protected + * @memberof TreeViewItemPrototype + */ + protected init(): void { + this.treeroot = undefined; + this.treepath = this.aid.toString(); + this._evt = { + id: this.aid, + data: { item: this, dblclick: false }, + }; + this._indent = 0; + } + /** + * Mount the tag and bind basic events + * + * @protected + * @memberof TreeViewItemPrototype + */ + protected mount(): void { + $(this.refs.container) + .css("padding", 0) + .css("margin", 0) + .css("white-space", "nowrap"); + $(this.refs.itemholder).css("display", "inline-block"); + $(this.refs.wrapper).click((e) => { + this.selected = true; + }); + $(this.refs.wrapper).dblclick((e) => { + this._evt.data.dblclick = true; + this.selected = true; + }); + + $(this.refs.toggle) + .css("display", "inline-block") + .css("width", "15px") + .addClass("afx-tree-view-item") + .click((e) => { + this.open = !this.open; + e.preventDefault(); + return e.stopPropagation(); + }); + } + + /** + * Layout definition of a node. This function + * returns the definition of the base outer layout + * of a node. Custom inner layout of the node should + * be defined in the [[itemlayout]] function + * + * @protected + * @returns {TagLayoutType[]} + * @memberof TreeViewItemPrototype + */ + protected layout(): TagLayoutType[] { + return [ + { + el: "div", + ref: "wrapper", + children: [ + { + el: "ul", + ref: "container", + children: [ + { el: "li", ref: "padding" }, + { el: "li", ref: "toggle" }, + { + el: "li", + ref: "itemholder", + class: "itemname", + children: this.itemlayout(), + }, + ], + }, + ], + }, + { + el: "ul", + ref: "childnodes", + }, + ]; + } + + /** + * This function need to be implemented by all subclasses + * to define the inner layout of the node + * + * @protected + * @abstract + * @returns {TagLayoutType[]} + * @memberof TreeViewItemPrototype + */ + protected abstract itemlayout(): TagLayoutType[]; + + /** + * This function is called when the node data change. + * It needs to be implemented on all subclasses of this + * class + * + * @protected + * @abstract + * @memberof TreeViewItemPrototype + */ + protected abstract ondatachange(): void; + } + + /** + * SimpleTreeViewItem extends [[TreeViewItemPrototype]] and + * define it inner layout using a [[LabelTag]] + * + * @export + * @class SimpleTreeViewItem + * @extends {TreeViewItemPrototype} + */ + export class SimpleTreeViewItem extends TreeViewItemPrototype { + /** + *Creates an instance of SimpleTreeViewItem. + * @memberof SimpleTreeViewItem + */ + constructor() { + super(); + } + + /** + * Refresh the label when data changed + * + * @protected + * @returns {void} + * @memberof SimpleTreeViewItem + */ + protected ondatachange(): void { + if (!this.data) { + return; + } + const v = this.data; + const label = this.refs.label as LabelTag; + label.set(v); + } + + /** + * Inner layout definition + * + * @protected + * @returns + * @memberof SimpleTreeViewItem + */ + protected itemlayout() { + return [{ el: "afx-label", ref: "label" }]; + } + } + + /** + * A tree view widget presents a hierarchical list of nodes. + * + * @export + * @class TreeViewTag + * @extends {AFXTag} + */ + export class TreeViewTag extends AFXTag { + /** + * Reference to the selected node + * + * @private + * @type {TreeViewItemPrototype} + * @memberof TreeViewTag + */ + private _selectedItem: TreeViewItemPrototype; + + /** + * Placeholder for tree select event handle + * + * @private + * @type {TagEventCallback} + * @memberof TreeViewTag + */ + private _ontreeselect: TagEventCallback; + + /** + * Place holder for tree double click event handle + * + * @private + * @type {TagEventCallback} + * @memberof TreeViewTag + */ + private _ontreedbclick: TagEventCallback; + + /** + * Placeholder for drag and drop event handle + * + * @private + * @type {TagEventCallback>} + * @memberof TreeViewTag + */ + private _ondragndrop: TagEventCallback< + DnDEventDataType + >; + + /** + * Tree data placeholder + * + * @private + * @type {TreeViewDataType} + * @memberof TreeViewTag + */ + private _data: TreeViewDataType; + + /** + * Placeholder for private dragndrop mouse down event handle + * + * @private + * @memberof TreeViewTag + */ + private _treemousedown: (e: JQuery.MouseEventBase) => void; + + /** + * Placeholder for private dragndrop mouse up event handle + * + * @private + * @memberof TreeViewTag + */ + private _treemouseup: (e: JQuery.MouseEventBase) => void; + + /** + * Placeholder for private dragndrop mouse move event handle + * + * @private + * @memberof TreeViewTag + */ + private _treemousemove: (e: JQuery.MouseEventBase) => void; + + /** + * Private data object passing between dragndrop mouse event + * + * @private + * @type {{ from: TreeViewTag; to: TreeViewTag }} + * @memberof TreeViewTag + */ + private _dnd: { from: TreeViewTag; to: TreeViewTag }; + + /** + * Reference to parent tree of the current tree. + * This value is undefined if the current tree is the root + * + * @type {TreeViewTag} + * @memberof TreeViewTag + */ + parent: TreeViewTag; + + /** + * Reference to the root tree, this value is undefined + * if the curent tree is root + * + * @type {TreeViewTag} + * @memberof TreeViewTag + */ + treeroot: TreeViewTag; + + /** + * tree path of the current tree from the root + * + * @type {string} + * @memberof TreeViewTag + */ + treepath: string; + + /** + * Indent level of the current tree + * + * @type {number} + * @memberof TreeViewTag + */ + indent: number; + + /** + * Indicates whether the tree should be expanded + * + * @type {boolean} + * @memberof TreeViewTag + */ + open: boolean; + /** + * Placeholder for the `fetch` function of the tree. + * This function is used to fetch the child nodes of the + * current tree. This function should return a promise on + * a list of [[TreeViewDataType]] + * + * @memberof TreeViewItemPrototype + */ + fetch: ( + d: TreeViewItemPrototype + ) => Promise; + + /** + *Creates an instance of TreeViewTag. + * @memberof TreeViewTag + */ + constructor() { + super(); + } + + /** + * Init the tree view before mounting: + * + * @protected + * @memberof TreeViewTag + */ + protected init(): void { + this.itemtag = "afx-tree-view-item"; + this._ontreeselect = this._ondragndrop = this._ontreedbclick = ( + e + ) => {}; + + this.indent = 0; + this.open = true; + this.treepath = this.aid.toString(); + } + + /** + * Layout definition + * + * @protected + * @returns {TagLayoutType[]} + * @memberof TreeViewTag + */ + protected layout(): TagLayoutType[] { + return []; + } + + /** + * Do nothing + * + * @protected + * @param {*} [d] + * @memberof TreeViewTag + */ + protected reload(d?: any): void {} + /** + * Setter: Enable/disable drag and drop event on the tree + * + * Getter: Check whether the drag and drop event is enabled + * + * @memberof TreeViewTag + */ + set dragndrop(v: boolean) { + this.attsw(v, "dragndrop"); + } + get dragndrop(): boolean { + return this.hasattr("dragndrop"); + } + + /** + * Set the tree select event handle + * + * @memberof TreeViewTag + */ + set ontreeselect(v: TagEventCallback) { + this._ontreeselect = v; + } + + /** + * Set the tree double click event handle + * + * @memberof TreeViewTag + */ + set ontreedbclick(v: TagEventCallback) { + this._ontreedbclick = v; + } + + /** + * Setter: + * + * Set the default tag name of the tree node. + * If there is no tag name in the node data, + * this value will be used when creating node. + * + * Defaut to `afx-tree-view-item` + * + * Getter: + * + * Get the default node tag name + * + * @memberof TreeViewTag + */ + set itemtag(v: string) { + $(this).attr("itemtag", v); + } + get itemtag(): string { + return $(this).attr("itemtag"); + } + + /** + * Unselect the selected element in the tree + * + * @memberof TreeViewTag + */ + unselect(): void { + if (this.selectedItem) { + this._selectedItem.selected = false; + } + } + + /** + * Setter: Set the selected node using its DOM element + * + * Getter: Get the DOM element of the selected node + * + * @type {TreeViewItemPrototype} + * @memberof TreeViewTag + */ + get selectedItem(): TreeViewItemPrototype { + return this._selectedItem; + } + set selectedItem(v: TreeViewItemPrototype) { + if (!v) { + return; + } + if (v === this.selectedItem) { + return; + } + v.selected = true; + } + + /** + * Expand all nodes in the tree + * + * @returns {void} + * @memberof TreeViewTag + */ + expandAll(): void { + if (this.is_leaf()) { + return; + } + return this.update("expand"); + } + + /** + * Collapse all nodes in the tree + * + * @returns {void} + * @memberof TreeViewTag + */ + collapseAll(): void { + if (this.is_leaf()) { + return; + } + return this.update("collapse"); + } + + /** + * This function will trigger the tree select or tree double click + * event + * + * @param {TagEventType} e + * @returns {void} + * @memberof TreeViewTag + */ + itemclick(e: TagEventType): void { + if (!e || !e.data) { + return; + } + if (e.data.item === this.selectedItem && !e.data.dblclick) { + return; + } + this._selectedItem = e.data.item; + const evt = { id: this.aid, data: e.data }; + if (e.data.dblclick) { + this._ontreedbclick(evt); + return this.observable.trigger("treedbclick", evt); + } else { + this._ontreeselect(evt); + return this.observable.trigger("treeselect", evt); + } + } + + /** + * Check whether the current tree is a root tree + * + * @returns {boolean} + * @memberof TreeViewTag + */ + is_root(): boolean { + return this.treeroot === undefined; + } + + /** + * Check whether the current tree tag is a leaf + * + * @returns {boolean} + * @memberof TreeViewTag + */ + is_leaf(): boolean { + const data = this.data; + if (!data) { + return true; + } + if (data.nodes) { + return false; + } else { + return true; + } + } + + /** + * Set drag and drop event handle + * + * @memberof TreeViewTag + */ + set ondragndrop( + v: TagEventCallback> + ) { + this._ondragndrop = v; + } + + /** + * Setter: + * + * Set the tree data. This operation will create + * all tree node elements of the current tree + * + * Getter: + * + * Get the tree data + * + * @memberof TreeViewTag + */ + set data(v: TreeViewDataType) { + this._selectedItem = undefined; + if (!v) { + return; + } + this._data = v; + $(this).empty(); + if (v.path) { + this.treepath = v.path; + } + let tag = this.itemtag; + if (v.tag) { + tag = v.tag; + } + const el = $(`<${tag}>`).appendTo(this); + el[0].uify(this.observable); + const element = el[0] as TreeViewItemPrototype; + element.treeroot = this.is_root() ? this : this.treeroot; + element.indent = this.indent; + element.treepath = this.treepath; + element.open = this.open; + element.fetch = this.fetch; + element.parent = this; + element.data = v; + if (this.is_root()) { + $(this).off("mousedown", this._treemousedown); + if (this.dragndrop) { + $(this).on("mousedown", this._treemousedown); + } + } + } + get data(): TreeViewDataType { + return this._data; + } + + /** + * Mount the tree view + * + * @protected + * @memberof TreeViewTag + */ + protected mount(): void { + this._dnd = { + from: undefined, + to: undefined, + }; + this._treemousedown = (e) => { + let obj: any = $(e.target).closest("afx-tree-view"); + if (obj.length === 0) { + return; + } + let el = obj[0] as TreeViewTag; + if (el === this) { + return; + } + this._dnd.from = el; + this._dnd.to = undefined; + $(window).on("mouseup", this._treemouseup); + return $(window).on("mousemove", this._treemousemove); + }; + + this._treemouseup = (e) => { + $(window).off("mouseup", this._treemouseup); + $(window).off("mousemove", this._treemousemove); + $("#systooltip").hide(); + let obj = $(e.target).closest("afx-tree-view"); + if (obj.length === 0) { + return; + } + let el = obj[0] as TreeViewTag; + if (el.is_leaf()) { + el = el.parent; + } + if ( + el === this._dnd.from || + el === this._dnd.from.parent + ) { + return; + } + this._dnd.to = el; + this._ondragndrop({ + id: this.aid, + data: this._dnd, + }); + this._dnd = { + from: undefined, + to: undefined, + }; + }; + + this._treemousemove = (e) => { + if (!e) { + return; + } + if (!this._dnd.from) { + return; + } + const data = this._dnd.from.data; + const $label = $("#systooltip"); + const top = e.clientY + 5; + const left = e.clientX + 5; + $label.show(); + const label = $label[0] as LabelTag; + label.set(data); + $label.css("top", top + "px").css("left", left + "px"); + }; + } + } + + define("afx-tree-view", TreeViewTag); + define("afx-tree-view-item", SimpleTreeViewItem); + } + } +} diff --git a/src/core/tags/WindowTag.ts b/src/core/tags/WindowTag.ts new file mode 100644 index 0000000..ee86136 --- /dev/null +++ b/src/core/tags/WindowTag.ts @@ -0,0 +1,532 @@ +namespace OS { + export namespace GUI { + export namespace tag { + /** + * A WindowTag represents a virtual window element + * used by AntOS applications and dialogs. + * + * @export + * @class WindowTag + * @extends {AFXTag} + */ + export class WindowTag extends AFXTag { + /** + * The element ID of the virtual desktop element + * + * @type {string} + * @memberof WindowTag + */ + desktop: string; + + /** + * Window width placeholder + * + * @private + * @type {number} + * @memberof WindowTag + */ + private _width: number; + + /** + * Window height placeholder + * + * @private + * @type {number} + * @memberof WindowTag + */ + private _height: number; + + /** + * Placeholder indicates whether the current window is shown + * + * @private + * @type {boolean} + * @memberof WindowTag + */ + private _shown: boolean; + + /** + * Placeholder indicates whether the current window is maximized + * + * @private + * @type {boolean} + * @memberof WindowTag + */ + private _isMaxi: boolean; + + /** + * This placeholder stores the latest offset of the current window. + * + * @private + * @type {GenericObject} + * @memberof WindowTag + */ + private _history: GenericObject; + + /** + * This placeholder stores the offset of the virtual desktop element + * + * @private + * @type {GenericObject} + * @memberof WindowTag + */ + private _desktop_pos: GenericObject; + + /** + * Creates an instance of WindowTag. + * @memberof WindowTag + */ + constructor() { + super(); + } + + /** + * Init window tag + * - `shown`: false + * - `isMaxi`: false + * - `minimizable`: false + * - `resizable`: true + * - `apptitle`: Untitled + * + * @protected + * @memberof WindowTag + */ + protected init(): void { + this._shown = false; + this._isMaxi = false; + this._history = {}; + this.desktop = GUI.workspace; + this._desktop_pos = $(this.desktop).offset(); + this.minimizable = true; + this.resizable = true; + this.apptitle = "Untitled"; + } + + /** + * Do nothing + * + * @protected + * @memberof WindowTag + */ + protected calibrate(): void {} + + /** + * Do nothing + * + * @protected + * @param {*} [d] + * @memberof WindowTag + */ + protected reload(d?: any): void {} + + /** + * Setter: Set the window width + * + * Getter: Get the window width + * + * @memberof WindowTag + */ + set width(v: number) { + this._width = v; + if (!v) { + return; + } + this.setsize({ w: v, h: this.height }); + } + get width(): number { + return this._width; + } + + /** + * Setter: Set the window height + * + * Getter: Get the window height + * + * @memberof WindowTag + */ + set height(v: number) { + this._height = v; + if (!v) { + return; + } + this.setsize({ + w: this.width, + h: v, + }); + } + get height(): number { + return this._height; + } + + /** + * Setter: enable/disable window minimizable + * + * getter: Check whether the window is minimizable + * + * @memberof WindowTag + */ + set minimizable(v: boolean) { + this.attsw(v, "minimizable"); + if (v) { + $(this.refs["minbt"]).show(); + } else { + $(this.refs["minbt"]).hide(); + } + } + get minimizable(): boolean { + return this.hasattr("minimizable"); + } + + /** + * Setter: enable/disable widow resizable + * + * Getter: Check whether the current window is resizable + * + * @memberof WindowTag + */ + set resizable(v: boolean) { + this.attsw(v, "resizable"); + if (v) { + $(this.refs["maxbt"]).show(); + $(this.refs["grip"]).show(); + } else { + $(this.refs["maxbt"]).hide(); + $(this.refs["grip"]).hide(); + } + } + get resizable(): boolean { + return this.hasattr("resizable"); + } + + /** + * Setter: Set the window title + * + * Getter: Get window title + * + * @memberof WindowTag + */ + set apptitle(v: string | FormattedString) { + $(this).attr("apptitle", v.__()); + if (v) { + (this.refs["txtTitle"] as LabelTag).text = v; + } + } + get apptitle(): string | FormattedString { + return $(this).attr("apptitle"); + } + + /** + * Resize all the children of the window based on its width and height + * + * @private + * @memberof WindowTag + */ + private resize(): void { + const ch = + $(this.refs["yield"]).height() / + $(this.refs["yield"]).children().length; + $(this.refs["yield"]) + .children() + .each(function (e) { + $(this).css("height", `${ch}px`); + }); + } + + /** + * Mount the window tag and bind basic events + * + * @protected + * @returns {void} + * @memberof WindowTag + */ + protected mount(): void { + this.contextmenuHandle = function (e) {}; + $(this.refs["minbt"]).click((e) => { + return this.observable.trigger("hide", { + id: this.aid, + }); + }); + + $(this.refs["maxbt"]).click((e) => { + return this.toggle_window(); + }); + + $(this.refs["closebt"]).click((e) => { + return this.observable.trigger("exit", { + id: this.aid, + }); + }); + const left = ($(this.desktop).width() - this.width) / 2; + const top = ($(this.desktop).height() - this.height) / 2; + $(this) + .css("position", "absolute") + .css("left", `${left}px`) + .css("top", `${top}px`) + .css("z-index", Ant.OS.GUI.zindex++); + $(this).on("mousedown", (e) => { + if (this._shown) { + return; + } + return this.observable.trigger("focus", { + id: this.aid, + }); + }); + + $(this.refs["dragger"]).dblclick((e) => { + return this.toggle_window(); + }); + + this.observable.on("resize", (e) => this.resize()); + + this.observable.on("focus", () => { + Ant.OS.GUI.zindex++; + $(this) + .show() + .css("z-index", Ant.OS.GUI.zindex) + .removeClass("unactive"); + this._shown = true; + }); + + this.observable.on("blur", () => { + this._shown = false; + return $(this).addClass("unactive"); + }); + this.observable.on("hide", () => { + $(this).hide(); + return (this._shown = false); + }); + + this.observable.on("toggle", () => { + if (this._shown) { + return this.observable.trigger("hide", { + id: this.aid, + }); + } else { + return this.observable.trigger("focus", { + id: this.aid, + }); + } + }); + this.enable_dragging(); + this.enable_resize(); + this.setsize({ + w: this.width, + h: this.height, + }); + return this.observable.trigger("rendered", { + id: this.aid, + }); + } + + /** + * Set the window size + * + * @private + * @param {GenericObject} o format: `{ w: window_width, h: window_height }` + * @returns {void} + * @memberof WindowTag + */ + private setsize(o: GenericObject): void { + if (!o) { + return; + } + this._width = o.w; + this._height = o.h; + $(this).css("width", `${o.w}px`).css("height", `${o.h}px`); + $(this.refs.winwrapper).css("height", `${o.h}px`); + this.observable.trigger("resize", { + id: this.aid, + data: o, + }); + } + + /** + * Enable to drag window on the virtual desktop + * + * @private + * @memberof WindowTag + */ + private enable_dragging(): void { + $(this.refs["dragger"]) + .css("user-select", "none") + .css("cursor", "default"); + $(this.refs["dragger"]).on("mousedown", (e) => { + e.preventDefault(); + const offset = $(this).offset(); + offset.top = e.clientY - offset.top; + offset.left = e.clientX - offset.left; + $(window).on("mousemove", (e) => { + let left: number, top: number; + if (this._isMaxi) { + this.toggle_window(); + top = 0; + const letf = e.clientX - $(this).width() / 2; + offset.top = 10; + offset.left = $(this).width() / 2; + } else { + top = + e.clientY - + offset.top - + this._desktop_pos.top; + left = + e.clientX - + this._desktop_pos.top - + offset.left; + left = left < 0 ? 0 : left; + top = top < 0 ? 0 : top; + } + + return $(this) + .css("top", `${top}px`) + .css("left", `${left}px`); + }); + return $(window).on("mouseup", function (e) { + $(window).unbind("mousemove", null); + return $(window).unbind("mouseup", null); + }); + }); + } + + /** + * Enable window resize, this only works if the window + * is resizable + * + * @private + * @memberof WindowTag + */ + private enable_resize(): void { + $(this.refs["grip"]) + .css("user-select", "none") + .css("cursor", "default") + .css("position", "absolute") + .css("bottom", "0") + .css("right", "0") + .css("cursor", "nwse-resize"); + + $(this.refs["grip"]).on("mousedown", (e) => { + e.preventDefault(); + const offset = { top: 0, left: 0 }; + offset.top = e.clientY; + offset.left = e.clientX; + $(window).on("mousemove", (e) => { + let w = $(this).width() + e.clientX - offset.left; + let h = $(this).height() + e.clientY - offset.top; + w = w < 100 ? 100 : w; + h = h < 100 ? 100 : h; + offset.top = e.clientY; + offset.left = e.clientX; + this._isMaxi = false; + this.setsize({ w, h }); + }); + + $(window).on("mouseup", function (e) { + $(window).unbind("mousemove", null); + return $(window).unbind("mouseup", null); + }); + }); + } + + /** + * Maximize the window or restore its previous width, height, + * and position + * + * @private + * @returns {void} + * @memberof WindowTag + */ + private toggle_window(): void { + let h: number, w: number; + if (!this.resizable) { + return; + } + if (this._isMaxi === false) { + this._history = { + top: $(this).css("top"), + left: $(this).css("left"), + width: $(this).css("width"), + height: $(this).css("height"), + }; + w = $(this.desktop).width(); + h = $(this.desktop).height(); + $(this).css("top", "0").css("left", "0"); + this.setsize({ w, h }); + this._isMaxi = true; + } else { + this._isMaxi = false; + $(this) + .css("top", this._history.top) + .css("left", this._history.left); + this.setsize({ + w: parseInt(this._history.width), + h: parseInt(this._history.height), + }); + } + } + + /** + * Layout definition of the window tag + * + * @protected + * @returns {TagLayoutType[]} + * @memberof WindowTag + */ + protected layout(): TagLayoutType[] { + return [ + { + el: "div", + class: "afx-window-wrapper", + ref: "winwrapper", + children: [ + { + el: "ul", + class: "afx-window-top", + children: [ + { + el: "li", + class: "afx-window-close", + ref: "closebt", + }, + { + el: "li", + class: "afx-window-minimize", + ref: "minbt", + }, + { + el: "li", + class: "afx-window-maximize", + ref: "maxbt", + }, + { + el: "li", + class: "afx-window-title", + ref: "dragger", + children: [ + { + el: "afx-label", + ref: "txtTitle", + }, + ], + }, + ], + }, + { el: "div", class: "afx-clear" }, + { + el: "div", + ref: "yield", + class: "afx-window-content", + }, + { + el: "div", + ref: "grip", + class: "afx-window-grip", + }, + ], + }, + ]; + } + } + + define("afx-app-window", WindowTag); + } + } +} diff --git a/src/core/tags/afx-app-window.tag b/src/core/tags/afx-app-window.tag deleted file mode 100644 index 53378d4..0000000 --- a/src/core/tags/afx-app-window.tag +++ /dev/null @@ -1,246 +0,0 @@ - -
-
    -
  • -
  • -
  • -
  • { apptitle?apptitle.__():apptitle }
  • -
-
-
- -
-
-
- - - \ No newline at end of file diff --git a/src/core/tags/afx-apps-dock.tag b/src/core/tags/afx-apps-dock.tag deleted file mode 100644 index d9afd19..0000000 --- a/src/core/tags/afx-apps-dock.tag +++ /dev/null @@ -1,82 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/core/tags/afx-button.tag b/src/core/tags/afx-button.tag deleted file mode 100644 index 84fc2cb..0000000 --- a/src/core/tags/afx-button.tag +++ /dev/null @@ -1,47 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/core/tags/afx-calendar-view.tag b/src/core/tags/afx-calendar-view.tag deleted file mode 100644 index dd41f5a..0000000 --- a/src/core/tags/afx-calendar-view.tag +++ /dev/null @@ -1,107 +0,0 @@ - -
- - -
- - - -
\ No newline at end of file diff --git a/src/core/tags/afx-color-picker.tag b/src/core/tags/afx-color-picker.tag deleted file mode 100644 index 6862df1..0000000 --- a/src/core/tags/afx-color-picker.tag +++ /dev/null @@ -1,110 +0,0 @@ - -
- -
-
-
- Hex: - -
-
- -
\ No newline at end of file diff --git a/src/core/tags/afx-dummy.tag b/src/core/tags/afx-dummy.tag deleted file mode 100644 index 5bf4fa0..0000000 --- a/src/core/tags/afx-dummy.tag +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/core/tags/afx-file-view.tag b/src/core/tags/afx-file-view.tag deleted file mode 100644 index 61a8284..0000000 --- a/src/core/tags/afx-file-view.tag +++ /dev/null @@ -1,226 +0,0 @@ - - - -
- -
- - -
\ No newline at end of file diff --git a/src/core/tags/afx-float-list.tag b/src/core/tags/afx-float-list.tag deleted file mode 100644 index 6632e19..0000000 --- a/src/core/tags/afx-float-list.tag +++ /dev/null @@ -1,183 +0,0 @@ - -
-
- -
-
- -
\ No newline at end of file diff --git a/src/core/tags/afx-grid-view.tag b/src/core/tags/afx-grid-view.tag deleted file mode 100644 index 7908375..0000000 --- a/src/core/tags/afx-grid-view.tag +++ /dev/null @@ -1,218 +0,0 @@ - - -
-
- -
-
- -
- - -
- -
- -
\ No newline at end of file diff --git a/src/core/tags/afx-hbox.tag b/src/core/tags/afx-hbox.tag deleted file mode 100644 index fbc732a..0000000 --- a/src/core/tags/afx-hbox.tag +++ /dev/null @@ -1,79 +0,0 @@ - -
- -
- -
\ No newline at end of file diff --git a/src/core/tags/afx-html.tag b/src/core/tags/afx-html.tag deleted file mode 100644 index 8f15866..0000000 --- a/src/core/tags/afx-html.tag +++ /dev/null @@ -1,20 +0,0 @@ - - - \ No newline at end of file diff --git a/src/core/tags/afx-label.tag b/src/core/tags/afx-label.tag deleted file mode 100644 index ec010ec..0000000 --- a/src/core/tags/afx-label.tag +++ /dev/null @@ -1,33 +0,0 @@ - - - - - { text?text.__():"" } - - - \ No newline at end of file diff --git a/src/core/tags/afx-list-view.tag b/src/core/tags/afx-list-view.tag deleted file mode 100644 index f704b8b..0000000 --- a/src/core/tags/afx-list-view.tag +++ /dev/null @@ -1,227 +0,0 @@ - -
-
- -
-
    -
  • - - -
      -
    • {ctn.text.toString()}
    • -
    -
  • -
-
-
- -
- -
\ No newline at end of file diff --git a/src/core/tags/afx-menu.tag b/src/core/tags/afx-menu.tag deleted file mode 100644 index 836f542..0000000 --- a/src/core/tags/afx-menu.tag +++ /dev/null @@ -1,179 +0,0 @@ - -
    -
  • -
  • 0, fix_padding:data.icon} no-reorder> - - - - {data.shortcut} - - - 0} child={data.child} onmenuselect = {data.onmenuselect} observable = {parent.root.observable} rootid = {parent.rid}> -
  • -
  • -
- -
\ No newline at end of file diff --git a/src/core/tags/afx-nspinner.tag b/src/core/tags/afx-nspinner.tag deleted file mode 100644 index 0ccd5cd..0000000 --- a/src/core/tags/afx-nspinner.tag +++ /dev/null @@ -1,100 +0,0 @@ - - -
    -
  • -
  • -
- -
\ No newline at end of file diff --git a/src/core/tags/afx-overlay.tag b/src/core/tags/afx-overlay.tag deleted file mode 100644 index a91a34a..0000000 --- a/src/core/tags/afx-overlay.tag +++ /dev/null @@ -1,45 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/core/tags/afx-resizer.tag b/src/core/tags/afx-resizer.tag deleted file mode 100644 index f240455..0000000 --- a/src/core/tags/afx-resizer.tag +++ /dev/null @@ -1,85 +0,0 @@ - - - diff --git a/src/core/tags/afx-slider.tag b/src/core/tags/afx-slider.tag deleted file mode 100644 index fe580d6..0000000 --- a/src/core/tags/afx-slider.tag +++ /dev/null @@ -1,107 +0,0 @@ - -
-
-
-
- -
\ No newline at end of file diff --git a/src/core/tags/afx-switch.tag b/src/core/tags/afx-switch.tag deleted file mode 100644 index 12f31db..0000000 --- a/src/core/tags/afx-switch.tag +++ /dev/null @@ -1,56 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/core/tags/afx-sys-panel.tag b/src/core/tags/afx-sys-panel.tag deleted file mode 100644 index a10e19d..0000000 --- a/src/core/tags/afx-sys-panel.tag +++ /dev/null @@ -1,54 +0,0 @@ - -
- - - -
- - -
\ No newline at end of file diff --git a/src/core/tags/afx-tab-bar.tag b/src/core/tags/afx-tab-bar.tag deleted file mode 100644 index 53db2b2..0000000 --- a/src/core/tags/afx-tab-bar.tag +++ /dev/null @@ -1,71 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/core/tags/afx-tab-container.tag b/src/core/tags/afx-tab-container.tag deleted file mode 100644 index 1926119..0000000 --- a/src/core/tags/afx-tab-container.tag +++ /dev/null @@ -1,91 +0,0 @@ - - - -
-
- - -
-
- -
\ No newline at end of file diff --git a/src/core/tags/afx-tree-view.tag b/src/core/tags/afx-tree-view.tag deleted file mode 100644 index a48317d..0000000 --- a/src/core/tags/afx-tree-view.tag +++ /dev/null @@ -1,169 +0,0 @@ - -
-
    -
  • -
  • - - - - - { data.name } -
  • -
-
-
    -
  • - " + i} treeroot= {parent.treeroot}> -
  • -
- - -
\ No newline at end of file diff --git a/src/core/tags/afx-vbox.tag b/src/core/tags/afx-vbox.tag deleted file mode 100644 index 40060be..0000000 --- a/src/core/tags/afx-vbox.tag +++ /dev/null @@ -1,81 +0,0 @@ - -
- -
- -
\ No newline at end of file diff --git a/src/core/tags/tag.ts b/src/core/tags/tag.ts new file mode 100644 index 0000000..9ba7bbd --- /dev/null +++ b/src/core/tags/tag.ts @@ -0,0 +1,665 @@ +/** + * + * Extend the HTMLElement interface with some utility function need + * by AFX API + * + * @interface HTMLElement + */ +interface HTMLElement { + /** + * Recursively update a tag and all its children + * + * @param {*} [d] data to send to all element in the DOM subtree + * @memberof HTMLElement + */ + update(d?: any): void; + + /** + * + * AFX will automatically bind the context menu on an HTMLElement + * if this function is defined on that element. The function should + * define the content of the context menu and its action + * + * Once the context menu is bound to the element, all context menu handle + * defined on any child of this element will be ignored. + * + * @param {JQuery.MouseEventBase} e a mouse event + * @param {OS.GUI.tag.MenuTag} m The context menu element [[MenuTag]] + * @memberof HTMLElement + */ + contextmenuHandle(e: JQuery.MouseEventBase, m: OS.GUI.tag.MenuTag): void; + + /** + * Mount the element and all the children on its DOM subtree. This action + * is performed in a top-down manner + * + * @memberof HTMLElement + */ + sync(): void; + + /** + * + * This action allows to generated all the DOM nodes defined by all AFX tags + * in its hierarchy. + * It performs two operations, one top-down operation to generate all the + * necessary DOM nodes, another bottom-up operation to init all the AFX tag + * in the current element DOM hierarchy + * + * @param {OS.API.Announcer} o an AntOS observable object + * @memberof HTMLElement + */ + afxml(o: OS.API.Announcer): void; + + /** + * Perform DOM generation ([[afxml]]) then mount ([[sync]]) all the + * elements. + * + * @param {OS.API.Announcer} o an AntOS observable object + * @param {boolean} [flag] indicates whether this is the top-most call of the operation + * @memberof HTMLElement + */ + uify(o: OS.API.Announcer, flag?: boolean): void; + + /** + * + * + * @type {*} + * @memberof HTMLElement + */ + mozRequestFullScreen: any; + + /** + * + * + * @type {*} + * @memberof HTMLElement + */ + webkitRequestFullscreen: any; + + /** + * + * + * @type {*} + * @memberof HTMLElement + */ + msRequestFullscreen: any; +} + +/** + * + * + * @interface Document + */ +interface Document { + mozCancelFullScreen: any; + webkitExitFullscreen: any; + cancelFullScreen: any; +} +namespace OS { + export namespace GUI { + /** + * [[TagLayoutType]] interface using by AFX tags to defined + * its internal DOM hierarchy + * + * @export + * @interface TagLayoutType + */ + export interface TagLayoutType { + /** + * Element tag name + * + * @type {string} + * @memberof TagLayoutType + */ + el: string; + + /** + * Children layout of the current element + * + * @type {TagLayoutType[]} + * @memberof TagLayoutType + */ + children?: TagLayoutType[]; + + /** + * Reference name of the element used by AFX Tag + * + * @type {string} + * @memberof TagLayoutType + */ + ref?: string; + + /** + * CSS class of the element + * + * @type {string} + * @memberof TagLayoutType + */ + class?: string; + + /** + * this is the `data-id` attribute of the element, + * can be query by the [[aid]] Tag API function. + * Not to be confused with the DOM `id` attribute + * + * @type {(string | number)} + * @memberof TagLayoutType + */ + id?: string | number; + + /** + * Tooltip text of the element + * + * @type {(string | FormattedString)} + * @memberof TagLayoutType + */ + tooltip?: string | FormattedString; + + /** + * `data-width` of the element, not to be confused with + * the `width` attribute of the DOM element + * + * @type {number} + * @memberof TagLayoutType + */ + width?: number; + + /** + ** `data-height` of the element, not to be confused with + * the `height` attribute of the DOM element + * + * @type {number} + * @memberof TagLayoutType + */ + height?: number; + } + + /** + * Data type for event issued by AFX tags + * + * @export + * @interface TagEventDataType + * @template T item template + */ + export interface TagEventDataType { + /** + * Reference to the item involved in the event + * + * @type {T} + * @memberof TagEventDataType + */ + item?: T; + + [propName: string]: any; + } + + /** + * Format of the event issued by AFX tags + * + * @export + * @interface TagEventType + * @template T data type + */ + export interface TagEventType { + /** + * `data-id` of the tag that trigger the + * event + * + * @type {(number | string)} + * @memberof TagEventType + */ + id: number | string; + + /** + * Data object of the event + * + * @type {T} + * @memberof TagEventType + */ + data: T; + } + + /** + * Drag and Drop data type sent between mouse events + * + * @export + * @interface DnDEventDataType + * @template T + */ + export interface DnDEventDataType { + /** + * Reference to the source DOM element + * + * @type {T} + * @memberof DnDEventDataType + */ + from: T; + + /** + * Reference to the target DOM element + * + * @type {T} + * @memberof DnDEventDataType + */ + to: T; + } + /** + * Tag event callback type + */ + export type TagEventCallback = (e: TagEventType) => void; + /** + * Top most element z index value, start by 10 + */ + export var zindex: number = 10; + + /** + * Base abstract class for tag implementation, any AFX tag should be + * subclass of this class + * + * @export + * @abstract + * @class AFXTag + * @extends {HTMLElement} + */ + export abstract class AFXTag extends HTMLElement { + /** + * The announcer object of the tag + * + * @type {API.Announcer} + * @memberof AFXTag + */ + observable: API.Announcer; + + /** + * Reference to some of the tag's children + * element. This reference object is built + * based on the `ref` property found in the + * tag layout [[TagLayoutType]] + * + * @protected + * @type {GenericObject} + * @memberof AFXTag + */ + protected refs: GenericObject; + + /** + * boolean value indicated whether the tag + * is already mounted in the DOM tree + * + * @protected + * @type {boolean} + * @memberof AFXTag + */ + protected _mounted: boolean; + + /** + *Creates an instance of AFXTag. + * @memberof AFXTag + */ + constructor() { + super(); + + if (!this.observable) { + this.observable = new Ant.OS.API.Announcer(); + } + this._mounted = false; + this.refs = {}; + } + + /** + * This function verifies if a property name of the input object + * corresponds to a setter of the current tag. If this is the + * case, it sets the value of that property to the setter + * + * @param {GenericObject} v input object + * @memberof AFXTag + */ + set(v: GenericObject) { + for (let k in v) { + let descriptor = this.descriptor_of(k); + if (descriptor && descriptor.set) { + this[k] = v[k]; + } + } + } + + /** + * Setter to set the tooltip text to the current tag. + * The text should be in the following format: + * ```text + * cr|cl|ct|cb: tooltip text + * ``` + * + * @memberof AFXTag + */ + set tooltip(v: string) { + if (!v) { + return; + } + $(this).attr("tooltip", v); + } + + /** + * + * This function looking for a property name of the tag + * in its prototype chain. The descriptor of the property + * will be returned if it exists + * + * @private + * @param {string} k the property name to be queried + * @returns {PropertyDescriptor} the property descriptor or undefined + * @memberof AFXTag + */ + private descriptor_of(k: string): PropertyDescriptor { + let desc: PropertyDescriptor; + let obj = this; + do { + desc = Object.getOwnPropertyDescriptor(obj, k); + } while (!desc && (obj = Object.getPrototypeOf(obj))); + return desc; + } + + /** + * Setter: set the id of the tag in string or number + * + * Getter: get the id of the current tag + * + * @memberof AFXTag + */ + set aid(v: string | number) { + $(this).attr("data-id", v); + } + + get aid(): string | number { + return $(this).attr("data-id"); + } + + /** + * Implementation from HTMLElement interface, + * this function mount the current tag hierarchy + * + * @returns {void} + * @memberof AFXTag + */ + sync(): void { + if (this._mounted) { + return; + } + this._mounted = true; + this.mount(); + super.sync(); + } + + /** + * Generate the DOM hierarchy of the current tag + * + * @param {API.Announcer} o observable object + * @memberof AFXTag + */ + afxml(o: API.Announcer): void { + if (o) this.observable = o; + if (!this.aid) + this.aid = ( + Math.floor(Math.random() * 100000) + 1 + ).toString(); + const children = $(this).children(); + for (let obj of this.layout()) { + const dom = this.mkui(obj); + if (dom) { + $(dom).appendTo(this); + } + } + if (this.refs.yield) { + for (let v of children) { + $(v).detach().appendTo(this.refs.yield); + } + } + const attrs = {}; + for (let i = 0; i < this.attributes.length; i++) { + const element = this.attributes[i]; + let descriptor = this.descriptor_of(element.nodeName); + if (descriptor && descriptor.set) { + let value = ""; + try { + value = JSON.parse(element.nodeValue); + } catch (e) { + value = element.nodeValue; + } + attrs[element.nodeName] = value; + } + } + super.afxml(this.observable); + this.init(); + for (let k in attrs) { + this[k] = attrs[k]; + } + } + + /** + * Update the current tag hierarchy + * + * @param {*} d any data object + * @memberof AFXTag + */ + update(d: any): void { + this.reload(d); + super.update(d); + } + + /** + * Init the current tag, this function + * is called before the [[mount]] function + * + * @protected + * @abstract + * @memberof AFXTag + */ + protected abstract init(): void; + + /** + * Mount only the current tag + * + * @protected + * @abstract + * @memberof AFXTag + */ + protected abstract mount(): void; + + /** + * Layout definition of a tag + * + * @protected + * @abstract + * @returns {TagLayoutType[]} tag layout object + * @memberof AFXTag + */ + protected abstract layout(): TagLayoutType[]; + + /** + * Update only the current tag, this function is + * called by [[update]] before chaining the + * update process to its children + * + * @protected + * @abstract + * @param {*} [d] + * @memberof AFXTag + */ + protected abstract reload(d?: any): void; + + /** + * This function is used to re-render the current + * tag + * + * @protected + * @memberof AFXTag + */ + protected calibrate(): void {} + + /** + * This function parses the input layout object + * and generates all the elements defined by + * the tag + * + * @private + * @param {TagLayoutType} tag tag layout object + * @returns {Element} the DOM element specified by the tag layout + * @memberof AFXTag + */ + private mkui(tag: TagLayoutType): Element { + if (!tag) { + return undefined; + } + const dom = $(`<${tag.el}>`); + if (tag.class) { + $(dom).addClass(tag.class); + } + if (tag.id) { + $(dom).attr("data-id", tag.id); + } + if (tag.height) { + $(dom).attr("data-height", tag.height); + } + if (tag.width) { + $(dom).attr("data-width", tag.width); + } + if (tag.tooltip) { + $(dom).attr("tooltip", tag.tooltip.__()); + } + if (tag.children) { + for (let v of tag.children) { + $(this.mkui(v)).appendTo(dom); + } + } + if (tag.ref) { + this.refs[tag.ref] = dom[0]; + } + // dom.mount @observable + return dom[0]; //.uify(@observable) + } + + /** + * This function inserts or removes an attribute name + * to/from the target element based on the input `flag`. + * + * @protected + * @param {boolean} flag indicates whether the attribute name should be inserted o removed + * @param {string} v the attribute name + * @param {HTMLElement} [el] the target element + * @memberof AFXTag + */ + protected attsw(flag: boolean, v: string, el?: HTMLElement): void { + if (flag) this.atton(v, el); + else this.attoff(v, el); + } + + /** + * Insert the attribute name to the target element + * + * @protected + * @param {string} v the attribute name + * @param {HTMLElement} [el] the target element + * @memberof AFXTag + */ + protected atton(v: string, el?: HTMLElement): void { + const element = el ? el : this; + $(element).attr(v, ""); + } + + /** + * Remove the attribute name from the target element + * + * @protected + * @param {string} v attribute name + * @param {HTMLElement} [el] the target element + * @memberof AFXTag + */ + protected attoff(v: string, el?: HTMLElement): void { + const element = el ? el : this; + element.removeAttribute(v); + } + + /** + * Verify if the target element has an attribute name + * + * @protected + * @param {string} v attribute name + * @param {HTMLElement} [el] target element + * @returns {boolean} + * @memberof AFXTag + */ + protected hasattr(v: string, el?: HTMLElement): boolean { + const element = el ? el : this; + return element.hasAttribute(v); + } + } + + HTMLElement.prototype.update = function (d): void { + $(this) + .children() + .each(function () { + return this.update(d); + }); + }; + HTMLElement.prototype.sync = function (): void { + $(this) + .children() + .each(function () { + return this.sync(); + }); + }; + HTMLElement.prototype.afxml = function (o: API.Announcer): void { + $(this) + .children() + .each(function () { + return this.afxml(o); + }); + }; + HTMLElement.prototype.uify = function ( + o: API.Announcer, + toplevel?: boolean + ): void { + this.afxml(o); + this.sync(); + if (o && toplevel) o.trigger("mounted", this.aid); + }; + /** + * All the AFX tags are defined in this namespace, + * these tags are defined as custom DOM elements and will be + * stored in the `customElements` registry of the browser + */ + export namespace tag { + /** + * Define an AFX tag as a custom element and add it to the + * global `customElements` registry. If the tag is redefined, i.e. + * the tag already exists, its behavior will be updated with the + * new definition + * + * @export + * @template T all classes that extends [[AFXTag]] + * @param {string} name name of the tag + * @param {{ new (): T }} cls the class that defines the tag + * @returns {void} + */ + export function define( + name: string, + cls: { new (): T } + ): void { + try { + customElements.define(name, cls); + } catch (error) { + const proto = customElements.get(name); + if (cls) { + const props = Object.getOwnPropertyNames(cls.prototype); + // redefine the class + for (let prop of props) { + proto.prototype[prop] = cls.prototype[prop]; + } + return; + } + throw error; + } + } + } + } +} diff --git a/src/core/vfs.coffee b/src/core/vfs.coffee deleted file mode 100644 index b072288..0000000 --- a/src/core/vfs.coffee +++ /dev/null @@ -1,357 +0,0 @@ -# Copyright 2017-2018 Xuan Sang LE - -# AnTOS Web desktop is is licensed under the GNU General Public -# License v3.0, see the LICENCE file for more information - -# This program is free software: you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation, either version 3 of -# the License, or (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. - -# You should have received a copy of the GNU General Public License -#along with this program. If not, see https://www.gnu.org/licenses/. -String.prototype.asFileHandler = () -> - list = @split "://" - handlers = _API.VFS.findHandlers list[0] - if not handlers or handlers.length is 0 - _courrier.osfail __("VFS unknown handler: {0}", @), (_API.throwe "OS.VFS"), @ - return null - return new handlers[0](@) - -this.OS.API.VFS = - handlers: { } - register: ( protos, cls ) -> - return self.OS.API.VFS.handlers[protos] = cls # if typeof protos is "string" - #_API.VFS.handlers[v] = cls for v in protos - findHandlers: (proto) -> - l = (v for k, v of _API.VFS.handlers when proto.trim().match (new RegExp k , "g")) - return l - -class BaseFileHandler - constructor: (path) -> - @dirty = false - @cache = undefined - @setPath path - - setPath: (p) -> - @ready = false - return unless p - @path = p.toString() - list = @path.split "://" - @protocol = list[0] - return unless list.length > 1 - re = list[1].replace(/^\/+|\/+$/g, '') - return if re is "" - @genealogy = re.split("/") - @basename = @genealogy[@genealogy.length - 1] unless @isRoot() - @ext = @basename.split( "." ).pop() unless @basename.lastIndexOf(".") is 0 or @basename.indexOf( "." ) is -1 - asFileHandler: () -> - @ - isRoot: () -> (not @genealogy) or (@genealogy.size is 0) - - child: (name) -> - if @isRoot() - return @path + name - else - return @path + "/" + name - - isHidden: () -> - return false if not @basename - @basename[0] is "." - - hash: () -> - return -1 unless @path - return @path.hash() - - sendB64: (p, f) -> - me = @ - m = if p is "object" then "text/plain" else p - return f "" unless @cache - if p is "object" or typeof @cache is "string" - b64 = if p is "object" then (JSON.stringify @cache).asBase64() else @cache.asBase64() - b64 = "data:#{m};base64,#{b64}" - f(b64) - else - reader = new FileReader() - reader.readAsDataURL(@cache) - reader.onload = () -> - f reader.result - reader.onerror = (e) -> - return _courrier.osfail __("VFS Cannot encode file: {0}", me.path), (_API.throwe "OS.VFS"), e - parent: () -> - return @ if @isRoot() - return (@protocol + "://" + (@genealogy.slice 0 , @genealogy.length - 1).join "/") - - onready: (f, err) -> - # read meta data - return f() if @ready - me = @ - me.meta (d) -> - if d.error - return if err then err d else _courrier.osfail "#{me.path}: #{d.error}", (_API.throwe "OS.VFS"), d.error - me.info = d.result - me.ready = true - f() - - read: (f, t) -> - me = @ - @onready (() -> me.action "read", t, f) - - write: (d, f) -> - me = @ - @action "write", d, (r) -> - _courrier.ostrigger "VFS", { m: "write", file: me } if r.result - f r - - mk: (d, f) -> - me = @ - @onready (() -> me.action "mk", d, (r) -> - _courrier.ostrigger "VFS", { m: "mk", file: me } if r.result - f r) - - remove: (f) -> - me = @ - @onready (() -> me.action "remove", null, (r) -> - _courrier.ostrigger "VFS", { m: "remove", file: me } if r.result - f r) - - upload: (f) -> - me = @ - @onready (() -> me.action "upload", null, (r) -> - _courrier.ostrigger "VFS", { m: "upload", file: me } if r.result - f r) - publish: (f) -> - me = @ - @onready (() -> me.action "publish", null, (r) -> - _courrier.ostrigger "VFS", { m: "publish", file: me } if r.result - f r) - download: (f) -> - me = @ - @onready (() -> me.action "download", null, f) - - move: (d, f) -> - me = @ - @onready (() -> me.action "move", d, (r) -> - _courrier.ostrigger "VFS", { m: "move", file: d.asFileHandler() } if r.result - f r) - - execute: (f) -> - me = @ - @onready (() -> me.action "execute", null, f) - - #mk: (f) -> - - meta: (f) -> - - getlink: () -> @path - # for main action read, write, remove, execute - # must be implemented by subclasses - action: (n, p, f) -> - return _courrier.osfail __("VFS unknown action: {0}", n), (_API.throwe "OS.VFS"), n - -# now export the class -self.OS.API.VFS.BaseFileHandler = BaseFileHandler - -# Remote file handle -class RemoteFileHandler extends self.OS.API.VFS.BaseFileHandler - constructor: (path) -> - super path - - meta: (f) -> - _API.handler.fileinfo @path, f - - getlink: () -> - _API.handler.get + "/" + @path - - action: (n, p, f) -> - me = @ - switch n - when "read" - return _API.handler.scandir @path, f if @info.type is "dir" - #read the file - return _API.handler.fileblob @path, f if p is "binary" - _API.handler.readfile @path, f, if p then p else "text" - when "mk" - return f { error: __("{0} is not a directory", @path) } if @info.type is "file" - _API.handler.mkdir "#{@path}/#{p}", f - when "write" - return _API.handler.write me.path, me.cache, f if p is "base64" - @sendB64 p, (data) -> - _API.handler.write me.path, data, f - when "upload" - return if @info.type is "file" - _API.handler.upload @path, f - when "remove" - _API.handler.delete @path, f - when "publish" - _API.handler.sharefile @path, true , f - when "download" - return if @info.type is "dir" - _API.handler.fileblob @path, (d) -> - blob = new Blob [d], { type: "octet/stream" } - _API.saveblob me.basename, blob - when "move" - _API.handler.move @path, p, f - else - return _courrier.osfail __("VFS unknown action: {0}", n), (_API.throwe "OS.VFS"), n - -self.OS.API.VFS.register "^(home|desktop|os|Untitled)$", RemoteFileHandler - -# Application Handler -class ApplicationHandler extends self.OS.API.VFS.BaseFileHandler - constructor: (path) -> - super path - @info = _OS.setting.system.packages[@basename] if @basename - @ready = true - - meta: (f) -> - f() - - action: (n, p, f) -> - me = @ - switch n - when "read" - return f { result: @info } if @info - return unless @isRoot() - f { result: ( v for k, v of _OS.setting.system.packages ) } - - when "mk" - return - - when "write" - return - - when "upload" - # install - return - - when "remove" - #uninstall - return - when "publish" - return - when "download" - return - - when "move" - return - else - return _courrier.osfail __("VFS unknown action: {0}", n), (_API.throwe "OS.VFS"), n - -self.OS.API.VFS.register "^app$", ApplicationHandler - -class BufferFileHandler extends self.OS.API.VFS.BaseFileHandler - constructor: (path, mime, data) -> - super path - @cache = data if data - @info = - mime: mime - path: path - size: if data then data.length else 0 - name: @basename - type: "file" - meta: (f) -> - f() - - onchange: (f) -> - @onchange = f - - action: (n, p, f) -> - me = @ - switch n - when "read" - return f { result: @cache } - - when "mk" - return - - when "write" - @cache = p - @onchange @ if @onchange - f { result: true } - - when "upload" - # install - return - - when "remove" - #uninstall - return - when "publish" - return - when "download" - blob = new Blob [@cache], { type: "octet/stream" } - _API.saveblob me.basename, blob - - when "move" - return - else - return _courrier.osfail __("VFS unknown action: {0}", n), (_API.throwe "OS.VFS"), n - -self.OS.API.VFS.register "^mem$", BufferFileHandler - -class URLFileHandler extends self.OS.API.VFS.BaseFileHandler - constructor: (path) -> - super path - @ready = true - meta: (f) -> - f { result: true } - action: (n, p, f) -> - me = @ - switch n - when "read" - _API.get @path, (d) -> - f(d) - , (e, s) -> - _courrier.oserror __("VFS cannot read : {0}", me.path), e, s - , if p then p else "text" - else - return _courrier.oserror __("VFS unknown action: {0}", n), (_API.throwe "OS.VFS"), n -self.OS.API.VFS.register "^(http|https)$", URLFileHandler - -class SharedFileHandler extends self.OS.API.VFS.BaseFileHandler - constructor: (path) -> - super path - @ready = true if @isRoot() - meta: (f) -> - _API.handler.fileinfo @path, f - - action: (n, p, f) -> - me = @ - switch n - when "read" - return _API.get "#{_API.handler.shared}/all", f, ((e, s)->) if @isRoot() - #read the file - return _API.handler.fileblob @path, f if p is "binary" - _API.handler.readfile @path, f, if p then p else "text" - when "mk" - return - - when "write" - _API.handler.write @path, p, f - - when "remove" - _API.handler.sharefile @basename, false, f - - when "upload" - return - - when "publish" - return f { result: @basename } - - when "download" - return if @info.type is "dir" - _API.handler.fileblob @path, (d) -> - blob = new Blob [d], { type: "octet/stream" } - _API.saveblob me.basename, blob - when "move" - return - else - return _courrier.osfail __("VFS unknown action: {0}", n), (_API.throwe "OS.VFS"), n - -self.OS.API.VFS.register "^shared$", SharedFileHandler \ No newline at end of file diff --git a/src/core/vfs.ts b/src/core/vfs.ts new file mode 100644 index 0000000..7f2473c --- /dev/null +++ b/src/core/vfs.ts @@ -0,0 +1,1702 @@ +// Copyright 2017-2018 Xuan Sang LE + +// AnTOS Web desktop is is licensed under the GNU General Public +// License v3.0, see the LICENCE file for more information + +// This program is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. + +// You should have received a copy of the GNU General Public License +//along with this program. If not, see https://www.gnu.org/licenses/. +type VFSFileHandleClass = { new (...args: any[]): OS.API.VFS.BaseFileHandle }; +interface String { + /** + * Convert a string to VFS file handle. + * + * This function will create a file handle object from the string + * with the help of [[VFS.findHandles]] + * + * @returns {OS.API.VFS.BaseFileHandle} + * @memberof String + */ + asFileHandle(): OS.API.VFS.BaseFileHandle; +} +namespace OS { + export namespace API { + /** + * User permission data type + * + * @export + * @interface UserPermissionType + */ + export interface UserPermissionType { + read: boolean; + write: boolean; + exec: boolean; + } + + /** + * VFS file meta-data data type + * + * @export + * @interface FileInfoType + */ + export interface FileInfoType { + /** + * File mime type + * + * @type {string} + * @memberof FileInfoType + */ + mime: string; + + /** + * File size + * + * @type {number} + * @memberof FileInfoType + */ + size: number; + + /** + * File name + * + * @type {string} + * @memberof FileInfoType + */ + name: string; + + /** + * File path + * + * @type {string} + * @memberof FileInfoType + */ + path: string; + + /** + * File type: + * - `file` + * - `dir` + * - `app` + * + * @type {string} + * @memberof FileInfoType + */ + type: string; + + /** + * File permission + * + * @type {{ + * group: UserPermissionType; + * owner: UserPermissionType; + * other: UserPermissionType; + * }} + * @memberof FileInfoType + */ + perm?: { + /** + * Group permission + * + * @type {UserPermissionType} + */ + group: UserPermissionType; + + /** + * Owner permission + * + * @type {UserPermissionType} + */ + owner: UserPermissionType; + + /** + * Other permission + * + * @type {UserPermissionType} + */ + other: UserPermissionType; + }; + + /** + * Creation time + * + * @type {string} + * @memberof FileInfoType + */ + ctime?: string; + + /** + * Modification time + * + * @type {string} + * @memberof FileInfoType + */ + mtime?: string; + + /** + * Group id + * + * @type {number} + * @memberof FileInfoType + */ + gid?: number; + + /** + * User id + * + * @type {number} + * @memberof FileInfoType + */ + uid?: number; + [propName: string]: any; + } + + /** + * This namespace is dedicated to all APIs related to + * AntOS Virtual File System (VFS) + */ + export namespace VFS { + String.prototype.asFileHandle = function (): BaseFileHandle { + const list = this.split("://"); + const handles = API.VFS.findHandles(list[0]); + if (!handles || handles.length === 0) { + announcer.osfail( + __("VFS unknown handle: {0}", this), + API.throwe("OS.VFS") + ); + return null; + } + return new handles[0](this); + }; + + /** + * Placeholder stores VFS file protocol patterns and its attached file handle class. + * + */ + export const handles: GenericObject = {}; + + /** + * Register a protocol to a handle class + * + * @export + * @param {string} protos VFS protocol pattern + * @param {VFSFileHandleClass} cls handle class + */ + export function register( + protos: string, + cls: VFSFileHandleClass + ): void { + handles[protos] = cls; + } + + /** + * Looking for a attached file handle class of a string protocol + * + * When converting a string to file handle, the system will look + * for a protocol pattern in the string, if the protocol found, + * its attached handle class (found in [[VFS.handles]]) will be + * used to initialize a file handle object from the string + * + * ```typescript + * "home://data/test.txt".asFileHandle() // -> an instance of RemoteFileHandle + * ``` + * @export + * @param {string} proto protocol string + * @returns {VFSFileHandleClass[]} + */ + export function findHandles(proto: string): VFSFileHandleClass[] { + const l = (() => { + const result = []; + for (let k in handles) { + const v = handles[k]; + if (proto.trim().match(new RegExp(k, "g"))) { + result.push(v); + } + } + return result; + })(); + return l; + } + + /** + * Abstract prototype of all all VFS file handle definition. + * + * This prototype provides a standardized interface to access + * to different underlay file systems such as remote file, + * cloud file (Dropbox, Google drive, etc.), URL or memory-based file + * + * @export + * @abstract + * @class BaseFileHandle + */ + export abstract class BaseFileHandle { + /** + * Flag indicates whether the file is dirty + * + * @type {boolean} + * @memberof BaseFileHandle + */ + dirty: boolean; + + /** + * Once read, file content will be cached in this placeholder + * + * @type {*} + * @memberof BaseFileHandle + */ + cache: any; + + /** + * Flag indicated whether the file meta-data is loaded + * + * @type {boolean} + * @memberof BaseFileHandle + */ + ready: boolean; + + /** + * File path + * + * @type {string} + * @memberof BaseFileHandle + */ + path: string; + + /** + * File protocol e.g: + * - `os://` + * - `home://` + * + * @type {string} + * @memberof BaseFileHandle + */ + protocol: string; + + /** + * List of path segments + * + * @type {string[]} + * @memberof BaseFileHandle + */ + genealogy: string[]; + + /** + * File base name + * + * @type {string} + * @memberof BaseFileHandle + */ + basename: string; + + /** + * Once loaded, [[ready]] will be set to true and + * file meta-data will be stored in this place holder + * + * @type {FileInfoType} + * @memberof BaseFileHandle + */ + info: FileInfoType; + + /** + * File extension + * + * @type {string} + * @memberof BaseFileHandle + */ + ext: string; + + /** + * + * File type + * @type {string} + * @memberof BaseFileHandle + */ + type: string; + /** + *Creates an instance of BaseFileHandle. + * @param {string} path file path + * @memberof BaseFileHandle + */ + constructor(path: string) { + this.dirty = false; + this.cache = undefined; + this.setPath(path); + } + + /** + * Set a file path to the current file handle + * + * @param {string} p + * @returns {void} + * @memberof BaseFileHandle + */ + setPath(p: string): void { + this.ready = false; + if (!p) { + return; + } + this.path = p.toString(); + const list = this.path.split("://"); + this.protocol = list[0]; + if (!(list.length > 1)) { + return; + } + const re = list[1].replace(/^\/+|\/+$/g, ""); + if (re === "") { + return; + } + this.genealogy = re.split("/"); + if (!this.isRoot()) { + this.basename = this.genealogy[ + this.genealogy.length - 1 + ]; + } + if ( + this.basename.lastIndexOf(".") !== 0 && + this.basename.indexOf(".") !== -1 + ) { + this.ext = this.basename.split(".").pop(); + } + } + + /** + * Getter: Get the file basename + * Setter: set the file name + * + * @returns {string} + * @memberof BaseFileHandle + */ + get filename(): string { + if (!this.basename) { + return "Untitled"; + } + return this.basename; + } + set filename(v: string) { + this.basename = v; + } + /** + * Set data to the file cache + * + * @param {*} v data object + * @returns {BaseFileHandle} + * @memberof BaseFileHandle + */ + setCache(v: any): BaseFileHandle { + this.cache = v; + return this; + } + + /** + * Return the object itself + * + * @returns {BaseFileHandle} + * @memberof BaseFileHandle + */ + asFileHandle(): BaseFileHandle { + return this; + } + + /** + * Check whether the current file is the root of the file tree + * + * @returns {boolean} + * @memberof BaseFileHandle + */ + isRoot(): boolean { + return !this.genealogy || this.genealogy.length === 0; + } + + /** + * Check whether the current file is a hidden file + * + * @returns {boolean} + * @memberof BaseFileHandle + */ + isHidden(): boolean { + if (!this.basename) { + return false; + } + return this.basename[0] === "."; + } + + /** + * Get hash number of the current file path + * + * @returns {number} + * @memberof BaseFileHandle + */ + hash(): number { + if (!this.path) { + return -1; + } + return this.path.hash(); + } + + /** + * Convert the current file cache to Base64 + * + * @protected + * @param {string} t type of the file cache: + * - `object` + * - `mime type` + * @returns {(Promise)} promise on the converted data + * @memberof BaseFileHandle + */ + protected b64(t: string): Promise { + // t is object or mime type + return new Promise((resolve, reject) => { + const m = t === "object" ? "text/plain" : t; + if (!this.cache) { + return resolve(""); + } + if (t === "object" || typeof this.cache === "string") { + let b64: string; + if (t === "object") { + b64 = JSON.stringify( + this.cache, + undefined, + 4 + ).asBase64(); + } else { + b64 = this.cache.asBase64(); + } + b64 = `data:${m};base64,${b64}`; + return resolve(b64); + } else { + const reader = new FileReader(); + reader.readAsDataURL(this.cache); + reader.onload = () => resolve(reader.result); + return (reader.onerror = (e) => reject(e)); + } + }); + } + + /** + * Get the parent file handle of the current file + * + * @returns {BaseFileHandle} + * @memberof BaseFileHandle + */ + parent(): BaseFileHandle { + if (this.isRoot()) { + return this; + } + return ( + this.protocol + + "://" + + this.genealogy + .slice(0, this.genealogy.length - 1) + .join("/") + ).asFileHandle(); + } + + /** + * Load the file meta-data before performing + * any task + * + * @returns {Promise} a promise on file meta-data + * @memberof BaseFileHandle + */ + onready(): Promise { + // read meta data + return new Promise((resolve, reject) => { + if (this.ready) { + return resolve(this.info); + } + return this.meta() + .then((d: RequestResult) => { + this.info = d.result as FileInfoType; + this.ready = true; + return resolve(d.result as FileInfoType); + }) + .catch((e: Error) => reject(__e(e))); + }); + } + + /** + * Public read operation + * + * This function calls the [[_rd]] function to perform the operation. + * + * If the current file is a directory, then the operation + * will return the meta-data of all files inside of the directory. + * Otherwise, file content will be returned + * + * @param {string} t data type + * - jsonp: the response is an json object + * - script: the response is a javascript code + * - xml, html: the response is a XML/HTML object + * - text: plain text + * - binary + * + * @returns {Promise} a promise on the file content + * @memberof BaseFileHandle + */ + read(t?: string): Promise { + return new Promise(async (resolve, reject) => { + try { + const r = await this.onready(); + try { + const d = await this._rd(t); + return resolve(d); + } catch (e) { + return reject(__e(e)); + } + } catch (e_1) { + return reject(__e(e_1)); + } + }); + } + + /** + * Write the file cache to the actual file + * + * This function calls the [[_wr]] function to perform the operation + * + * @param {string} t data type + * - `base64` + * - `object` + * - `mime type` + * + * @returns {Promise} promise on the operation result + * @memberof BaseFileHandle + */ + write(t: string): Promise { + return new Promise(async (resolve, reject) => { + try { + const r: RequestResult = await this._wr(t); + announcer.ostrigger("VFS", { + m: "write", + file: this, + }); + return resolve(r); + } catch (e) { + return reject(__e(e)); + } + }); + } + + /** + * Sub-directory creation + * + * This function calls the [[_mk]] function to perform the operation + * + * @param {string} d sub directory name + * @returns {Promise} promise on the operation result + * @memberof BaseFileHandle + */ + mk(d: string): Promise { + return new Promise(async (resolve, reject) => { + try { + const r = await this.onready(); + try { + const d_1 = await this._mk(d); + announcer.ostrigger("VFS", { + m: "mk", + file: this, + }); + return resolve(d_1); + } catch (e) { + return reject(__e(e)); + } + } catch (e_1) { + return reject(__e(e_1)); + } + }); + } + + /** + * Delete the file + * + * This function calls the [[_rm]] function to perform the operation + * + * @returns {Promise} promise on the operation result + * @memberof BaseFileHandle + */ + remove(): Promise { + return new Promise(async (resolve, reject) => { + try { + const r = await this.onready(); + try { + const d = await this._rm(); + announcer.ostrigger("VFS", { + m: "remove", + file: this, + }); + return resolve(d); + } catch (e) { + return reject(__e(e)); + } + } catch (e_1) { + return reject(__e(e_1)); + } + }); + } + + /** + * Upload a file to the current directory + * + * Only work when the current file is a directory + * + * This function calls the [[_up]] function to perform the operation + * + * @returns {Promise} promise on the operation result + * @memberof BaseFileHandle + */ + upload(): Promise { + return new Promise(async (resolve, reject) => { + try { + const r = await this.onready(); + try { + const d = await this._up(); + announcer.ostrigger("VFS", { + m: "upload", + file: this, + }); + return resolve(d); + } catch (e) { + return reject(__e(e)); + } + } catch (e_1) { + return reject(__e(e_1)); + } + }); + } + + /** + * Share the file by publish it. + * + * Only work with file + * + * This function calls the [[_pub]] function to perform the operation + * + * @returns {Promise} promise on operation result + * @memberof BaseFileHandle + */ + publish(): Promise { + return new Promise(async (resolve, reject) => { + try { + const r = await this.onready(); + try { + const d = await this._pub(); + announcer.ostrigger("VFS", { + m: "publish", + file: this, + }); + return resolve(d); + } catch (e) { + return reject(__e(e)); + } + } catch (e_1) { + return reject(__e(e_1)); + } + }); + } + + /** + * Download the file. + * + * Only work with file + * + * This function calls the [[_down]] function to perform the operation + * + * @returns {Promise} Promise on the operation result + * @memberof BaseFileHandle + */ + download(): Promise { + return new Promise(async (resolve, reject) => { + try { + const r = await this.onready(); + try { + const d = await this._down(); + announcer.ostrigger("VFS", { + m: "download", + file: this, + }); + return resolve(d); + } catch (e) { + return reject(__e(e)); + } + } catch (e_1) { + return reject(__e(e_1)); + } + }); + } + + /** + * Move the current file to another location + * + * This function calls the [[_mv]] function to perform the operation + * + * @param {string} d destination location + * @returns {Promise} promise on the operation result + * @memberof BaseFileHandle + */ + move(d: string): Promise { + return new Promise(async (resolve, reject) => { + try { + const r = await this.onready(); + try { + const data = await this._mv(d); + announcer.ostrigger("VFS", { + m: "move", + file: d.asFileHandle(), + }); + return resolve(data); + } catch (e) { + return reject(__e(e)); + } + } catch (e_1) { + return reject(__e(e_1)); + } + }); + } + + /** + * Execute the current file. + * + * This action depends on each file protocol + * + * This function calls the [[_exec]] function to perform the operation + * + * @returns {Promise} + * @memberof BaseFileHandle + */ + execute(): Promise { + return new Promise(async (resolve, reject) => { + try { + const r = await this.onready(); + try { + const d = await this._exec(); + announcer.ostrigger("VFS", { + m: "execute", + file: this, + }); + return resolve(d); + } catch (e) { + return reject(__e(e)); + } + } catch (e_1) { + return reject(__e(e_1)); + } + }); + } + + /** + * Get an accessible link to the file + * that can be accessed from the browser + * + * @returns {string} + * @memberof BaseFileHandle + */ + getlink(): string { + return this.path; + } + + /** + * Helper function returns a promise on unsupported action + * + * @param {string} t action name + * @returns {Promise} + * @memberof BaseFileHandle + */ + protected unsupported(t: string): Promise { + return new Promise((resolve, reject) => { + return reject( + API.throwe( + __( + "Action {0} is unsupported on: {1}", + t, + this.path + ) + ) + ); + }); + } + + /** + * Low level protocol-specific read operation + * + * This function should be overridden on the file handle class + * that supports the operation + * + * @protected + * @param {string} t data type, see [[read]] + * @returns {Promise} + * @memberof BaseFileHandle + */ + protected _rd(t: string): Promise { + return this.unsupported("read"); + } + + /** + * Low level protocol-specific write operation + * + * This function should be overridden by the file handle class + * that supports the operation + * + * @protected + * @param {string} t data type, see [[write]] + * @param {*} [d] + * @returns {Promise} + * @memberof BaseFileHandle + */ + protected _wr(t: string, d?: any): Promise { + return this.unsupported("write"); + } + + /** + * Low level protocol-specific sub-directory creation + * + * This function should be overridden by the file handle class + * that supports the operation + * + * @protected + * @param {string} d sub directory name + * @returns {Promise} + * @memberof BaseFileHandle + */ + protected _mk(d: string): Promise { + return this.unsupported("mk"); + } + /** + * Low level protocol-specific delete operation + * + * This function should be overridden by the file handle class + * that supports the operation + * + * @returns {Promise} + * @memberof BaseFileHandle + */ + protected _rm(): Promise { + return this.unsupported("remove"); + } + + /** + * Low level protocol-specific move operation + * + * This function should be overridden by the file handle class + * that supports the operation + * + * @protected + * @param {string} d + * @returns {Promise} + * @memberof BaseFileHandle + */ + protected _mv(d: string): Promise { + return this.unsupported("move"); + } + + /** + * Low level protocol-specific upload operation + * + * This function should be overridden by the file handle class + * that supports the operation + * + * @returns {Promise} + * @memberof BaseFileHandle + */ + protected _up(): Promise { + return this.unsupported("upload"); + } + + /** + * Low level protocol-specific download operation + * + * This function should be overridden by the file handle class + * that supports the operation + * + * @returns {Promise} + * @memberof BaseFileHandle + */ + protected _down(): Promise { + return this.unsupported("download"); + } + + /** + * Low level protocol-specific execute operation + * + * This function should be overridden by the file handle class + * that supports the operation + * + * @returns {Promise} + * @memberof BaseFileHandle + */ + protected _exec(): Promise { + return this.unsupported("execute"); + } + + /** + * Low level protocol-specific share operation + * + * This function should be overridden by the file handle class + * that supports the operation + * + * @returns {Promise} + * @memberof BaseFileHandle + */ + protected _pub(): Promise { + return this.unsupported("publish"); + } + + /** + * Read the current file meta-data + * + * should be implemented by subclasses + * + * @abstract + * @returns {Promise} + * @memberof BaseFileHandle + */ + abstract meta(): Promise; + } + + /** + * Remote file handle allows to perform file operation + * on AntOS remote server files. Its protocol is defined + * by the following pattern: + * + * ``` + * ^(home|desktop|os|Untitled)$ + * ``` + * + * @class RemoteFileHandle + * @extends {BaseFileHandle} + */ + export class RemoteFileHandle extends BaseFileHandle { + /** + *Creates an instance of RemoteFileHandle. + * @param {string} path file path + * @memberof RemoteFileHandle + */ + constructor(path: string) { + super(path); + } + + /** + * Read remote file meta-data + * + * @returns {Promise} + * @memberof RemoteFileHandle + */ + meta(): Promise { + return new Promise(async (resolve, reject) => { + try { + const d = await API.handle.fileinfo(this.path); + if (d.error) { + return reject( + API.throwe( + __("{0}: {1}", d.error, this.path) + ) + ); + } + return resolve(d); + } catch (e) { + return reject(__e(e)); + } + }); + } + + /** + * Remote file access link + * + * @returns {string} + * @memberof RemoteFileHandle + */ + getlink(): string { + return API.handle.get + "/" + this.path; + } + + /** + * Read remote file content. + * + * If the current file is a directory, then the operation + * will return the meta-data of all files inside of the directory. + * Otherwise, file content will be returned + * + * @protected + * @param {string} t data type see [[read]] + * @returns {Promise} + * @memberof RemoteFileHandle + */ + protected _rd(t: string): Promise { + // t: binary, text, any type + if (!this.info) { + return new Promise((resolve, reject) => { + return reject( + API.throwe( + __( + "file meta-data not found: {0}", + this.path + ) + ) + ); + }); + } + if (this.info.type === "dir") { + return API.handle.scandir(this.path); + } + //read the file + if (t === "binary") { + return API.handle.fileblob(this.path); + } + return API.handle.readfile(this.path, t ? t : "text"); + } + + /** + * Write file cache to the remote file + * + * @protected + * @param {string} t data type see [[write]] + * @returns {Promise} + * @memberof RemoteFileHandle + */ + protected _wr(t: string): Promise { + // t is base64 or undefined + return new Promise(async (resolve, reject) => { + if (t === "base64") { + try { + const d = await API.handle.write( + this.path, + this.cache + ); + if (d.error) { + return reject( + API.throwe( + __("{0}: {1}", d.error, this.path) + ) + ); + } + return resolve(d); + } catch (e) { + return reject(__e(e)); + } + } else { + try { + const r = await this.b64(t); + try { + const result = await API.handle.write( + this.path, + r as string + ); + if (result.error) { + return reject( + API.throwe( + __( + "{0}: {1}", + result.error, + this.path + ) + ) + ); + } + return resolve(result); + } catch (e_1) { + return reject(__e(e_1)); + } + } catch (e_2) { + return reject(__e(e_2)); + } + } + }); + } + + /** + * Create sub directory + * + * Only work on directory file handle + * + * @protected + * @param {string} d sub directory name + * @returns {Promise} + * @memberof RemoteFileHandle + */ + protected _mk(d: string): Promise { + return new Promise((resolve, reject) => { + if (!this.info) { + return reject( + API.throwe( + __( + "file meta-data not found: {0}", + this.path + ) + ) + ); + } + if (this.info.type === "file") { + return reject( + API.throwe( + __("{0} is not a directory", this.path) + ) + ); + } + return API.handle + .mkdir(`${this.path}/${d}`) + .then((d) => { + if (d.error) { + return reject( + API.throwe( + __("{0}: {1}", d.error, this.path) + ) + ); + } + return resolve(d); + }) + .catch((e) => reject(__e(e))); + }); + } + + /** + * Delete file/folder + * + * @protected + * @returns {Promise} + * @memberof RemoteFileHandle + */ + protected _rm(): Promise { + return new Promise(async (resolve, reject) => { + try { + const d = await API.handle.remove(this.path); + if (d.error) { + return reject( + API.throwe( + __("{0}: {1}", d.error, this.path) + ) + ); + } + return resolve(d); + } catch (e) { + return reject(__e(e)); + } + }); + } + + /** + * Move file/folder + * + * @protected + * @param {string} d + * @returns {Promise} + * @memberof RemoteFileHandle + */ + protected _mv(d: string): Promise { + return new Promise(async (resolve, reject) => { + try { + const r = await API.handle.move(this.path, d); + if (r.error) { + return reject( + API.throwe( + __("{0}: {1}", r.error, this.path) + ) + ); + } + return resolve(r); + } catch (e) { + return reject(__e(e)); + } + }); + } + + /** + * Upload a file + * + * Only work with directory file handle + * + * @protected + * @returns {Promise} + * @memberof RemoteFileHandle + */ + protected _up(): Promise { + return new Promise((resolve, reject) => { + if (this.info.type !== "dir") { + return reject( + API.throwe(__("{0} is not a file", this.path)) + ); + } + return API.handle + .upload(this.path) + .then((d) => { + if (d.error) { + return reject( + API.throwe( + __("{0}: {1}", d.error, this.path) + ) + ); + } + return resolve(d); + }) + .catch((e) => reject(__e(e))); + }); + } + + /** + * Download a file + * + * only work with file + * + * @protected + * @returns {Promise} + * @memberof RemoteFileHandle + */ + protected _down(): Promise { + return new Promise((resolve, reject) => { + if (this.info.type === "dir") { + return API.throwe( + __("{0} is not a file", this.path) + ); + } + return API.handle + .fileblob(this.path) + .then((d) => { + const blob = new Blob([d], { + type: "octet/stream", + }); + API.saveblob(this.basename, blob); + return resolve(); + }) + .catch((e) => reject(__e(e))); + }); + } + + /** + * Publish a file + * + * @protected + * @returns {Promise} + * @memberof RemoteFileHandle + */ + protected _pub(): Promise { + return new Promise(async (resolve, reject) => { + try { + const d = await API.handle.sharefile( + this.path, + true + ); + if (d.error) { + return reject( + API.throwe( + __("{0}: {1}", d.error, this.path) + ) + ); + } + return resolve(d); + } catch (e) { + return reject(__e(e)); + } + }); + } + } + + register("^(home|desktop|os|Untitled)$", RemoteFileHandle); + + /** + * Application file is an AntOS special file allowing to + * refer to an application as a regular file. Its protocol + * pattern is defined as: + * + * ```typescript + * "^app$" // e.g. app://Setting + * ``` + * + * @class ApplicationHandle + * @extends {BaseFileHandle} + */ + export class ApplicationHandle extends BaseFileHandle { + /** + *Creates an instance of ApplicationHandle. + * @param {string} path file path + * @memberof ApplicationHandle + */ + constructor(path: string) { + super(path); + if (this.basename) { + let v: any = OS.setting.system.packages[this.basename]; + v.type = "app"; + v.mime = "antos/app"; + v.size = 0; + this.info = v as FileInfoType; + } + this.ready = true; + } + + /** + * Read application meta-data + * + * @returns {Promise} + * @memberof ApplicationHandle + */ + meta(): Promise { + return new Promise((resolve, reject) => + resolve({ + result: this.info, + error: false, + }) + ); + } + + /** + * If the current file is root (e.g. `app://`), the operation + * will return all system packages meta-data. + * + * Otherwise, an error will be thrown + * + * @protected + * @param {string} t + * @returns {Promise} + * @memberof ApplicationHandle + */ + protected _rd(t: string): Promise { + return new Promise((resolve, reject) => { + if (this.info) { + return resolve({ + result: this.info, + error: false, + }); + } + if (!this.isRoot()) { + return reject( + API.throwe( + __("Application meta data isnt found") + ) + ); + } + const result = []; + for (let k in OS.setting.system.packages) { + const v = OS.setting.system.packages[k]; + result.push(v); + } + return resolve({ + result: result, + error: false, + }); + }); + } + } + + register("^app$", ApplicationHandle); + + /** + * A buffer file handle represents a virtual file that is stored + * on the system memory. Its protocol pattern is defined as: + * + * ```typescript + * "^mem$" // e.g. mem://test.txt + * ``` + * + * @class BufferFileHandle + * @extends {BaseFileHandle} + */ + export class BufferFileHandle extends BaseFileHandle { + /** + *Creates an instance of BufferFileHandle. + * @param {string} path file path + * @param {string} mime file mime-type + * @param {*} data file data + * @memberof BufferFileHandle + */ + constructor(path: string, mime: string, data: any) { + super(path); + if (data) { + this.cache = data; + } + this.info = { + mime: mime, + path: path, + size: data ? data.length : 0, + name: this.basename, + type: "file", + }; + } + + /** + * Read the file meta-data + * + * @returns {Promise} + * @memberof BufferFileHandle + */ + meta(): Promise { + return new Promise((resolve, reject) => + resolve({ + result: this.info, + error: false, + }) + ); + } + + /** + * Read file content stored in the file cached + * + * @protected + * @param {string} t data type see [[read]] + * @returns {Promise} + * @memberof BufferFileHandle + */ + protected _rd(t: string): Promise { + return new Promise((resolve, reject) => { + return resolve(this.cache); + }); + } + + /** + * Write data to the file cache + * + * @protected + * @param {string} t data type, see [[write]] + * @param {*} d data + * @returns {Promise} + * @memberof BufferFileHandle + */ + protected _wr(t: string, d: any): Promise { + this.cache = d; + return new Promise((resolve, reject) => + resolve({ + result: true, + error: false, + }) + ); + } + + /** + * Download the buffer file + * + * @protected + * @returns {Promise} + * @memberof BufferFileHandle + */ + protected _down(): Promise { + return new Promise((resolve, reject) => { + const blob = new Blob([this.cache], { + type: "octet/stream", + }); + API.saveblob(this.basename, blob); + return resolve(); + }); + } + } + + API.VFS.register("^mem$", BufferFileHandle); + + /** + * URL file handle represents a HTTP/HTTPs link url + * as an AntOS VFS file handle. Its protocol is defined as + * + * ``` + * ^(http|https|ftp)$ + * ``` + * + * @class URLFileHandle + * @extends {BaseFileHandle} + */ + export class URLFileHandle extends BaseFileHandle { + /** + *Creates an instance of URLFileHandle. + * @param {string} path + * @memberof URLFileHandle + */ + constructor(path: string) { + super(path); + this.ready = true; + this.info = { + path: path, + name: path, + mime: "url", + type: "url", + size: 0, + }; + } + + /** + * Read file meta-data + * + * @returns {Promise} + * @memberof URLFileHandle + */ + meta(): Promise { + return new Promise((resolve, reject) => + resolve({ + result: this.info, + error: false, + }) + ); + } + + /** + * Read URL content + * + * @protected + * @param {string} t data type see [[read]] + * @returns {Promise} + * @memberof URLFileHandle + */ + protected _rd(t: string): Promise { + return API.get(this.path, t ? t : "text"); + } + } + + API.VFS.register("^(http|https|ftp)$", URLFileHandle); + + /** + * Shared file handle represents all AntOS shared file. + * Its protocol is defined as: + * + * ``` + * ^shared$ + * ``` + * + * @class SharedFileHandle + * @extends {API.VFS.BaseFileHandle} + */ + export class SharedFileHandle extends API.VFS.BaseFileHandle { + /** + *Creates an instance of SharedFileHandle. + * @param {string} path file path + * @memberof SharedFileHandle + */ + constructor(path: string) { + super(path); + if (this.isRoot()) { + this.ready = true; + } + } + + /** + * Read file meta-data + * + * @returns {Promise} + * @memberof SharedFileHandle + */ + meta(): Promise { + return API.handle.fileinfo(this.path); + } + + /** + * Read file content + * + * @protected + * @param {string} t data type, see [[read]] + * @returns {Promise} + * @memberof SharedFileHandle + */ + protected _rd(t: string): Promise { + if (this.isRoot()) { + return API.get(`${API.handle.shared}/all`, t); + } + //read the file + if (t === "binary") { + return API.handle.fileblob(this.path); + } + return API.handle.readfile(this.path, t ? t : "text"); + } + + /** + * write data to shared file + * + * @protected + * @param {string} t data type, see [[write]] + * @param {string} d file data + * @returns {Promise} + * @memberof SharedFileHandle + */ + protected _wr(t: string, d: string): Promise { + return new Promise(async (resolve, reject) => { + try { + const r = await API.handle.write(this.path, d); + if (r.error) { + return reject( + API.throwe( + __("{0}: {1}", r.error, this.path) + ) + ); + } + return resolve(r); + } catch (e) { + return reject(__e(e)); + } + }); + } + + /** + * Un-publish the file + * + * @protected + * @returns {Promise} + * @memberof SharedFileHandle + */ + protected _rm(): Promise { + return new Promise(async (resolve, reject) => { + try { + const d = await API.handle.sharefile( + this.basename, + false + ); + if (d.error) { + return reject( + API.throwe( + __("{0}: {1}", d.error, this.path) + ) + ); + } + return resolve(d); + } catch (e) { + return reject(__e(e)); + } + }); + } + + /** + * Download shared file + * + * @protected + * @returns {Promise} + * @memberof SharedFileHandle + */ + protected _down(): Promise { + return new Promise((resolve, reject) => { + if (this.info.type === "dir") { + return reject( + API.throwe(__("{0} is not a file", this.path)) + ); + } + return API.handle + .fileblob(this.path) + .then((data) => { + const blob = new Blob([data], { + type: "octet/stream", + }); + API.saveblob(this.basename, blob); + return resolve(); + }) + .catch((e) => reject(__e(e))); + }); + } + + /** + * Un publish the file + * + * @protected + * @returns {Promise} + * @memberof SharedFileHandle + */ + protected _pub(): Promise { + return new Promise((resolve, reject) => + resolve({ + result: this.basename, + error: false, + }) + ); + } + } + + API.VFS.register("^shared$", SharedFileHandle); + } + } +} diff --git a/src/core/vfs/GoogleDriveHandle.js b/src/core/vfs/GoogleDriveHandle.js new file mode 100644 index 0000000..ee0cb67 --- /dev/null +++ b/src/core/vfs/GoogleDriveHandle.js @@ -0,0 +1,401 @@ +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS202: Simplify dynamic range loops + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ + +// Copyright 2017-2018 Xuan Sang LE + +// AnTOS Web desktop is is licensed under the GNU General Public +// License v3.0, see the LICENCE file for more information + +// This program is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. + +// You should have received a copy of the GNU General Public License +//along with this program. If not, see https://www.gnu.org/licenses/. + +// GoogleDrive File Handle +let G_CACHE = {"gdv://":{ id: "root", mime: 'dir' } }; + +class GoogleDriveHandle extends this.OS.API.VFS.BaseFileHandle { + constructor(path) { + super(path); + const me = this; + this.setting = Ant.OS.setting.VFS.gdrive; + if (!this.setting) { return Ant.OS.announcer.oserror(__("Unknown API setting for {0}", "GAPI"), (Ant.OS.API.throwe("OS.VFS")), null); } + if (this.isRoot()) { this.gid = 'root'; } + this.cache = ""; + } + + oninit(f) { + const me = this; + if (!this.setting) { return; } + const fn = function(r) { + if (r) { return f(); } + // perform the login + G_CACHE = {"gdv://":{ id: "root", mime: 'dir' } }; + return gapi.auth2.getAuthInstance().signIn(); + }; + + if (Ant.OS.API.libready(this.setting.apilink)) { + gapi.auth2.getAuthInstance().isSignedIn.listen(r => fn(r)); + return fn(gapi.auth2.getAuthInstance().isSignedIn.get()); + } else { + return Ant.OS.API.require(this.setting.apilink, function() { + // avoid popup block + const q = Ant.OS.announcer.getMID(); + return gapi.load("client:auth2", function() { + Ant.OS.API.loading(q, "GAPI"); + return gapi.client.init({ + apiKey: me.setting.API_KEY, + clientId: me.setting.CLIENT_ID, + discoveryDocs: me.setting.DISCOVERY_DOCS, + scope: me.setting.SCOPES + }) + .then(function() { + Ant.OS.API.loaded(q, "OK"); + gapi.auth2.getAuthInstance().isSignedIn.listen(r => fn(r)); + return _GUI.openDialog("YesNoDialog", function(d) { + if (!d) { return Ant.OS.announcer.osinfo(__("User abort the authentication")); } + return fn(gapi.auth2.getAuthInstance().isSignedIn.get()); + } + , __("Authentication") + , { text: __("Would you like to login to {0}?", "Google Drive") });}) + .catch(function(err) { + Ant.OS.API.loaded(q, "FAIL"); + return Ant.OS.announcer.oserror(__("VFS cannot init {0}: {1}", "GAPI",err.error), (Ant.OS.API.throwe("OS.VFS")), err); + }); + }); + }); + } + } + + meta(f) { + const me = this; + return this.oninit(function() { + const q = Ant.OS.announcer.getMID(); + if (G_CACHE[me.path]) { me.gid = G_CACHE[me.path].id; } + if (me.gid) { + //console.log "Gid exists ", me.gid + Ant.OS.API.loading(q, "GAPI"); + return gapi.client.drive.files.get({ + fileId: me.gid, + fields: me.fields() + }) + .then(function(r) { + Ant.OS.API.loaded(q, "OK"); + if (!r.result) { return; } + r.result.mime = r.result.mimeType; + return f(r);}).catch(function(err) { + Ant.OS.API.loaded(q, "FAIL"); + return Ant.OS.announcer.oserror(__("VFS cannot get meta data for {0}", me.gid), (Ant.OS.API.throwe("OS.VFS")), err); + }); + } else { + //console.log "Find file in ", me.parent() + const fp = me.parent().asFileHandle(); + return fp.meta(function(d) { + const file = d.result; + const q1 = Ant.OS.announcer.getMID(); + Ant.OS.API.loading(q1, "GAPI"); + G_CACHE[fp.path] = { id: file.id, mime: file.mimeType }; + return gapi.client.drive.files.list({ + q: `name = '${me.basename}' and '${file.id}' in parents and trashed = false`, + fields: `files(${me.fields()})` + }) + .then(function(r) { + //console.log r + Ant.OS.API.loaded(q1, "OK"); + if (!r.result.files || !(r.result.files.length > 0)) { return; } + G_CACHE[me.path] = { id: r.result.files[0].id, mime: r.result.files[0].mimeType }; + r.result.files[0].mime = r.result.files[0].mimeType; + return f({ result: r.result.files[0] });}) + .catch(function(err) { + Ant.OS.API.loaded(q1, "FAIL"); + return Ant.OS.announcer.oserror(__("VFS cannot get meta data for {0}", me.path), (Ant.OS.API.throwe("OS.VFS")), err); + }); + }); + } + }); + } + + fields() { + return "webContentLink, id, name,mimeType,description, kind, parents, properties, iconLink, createdTime, modifiedTime, owners, permissions, fullFileExtension, fileExtension, size"; + } + isFolder() { + return this.info.mimeType === "application/vnd.google-apps.folder"; + } + + save(id, m, f) { + const me = this; + const user = gapi.auth2.getAuthInstance().currentUser.get(); + const oauthToken = user.getAuthResponse().access_token; + const q = Ant.OS.announcer.getMID(); + const xhr = new XMLHttpRequest(); + const url = 'https://www.googleapis.com/upload/drive/v3/files/' + id + '?uploadType=media'; + xhr.open('PATCH', url); + xhr.setRequestHeader('Authorization', 'Bearer ' + oauthToken); + xhr.setRequestHeader('Content-Type', m); + xhr.setRequestHeader('Content-Encoding', 'base64'); + xhr.setRequestHeader('Content-Transfer-Encoding', 'base64'); + Ant.OS.API.loading(q, "GAPI"); + const error = function(e, s) { + Ant.OS.API.loaded(q, "FAIL"); + return Ant.OS.announcer.oserror(__("VFS cannot save : {0}", me.path), e, s); + }; + xhr.onreadystatechange = function() { + if ( xhr.readyState === 4 ) { + if ( xhr.status === 200 ) { + Ant.OS.API.loaded(q, "OK"); + return f({ result: JSON.parse(xhr.responseText) }); + } else { + return error(xhr, xhr.status); + } + } + }; + xhr.onerror = () => error(xhr, xhr.status); + if (m === "base64") { return xhr.send(me.cache.replace(/^data:[^;]+;base64,/g, "")); } + return this.sendB64(m, data => xhr.send(data.replace(/^data:[^;]+;base64,/g, ""))); + } + + getlink() { + if (this.ready) { return this.info.webContentLink; } + return undefined; + } + + action(n, p, f) { + const me = this; + const q = Ant.OS.announcer.getMID(); + Ant.OS.API.loading(q, "GAPI"); + switch (n) { + case "read": + if (!this.info.id) { return; } + if (this.isFolder()) { + return gapi.client.drive.files.list({ + q: `'${me.info.id}' in parents and trashed = false`, + fields: `files(${me.fields()})` + }) + .then(function(r) { + Ant.OS.API.loaded(q, "OK"); + if (!r.result.files) { return; } + for (let file of Array.from(r.result.files)) { + file.path = me.child(file.name); + file.mime = file.mimeType; + file.filename = file.name; + file.type = "file"; + file.gid = file.id; + if (file.mimeType === "application/vnd.google-apps.folder") { + file.mime = "dir"; + file.type = "dir"; + file.size = 0; + } + G_CACHE[file.path] = { id: file.gid, mime: file.mime }; + } + return f({ result: r.result.files });}) + .catch(function(err) { + Ant.OS.API.loaded(q, "FAIL"); + return Ant.OS.announcer.oserror(__("VFS cannot read : {0}", me.path), (Ant.OS.API.throwe("OS.VFS")), err); + }); + } else { + return gapi.client.drive.files.get({ + fileId: me.info.id, + alt: 'media' + }) + .then(function(r) { + Ant.OS.API.loaded(q, "OK"); + if (p !== "binary") { return f(r.body); } + return f(r.body.asUint8Array());}).catch(function(err) { + Ant.OS.API.loaded(q, "FAIL"); + return Ant.OS.announcer.oserror(__("VFS cannot read : {0}", me.path), (Ant.OS.API.throwe("OS.VFS")), err); + }); + } + + case "mk": + if (!this.isFolder()) { return f({ error: __("{0} is not a directory", this.path) }); } + var meta = { + name: p, + parents: [this.info.id], + mimeType: 'application/vnd.google-apps.folder' + }; + + gapi.client.drive.files.create({ + resource: meta, + fields: 'id' + }) + .then(function(r) { + Ant.OS.API.loaded(q, "OK"); + //console.log r + if (!r || !r.result) { return Ant.OS.announcer.oserror(__("VFS cannot create : {0}", p), (Ant.OS.API.throwe("OS.VFS")), r); } + G_CACHE[me.child(p)] = { id: r.result.id, mime: "dir" }; + return f(r);}).catch(function(err) { + Ant.OS.API.loaded(q, "FAIL"); + return Ant.OS.announcer.oserror(__("VFS cannot create : {0}", p), (Ant.OS.API.throwe("OS.VFS")), err); + }); + + return; + + case "write": + var gid = undefined; + if (G_CACHE[me.path]) { gid = G_CACHE[me.path].id; } + if (gid) { + Ant.OS.API.loaded(q, "OK"); + return this.save(gid, p, f); + } else { + const dir = this.parent().asFileHandle(); + return dir.onready(function() { + meta = { + name: me.basename, + mimeType: p, + parents: [dir.info.id] + }; + + return gapi.client.drive.files.create({ + resource: meta, + fields: 'id' + }) + .then(function(r) { + Ant.OS.API.loaded(q, "OK"); + if (!r || !r.result) { return Ant.OS.announcer.oserror(__("VFS cannot write : {0}", me.path), (Ant.OS.API.throwe("OS.VFS")), r); } + G_CACHE[me.path] = { id: r.result.id, mime: p }; + return me.save(r.result.id, p, f);}).catch(function(err) { + Ant.OS.API.loaded(q, "FAIL"); + return Ant.OS.announcer.oserror(__("VFS cannot write : {0}", me.path), (Ant.OS.API.throwe("OS.VFS")), err); + }); + }); + } + + case "upload": + if (!this.isFolder()) { return; } + //insert a temporal file selector + var o = ($('')).attr('type', 'file').css("display", "none"); + Ant.OS.API.loaded(q, "OK"); + o.change(function() { + //Ant.OS.API.loading q, p + const fo = o[0].files[0]; + const file = (me.child(fo.name)).asFileHandle(); + file.cache = fo; + file.write(fo.type, f); + return o.remove(); + }); + + //Ant.OS.API.loaded q, p, "OK" + //Ant.OS.API.loaded q, p, "FAIL" + + return o.click(); + + case "remove": + if (!this.info.id) { return; } + return gapi.client.drive.files.delete({ + fileId: me.info.id + }) + .then(function(r) { + //console.log r + Ant.OS.API.loaded(q, "OK"); + if (!r) { return Ant.OS.announcer.oserror(__("VFS cannot delete : {0}", me.path), (Ant.OS.API.throwe("OS.VFS")), r); } + G_CACHE[me.path] = null; + return f({ result: true });}) + .catch(function(err) { + Ant.OS.API.loaded(q, "FAIL"); + return Ant.OS.announcer.oserror(__("VFS cannot delete : {0}", me.path), (Ant.OS.API.throwe("OS.VFS")), err); + }); + + case "publish": + Ant.OS.API.loaded(q, "OK"); + return; + + case "download": + return gapi.client.drive.files.get({ + fileId: me.info.id, + alt: 'media' + }) + .then(function(r) { + Ant.OS.API.loaded(q, "OK"); + if (!r.body) { return Ant.OS.announcer.oserror(__("VFS cannot download file : {0}", me.path), (Ant.OS.API.throwe("OS.VFS")), r); } + let bytes = []; + for (let i = 0, end = r.body.length - 1, asc = 0 <= end; asc ? i <= end : i >= end; asc ? i++ : i--) { + bytes.push(r.body.charCodeAt(i)); + } + bytes = new Uint8Array(bytes); + const blob = new Blob([bytes], { type: "octet/stream" }); + return Ant.OS.API.saveblob(me.basename, blob);}).catch(function(err) { + Ant.OS.API.loaded(q, "FAIL"); + return Ant.OS.announcer.oserror(__("VFS cannot download file : {0}", me.path), (Ant.OS.API.throwe("OS.VFS")), err); + }); + + case "move": + var dest = p.asFileHandle().parent().asFileHandle(); + return dest.onready(function() { + const previousParents = me.info.parents.join(','); + return gapi.client.drive.files.update({ + fileId: me.info.id, + addParents: dest.info.id, + removeParents: previousParents, + fields: "id" + }) + .then(function(r) { + Ant.OS.API.loaded(q, "OK"); + if (!r) { return Ant.OS.announcer.oserror(__("VFS cannot move : {0}", me.path), (Ant.OS.API.throwe("OS.VFS")), r); } + return f(r);}).catch(function(err) { + Ant.OS.API.loaded(q, "FAIL"); + return Ant.OS.announcer.oserror(__("VFS cannot move : {0}", me.gid), (Ant.OS.API.throwe("OS.VFS")), err); + }); + } + , err => Ant.OS.API.loaded(q, "FAIL")); + default: + Ant.OS.API.loaded(q, "FAIL"); + return Ant.OS.announcer.osfail(__("VFS unknown action: {0}", n), (Ant.OS.API.throwe("OS.VFS")), n); + } + } +} + + +self.OS.API.VFS.register("^gdv$", GoogleDriveHandle); +// search the cache for file +self.OS.API.onsearch("Google Drive", function(t) { + const arr = []; + const term = new RegExp(t, "i"); + for (let k in G_CACHE) { + const v = G_CACHE[k]; + if ((k.match(term)) || (v && v.mime.match(term))) { + const file = k.asFileHandle(); + file.text = file.basename; + file.mime = v.mime; + file.iconclass = "fa fa-file"; + if (file.mime === "dir") { file.iconclass = "fa fa-folder"; } + file.complex = true; + file.detail = [{ text: file.path }]; + arr.push(file); + } + } + return arr; +}); + +self.OS.onexit("cleanUpGoogleDrive", function() { + G_CACHE = { "gdv://": { id: "root", mime: 'dir' } }; + if (!Ant.OS.setting.VFS.gdrive || !Ant.OS.API.libready(Ant.OS.setting.VFS.gdrive.apilink)) { return; } + const auth2 = gapi.auth2.getAuthInstance(); + if (!auth2) { return; } + if (auth2.isSignedIn.get()) { + let el; + return el = $('${2}\nsnippet iframe.\n ${3}\nsnippet iframe#\n ${3}\nsnippet img\n ${2}${3}\nsnippet img.\n ${3}${4}\nsnippet img#\n ${3}${4}\nsnippet input\n ${5}\nsnippet input.\n ${6}\nsnippet input:text\n ${4}\nsnippet input:submit\n ${4}\nsnippet input:hidden\n ${4}\nsnippet input:button\n ${4}\nsnippet input:image\n ${5}\nsnippet input:checkbox\n ${3}\nsnippet input:radio\n ${3}\nsnippet input:color\n ${4}\nsnippet input:date\n ${4}\nsnippet input:datetime\n ${4}\nsnippet input:datetime-local\n ${4}\nsnippet input:email\n ${4}\nsnippet input:file\n ${4}\nsnippet input:month\n ${4}\nsnippet input:number\n ${4}\nsnippet input:password\n ${4}\nsnippet input:range\n ${4}\nsnippet input:reset\n ${4}\nsnippet input:search\n ${4}\nsnippet input:time\n ${4}\nsnippet input:url\n ${4}\nsnippet input:week\n ${4}\nsnippet ins\n ${1}\nsnippet kbd\n ${1}\nsnippet keygen\n ${1}\nsnippet label\n \nsnippet label:i\n \n ${7}\nsnippet label:s\n \n \nsnippet legend\n ${1}\nsnippet legend+\n ${1}\nsnippet li\n
  • ${1}
  • \nsnippet li.\n
  • ${2}
  • \nsnippet li+\n
  • ${1}
  • \n li+${2}\nsnippet lia\n
  • ${1}
  • \nsnippet lia+\n
  • ${1}
  • \n lia+${3}\nsnippet link\n ${5}\nsnippet link:atom\n ${2}\nsnippet link:css\n ${4}\nsnippet link:favicon\n ${2}\nsnippet link:rss\n ${2}\nsnippet link:touch\n ${2}\nsnippet map\n \n ${2}\n \nsnippet map.\n \n ${3}\n \nsnippet map#\n \n ${5}${6}\n ${7}\nsnippet mark\n ${1}\nsnippet menu\n \n ${1}\n \nsnippet menu:c\n \n ${1}\n \nsnippet menu:t\n \n ${1}\n \nsnippet meta\n ${3}\nsnippet meta:compat\n ${3}\nsnippet meta:refresh\n ${3}\nsnippet meta:utf\n ${3}\nsnippet meter\n ${1}\nsnippet nav\n \nsnippet nav.\n \nsnippet nav#\n \nsnippet noscript\n \nsnippet object\n \n ${3}\n ${4}\n# Embed QT Movie\nsnippet movie\n \n \n \n \n \n ${6}\nsnippet ol\n
      \n ${1}\n
    \nsnippet ol.\n
      \n ${2}\n
    \nsnippet ol+\n
      \n
    1. ${1}
    2. \n li+${2}\n
    \nsnippet opt\n \nsnippet opt+\n \n opt+${3}\nsnippet optt\n \nsnippet optgroup\n \n \n opt+${3}\n \nsnippet output\n ${1}\nsnippet p\n

    ${1}

    \nsnippet param\n ${3}\nsnippet pre\n
    \n		${1}\n	
    \nsnippet progress\n ${1}\nsnippet q\n ${1}\nsnippet rp\n ${1}\nsnippet rt\n ${1}\nsnippet ruby\n \n ${1}\n \nsnippet s\n ${1}\nsnippet samp\n \n ${1}\n \nsnippet script\n