Merge pull request #4 from lxsang/antos-1.0.0a

Antos 1.0.0a
This commit is contained in:
Xuan Sang LE 2020-06-22 11:33:33 +02:00 committed by GitHub
commit 7a619ac3a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
774 changed files with 30843 additions and 53558 deletions

7
.gitignore vendored
View File

@ -1,3 +1,8 @@
build
node_modules
.DS_Store
.DS_Store
package-lock.json
dist
docs
coffees
.vscode

158
Makefile
View File

@ -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

View File

@ -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 <xsang.le AT gmail DOT com>
Copyright 2017-2020 Xuan Sang LE <xsang.le AT gmail DOT com>
AnTOS is is licensed under the GNU General Public License v3.0, see the LICENCE file for more information

8
jest.config.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
roots: ['<rootDir>'],
transform : {
'^.+\\.ts$': 'ts-jest'
},
testRegex: '(/tests/test.*|(\\.|/)(test|spec))\\.[tj]s?$',
moduleFileExtensions: ['js', 'ts'],
}

View File

@ -1,26 +0,0 @@
# Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
# 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()

25
src/bootstrap.ts Normal file
View File

@ -0,0 +1,25 @@
// Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
// 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();
};

353
src/core/Announcerment.ts Normal file
View File

@ -0,0 +1,353 @@
// Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
// 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<ObservableEntryType>}
* @memberof Announcer
*/
observable: GenericObject<ObservableEntryType>;
/**
* 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;
}
}
}

View File

@ -1,132 +0,0 @@
# Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
# 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

443
src/core/BaseApplication.ts Normal file
View File

@ -0,0 +1,443 @@
// Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
// 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<any>}
* @memberof BaseApplication
*/
setting: GenericObject<any>;
/**
* 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<any>} promise the promise on a task to be performed
* @returns {Promise<any>}
* @memberof BaseApplication
*/
protected load(promise: Promise<any>): Promise<any> {
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;
}
}

View File

@ -1,339 +0,0 @@
# Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
# 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: <tag_name>,
buttons:[
{
label: 'buton label',
onclick: function(d){...}
}, ...
]
}
###
class BasicDialog extends BaseDialog
constructor: ( name, @conf, @title) ->
super name
init: () ->
@title = @name if not @title
html = "<afx-app-window data-id = '#{@name}' width='#{@conf.width}' height='#{@conf.height}'>"
html += "<afx-hbox><div data-width='7'></div><afx-vbox><div data-height='5'></div>"
html += "<#{v.tag} #{v.att} data-id = 'content#{k}'></#{v.tag}>" for k,v of @conf.tags
html += "<div data-height = '35' style=' text-align:right;padding-top:3px;'>"
html += "<afx-button data-id = 'bt#{k}' text = '#{v.label}' style='margin-left:5px;'></afx-button>" for k,v of @conf.buttons
html += "</div><div data-height='5'></div></afx-vbox><div data-width='7'></div></afx-hbox></afx-app-window>"
#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

1126
src/core/BaseDialog.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +0,0 @@
# Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
# 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 BaseEvent
constructor: (@name, @force) ->
@prevent = false
preventDefault: () ->
@prevent = true if not @force
this.OS.GUI.BaseEvent = BaseEvent

View File

@ -1,121 +0,0 @@
# Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
# 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 BaseModel
constructor: (@name, @args) ->
me = @
@observable = riot.observable()
@_api = self.OS.API
@_gui = self.OS.GUI
@systemsetting = self.OS.setting
me = @
@on "exit", () -> me.quit()
@host = "#desktop"
@dialog = undefined
render: (p) ->
_GUI.loadScheme p, @, @host
quit: (force) ->
evt = new _GUI.BaseEvent("exit", force)
@onexit(evt)
if not evt.prevent
@observable.off "*"
delete @.observable
@dialog.quit() if @dialog
_PM.kill @
path: () ->
mt = @meta()
return mt.path if mt and mt.path
return null
# call a server side script
call: (cmd, func) ->
@_api.apigateway cmd, false, func
# get a stream
stream: () ->
return @_api.apigateway null, true, null
init: ->
#implement by sub class
onexit: (e) ->
#implement by subclass
one: (e, f) -> @observable.one e, f
on: (e, f) -> @observable.on e, f
off: (e, f) ->
return @observable.off e unless f
@observable.off e, f
trigger: (e, d) -> @observable.trigger e, d
subscribe: (e, f) ->
_courrier.on e, f, @
openDialog: (d, f, title, data) ->
if @dialog
@dialog.show()
return
if typeof d is "string"
if not _GUI.subwindows[d]
@error __("Dialog {0} not found", d)
return
@dialog = new _GUI.subwindows[d]()
else
@dialog = d
#@dialog.observable = riot.observable() unless @dialog
@dialog.parent = @
@dialog.handler = f
@dialog.pid = @pid
@dialog.data = data
@dialog.title = title
@dialog.init()
ask: (t, m, f) ->
@._gui.openDialog "YesNoDialog", (d) ->
f() if d
, t, { text: m }
publish: (t, m, e) ->
mt = @meta()
icon = undefined
icon = "#{mt.path}/#{mt.icon}" if mt.icon
_courrier.trigger t, { id: @pid, name: @name, data: { m: m, icon: icon, iconclass: mt.iconclass }, error: e }
notify: (m) ->
@publish "notification", m
warn: (m) ->
@publish "warning", m
error: (m) ->
@publish "error", m, (@_api.throwe @name)
fail: (m) ->
@publish "fail", m
throwe: () ->
@_api.throwe @name
update:->
@scheme.update() if @scheme
find: (id) -> ($ "[data-id='#{id}']", @scheme)[0] if @scheme
select: (sel) -> $ sel, @scheme if @scheme
this.OS.GUI.BaseModel = BaseModel

712
src/core/BaseModel.ts Normal file
View File

@ -0,0 +1,712 @@
// Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
// 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 {
/**
* Application argument type definition
*
* @export
* @interface AppArgumentsType
*/
export interface AppArgumentsType {
/**
* File type to be open by the app
*
* @type {string}
* @memberof AppArgumentsType
*/
type?: string;
/**
* File path to be opened
*
* @type {string}
* @memberof AppArgumentsType
*/
path: string;
/**
* Any other object
*/
[propName: string]: any;
}
/**
* Enum definition of different model types
*
* @export
* @enum {number}
*/
export enum ModelType {
/**
* Applications
*/
Application,
/**
* Services
*/
Service,
/**
* Sub-window such as dialogs
*/
SubWindow,
}
/**
* Base AntOS event definition
*
* @export
* @class BaseEvent
*/
export class BaseEvent {
/**
* The event name placeholder
*
* @type {string}
* @memberof BaseEvent
*/
name: string;
/**
* Placeholder indicates whether the event is forced to
* be happen
*
* @private
* @type {boolean}
* @memberof BaseEvent
*/
private force: boolean;
/**
* Placeholder indicates whether the event is prevented.
* This value has not effect if `force` is set to `true`
*
* @type {boolean}
* @memberof BaseEvent
*/
prevent: boolean;
/**
*Creates an instance of BaseEvent.
* @param {string} name event name
* @param {boolean} force indicates whether the event is forced
* @memberof BaseEvent
*/
constructor(name: string, force: boolean) {
this.name = name;
this.force = force;
this.prevent = false;
}
/**
* Prevent the current event. This function
* has no effect if `force` is set to true
*
* @memberof BaseEvent
*/
preventDefault(): void {
if (!this.force) {
this.prevent = true;
}
}
}
/**
* The root model of all applications, dialogs or services
* in the system
*
* @export
* @abstract
* @class BaseModel
*/
export abstract class BaseModel {
/**
* The class name
*
* @type {string}
* @memberof BaseModel
*/
name: string;
/**
* The argument of the model
*
* @type {AppArgumentsType[]}
* @memberof BaseModel
*/
args: AppArgumentsType[];
/**
* Each model has its own local announcement system
* to handle all local events inside that model.
*
* This observable object is propagate to all the
* UI elements ([[AFXTag]]) inside the model
*
* @protected
* @type {API.Announcer}
* @memberof BaseModel
*/
protected _observable: API.Announcer;
/**
* Reference to the core API namespace
*
* @protected
* @type {typeof API}
* @memberof BaseModel
*/
protected _api: typeof API;
/**
* Reference to the core GUI namespace
*
* @protected
* @type {typeof GUI}
* @memberof BaseModel
*/
protected _gui: typeof GUI;
/**
* Reference to the model's dialog
*
* @type {GUI.BaseDialog}
* @memberof BaseModel
*/
dialog: GUI.BaseDialog;
/**
* The HTML element ID of the virtual desktop
*
* @protected
* @type {string}
* @memberof BaseModel
*/
protected host: string;
/**
* The process number of the current model.
* For sub-window this number is the number
* of the parent window
*
* @type {number}
* @memberof BaseModel
*/
pid: number;
/**
* Reference the DOM element of the UI scheme belong to
* this model
*
* @type {HTMLElement}
* @memberof BaseModel
*/
scheme: HTMLElement;
/**
* Reference to the system setting
*
* @protected
* @type {typeof setting}
* @memberof BaseModel
*/
protected systemsetting: typeof setting;
/**
* Placeholder for the process creation timestamp
*
* @type {number}
* @memberof BaseModel
*/
birth: number;
/**
* Different model type
*
* @static
* @type {ModelType}
* @memberof BaseModel
*/
static type: ModelType;
/**
* Allow singleton on this model
*
* @static
* @type {boolean}
* @memberof BaseModel
*/
static singleton: boolean;
/**
* The javascript or css files that the model depends on. All dependencies
* will be loaded before the model is rendered
*
* @static
* @type {string[]} list of VFS paths of dependencies
* @memberof BaseModel
*/
static dependencies: string[];
/**
* Reference to the CSS Element of the model
*
* @static
* @type {(HTMLElement | string)}
* @memberof BaseModel
*/
static style: HTMLElement | string;
/**
* Place holder for model meta-data
*
* @static
* @type {API.PackageMetaType}
* @memberof BaseModel
*/
static meta: API.PackageMetaType;
/**
*Creates an instance of BaseModel.
* @param {string} name class name
* @param {AppArgumentsType[]} args arguments
* @memberof BaseModel
*/
constructor(name: string, args: AppArgumentsType[]) {
this.name = name;
this.args = args;
this._observable = new API.Announcer();
this._api = API;
this._gui = GUI;
this.systemsetting = setting;
this.on("exit", () => this.quit(false));
this.host = this._gui.workspace;
this.dialog = undefined;
this.subscribe("systemlocalechange", (name) => {
this.updateLocale(name);
return this.update();
});
}
/**
* Getter: get the local announcer object
*
* @readonly
* @type {API.Announcer}
* @memberof BaseModel
*/
get observable(): API.Announcer {
return this._observable;
}
/**
* Update the model locale
*
* @protected
* @param {string} name
* @memberof BaseModel
*/
protected updateLocale(name: string) {}
/**
* Render the model's UI
*
* @protected
* @param {string} p VFS path to the UI scheme definition
* @returns {void}
* @memberof BaseModel
*/
protected render(p: string): void {
return GUI.loadScheme(p, this, this.host);
}
/**
* Exit the model
*
* @param {boolean} force set this value to `true` will bypass the prevented exit event by user
* @returns {void}
* @memberof BaseModel
*/
quit(force: boolean): void {
const evt = new BaseEvent("exit", force);
this.onexit(evt);
if (!evt.prevent) {
this.observable.off("*");
delete this._observable;
if (this.dialog) {
this.dialog.quit();
}
return PM.kill(this);
}
}
/**
* Model meta data, need to be implemented by
* subclasses
*
* @abstract
* @returns {API.PackageMetaType}
* @memberof BaseModel
*/
abstract meta(): API.PackageMetaType;
/**
* VFS path to the model asset
*
* @returns {string}
* @memberof BaseModel
*/
path(): string {
const mt = this.meta();
if (mt && mt.path) {
return mt.path;
}
return null;
}
/**
* Execute a server side script and get back the result
*
* @protected
* @param {GenericObject<any>} cmd execution indication, should be:
*
* ```
* {
* path?: string, // VFS path to the server side script
* code: string, // or server side code to be executed
* parameters: any // the parameters of the server side execution
* }
* ```
*
* @returns {Promise<any>}
* @memberof BaseModel
*/
protected call(cmd: GenericObject<any>): Promise<any> {
return this._api.apigateway(cmd, false);
}
/**
* Connect to the server side api using a websocket connection
*
* Server side script can be execute inside the stream by writing
* data in JSON format with the following interface
*
* ```
* {
* path?: string, // VFS path to the server side script
* code: string, // or server side code to be executed
* parameters: any // the parameters of the server side execution
* }
* ```
*
* @protected
* @returns {Promise<WebSocket>}
* @memberof BaseModel
*/
protected stream(): Promise<WebSocket> {
return this._api.apigateway(null, true) as Promise<WebSocket>;
}
/**
* Init the model before UI rendering
*
* @abstract
* @memberof BaseModel
*/
abstract init(): void;
/**
* Main entry point after UI rendering
*
* @abstract
* @memberof BaseModel
*/
abstract main(): void;
/**
* Show the model
*
* @abstract
* @memberof BaseModel
*/
abstract show(): void;
/**
* Hide the model
*
* @abstract
* @memberof BaseModel
*/
abstract hide(): void;
/**
* Function called when the model exits
*
* @protected
* @abstract
* @param {BaseEvent} e exit event
* @memberof BaseModel
*/
protected abstract onexit(e: BaseEvent): void;
/**
* subscribe once to a local event
*
* @protected
* @param {string} e name of the event
* @param {(d: any) => void} f event callback
* @returns {void}
* @memberof BaseModel
*/
protected one(e: string, f: (d: any) => void): void {
return this.observable.one(e, f);
}
/**
* Subscribe to a local event
*
* @protected
* @param {string} e event name
* @param {(d: any) => void} f event callback
* @returns {void}
* @memberof BaseModel
*/
protected on(e: string, f: (d: any) => void): void {
return this.observable.on(e, f);
}
/**
* Unsubscribe an event
*
* @protected
* @param {string} e event name or `*` (all events)
* @param {(d: any) => void} [f] callback to be unsubscribed, can be `undefined`
* @returns {void}
* @memberof BaseModel
*/
protected off(e: string, f?: (d: any) => void): void {
if (!f) {
return this.observable.off(e);
}
return this.observable.off(e, f);
}
/**
* trigger a local event
*
* @protected
* @param {string} e event name
* @param {*} [d] event data
* @returns {void}
* @memberof BaseModel
*/
trigger(e: string, d?: any): void {
if (!this.observable) return;
this.observable.trigger(e, d);
}
/**
* subscribe to an event on the global announcement system
*
* @protected
* @param {string} e event name
* @param {(d: any) => void} f event callback
* @returns {void}
* @memberof BaseModel
*/
subscribe(e: string, f: (d: any) => void): void {
return announcer.on(e, f, this);
}
/**
* Open a dialog
*
* @param {(GUI.BaseDialog | string)} d a dialog object or a dialog class name
* @param {GenericObject<any>} [data] input data of the dialog, refer to each
* dialog definition for the format of the input data
* @returns {Promise<any>} A promise on the callback data of the dialog, refer
* to each dialog definition for the format of the callback data
* @memberof BaseModel
*/
openDialog(
d: GUI.BaseDialog | string,
data?: GenericObject<any>
): Promise<any> {
return new Promise((resolve, reject) => {
if (this.dialog) {
this.dialog.show();
return;
}
if (typeof d === "string") {
if (!GUI.dialogs[d]) {
this.error(__("Dialog {0} not found", d));
return;
}
this.dialog = new OS.GUI.dialogs[d as string]();
} else {
this.dialog = d;
}
//@dialog.observable = riot.observable() unless @dialog
this.dialog.parent = this;
this.dialog.handle = resolve;
this.dialog.pid = this.pid;
this.dialog.data = data;
return this.dialog.init();
});
}
/**
* Open a [[YesNoDialog]] to confirm a task
*
* @protected
* @param {GenericObject<any>} data [[YesNoDialog]] input data
* @returns {Promise<boolean>}
* @memberof BaseModel
*/
protected ask(data: GenericObject<any>): Promise<boolean> {
return this._gui.openDialog("YesNoDialog", data);
}
/**
* Trigger a global event
*
* @protected
* @param {string} t event name
* @param {(string | FormattedString)} m event message
* @param {Error} [e] error object if any
* @returns {void}
* @memberof BaseModel
*/
protected publish(
t: string,
m: string | FormattedString,
e?: Error
): void {
const mt = this.meta();
let icon: string = undefined;
if (mt.icon) {
icon = `${mt.path}/${mt.icon}`;
}
return announcer.trigger(t, {
id: this.pid,
name: this.name,
data: {
m: m,
icon: icon,
iconclass: mt.iconclass,
e: e,
},
});
}
/**
* Publish a global notification
*
* @param {(string | FormattedString)} m notification string
* @returns {void}
* @memberof BaseModel
*/
notify(m: string | FormattedString): void {
return this.publish("notification", m);
}
/**
* Publish a global warning
*
* @param {(string | FormattedString)} m warning string
* @returns {void}
* @memberof BaseModel
*/
warn(m: string | FormattedString): void {
return this.publish("warning", m);
}
/**
* Report a global error
*
* @param {(string | FormattedString)} m error message
* @param {Error} [e] error object if any
* @returns
* @memberof BaseModel
*/
error(m: string | FormattedString, e?: Error) {
return this.publish("error", m, e ? e : this._api.throwe(m));
}
/**
* Report a global fail event
*
* @param {string} m fail message
* @param {Error} [e] error object if any
* @returns
* @memberof BaseModel
*/
fail(m: string, e?: Error) {
return this.publish("fail", m, e ? e : this._api.throwe(m));
}
/**
* Throw an error inside the model
*
* @returns {Error}
* @memberof BaseModel
*/
throwe(): Error {
return this._api.throwe(this.name);
}
/**
* Update the model, this will update all its UI elements
*
* @returns {void}
* @memberof BaseModel
*/
update(): void {
if (this.scheme) {
return this.scheme.update();
}
}
/**
* Find a HTMLElement in the UI of the model
* using the `data-id` attribute of the element
*
* @protected
* @param {string} id
* @returns {HTMLElement}
* @memberof BaseModel
*/
protected find(id: string): HTMLElement {
if (this.scheme) {
return $(`[data-id='${id}']`, this.scheme)[0];
}
}
/**
* Select all DOM Element inside the UI of the model
* using JQuery selector
*
* @protected
* @param {string} sel
* @returns {HTMLElement}
* @memberof BaseModel
*/
protected select(sel: string): JQuery<HTMLElement> {
if (this.scheme) {
return $(sel, this.scheme);
}
}
}
}

View File

@ -1,61 +0,0 @@
# Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
# 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 BaseService extends this.OS.GUI.BaseModel
constructor: (name, args) ->
super name, args
@icon = undefined
@iconclass = "fa-paper-plane-o"
@text = ""
@timer = undefined
@holder = undefined
init: ()->
#implement by user
# event registe, etc
# scheme loader
meta: () ->
_OS.APP[@name].meta
attach: (h) ->
@holder = h
update: () ->
@holder.update() if @holder
@scheme.update() if @scheme
watch: ( t, f) ->
me = @
func = () ->
f()
me.timer = setTimeout (() -> func()), t
func()
onexit: (evt) ->
console.log "clean timer" if @timer
clearTimeout @timer if @timer
@cleanup(evt)
($ @scheme).remove() if @scheme
main: () ->
show: () ->
awake: (e) ->
#implement by user to tart the service
cleanup: (evt) ->
#implemeted by user
BaseService.type = 2
BaseService.singleton = true
this.OS.GUI.BaseService = BaseService

252
src/core/BaseService.ts Normal file
View File

@ -0,0 +1,252 @@
// Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
// 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 application {
/**
* Services are processes that run in the background and
* are waken up in certain circumstances such as by global
* events or user interactions.
*
* Each service takes an entry in the system tray menu
* located on the system panel. This menu entry is used
* to access to service visual contents such as: options,
* task performing based on user interaction, etc.
*
* Services are singleton processes, there is only
* one process of a service at a time
*
* @export
* @abstract
* @class BaseService
* @extends {BaseModel}
*/
export abstract class BaseService extends BaseModel {
/**
* The service icon shown in the system tray
*
* @type {string}
* @memberof BaseService
*/
icon: string;
/**
* CSS class of the service icon shown in the system tray
*
* @type {string}
* @memberof BaseService
*/
iconclass: string;
/**
* Text of the service shown in the system tray
*
* @type {string}
* @memberof BaseService
*/
text: string;
/**
* Reference to the menu entry DOM element attached
* to the service
*
* @type {HTMLElement}
* @memberof BaseService
*/
domel: HTMLElement;
/**
* Reference to the timer that periodically executes the callback
* defined in [[watch]].
*
* @private
* @type {number}
* @memberof BaseService
*/
private timer: number;
/**
* Reference to the system tray menu
*
* @type {HTMLElement}
* @memberof BaseService
*/
holder: HTMLElement;
/**
* Placeholder for service select callback
*
* @memberof BaseService
*/
onmenuselect: (
d: OS.GUI.TagEventType<GUI.tag.MenuEventData>
) => void;
/**
*Creates an instance of BaseService.
* @param {string} name service class name
* @param {AppArgumentsType[]} args service arguments
* @memberof BaseService
*/
constructor(name: string, args: AppArgumentsType[]) {
super(name, args);
this.icon = undefined;
this.iconclass = "fa-paper-plane-o";
this.text = "";
this.timer = undefined;
this.holder = undefined;
this.onmenuselect = (d) => {
return this.awake(d);
};
}
/**
* Do nothing
*
* @memberof BaseService
*/
hide(): void {}
/**
* Init the service before attaching it to
* the system tray: event subscribe, scheme
* loading.
*
* Should be implemented by all subclasses
*
* @abstract
* @memberof BaseService
*/
abstract init(): void;
/**
* Refresh the service menu entry in the
* system tray
*
* @memberof BaseService
*/
update(): void {
(this.domel as GUI.tag.MenuEntryTag).data = this;
}
/**
* Get the service meta-data
*
* @returns {API.PackageMetaType}
* @memberof BaseService
*/
meta(): API.PackageMetaType {
return application[this.name].meta;
}
/**
* Attach the service to a menu element
* such as the system tray menu
*
* @param {HTMLElement} h
* @memberof BaseService
*/
attach(h: HTMLElement): void {
this.holder = h;
}
/**
* Set the callback that will be called periodically
* after a period of time.
*
* Each service should only have at most one watcher
*
* @protected
* @param {number} t period time in seconds
* @param {() => void} f callback function
* @returns {number}
* @memberof BaseService
*/
protected watch(t: number, f: () => void): number {
var func = () => {
f();
if (this.timer) {
clearTimeout(this.timer);
}
return (this.timer = setTimeout(() => func(), t));
};
return func();
}
/**
* This function is called when the service
* is exited
*
* @protected
* @param {BaseEvent} evt exit event
* @returns
* @memberof BaseService
*/
protected onexit(evt: BaseEvent) {
if (this.timer) {
console.log("clean timer");
}
if (this.timer) {
clearTimeout(this.timer);
}
this.cleanup(evt);
if (this.scheme) {
return $(this.scheme).remove();
}
}
/**
* Do nothing
*
* @memberof BaseService
*/
main(): void {}
/**
* Do nothing
*
* @memberof BaseService
*/
show(): void {}
/**
* Awake the service, this function is usually called when
* the system tray menu entry attached to the service is
* selected.
*
* This function should be implemented by all subclasses
*
* @abstract
* @param {GUI.TagEventType} e
* @memberof BaseService
*/
abstract awake(e: GUI.TagEventType<GUI.tag.MenuEventData>): void;
/**
* Do nothing
*
* @protected
* @param {BaseEvent} evt
* @memberof BaseService
*/
protected cleanup(evt: BaseEvent) {}
}
BaseService.type = ModelType.Service;
BaseService.singleton = true;
}
}

View File

@ -1,403 +0,0 @@
# Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
# 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 FormatedString
constructor: (@fs, args) ->
@values = []
return unless args
@values[i] = args[i] for i in [0..args.length - 1]
toString: () ->
@__()
__: () ->
me = @
return @fs.l().replace /{(\d+)}/g, (match, number) ->
return if typeof me.values[number] != 'undefined' then me.values[number].__() else match
hash: () ->
@__().hash()
asBase64: () ->
@__().asBase64()
unescape: () ->
@__().unescape()
asUint8Array: () ->
@__().asUint8Array()
format: () ->
args = arguments
@values[i] = args[i] for i in [0..args.length - 1]
class Version
constructor:(@string) ->
arr = @string.split "-"
br =
"r": 3,
"b": 2,
"a": 1
@branch = 3
@branch = br[arr[1]] if arr.length is 2 and br[arr[1]]
mt = arr[0].match /\d+/g
throw new Error __("Version string is in invalid format: {0}", @string) if not mt
@major = 0
@minor = 0
@patch = 0
@major = Number mt[0] if mt.length >= 1
@minor = Number mt[1] if mt.length >= 2
@patch = Number mt[2] if mt.length >= 3
# this function return
# 0 if the version is unchanged
# 1 if the current version is newer
# -1 if the current version is older
compare: (o) ->
other = o.__v()
return 1 if @branch > other.branch
return -1 if @branch < other.branch
return 0 if @major is other.major and @minor is other.minor and @patch is other.patch
return 1 if @major > other.major
return -1 if @major < other.major
return 1 if @minor > other.minor
return -1 if @minor < other.minor
return 1 if @patch > other.patch
return -1
nt: (o) ->
return (@compare o) is 1
ot: (o) ->
return (@compare o) is -1
__v: () -> @
toString: () -> @string
Object.defineProperty Object.prototype, '__',
value: () ->
return @toString()
enumerable: false
writable: true
String.prototype.hash = () ->
hash = 5381
i = this.length
hash = (hash * 33) ^ this.charCodeAt(--i) while i
hash >>> 0
String.prototype.__v = () ->
return new Version @
String.prototype.asBase64 = () ->
tmp = encodeURIComponent this
return btoa ( tmp.replace /%([0-9A-F]{2})/g, (match, p1) ->
return String.fromCharCode (parseInt p1, 16)
)
String.prototype.unescape = () ->
d = @
d = d.replace /\\\\/g, "\\"
d = d.replace /\\"/g, '"'
d = d.replace /\\n/g, "\n"
d = d.replace /\\t/g, "\t"
d = d.replace /\\b/g, "\b"
d = d.replace /\\f/g, "\f"
d = d.replace /\\r/g, "\r"
d
String.prototype.asUint8Array = () ->
bytes = []
for i in [0..(@length - 1)]
bytes.push @charCodeAt i
bytes = new Uint8Array(bytes)
return bytes
if not String.prototype.format
String.prototype.format = () ->
args = arguments
return @replace /{(\d+)}/g, (match, number) ->
return if typeof args[number] != 'undefined' then args[number].__() else match
String.prototype.f = () ->
args = arguments
return new FormatedString(@, args)
String.prototype.__ = () ->
match = @match(/^__\((.*)\)$/)
return match[1].l() if match
return @
String.prototype.l = () ->
_API = window.OS.API
_API.lang[@] = @ unless _API.lang[@]
return _API.lang[@]
# language directive
this.__ = () ->
_API = window.OS.API
args = arguments
return "Undefined" unless args.length > 0
d = args[0]
d.l()
return new FormatedString d, (args[i] for i in [1 .. args.length - 1])
Date.prototype.toString = () ->
dd = @getDate()
mm = @getMonth() + 1
yyyy = @getFullYear()
hh = @getHours()
mi = @getMinutes()
se = @getSeconds()
dd = "0#{dd}" if dd < 10
mm = "0#{mm}" if mm < 10
hh = "0#{hh}" if hh < 10
mi = "0#{mi}" if mi < 10
se = "0#{se}" if se < 10
return "#{dd}/#{mm}/#{yyyy} #{hh}:#{mi}:#{se}"
Date.prototype.timestamp = () ->
return @getTime() / 1000 | 0
self.OS.API =
# the handler object could be a any remote or local handle to
# fetch user data, used by the API to make requests
# handlers are defined in /src/handlers
handler: {}
shared: {} # shared libraries
searchHandler:{}
lang:{}
#request a user data
mid: () ->
return _courrier.getMID()
post: (p, d, c, f) ->
q = _courrier.getMID()
_API.loading q, p
$.ajax {
type: 'POST',
url: p,
contentType: 'application/json',
data: JSON.stringify d,
dataType: 'json',
success: null
}
#$.getJSON p, d
.done (data) ->
_API.loaded q, p, "OK"
c(data)
.fail (e, s) ->
_API.loaded q, p, "FAIL"
f(e, s)
blob: (p, c, f) ->
q = _courrier.getMID()
r = new XMLHttpRequest()
r.open "GET", p, true
r.responseType = "arraybuffer"
r.onload = (e) ->
if @status is 200 and @readyState is 4
c @response
_API.loaded q, p, "OK"
else
f e, @
_API.loaded q, p, "FAIL"
_API.loading q, p
r.send()
upload: (p, d, c, f) ->
q = _courrier.getMID()
#insert a temporal file selector
o = ($ '<input>').attr('type', 'file').css("display", "none")
o.change () ->
_API.loading q, p
formd = new FormData()
formd.append 'path', d
# TODO: only one file is selected at this time
formd.append 'upload', o[0].files[0]
$.ajax {
url: p,
data: formd,
type: 'POST',
contentType: false,
processData: false,
}
.done (data) ->
_API.loaded q, p, "OK"
c(data)
o.remove()
.fail (e, s) ->
_API.loaded q, p, "FAIL"
f(e, s)
o.remove()
o.click()
saveblob: (name, b) ->
url = window.URL.createObjectURL b
o = ($ '<a>')
.attr("href", url)
.attr("download", name)
.css("display", "none")
.appendTo("body")
o[0].click()
window.URL.revokeObjectURL(url)
o.remove()
systemConfig: ->
_API.request 'config', (result) ->
console.log result
loading: (q, p) ->
_courrier.trigger "loading", { id: q, data: { m: "#{p}", s: true }, name: "OS" }
loaded: (q, p, m ) ->
_courrier.trigger "loaded", { id: q, data: { m: "#{m}: #{p}", s: false }, name: "OS" }
get: (p, c, f, t) ->
conf =
type: 'GET',
url: p,
conf.dataType = t if t
q = _courrier.getMID()
_API.loading q, p
$.ajax conf
.done (data) ->
_API.loaded q, p, "OK"
c(data)
.fail (e, s) ->
_API.loaded q, p, "FAIL"
f(e, s)
script: (p, c, f) ->
q = _courrier.getMID()
_API.loading q, p
$.getScript p
.done (data) ->
_API.loaded q, p, "OK"
c(data)
.fail (e, s) ->
_API.loaded q, p, "FAIL"
f(e, s)
resource: (r, c, f) ->
path = "resources/#{r}"
_API.get path, c, f
libready: (l) ->
return _API.shared[l] || false
require: (l,f) ->
if not _API.shared[l]
if l.match /^(https?:\/\/[^\s]+)/g
_API.script l, () ->
_API.shared[l] = true
_courrier.trigger "sharedlibraryloaded", l
f() if f
, (e, s) ->
_courrier.oserror __("Cannot load 3rd library at: {0}", l), e, r
else
path = "os://scripts/"
js = "#{path}#{l}.js"
js.asFileHandler().onready (d) ->
_API.shared[l] = true
el = $ '<script>', { src: "#{_API.handler.get}/#{js}" }
.appendTo 'head'
#load css file
css = "#{path}#{l}.css"
css.asFileHandler().onready (d) ->
el = $ '<link>', { rel: 'stylesheet', type: 'text/css', 'href': "#{_API.handler.get}/#{css}" }
.appendTo 'head'
, () ->
console.log "loaded", l
_courrier.trigger "sharedlibraryloaded", l
f() if f
else
console.log l, "Library exist, no need to load"
f() if f
_courrier.trigger "sharedlibraryloaded", l
requires:(libs, f) ->
return f() unless libs.length > 0
_courrier.observable.one "sharedlibraryloaded", (l) ->
libs.splice 0, 1
_API.requires libs, f
_API.require libs[0], null
packages:
fetch: (f) ->
_API.handler.packages {
command: "list", args: { paths: (v for k, v of _OS.setting.system.pkgpaths) }
}, f
cache: (f) ->
_API.handler.packages {
command: "cache", args: { paths: (v for k, v of _OS.setting.system.pkgpaths) }
}, f
setting: (f) ->
_API.handler.setting f
apigateway: (d, ws, c) ->
return _API.handler.apigateway d, ws, c
search: (text) ->
r = []
for k, v of _API.searchHandler
ret = _API.searchHandler[k](text)
if ret.length > 0
ret.unshift { text: k, class: "search-header", dataid: "header" }
r = r.concat ret
return r
onsearch: (name, fn) ->
self.OS.API.searchHandler[name] = fn unless self.OS.API.searchHandler[name]
setLocale: (name, f) ->
path = "resources/languages/#{name}.json"
_API.get path, (d) ->
_OS.setting.system.locale = name
_API.lang = d
if f then f() else _courrier.trigger "systemlocalechange", name
, (e, s) ->
#_OS.setting.system.locale = "en_GB"
_courrier.oserror __("Language file {0} not found", path), e, s
f() if f
, "json"
throwe: (n) ->
err = undefined
try
throw new Error(n)
catch e
err = e
return "" if not err
return err
# utilities functioncs
switcher: () ->
o = {}
p = {}
p[arguments[i]] = false for i in [0..arguments.length - 1 ]
Object.defineProperty o, "__p", {
enumerable: false,
value: p
}
fn = (o, v) ->
Object.defineProperty o, v, {
enumerable: true,
set: (value) ->
for k,l of @__p
@__p[k] = false
o.__p[v] = value
, get: () ->
return o.__p[v]
}
for k, v of o.__p
fn o, k
Object.defineProperty o, "selected", {
configurable: true,
enumerable: false,
get: () ->
for k,v of o.__p
return k if v
}
return o

View File

@ -1,152 +0,0 @@
# Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
# 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/.
'use strict'
#define the OS object
self = this
self.OS or=
API: {}
GUI: {}
APP: {}
setting:
user: {}
applications: {}
desktop: {}
appearance: {}
VFS: {}
system: {}
courrier:
observable: riot.observable()
quota: 0
listeners: {}
on: (e, f, a) ->
_courrier.listeners[a.pid] = [] unless _courrier.listeners[a.pid]
_courrier.listeners[a.pid].push { e: e, f: f }
_courrier.observable.on e, f
trigger: (e, d) -> _courrier.observable.trigger e, d
osfail: (m, e, s) ->
_courrier.ostrigger "fail", { m: m, e: e, s: s }
oserror: (m, e, s) ->
_courrier.ostrigger "error", { m: m, e: e, s: s }
osinfo: (m) ->
_courrier.ostrigger "info", { m: m, e: null, s: null }
ostrigger: (e, d) ->
_courrier.trigger e, { id: 0, data: d, name: "OS" }
unregister: (app) ->
return unless _courrier.listeners[app.pid] and _courrier.listeners[app.pid].length > 0
_courrier.observable.off i.e, i.f for i in _courrier.listeners[app.pid]
delete _courrier.listeners[app.pid]
# _courrier.listeners[app.pid]
getMID: () ->
_courrier.quota += 1
_courrier.quota
register: (name, x) ->
if x.type is 3 then self.OS.GUI.subwindows[name] = x else _OS.APP[name] = x
PM:
pidalloc: 0
processes: {}
createProcess: (app, cls, args) ->
f = () ->
#if it is single ton
# and a process is existing
# just return it
if cls.singleton and _PM.processes[app] and _PM.processes[app].length == 1
_PM.processes[app][0].show()
else
_PM.processes[app] = [] if not _PM.processes[app]
obj = new cls(args)
obj.birth = (new Date).getTime()
_PM.pidalloc++
obj.pid = _PM.pidalloc
_PM.processes[app].push obj
if cls.type is 1 then _GUI.dock obj, cls.meta else _GUI.attachservice obj
if cls.type is 2
_courrier.trigger "srvroutineready", app
if cls.dependencies
libs = (v for v in cls.dependencies)
_API.requires libs, f
else
f()
appByPid: (pid) ->
app = undefined
find = (l) ->
return a for a in l when a.pid is pid
for k, v of _PM.processes
app = find v
break if app
app
kill: (app) ->
return if not app.name or not _PM.processes[app.name]
i = _PM.processes[app.name].indexOf app
if i >= 0
if _OS.APP[app.name].type == 1 then _GUI.undock app else _GUI.detachservice app
_courrier.unregister app
delete _PM.processes[app.name][i]
_PM.processes[app.name].splice i, 1
killAll: (app, force) ->
return unless _PM.processes[app]
a.quit(force) for a in _PM.processes[app]
cleanup: ->
console.log "Clean up system"
_PM.killAll a, true for a, v of _PM.processes
_courrier.observable.off("*") if _courrier.observable
$(window).off('keydown')
($ "#workspace").off("mouseover")
delete _courrier.observable
($ "#wrapper").empty()
_GUI.clearTheme()
_courrier.observable = riot.observable()
_courrier.quota = 0
_OS.APP = {}
_OS.setting =
user: {}
applications: {}
desktop: {}
appearance: {}
VFS: {}
system: {}
_PM.processes = {}
_PM.pidalloc = 0
boot: ->
#first login
console.log "Booting sytem"
_API.handler.auth (d) ->
# in case someone call it more than once :)
if d.error
# show login screen
_GUI.login()
else
# startX :)
_GUI.startAntOS d.result
cleanupHandlers: {}
exit: ->
#do clean up first
f() for n, f of _OS.cleanupHandlers
_API.handler.setting (r) ->
_OS.cleanup()
_API.handler.logout()
onexit: (n, f) ->
self.OS.cleanupHandlers[n] = f unless self.OS.cleanupHandlers[n]

1722
src/core/core.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,38 +0,0 @@
# Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
# 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 DB
constructor: (@table) ->
save: (d, f) ->
_API.handler.dbquery "save", { table: @table, data: d }, f
delete: (c, f) ->
rq = { table: @table }
return ( _courrier.oserror __("VDB Unknown condition for delete command"),
(_API.throwe "OS.DB"), c ) unless c and c isnt ""
if isNaN c
rq.cond = c
else
rq.id = c
_API.handler.dbquery "delete", rq, f
get: (id, f) ->
_API.handler.dbquery "get", { table: @table, id: id }, f
find: (cond, f) ->
_API.handler.dbquery "select", { table: @table, cond: cond }, f
self.OS.API.DB = DB

202
src/core/db.ts Normal file
View File

@ -0,0 +1,202 @@
// Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
// 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 {
/**
* Simple Virtual Database (VDB) application API.
*
* This API abstracts and provides a standard way to
* connect to a server-side relational database (e.g. sqlite).
*
* Each user when connected has their own database previously
* created. All VDB operations related to that user will be
* performed on this database.
*
* The creation of user database need to be managed by the server-side API.
* The VDB API assumes that the database already exist. All operations
* is performed in tables level
*
* @export
* @class DB
*/
export class DB {
/**
* A table name on the user's database
*
* @private
* @type {string}
* @memberof DB
*/
private table: string;
/**
*Creates an instance of DB.
* @param {string} table table name
* @memberof DB
*/
constructor(table: string) {
this.table = table;
}
/**
* Save data to the current table. The input
* data must conform to the table record format.
*
* On the server side, if the table doest not
* exist yet, it should be created automatically
* by inferring the data structure of the input
* object
*
* @param {GenericObject<any>} d data object represents a current table record
* @returns {Promise<API.RequestResult>}
* @memberof DB
*/
save(d: GenericObject<any>): Promise<API.RequestResult> {
return new Promise(async (resolve, reject) => {
try {
const r = await API.handle.dbquery("save", {
table: this.table,
data: d,
});
if (r.error) {
return reject(API.throwe(r.error.toString()));
}
return resolve(r);
} catch (e) {
return reject(__e(e));
}
});
}
/**
* delete record(s) from the current table by
* a conditional object
*
* @param {*} c conditional object, c can be:
*
* * a `number`: the operation will delete the record with `id = c`
* * a `string`: The SQL string condition that selects record to delete
* * a conditional object represents a SQL condition statement as an object,
* example: `pid = 10 AND cid = 2` is represented by:
*
* ```typescript
* {
* exp: {
* "and": {
* pid: 10,
* cid: 2
* }
* }
* ```
*
* @returns {Promise<API.RequestResult>}
* @memberof DB
*/
delete(
c: GenericObject<any> | number | string
): Promise<API.RequestResult> {
return new Promise(async (resolve, reject) => {
const rq: any = { table: this.table };
if (!c || c === "") {
reject(API.throwe("OS.DB: unknown condition"));
}
if (isNaN(c as number)) {
rq.cond = c;
} else {
rq.id = c;
}
try {
const r = await API.handle.dbquery("delete", rq);
if (r.error) {
return reject(API.throwe(r.error.toString()));
}
return resolve(r);
} catch (e) {
return reject(__e(e));
}
});
}
/**
* Get a record in the table by its primary key
*
* @param {number} id the primary key value
* @returns {Promise<GenericObject<any>>} Promise on returned record data
* @memberof DB
*/
get(id: number): Promise<GenericObject<any>> {
return new Promise(async (resolve, reject) => {
try {
const r = await API.handle.dbquery("get", {
table: this.table,
id: id,
});
if (r.error) {
return reject(API.throwe(r.error.toString()));
}
return resolve(r.result as GenericObject<any>);
} catch (e) {
return reject(__e(e));
}
});
}
/**
* Find records by a condition
*
* @param {GenericObject<any>} cond conditional object
*
* a conditional object represents a SQL condition statement as an object,
* example: `pid = 10 AND cid = 2 ORDER BY date DESC` is represented by:
*
* ```typescript
* {
* exp: {
* "and": {
* pid: 10,
* cid: 2
* }
* },
* order: {
* date: "DESC"
* }
* }
* ```
* @returns {Promise<GenericObject<any>[]>}
* @memberof DB
*/
find(cond: GenericObject<any>): Promise<GenericObject<any>[]> {
return new Promise(async (resolve, reject) => {
try {
const r = await API.handle.dbquery("select", {
table: this.table,
cond,
});
if (r.error) {
return reject(API.throwe(r.error.toString()));
}
return resolve(r.result as GenericObject<any>[]);
} catch (e) {
return reject(__e(e));
}
});
}
}
}
}

View File

@ -1,491 +0,0 @@
# Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
# 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.GUI =
subwindows: new Object()
dialog: undefined
fullscreen: false
shortcut:
ALT: {}
CTRL: {}
SHIFT: {}
META: {}
SYS_MENU: [
{
text: "",
iconclass: "fa fa-eercast",
dataid: "sys-menu-root",
child: [
{
text: "__(Applications)",
child: [],
dataid: "sys-apps"
iconclass: "fa fa-adn",
onmenuselect: (d) ->
_GUI.launch d.item.data.app
}
],
onmenuselect: (d) ->
return _OS.exit() if d.item.data.dataid is "sys-logout"
return _GUI.toggleFullscreen() if d.item.data.dataid is "os-fullsize"
_GUI.launch d.item.data.app unless d.item.data.dataid
}
]
htmlToScheme: (html, app, parent) ->
scheme = $.parseHTML html
$(app.scheme).remove() if app.scheme
($ parent).append scheme
app.scheme = scheme[0]
riot.mount ($ scheme), { observable: app.observable }
app.main()
app.show()
loadScheme: (path, app, parent) ->
path.asFileHandler().read (x) ->
return null unless x
_GUI.htmlToScheme x, app, parent
#, (e, s) ->
# _courrier.osfail "Cannot load scheme file: #{path} for #{app.name} (#{app.pid})", e, s
clearTheme: () ->
$ "head link#ostheme"
.attr "href", ""
loadTheme: (name, force) ->
_GUI.clearTheme() if force
path = "resources/themes/#{name}/#{name}.css"
$ "head link#ostheme"
.attr "href", path
pushServices: (srvs) ->
return unless srvs.length > 0
_courrier.observable.one "srvroutineready", () ->
srvs.splice 0, 1
_GUI.pushServices srvs
_GUI.pushService srvs[0]
openDialog: (d, f, title, data) ->
if _GUI.dialog
_GUI.dialog.show()
return
if not _GUI.subwindows[d]
ex = _API.throwe "Dialog"
return _courrier.oserror __("Dialog {0} not found", d), ex, null
_GUI.dialog = new _GUI.subwindows[d]()
_GUI.dialog.parent = _GUI
_GUI.dialog.handler = f
_GUI.dialog.pid = -1
_GUI.dialog.data = data
_GUI.dialog.title = title
_GUI.dialog.init()
pushService: (ph) ->
arr = ph.split "/"
srv = arr[1]
app = arr[0]
return _PM.createProcess srv, _OS.APP[srv] if _OS.APP[srv]
_GUI.loadApp app,
(a) ->
return _PM.createProcess srv, _OS.APP[srv] if _OS.APP[srv]
(e, s) ->
_courrier.trigger "srvroutineready", srv
_courrier.osfail __("Cannot read service script: {0}", srv), e, s
appsByMime: (mime) ->
metas = ( v for k, v of _OS.setting.system.packages when v and v.app )
mimes = ( m.mimes for m in metas when m)
apps = []
# search app by mimes
f = ( arr, idx ) ->
try
arr.filter (m, i) ->
if mime.match (new RegExp m, "g")
return false if (apps.indexOf metas[idx]) >= 0
apps.push metas[idx]
return false
return false
catch e
_courrier.osfail __("Error find app by mimes {0}", mime), e, mime
( f m, i if m ) for m, i in mimes
return apps
appsWithServices: () ->
o = {}
o[k] = v for k, v of _OS.setting.system.packages when v and v.services and v.services.length > 0
o
openWith: (it) ->
return unless it
return _GUI.launch it.app if it.type is "app" and it.app
return _courrier.osinfo __("Application {0} is not executable", it.text) if it.type is "app"
apps = _GUI.appsByMime ( if it.type is "dir" then "dir" else it.mime )
return _courrier.osinfo __("No application available to open {0}", it.filename) if apps.length is 0
return _GUI.launch apps[0].app, [it.path] if apps.length is 1
list = ( { text: e.app, icon: e.icon, iconclass: e.iconclass } for e in apps )
_GUI.openDialog "SelectionDialog", ( d ) ->
_GUI.launch d.text, [it.path]
, __("Open with"), list
forceLaunch: (app, args) ->
console.warn "This method is used for developing only, please use the launch method instead"
_GUI.unloadApp app
_GUI.launch app, args
unloadApp: (app) ->
_PM.killAll app, true
($ _OS.APP[app].style).remove() if _OS.APP[app] and _OS.APP[app].style
delete _OS.APP[app]
loadApp: (app, ok, err) ->
path = "os://packages/#{app}"
path = _OS.setting.system.packages[app].path if _OS.setting.system.packages[app].path
js = path + "/main.js"
js.asFileHandler().read (d) ->
# load app meta data
"#{path}/package.json".asFileHandler().read (data) ->
data.path = path
_OS.APP[app].meta = data if _OS.APP[app]
_OS.APP[v].meta = data for v in data.services if data.services
#load css file
css = "#{path}/main.css"
css.asFileHandler().onready (d) ->
stamp = (new Date).timestamp()
el = $ '<link>', { rel: 'stylesheet', type: 'text/css', 'href': "#{_API.handler.get}/#{css}?stamp=#{stamp}" }
.appendTo 'head'
_OS.APP[app].style = el[0] if _OS.APP[app]
ok app
, () ->
#launch
ok app
, "json"
#ok app
, "script"
launch: (app, args) ->
if not _OS.APP[app]
# first load it
_GUI.loadApp app,
(a)->
_PM.createProcess a, _OS.APP[a], args
, (e, s) ->
else
# now launch it
if _OS.APP[app]
_PM.createProcess app, _OS.APP[app], args
dock: (app, meta) ->
# dock an application to a dock
# create a data object
data =
icon: null
iconclass: meta.iconclass || ""
app: app
onbtclick: () -> app.toggle()
# TODO: this path is not good, need to create a blob of it
data.icon = "#{meta.path}/#{meta.icon}" if meta.icon
# TODO: add default app icon class in system setting
# so that it can be themed
data.iconclass = "fa fa-cogs" if (not meta.icon) and (not meta.iconclass)
dock = $ "#sysdock"
app.init()
app.one "rendered", () ->
dock.get(0).newapp data
app.sysdock = dock.get(0)
app.appmenu = ($ "[data-id = 'appmenu']", "#syspanel")[0]
app.subscribe "systemlocalechange", (name) -> app.update()
app.subscribe "appregistry", ( m ) ->
app.applySetting m.data.m if (m.name is app.name)
toggleFullscreen: () ->
el = ($ "body")[0]
if _GUI.fullscreen
return document.exitFullscreen() if document.exitFullscreen
return document.mozCancelFullScreen() if document.mozCancelFullScreen
return document.webkitExitFullscreen() if document.webkitExitFullscreen
return document.cancelFullScreen() if document.cancelFullScreen
else
return el.requestFullscreen() if el.requestFullscreen
return el.mozRequestFullScreen() if el.mozRequestFullScreen
return el.webkitRequestFullscreen() if el.webkitRequestFullscreen
return el.msRequestFullscreen() if el.msRequestFullscreen
undock: (app) ->
($ "#sysdock").get(0).removeapp app
attachservice: (srv) ->
($ "#syspanel")[0].attachservice srv
srv.init()
srv.subscribe "systemlocalechange", (name) -> srv.update()
detachservice: (srv) ->
($ "#syspanel")[0].detachservice srv
bindContextMenu: (event) ->
handler = (e) ->
if e.contextmenuHandler
e.contextmenuHandler event, ($ "#contextmenu")[0]
else
p = $(e).parent().get(0)
handler p if p isnt ($ "#workspace").get(0)
handler event.target
event.preventDefault()
bindKey: (k, f) ->
arr = k.split "-"
return unless arr.length is 2
fnk = arr[0].toUpperCase()
c = arr[1].toUpperCase()
return unless _GUI.shortcut[fnk]
_GUI.shortcut[fnk][c] = f
wallpaper: (obj) ->
if obj
_OS.setting.appearance.wp = obj
wp = _OS.setting.appearance.wp
$("body").css("background-image", "url(#{_API.handler.get}/#{wp.url})" )
.css("background-size", wp.size)
.css("background-repeat", wp.repeat)
showTooltip: (el, text, e) ->
el = el[0]
label = ($ "#systooltip")[0]
$("#workspace").on "mousemove", (ev) ->
if $(ev.target).closest(el).length is 0
$(label).hide()
$("#workspace").off "mousemove"
arr = text.split /:(.+)/
tip = text
tip = arr[1] if arr.length > 1
offset = $(el).offset()
w = $(el).width()
h = $(el).height()
label.set "text", tip
$(label).show()
switch arr[0]
when "cr" # center right of the element
left = offset.left + w + 5
top = offset.top + h / 2 - $(label).height() / 2
$(label).css "top", top + "px"
.css "left", left + "px"
else
return unless e
$(label).css "top", e.clientY + 5 + "px"
.css "left", e.clientX + 5 + "px"
initDM: ->
# check login first
_API.resource "schemes/dm.html", (x) ->
return null unless x
scheme = $.parseHTML x
($ "#wrapper").append scheme
_courrier.observable.one "sysdockloaded", () ->
($ window).bind 'keydown', (event) ->
dock = ($ "#sysdock")[0]
return unless dock
app = dock.get "selectedApp"
#return true unless app
c = String.fromCharCode(event.which).toUpperCase()
fnk = undefined
if event.ctrlKey
fnk = "CTRL"
else if event.metaKey
fnk = "META"
else if event.shiftKey
fnk = "SHIFT"
else if event.altKey
fnk = "ALT"
return unless fnk
r = if app then app.shortcut fnk, c, event else true
return event.preventDefault() if not r
return unless _GUI.shortcut[fnk]
return unless _GUI.shortcut[fnk][c]
_GUI.shortcut[fnk][c](event)
event.preventDefault()
# system menu and dock
riot.mount ($ "#syspanel", $ "#wrapper")
riot.mount ($ "#sysdock", $ "#workspace"), { items: [] }
riot.mount ($ "#systooltip", $ "#wrapper")
# context menu
riot.mount ($ "#contextmenu", $ "#wrapper")
($ "#workspace").contextmenu (e) -> _GUI.bindContextMenu e
# tooltip
($ "#workspace").mouseover (e) ->
el = $(e.target).closest "[tooltip]"
return unless el.length > 0
_GUI.showTooltip el, ($(el).attr "tooltip"), e
# desktop default file manager
desktop = $ "#desktop"
fp = _OS.setting.desktop.path.asFileHandler()
desktop[0].fetch = () ->
fn = () ->
fp = _OS.setting.desktop.path.asFileHandler()
fp.read (d) ->
return _courrier.osfail d.error, (_API.throwe "OS.VFS"), d.error if d.error
items = []
$.each d.result, (i, v) ->
return if v.filename[0] is '.' and not _OS.setting.desktop.showhidden
v.text = v.filename
#v.text = v.text.substring(0,9) + "..." ifv.text.length > 10
v.iconclass = v.type
items.push(v)
desktop[0].set "items", items
desktop[0].refresh()
fp.onready () ->
fn()
, ( e ) -> # try to create the path
console.log "#{fp.path} not found"
name = fp.basename
fp.parent().asFileHandler().mk name, (r) ->
ex = _API.throwe "OS.VFS"
if r.error then _courrier.osfail d.error, ex, d.error else fn()
desktop[0].ready = (e) ->
e.observable = _courrier
window.onresize = () ->
_courrier.trigger "desktopresize"
e.refresh()
desktop[0].set "onlistselect", (d) ->
($ "#sysdock").get(0).set "selectedApp", null
desktop[0].set "onlistdbclick", ( d ) ->
($ "#sysdock").get(0).set "selectedApp", null
it = desktop[0].get "selected"
_GUI.openWith it
#($ "#workingenv").on "click", (e) ->
# desktop[0].set "selected", -1
desktop.on "click", (e) ->
return unless e.target is desktop[0]
desktop[0].set "selected", -1
($ "#sysdock").get(0).set "selectedApp", null
#console.log "desktop clicked"
desktop[0].contextmenuHandler = (e, m) ->
desktop[0].set "selected", -1 if e.target is desktop[0]
($ "#sysdock").get(0).set "selectedApp", null
menu = [
{ text: __("Open"), dataid: "desktop-open" },
{ text: __("Refresh"), dataid: "desktop-refresh" }
]
menu = menu.concat ( v for k, v of _OS.setting.desktop.menu)
m.set "items", menu
m.set "onmenuselect", (evt) ->
switch evt.item.data.dataid
when "desktop-open"
it = desktop[0].get "selected"
return _GUI.openWith it if it
it = _OS.setting.desktop.path.asFileHandler()
it.mime = "dir"
_GUI.openWith it
when "desktop-refresh"
desktop[0].fetch()
else
_GUI.launch evt.item.data.app, evt.item.data.args if evt.item.data.app
m.show(e)
desktop[0].fetch()
_courrier.observable.on "VFS", (d) ->
desktop[0].fetch() if d.data.file.hash() is fp.hash() or d.data.file.parent().hash() is fp.hash()
_courrier.ostrigger "desktoploaded"
# mount it
riot.mount desktop
, (e, s) ->
alert __("System fail: Cannot init desktop manager")
console.log s, e
refreshDesktop: () ->
($ "#desktop")[0].fetch()
refreshSystemMenu: () ->
_GUI.SYS_MENU[0].child.length = 1
_GUI.SYS_MENU[0].child[0].child.length = 0
_GUI.SYS_MENU[0].child[0].child.push v for k, v of _OS.setting.system.packages when (v and v.app)
_GUI.SYS_MENU[0].child.push v for k, v of _OS.setting.system.menu
_GUI.SYS_MENU[0].child.push
text: "__(Toggle Full screen)",
dataid: "os-fullsize",
iconclass: "fa fa-tv"
_GUI.SYS_MENU[0].child.push
text: "__(Log out)",
dataid: "sys-logout",
iconclass: "fa fa-user-times"
($ "[data-id = 'os_menu']", "#syspanel")[0].update()
buildSystemMenu: () ->
($ "[data-id = 'os_menu']", "#syspanel")[0].set "items", _GUI.SYS_MENU
#console.log menu
mkdialog: (conf) ->
return new _GUI.BasicDialog conf.name, conf.layout
login: () ->
_API.resource "schemes/login.html", (x) ->
return null unless x
scheme = $.parseHTML x
($ "#wrapper").append scheme
($ "#btlogin").click () ->
data =
username: ($ "#txtuser").val(),
password: ($ "#txtpass").val()
_API.handler.login data, (d) ->
if d.error then ($ "#login_error").html d.error else _GUI.startAntOS d.result
($ "#txtpass").keyup (e) ->
($ "#btlogin").click() if e.which is 13
, (e, s) ->
alert __("System fail: Cannot init login screen")
startAntOS: (conf) ->
# clean up things
_OS.cleanup()
# get setting from conf
_OS.systemSetting conf
#console.log _OS.setting
# load theme
_GUI.loadTheme _OS.setting.appearance.theme
_GUI.wallpaper()
_courrier.observable.one "syspanelloaded", () ->
# TODO load packages list then build system menu
_courrier.observable.on "systemlocalechange", (name) ->
($ "#syspanel")[0].update()
_API.packages.cache (ret) ->
if ret.result
_API.packages.fetch (r) ->
if r.result
for k, v of r.result
v.text = v.name
v.filename = k
v.type = "app"
v.mime = "antos/app"
v.icon = "#{v.path}/#{v.icon}" if v.icon
v.iconclass = "fa fa-adn" unless v.iconclass or v.icon
_OS.setting.system.packages = if r.result then r.result else
_GUI.refreshSystemMenu()
_GUI.buildSystemMenu()
# push startup services
# TODO: get services list from user setting
_GUI.pushServices (v for v in _OS.setting.system.startup.services)
(_GUI.launch a) for a in _OS.setting.system.startup.apps
#_GUI.launch "DummyApp"
# initDM
_API.setLocale _OS.setting.system.locale, () ->
_GUI.initDM()

1227
src/core/gui.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,125 +0,0 @@
# Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
# 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

View File

@ -1,23 +0,0 @@
# Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
# 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

View File

@ -0,0 +1,465 @@
// Copyright 2017-2020 Xuan Sang LE <xsang.le AT gmail DOT com>
// 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<any>}
* @memberof PackageCommandType
*/
args: GenericObject<any>;
}
/**
*
* 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>
* | any[]
* | FileInfoType
* | FileInfoType[]
* | setting.UserSettingType)}
* @memberof RequestResult
*/
result:
| string
| boolean
| GenericObject<any>
| 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<RequestResult>} A promise on a [[RequestResult]]
* which contains an error or a list of FileInfoType
*/
export function scandir(p: string): Promise<RequestResult> {
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<RequestResult>} A promise on a RequestResult
* which contains an error or true on success
*/
export function mkdir(p: string): Promise<RequestResult> {
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<RequestResult>} A promise on a RequestResult
* which contains an error or true on success
*/
export function sharefile(
p: string,
pub: boolean
): Promise<RequestResult> {
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<RequestResult>} A promise on a [[RequestResult]]
* which contains an error or an object of FileInfoType
*/
export function fileinfo(p: string): Promise<RequestResult> {
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<any>} A promise on a [[RequestResult]]
* which contains an error or an object of [[FileInfoType]]
*/
export function readfile(p: string, t: string): Promise<any> {
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<RequestResult>} A promise on a [[RequestResult]]
* which contains an error or a success response
*/
export function move(s: string, d: string): Promise<RequestResult> {
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<RequestResult>} A promise on a [[RequestResult]]
* which contains an error or a success response
*/
export function remove(p: string): Promise<RequestResult> {
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<ArrayBuffer>} a Promise on an array buffer
*/
export function fileblob(p: string): Promise<ArrayBuffer> {
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<RequestResult>} a promise on a [[RequestResult]]
*/
export function packages(
d: PackageCommandType
): Promise<RequestResult> {
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<RequestResult>} a promise on a [[RequestResult]]
*/
export function upload(d: string): Promise<RequestResult> {
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<RequestResult>} a promise on a [[RequestResult]]
*/
export function write(
p: string,
d: string
): Promise<RequestResult> {
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<any>} 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<any>} a promise on the result object (any)
*/
export function apigateway(
d: GenericObject<any>,
ws: boolean
): Promise<any> {
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<RequestResult>} a promise on a [[RequestResult]] that
* contains an error or a [[UserSettingType]] object
*/
export function auth(): Promise<RequestResult> {
const p = `${API.REST}/user/auth`;
return API.post(p, {});
}
/**
* Perform a login operation
*
* @export
* @param {UserLoginType} d user data [[UserLoginType]]
* @returns {Promise<RequestResult>} a promise on a [[RequestResult]] that
* contains an error or a [[UserSettingType]] object
*/
export function login(d: UserLoginType): Promise<RequestResult> {
const p = `${API.REST}/user/login`;
return API.post(p, d);
}
/**
* Perform a logout operation
*
* @export
* @returns {Promise<RequestResult>} a promise on a [[RequestResult]]
*/
export function logout(): Promise<RequestResult> {
const p = `${API.REST}/user/logout`;
return API.post(p, {});
}
/**
* Save the current user settings
*
* @export
* @returns {Promise<RequestResult>} a promise on a [[RequestResult]]
*/
export function setting(): Promise<RequestResult> {
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<any>} 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<RequestResult>} 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<any>
): Promise<RequestResult> {
const path = `${API.REST}/VDB/${cmd}`;
return API.post(path, d);
}
}
}
}

View File

@ -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 <xsang.le AT gmail DOT com>
// 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));
}
};

4
src/core/languages/gen.sh Normal file → Executable file
View File

@ -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

View File

@ -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}"
}

View File

@ -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"
}

155
src/core/pm.ts Normal file
View File

@ -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 <T extends BaseModel>(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<BaseModel[]> = {};
/**
* 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<ProcessType>} a promise on the created process
*/
export function createProcess(
app: string,
cls: ModelTypeClass,
args?: AppArgumentsType[]
): Promise<ProcessType> {
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<any>) {
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));
}
}
}

View File

@ -1,11 +0,0 @@
<afx-app-window data-id = 'about-window' width='300' height='200'>
<afx-vbox>
<div style="text-align:center; margin-top:10px;" data-height="50">
<h3 style = "margin:0;padding:0;">
<afx-label data-id = 'mylabel'></afx-label>
</h3>
<i><p style = "margin:0; padding:0" data-id = 'mydesc'></p></i>
</div>
<afx-grid-view data-id = 'mygrid'></afx-grid-view>
</afx-vbox>
</afx-app-window>

View File

@ -1,10 +0,0 @@
<afx-sys-panel id = "syspanel"></afx-sys-panel>
<!--div class = "afx-clear"></div-->
<div id = "workspace">
<afx-apps-dock id="sysdock"></afx-apps-dock>
<afx-float-list id = "desktop" ></afx-float-list>
<!--div id = "workingenv"></div-->
</div>
<afx-menu id="contextmenu" data-id="contextmenu" context="true" style="display:none;"></afx-menu>
<afx-label id="systooltip" data-id="systooltip" style="display:none;position:absolute;"></afx-label>

View File

@ -1,13 +0,0 @@
<afx-app-window data-id = 'file-dialog-window' width='400' height='300'>
<afx-hbox>
<afx-list-view data-id = "location" dropdown = "false" data-width = "120"></afx-list-view>
<afx-vbox>
<afx-file-view data-id = "fileview" view='tree' status = false></afx-file-view>
<input data-height = '26' type = "text" data-id = "filename" style="margin-left:5px; margin-right:5px;display:none;" />
<div data-height = '30' style=' text-align:right;padding:3px;'>
<afx-button data-id = "bt-ok" text = "__(Ok)"></afx-button>
<afx-button data-id = "bt-cancel" text = "__(Cancel)"></afx-button>
</div>
</afx-vbox>
</afx-hbox>
</afx-app-window>

View File

@ -1,78 +0,0 @@
<!--
Since nothing is loaded, the login form is a standalone element,
it deserves it own unique style
For now, it can not be themed !!!
-->
<style>
body, html{
background-color: rgba(215,215,215,0.7);
}
#login_form{
width:300px;
height: 200px;
display: block;
border:1px solid #a6a6a6;
border-radius: 6px;
box-shadow: 1px 1px 1px #9f9F9F;
position: absolute;
margin: auto;
top:0;
right: 0;
bottom: 0;
left: 0;
font-family:Verdana, Geneva, Tahoma, sans-serif;
font-size: 13px;
text-align: center;
background-color: white;
color: #414339;
}
#login_form p{
display: inline-block;
background-color:#dfdfdf;
border-bottom: 1px solid #a6a6a6;
padding:10px;
width: calc(100% - 20px);
border-top-left-radius: 6px;
border-top-right-radius: 6px;
font-weight: bold;
margin:0;
}
#login_form input {
width: 250px;
outline: none;
margin-top:10px;
border-radius: 6px;
box-sizing: border-box;
font-size: 13px;
padding: 5px;
border: 1px solid #a6a6a6;
background-color: white;
color: #414339;
}
#login_form button{
margin-top:10px;
width: 250px;
height: 30px;
background-color: #2786F3;
color: white;
border: 1px solid #dedede;
border-radius: 6px;
font-family: Verdana, Geneva, Tahoma, sans-serif;
font-size: 13px;
padding:5px;
}
#login_error{
padding:3px;
color:red;
font-weight: normal;
}
</style>
<div id = "login_form">
<p>Welcome to AntOS, please identify</p>
<input id = "txtuser" type = "text" value = "demo" />
<input id = "txtpass" type = "password" value = "demo" />
<button id = "btlogin">Login</button>
<span id = "login_error"></span>
</div>

View File

@ -1,91 +0,0 @@
# Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
# 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

569
src/core/settings.ts Normal file
View File

@ -0,0 +1,569 @@
// Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
// 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<any> = {};
/**
* 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;
});
}

245
src/core/tags/AppDockTag.ts Normal file
View File

@ -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<any>}
* @memberof AppDockTag
*/
private _onappselect: TagEventCallback<any>;
/**
* 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 = $("<afx-button>");
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);
}
}
}

198
src/core/tags/ButtonTag.ts Normal file
View File

@ -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<JQuery.MouseEventBase>}
* @memberof ButtonTag
*/
private _onbtclick: TagEventCallback<JQuery.MouseEventBase>;
/**
* Custom user data
*
* @type {GenericObject<any>}
* @memberof ButtonTag
*/
data: GenericObject<any>;
/**
*Creates an instance of ButtonTag.
* @memberof ButtonTag
*/
constructor() {
super();
}
/**
* Set the click callback handle for the target button
*
* @memberof ButtonTag
*/
set onbtclick(v: TagEventCallback<JQuery.MouseEventBase>) {
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<JQuery.MouseEventBase> = {
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);
}
}
}

View File

@ -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<Date>}
* @memberof CalendarTag
*/
private _ondateselect: TagEventCallback<Date>;
/**
*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<Date>) {
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<TagEventDataType<tag.GridCellPrototype>>
): 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);
}
}
}

View File

@ -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<ColorType>}
* @memberof ColorPickerTag
*/
private _oncolorselect: TagEventCallback<ColorType>;
/**
* 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<ColorType>) {
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);
}
}
}

View File

@ -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<API.FileInfoType>}
* @memberof FileViewTag
*/
private _onfileselect: TagEventCallback<API.FileInfoType>;
/**
* placeholder for file open event callback
*
* @private
* @type {TagEventCallback<API.FileInfoType>}
* @memberof FileViewTag
*/
private _onfileopen: TagEventCallback<API.FileInfoType>;
/**
* 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<string | number>[])}
* @memberof FileViewTag
*/
private _header: GenericObject<string | number>[];
/**
* placeholder for the user-specified meta-data fetch function
*
* @private
* @memberof FileViewTag
*/
private _fetch: (p: string) => Promise<API.FileInfoType[]>;
/**
*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<API.FileInfoType[]>) {
this._fetch = v;
}
/**
* set the callback handle for the file select event.
* The parameter of the callback should be an object
* of type [[TagEventType]]<T> with the data type `T` is [[API.FileInfoType]]
*
* @memberof FileViewTag
*/
set onfileselect(e: TagEventCallback<API.FileInfoType>) {
this._onfileselect = e;
}
/**
set the callback handle for the file open event.
* The parameter of the callback should be an object
* of type [[TagEventType]]<T> with the data type `T` is [[API.FileInfoType]]
*
* @memberof FileViewTag
*/
set onfileopen(e: TagEventCallback<API.FileInfoType>) {
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<TreeViewTag | ListViewItemTag>
>
) {
(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);
}
}
}

View File

@ -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<any>[]) {}
/**
* 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<any>} v an element data
* @returns
* @memberof FloatListTag
*/
push(v: GenericObject<any>) {
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);
}
}
}

View File

@ -0,0 +1,896 @@
/**
* Extend the Array interface with some
* property needed by AFX API
*
* @interface Array
* @template T
*/
interface Array<T> {
/**
* 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<any>}
* @memberof Array
*/
domel?: GenericObject<any>;
}
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<any>[]}
* @memberof GridRowTag
*/
data: GenericObject<any>[];
/**
*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<GridCellPrototype>;
/**
* 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<CellEventData>}
* @memberof GridCellPrototype
*/
private _oncellselect: TagEventCallback<CellEventData>;
/**
* placeholder for cell double click event callback
*
* @private
* @type {TagEventCallback<CellEventData>}
* @memberof GridCellPrototype
*/
private _oncelldbclick: TagEventCallback<CellEventData>;
/**
* Data placeholder of the current cell
*
* @private
* @type {GenericObject<any>}
* @memberof GridCellPrototype
*/
private _data: GenericObject<any>;
/**
*Creates an instance of GridCellPrototype.
* @memberof GridCellPrototype
*/
constructor() {
super();
}
/**
* Set the cell selected event callback
*
* @memberof GridCellPrototype
*/
set oncellselect(v: TagEventCallback<CellEventData>) {
this._oncellselect = v;
}
/**
* Set the cell double click event callback
*
* @memberof GridCellPrototype
*/
set oncelldbclick(v: TagEventCallback<CellEventData>) {
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<any>) {
if (!v) return;
this._data = v;
this.ondatachange();
if (!v.selected) {
return;
}
this.selected = v.selected;
}
get data(): GenericObject<any> {
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<GridCellPrototype>
): 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<GridCellPrototype>} e
* @param {boolean} flag
* @returns {void}
* @memberof GridCellPrototype
*/
private cellselect(
e: TagEventType<GridCellPrototype>,
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<any>[]}
* @memberof GridViewTag
*/
private _header: GenericObject<any>[];
/**
* Grid rows data placeholder
*
* @private
* @type {GenericObject<any>[][]}
* @memberof GridViewTag
*/
private _rows: GenericObject<any>[][];
/**
* 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<CellEventData>}
* @memberof GridViewTag
*/
private _oncellselect: TagEventCallback<CellEventData>;
/**
* Row select event callback placeholder
*
* @private
* @type {TagEventCallback<CellEventData>}
* @memberof GridViewTag
*/
private _onrowselect: TagEventCallback<CellEventData>;
/**
* Cell double click event callback placeholder
*
* @private
* @type {TagEventCallback<CellEventData>}
* @memberof GridViewTag
*/
private _oncelldbclick: TagEventCallback<CellEventData>;
/**
* 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<CellEventData>
): 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<CellEventData>) {
this._oncellselect = v;
}
/**
* set the row select event callback
*
* @memberof GridViewTag
*/
set onrowselect(v: TagEventCallback<CellEventData>) {
this._onrowselect = v;
}
/**
* set the cell double click event callback
*
* @memberof GridViewTag
*/
set oncelldbclick(v: TagEventCallback<CellEventData>) {
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<any>[]}
* @memberof GridViewTag
*/
get header(): GenericObject<any>[] {
return this._header;
}
set header(v: GenericObject<any>[]) {
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<any>[][]) {
$(this.refs.grid).empty();
this._rows = rows;
rows.map((row) => this.push(row, false));
}
get rows(): GenericObject<any>[][] {
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<any>[]} 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<any>[], flag: boolean): void {
const rowel = $("<afx-grid-row>").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<any>[]} row list of cell data in the row
* @memberof GridViewTag
*/
unshift(row: GenericObject<any>[]): void {
this.push(row, true);
}
/**
* This function triggers the cell select event
*
* @private
* @param {TagEventType<CellEventData>} e event contains cell event data
* @param {boolean} flag indicates whether the event is double clicked
* @returns {void}
* @memberof GridViewTag
*/
private cellselect(
e: TagEventType<CellEventData>,
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<CellEventData>} e
* @returns {void}
* @memberof GridViewTag
*/
private rowselect(e: TagEventType<CellEventData>): 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);
}
}
}

149
src/core/tags/LabelTag.ts Normal file
View File

@ -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);
}
}
}

1220
src/core/tags/ListViewTag.ts Normal file

File diff suppressed because it is too large Load Diff

826
src/core/tags/MenuTag.ts Normal file
View File

@ -0,0 +1,826 @@
namespace OS {
export namespace GUI {
export namespace tag {
/**
* Menu event data interface definition
*/
export type MenuEventData = TagEventDataType<MenuEntryTag>;
/**
* 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<any>}
* @memberof MenuEntryTag
*/
private _data: GenericObject<any>;
/**
* placeholder of `menu entry select` event handle
*
* @private
* @type {TagEventCallback<MenuEventData>}
* @memberof MenuEntryTag
*/
private _onmenuselect: TagEventCallback<MenuEventData>;
/**
* placeholder of `sub-menu entry select event` handle
*
* @private
* @type {TagEventCallback<MenuEventData>}
* @memberof MenuEntryTag
*/
private _onchildselect: TagEventCallback<MenuEventData>;
/**
* 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<MenuEventData>
): 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<MenuEventData>) {
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<MenuEventData>) {
this._onchildselect = v;
}
get onchildselect(): TagEventCallback<MenuEventData> {
return this._onchildselect;
}
/**
* Setter: Set data to the entry
*
* Getter: Get data of the current menu entry
*
* @memberof MenuEntryTag
*/
set data(data: GenericObject<any>) {
this._data = data;
this.set(data);
}
get data(): GenericObject<any> {
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<any>[]) {
$(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<any>[] {
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<MenuEventData>}
* @memberof MenuTag
*/
private _onmenuselect: TagEventCallback<MenuEventData>;
/**
* Menu data placeholder
*
* @private
* @type {GenericObject<any>[]}
* @memberof MenuTag
*/
private _items: GenericObject<any>[];
/**
*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<MenuEventData>
): 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<any>[]) {
this._items = data;
$(this.refs.container).empty();
data.map((item) => this.push(item, false));
}
get items(): GenericObject<any>[] {
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<MenuEventData>) {
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<MenuEventData> {
return this.handleselect;
}
/**
* This function triggers the menu select event
*
* @private
* @param {TagEventType} e
* @memberof MenuTag
*/
private handleselect(e: TagEventType<MenuEventData>): 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<any>} item menu entry data
* @memberof MenuTag
*/
unshift(item: GenericObject<any>): 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<any>} 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<any>, 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);
}
}
}

View File

@ -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<number>}
* @memberof NSpinnerTag
*/
private _onchange: TagEventCallback<number>;
/**
* 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<number>) {
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);
}
}
}

159
src/core/tags/OverlayTag.ts Normal file
View File

@ -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);
}
}
}

239
src/core/tags/ResizerTag.ts Normal file
View File

@ -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
* <afx-hbox>
* <afx-vbox>...</afx-vbox>
* <afx-resizer></afx-resizer>
* <afx-vbox>...</afx-vbox>
* </afx-hbox>
* ```
*
* @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);
}
}
}

275
src/core/tags/SliderTag.ts Normal file
View File

@ -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<number>}
* @memberof SliderTag
*/
private _onchange: TagEventCallback<number>;
/**
* Placeholder of the on changing event handle
*
* @private
* @type {TagEventCallback<number>}
* @memberof SliderTag
*/
private _onchanging: TagEventCallback<number>;
/**
* 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<number>) {
this._onchange = f;
}
/**
* Set value changing event handle.
* This handle is triggered when moving the
* slider indicator
*
* @memberof SliderTag
*/
set onvaluechanging(f: TagEventCallback<number>) {
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);
}
}
}

144
src/core/tags/SwitchTag.ts Normal file
View File

@ -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<boolean>}
* @memberof SwitchTag
*/
private _onchange: TagEventCallback<boolean>;
/**
* 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<boolean>) {
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);
}
}
}

View File

@ -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<string | FormattedString>)}
* @memberof SystemPanelTag
*/
private _osmenu: GenericObject<string | FormattedString>;
/**
* 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);
}
}
}

204
src/core/tags/TabBarTag.ts Normal file
View File

@ -0,0 +1,204 @@
namespace OS {
export namespace GUI {
export namespace tag {
/**
* Tag event data type definition
*/
type TabEventData = TagEventDataType<ListViewItemTag>;
/**
* 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<TabEventData>) => boolean;
/**
* Placeholder of tab select event handle
*
* @private
* @type {TagEventCallback<TabEventData>}
* @memberof TabBarTag
*/
private _ontabselect: TagEventCallback<TabEventData>;
/**
*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<any>} item tab data
* @memberof TabBarTag
*/
push(item: GenericObject<any>): 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<any>} item tab data
* @memberof TabBarTag
*/
unshift(item: GenericObject<any>): 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<any>[]) {
for (let i of v) {
i.closable = this.closable;
}
(this.refs.list as ListViewTag).data = v;
}
get items(): GenericObject<any>[] {
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<TabEventData>) => boolean) {
this._ontabclose = v;
}
/**
* Set the tab select event handle
*
* @memberof TabBarTag
*/
set ontabselect(v: TagEventCallback<TabEventData>) {
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);
}
}
}

View File

@ -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<TabContainerTabType>}
* @memberof TabContainerTag
*/
private _ontabselect: TagEventCallback<TabContainerTabType>;
/**
*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<TabContainerTabType>) {
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<any>;
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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<TreeItemEventData>}
* @memberof TreeViewItemPrototype
*/
private _evt: TagEventType<TreeItemEventData>;
/**
* 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<TreeViewDataType[]>;
/**
*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 = $("<afx-tree-view>").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<TreeItemEventData>}
* @memberof TreeViewTag
*/
private _ontreeselect: TagEventCallback<TreeItemEventData>;
/**
* Place holder for tree double click event handle
*
* @private
* @type {TagEventCallback<TreeItemEventData>}
* @memberof TreeViewTag
*/
private _ontreedbclick: TagEventCallback<TreeItemEventData>;
/**
* Placeholder for drag and drop event handle
*
* @private
* @type {TagEventCallback<DnDEventDataType<TreeViewTag>>}
* @memberof TreeViewTag
*/
private _ondragndrop: TagEventCallback<
DnDEventDataType<TreeViewTag>
>;
/**
* 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<TreeViewDataType[]>;
/**
*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<TreeItemEventData>) {
this._ontreeselect = v;
}
/**
* Set the tree double click event handle
*
* @memberof TreeViewTag
*/
set ontreedbclick(v: TagEventCallback<TreeItemEventData>) {
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<TreeItemEventData>): 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<DnDEventDataType<TreeViewTag>>
) {
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);
}
}
}

532
src/core/tags/WindowTag.ts Normal file
View File

@ -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<any>}
* @memberof WindowTag
*/
private _history: GenericObject<any>;
/**
* This placeholder stores the offset of the virtual desktop element
*
* @private
* @type {GenericObject<any>}
* @memberof WindowTag
*/
private _desktop_pos: GenericObject<any>;
/**
* 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<any>} o format: `{ w: window_width, h: window_height }`
* @returns {void}
* @memberof WindowTag
*/
private setsize(o: GenericObject<any>): 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);
}
}
}

View File

@ -1,246 +0,0 @@
<afx-app-window ref = "window" >
<div class = "afx-window-wrapper">
<ul class= "afx-window-top" >
<li class = "afx-window-close" onclick = {close}></li>
<li if = {minimizable == true} class = "afx-window-minimize" onclick = {minimize}></li>
<li if = {resizable == true} class = "afx-window-maximize" onclick={maximize}></li>
<li ref = "dragger" class = "afx-window-title">{ apptitle?apptitle.__():apptitle }</li>
</ul>
<div class = "afx-clear"></div>
<div ref = "content" class = "afx-window-content">
<yield/>
</div>
<div if = {resizable == true} ref = "grip" class = "afx-window-grip">
</div>
<script>
this.apptitle = opts.apptitle || ""
if(opts.minimizable == undefined)
this.minimizable = true
else
this.minimizable = eval(opts.minimizable)
if(opts.resizable == undefined)
this.resizable = true
else
this.resizable = eval(opts.resizable)
var self = this
var offset = {top:0,left:0}
var desktop_pos = $("#desktop").offset()
var isMaxi = false
var history = {}
var width = opts.width || 400
var height = opts.height || 300
this.root.observable = opts.observable || riot.observable()
if(!window._zindex) window._zindex = 10
this.shown = false
self.root.contextmenuHandler = function (e) {}
self.root.set = function(k,v)
{
if(k == "*")
for(var i in v)
self[i] = v[i]
else
self[k] = v
if(k == "apptitle")
self.root.observable.trigger("apptitlechange")
self.update()
}
self.root.get = function(k)
{
return self[k]
}
self.root.update = function()
{
self.update()
}
minimize()
{
this.root.observable.trigger("hide")
}
close()
{
this.root.observable.trigger("exit")
}
this.on('mount', function() {
var left,top
//left = 20 + Math.floor(Math.random() * ($("#desktop").width() - width))
//top = 20 + Math.floor(Math.random() * ($("#desktop").height() - height))
left = ($("#desktop").width() - width)/2
top = ($("#desktop").height() - height)/2
$(self.refs.window)
.css("position",'absolute')
.css("left",left + "px")
.css("top",top + "px")
.css("width",width + "px")
.css("height", height + "px")
.css("z-index",window._zindex++)
$(self.refs.window).on("mousedown", function(e){
if(self.shown == false)
self.root.observable.trigger("focus")
})
$(self.refs.window).click(function(e) {
//e.stopPropagation()
//e.windowactive = true
//self.root.observable.trigger("windowselect")
})
enable_dragging()
if(self.resizable)
enable_resize()
$(self.refs.dragger).dblclick(function(e){
toggle_window()
})
$(self.refs.content).children().each(function(e){
this.observable = self.root.observable
})
var fn = function()
{
var ch = $(self.refs.content).height()/ $(self.refs.content).children().length
$(self.refs.content).children().each(function(e){
$(this).css("height",ch+"px")
})
}
fn()
self.root.observable.on("resize", function(){ fn()})
self.root.observable.on("focus",function(){
window._zindex++
$(self.refs.window)
.show()
.css("z-index",window._zindex)
.removeClass("unactive")
self.shown = true
})
self.root.observable.on("blur", function(){
self.shown = false
$(self.refs.window)
.addClass("unactive")
// add css to blur app :)
})
self.root.observable.on("hide", function()
{
$(self.refs.window).hide()
self.shown = false
})
self.root.observable.on("toggle", function(){
if(self.shown)
self.root.observable.trigger("hide")
else
self.root.observable.trigger("focus")
})
self.root.observable.trigger("rendered", self.root)
})
var enable_dragging = function()
{
$(self.refs.dragger)
.css("user-select","none")
.css("cursor","default")
$(self.refs.dragger).on("mousedown", function(e){
e.preventDefault()
offset = $(self.refs.window).offset()
offset.top = e.clientY - offset.top
offset.left = e.clientX - offset.left
$(window).on("mousemove", function(e){
var top,left
if(isMaxi)
{
toggle_window()
top = 0
letf = e.clientX - $(self.refs.window).width()/2
offset.top = 10 //center
offset.left = $(self.refs.window).width()/2
} else
{
top = e.clientY - offset.top - desktop_pos.top
left = e.clientX - desktop_pos.top - offset.left
left = left < 0?0:left;
top = top < 0?0:top;
}
$(self.refs.window).css("top", top +"px")
.css("left",left + "px")
})
$(window).on("mouseup", function(e){
//console.log("unbind mouse up")
$(window).unbind("mousemove", null)
})
})
}
var enable_resize = function()
{
if(!self.resizable) return
$(self.refs.grip)
.css("user-select","none")
.css("cursor","default")
.css("position","absolute")
.css("bottom","0")
.css("right","0")
.css("cursor","nwse-resize")
$(self.refs.grip).on("mousedown", function(e){
e.preventDefault()
offset.top = e.clientY
offset.left = e.clientX
$(window).on("mousemove", function(e){
var w,h
w = $(self.refs.window).width() + e.clientX - offset.left
h = $(self.refs.window).height() + e.clientY - offset.top
w = w < 100 ? 100:w
h = h < 100 ?100:h
offset.top = e.clientY
offset.left = e.clientX
$(self.refs.window)
.css("width", w +"px")
.css("height",h + "px")
isMaxi = false
self.root.observable.trigger('resize',
{id:$(self.root).attr("data-id"),w:w,h:h})
})
$(window).on("mouseup", function(e){
$(window).unbind("mousemove", null)
})
})
}
var toggle_window = function()
{
if(!self.resizable) return
if(isMaxi == false)
{
history = {
top: $(self.refs.window).css("top"),
left:$(self.refs.window).css("left"),
width:$(self.refs.window).css("width"),
height:$(self.refs.window).css("height")
}
var w,h
w = ($("#desktop").width() - 5)
h = ($("#desktop").height() - 10)
$(self.refs.window)
.css("width", w + "px")
.css("height", h + "px")
.css("top","0").css("left","0")
self.root.observable.trigger('resize',
{id:$(self.root).attr("data-id"),w:w,h:h})
isMaxi = true
}
else
{
isMaxi = false
$(self.refs.window)
.css("width",history.width)
.css("height",history.height)
.css("top",history.top).css("left",history.left)
self.root.observable.trigger('resize',
{id:$(self.root).attr("data-id"),w:history.width,h:history.height} )
}
}
maximize()
{
toggle_window()
}
</script>
</afx-app-window>

View File

@ -1,82 +0,0 @@
<afx-apps-dock>
<afx-button class = {selected: parent.selectedApp && it.app.pid == parent.selectedApp.pid} each={ it,i in items} iconclass = {it.iconclass} icon = {it.icon} appindex = {i} text = {it.text} onbtclick = {it.onbtclick} tooltip= {"cr:" + it.app.title()} >
</afx-button>
<script>
this.items = opts.items || []
var self = this
self.selectedApp = null
self.root.set = function(k,v)
{
if(k == "*")
for(var i in v)
self[i] = v[i]
else
{
self[k] = v
if(k == "selectedApp")
{
for(var i in self.items)
self.items[i].app.blur()
if(v)
$("#desktop")[0].set("selected", -1)
}
}
self.update()
}
self.root.newapp = function(i)
{
self.items.push(i)
self.selectedApp = i.app
self.update()
for(var i in self.items)
self.items[i].app.blur()
}
self.root.removeapp = function(a)
{
var i = -1;
for(var k in self.items)
if(self.items[k].app.pid == a.pid)
{
i = k; break;
}
if(i != -1)
{
delete self.items[i].app
self.items.splice(i,1)
self.update()
}
}
self.root.update = function()
{
self.update()
}
self.root.get = function(k)
{
return self[k]
}
this.on("mount", function(){
window.OS.courrier.trigger("sysdockloaded")
})
self.root.contextmenuHandler = function(e, m)
{
if(e.target == self.root) return;
var appidx = $(e.target).closest( "afx-button" ).attr("appindex")
var app = self.items[appidx].app
m.set("items", [
{ text: "__(Show)", dataid:"show" },
{ text: "__(Hide)", dataid:"hide" },
{ text: "__(Close)", dataid:"quit" }
])
m.set("onmenuselect", function(evt)
{
if(app[evt.item.data.dataid])
app[evt.item.data.dataid]()
})
m.show(e)
}
</script>
</afx-apps-dock>

View File

@ -1,47 +0,0 @@
<afx-button >
<button class= { btactive: selected } disabled={ enable == false } onclick="{ _onbtclick }" ref = "mybtn" >
<afx-label color = {color} icon={icon} iconclass = {iconclass} text = {text} ></afx-label>
</button>
<script>
opts.enable = opts.enable || "true"
this.enable = eval(opts.enable) || false
this.icon = opts.icon
this.iconclass = opts.iconclass
this.color = opts.color
this.text = opts.text || ""
this.selected = eval(opts.selected) || false
this.toggle = eval(opts.toggle) || false
var self = this
this.onbtclick = opts.onbtclick
self.root.set = function(k,v)
{
if(k == "*")
for(var i in v)
self[i] = v[i]
else
self[k] = v
self.update()
}
self.root.trigger = function()
{
$(self.refs.mybtn).trigger("click")
}
self.root.get = function(k)
{
return self[k]
}
this._onbtclick = function(e)
{
if(typeof self.onbtclick == 'string')
eval(self.onbtclick)
else if(self.onbtclick)
self.onbtclick(e)
if(self.root.observable)
{
self.root.observable.trigger("btclick",{id:$(self.root).attr("data-id"),data:self.root})
}
if(self.toggle)
self.root.set("selected",!self.selected)
}
</script>
</afx-button>

View File

@ -1,107 +0,0 @@
<afx-calendar-view>
<div><i class ="prevmonth" onclick={prevmonth}></i>
<afx-label text = {mtext}></afx-label>
<afx-label text = {year}></afx-label>
<i onclick={nextmonth} class="nextmonth"></i></div>
<afx-grid-view data-id ={"grid_" + rid} style = "height:100%;" ref = "grid" header = {header}> </afx-grid-view>
<script >
this.header = [{value:"__(Sun)"},{value:"__(Mon)"},{value:"__(Tue)"},{value:"__(Wed)"},{value:"__(Thu)"},{value:"__(Fri)"},{value:"__(Sat)"}]
this.root.observable = opts.observable
var self = this
this.day = 0
this.month = 0
this.year = 0
this.ondayselect = opts.ondayselect
this.rid = $(self.root).attr("data-id") || Math.floor(Math.random() * 100000) + 1
this.selectedDate = undefined
self.root.get = function(k)
{
return self[k]
}
this.on("mount", function (e) {
self.refs.grid.root.observable = self.root.observable
calendar(null)
self.root.observable.on("gridcellselect", function(d){
if(d.id != "grid_" + self.rid) return
if(d.data.value == "") return
var data = {id:self.rid, data:new Date(self.year, self.month,d.data.value)};
if(self.ondayselect)
self.ondayselect(data)
self.selectedDate = data.data
self.root.observable.trigger("dayselect",data)
})
})
prevmonth()
{
self.selectedDate = undefined
this.month--
if(this.month < 0)
{
this.month = 11
this.year--
}
calendar(new Date(this.year, this.month,1))
}
nextmonth()
{
self.selectedDate = undefined
this.month++
if(this.month > 11)
{
this.month = 0
this.year++
}
calendar(new Date(this.year, this.month,1))
}
var calendar = function (date) {
if (date === null)
date = new Date()
self.day = date.getDate()
self.month = date.getMonth()
self.year = date.getFullYear()
var now ={ d:(new Date()).getDate(), m:(new Date()).getMonth(), y:(new Date()).getFullYear()}
months = ["__(January)", "__(February)", "__(March)", "__(April)", "__(May)", "__(June)", "__(July)", "__(August)", "__(September)", "__(October)", "__(November)", "__(December)"]
this_month = new Date(self.year, self.month, 1)
next_month = new Date(self.year, self.month + 1, 1)
// Find out when this month starts and ends.
first_week_day = this_month.getDay()
days_in_this_month = Math.round((next_month.getTime() - this_month.getTime()) / (1000 * 60 * 60 * 24))
self.mtext = months[self.month]
var rows = []
var row = []
// Fill the first week of the month with the appropriate number of blanks.
for (week_day = 0; week_day < first_week_day; week_day++)
row.push({value:""})
week_day = first_week_day;
for (day_counter = 1; day_counter <= days_in_this_month; 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 && self.month == now.m && self.year == now.y)
row.push({value:day_counter, selected:true})
else
row.push({value:day_counter})
week_day++;
}
for(var i = 0; i <= 7 - row.length;i++)
row.push({value:""})
rows.push(row)
self.refs.grid.root.set("rows",rows)
}
</script>
</afx-calendar-view>

View File

@ -1,110 +0,0 @@
<afx-color-picker>
<div style = "width:310px; height:190px;display:block; padding:3px;">
<canvas class = "color-palette" width="284" height="155" style ="float:left;" ref = "palette" ></canvas>
<div class = "color-sample" style= "width:15px; height:155px; text-align:center; margin-left:3px; display:block;float:left;" ref = "colorval"></div>
<div class = "afx-clear"></div>
<div style ="margin-top:3px;">
<span>Hex:</span><input type = "text" ref = "hextext" style = "width:70px; margin-left:3px;margin-right:5px;"></input>
<span ref = 'rgbtext'></span>
</div>
</div>
<script>
var self = this
var colorctx = undefined
self.root.observable = opts.observable
self.oncolorsetect = opts.oncolorsetect
self.selectedColor = undefined
self.root.set = function(k,v)
{
if(k == "*")
for(var i in v)
self[i] = v[i]
else
self[k] = v
self.update()
}
self.root.get = function(k)
{
return self[k]
}
var build_palette = function()
{
colorctx = $(self.refs.palette).get(0).getContext('2d')
var gradient = colorctx.createLinearGradient(0,0,$(self.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, $(self.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)
//$(self.refs.palette).css("position", "absolute")
// now add mouse move event
var getHex = function(c)
{
s = c.toString(16)
if(s.length == 1) s = "0" + s
return s
}
var pick_color = function(e)
{
$(self.refs.palette).css("cursor","crosshair")
var offset = $(self.refs.palette).offset()
var x = e.pageX - offset.left
var y = e.pageY - offset.top
var color = colorctx.getImageData(x,y, 1, 1)
var data = {
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
}
var mouse_move_h = function(e)
{
var data = pick_color(e)
$(self.refs.colorval).css("background-color", data.text)
}
$(self.refs.palette).mouseenter(function(e){
$(self.refs.palette).on("mousemove",mouse_move_h)
})
$(self.refs.palette).mouseout(function(e){
$(self.refs.palette).unbind("mousemove",mouse_move_h)
if(self.selectedColor)
$(self.refs.colorval).css("background-color", self.selectedColor.text)
})
$(self.refs.palette).on("click", function(e){
data = pick_color(e)
$(self.refs.rgbtext).html(data.text)
$(self.refs.hextext).val(data.hex)
self.selectedColor = data
if(self.oncolorsetect)
self.oncolorsetect(data)
if(! self.root.observable) return
self.root.observable.trigger("colorselect",data)
})
}
this.on("mount", function(){
build_palette()
})
</script>
</afx-color-picker>

View File

@ -1,10 +0,0 @@
<afx-dummy>
<yield/>
<script>
var self = this
self.root.update = function()
{
self.update()
}
</script>
</afx-dummy>

View File

@ -1,226 +0,0 @@
<afx-file-view>
<afx-list-view ref="listview" observable = {root.observable}></afx-list-view>
<afx-grid-view ref = "gridview" header = {header} observable = {root.observable}></afx-grid-view>
<div class = "treecontainer" ref="treecontainer">
<afx-tree-view ref = "treeview" observable = {root.observable}></afx-tree-view>
</div>
<afx-label if = {status == true} class = "status" ref = "stbar"></afx-label>
<script>
var self = this
self.root.observable = opts.observable || riot.observable()
self.view = opts.view || 'list'
self.data = opts.data || []
self.path = opts.path || "home:///"
self.onfileselect
self.onfileopen
this.status = opts.status == undefined?true:opts.status
this.selectedFile = undefined
this.showhidden = opts.showhidden
this.preventUpdate = false
this.fetch = opts.fetch
this.chdir = opts.chdir
this.rid = $(self.root).attr("data-id") || Math.floor(Math.random() * 100000) + 1
this.header = [{value:"__(File name)"},{value: "__(Type)", width:150}, {value: "__(Size)", width:70}]
self.root.set = function(k,v)
{
if(k == "*")
for(var i in v)
self[i] = v[i]
else
self[k] = v
if(k == 'view')
switchView()
if(k == "data")
self.selectedFile = undefined
if(k != "preventUpdate")
self.update()
}
self.root.get = function(k)
{
return self[k]
}
var sortByType = function(a,b)
{
return a.type < b.type ? -1 : ( a.type > b.type ? 1: 0 )
}
var calibre_size = function()
{
var h = $(self.root).outerHeight()
var w = $(self.root).width()
if(self.refs.stbar)
h -= ($(self.refs.stbar.root).height() + 10)
$(self.refs.listview.root).css("height", h + "px")
$(self.refs.gridview.root).css("height", h + "px")
$(self.refs.treecontainer).css("height", h + "px")
$(self.refs.listview.root).css("width", w + "px")
$(self.refs.gridview.root).css("width", w + "px")
$(self.refs.treecontainer).css("width", w + "px")
}
var refreshList = function(){
var items = []
$.each(self.data, function(i, v){
if(v.filename[0] == '.' && !self.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)
})
self.refs.listview.root.set("items", items)
}
var refreshGrid = function(){
var rows = []
$.each(self.data, function(i,v){
if(v.filename[0] == '.' && !self.showhidden) return
var row = [{value:v.filename, iconclass: v.iconclass?v.iconclass:v.type, icon:v.icon},{value:v.mime},{value:v.size},{idx:i}]
rows.push(row)
})
self.refs.gridview.root.set("rows",rows)
}
var refreshTree = function(){
self.refs.treeview.root.set("selectedItem", null)
var tdata = {}
tdata.name = self.path
tdata.nodes = getTreeData(self.data)
self.refs.treeview.root.set("data", tdata)
}
var getTreeData = function(data)
{
nodes = []
$.each(data, function(i,v){
if(v.filename[0] == '.' && !self.showhidden) return
v.name = v.filename
if(v.type == 'dir')
{
v.nodes = []
v.open = false
}
v.iconclass = v.iconclass?v.iconclass:v.type
v.icon = v.icon
nodes.push(v)
})
return nodes
}
var refreshData = function(){
self.data.sort(sortByType)
if(self.view == "icon")
refreshList()
else if(self.view == "list")
refreshGrid()
else
refreshTree()
}
var switchView = function()
{
$(self.refs.listview.root).hide()
$(self.refs.gridview.root).hide()
$(self.refs.treecontainer).hide()
self.selectedFile = undefined
self.refs.listview.root.set("selected", -1)
self.refs.treeview.selectedItem = undefined
self.refs.treeview.root.set("fetch",function(e,f){
if(!self.fetch) return
self.fetch(e, function(d){
f(getTreeData(d))
})
})
if(self.refs.stbar)
self.refs.stbar.root.set("text", "")
switch (self.view) {
case 'icon':
$(self.refs.listview.root).show()
break;
case 'list':
$(self.refs.gridview.root).show()
break;
case 'tree':
$(self.refs.treecontainer).show()
break;
default:
break;
}
calibre_size()
}
self.on("updated", function(){
if(self.preventUpdate)
{
self.preventUpdate = false
}
else
refreshData()
//console.log("update")
//calibre_size()
})
self.on("mount", function(){
switchView()
self.refs.listview.onlistselect = function(data)
{
data.id = self.rid
self.root.observable.trigger("fileselect",data)
}
self.refs.listview.onlistdbclick = function(data)
{
data.id = self.rid
self.root.observable.trigger("filedbclick",data)
}
self.refs.gridview.root.observable = self.root.observable
self.refs.gridview.ongridselect = function(d)
{
var data = {id:self.rid, data:self.data[d.data.child[3].idx], idx:d.data.child[3].idx}
self.root.observable.trigger("fileselect",data)
}
self.refs.gridview.ongriddbclick = function(d)
{
var data = {id:self.rid, data:self.data[d.data.child[3].idx], idx:d.data.child[3].idx}
self.root.observable.trigger("filedbclick",data)
}
self.refs.treeview.ontreeselect = function(d)
{
if(!d) return;
var data;
var el = d;
if(d.treepath == 0)// select the root
{
el = self.path.asFileHandler()
el.size = 0
el.filename = el.path
}
var data = {id:self.rid, data:el}
self.root.observable.trigger("fileselect",data)
}
self.refs.treeview.ontreedbclick = function(d)
{
if(!d || d.treepath == 0) return;
var data = {id:self.rid, data:d}
self.root.observable.trigger("filedbclick",data)
}
self.root.observable.on("fileselect", function(e){
if(e.id != self.rid) return
self.selectedFile = e.data
if(self.onfileselect)
self.onfileselect(e.data)
if(self.refs.stbar)
self.refs.stbar.root.set("text", __("Selected: {0} ({1} bytes)", e.data.filename, e.data.size?e.data.size:"0"))//.html()
})
self.root.observable.on("filedbclick", function(e){
if(e.id != self.rid ) return
if(e.data.type != "dir" && self.onfileopen)
self.onfileopen(e.data)
else if(self.chdir && e.data.type == "dir")
self.chdir(e.data.path)
})
calibre_size()
self.root.observable.on("resize", function(e){
calibre_size()
})
self.root.observable.on("calibrate", function(e){
calibre_size()
})
/*self.root.observable.on("*", function(e){
console.log(e)
})*/
})
</script>
</afx-file-view>

View File

@ -1,183 +0,0 @@
<afx-float-list ref = "container">
<div ref = "list">
<div each={item,i in items } class={float_list_item:true, float_list_item_selected: parent._autoselect(item,i)} ondblclick = {parent._dbclick} onmousedown = {parent._select} oncontextmenu = {parent._select}>
<afx-label color = {item.color} iconclass = {item.iconclass} icon = {item.icon} text = {item.text}></afx-label>
</div>
</div>
<script>
this.items = opts.items || []
var self = this
self.selidx = -1
self.onlistselect = opts.onlistselect
self.onlistdbclick = opts.onlistdbclick
self.fetch = undefined
this.root.observable = opts.observable || riot.observable()
this.rid = $(self.root).attr("data-id") || Math.floor(Math.random() * 100000) + 1
self.dir = opts.dir || "horizontal"
self.root.set = function(k,v)
{
if(k == "selected")
{
if(self.selidx != -1)
self.items[self.selidx].selected =false
if(v == -1)
self.selidx = -1
else
if(self.items[v]) self.items[v].selected = true
}
else if(k == "*")
for(var i in v)
self[i] = v[i]
else
self[k] = v
self.update()
}
self.root.get = function(k)
{
if(k == "selected")
if(self.selidx == -1)
return undefined
else
return self.items[self.selidx]
return self[k]
}
self.root.push = function(e,u)
{
self.items.push(e)
if(u) self.update()
}
self.root.unshift = function(e,u)
{
self.items.unshift(e)
if(u) self.update()
}
self.root.remove = function(e,u)
{
var i = self.items.indexOf(e)
if(i >= 0)
{
if(self.selidx != -1)
{
self.items[self.selidx].selected =false
self.selidx = -1
}
self.items.splice(i, 1)
if(u)
self.update()
}
}
self.root.refresh = function()
{
_refresh()
}
this.on("mount", function(){
if(self.root.ready)
self.root.ready(self.root)
// now refresh the list
_refresh()
})
var _refresh = function()
{
var ctop = 20
var cleft = 20
var gw = $(self.refs.container).width()
var gh = $(self.refs.container).height()
$(self.refs.list)
.children()
.each(function(e)
{
$(this).unbind("mousedown")
_enable_drag($(this))
var w = $(this).width()
var h = $(this).height()
$(this).css("top", ctop + "px").css("left", cleft + "px")
if(self.dir == "horizontal")
{
ctop += h + 20
if(ctop > gh)
{
ctop = 20
cleft += w + 20
}
}
else
{
cleft += w + 20
if(cleft > gw )
{
cleft = 20
ctop += h + 20
}
}
})
}
var _enable_drag = function(el)
{
var globalof = $(self.refs.container).offset()
el
.css("user-select","none")
.css("cursor","default")
.css("position",'absolute')
.on("mousedown", function(e){
e.preventDefault()
offset = el.offset()
offset.top = e.clientY - offset.top
offset.left = e.clientX - offset.left
$(window).on("mousemove", function(e){
var top,left
top = e.clientY - offset.top - globalof.top
left = e.clientX - globalof.top - offset.left
left = left < 0?0:left
top = top < 0?0:top
el.css("top", top +"px").css("left",left + "px")
})
$(window).on("mouseup", function(e){
//console.log("unbind mouse up")
$(window).unbind("mousemove", null)
})
})
}
_autoselect(it,i)
{
if(self.selidx == i) return true
if(!it.selected || it.selected == false) return false
var data = {
id:self.rid,
data:it,
idx:i}
//if(self.selidx != -1)
// self.items[self.selidx].selected =false
self.selidx = i
if(self.onlistselect)
self.onlistselect(data)
this.root.observable.trigger('listselect',data)
return true
}
_select(event)
{
if(self.selidx != -1 && self.selidx < self.items.length)
self.items[self.selidx].selected =false
event.item.item.selected = true
//console.log(self.items)
self.update()
//event.preventUpdate = true
}
_dbclick(event)
{
data = {
id:self.rid,
data:event.item.item,
idx: event.item.i}
if(self.onlistdbclick)
self.onlistdbclick(data)
self.root.observable.trigger('listdbclick', data)
}
</script>
</afx-float-list>

View File

@ -1,218 +0,0 @@
<afx-grid-view>
<afx-grid-row ref="gridhead" rootid = {rid} observable = {root.observable} header="true" class = {grid_row_header:header} if = {header} cols = {header}> </afx-grid-row>
<div ref = "scroller" style="width:100%; overflow:auto;">
<div ref = "container" style ="padding-bottom:10px">
<afx-grid-row each={ child, i in rows } class = {selected: child.selected} rootid = {parent.rid} observable = {parent.root.observable} index = {i} cols = {child} ondblclick = {parent._dbclick} onclick = {parent._select} oncontextmenu = {parent._select} head = {parent.refs.gridhead} ></afx-grid-row>
</div>
</div>
<script>
this.header = opts.header
this.rows = opts.rows || []
var self = this
this.rid = $(self.root).attr("data-id") || Math.floor(Math.random() * 100000) + 1
self.selidx = -1
self.nrow = 0
self.ongridselect = opts.ongridselect
self.ongriddbclick = opts.ongriddbclick
self.root.observable = opts.observable
self.root.set = function(k,v)
{
if(k == "selected")
self._select({item:self.rows[v], preventDefault:function(){}})
else if(k == "*")
for(var i in v)
self[i] = v[i]
else
self[k] = v
self.update()
}
this.calibrate_size = function()
{
if(self.header && self.refs.gridhead)
{
$(self.refs.scroller).css("height",
$(self.root).height() - $(self.refs.gridhead.root).children().first().height()
+ "px")
}
else
$(self.refs.scroller).css("height",
$(self.root).height() + "px")
}
self.root.get = function(k)
{
if(k == "selected")
return (self.selidx == -1?null:self.rows[self.selidx])
return self[k]
}
this.on("mount", function(){
if(self.refs.gridhead)
self.refs.gridhead.observable = self.root.observable
$(self.refs.container)
.css("display","table")
//.css("flex-direction","column")
.css("width","100%")
self.calibrate_size()
self.root.observable.on("resize",function(){
if(self.root)
self.calibrate_size()
})
})
this.on("updated",function(){
if(self.selidx >= self.rows.length)
self.selidx = -1
if(self.nrow == self.rows.length) return
self.nrow = self.rows.length
self.calibrate_size()
})
_select(event)
{
var data = {
id:self.rid,
data:event.item}
if(self.ongridselect)
self.ongridselect(data)
if(self.selidx != -1)
self.rows[self.selidx].selected =false
self.selidx = event.item.i
self.rows[self.selidx].selected = true
self.root.observable.trigger('gridselect',data)
event.preventUpdate = true
self.update()
//event.preventDefault()
}
_dbclick(event)
{
data = {
id:self.rid,
data:event.item}
if(self.ongriddbclick)
self.ongriddbclick(data)
self.root.observable.trigger('griddbclick', data)
}
</script>
</afx-grid-view>
<afx-grid-row>
<div style = {!header? "display: table-cell;" :""} onclick = {parent._cell_select} each = { child,i in cols } class = {string:typeof child.value == "string", number: typeof child.value == "number", cellselected: parent._auto_cell_select(child,i)} >
<afx-label color={child.color} icon = {child.icon} iconclass = {child.iconclass} text = {child.value} ></afx-label>
</div>
<script>
this.cols = opts.cols || []
var self = this
this.rid = opts.rootid
this.index = opts.index
this.header = eval(opts.header)||false
this.head = opts.head
this.selidx = -1
self.observable = opts.observable
this.colssize = []
var update_header_size = function()
{
if(!self.cols || self.cols.length == 0) return
var totalw = $(self.root).parent().width()
if(totalw == 0) return
var ocw = 0
var nauto = 0
self.colssize = []
$.each(self.cols, function(i,e){
if(e.width)
{
self.colssize.push(e.width)
ocw += e.width
}
else
{
self.colssize.push(-1)
nauto++
}
})
if(nauto > 0)
{
var cellw = parseInt((totalw - ocw)/ nauto)
$.each(self.colssize,function(i,e){if(e == -1) self.colssize[i] = cellw})
}
calibrate_size()
}
var calibrate_size = function()
{
var i = 0
$(self.root)
.children()
.each(function(){
$(this).css("width", self.colssize[i]+"px")
i++
})
}
this.on("updated", function(){
if(self.header)
update_header_size()
else if(self.head && self.index == 0)
{
self.colssize = self.head.colssize
calibrate_size()
}
})
this.on("mount", function(){
if (self.header)
{
$(self.root)
.css("display", "flex")
.css("flex-direction", "row")
update_header_size()
}
else
{
$(self.root)
.css("display","table-row")
//.css("flex-direction","row")
.css("width","100%")
if(self.head && self.index == 0)
{
self.colssize = self.head.colssize
calibrate_size()
}
}
self.observable.on("gridcellselect", function(data){
if(data.id != self.rid || self.selidx == -1) return;
if(data.row != self.index)
{
self.cols[self.selidx].selected = false
self.selidx = -1
}
})
self.observable.on("resize",function(){
self.update()
})
})
_cell_select(event)
{
if(self.header) return;
if(self.selidx != -1)
{
self.cols[self.selidx].selected = false
self.selidx = -1
}
self.cols[event.item.i].selected = true
}
_auto_cell_select(child,i)
{
if(!child.selected || self.header) return false;
if(self.selidx == i) return true;
var data = {
id:self.rid,
data:child,
col:i,
row:self.index}
self.selidx = i
self.observable.trigger("gridcellselect",data)
return true;
}
</script>
</afx-grid-row>

View File

@ -1,79 +0,0 @@
<afx-hbox style = "display:block;">
<div ref = "container" class="afx-hbox-container">
<yield/>
</div>
<script>
var self = this
this.rid = $(self.root).attr("data-id") || Math.floor(Math.random() * 100000) + 1
this.on('mount', function(){
self.root.observable = (self.parent && self.parent.root && self.parent.root.observable) || opts.observable || riot.observable()
$(self.refs.container)
.css("display","flex")
.css("flex-direction","row")
.css("width","100%")
calibrate_size()
if(self.root.observable)
{
self.root.observable.on("resize", function(w,h){
calibrate_size()
})
self.root.observable.on("calibrate", function(){
calibrate_size()
})
}
})
self.root.update = function()
{
self.update()
}
var calibrate_size = function()
{
var auto_width = []
var csize, ocwidth = 0, avaiheight;
avaiheight = $(self.root).height()
avaiWidth = $(self.root).width()
/*if(avaiheight == 0)
{
avaiheight = $(self.parent.root).height()
$(self.root).css("height", avaiheight+"px")
}
if(avaiWidth == 0)
{
avaiWidth = $(self.parent.root).width()
$(self.root).css("height", avaiWidth+"px")
}*/
$(self.refs.container).css("height",avaiheight + "px")
$(self.refs.container)
.children()
.each(function(e)
{
this.observable = self.root.observable
//.css("height",avaiheight + "px")
var dw = $(this).attr("data-width")
if(dw)
{
if(dw == "grow") return
if(dw[dw.length-1] === "%")
dw = Number(dw.slice(0,-1))*avaiWidth/100;
$(this).css("width",dw + "px")
ocwidth += Number(dw)
}
else
{
$(this).css("flex-grow","1")
auto_width.push(this)
}
})
csize = (avaiWidth - ocwidth)/ (auto_width.length)
if(csize > 0)
$.each(auto_width, function(i,v)
{
$(v).css("width", csize + "px")
})
self.root.observable.trigger("hboxchange",
{id:self.rid, w:csize, h:avaiheight})
}
</script>
</afx-hbox>

View File

@ -1,20 +0,0 @@
<afx-html ref = "container">
<script>
this.content = opts.content
this.root.innerHTML = this.content
this.updateContent = undefined
var self = this
this.root.set = function(k, v)
{
self[k] = v
if(k == "content")
self.root.innerHTML = v
else if(k == "updateContent")
self.update()
}
this.on("update", function(){
if(self.updateContent)
self.root.innerHTML = self.updateContent()
})
</script>
</afx-html>

View File

@ -1,33 +0,0 @@
<afx-label>
<span style = {color?"color:" + color:""} >
<i if={iconclass} class = {iconclass} ></i>
<i if={icon} class="icon-style" style = { "background: url("+window.OS.API.handler.get+"/"+icon+");background-size: 100% 100%;background-repeat: no-repeat;" }></i>
{ text?text.__():"" }
</span>
<script>
this.iconclass = opts.iconclass
this.icon = opts.icon
this.text = opts.text
this.color = opts.color
var self = this
this.on("update",function(){
self.iconclass = opts.iconclass
self.icon = opts.icon
self.text = opts.text
self.color = opts.color
})
self.root.set = function(k,v)
{
if(k == "*")
for(var i in v)
opts[i] = v[i]
else
opts[k] = v
self.update()
}
self.root.get = function(k)
{
return self[k]
}
</script>
</afx-label>

View File

@ -1,227 +0,0 @@
<afx-list-view class = {dropdown: opts.dropdown == "true"} style = "display:flex; flex-direction:column">
<div class = "list-container" ref = "container" style="flex:1;">
<div if = {opts.dropdown == "true"} ref = "current" onclick = {show_list}>
<afx-label ref = "drlabel"></afx-label>
</div>
<ul ref = "mlist" >
<li each={item,i in items } class={selected: parent._autoselect(item,i)} ondblclick = {parent._dbclick} onclick = {parent._select} oncontextmenu = {parent._select}>
<afx-label class = {item.class} color = {item.color} iconclass = {item.iconclass} icon = {item.icon} text = {item.text}></afx-label>
<i if = {item.closable} class = "closable" click = {parent._remove}></i>
<ul if = {item.complex} class = "complex-content">
<li each = {ctn,j in item.detail} class = {ctn.class}>{ctn.text.toString()}</li>
</ul>
</li>
</ul>
</div>
<div if = {opts.dropdown != "true" && buttons} class = "button_container">
<afx-button each = {btn,i in buttons} text = {btn.text} icon = {btn.icon} iconclass = {btn.iconclass} onbtclick = {btn.onbtclick}></afx-button>
</div>
<script>
this.items = opts.items || []
var self = this
self.selidx = -1
self.onlistselect = opts.onlistselect
self.onlistdbclick = opts.onlistdbclick
self.onitemclose = opts.onitemclose
self.buttons = opts.buttons
var onclose = false
this.rid = $(self.root).attr("data-id") || Math.floor(Math.random() * 100000) + 1
self.root.set = function(k,v)
{
if(k == "selected")
{
if(self.selidx != -1)
self.items[self.selidx].selected =false
if(v == -1)
self.selidx = -1
else
if(self.items[v]) self.items[v].selected = true
}
else if(k == "*")
for(var i in v)
self[i] = v[i]
else
self[k] = v
self.update()
}
self.root.get = function(k)
{
if(k == "selected")
if(self.selidx != -1)
return self.items[self.selidx]
else
return undefined
else if(k == "count")
return self.items.length
return self[k]
}
self.root.selectNext = function()
{
var idx = self.selidx + 1
if(idx >= self.items.length) return;
if(self.selidx != -1)
self.items[self.selidx].selected =false
self.items[idx].selected =true
self.update()
}
self.root.selectPrev = function()
{
var idx = self.selidx - 1
if(idx < 0) return;
if(self.selidx != -1)
self.items[self.selidx].selected =false
self.items[idx].selected =true
self.update()
}
self.root.push = function(e,u)
{
self.items.push(e)
if(u) self.update()
}
self.root.unshift = function(e,u)
{
self.items.unshift(e)
if(u) self.update()
}
self.root.replaceItem = function(o, n, u)
{
var ix = self.items.indexOf(o)
if(ix >= 0)
{
self.items[ix] = n
if(u) self.update()
}
}
self.root.remove = function(e,u)
{
var i = self.items.indexOf(e)
if(i >= 0)
{
if(self.selidx != -1)
{
self.items[self.selidx].selected =false
self.selidx = -1
}
self.items.splice(i, 1)
if(u)
self.update()
onclose = true
}
}
if(opts.observable)
this.root.observable = opts.observable
else
{
this.root.observable = riot.observable()
}
this.on("mount", function(){
self.root.observable = opts.observable || (self.parent && self.parent.root && self.parent.root.observable) || riot.observable()
if(opts.dropdown == "true")
{
var cl = function()
{
$(self.refs.container).css("width", $(self.root).width() + "px" )
$(self.refs.current).css("width", $(self.root).width() + "px" )
$(self.refs.mlist).css("width", $(self.root).width() + "px" )
}
cl()
self.root.observable.on("calibrate", function(){
cl()
})
self.root.observable.on("resize", function(){
cl()
})
$(document).click(function(event) {
if(!$(event.target).closest(self.refs.container).length) {
$(self.refs.mlist).hide()
}
})
//$(self.root).css("position","relative")
$(self.refs.container)
.css("position","absolute")
.css("display","inline-block")
$(self.refs.mlist)
.css("position","absolute")
.css("display","none")
.css("top","100%")
.css("left","0")
self.root.observable.on("vboxchange", function(e){
if(e.id == self.parent.rid)
$(self.refs.container).css("width", $(self.root).parent().innerWidth() + "px" )
})
}
})
show_list(event)
{
var desktoph = $("#desktop").height()
var off = $(self.root).offset().top + $(self.refs.mlist).height()
if( off > desktoph )
$(self.refs.mlist)
.css("top","-" + $(self.refs.mlist).outerHeight() + "px")
else
$(self.refs.mlist).css("top","100%")
$(self.refs.mlist).show()
//event.preventDefault()
event.preventUpdate = true
}
_remove(event)
{
r = true
if(self.onitemclose)
r = self.onitemclose(event)
if(r)
self.root.remove(event.item.item, true)
}
_autoselect(it,i)
{
if(!it.selected || it.selected == false) return false
if(self.selidx == i) return true
var data = {
id:self.rid,
data:it,
idx:i}
//if(self.selidx != -1)
// self.items[self.selidx].selected =false
self.selidx = i
if(opts.dropdown == "true")
{
$(self.refs.mlist).hide()
self.refs.drlabel.root.set("*",it)
}
if(self.onlistselect)
self.onlistselect(data)
this.root.observable.trigger('listselect',data)
//console.log("list select")
return true
}
_select(event)
{
if(onclose)
{
onclose = false
event.preventUpdate = true
return
}
if(self.selidx != -1 && self.selidx < self.items.length)
self.items[self.selidx].selected =false
event.item.item.selected = true
}
_dbclick(event)
{
data = {
id:self.rid,
data:event.item.item,
idx: event.item.i}
if(self.onlistdbclick)
self.onlistdbclick(data)
self.root.observable.trigger('listdbclick', data)
}
</script>
</afx-list-view>

View File

@ -1,179 +0,0 @@
<afx-menu >
<ul class={context: opts.context == "true"}>
<li class="afx-corner-fix"></li>
<li ref = "container" each={ data,i in items } class = {afx_submenu:data.child != null && data.child.length > 0, fix_padding:data.icon} no-reorder>
<a href="#" onclick = {parent.onselect}>
<afx-switch if = {data.switch || data.radio} class = {checked:parent.checkItem(data)} enable = false swon = {data.checked} ></afx-switch>
<afx-label color = {data.color} iconclass = {data.iconclass} icon = {data.icon} text = {data.text} ></afx-label>
<span if={data.shortcut} class = "shortcut">{data.shortcut}</span>
</a>
<afx-menu ref = "submenus" index = {i} if={data.child != null && data.child.length > 0} child={data.child} onmenuselect = {data.onmenuselect} observable = {parent.root.observable} rootid = {parent.rid}></afx-menu>
</li>
<li class="afx-corner-fix"></li>
</ul>
<script>
this.items = opts.child || []
if(opts.index != undefined)
this.index = opts.index
else
this.index = -1
var isRoot
var lastChecked = undefined
if(opts.rootid)
{
this.rid = opts.rootid
isRoot = false
}
else
{
this.rid = $(this.root).attr("data-id") || Math.floor(Math.random() * 100000) + 1
isRoot = true
}
var self = this
this.onmenuselect = opts.onmenuselect
checkItem(d)
{
if(d.checked == true && d.radio)
{
if(lastChecked)
lastChecked.checked = false
lastChecked = d
lastChecked.checked = true
}
return false
}
self.root.set = function(k,v)
{
if(k == "*")
for(var i in v)
self[i] = v[i]
else
self[k] = v
self.update()
}
self.root.push = function(e,u)
{
self.items.push(e)
if(u)
self.update()
}
self.root.unshift = function(e,u)
{
self.items.unshift(e)
if(u)
self.update()
}
self.root.remove = function(e,u)
{
var i = self.items.indexOf(e)
if(i >= 0)
self.items.splice(i, 1)
if(u)
self.update()
}
self.root.update = function()
{
self.update()
}
self.root.get = function(k)
{
return self[k]
}
self.root.show = function(e)
{
//only for menucontext
if(opts.context != "true") return
$(self.root)
.css("top", e.clientY - 15 + "px")
.css("left",e.clientX -5 + "px")
.show()
$(document).on("click",mnhide)
}
if(opts.observable)
{
this.root.observable = opts.observable
}
else
{
this.root.observable = riot.observable()
this.root.observable.on('menuselect',function(data){
if(self.onmenuselect)
self.onmenuselect(data)
if(opts.context == "true")
$(self.root).hide()
else if(!data.root && self.refs.container)
{
var arr = self.refs.container.length?self.refs.container:[self.refs.container]
for( var i in arr)
$("afx-menu",arr[i]).first().hide()
}
})
}
var mnhide = function(event)
{
if(opts.context == "true")
{
if(event.button == 2 || event.originalEvent.button == 2)
{
return
}
if(!$(event.target).closest(self.root).length) {
$(self.root).hide()
$(document).unbind("click",mnhide)
}
return
}
if(!$(event.target).closest(self.refs.container).length && self.refs.container) {
var arr = self.refs.container.length?self.refs.container:[self.refs.container]
for( var i in arr)
$("afx-menu",arr[i]).first().hide()
$(document).unbind("click",mnhide)
}
else
{
if(self.refs.container && self.refs.container.length)
for(var i in self.refs.container)
if(!$(event.target).closest(self.refs.container[i]).length) {
$("afx-menu",self.refs.container[i]).first().hide()
}
}
}
onselect(event)
{
var data = {id:self.rid, root:isRoot, e:event, item:event.item}
if(event.item.data.switch)
{
event.item.data.checked = !event.item.data.checked
} else if(event.item.data.radio)
{
if(lastChecked)
{
lastChecked.checked = false
}
event.item.data.checked = true
lastChecked = event.item.data
}
this.root.observable.trigger('menuselect',data)
if( this.onmenuselect && !isRoot) this.onmenuselect(data)
event.preventDefault()
$(document).unbind("click",mnhide)
if(opts.context == "true") return
if(isRoot && self.refs.container)
{
if(self.refs.container.length)
$("afx-menu",self.refs.container[event.item.i]).first().show()
else
$("afx-menu",self.refs.container).first().show()
$(document).on("click",mnhide)
return
}
}
</script>
</afx-menu>

View File

@ -1,100 +0,0 @@
<afx-nspinner>
<input ref = "holder" type="text" value = {value}></input>
<ul ref = "spinner">
<li class = "incr" ref= "incr" onclick="{ _incr }"> <i></i> </li>
<li class = "decr" ref = "decr" onclick="{ _decr }"> <i></i> </li>
</ul>
<script>
this.value = eval(opts.value) || 0
this.step = Number(opts.step) || 1
this.onchange = opts.onchange
var self = this
this.rid = $(self.root).attr("data-id") || Math.floor(Math.random() * 100000) + 1
self.root.set = function(k,v)
{
if(k == "*")
for(var i in v)
self[i] = v[i]
else
self[k] = v
self.update()
}
self.root.get = function(k)
{
return self[k]
}
self._incr = function(e)
{
self.value = self.value + self.step;
self.update();
if(self.onchange) self.onchange(self.value);
}
self.on("mount", function(){
self.root.observable = opts.observable || (self.parent && self.parent.root && self.parent.root.observable) || riot.observable()
$(self.refs.spinner).css("width", "20px" );
var cl = function()
{
$(self.refs.holder).css("width", $(self.root).width() - 20 + "px" )
$(self.refs.holder).css("height", $(self.root).height() + "px" )
$(self.refs.spinner)
.css("width","20px")
.css("height", $(self.root).height() + "px" )
$(self.refs.incr)
.css("height", $(self.root).height()/2 - 2 + "px")
.css("position", "relative")
$(self.refs.decr).css("height", $(self.root).height()/2 -2 + "px")
.css("position", "relative")
$(self.refs.spinner).find("li")
.css("display","block")
.css("text-align", "center")
.css("vertical-align", "middle")
$(self.refs.spinner).find("i")
.css("font-size", "16px")
.css("position", "absolute")
var fn = function(ie, pos)
{
var el = $(ie).find("i")
el
.css(pos,($(ie).height()-el.height()) /2 + "px" )
.css("left", ($(ie).width()-el.width())/2 + "px" )
}
fn(self.refs.decr, "bottom")
fn(self.refs.incr, "top")
}
cl()
self.root.observable.on("calibrate", function(){
cl()
})
self.root.observable.on("resize", function(){
cl()
});
$(self.refs.holder).on('keyup', function (e) {
if (e.keyCode == 13) {
var val = self.refs.holder.value;
if(!isNaN(val))
{
val = eval(val)
if(val < 0)
val = self.value;
self.value = val;
}
self.refs.holder.value = self.value;
if(self.onchange) self.onchange(self.value);
}
});
})
self._decr = function(e)
{
if(self.value == 0) return;
self.value = self.value - self.step;
self.update();
if(self.onchange) self.onchange(self.value);
}
</script>
</afx-nspinner>

View File

@ -1,45 +0,0 @@
<afx-overlay>
<yield/>
<script>
this.width = opts.width || 200
this.height = opts.height || 400
var self = this;
self.commander = null
this.root.observable = opts.observable || riot.observable()
var id = $(self.root).attr("data-id")
var calibre_size = function()
{
$(self.root)
.css("width", self.width + "px")
.css("height", self.height + "px")
self.root.observable.trigger("resize", {id:id,w:self.width,h:self.height})
}
self.root.set = function(k,v)
{
if(k == "*")
for(var i in v)
self[i] = v[i]
else
self[k] = v
if( k == "width" || k == "height")
calibre_size()
self.update()
}
self.root.get = function(k)
{
return self[k]
}
self.on("mount", function(){
$(self.root)
.css("position", "absolute")
//.css("z-index",1000000)
$(self.root).children().each(function(e){
this.observalbe = self.root.observalbe
})
calibre_size()
self.root.observable.trigger("rendered", self.root)
})
</script>
</afx-overlay>

View File

@ -1,85 +0,0 @@
<afx-resizer>
<script>
var self = this
self.dir = "hz"
self.resizable = undefined
self.parent = undefined
self.minsize = 0
self.on("mount", function(){
//self.parent = $(self.root).parent().parent()
var tagname = $(self.parent.root).prop("tagName")
self.resizable = $(self.root).prev().length == 1 ? $(self.root).prev()[0]: undefined
//self.nextel = $(self.root).next().length == 1 ? $(self.root).next()[0]: undefined
if(tagname == "AFX-HBOX")
{
self.dir = "hz"
$(self.root).css("cursor", "col-resize")
if(self.resizable)
{
self.minsize = parseInt($(self.resizable).attr("min-width"))
}
}
else if(tagname == "AFX-VBOX")
{
self.dir = "ve"
$(self.root).css("cursor", "row-resize")
if(self.resizable)
{
self.minsize = parseInt($(self.resizable).attr("min-height"))
}
}
else
{
//$(self.root).css("cursor", "normal")
self.dir = "none"
}
if(!self.minsize)
self.minsize = 10
enable_dragging()
})
var enable_dragging = function()
{
$(self.root)
.css("user-select","none")
$(self.root).on("mousedown", function(e){
e.preventDefault()
$(window).on("mousemove", function(evt){
if(!self.resizable) return
if(self.dir == "hz")
horizontalResize(evt)
else if (self.dir == "ve")
verticalResize(evt)
})
$(window).on("mouseup", function(evt){
//console.log("unbind mouse up")
$(window).unbind("mousemove", null)
})
})
}
var horizontalResize = function(e)
{
if(!self.resizable) return
var offset = $(self.resizable).offset()
w = Math.round(e.clientX - offset.left)
if(w < self.minsize) w = self.minsize
$(self.resizable).attr("data-width", w.toString())
self.parent.root.observable.trigger("calibrate", self.resizable)
}
var verticalResize = function(e)
{
//console.log("vboz")
if(!self.resizable) return
var offset = $(self.resizable).offset()
//console.log($(self.resizable).innerHeight())
//console.log(e.clientY, offset.top)
h = Math.round(e.clientY - offset.top)
if(h < self.minsize) h = minsize
$(self.resizable).attr("data-height", h.toString())
self.parent.root.observable.trigger("calibrate", self.resizable)
}
</script>
</afx-resizer>

View File

@ -1,107 +0,0 @@
<afx-slider>
<div class= "container" ref="container">
<div class = "progress" ref ="prg"></div>
<div if = {dragable} class = "dragpoint" ref = "point"></div>
</div>
<script>
this.value = Number(opts.value) || 0
this.max = Number(opts.max) || 100
if(opts.dragable != undefined)
this.dragable = eval(opts.dragable)
else
this.dragable = true
this.onchanging = opts.onchanging
this.onchange = opts.onchange
//this.rid = $(self.root).attr("data-id") || Math.floor(Math.random() * 100000) + 1
var self = this
self.root.set = function(k,v)
{
if(k == "*")
for(var i in v)
self[i] = v[i]
else
self[k] = v
/*if(k == "value")
{
if(self.onchange) self.onchange(self.value)
if(self.onchanging) self.onchanging(self.value)
}*/
self.update()
}
self.root.get = function(k)
{
return self[k]
}
var calibrate = function() {
if(self.value > self.max) self.value = self.max
$(self.refs.container).css("width", $(self.root).width() + "px")
var w = $(self.refs.container).width()*self.value/ self.max
$(self.refs.prg).css("width", w + "px").css("height", $(self.refs.container).height()+"px")
if(self.dragable)
{
var ow = w - $(self.refs.point).width()/2
var top = Math.floor(($(self.refs.prg).height() - $(self.refs.point).height())/2)
$(self.refs.point).css("left", ow + "px").css("top", top + "px")
}
}
self.on("update", function(){
calibrate()
})
var enable_dragging = function()
{
$(self.refs.point)
.css("user-select","none")
.css("cursor","default")
$(self.refs.point).on("mousedown", function(e){
e.preventDefault()
offset = $(self.refs.container).offset()
$(window).on("mousemove", function(e){
var left
left = e.clientX - offset.left
left = left < 0?0:left
var maxw = $(self.refs.container).width();
left = left > maxw?maxw : left
self.value = left*self.max/maxw
calibrate()
if(self.onchanging) self.onchanging(self.value)
})
$(window).on("mouseup", function(e){
if(self.onchange) self.onchange(self.value)
$(window).unbind("mousemove", null)
$(window).unbind("mouseup", null)
})
})
}
self.on("mount", function(){
self.root.observable = opts.observable || (self.parent && self.parent.root && self.parent.root.observable) || riot.observable()
if(self.dragable)
{
$(self.refs.point).css("position", "absolute")
$(self.refs.point).hide()
$(self.root).mouseover(function(){
$(self.refs.point).show()
}).mouseout(function(){
$(self.refs.point).hide()
})
enable_dragging()
}
$(self.refs.container).click( function(e){
var offset = $(self.refs.container).offset()
var left = e.clientX - offset.left
var maxw = $(self.refs.container).width()
self.value = left*self.max/maxw
calibrate()
if(self.onchange) self.onchange(self.value)
if(self.onchanging) self.onchanging(self.value)
})
self.root.observable.on("calibrate",function(){
calibrate()
})
self.root.observable.on("resize", function(){
calibrate()
})
calibrate()
})
</script>
</afx-slider>

View File

@ -1,56 +0,0 @@
<afx-switch>
<span class = {swon: swon} onclick = {toggle}></span>
<script>
if(opts.swon != undefined)
this.swon = opts.swon
else
this.swon = false
var self = this
//this.root.observable = opts.observable
if(opts.enable != undefined)
this.enable = opts.enable
else
this.enable = true
this.onchange = opts.onchange
this.rid = $(self.root).attr("data-id") || Math.floor(Math.random() * 100000) + 1
self.root.set = function(k,v)
{
if(k == "*")
for(var i in v)
opts[i] = v[i]
else
opts[k] = v
self.update()
}
self.root.get = function(k)
{
return self[k]
}
this.root.toggle = function()
{
opts.swon = !self.swon
self.update()
}
/*this.on("mount", function(){
})*/
this.on("update", function(e){
self.swon = typeof opts.swon == "function"?opts.swon():opts.swon
})
toggle(e)
{
if(!self.enable) return
opts.swon = !self.swon
self.swon = opts.swon
var data = {
id: self.rid,
data: opts.swon
}
if(opts.onchange)
opts.onchange(data)
if(self.root.observable)
self.root.observable.trigger("switch", data)
}
</script>
</afx-switch>

View File

@ -1,54 +0,0 @@
<afx-sys-panel>
<div>
<afx-menu data-id = "os_menu" ref = "aOsmenu" child={osmenu.child} onmenuselect = {osmenu.onmenuselect} class="afx-panel-os-menu"></afx-menu>
<afx-menu data-id = "appmenu" ref = "aAppmenu" child={appmenu.child} class = "afx-panel-os-app"></afx-menu>
<afx-menu data-id = "sys_tray" ref = "aTray" child={systray.child} onmenuselect = {systray.onmenuselect} class = "afx-panel-os-stray"></afx-menu>
</div>
<script>
this.osmenu = { child: [] }
this.appmenu = { child: [] }
this.systray = {
child: [],
onmenuselect: function(d){
if(d.root)
d.e.item.data.awake(d.e)
}
}
var self = this
self.root.attachservice = function(s)
{
self.refs.aTray.root.unshift(s,true)
s.attach(self.refs.aTray)
}
self.root.detachservice = function(s)
{
self.refs.aTray.root.remove(s, true)
}
self.root.set = function(k,v)
{
if(k == "*")
for(var i in v)
self[i] = v[i]
else
self[k] = v
self.update()
}
self.root.get = function(k)
{
return self[k]
}
self.root.update = function()
{
self.update()
}
this.on('mount', function() {
//console.log(self.refs.aOsmenu.root)
$(self.refs.aOsmenu.root).css("z-index",1000000)
$(self.refs.aAppmenu.root).css("z-index",1000000)
$(self.refs.aTray.root).css("z-index",1000000)
window.OS.courrier.trigger("syspanelloaded")
})
</script>
</afx-sys-panel>

View File

@ -1,71 +0,0 @@
<afx-tab-bar>
<afx-list-view ref = "list" />
<script>
var self = this
this.closable = opts.closable || false
self.ontabselect = opts.ontabselect
self.root.get = function (k) {
return self.refs.list.root.get(k)
}
self.root.update = function(){
self.update(true)
}
self.on("mount", function(){
self.root.observable = opts.observable || (self.parent && self.parent.root && self.parent.root.observable) || riot.observable()
self.refs.list.root.observable = self.root.observable
/*self.root.observable.on("listselect", function(){
console.log("list select")
})*/
self.refs.list.root.set ("onlistselect",function (e) {
//console.log("tab is seleced")
self.root.observable.trigger("tabselect", e)
if(self.ontabselect)
self.ontabselect(e)
})
})
self.root.set = function (k,v){
if( k == "*")
for(var i in v)
self.refs.list.root.set(i,v[i])
else if(k == "closable")
{
self.closable = v
}
else if(k == "ontabselect")
self.ontabselect = v
else
{
if(k == "items")
{
for(var i in v)
v[i].closable = self.closable
}
self.refs.list.root.set(k,v)
}
//self.update()
}
self.root.push = function(e,u)
{
e.closable = self.closable
self.refs.list.root.push(e,u)
}
self.root.replaceItem = function(o,n,u)
{
n.closable = self.closable
self.refs.list.root.replaceItem(o,n,u)
}
self.root.unshift = function(e,u)
{
self.refs.list.root.unshift(e,u)
}
self.root.remove = function(e,u)
{
self.refs.list.root.remove(e,u)
}
</script>
</afx-tab-bar>

View File

@ -1,91 +0,0 @@
<afx-tab-container>
<afx-hbox ref = "mybox" if = {bar == "left"}>
<afx-tab-bar data-ref="tabbar" ></afx-tab-bar>
<div data-ref="container"></div>
</afx-hbox>
<afx-vbox ref = "mybox" if = { bar == "top"}>
<afx-tab-bar data-ref="tabbar" ></afx-tab-bar>
<div data-ref="container"></div>
</afx-vbox>
<script>
this.bar = opts.bar || "top"
this.barwidth = opts.barwidth
this.barheight = opts.barheight
var schemes = []
var self = this
var calibrate = function()
{
$(self.refs.mybox.root).css("width", $(self.root).width()+"px")
$(self.refs.mybox.root).css("height", $(self.root).height()+"px")
self.root.observable.trigger("calibrate")
}
self.on("mount", function () {
self.tabbar = $("[data-ref='tabbar']", self.root)[0]
self.container = $("[data-ref='container']", self.root)[0]
if(self.barwidth)
$(self.tabbar).attr("data-width", self.barwidth)
if(self.barheight)
$(self.tabbar).attr("data-height", self.barheight)
self.root.observable = (self.parent && self.parent.root && self.parent.root.observable) || opts.observable || riot.observable()
self.tabbar.set("ontabselect", function(e){
$(self.container).children().each(function(el){
$(this).hide()
})
$(e.data.scheme).show()
e.data.handler.render()
calibrate()
})
self.root.observable.on("resize", function(){
calibrate()
})
})
self.on("update", function(){
$(self.container).children().each(function(e){
if(this.update) this.update()
})
})
var render = function(el)
{
var sch = $.parseHTML(el.scheme)
$(self.container).append(sch)
el.scheme = sch
riot.mount(sch, {observable: self.root.observable})
$(sch).hide()
el.handler = el.handler(sch[0])
//console.log(el.handler)
//el.handler.render()
self.root.observable.trigger("tabrendered")
//self.root.observable.trigger("calibrate")
}
var addTab = function(el)
{
if(!el.f)
el.f = (function(){})
self.tabbar.push(el)
if(el.url)
{
el.url.asFileHandler().read(function(d){
el.scheme = d
render(el)
})
}
else
{
render(el)
}
}
self.root.setTabs = function(arr)
{
if(arr.length <= 0)
{
self.tabbar.set("selected", 0)
return
}
self.root.observable.one("tabrendered", function(){
arr.splice(0,1)
self.root.setTabs(arr)
})
addTab(arr[0])
}
</script>
</afx-tab-container>

View File

@ -1,169 +0,0 @@
<afx-tree-view>
<div class={afx_tree_item_selected:treeroot.selectedItem && treeroot.selectedItem.treepath == data.treepath, afx_folder_item: isFolder(), afx_tree_item_odd: index%2 != 0 } onclick={select} ondblclick = {_dbclick} oncontextmenu = {select}>
<ul style = "padding:0;margin:0;white-space: nowrap;">
<li ref = "padding" ></li>
<li class = "itemname" style="display:inline-block;" >
<i if={ !isFolder() && data.iconclass} class = {data.iconclass} ></i>
<i if={!isFolder() && data.icon} class="icon-style" style = { "background: url("+data.icon+");background-size: 100% 100%;background-repeat: no-repeat;" }></i>
<span onclick={ toggle } if={ isFolder() } class={open ? 'afx-tree-view-folder-open' : 'afx-tree-view-folder-close'}></span>
{ data.name }
</li>
</ul>
</div>
<ul if={ isFolder() } show={ isFolder() && open }>
<li each={ child, i in data.nodes }>
<afx-tree-view ontreeselect = {parent.ontreeselect} index = {i} fetch = {parent.fetch} ontreedbclick = {parent.ontreedbclick} data={child} indent={indent+1} observable = {parent.root.observable} path = {parent.data.treepath + ">" + i} treeroot= {parent.treeroot}></afx-tree-view>
</li>
</ul>
<script>
var self = this
self.open = true
self.data = { name:"", nodes:null, treepath: opts.path, i:-1}
if(opts.data)
{
self.data = opts.data
//self.name = opts.data.name
//self.nodes = opts.data.nodes
//self.icon = opts.data.icon
self.open = opts.data.open == undefined?true:opts.data.open
//self.iconclass = opts.data.iconclass
}
self.rid = $(self.root).attr("data-id") || Math.floor(Math.random() * 100000) + 1
self.data.rid = self.rid
self.data.i = opts.index
self.ontreeselect = opts.ontreeselect
self.ontreedbclick = opts.ontreedbclick
self.fetch = opts.fetch
self.indent = opts.indent || 0
var istoggle = false
if(opts.treeroot)
{
this.treeroot = opts.treeroot
this.treeroot.counter++
}
else
{
this.treeroot = self
this.treeroot.counter = 0
}
self.data.treepath = opts.path || 0
//self.selected = false
self.selectedItem = null
self.index = this.treeroot.counter
var _dfind = function(l,d, k, v)
{
if( d[k] == v ) return l.push(d)
if(d.nodes && d.nodes.length > 0)
for(var i in d.nodes)
_dfind(l, d.nodes[i],k,v)
}
self.root.find = function(k, v)
{
var l = []
_dfind(l,self.data,k,v)
return l
}
self.root.set = function(k,v)
{
if(k == "*")
for(var i in v)
self[i] = v[i]
else if (k == "data")
for(var i in v)
self.data[i] = v[i]
else if (k == "selectedItem")
{
if(self.ontreeselect)
self.ontreeselect(self.data)
self.treeroot.selectedItem = v
self.root.observable.trigger('treeselect',self.data)
}
else
self[k] = v
self.update()
}
self.root.get = function(k)
{
//if(k == "data")
// return {name:self.name, nodes: self.nodes, icon:self.icon, iconclass: self.iconclass, selectedItem:self.selectedItem}
return self[k]
}
if(opts.observable)
this.root.observable = opts.observable
else
{
this.root.observable = riot.observable()
}
this.on("mount", function(){
$(self.refs.padding)
.css("display", "inline-block")
.css("height","1px")
.css("padding",0)
.css("margin", 0)
.css("background-color","transparent")
.css("width", self.indent*15 + "px" )
})
isFolder() {
return self.data.nodes //&& self.nodes.length
}
toggle(e) {
self.open = !self.open
e.preventDefault()
istoggle = true
if(self.open && self.data.nodes.length == 0 && self.fetch)
{
self.fetch(e.item, function(d){
self.data.nodes = d
self.update()
})
}
}
select(event)
{
if(istoggle)
{
istoggle = false
return
}
/*var data = {
id:self.rid,
data:event.item,
path:self.data.path
} */
if(self.ontreeselect)
self.ontreeselect(self.data)
self.treeroot.selectedItem = self.data
this.root.observable.trigger('treeselect',self.data)
event.preventUpdate = true
self.treeroot.update()
event.preventDefault()
}
_dbclick(event)
{
if(istoggle)
{
istoggle = false
return
}
/*data = {
id:self.rid,
data:event.item,
path: self.data.path}*/
if(self.ontreedbclick)
{
self.ontreedbclick(self.data)
}
self.root.observable.trigger('treedbclick', self.data)
}
</script>
</afx-tree-view>

View File

@ -1,81 +0,0 @@
<afx-vbox style = "display:block;">
<div ref = "container" class="afx-vbox-container">
<yield/>
</div>
<script>
var self = this
this.rid = $(self.root).attr("data-id") || Math.floor(Math.random() * 100000) + 1
this.on('mount', function(){
self.root.observable = (self.parent && self.parent.root && self.parent.root.observable) || opts.observable || riot.observable()
$(self.refs.container)
.css("display","flex")
.css("flex-direction","column")
.css("width","100%")
//.css("background-color","red")
//.css("overflow", "hidden")
calibrate_size()
if(self.root.observable)
{
self.root.observable.on("resize", function(w,h){
calibrate_size()
})
self.root.observable.on("calibrate", function(){
calibrate_size()
})
}
})
self.root.update = function()
{
self.update()
}
var calibrate_size = function()
{
var auto_height = []
var csize, ocheight = 0, avaiheight;
avaiheight = $(self.root).height()
avaiwidth = $(self.root).width()
/*if(avaiheight == 0)
{
avaiheight = $(self.parent.root).height()
$(self.root).css("height", avaiheight+"px")
}
if(avaiwidth == 0)
{
avaiwidth = $(self.parent.root).width()
$(self.root).css("height", avaiwidth+"px")
}*/
$(self.refs.container).css("height",avaiheight + "px")
$(self.refs.container)
.children()
.each(function(e)
{
this.observable = self.root.observable
//.css("border","1px solid black")
var dw = $(this).attr("data-height")
if(dw)
{
if(dw == "grow") return
if(dw[dw.length-1] === "%")
dw = Number(dw.slice(0,-1))*avaiheight/100;
$(this).css("height",dw + "px")
ocheight += Number(dw)
}
else
{
$(this).css("flex-grow","1")
auto_height.push(this)
}
})
csize = (avaiheight - ocheight)/ (auto_height.length)
if(csize > 0)
$.each(auto_height, function(i,v)
{
$(v).css("height", csize + "px")
})
self.root.observable.trigger("vboxchange",
{id:self.rid, w:avaiwidth, h:csize})
}
</script>
</afx-vbox>

665
src/core/tags/tag.ts Normal file
View File

@ -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<T> {
/**
* 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<T> {
/**
* `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<T> {
/**
* 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<T> = (e: TagEventType<T>) => 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<HTMLElement>}
* @memberof AFXTag
*/
protected refs: GenericObject<HTMLElement>;
/**
* 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<any>} v input object
* @memberof AFXTag
*/
set(v: GenericObject<any>) {
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<T extends AFXTag>(
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;
}
}
}
}
}

View File

@ -1,357 +0,0 @@
# Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
# 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

1702
src/core/vfs.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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 <xsang.le AT gmail DOT com>
// 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 = ($('<input>')).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 = $('<iframe/>', {
src: 'https://www.google.com/accounts/Logout',
frameborder: 0,
onload() {
//console.log("disconnect")
return auth2.disconnect();
}
//$(this).remove()
});
}
});
//($ "body").append(el)

View File

@ -1,355 +0,0 @@
# Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
# 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 Handler
G_CACHE = {"gdv://":{ id: "root", mime: 'dir' } }
class GoogleDriveHandler extends this.OS.API.VFS.BaseFileHandler
constructor: (path) ->
super path
me = @
@setting = _OS.setting.VFS.gdrive
return _courrier.oserror __("Unknown API setting for {0}", "GAPI"), (_API.throwe "OS.VFS"), null unless @setting
@gid = 'root' if @isRoot()
@cache = ""
oninit: (f) ->
me = @
return unless @setting
fn = (r) ->
return f() if r
# perform the login
G_CACHE = {"gdv://":{ id: "root", mime: 'dir' } }
gapi.auth2.getAuthInstance().signIn()
if _API.libready @setting.apilink
gapi.auth2.getAuthInstance().isSignedIn.listen (r) ->
fn(r)
fn(gapi.auth2.getAuthInstance().isSignedIn.get())
else
_API.require @setting.apilink, () ->
# avoid popup block
q = _courrier.getMID()
gapi.load "client:auth2", () ->
_API.loading q, "GAPI"
gapi.client.init {
apiKey: me.setting.API_KEY,
clientId: me.setting.CLIENT_ID,
discoveryDocs: me.setting.DISCOVERY_DOCS,
scope: me.setting.SCOPES
}
.then () ->
_API.loaded q, "OK"
gapi.auth2.getAuthInstance().isSignedIn.listen (r) ->
fn(r)
_GUI.openDialog "YesNoDialog", (d) ->
return _courrier.osinfo __("User abort the authentication") unless d
fn(gapi.auth2.getAuthInstance().isSignedIn.get())
, __("Authentication")
, { text: __("Would you like to login to {0}?", "Google Drive") }
.catch (err) ->
_API.loaded q, "FAIL"
_courrier.oserror __("VFS cannot init {0}: {1}", "GAPI",err.error), (_API.throwe "OS.VFS"), err
meta: (f) ->
me = @
@oninit () ->
q = _courrier.getMID()
me.gid = G_CACHE[me.path].id if G_CACHE[me.path]
if me.gid
#console.log "Gid exists ", me.gid
_API.loading q, "GAPI"
gapi.client.drive.files.get {
fileId: me.gid,
fields: me.fields()
}
.then (r) ->
_API.loaded q, "OK"
return unless r.result
r.result.mime = r.result.mimeType
f(r)
.catch (err) ->
_API.loaded q, "FAIL"
_courrier.oserror __("VFS cannot get meta data for {0}", me.gid), (_API.throwe "OS.VFS"), err
else
#console.log "Find file in ", me.parent()
fp = me.parent().asFileHandler()
fp.meta (d) ->
file = d.result
q1 = _courrier.getMID()
_API.loading q1, "GAPI"
G_CACHE[fp.path] = { id: file.id, mime: file.mimeType }
gapi.client.drive.files.list {
q: "name = '#{me.basename}' and '#{file.id}' in parents and trashed = false",
fields: "files(#{me.fields()})"
}
.then (r) ->
#console.log r
_API.loaded q1, "OK"
return unless r.result.files and r.result.files.length > 0
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
f { result: r.result.files[0] }
.catch (err) ->
_API.loaded q1, "FAIL"
_courrier.oserror __("VFS cannot get meta data for {0}", me.path), (_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 @info.mimeType is "application/vnd.google-apps.folder"
save: (id, m, f) ->
me = @
user = gapi.auth2.getAuthInstance().currentUser.get()
oauthToken = user.getAuthResponse().access_token
q = _courrier.getMID()
xhr = new XMLHttpRequest()
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'
_API.loading q, "GAPI"
error = (e, s) ->
_API.loaded q, "FAIL"
_courrier.oserror __("VFS cannot save : {0}", me.path), e, s
xhr.onreadystatechange = () ->
if ( xhr.readyState == 4 )
if ( xhr.status == 200 )
_API.loaded q, "OK"
f { result: JSON.parse(xhr.responseText) }
else
error xhr, xhr.status
xhr.onerror = () ->
error xhr, xhr.status
return xhr.send me.cache.replace /^data:[^;]+;base64,/g, "" if m is "base64"
@sendB64 m, (data) ->
xhr.send data.replace /^data:[^;]+;base64,/g, ""
getlink: () ->
return @info.webContentLink if @ready
return undefined
action: (n, p, f) ->
me = @
q = _courrier.getMID()
_API.loading q, "GAPI"
switch n
when "read"
return unless @info.id
if @isFolder()
gapi.client.drive.files.list {
q: "'#{me.info.id}' in parents and trashed = false",
fields: "files(#{me.fields()})"
}
.then (r) ->
_API.loaded q, "OK"
return unless r.result.files
for file in 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 is "application/vnd.google-apps.folder"
file.mime = "dir"
file.type = "dir"
file.size = 0
G_CACHE[file.path] = { id: file.gid, mime: file.mime }
f { result: r.result.files }
.catch (err) ->
_API.loaded q, "FAIL"
_courrier.oserror __("VFS cannot read : {0}", me.path), (_API.throwe "OS.VFS"), err
else
gapi.client.drive.files.get {
fileId: me.info.id,
alt: 'media'
}
.then (r) ->
_API.loaded q, "OK"
return f r.body unless p is "binary"
f r.body.asUint8Array()
.catch (err) ->
_API.loaded q, "FAIL"
_courrier.oserror __("VFS cannot read : {0}", me.path), (_API.throwe "OS.VFS"), err
when "mk"
return f { error: __("{0} is not a directory", @path) } unless @isFolder()
meta =
name: p,
parents: [@info.id],
mimeType: 'application/vnd.google-apps.folder'
gapi.client.drive.files.create {
resource: meta,
fields: 'id'
}
.then (r) ->
_API.loaded q, "OK"
#console.log r
return _courrier.oserror __("VFS cannot create : {0}", p), (_API.throwe "OS.VFS"), r unless r and r.result
G_CACHE[me.child p] = { id: r.result.id, mime: "dir" }
f r
.catch (err) ->
_API.loaded q, "FAIL"
_courrier.oserror __("VFS cannot create : {0}", p), (_API.throwe "OS.VFS"), err
return
when "write"
gid = undefined
gid = G_CACHE[me.path].id if G_CACHE[me.path]
if gid
_API.loaded q, "OK"
@save gid, p, f
else
dir = @parent().asFileHandler()
dir.onready () ->
meta =
name: me.basename,
mimeType: p,
parents: [dir.info.id]
gapi.client.drive.files.create {
resource: meta,
fields: 'id'
}
.then (r) ->
_API.loaded q, "OK"
return _courrier.oserror __("VFS cannot write : {0}", me.path), (_API.throwe "OS.VFS"), r unless r and r.result
G_CACHE[me.path] = { id: r.result.id, mime: p }
me.save r.result.id, p, f
.catch (err) ->
_API.loaded q, "FAIL"
_courrier.oserror __("VFS cannot write : {0}", me.path), (_API.throwe "OS.VFS"), err
when "upload"
return unless @isFolder()
#insert a temporal file selector
o = ($ '<input>').attr('type', 'file').css("display", "none")
_API.loaded q, "OK"
o.change () ->
#_API.loading q, p
fo = o[0].files[0]
file = (me.child fo.name).asFileHandler()
file.cache = fo
file.write fo.type, f
o.remove()
#_API.loaded q, p, "OK"
#_API.loaded q, p, "FAIL"
o.click()
when "remove"
return unless @info.id
gapi.client.drive.files.delete {
fileId: me.info.id
}
.then (r) ->
#console.log r
_API.loaded q, "OK"
return _courrier.oserror __("VFS cannot delete : {0}", me.path), (_API.throwe "OS.VFS"), r unless r
G_CACHE[me.path] = null
f { result: true }
.catch (err) ->
_API.loaded q, "FAIL"
_courrier.oserror __("VFS cannot delete : {0}", me.path), (_API.throwe "OS.VFS"), err
when "publish"
_API.loaded q, "OK"
return
when "download"
gapi.client.drive.files.get {
fileId: me.info.id,
alt: 'media'
}
.then (r) ->
_API.loaded q, "OK"
return _courrier.oserror __("VFS cannot download file : {0}", me.path), (_API.throwe "OS.VFS"), r unless r.body
bytes = []
for i in [0..(r.body.length - 1)]
bytes.push r.body.charCodeAt i
bytes = new Uint8Array(bytes)
blob = new Blob [bytes], { type: "octet/stream" }
_API.saveblob me.basename, blob
.catch (err) ->
_API.loaded q, "FAIL"
_courrier.oserror __("VFS cannot download file : {0}", me.path), (_API.throwe "OS.VFS"), err
when "move"
dest = p.asFileHandler().parent().asFileHandler()
dest.onready () ->
previousParents = me.info.parents.join ','
gapi.client.drive.files.update {
fileId: me.info.id,
addParents: dest.info.id,
removeParents: previousParents,
fields: "id"
}
.then (r) ->
_API.loaded q, "OK"
return _courrier.oserror __("VFS cannot move : {0}", me.path), (_API.throwe "OS.VFS"), r unless r
f r
.catch (err) ->
_API.loaded q, "FAIL"
_courrier.oserror __("VFS cannot move : {0}", me.gid), (_API.throwe "OS.VFS"), err
, (err) ->
_API.loaded q, "FAIL"
else
_API.loaded q, "FAIL"
return _courrier.osfail __("VFS unknown action: {0}", n), (_API.throwe "OS.VFS"), n
self.OS.API.VFS.register "^gdv$", GoogleDriveHandler
# search the cache for file
self.OS.API.onsearch "Google Drive", (t) ->
arr = []
term = new RegExp t, "i"
for k, v of G_CACHE
if (k.match term) or (v and v.mime.match term)
file = k.asFileHandler()
file.text = file.basename
file.mime = v.mime
file.iconclass = "fa fa-file"
file.iconclass = "fa fa-folder" if file.mime is "dir"
file.complex = true
file.detail = [{ text: file.path }]
arr.push file
return arr
self.OS.onexit "cleanUpGoogleDrive", () ->
G_CACHE = { "gdv://": { id: "root", mime: 'dir' } }
return unless _OS.setting.VFS.gdrive and _API.libready _OS.setting.VFS.gdrive.apilink
auth2 = gapi.auth2.getAuthInstance()
return unless auth2
if auth2.isSignedIn.get()
el = $ '<iframe/>', {
src: 'https://www.google.com/accounts/Logout',
frameborder: 0,
onload: () ->
#console.log("disconnect")
auth2.disconnect()
#$(this).remove()
}
#($ "body").append(el)

View File

@ -23,12 +23,9 @@
<title>AntOS webOS</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="resources/themes/system/font-awesome.css" rel="stylesheet">
<link href="resources/themes/system/system.css" rel="stylesheet">
<link id="ostheme" rel="stylesheet" href="">
<!--link href="theme/antos/style.css" rel="stylesheet"-->
<script src="scripts/jquery-3.2.1.min.js"></script>
<script src="resources/antos_tags.js" type="riot/tag"></script>
<script src="scripts/riot.compiler.min.js"> </script>
<script src="scripts/antos.js"></script>
</head>
<body>

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,8 @@
define("ace/ext/beautify/php_rules",["require","exports","module","ace/token_iterator"],function(e,t,n){"use strict";var r=e("ace/token_iterator").TokenIterator;t.newLines=[{type:"support.php_tag",value:"<?php"},{type:"support.php_tag",value:"<?"},{type:"support.php_tag",value:"?>"},{type:"paren.lparen",value:"{",indent:!0},{type:"paren.rparen",breakBefore:!0,value:"}",indent:!1},{type:"paren.rparen",breakBefore:!0,value:"})",indent:!1,dontBreak:!0},{type:"comment"},{type:"text",value:";"},{type:"text",value:":",context:"php"},{type:"keyword",value:"case",indent:!0,dontBreak:!0},{type:"keyword",value:"default",indent:!0,dontBreak:!0},{type:"keyword",value:"break",indent:!1,dontBreak:!0},{type:"punctuation.doctype.end",value:">"},{type:"meta.tag.punctuation.end",value:">"},{type:"meta.tag.punctuation.begin",value:"<",blockTag:!0,indent:!0,dontBreak:!0},{type:"meta.tag.punctuation.begin",value:"</",indent:!1,breakBefore:!0,dontBreak:!0},{type:"punctuation.operator",value:";"}],t.spaces=[{type:"xml-pe",prepend:!0},{type:"entity.other.attribute-name",prepend:!0},{type:"storage.type",value:"var",append:!0},{type:"storage.type",value:"function",append:!0},{type:"keyword.operator",value:"="},{type:"keyword",value:"as",prepend:!0,append:!0},{type:"keyword",value:"function",append:!0},{type:"support.function",next:/[^\(]/,append:!0},{type:"keyword",value:"or",append:!0,prepend:!0},{type:"keyword",value:"and",append:!0,prepend:!0},{type:"keyword",value:"case",append:!0},{type:"keyword.operator",value:"||",append:!0,prepend:!0},{type:"keyword.operator",value:"&&",append:!0,prepend:!0}],t.singleTags=["!doctype","area","base","br","hr","input","img","link","meta"],t.transform=function(e,n,r){var i=e.getCurrentToken(),s=t.newLines,o=t.spaces,u=t.singleTags,a="",f=0,l=!1,c,h,p={},d,v={},m=!1,g="";while(i!==null){if(!i){i=e.stepForward();continue}i.type=="support.php_tag"&&i.value!="?>"?r="php":i.type=="support.php_tag"&&i.value=="?>"?r="html":i.type=="meta.tag.name.style"&&r!="css"?r="css":i.type=="meta.tag.name.style"&&r=="css"?r="html":i.type=="meta.tag.name.script"&&r!="js"?r="js":i.type=="meta.tag.name.script"&&r=="js"&&(r="html"),v=e.stepForward(),v&&v.type.indexOf("meta.tag.name")==0&&(d=v.value),p.type=="support.php_tag"&&p.value=="<?="&&(l=!0),i.type=="meta.tag.name"&&(i.value=i.value.toLowerCase()),i.type=="text"&&(i.value=i.value.trim());if(!i.value){i=v;continue}g=i.value;for(var y in o)i.type==o[y].type&&(!o[y].value||i.value==o[y].value)&&v&&(!o[y].next||o[y].next.test(v.value))&&(o[y].prepend&&(g=" "+i.value),o[y].append&&(g+=" "));i.type.indexOf("meta.tag.name")==0&&(c=i.value),m=!1;for(y in s)if(i.type==s[y].type&&(!s[y].value||i.value==s[y].value)&&(!s[y].blockTag||u.indexOf(d)===-1)&&(!s[y].context||s[y].context===r)){s[y].indent===!1&&f--;if(s[y].breakBefore&&(!s[y].prev||s[y].prev.test(p.value))){a+="\n",m=!0;for(y=0;y<f;y++)a+=" "}break}if(l===!1)for(y in s)if(p.type==s[y].type&&(!s[y].value||p.value==s[y].value)&&(!s[y].blockTag||u.indexOf(c)===-1)&&(!s[y].context||s[y].context===r)){s[y].indent===!0&&f++;if(!s[y].dontBreak&&!m){a+="\n";for(y=0;y<f;y++)a+=" "}break}a+=g,p.type=="support.php_tag"&&p.value=="?>"&&(l=!1),h=c,p=i,i=v;if(i===null)break}return a}}),define("ace/ext/beautify",["require","exports","module","ace/token_iterator","ace/ext/beautify/php_rules"],function(e,t,n){"use strict";var r=e("ace/token_iterator").TokenIterator,i=e("./beautify/php_rules").transform;t.beautify=function(e){var t=new r(e,0,0),n=t.getCurrentToken(),s=e.$modeId.split("/").pop(),o=i(t,s);e.doc.setValue(o)},t.commands=[{name:"beautify",exec:function(e){t.beautify(e.session)},bindKey:"Ctrl-Shift-B"}]});
(function() {
window.require(["ace/ext/beautify"], function() {});
define("ace/ext/beautify",["require","exports","module","ace/token_iterator"],function(e,t,n){"use strict";function i(e,t){return e.type.lastIndexOf(t+".xml")>-1}var r=e("../token_iterator").TokenIterator;t.singletonTags=["area","base","br","col","command","embed","hr","html","img","input","keygen","link","meta","param","source","track","wbr"],t.blockTags=["article","aside","blockquote","body","div","dl","fieldset","footer","form","head","header","html","nav","ol","p","script","section","style","table","tbody","tfoot","thead","ul"],t.beautify=function(e){var n=new r(e,0,0),s=n.getCurrentToken(),o=e.getTabString(),u=t.singletonTags,a=t.blockTags,f,l=!1,c=!1,h=!1,p="",d="",v="",m=0,g=0,y=0,b=0,w=0,E=0,S=0,x,T=0,N=0,C=[],k=!1,L,A=!1,O=!1,M=!1,_=!1,D={0:0},P=[],H=function(){f&&f.value&&f.type!=="string.regexp"&&(f.value=f.value.replace(/^\s*/,""))},B=function(){p=p.replace(/ +$/,"")},j=function(){p=p.trimRight(),l=!1};while(s!==null){T=n.getCurrentTokenRow(),C=n.$rowTokens,f=n.stepForward();if(typeof s!="undefined"){d=s.value,w=0,M=v==="style"||e.$modeId==="ace/mode/css",i(s,"tag-open")?(O=!0,f&&(_=a.indexOf(f.value)!==-1),d==="</"&&(_&&!l&&N<1&&N++,M&&(N=1),w=1,_=!1)):i(s,"tag-close")?O=!1:i(s,"comment.start")?_=!0:i(s,"comment.end")&&(_=!1),!O&&!N&&s.type==="paren.rparen"&&s.value.substr(0,1)==="}"&&N++,T!==x&&(N=T,x&&(N-=x));if(N){j();for(;N>0;N--)p+="\n";l=!0,!i(s,"comment")&&!s.type.match(/^(comment|string)$/)&&(d=d.trimLeft())}if(d){s.type==="keyword"&&d.match(/^(if|else|elseif|for|foreach|while|switch)$/)?(P[m]=d,H(),h=!0,d.match(/^(else|elseif)$/)&&p.match(/\}[\s]*$/)&&(j(),c=!0)):s.type==="paren.lparen"?(H(),d.substr(-1)==="{"&&(h=!0,A=!1,O||(N=1)),d.substr(0,1)==="{"&&(c=!0,p.substr(-1)!=="["&&p.trimRight().substr(-1)==="["?(j(),c=!1):p.trimRight().substr(-1)===")"?j():B())):s.type==="paren.rparen"?(w=1,d.substr(0,1)==="}"&&(P[m-1]==="case"&&w++,p.trimRight().substr(-1)==="{"?j():(c=!0,M&&(N+=2))),d.substr(0,1)==="]"&&p.substr(-1)!=="}"&&p.trimRight().substr(-1)==="}"&&(c=!1,b++,j()),d.substr(0,1)===")"&&p.substr(-1)!=="("&&p.trimRight().substr(-1)==="("&&(c=!1,b++,j()),B()):s.type!=="keyword.operator"&&s.type!=="keyword"||!d.match(/^(=|==|===|!=|!==|&&|\|\||and|or|xor|\+=|.=|>|>=|<|<=|=>)$/)?s.type==="punctuation.operator"&&d===";"?(j(),H(),h=!0,M&&N++):s.type==="punctuation.operator"&&d.match(/^(:|,)$/)?(j(),H(),d.match(/^(,)$/)&&S>0&&E===0?N++:(h=!0,l=!1)):s.type==="support.php_tag"&&d==="?>"&&!l?(j(),c=!0):i(s,"attribute-name")&&p.substr(-1).match(/^\s$/)?c=!0:i(s,"attribute-equals")?(B(),H()):i(s,"tag-close")&&(B(),d==="/>"&&(c=!0)):(j(),H(),c=!0,h=!0);if(l&&(!s.type.match(/^(comment)$/)||!!d.substr(0,1).match(/^[/#]$/))&&(!s.type.match(/^(string)$/)||!!d.substr(0,1).match(/^['"]$/))){b=y;if(m>g){b++;for(L=m;L>g;L--)D[L]=b}else m<g&&(b=D[m]);g=m,y=b,w&&(b-=w),A&&!E&&(b++,A=!1);for(L=0;L<b;L++)p+=o}s.type==="keyword"&&d.match(/^(case|default)$/)&&(P[m]=d,m++),s.type==="keyword"&&d.match(/^(break)$/)&&P[m-1]&&P[m-1].match(/^(case|default)$/)&&m--,s.type==="paren.lparen"&&(E+=(d.match(/\(/g)||[]).length,S+=(d.match(/\{/g)||[]).length,m+=d.length),s.type==="keyword"&&d.match(/^(if|else|elseif|for|while)$/)?(A=!0,E=0):!E&&d.trim()&&s.type!=="comment"&&(A=!1);if(s.type==="paren.rparen"){E-=(d.match(/\)/g)||[]).length,S-=(d.match(/\}/g)||[]).length;for(L=0;L<d.length;L++)m--,d.substr(L,1)==="}"&&P[m]==="case"&&m--}s.type=="text"&&(d=d.replace(/\s+$/," ")),c&&!l&&(B(),p.substr(-1)!=="\n"&&(p+=" ")),p+=d,h&&(p+=" "),l=!1,c=!1,h=!1;if(i(s,"tag-close")&&(_||a.indexOf(v)!==-1)||i(s,"doctype")&&d===">")_&&f&&f.value==="</"?N=-1:N=1;i(s,"tag-open")&&d==="</"?m--:i(s,"tag-open")&&d==="<"&&u.indexOf(f.value)===-1?m++:i(s,"tag-name")?v=d:i(s,"tag-close")&&d==="/>"&&u.indexOf(v)===-1&&m--,x=T}}s=f}p=p.trim(),e.doc.setValue(p)},t.commands=[{name:"beautify",description:"Format selection (Beautify)",exec:function(e){t.beautify(e.session)},bindKey:"Ctrl-Shift-B"}]}); (function() {
window.require(["ace/ext/beautify"], function(m) {
if (typeof module == "object" && typeof exports == "object" && module) {
module.exports = m;
}
});
})();

View File

@ -0,0 +1,8 @@
define("ace/ext/code_lens",["require","exports","module","ace/line_widgets","ace/lib/event","ace/lib/lang","ace/lib/dom","ace/editor","ace/config"],function(e,t,n){"use strict";function u(e){var t=e.$textLayer,n=t.$lenses;n&&n.forEach(function(e){e.remove()}),t.$lenses=null}function a(e,t){var n=e&t.CHANGE_LINES||e&t.CHANGE_FULL||e&t.CHANGE_SCROLL||e&t.CHANGE_TEXT;if(!n)return;var r=t.session,i=t.session.lineWidgets,s=t.$textLayer,a=s.$lenses;if(!i){a&&u(t);return}var f=t.$textLayer.$lines.cells,l=t.layerConfig,c=t.$padding;a||(a=s.$lenses=[]);var h=0;for(var p=0;p<f.length;p++){var d=f[p].row,v=i[d],m=v&&v.lenses;if(!m||!m.length)continue;var g=a[h];g||(g=a[h]=o.buildDom(["div",{"class":"ace_codeLens"}],t.container)),g.style.height=l.lineHeight+"px",h++;for(var y=0;y<m.length;y++){var b=g.childNodes[2*y];b||(y!=0&&g.appendChild(o.createTextNode("\u00a0|\u00a0")),b=o.buildDom(["a"],g)),b.textContent=m[y].title,b.lensCommand=m[y]}while(g.childNodes.length>2*y-1)g.lastChild.remove();var w=t.$cursorLayer.getPixelPosition({row:d,column:0},!0).top-l.lineHeight*v.rowsAbove-l.offset;g.style.top=w+"px";var E=t.gutterWidth,S=r.getLine(d).search(/\S|$/);S==-1&&(S=0),E+=S*l.characterWidth,E-=t.scrollLeft,g.style.paddingLeft=c+E+"px"}while(h<a.length)a.pop().remove()}function f(e){if(!e.lineWidgets)return;var t=e.widgetManager;e.lineWidgets.forEach(function(e){e&&e.lenses&&t.removeLineWidget(e)})}function l(e){e.codeLensProviders=[],e.renderer.on("afterRender",a),e.$codeLensClickHandler||(e.$codeLensClickHandler=function(t){var n=t.target.lensCommand;n&&e.execCommand(n.id,n.arguments)},i.addListener(e.container,"click",e.$codeLensClickHandler,e)),e.$updateLenses=function(){function o(){var r=n.selection.cursor,i=n.documentToScreenRow(r);t.setLenses(n,s);var o=n.$undoManager&&n.$undoManager.$lastDelta;if(o&&o.action=="remove"&&o.lines.length>1)return;var u=n.documentToScreenRow(r),a=e.renderer.layerConfig.lineHeight,f=n.getScrollTop()+(u-i)*a;n.setScrollTop(f)}var n=e.session;if(!n)return;n.widgetManager||(n.widgetManager=new r(n),n.widgetManager.attach(e));var i=e.codeLensProviders.length,s=[];e.codeLensProviders.forEach(function(e){e.provideCodeLenses(n,function(e,t){if(e)return;t.forEach(function(e){s.push(e)}),i--,i==0&&o()})})};var n=s.delayedCall(e.$updateLenses);e.$updateLensesOnInput=function(){n.delay(250)},e.on("input",e.$updateLensesOnInput)}function c(e){e.off("input",e.$updateLensesOnInput),e.renderer.off("afterRender",a),e.$codeLensClickHandler&&e.container.removeEventListener("click",e.$codeLensClickHandler)}var r=e("../line_widgets").LineWidgets,i=e("../lib/event"),s=e("../lib/lang"),o=e("../lib/dom");t.setLenses=function(e,t){var n=Number.MAX_VALUE;f(e),t&&t.forEach(function(t){var r=t.start.row,i=t.start.column,s=e.lineWidgets&&e.lineWidgets[r];if(!s||!s.lenses)s=e.widgetManager.$registerLineWidget({rowCount:1,rowsAbove:1,row:r,column:i,lenses:[]});s.lenses.push(t.command),r<n&&(n=r)}),e._emit("changeFold",{data:{start:{row:n}}})},t.registerCodeLensProvider=function(e,t){e.setOption("enableCodeLens",!0),e.codeLensProviders.push(t),e.$updateLensesOnInput()},t.clear=function(e){t.setLenses(e,null)};var h=e("../editor").Editor;e("../config").defineOptions(h.prototype,"editor",{enableCodeLens:{set:function(e){e?l(this):c(this)}}}),o.importCssString(".ace_codeLens { position: absolute; color: #aaa; font-size: 88%; background: inherit; width: 100%; display: flex; align-items: flex-end; pointer-events: none;}.ace_codeLens > a { cursor: pointer; pointer-events: auto;}.ace_codeLens > a:hover { color: #0000ff; text-decoration: underline;}.ace_dark > .ace_codeLens > a:hover { color: #4e94ce;}","")}); (function() {
window.require(["ace/ext/code_lens"], function(m) {
if (typeof module == "object" && typeof exports == "object" && module) {
module.exports = m;
}
});
})();

View File

@ -1,5 +1,8 @@
define("ace/ext/elastic_tabstops_lite",["require","exports","module","ace/editor","ace/config"],function(e,t,n){"use strict";var r=function(e){this.$editor=e;var t=this,n=[],r=!1;this.onAfterExec=function(){r=!1,t.processRows(n),n=[]},this.onExec=function(){r=!0},this.onChange=function(e){r&&(n.indexOf(e.start.row)==-1&&n.push(e.start.row),e.end.row!=e.start.row&&n.push(e.end.row))}};(function(){this.processRows=function(e){this.$inChange=!0;var t=[];for(var n=0,r=e.length;n<r;n++){var i=e[n];if(t.indexOf(i)>-1)continue;var s=this.$findCellWidthsForBlock(i),o=this.$setBlockCellWidthsToMax(s.cellWidths),u=s.firstRow;for(var a=0,f=o.length;a<f;a++){var l=o[a];t.push(u),this.$adjustRow(u,l),u++}}this.$inChange=!1},this.$findCellWidthsForBlock=function(e){var t=[],n,r=e;while(r>=0){n=this.$cellWidthsForRow(r);if(n.length==0)break;t.unshift(n),r--}var i=r+1;r=e;var s=this.$editor.session.getLength();while(r<s-1){r++,n=this.$cellWidthsForRow(r);if(n.length==0)break;t.push(n)}return{cellWidths:t,firstRow:i}},this.$cellWidthsForRow=function(e){var t=this.$selectionColumnsForRow(e),n=[-1].concat(this.$tabsForRow(e)),r=n.map(function(e){return 0}).slice(1),i=this.$editor.session.getLine(e);for(var s=0,o=n.length-1;s<o;s++){var u=n[s]+1,a=n[s+1],f=this.$rightmostSelectionInCell(t,a),l=i.substring(u,a);r[s]=Math.max(l.replace(/\s+$/g,"").length,f-u)}return r},this.$selectionColumnsForRow=function(e){var t=[],n=this.$editor.getCursorPosition();return this.$editor.session.getSelection().isEmpty()&&e==n.row&&t.push(n.column),t},this.$setBlockCellWidthsToMax=function(e){var t=!0,n,r,i,s=this.$izip_longest(e);for(var o=0,u=s.length;o<u;o++){var a=s[o];if(!a.push){console.error(a);continue}a.push(NaN);for(var f=0,l=a.length;f<l;f++){var c=a[f];t&&(n=f,i=0,t=!1);if(isNaN(c)){r=f;for(var h=n;h<r;h++)e[h][o]=i;t=!0}i=Math.max(i,c)}}return e},this.$rightmostSelectionInCell=function(e,t){var n=0;if(e.length){var r=[];for(var i=0,s=e.length;i<s;i++)e[i]<=t?r.push(i):r.push(0);n=Math.max.apply(Math,r)}return n},this.$tabsForRow=function(e){var t=[],n=this.$editor.session.getLine(e),r=/\t/g,i;while((i=r.exec(n))!=null)t.push(i.index);return t},this.$adjustRow=function(e,t){var n=this.$tabsForRow(e);if(n.length==0)return;var r=0,i=-1,s=this.$izip(t,n);for(var o=0,u=s.length;o<u;o++){var a=s[o][0],f=s[o][1];i+=1+a,f+=r;var l=i-f;if(l==0)continue;var c=this.$editor.session.getLine(e).substr(0,f),h=c.replace(/\s*$/g,""),p=c.length-h.length;l>0&&(this.$editor.session.getDocument().insertInLine({row:e,column:f+1},Array(l+1).join(" ")+" "),this.$editor.session.getDocument().removeInLine(e,f,f+1),r+=l),l<0&&p>=-l&&(this.$editor.session.getDocument().removeInLine(e,f+l,f),r+=l)}},this.$izip_longest=function(e){if(!e[0])return[];var t=e[0].length,n=e.length;for(var r=1;r<n;r++){var i=e[r].length;i>t&&(t=i)}var s=[];for(var o=0;o<t;o++){var u=[];for(var r=0;r<n;r++)e[r][o]===""?u.push(NaN):u.push(e[r][o]);s.push(u)}return s},this.$izip=function(e,t){var n=e.length>=t.length?t.length:e.length,r=[];for(var i=0;i<n;i++){var s=[e[i],t[i]];r.push(s)}return r}}).call(r.prototype),t.ElasticTabstopsLite=r;var i=e("../editor").Editor;e("../config").defineOptions(i.prototype,"editor",{useElasticTabstops:{set:function(e){e?(this.elasticTabstops||(this.elasticTabstops=new r(this)),this.commands.on("afterExec",this.elasticTabstops.onAfterExec),this.commands.on("exec",this.elasticTabstops.onExec),this.on("change",this.elasticTabstops.onChange)):this.elasticTabstops&&(this.commands.removeListener("afterExec",this.elasticTabstops.onAfterExec),this.commands.removeListener("exec",this.elasticTabstops.onExec),this.removeListener("change",this.elasticTabstops.onChange))}}})});
(function() {
window.require(["ace/ext/elastic_tabstops_lite"], function() {});
define("ace/ext/elastic_tabstops_lite",["require","exports","module","ace/editor","ace/config"],function(e,t,n){"use strict";var r=function(e){this.$editor=e;var t=this,n=[],r=!1;this.onAfterExec=function(){r=!1,t.processRows(n),n=[]},this.onExec=function(){r=!0},this.onChange=function(e){r&&(n.indexOf(e.start.row)==-1&&n.push(e.start.row),e.end.row!=e.start.row&&n.push(e.end.row))}};(function(){this.processRows=function(e){this.$inChange=!0;var t=[];for(var n=0,r=e.length;n<r;n++){var i=e[n];if(t.indexOf(i)>-1)continue;var s=this.$findCellWidthsForBlock(i),o=this.$setBlockCellWidthsToMax(s.cellWidths),u=s.firstRow;for(var a=0,f=o.length;a<f;a++){var l=o[a];t.push(u),this.$adjustRow(u,l),u++}}this.$inChange=!1},this.$findCellWidthsForBlock=function(e){var t=[],n,r=e;while(r>=0){n=this.$cellWidthsForRow(r);if(n.length==0)break;t.unshift(n),r--}var i=r+1;r=e;var s=this.$editor.session.getLength();while(r<s-1){r++,n=this.$cellWidthsForRow(r);if(n.length==0)break;t.push(n)}return{cellWidths:t,firstRow:i}},this.$cellWidthsForRow=function(e){var t=this.$selectionColumnsForRow(e),n=[-1].concat(this.$tabsForRow(e)),r=n.map(function(e){return 0}).slice(1),i=this.$editor.session.getLine(e);for(var s=0,o=n.length-1;s<o;s++){var u=n[s]+1,a=n[s+1],f=this.$rightmostSelectionInCell(t,a),l=i.substring(u,a);r[s]=Math.max(l.replace(/\s+$/g,"").length,f-u)}return r},this.$selectionColumnsForRow=function(e){var t=[],n=this.$editor.getCursorPosition();return this.$editor.session.getSelection().isEmpty()&&e==n.row&&t.push(n.column),t},this.$setBlockCellWidthsToMax=function(e){var t=!0,n,r,i,s=this.$izip_longest(e);for(var o=0,u=s.length;o<u;o++){var a=s[o];if(!a.push){console.error(a);continue}a.push(NaN);for(var f=0,l=a.length;f<l;f++){var c=a[f];t&&(n=f,i=0,t=!1);if(isNaN(c)){r=f;for(var h=n;h<r;h++)e[h][o]=i;t=!0}i=Math.max(i,c)}}return e},this.$rightmostSelectionInCell=function(e,t){var n=0;if(e.length){var r=[];for(var i=0,s=e.length;i<s;i++)e[i]<=t?r.push(i):r.push(0);n=Math.max.apply(Math,r)}return n},this.$tabsForRow=function(e){var t=[],n=this.$editor.session.getLine(e),r=/\t/g,i;while((i=r.exec(n))!=null)t.push(i.index);return t},this.$adjustRow=function(e,t){var n=this.$tabsForRow(e);if(n.length==0)return;var r=0,i=-1,s=this.$izip(t,n);for(var o=0,u=s.length;o<u;o++){var a=s[o][0],f=s[o][1];i+=1+a,f+=r;var l=i-f;if(l==0)continue;var c=this.$editor.session.getLine(e).substr(0,f),h=c.replace(/\s*$/g,""),p=c.length-h.length;l>0&&(this.$editor.session.getDocument().insertInLine({row:e,column:f+1},Array(l+1).join(" ")+" "),this.$editor.session.getDocument().removeInLine(e,f,f+1),r+=l),l<0&&p>=-l&&(this.$editor.session.getDocument().removeInLine(e,f+l,f),r+=l)}},this.$izip_longest=function(e){if(!e[0])return[];var t=e[0].length,n=e.length;for(var r=1;r<n;r++){var i=e[r].length;i>t&&(t=i)}var s=[];for(var o=0;o<t;o++){var u=[];for(var r=0;r<n;r++)e[r][o]===""?u.push(NaN):u.push(e[r][o]);s.push(u)}return s},this.$izip=function(e,t){var n=e.length>=t.length?t.length:e.length,r=[];for(var i=0;i<n;i++){var s=[e[i],t[i]];r.push(s)}return r}}).call(r.prototype),t.ElasticTabstopsLite=r;var i=e("../editor").Editor;e("../config").defineOptions(i.prototype,"editor",{useElasticTabstops:{set:function(e){e?(this.elasticTabstops||(this.elasticTabstops=new r(this)),this.commands.on("afterExec",this.elasticTabstops.onAfterExec),this.commands.on("exec",this.elasticTabstops.onExec),this.on("change",this.elasticTabstops.onChange)):this.elasticTabstops&&(this.commands.removeListener("afterExec",this.elasticTabstops.onAfterExec),this.commands.removeListener("exec",this.elasticTabstops.onExec),this.removeListener("change",this.elasticTabstops.onChange))}}})}); (function() {
window.require(["ace/ext/elastic_tabstops_lite"], function(m) {
if (typeof module == "object" && typeof exports == "object" && module) {
module.exports = m;
}
});
})();

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,8 @@
;
(function() {
window.require(["ace/ext/error_marker"], function() {});
; (function() {
window.require(["ace/ext/error_marker"], function(m) {
if (typeof module == "object" && typeof exports == "object" && module) {
module.exports = m;
}
});
})();

View File

@ -1,5 +1,8 @@
define("ace/ext/menu_tools/overlay_page",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";var r=e("../../lib/dom"),i="#ace_settingsmenu, #kbshortcutmenu {background-color: #F7F7F7;color: black;box-shadow: -5px 4px 5px rgba(126, 126, 126, 0.55);padding: 1em 0.5em 2em 1em;overflow: auto;position: absolute;margin: 0;bottom: 0;right: 0;top: 0;z-index: 9991;cursor: default;}.ace_dark #ace_settingsmenu, .ace_dark #kbshortcutmenu {box-shadow: -20px 10px 25px rgba(126, 126, 126, 0.25);background-color: rgba(255, 255, 255, 0.6);color: black;}.ace_optionsMenuEntry:hover {background-color: rgba(100, 100, 100, 0.1);transition: all 0.3s}.ace_closeButton {background: rgba(245, 146, 146, 0.5);border: 1px solid #F48A8A;border-radius: 50%;padding: 7px;position: absolute;right: -8px;top: -8px;z-index: 100000;}.ace_closeButton{background: rgba(245, 146, 146, 0.9);}.ace_optionsMenuKey {color: darkslateblue;font-weight: bold;}.ace_optionsMenuCommand {color: darkcyan;font-weight: normal;}.ace_optionsMenuEntry input, .ace_optionsMenuEntry button {vertical-align: middle;}.ace_optionsMenuEntry button[ace_selected_button=true] {background: #e7e7e7;box-shadow: 1px 0px 2px 0px #adadad inset;border-color: #adadad;}.ace_optionsMenuEntry button {background: white;border: 1px solid lightgray;margin: 0px;}.ace_optionsMenuEntry button:hover{background: #f0f0f0;}";r.importCssString(i),n.exports.overlayPage=function(t,n,i,s,o,u){function l(e){e.keyCode===27&&a.click()}i=i?"top: "+i+";":"",o=o?"bottom: "+o+";":"",s=s?"right: "+s+";":"",u=u?"left: "+u+";":"";var a=document.createElement("div"),f=document.createElement("div");a.style.cssText="margin: 0; padding: 0; position: fixed; top:0; bottom:0; left:0; right:0;z-index: 9990; background-color: rgba(0, 0, 0, 0.3);",a.addEventListener("click",function(){document.removeEventListener("keydown",l),a.parentNode.removeChild(a),t.focus(),a=null}),document.addEventListener("keydown",l),f.style.cssText=i+s+o+u,f.addEventListener("click",function(e){e.stopPropagation()});var c=r.createElement("div");c.style.position="relative";var h=r.createElement("div");h.className="ace_closeButton",h.addEventListener("click",function(){a.click()}),c.appendChild(h),f.appendChild(c),f.appendChild(n),a.appendChild(f),document.body.appendChild(a),t.blur()}}),define("ace/ext/menu_tools/get_editor_keyboard_shortcuts",["require","exports","module","ace/lib/keys"],function(e,t,n){"use strict";var r=e("../../lib/keys");n.exports.getEditorKeybordShortcuts=function(e){var t=r.KEY_MODS,n=[],i={};return e.keyBinding.$handlers.forEach(function(e){var t=e.commandKeyBinding;for(var r in t){var s=r.replace(/(^|-)\w/g,function(e){return e.toUpperCase()}),o=t[r];Array.isArray(o)||(o=[o]),o.forEach(function(e){typeof e!="string"&&(e=e.name),i[e]?i[e].key+="|"+s:(i[e]={key:s,command:e},n.push(i[e]))})}}),n}}),define("ace/ext/keybinding_menu",["require","exports","module","ace/editor","ace/ext/menu_tools/overlay_page","ace/ext/menu_tools/get_editor_keyboard_shortcuts"],function(e,t,n){"use strict";function i(t){if(!document.getElementById("kbshortcutmenu")){var n=e("./menu_tools/overlay_page").overlayPage,r=e("./menu_tools/get_editor_keyboard_shortcuts").getEditorKeybordShortcuts,i=r(t),s=document.createElement("div"),o=i.reduce(function(e,t){return e+'<div class="ace_optionsMenuEntry"><span class="ace_optionsMenuCommand">'+t.command+"</span> : "+'<span class="ace_optionsMenuKey">'+t.key+"</span></div>"},"");s.id="kbshortcutmenu",s.innerHTML="<h1>Keyboard Shortcuts</h1>"+o+"</div>",n(t,s,"0","0","0",null)}}var r=e("ace/editor").Editor;n.exports.init=function(e){r.prototype.showKeyboardShortcuts=function(){i(this)},e.commands.addCommands([{name:"showKeyboardShortcuts",bindKey:{win:"Ctrl-Alt-h",mac:"Command-Alt-h"},exec:function(e,t){e.showKeyboardShortcuts()}}])}});
(function() {
window.require(["ace/ext/keybinding_menu"], function() {});
define("ace/ext/menu_tools/overlay_page",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";var r=e("../../lib/dom"),i="#ace_settingsmenu, #kbshortcutmenu {background-color: #F7F7F7;color: black;box-shadow: -5px 4px 5px rgba(126, 126, 126, 0.55);padding: 1em 0.5em 2em 1em;overflow: auto;position: absolute;margin: 0;bottom: 0;right: 0;top: 0;z-index: 9991;cursor: default;}.ace_dark #ace_settingsmenu, .ace_dark #kbshortcutmenu {box-shadow: -20px 10px 25px rgba(126, 126, 126, 0.25);background-color: rgba(255, 255, 255, 0.6);color: black;}.ace_optionsMenuEntry:hover {background-color: rgba(100, 100, 100, 0.1);transition: all 0.3s}.ace_closeButton {background: rgba(245, 146, 146, 0.5);border: 1px solid #F48A8A;border-radius: 50%;padding: 7px;position: absolute;right: -8px;top: -8px;z-index: 100000;}.ace_closeButton{background: rgba(245, 146, 146, 0.9);}.ace_optionsMenuKey {color: darkslateblue;font-weight: bold;}.ace_optionsMenuCommand {color: darkcyan;font-weight: normal;}.ace_optionsMenuEntry input, .ace_optionsMenuEntry button {vertical-align: middle;}.ace_optionsMenuEntry button[ace_selected_button=true] {background: #e7e7e7;box-shadow: 1px 0px 2px 0px #adadad inset;border-color: #adadad;}.ace_optionsMenuEntry button {background: white;border: 1px solid lightgray;margin: 0px;}.ace_optionsMenuEntry button:hover{background: #f0f0f0;}";r.importCssString(i),n.exports.overlayPage=function(t,n,r){function o(e){e.keyCode===27&&u()}function u(){if(!i)return;document.removeEventListener("keydown",o),i.parentNode.removeChild(i),t&&t.focus(),i=null,r&&r()}function a(e){s=e,e&&(i.style.pointerEvents="none",n.style.pointerEvents="auto")}var i=document.createElement("div"),s=!1;return i.style.cssText="margin: 0; padding: 0; position: fixed; top:0; bottom:0; left:0; right:0;z-index: 9990; "+(t?"background-color: rgba(0, 0, 0, 0.3);":""),i.addEventListener("click",function(e){s||u()}),document.addEventListener("keydown",o),n.addEventListener("click",function(e){e.stopPropagation()}),i.appendChild(n),document.body.appendChild(i),t&&t.blur(),{close:u,setIgnoreFocusOut:a}}}),define("ace/ext/menu_tools/get_editor_keyboard_shortcuts",["require","exports","module","ace/lib/keys"],function(e,t,n){"use strict";var r=e("../../lib/keys");n.exports.getEditorKeybordShortcuts=function(e){var t=r.KEY_MODS,n=[],i={};return e.keyBinding.$handlers.forEach(function(e){var t=e.commandKeyBinding;for(var r in t){var s=r.replace(/(^|-)\w/g,function(e){return e.toUpperCase()}),o=t[r];Array.isArray(o)||(o=[o]),o.forEach(function(e){typeof e!="string"&&(e=e.name),i[e]?i[e].key+="|"+s:(i[e]={key:s,command:e},n.push(i[e]))})}}),n}}),define("ace/ext/keybinding_menu",["require","exports","module","ace/editor","ace/ext/menu_tools/overlay_page","ace/ext/menu_tools/get_editor_keyboard_shortcuts"],function(e,t,n){"use strict";function i(t){if(!document.getElementById("kbshortcutmenu")){var n=e("./menu_tools/overlay_page").overlayPage,r=e("./menu_tools/get_editor_keyboard_shortcuts").getEditorKeybordShortcuts,i=r(t),s=document.createElement("div"),o=i.reduce(function(e,t){return e+'<div class="ace_optionsMenuEntry"><span class="ace_optionsMenuCommand">'+t.command+"</span> : "+'<span class="ace_optionsMenuKey">'+t.key+"</span></div>"},"");s.id="kbshortcutmenu",s.innerHTML="<h1>Keyboard Shortcuts</h1>"+o+"</div>",n(t,s)}}var r=e("../editor").Editor;n.exports.init=function(e){r.prototype.showKeyboardShortcuts=function(){i(this)},e.commands.addCommands([{name:"showKeyboardShortcuts",bindKey:{win:"Ctrl-Alt-h",mac:"Command-Alt-h"},exec:function(e,t){e.showKeyboardShortcuts()}}])}}); (function() {
window.require(["ace/ext/keybinding_menu"], function(m) {
if (typeof module == "object" && typeof exports == "object" && module) {
module.exports = m;
}
});
})();

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,8 @@
define("ace/ext/linking",["require","exports","module","ace/editor","ace/config"],function(e,t,n){function i(e){var n=e.editor,r=e.getAccelKey();if(r){var n=e.editor,i=e.getDocumentPosition(),s=n.session,o=s.getTokenAt(i.row,i.column);t.previousLinkingHover&&t.previousLinkingHover!=o&&n._emit("linkHoverOut"),n._emit("linkHover",{position:i,token:o}),t.previousLinkingHover=o}else t.previousLinkingHover&&(n._emit("linkHoverOut"),t.previousLinkingHover=!1)}function s(e){var t=e.getAccelKey(),n=e.getButton();if(n==0&&t){var r=e.editor,i=e.getDocumentPosition(),s=r.session,o=s.getTokenAt(i.row,i.column);r._emit("linkClick",{position:i,token:o})}}var r=e("ace/editor").Editor;e("../config").defineOptions(r.prototype,"editor",{enableLinking:{set:function(e){e?(this.on("click",s),this.on("mousemove",i)):(this.off("click",s),this.off("mousemove",i))},value:!1}}),t.previousLinkingHover=!1});
(function() {
window.require(["ace/ext/linking"], function() {});
define("ace/ext/linking",["require","exports","module","ace/editor","ace/config"],function(e,t,n){function i(e){var n=e.editor,r=e.getAccelKey();if(r){var n=e.editor,i=e.getDocumentPosition(),s=n.session,o=s.getTokenAt(i.row,i.column);t.previousLinkingHover&&t.previousLinkingHover!=o&&n._emit("linkHoverOut"),n._emit("linkHover",{position:i,token:o}),t.previousLinkingHover=o}else t.previousLinkingHover&&(n._emit("linkHoverOut"),t.previousLinkingHover=!1)}function s(e){var t=e.getAccelKey(),n=e.getButton();if(n==0&&t){var r=e.editor,i=e.getDocumentPosition(),s=r.session,o=s.getTokenAt(i.row,i.column);r._emit("linkClick",{position:i,token:o})}}var r=e("../editor").Editor;e("../config").defineOptions(r.prototype,"editor",{enableLinking:{set:function(e){e?(this.on("click",s),this.on("mousemove",i)):(this.off("click",s),this.off("mousemove",i))},value:!1}}),t.previousLinkingHover=!1}); (function() {
window.require(["ace/ext/linking"], function(m) {
if (typeof module == "object" && typeof exports == "object" && module) {
module.exports = m;
}
});
})();

View File

@ -1,5 +1,8 @@
define("ace/ext/modelist",["require","exports","module"],function(e,t,n){"use strict";function i(e){var t=a.text,n=e.split(/[\/\\]/).pop();for(var i=0;i<r.length;i++)if(r[i].supportsFile(n)){t=r[i];break}return t}var r=[],s=function(e,t,n){this.name=e,this.caption=t,this.mode="ace/mode/"+e,this.extensions=n;var r;/\^/.test(n)?r=n.replace(/\|(\^)?/g,function(e,t){return"$|"+(t?"^":"^.*\\.")})+"$":r="^.*\\.("+n+")$",this.extRe=new RegExp(r,"gi")};s.prototype.supportsFile=function(e){return e.match(this.extRe)};var o={ABAP:["abap"],ABC:["abc"],ActionScript:["as"],ADA:["ada|adb"],Apache_Conf:["^htaccess|^htgroups|^htpasswd|^conf|htaccess|htgroups|htpasswd"],AsciiDoc:["asciidoc|adoc"],Assembly_x86:["asm|a"],AutoHotKey:["ahk"],BatchFile:["bat|cmd"],Bro:["bro"],C_Cpp:["cpp|c|cc|cxx|h|hh|hpp|ino"],C9Search:["c9search_results"],Cirru:["cirru|cr"],Clojure:["clj|cljs"],Cobol:["CBL|COB"],coffee:["coffee|cf|cson|^Cakefile"],ColdFusion:["cfm"],CSharp:["cs"],Csound_Document:["csd"],Csound_Orchestra:["orc"],Csound_Score:["sco"],CSS:["css"],Curly:["curly"],D:["d|di"],Dart:["dart"],Diff:["diff|patch"],Dockerfile:["^Dockerfile"],Dot:["dot"],Drools:["drl"],Edifact:["edi"],Eiffel:["e|ge"],EJS:["ejs"],Elixir:["ex|exs"],Elm:["elm"],Erlang:["erl|hrl"],Forth:["frt|fs|ldr|fth|4th"],Fortran:["f|f90"],FTL:["ftl"],Gcode:["gcode"],Gherkin:["feature"],Gitignore:["^.gitignore"],Glsl:["glsl|frag|vert"],Gobstones:["gbs"],golang:["go"],GraphQLSchema:["gql"],Groovy:["groovy"],HAML:["haml"],Handlebars:["hbs|handlebars|tpl|mustache"],Haskell:["hs"],Haskell_Cabal:["cabal"],haXe:["hx"],Hjson:["hjson"],HTML:["html|htm|xhtml|vue|we|wpy"],HTML_Elixir:["eex|html.eex"],HTML_Ruby:["erb|rhtml|html.erb"],INI:["ini|conf|cfg|prefs"],Io:["io"],Jack:["jack"],Jade:["jade|pug"],Java:["java"],JavaScript:["js|jsm|jsx"],JSON:["json"],JSONiq:["jq"],JSP:["jsp"],JSSM:["jssm|jssm_state"],JSX:["jsx"],Julia:["jl"],Kotlin:["kt|kts"],LaTeX:["tex|latex|ltx|bib"],LESS:["less"],Liquid:["liquid"],Lisp:["lisp"],LiveScript:["ls"],LogiQL:["logic|lql"],LSL:["lsl"],Lua:["lua"],LuaPage:["lp"],Lucene:["lucene"],Makefile:["^Makefile|^GNUmakefile|^makefile|^OCamlMakefile|make"],Markdown:["md|markdown"],Mask:["mask"],MATLAB:["matlab"],Maze:["mz"],MEL:["mel"],MIXAL:["mixal"],MUSHCode:["mc|mush"],MySQL:["mysql"],Nix:["nix"],NSIS:["nsi|nsh"],ObjectiveC:["m|mm"],OCaml:["ml|mli"],Pascal:["pas|p"],Perl:["pl|pm"],pgSQL:["pgsql"],PHP:["php|phtml|shtml|php3|php4|php5|phps|phpt|aw|ctp|module"],Pig:["pig"],Powershell:["ps1"],Praat:["praat|praatscript|psc|proc"],Prolog:["plg|prolog"],Properties:["properties"],Protobuf:["proto"],Python:["py"],R:["r"],Razor:["cshtml|asp"],RDoc:["Rd"],Red:["red|reds"],RHTML:["Rhtml"],RST:["rst"],Ruby:["rb|ru|gemspec|rake|^Guardfile|^Rakefile|^Gemfile"],Rust:["rs"],SASS:["sass"],SCAD:["scad"],Scala:["scala"],Scheme:["scm|sm|rkt|oak|scheme"],SCSS:["scss"],SH:["sh|bash|^.bashrc"],SJS:["sjs"],Smarty:["smarty|tpl"],snippets:["snippets"],Soy_Template:["soy"],Space:["space"],SQL:["sql"],SQLServer:["sqlserver"],Stylus:["styl|stylus"],SVG:["svg"],Swift:["swift"],Tcl:["tcl"],Tex:["tex"],Text:["txt"],Textile:["textile"],Toml:["toml"],TSX:["tsx"],Twig:["twig|swig"],Typescript:["ts|typescript|str"],Vala:["vala"],VBScript:["vbs|vb"],Velocity:["vm"],Verilog:["v|vh|sv|svh"],VHDL:["vhd|vhdl"],Wollok:["wlk|wpgm|wtest"],XML:["xml|rdf|rss|wsdl|xslt|atom|mathml|mml|xul|xbl|xaml"],XQuery:["xq"],YAML:["yaml|yml"],Django:["html"]},u={ObjectiveC:"Objective-C",CSharp:"C#",golang:"Go",C_Cpp:"C and C++",Csound_Document:"Csound Document",Csound_Orchestra:"Csound",Csound_Score:"Csound Score",coffee:"CoffeeScript",HTML_Ruby:"HTML (Ruby)",HTML_Elixir:"HTML (Elixir)",FTL:"FreeMarker"},a={};for(var f in o){var l=o[f],c=(u[f]||f).replace(/_/g," "),h=f.toLowerCase(),p=new s(h,c,l[0]);a[h]=p,r.push(p)}n.exports={getModeForPath:i,modes:r,modesByName:a}});
(function() {
window.require(["ace/ext/modelist"], function() {});
define("ace/ext/modelist",["require","exports","module"],function(e,t,n){"use strict";function i(e){var t=a.text,n=e.split(/[\/\\]/).pop();for(var i=0;i<r.length;i++)if(r[i].supportsFile(n)){t=r[i];break}return t}var r=[],s=function(e,t,n){this.name=e,this.caption=t,this.mode="ace/mode/"+e,this.extensions=n;var r;/\^/.test(n)?r=n.replace(/\|(\^)?/g,function(e,t){return"$|"+(t?"^":"^.*\\.")})+"$":r="^.*\\.("+n+")$",this.extRe=new RegExp(r,"gi")};s.prototype.supportsFile=function(e){return e.match(this.extRe)};var o={ABAP:["abap"],ABC:["abc"],ActionScript:["as"],ADA:["ada|adb"],Alda:["alda"],Apache_Conf:["^htaccess|^htgroups|^htpasswd|^conf|htaccess|htgroups|htpasswd"],Apex:["apex|cls|trigger|tgr"],AQL:["aql"],AsciiDoc:["asciidoc|adoc"],ASL:["dsl|asl"],Assembly_x86:["asm|a"],AutoHotKey:["ahk"],BatchFile:["bat|cmd"],C_Cpp:["cpp|c|cc|cxx|h|hh|hpp|ino"],C9Search:["c9search_results"],Cirru:["cirru|cr"],Clojure:["clj|cljs"],Cobol:["CBL|COB"],coffee:["coffee|cf|cson|^Cakefile"],ColdFusion:["cfm"],Crystal:["cr"],CSharp:["cs"],Csound_Document:["csd"],Csound_Orchestra:["orc"],Csound_Score:["sco"],CSS:["css"],Curly:["curly"],D:["d|di"],Dart:["dart"],Diff:["diff|patch"],Dockerfile:["^Dockerfile"],Dot:["dot"],Drools:["drl"],Edifact:["edi"],Eiffel:["e|ge"],EJS:["ejs"],Elixir:["ex|exs"],Elm:["elm"],Erlang:["erl|hrl"],Forth:["frt|fs|ldr|fth|4th"],Fortran:["f|f90"],FSharp:["fsi|fs|ml|mli|fsx|fsscript"],FSL:["fsl"],FTL:["ftl"],Gcode:["gcode"],Gherkin:["feature"],Gitignore:["^.gitignore"],Glsl:["glsl|frag|vert"],Gobstones:["gbs"],golang:["go"],GraphQLSchema:["gql"],Groovy:["groovy"],HAML:["haml"],Handlebars:["hbs|handlebars|tpl|mustache"],Haskell:["hs"],Haskell_Cabal:["cabal"],haXe:["hx"],Hjson:["hjson"],HTML:["html|htm|xhtml|vue|we|wpy"],HTML_Elixir:["eex|html.eex"],HTML_Ruby:["erb|rhtml|html.erb"],INI:["ini|conf|cfg|prefs"],Io:["io"],Jack:["jack"],Jade:["jade|pug"],Java:["java"],JavaScript:["js|jsm|jsx"],JSON:["json"],JSON5:["json5"],JSONiq:["jq"],JSP:["jsp"],JSSM:["jssm|jssm_state"],JSX:["jsx"],Julia:["jl"],Kotlin:["kt|kts"],LaTeX:["tex|latex|ltx|bib"],LESS:["less"],Liquid:["liquid"],Lisp:["lisp"],LiveScript:["ls"],LogiQL:["logic|lql"],LSL:["lsl"],Lua:["lua"],LuaPage:["lp"],Lucene:["lucene"],Makefile:["^Makefile|^GNUmakefile|^makefile|^OCamlMakefile|make"],Markdown:["md|markdown"],Mask:["mask"],MATLAB:["matlab"],Maze:["mz"],MediaWiki:["wiki|mediawiki"],MEL:["mel"],MIXAL:["mixal"],MUSHCode:["mc|mush"],MySQL:["mysql"],Nginx:["nginx|conf"],Nim:["nim"],Nix:["nix"],NSIS:["nsi|nsh"],Nunjucks:["nunjucks|nunjs|nj|njk"],ObjectiveC:["m|mm"],OCaml:["ml|mli"],Pascal:["pas|p"],Perl:["pl|pm"],Perl6:["p6|pl6|pm6"],pgSQL:["pgsql"],PHP:["php|inc|phtml|shtml|php3|php4|php5|phps|phpt|aw|ctp|module"],PHP_Laravel_blade:["blade.php"],Pig:["pig"],Powershell:["ps1"],Praat:["praat|praatscript|psc|proc"],Prisma:["prisma"],Prolog:["plg|prolog"],Properties:["properties"],Protobuf:["proto"],Puppet:["epp|pp"],Python:["py"],QML:["qml"],R:["r"],Razor:["cshtml|asp"],RDoc:["Rd"],Red:["red|reds"],RHTML:["Rhtml"],RST:["rst"],Ruby:["rb|ru|gemspec|rake|^Guardfile|^Rakefile|^Gemfile"],Rust:["rs"],SASS:["sass"],SCAD:["scad"],Scala:["scala|sbt"],Scheme:["scm|sm|rkt|oak|scheme"],SCSS:["scss"],SH:["sh|bash|^.bashrc"],SJS:["sjs"],Slim:["slim|skim"],Smarty:["smarty|tpl"],snippets:["snippets"],Soy_Template:["soy"],Space:["space"],SQL:["sql"],SQLServer:["sqlserver"],Stylus:["styl|stylus"],SVG:["svg"],Swift:["swift"],Tcl:["tcl"],Terraform:["tf","tfvars","terragrunt"],Tex:["tex"],Text:["txt"],Textile:["textile"],Toml:["toml"],TSX:["tsx"],Twig:["latte|twig|swig"],Typescript:["ts|typescript|str"],Vala:["vala"],VBScript:["vbs|vb"],Velocity:["vm"],Verilog:["v|vh|sv|svh"],VHDL:["vhd|vhdl"],Visualforce:["vfp|component|page"],Wollok:["wlk|wpgm|wtest"],XML:["xml|rdf|rss|wsdl|xslt|atom|mathml|mml|xul|xbl|xaml"],XQuery:["xq"],YAML:["yaml|yml"],Zeek:["zeek|bro"],Django:["html"]},u={ObjectiveC:"Objective-C",CSharp:"C#",golang:"Go",C_Cpp:"C and C++",Csound_Document:"Csound Document",Csound_Orchestra:"Csound",Csound_Score:"Csound Score",coffee:"CoffeeScript",HTML_Ruby:"HTML (Ruby)",HTML_Elixir:"HTML (Elixir)",FTL:"FreeMarker",PHP_Laravel_blade:"PHP (Blade Template)",Perl6:"Perl 6",AutoHotKey:"AutoHotkey / AutoIt"},a={};for(var f in o){var l=o[f],c=(u[f]||f).replace(/_/g," "),h=f.toLowerCase(),p=new s(h,c,l[0]);a[h]=p,r.push(p)}n.exports={getModeForPath:i,modes:r,modesByName:a}}); (function() {
window.require(["ace/ext/modelist"], function(m) {
if (typeof module == "object" && typeof exports == "object" && module) {
module.exports = m;
}
});
})();

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More