├── alpha.bat ├── src ├── core │ ├── PropertyBinding.ts │ ├── Color.ts │ ├── IValueConverter.ts │ ├── KeyValuePairs.ts │ ├── IFetchEvent.ts │ ├── WebImage.ts │ ├── FormattedString.ts │ ├── IScreen.ts │ ├── MarkdownError.ts │ ├── ExtendControl.ts │ ├── AtomEnumerator.ts │ ├── FormattedError.ts │ ├── TransientDisposable.ts │ ├── InjectProperty.ts │ ├── Markdown.ts │ ├── sleep.ts │ ├── WatchProperty.ts │ ├── AtomDisposableList.ts │ ├── CancelTokenFactory.ts │ ├── Defer.ts │ ├── SingleInvoker.ts │ ├── AtomDispatcher.ts │ ├── PropertyMap.ts │ ├── BindableProperty.ts │ ├── AtomOnce.ts │ ├── StringHelper.ts │ ├── AtomMap.ts │ ├── InheritedProperty.ts │ └── AtomUri.ts ├── web │ ├── images │ │ ├── busy.gif │ │ ├── Busy.ts │ │ ├── Button.ts │ │ ├── CloseButton.ts │ │ ├── CloseButtonHover.ts │ │ ├── close-button-hover.svg │ │ ├── close-button.svg │ │ ├── CloseButtonHoverDataUrl.ts │ │ ├── ButtonDataUrl.ts │ │ └── CloseButtonDataUrl.ts │ ├── controls │ │ ├── AtomTemplate.ts │ │ ├── AtomPage.ts │ │ ├── AtomTemplateControl.ts │ │ ├── AtomViewStack.ts │ │ ├── AtomToggleButtonBar.ts │ │ ├── AtomContentControl.ts │ │ ├── AtomNotification.tsx │ │ ├── AtomViewPager.ts │ │ ├── AtomListBox.ts │ │ ├── AtomGridSplitter.ts │ │ └── AtomComboBox.ts │ ├── samples │ │ ├── tabs │ │ │ ├── views │ │ │ │ ├── List.json │ │ │ │ ├── List.ts │ │ │ │ ├── ListDataUrl.ts │ │ │ │ ├── TabHost.ts │ │ │ │ └── Page1.tsx │ │ │ └── app.ts │ │ ├── MovieService.ts │ │ ├── demo │ │ │ ├── app.ts │ │ │ └── views │ │ │ │ ├── MovieListViewModel.ts │ │ │ │ └── MovieList.ts │ │ └── window │ │ │ └── WindowSample.tsx │ ├── styles │ │ ├── AtomPopupStyle.ts │ │ ├── AtomPageLinkStyle.ts │ │ ├── AtomFrameStyle.ts │ │ ├── AtomNotificationStyle.ts │ │ ├── AtomListBoxStyle.ts │ │ ├── AtomAlertWindowStyle.ts │ │ ├── AtomTheme.ts │ │ ├── AtomStyleSheet.ts │ │ ├── CSS.ts │ │ ├── AtomToggleButtonBarStyle.ts │ │ └── StyleBuilder.ts │ └── services │ │ ├── NotificationPopup.tsx │ │ ├── MarkdownService.ts │ │ └── WebBusyIndicatorService.ts ├── Pack.ts ├── di │ ├── IMockOrInject.ts │ ├── IServiceProvider.ts │ ├── RegisterScoped.ts │ ├── RegisterSingleton.ts │ ├── DISingleton.ts │ ├── DITransient.ts │ ├── TypeKey.ts │ ├── ServiceCollection.ts │ └── Register.ts ├── index.d.ts ├── tests │ ├── web │ │ ├── styles │ │ │ ├── TestFrameStyle.ts │ │ │ └── StyleBuilderTest.ts │ │ ├── services │ │ │ └── MarkdownServiceTest.ts │ │ ├── sample │ │ │ └── PageSample.tsx │ │ ├── controls │ │ │ ├── AtomControlDataTest.ts │ │ │ ├── AtomWindowTest.ts │ │ │ ├── AtomGridViewTests.ts │ │ │ ├── AtomControlPropertiesTest.ts │ │ │ └── AtomControlStyleTest.ts │ │ ├── view-models │ │ │ └── test.ts │ │ ├── core │ │ │ └── AtomUITest.ts │ │ └── window │ │ │ └── WindowTest.tsx │ ├── core │ │ ├── AtomUriTest.ts │ │ ├── AtomDisposableListTest.ts │ │ ├── PropertyBinderTest.ts │ │ ├── StringHelperTests.ts │ │ ├── AtomOnceTest.ts │ │ ├── ColorTests.ts │ │ ├── RouteTest.ts │ │ └── AtomListTests.ts │ ├── di │ │ ├── DIGlobalTest.ts │ │ ├── tests.ts │ │ └── InjectTest.ts │ ├── view-model │ │ ├── CancelTokenFactoryTest.ts │ │ ├── ActionTest.ts │ │ └── ParentViewModelTest.ts │ ├── AppTest.ts │ ├── services │ │ └── CacheServiceTest.ts │ └── AtomClassTest.ts ├── services │ ├── http │ │ ├── JsonError.ts │ │ └── AjaxOptions.ts │ ├── BusyIndicatorService.ts │ ├── ReferenceService.ts │ └── CacheService.ts ├── view-model │ ├── baseTypes.ts │ ├── BindableUrlParameter.ts │ ├── bindProperty.ts │ ├── Delay.ts │ ├── bindPromise.ts │ ├── Disposable.ts │ ├── Once.ts │ ├── bindUrlParameter.ts │ └── AtomWindowViewModel.ts ├── unit │ ├── AtomTest.ts │ └── AtomWebTest.ts ├── xf │ ├── services │ │ └── XFBusyIndicatorService.ts │ └── XFApp.ts ├── MockApp.ts └── test.ts ├── .gitignore ├── typedoc.js ├── .npmignore ├── core-docs ├── change-name.js └── undo.js ├── tslint.json ├── .travis.yml ├── tsconfig.json ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── package.json ├── .github └── workflows │ └── node.yml ├── README.md └── generated-cert-1 /alpha.bat: -------------------------------------------------------------------------------- 1 | npm version prerelease --preid=alpha 2 | -------------------------------------------------------------------------------- /src/core/PropertyBinding.ts: -------------------------------------------------------------------------------- 1 | export { PropertyBinding } from "./AtomComponent"; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | dist 3 | node_modules 4 | coverage 5 | html-report 6 | docs/ 7 | -------------------------------------------------------------------------------- /typedoc.js: -------------------------------------------------------------------------------- 1 | var td = require('typedoc/dist/lib/cli.js'); 2 | new td.CliApplication(); 3 | -------------------------------------------------------------------------------- /src/web/images/busy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web-atoms/core/HEAD/src/web/images/busy.gif -------------------------------------------------------------------------------- /src/Pack.ts: -------------------------------------------------------------------------------- 1 | export default function Pack(... a: any[]): any { 2 | // used for packing.. don't do anything.. 3 | } 4 | -------------------------------------------------------------------------------- /src/core/Color.ts: -------------------------------------------------------------------------------- 1 | import { ColorItem } from "./Colors"; 2 | 3 | type Color = string | ColorItem; 4 | 5 | export default Color; 6 | -------------------------------------------------------------------------------- /src/core/IValueConverter.ts: -------------------------------------------------------------------------------- 1 | export interface IValueConverter { 2 | fromSource(v: any): any; 3 | fromTarget(v: any): any; 4 | } 5 | -------------------------------------------------------------------------------- /src/di/IMockOrInject.ts: -------------------------------------------------------------------------------- 1 | export interface IMockOrInject { 2 | mock?: string; 3 | inject?: string; 4 | globalVar?: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/core/KeyValuePairs.ts: -------------------------------------------------------------------------------- 1 | export interface IKeyValuePair { 2 | key: string; 3 | value: string; 4 | } 5 | 6 | export type KeyValuePairs = IKeyValuePair[]; 7 | -------------------------------------------------------------------------------- /src/core/IFetchEvent.ts: -------------------------------------------------------------------------------- 1 | import { CancelToken } from "./types"; 2 | 3 | export default interface IFetchEvent { 4 | search?: string; 5 | value?: any; 6 | cancel?: CancelToken; 7 | } 8 | -------------------------------------------------------------------------------- /src/di/IServiceProvider.ts: -------------------------------------------------------------------------------- 1 | import { IClassOf } from "../core/types"; 2 | 3 | export interface IServiceProvider { 4 | 5 | resolve(c: string | IClassOf, create?: boolean ): T; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/web/controls/AtomTemplate.ts: -------------------------------------------------------------------------------- 1 | import { AtomControl } from "./AtomControl"; 2 | 3 | export class AtomTemplate extends AtomControl { 4 | 5 | public contentPresenter: HTMLElement; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/web/samples/tabs/views/List.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "label": "Movie 1", 4 | "value": "movie1" 5 | }, 6 | { 7 | "label": "Movie 2", 8 | "value": "movie2" 9 | } 10 | ] -------------------------------------------------------------------------------- /src/core/WebImage.ts: -------------------------------------------------------------------------------- 1 | export default class WebImage { 2 | 3 | constructor(public readonly url: string) { 4 | 5 | } 6 | 7 | public toString() { 8 | return this.url; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/web/images/Busy.ts: -------------------------------------------------------------------------------- 1 | import WebImage from "../../core/WebImage"; 2 | 3 | // tslint:disable 4 | declare var UMD: any; 5 | export default new WebImage(UMD.resolvePath("web-atoms-core/dist/web/images/busy.gif")); 6 | -------------------------------------------------------------------------------- /src/web/images/Button.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable 2 | import WebImage from "../../core/WebImage"; 3 | declare var UMD: any; 4 | export default new WebImage(UMD.resolvePath("web-atoms-core/dist/web/images/close-button.svg")); 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | html-report 4 | package-lock.json 5 | dist/tests 6 | src/tests 7 | .github 8 | .vscode 9 | core-docs 10 | tslint.json 11 | typedoc.js 12 | .travis.yml 13 | alpha.bat 14 | generated-cert-1 15 | -------------------------------------------------------------------------------- /src/web/images/CloseButton.ts: -------------------------------------------------------------------------------- 1 | import WebImage from "../../core/WebImage"; 2 | 3 | // tslint:disable 4 | declare var UMD: any; 5 | export default new WebImage(UMD.resolvePath("web-atoms-core/dist/web/images/close-button.svg")); 6 | -------------------------------------------------------------------------------- /src/web/images/CloseButtonHover.ts: -------------------------------------------------------------------------------- 1 | import WebImage from "../../core/WebImage"; 2 | 3 | // tslint:disable 4 | declare var UMD: any; 5 | export default new WebImage(UMD.resolvePath("web-atoms-core/dist/web/images/close-button-hover.svg")); 6 | -------------------------------------------------------------------------------- /src/web/samples/tabs/views/List.ts: -------------------------------------------------------------------------------- 1 | import WebImage from "../../../../core/WebImage"; 2 | 3 | // tslint:disable 4 | declare var UMD: any; 5 | export default new WebImage(UMD.resolvePath("web-atoms-core/dist/web/samples/tabs/views/List.json")); 6 | -------------------------------------------------------------------------------- /src/di/RegisterScoped.ts: -------------------------------------------------------------------------------- 1 | import { IClassOf } from "../core/types"; 2 | import { Register } from "./Register"; 3 | import { Scope } from "./ServiceCollection"; 4 | 5 | export function RegisterScoped(id: any): any { 6 | Register({scope: Scope.Scoped})(id); 7 | } 8 | -------------------------------------------------------------------------------- /src/di/RegisterSingleton.ts: -------------------------------------------------------------------------------- 1 | import { IClassOf } from "../core/types"; 2 | import { Register } from "./Register"; 3 | import { Scope } from "./ServiceCollection"; 4 | 5 | export function RegisterSingleton(target: any): void { 6 | Register({scope: Scope.Global})(target); 7 | } 8 | -------------------------------------------------------------------------------- /core-docs/change-name.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | const p = JSON.parse(fs.readFileSync("package.json", "utf8")); 4 | p.name = "@web-atoms/core-docs"; 5 | fs.copyFileSync("package.json", "package-original.json"); 6 | fs.writeFileSync("package.json", JSON.stringify(p)); 7 | 8 | -------------------------------------------------------------------------------- /src/core/FormattedString.ts: -------------------------------------------------------------------------------- 1 | import { App } from "../App"; 2 | 3 | export default abstract class FormattedString { 4 | 5 | constructor(public readonly text: string) { 6 | } 7 | 8 | public abstract applyTo(app: App, element: any); 9 | 10 | public abstract toHtmlString(app: App): string; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/core/IScreen.ts: -------------------------------------------------------------------------------- 1 | export type IScreenType = "mobile" | "tablet" | "desktop"; 2 | 3 | export interface IScreen { 4 | width?: number; 5 | height?: number; 6 | scrollLeft?: number; 7 | scrollTop?: number; 8 | orientation?: "portrait" | "landscape"; 9 | screenType?: IScreenType; 10 | } 11 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "max-classes-per-file":false, 9 | "object-literal-sort-keys":false, 10 | "trailing-comma":false, 11 | "no-debugger": true 12 | }, 13 | "rulesDirectory": [] 14 | } -------------------------------------------------------------------------------- /src/core/MarkdownError.ts: -------------------------------------------------------------------------------- 1 | import FormattedError from "./FormattedError"; 2 | import Markdown from "./Markdown"; 3 | 4 | export default class MarkdownError extends FormattedError { 5 | constructor(text: string) { 6 | const a = super(Markdown.from(text)) as any; 7 | a.__proto__ = MarkdownError.prototype; 8 | return a; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/di/DISingleton.ts: -------------------------------------------------------------------------------- 1 | import { IMockOrInject } from "./IMockOrInject"; 2 | import { Register } from "./Register"; 3 | import { Scope } from "./ServiceCollection"; 4 | 5 | export default function DISingleton(mockOrInject?: IMockOrInject): ((target: any) => void) { 6 | return (target: any): void => { 7 | Register({ scope: Scope.Global, mockOrInject })(target); 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/di/DITransient.ts: -------------------------------------------------------------------------------- 1 | import { IMockOrInject } from "./IMockOrInject"; 2 | import { Register } from "./Register"; 3 | import { Scope } from "./ServiceCollection"; 4 | 5 | export default function DITransient(mockOrInject?: IMockOrInject): ((target: any) => void) { 6 | return (target: any): void => { 7 | Register({scope: Scope.Transient, mockOrInject})(target); 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/web/samples/MovieService.ts: -------------------------------------------------------------------------------- 1 | import { RegisterSingleton } from "../../di/RegisterSingleton"; 2 | import { BaseService, Get } from "../../services/http/RestService"; 3 | 4 | @RegisterSingleton 5 | export class MovieService extends BaseService { 6 | 7 | @Get("@web-atoms/core/dist/web/samples/tabs/views/List.json") 8 | public async countryList(): Promise { 9 | return null; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/web/samples/tabs/views/ListDataUrl.ts: -------------------------------------------------------------------------------- 1 | import WebImage from "../../../../core/WebImage"; 2 | 3 | // tslint:disable 4 | 5 | const base64 = ["Ww0KICAgIHsNCiAgICAgICAgImxhYmVsIjogIk1vdmllIDEiLA0KICAgICAgICAidmFsdWUiOiAibW92", 6 | "aWUxIg0KICAgIH0sDQogICAgew0KICAgICAgICAibGFiZWwiOiAiTW92aWUgMiIsDQogICAgICAgICJ2", 7 | "YWx1ZSI6ICJtb3ZpZTIiDQogICAgfQ0KXQ=="]; 8 | 9 | export default new WebImage(`data:image/jpeg;base64,${base64.join("")}`); 10 | -------------------------------------------------------------------------------- /src/web/controls/AtomPage.ts: -------------------------------------------------------------------------------- 1 | import { BindableProperty } from "../../core/BindableProperty"; 2 | import { AtomControl } from "./AtomControl"; 3 | 4 | export class AtomPage extends AtomControl { 5 | 6 | public title: string; 7 | 8 | public tag: string; 9 | 10 | public preCreate(): void { 11 | this.title = null; 12 | this.tag = null; 13 | this.bind(this.element, "title", [["viewModel", "title"]]); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/web/styles/AtomPopupStyle.ts: -------------------------------------------------------------------------------- 1 | import { AtomStyle } from "../styles/AtomStyle"; 2 | import { IStyleDeclaration } from "./IStyleDeclaration"; 3 | export class AtomPopupStyle extends AtomStyle { 4 | 5 | public get root(): IStyleDeclaration { 6 | return { 7 | backgroundColor: "white", 8 | border: "solid 1px lightgray", 9 | padding: "5px", 10 | borderRadius: "5px" 11 | }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10.16" 4 | install: 5 | - npm install 6 | - npm install typescript 7 | - npm install codecov -g 8 | cache: 9 | directories: 10 | - "node_modules" 11 | before_script: 12 | - tsc 13 | script: 14 | - ./node_modules/.bin/istanbul cover ./dist/test.js 15 | - ./node_modules/.bin/remap-istanbul -i ./coverage/coverage.json -t json -o ./coverage/coverage.json 16 | after_success: 17 | - codecov 18 | -------------------------------------------------------------------------------- /src/web/samples/demo/app.ts: -------------------------------------------------------------------------------- 1 | import WebApp from "../../../web/WebApp"; 2 | import { MovieList } from "./views/MovieList"; 3 | import { MovieListViewModel } from "./views/MovieListViewModel"; 4 | 5 | export class SampleApp extends WebApp { 6 | 7 | public main(): void { 8 | const ml = new MovieList(this); 9 | ml.viewModel = this.get(MovieListViewModel); 10 | document.body.appendChild(ml.element); 11 | } 12 | 13 | } 14 | 15 | const app = new SampleApp(); 16 | -------------------------------------------------------------------------------- /src/web/styles/AtomPageLinkStyle.ts: -------------------------------------------------------------------------------- 1 | import { AtomStyle } from "./AtomStyle"; 2 | import { IStyleDeclaration } from "./IStyleDeclaration"; 3 | 4 | export default class AtomPageLinkStyle extends AtomStyle { 5 | 6 | public get root(): IStyleDeclaration { 7 | return { 8 | subclasses: { 9 | ".page": this.page 10 | } 11 | }; 12 | } 13 | 14 | public get page(): IStyleDeclaration { 15 | return {}; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/core/ExtendControl.ts: -------------------------------------------------------------------------------- 1 | type IAtomControl = new (... args: any[]) => {}; 2 | 3 | export default function ExtendControl(ctrl:T) { 4 | 5 | abstract class ExtendedControl extends ctrl { 6 | constructor(... args: any[]) { 7 | super(... args); 8 | (this as any).runAfterInit(() => (this as any).app.runAsync(() => this.init())); 9 | } 10 | 11 | abstract init(): Promise; 12 | } 13 | return ExtendedControl; 14 | } 15 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" { 2 | const value: string; 3 | export default value; 4 | } 5 | 6 | declare module "*.jpg" { 7 | const value: string; 8 | export default value; 9 | } 10 | 11 | declare module "*.jpeg" { 12 | const value: string; 13 | export default value; 14 | } 15 | 16 | declare module "*.gif" { 17 | const value: string; 18 | export default value; 19 | } 20 | 21 | declare module "*.svg" { 22 | const value: string; 23 | export default value; 24 | } 25 | -------------------------------------------------------------------------------- /src/core/AtomEnumerator.ts: -------------------------------------------------------------------------------- 1 | export default class AtomEnumerator { 2 | 3 | private index: number = -1; 4 | 5 | constructor(private readonly items: T[]) { 6 | } 7 | 8 | public next(): boolean { 9 | this.index ++; 10 | return this.index < this.items.length; 11 | } 12 | 13 | public get current(): T { 14 | return this.items[this.index]; 15 | } 16 | 17 | public get currentIndex(): number { 18 | return this.index; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/tests/web/styles/TestFrameStyle.ts: -------------------------------------------------------------------------------- 1 | import { AtomStyle } from "../../../web/styles/AtomStyle"; 2 | import { IStyleDeclaration } from "../../../web/styles/IStyleDeclaration"; 3 | import StyleBuilder from "../../../web/styles/StyleBuilder"; 4 | 5 | export default class TestFrameStyle extends AtomStyle { 6 | 7 | public get root(): IStyleDeclaration { 8 | return { 9 | ... StyleBuilder.newStyle.size(800, 800).toStyle(), 10 | position: "absolute" 11 | }; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/core/FormattedError.ts: -------------------------------------------------------------------------------- 1 | import FormattedString from "./FormattedString"; 2 | 3 | export default class FormattedError implements Error { 4 | public name: string; 5 | public message: string; 6 | public stack?: string; 7 | 8 | public readonly formattedMessage: FormattedString; 9 | 10 | constructor(msg: FormattedString) { 11 | const e = new Error(msg.toString()); 12 | (e as any).formattedMessage = msg; 13 | (e as any).__proto__ = FormattedError.prototype; 14 | return (e as any); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/services/http/JsonError.ts: -------------------------------------------------------------------------------- 1 | export default class JsonError extends Error { 2 | 3 | constructor( 4 | message: string, 5 | public readonly json: any) { 6 | super(message); 7 | } 8 | 9 | public get errors(): Array<{name: string, reason: string}> { 10 | return this.json.paramErrors ?? []; 11 | } 12 | 13 | public get details() { 14 | if (this.json.paramErrors) { 15 | return this.errors.map((x) => `${x.name}: ${x.reason}`).join("\n"); 16 | } 17 | return this.json?.details; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/view-model/baseTypes.ts: -------------------------------------------------------------------------------- 1 | import { IDisposable } from "../core/types"; 2 | 3 | export interface IAtomViewModel { 4 | setupWatch(ft: () => any, proxy?: () => any, forValidation?: boolean, name?: string): IDisposable ; 5 | } 6 | 7 | export type viewModelInit = (vm: any) => void; 8 | 9 | export type viewModelInitFunc = (target: any, key: string | symbol) => void; 10 | 11 | export function registerInit(target: any, fx: viewModelInit ): void { 12 | const t: any = target as any; 13 | const inits: viewModelInit[] = t._$_inits = t._$_inits || []; 14 | inits.push(fx); 15 | } 16 | -------------------------------------------------------------------------------- /src/services/BusyIndicatorService.ts: -------------------------------------------------------------------------------- 1 | import { IDisposable } from "../core/types"; 2 | import { RegisterSingleton } from "../di/RegisterSingleton"; 3 | 4 | export interface IBackgroundTaskInfo { 5 | title?: string; 6 | description?: string; 7 | icon?: string; 8 | } 9 | 10 | @RegisterSingleton 11 | export class BusyIndicatorService { 12 | 13 | public createIndicator(info?: IBackgroundTaskInfo): IDisposable { 14 | return { 15 | dispose() { 16 | // do nothing. 17 | } 18 | }; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/services/http/AjaxOptions.ts: -------------------------------------------------------------------------------- 1 | import { CancelToken, INameValues } from "../../core/types"; 2 | export class AjaxOptions { 3 | public dataType?: string; 4 | public contentType?: string; 5 | public method?: string; 6 | public url?: string; 7 | public data?: any; 8 | public cancel?: CancelToken; 9 | public headers?: INameValues; 10 | public cache?: any; 11 | public attachments?: any[]; 12 | public responseText?: string; 13 | public responseHeaders?: string; 14 | public status?: number; 15 | public responseType?: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/view-model/BindableUrlParameter.ts: -------------------------------------------------------------------------------- 1 | import { BindableProperty } from "../core/BindableProperty"; 2 | import { AtomViewModel } from "./AtomViewModel"; 3 | import { registerInit } from "./baseTypes"; 4 | import bindUrlParameter from "./bindUrlParameter"; 5 | 6 | export default function BindableUrlParameter(name: string): any { 7 | return (target: AtomViewModel, key: string | string, descriptor: PropertyDecorator): void => { 8 | registerInit(target, (vm) => { 9 | bindUrlParameter(vm, key, name); 10 | }); 11 | return BindableProperty(target, key); 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/unit/AtomTest.ts: -------------------------------------------------------------------------------- 1 | import TestItem from "@web-atoms/unit-test/dist/TestItem"; 2 | import { IClassOf } from "../core/types"; 3 | import { MockApp } from "../MockApp"; 4 | import { AtomViewModel, waitForReady } from "../view-model/AtomViewModel"; 5 | 6 | export class AtomTest extends TestItem { 7 | 8 | constructor(protected readonly app: MockApp = new MockApp()) { 9 | super(); 10 | } 11 | 12 | public async createViewModel(c: IClassOf): Promise { 13 | const vm = this.app.resolve(c, true); 14 | await waitForReady(vm); 15 | return vm; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/core/TransientDisposable.ts: -------------------------------------------------------------------------------- 1 | import { IDisposable } from "./types"; 2 | 3 | export default abstract class TransientDisposable implements IDisposable { 4 | 5 | constructor(owner?: any) { 6 | if (owner) { 7 | this.registerIn(owner); 8 | } 9 | } 10 | 11 | public abstract dispose(); 12 | 13 | public registerIn(value: any) { 14 | const v = value.disposables; 15 | if (v) { 16 | v.push(this); 17 | } else { 18 | if (value.registerDisposable) { 19 | value.registerDisposable(this); 20 | } 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/tests/web/services/MarkdownServiceTest.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Test from "@web-atoms/unit-test/dist/Test"; 3 | import TestItem from "@web-atoms/unit-test/dist/TestItem"; 4 | import MarkdownService from "../../../web/services/MarkdownService"; 5 | 6 | export default class MarkdownServiceTest extends TestItem { 7 | 8 | @Test 9 | public test(): void { 10 | const ms = new MarkdownService(); 11 | Assert.equals("a b", ms.toHtml("_a_ __b__")); 12 | Assert.equals("a nl
b", ms.toHtml("_a_ nl\n __b__")); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/web/samples/window/WindowSample.tsx: -------------------------------------------------------------------------------- 1 | import Bind from "../../../core/Bind"; 2 | import XNode from "../../../core/XNode"; 3 | import { NavigationService } from "../../../services/NavigationService"; 4 | import { AtomControl } from "../../controls/AtomControl"; 5 | 6 | export default class WindowSample extends AtomControl { 7 | 8 | public create() { 9 | const ns = this.resolve(NavigationService); 10 | this.render(
11 | 12 | 13 |
); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/xf/services/XFBusyIndicatorService.ts: -------------------------------------------------------------------------------- 1 | import { AtomBridge } from "../../core/AtomBridge"; 2 | import { IDisposable } from "../../core/types"; 3 | import { RegisterSingleton } from "../../di/RegisterSingleton"; 4 | import { BusyIndicatorService } from "../../services/BusyIndicatorService"; 5 | 6 | @RegisterSingleton 7 | export default class XFBusyIndicatorService extends BusyIndicatorService { 8 | 9 | public createIndicator(): IDisposable { 10 | const popup = AtomBridge.instance.createBusyIndicator(); 11 | return { 12 | dispose: () => { 13 | popup.dispose(); 14 | } 15 | }; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/tests/web/styles/StyleBuilderTest.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Test from "@web-atoms/unit-test/dist/Test"; 3 | import { AtomTest } from "../../../unit/AtomTest"; 4 | import StyleBuilder from "../../../web/styles/StyleBuilder"; 5 | 6 | export default class StyleBuilderTest extends AtomTest { 7 | 8 | @Test 9 | public size(): void { 10 | const s = StyleBuilder.newStyle 11 | .size(100, 100) 12 | .toStyle(); 13 | Assert.equals("100px", s.width); 14 | Assert.equals("100px", s.height); 15 | } 16 | 17 | @Test 18 | public border(): void { 19 | // 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /core-docs/undo.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const Path = require("path"); 3 | 4 | fs.unlinkSync("package.json"); 5 | fs.renameSync("package-original.json", "package.json"); 6 | 7 | const deleteFolderRecursive = function(path) { 8 | if (fs.existsSync(path)) { 9 | fs.readdirSync(path).forEach((file, index) => { 10 | const curPath = Path.join(path, file); 11 | if (fs.lstatSync(curPath).isDirectory()) { 12 | // recursive 13 | deleteFolderRecursive(curPath); 14 | } else { // delete file 15 | fs.unlinkSync(curPath); 16 | } 17 | }); 18 | fs.rmdirSync(path); 19 | } 20 | }; 21 | 22 | deleteFolderRecursive("./docs"); 23 | -------------------------------------------------------------------------------- /src/web/styles/AtomFrameStyle.ts: -------------------------------------------------------------------------------- 1 | import { AtomStyle } from "./AtomStyle"; 2 | import { IStyleDeclaration } from "./IStyleDeclaration"; 3 | import StyleBuilder from "./StyleBuilder"; 4 | 5 | export default class AtomFrameStyle extends AtomStyle { 6 | 7 | public get root(): IStyleDeclaration { 8 | return { 9 | ... StyleBuilder.newStyle.absolute(0, 0).toStyle(), 10 | width: "100%", 11 | height: "100%", 12 | subclasses: { 13 | " > *": { 14 | ... StyleBuilder.newStyle.absolute(0, 0).toStyle(), 15 | width: "100%", 16 | height: "100%", 17 | } 18 | } 19 | }; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/core/InjectProperty.ts: -------------------------------------------------------------------------------- 1 | import { AtomComponent } from "./AtomComponent"; 2 | 3 | export default function InjectProperty(target: AtomComponent, key: string): void { 4 | 5 | Object.defineProperty(target, key, { 6 | get: function() { 7 | const plist = (Reflect as any).getMetadata("design:type", target, key); 8 | const result = this.app.resolve(plist); 9 | // get is compatible with AtomWatcher 10 | // as it will ignore getter and it will 11 | // not try to set a binding refresher 12 | Object.defineProperty(this, key, { 13 | get: () => result 14 | }); 15 | return result; 16 | }, 17 | configurable: true 18 | }); 19 | 20 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module":"umd", 5 | "incremental": true, 6 | "sourceMap": true, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "moduleResolution": "node", 10 | "outDir": "dist", 11 | "downlevelIteration": false, 12 | "experimentalDecorators": true, 13 | "emitDecoratorMetadata": true, 14 | "importHelpers": true, 15 | "jsx": "react", 16 | "jsxFactory": "XNode.create", 17 | "lib": [ 18 | "es6", 19 | "dom" 20 | ] 21 | }, 22 | "include": [ 23 | "src/**/*" 24 | ], 25 | "exclude": [ 26 | "node_modules", 27 | "tests" 28 | ] 29 | } -------------------------------------------------------------------------------- /src/web/samples/tabs/app.ts: -------------------------------------------------------------------------------- 1 | import { NavigationService } from "../../../services/NavigationService"; 2 | import { AtomTabbedPage } from "../../controls/AtomTabbedPage"; 3 | import WebApp from "../../WebApp"; 4 | 5 | export class TabApp extends WebApp { 6 | public main(): void { 7 | const page = new AtomTabbedPage(this); 8 | this.root = page; 9 | 10 | setTimeout(async () => { 11 | const nav = this.resolve(NavigationService) as NavigationService; 12 | await nav.openPage("web-atoms-core/dist/web/samples/tabs/views/Page1", { 13 | message: "Page 1" 14 | }); 15 | await nav.openPage("web-atoms-core/dist/web/samples/tabs/views/Page1", { 16 | message: "Page 2" 17 | }); 18 | }, 1000); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/core/Markdown.ts: -------------------------------------------------------------------------------- 1 | import { App } from "../App"; 2 | import MarkdownService from "../web/services/MarkdownService"; 3 | import FormattedString from "./FormattedString"; 4 | 5 | export default class Markdown extends FormattedString { 6 | 7 | public static from(text: string): Markdown { 8 | return new Markdown(text); 9 | } 10 | 11 | public applyTo(app: App, element: any): void { 12 | (element as HTMLElement).innerHTML = this.toHtmlString(app); 13 | } 14 | 15 | public toString(): string { 16 | return this.text; 17 | } 18 | 19 | public toHtmlString(app?: App): string { 20 | const ms = app 21 | ? app.resolve(MarkdownService) as MarkdownService 22 | : MarkdownService.instance; 23 | return ms.toHtml(this.text); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/tests/core/AtomUriTest.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Test from "@web-atoms/unit-test/dist/Test"; 3 | import { AtomUri } from "../../core/AtomUri"; 4 | import { AtomTest } from "../../unit/AtomTest"; 5 | 6 | export class AtomUriTest extends AtomTest { 7 | 8 | @Test 9 | public parse(): void { 10 | 11 | const fullUrlString = "https://localhost:8080/folder/file?a=b&c=d#aa=bb"; 12 | 13 | const fullUrl = new AtomUri(fullUrlString); 14 | 15 | Assert.equals("8080", fullUrl.port); 16 | 17 | Assert.equals(fullUrlString, fullUrl.toString()); 18 | 19 | fullUrl.path = "/folder1/file"; 20 | 21 | Assert.equals("https://localhost:8080/folder1/file?a=b&c=d#aa=bb", fullUrl.toString()); 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/core/sleep.ts: -------------------------------------------------------------------------------- 1 | import { CancelToken } from "./types"; 2 | 3 | export default function sleep(timeInMS: number, ct?: CancelToken, throwOnCancel = true) { 4 | let token = 0; 5 | return new Promise((resolve, reject) => { 6 | ct?.registerForCancel((reason) => { 7 | if (token) { 8 | clearTimeout(token); 9 | if (throwOnCancel) { 10 | reject(reason); 11 | } else { 12 | resolve(); 13 | } 14 | } 15 | }); 16 | if (ct?.cancelled) { 17 | if (throwOnCancel) { 18 | reject("cancelled"); 19 | } else { 20 | resolve(); 21 | } 22 | return; 23 | } 24 | token = setTimeout(resolve, timeInMS); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/tests/core/AtomDisposableListTest.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Test from "@web-atoms/unit-test/dist/Test"; 3 | import TestItem from "@web-atoms/unit-test/dist/TestItem"; 4 | import { AtomDisposableList } from "../../core/AtomDisposableList"; 5 | 6 | export class AtomDisposableListTest extends TestItem { 7 | 8 | @Test 9 | public test(): void { 10 | 11 | let b: boolean = false; 12 | let e: boolean = false; 13 | const d = new AtomDisposableList(); 14 | d.add(() => { 15 | b = true; 16 | }); 17 | d.add({ 18 | dispose() { 19 | e = true; 20 | } 21 | }); 22 | d.dispose(); 23 | 24 | Assert.isTrue(b); 25 | 26 | Assert.isTrue(e); 27 | 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/web/styles/AtomNotificationStyle.ts: -------------------------------------------------------------------------------- 1 | import Colors from "../../core/Colors"; 2 | import { AtomStyle } from "./AtomStyle"; 3 | import { IStyleDeclaration } from "./IStyleDeclaration"; 4 | 5 | export default class AtomNotificationStyle extends AtomStyle { 6 | 7 | public get root(): IStyleDeclaration { 8 | return { 9 | padding: "5px", 10 | borderRadius: "5px", 11 | border: "solid 1px lightgray", 12 | fontFamily: "Verdana, Geneva, sans-serif", 13 | fontSize: "16px", 14 | subclasses: { 15 | ".error": { 16 | borderColor: Colors.red, 17 | color: Colors.red, 18 | }, 19 | ".warning": { 20 | backgroundColor: Colors.lightYellow 21 | } 22 | } 23 | }; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/tests/di/DIGlobalTest.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Test from "@web-atoms/unit-test/dist/Test"; 3 | import DISingleton from "../../di/DISingleton"; 4 | import { AtomTest } from "../../unit/AtomTest"; 5 | 6 | export default class DIGlobalTest extends AtomTest { 7 | 8 | @Test 9 | public test() { 10 | const a = this.app.resolve(GlobalService) as GlobalService; 11 | 12 | const r = a.getName(); 13 | 14 | Assert.equals("this is global service", r); 15 | } 16 | 17 | } 18 | 19 | declare var global; 20 | 21 | global.a = {}; 22 | 23 | global.a.globalServiceImpl = { 24 | getName() { 25 | return "this is global service"; 26 | } 27 | }; 28 | 29 | @DISingleton({ globalVar: "a.globalServiceImpl" }) 30 | class GlobalService { 31 | 32 | public getName(): string { 33 | throw new Error("Not implemented"); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/tests/web/sample/PageSample.tsx: -------------------------------------------------------------------------------- 1 | import XNode from "../../../core/XNode"; 2 | import { AtomViewModel } from "../../../view-model/AtomViewModel"; 3 | import { AtomControl } from "../../../web/controls/AtomControl"; 4 | import { AtomListBox } from "../../../web/controls/AtomListBox"; 5 | 6 | class PageSampleViewModel extends AtomViewModel {} 7 | 8 | export default class PageSample extends AtomControl { 9 | 10 | public viewModel: PageSampleViewModel; 11 | 12 | protected create() { 13 | super.create(); 14 | 15 | this.renderer =
16 |
17 |
18 | 19 |
20 | 24 |
; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/MockApp.ts: -------------------------------------------------------------------------------- 1 | import { App } from "./App"; 2 | import { MockNavigationService } from "./services/MockNavigationService"; 3 | import { NavigationService } from "./services/NavigationService"; 4 | 5 | export class MockApp extends App { 6 | 7 | private styleElement: HTMLElement; 8 | 9 | constructor() { 10 | super(); 11 | this.put(NavigationService, new MockNavigationService(this)); 12 | } 13 | 14 | public updateDefaultStyle(textContent: string) { 15 | if (this.styleElement) { 16 | if (this.styleElement.textContent === textContent) { 17 | return; 18 | } 19 | } 20 | const ss = document.createElement("style"); 21 | 22 | ss.textContent = textContent; 23 | if (this.styleElement) { 24 | this.styleElement.remove(); 25 | } 26 | document.head.appendChild(ss); 27 | this.styleElement = ss; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/core/WatchProperty.ts: -------------------------------------------------------------------------------- 1 | import { AtomBinder } from "./AtomBinder"; 2 | import type { AtomComponent } from "./AtomComponent"; 3 | import { AtomWatcher } from "./AtomWatcher"; 4 | 5 | export default function WatchProperty(target: AtomComponent, key: string, descriptor: any): any { 6 | 7 | const { get } = descriptor; 8 | const isSetup = Symbol.for(`isSetup${key}`); 9 | return { 10 | // tslint:disable-next-line: object-literal-shorthand 11 | get: function() { 12 | const watcher = new AtomWatcher(this, get, () => { 13 | AtomBinder.refreshValue(this, key); 14 | }, this); 15 | watcher.init(false); 16 | this.registerDisposable(watcher); 17 | this[isSetup] = watcher; 18 | Object.defineProperty(this, key, descriptor); 19 | return get.apply(this); 20 | }, 21 | configurable: true 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | 9 | { 10 | "name": "Launch localhost", 11 | "type": "chrome", 12 | "request": "launch", 13 | "url": "http://http://192.168.0.187:8080/", 14 | "webRoot": "${workspaceFolder}/", 15 | }, 16 | { 17 | "type": "node", 18 | "request": "launch", 19 | "name": "Test Selected File", 20 | "args": ["${workspaceFolder}/node_modules/@web-atoms/unit-test/index.js", "./dist/tests"], 21 | "cwd": "${workspaceFolder}", 22 | "protocol": "inspector", 23 | "outFiles": [ 24 | "${workspaceFolder}/dist/**/*.js" 25 | ] 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /src/core/AtomDisposableList.ts: -------------------------------------------------------------------------------- 1 | import { IDisposable } from "./types"; 2 | 3 | export class AtomDisposableList implements IDisposable { 4 | 5 | // tslint:disable-next-line:ban-types 6 | private disposables: IDisposable[] = []; 7 | 8 | // tslint:disable-next-line:ban-types 9 | public add(d: (() => void) | IDisposable): IDisposable { 10 | if (typeof d === "function") { 11 | const fx = d; 12 | d = { 13 | dispose: fx 14 | }; 15 | } 16 | this.disposables.push(d); 17 | const dx = d; 18 | return { 19 | dispose: () => { 20 | this.disposables = this.disposables.filter((x) => x !== dx); 21 | dx.dispose(); 22 | } 23 | }; 24 | } 25 | 26 | public dispose(): void { 27 | for (const iterator of this.disposables) { 28 | iterator.dispose(); 29 | } 30 | this.disposables.length = 0; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "terminal.integrated.profiles.windows": { 3 | "PowerShell": { 4 | "source": "PowerShell", 5 | "args": [ 6 | "-ExecutionPolicy", 7 | "Bypass" 8 | ] 9 | } 10 | }, 11 | "terminal.integrated.defaultProfile.windows": "PowerShell", 12 | "coverage-gutters.lcovname": "./coverage/lcov.info", 13 | "cSpell.words": [ 14 | "Akash", 15 | "Descendents", 16 | "ILVM", 17 | "IUMD", 18 | "Kava", 19 | "Scroller", 20 | "Selectable", 21 | "Simmi", 22 | "bindables", 23 | "bodyformmodel", 24 | "downlevel", 25 | "grayscale", 26 | "keyframename", 27 | "lightgray", 28 | "mouseevent", 29 | "nesw", 30 | "nowrap", 31 | "nwse", 32 | "posttest", 33 | "postversion", 34 | "rawbody", 35 | "typedoc", 36 | "xmlbody", 37 | "xmlhttprequest", 38 | "xnode" 39 | ] 40 | } -------------------------------------------------------------------------------- /src/services/ReferenceService.ts: -------------------------------------------------------------------------------- 1 | import DISingleton from "../di/DISingleton"; 2 | 3 | export class ObjectReference { 4 | 5 | public consume: () => any; 6 | 7 | public timeout: any; 8 | 9 | constructor(public key: string, public value: any) {} 10 | 11 | } 12 | 13 | @DISingleton() 14 | export default class ReferenceService { 15 | 16 | private cache: { [key: string]: any} = {}; 17 | 18 | private id: number = 1; 19 | 20 | public get(key: string): ObjectReference { 21 | return this.cache[key]; 22 | } 23 | 24 | public put(item: any, ttl: number = 60): ObjectReference { 25 | const key = `k${this.id++}`; 26 | const r = new ObjectReference(key, item); 27 | r.consume = () => { 28 | delete this.cache[key]; 29 | if (r.timeout) { 30 | clearTimeout(r.timeout); 31 | } 32 | return r.value; 33 | }; 34 | r.timeout = setTimeout(() => { 35 | r.timeout = 0; 36 | r.consume(); 37 | }, ttl * 1000); 38 | this.cache[key] = r; 39 | return r; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/view-model/bindProperty.ts: -------------------------------------------------------------------------------- 1 | import { IValueConverter } from "../core/IValueConverter"; 2 | import { PropertyBinding } from "../core/PropertyBinding"; 3 | import { IDisposable } from "../core/types"; 4 | import { AtomViewModel } from "./AtomViewModel"; 5 | 6 | /** 7 | * Binds source property to target property with optional two ways 8 | * @param target target whose property will be set 9 | * @param propertyName name of target property 10 | * @param source source to read property from 11 | * @param path property path of source 12 | * @param twoWays optional, two ways {@link IValueConverter} 13 | */ 14 | export default function bindProperty( 15 | vm: AtomViewModel, 16 | target: any, 17 | propertyName: string, 18 | source: any, 19 | path: string[][], 20 | twoWays?: IValueConverter | ((v: any) => any) ): IDisposable { 21 | const pb = new PropertyBinding( 22 | target, 23 | null, 24 | propertyName, 25 | path, 26 | (twoWays && typeof twoWays !== "function") ? true : false , twoWays, source); 27 | return vm.registerDisposable(pb); 28 | } 29 | -------------------------------------------------------------------------------- /src/web/samples/tabs/views/TabHost.ts: -------------------------------------------------------------------------------- 1 | import { App } from "../../../../App"; 2 | import { Inject } from "../../../../di/Inject"; 3 | import { NavigationService } from "../../../../services/NavigationService"; 4 | import { AtomViewModel } from "../../../../view-model/AtomViewModel"; 5 | import { AtomTabbedPage } from "../../../controls/AtomTabbedPage"; 6 | 7 | export default class TabHost extends AtomTabbedPage { 8 | 9 | protected create(): void { 10 | this.tabChannelName = "app"; 11 | this.viewModel = this.resolve(TabHostViewModel); 12 | } 13 | } 14 | 15 | class TabHostViewModel extends AtomViewModel { 16 | 17 | constructor( 18 | @Inject app: App, 19 | @Inject private nav: NavigationService) { 20 | super(app); 21 | } 22 | 23 | public async init(): Promise { 24 | await this.nav.openPage("tab://app/web-atoms-core/dist/web/samples/tabs/views/Page1", { 25 | message: "Page 1", 26 | title: "Page 1" 27 | }); 28 | await this.nav.openPage("tab://app/web-atoms-core/dist/web/samples/tabs/views/Page1", { 29 | message: "Page 2", 30 | title: "Page 2" 31 | }); 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/unit/AtomWebTest.ts: -------------------------------------------------------------------------------- 1 | import { AtomDispatcher } from "../core/AtomDispatcher"; 2 | import { MockApp } from "../MockApp"; 3 | import { MockNavigationService } from "../services/MockNavigationService"; 4 | import { NavigationService } from "../services/NavigationService"; 5 | import { AtomTest } from "./AtomTest"; 6 | import { AtomGridView } from "../web/controls/AtomGridView"; 7 | import { AtomStyleSheet } from "../web/styles/AtomStyleSheet"; 8 | import { AtomTheme } from "../web/styles/AtomTheme"; 9 | 10 | export class MockWebApp extends MockApp { 11 | 12 | } 13 | 14 | export default class AtomWebTest extends AtomTest { 15 | 16 | public get navigationService(): MockNavigationService { 17 | return this.app.get(NavigationService as any); 18 | } 19 | 20 | constructor() { 21 | super(new MockWebApp()); 22 | this.app.put(AtomTheme, this.app.resolve(AtomTheme)); 23 | this.app.put(AtomStyleSheet, this.app.resolve(AtomTheme)); 24 | } 25 | 26 | public async dispose(): Promise { 27 | if (this.navigationService.assert) { 28 | this.navigationService.assert(); 29 | } 30 | await this.app.waitForPendingCalls(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/tests/core/PropertyBinderTest.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Test from "@web-atoms/unit-test/dist/Test"; 3 | import { PropertyBinding } from "../../core/PropertyBinding"; 4 | import { AtomTest } from "../../unit/AtomTest"; 5 | 6 | class SourceClass { 7 | 8 | public source: string = "5"; 9 | } 10 | 11 | class DestinationClass { 12 | 13 | public destination: number; 14 | } 15 | 16 | export class PropertyBinderTest extends AtomTest { 17 | 18 | @Test 19 | public twoWayBindingTest(): void { 20 | const source = new SourceClass(); 21 | const destination = new DestinationClass(); 22 | 23 | const pb = new PropertyBinding( 24 | destination, 25 | null, 26 | "destination", 27 | [["this", "source"]], 28 | true, { 29 | fromSource(v): any { 30 | return parseInt(v, 10); 31 | }, 32 | fromTarget(v: any): any { 33 | return v + ""; 34 | } 35 | }, source 36 | ); 37 | 38 | Assert.equals(5, destination.destination); 39 | 40 | destination.destination = 10; 41 | Assert.equals("10", source.source); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/tests/web/controls/AtomControlDataTest.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Test from "@web-atoms/unit-test/dist/Test"; 3 | import AtomWebTest from "../../../unit/AtomWebTest"; 4 | import { AtomControl } from "../../../web/controls/AtomControl"; 5 | 6 | export class AtomControlDataTest extends AtomWebTest { 7 | 8 | @Test 9 | public async data(): Promise { 10 | 11 | const root = new AtomControl(this.app); 12 | 13 | const child = new AtomControl(this.app); 14 | 15 | const a = {}; 16 | 17 | root.data = a; 18 | 19 | root.append(child); 20 | 21 | Assert.equals(a, child.data); 22 | 23 | } 24 | 25 | @Test 26 | public async dataInherited(): Promise { 27 | 28 | const root = new AtomControl(this.app); 29 | 30 | const child = new AtomControl(this.app); 31 | 32 | const a = {}; 33 | 34 | root.append(child); 35 | 36 | root.data = a; 37 | 38 | Assert.equals(a, child.data); 39 | 40 | } 41 | 42 | @Test 43 | public async dataUndefined(): Promise { 44 | 45 | const root = new AtomControl(this.app); 46 | 47 | Assert.isUndefined(root.data); 48 | 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/tests/core/StringHelperTests.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Test from "@web-atoms/unit-test/dist/Test"; 3 | import { StringHelper } from "../../core/StringHelper"; 4 | import { AtomTest } from "../../unit/AtomTest"; 5 | export class StringHelperTest extends AtomTest { 6 | @Test 7 | public camelToHyphen(): void { 8 | Assert.equals("this", StringHelper.fromCamelToHyphen("this")); 9 | Assert.equals("this-is-test", StringHelper.fromCamelToHyphen("thisIsTest")); 10 | Assert.equals("this-is-sample-test", StringHelper.fromCamelToHyphen("thisIsSampleTest")); 11 | } 12 | 13 | @Test 14 | public camelToPascal(): void { 15 | Assert.equals("This", StringHelper.fromCamelToPascal("this")); 16 | Assert.equals("ThisIsTest", StringHelper.fromCamelToPascal("thisIsTest")); 17 | Assert.equals("ThisIsSampleTest", StringHelper.fromCamelToPascal("thisIsSampleTest")); 18 | } 19 | 20 | @Test 21 | public pascalToCamel(): void { 22 | Assert.equals("this", StringHelper.fromPascalToCamel("This")); 23 | Assert.equals("thisIsTest", StringHelper.fromPascalToCamel("ThisIsTest")); 24 | Assert.equals("thisIsSampleTest", StringHelper.fromPascalToCamel("ThisIsSampleTest")); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/core/CancelTokenFactory.ts: -------------------------------------------------------------------------------- 1 | import DITransient from "../di/DITransient"; 2 | import { AtomBinder } from "./AtomBinder"; 3 | import TransientDisposable from "./TransientDisposable"; 4 | import { CancelToken, IDisposable } from "./types"; 5 | 6 | /** 7 | * We recommend using CancelTokenFactory instead of using CancelToken directly. 8 | * This class will cancel previous token before creating new token for given key. 9 | */ 10 | @DITransient() 11 | export default class CancelTokenFactory 12 | extends TransientDisposable 13 | implements IDisposable { 14 | private mToken: { [key: string]: CancelToken } = {}; 15 | 16 | /** 17 | * This will create a new token and cancel previous token 18 | */ 19 | public newToken(key?: string, timeout: number = -1): CancelToken { 20 | key = key || "__old"; 21 | const old = this.mToken [key]; 22 | if (old) { 23 | old.cancel(); 24 | } 25 | const n = this.mToken[key] = new CancelToken(timeout); 26 | return n; 27 | } 28 | 29 | public dispose(): void { 30 | for (const key in this.mToken) { 31 | if (this.mToken.hasOwnProperty(key)) { 32 | const element = this.mToken[key]; 33 | element.dispose(); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/core/Defer.ts: -------------------------------------------------------------------------------- 1 | import { CancelToken } from "./types"; 2 | 3 | /** 4 | * Defers execution for given milliseconds. And previous pending 5 | * execution is cancelled, so only the last execution will be executed. 6 | * 7 | * This is important when you want to watch multiple events and avoid multiple refresh 8 | * @param n number of milliseconds to defer 9 | */ 10 | export default function Defer(n: number = 250) { 11 | 12 | return (target: any, key: string, descriptor: any) => { 13 | // tslint:disable-next-line: ban-types 14 | const old = descriptor.value as Function; 15 | const k = Symbol.for(`defer_${key}`); 16 | descriptor.value = function(... a: any[]) { 17 | const id = this[k]; 18 | if (id) { 19 | clearTimeout(id); 20 | } 21 | this[k] = setTimeout(() => { 22 | this[k] = undefined; 23 | const result = old.apply(this, a); 24 | if (result?.then) { 25 | result.catch((e) => { 26 | if (CancelToken.isCancelled(e)) { 27 | return; 28 | } 29 | console.error(e); 30 | }); 31 | } 32 | }, n); 33 | }; 34 | }; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/web/controls/AtomTemplateControl.ts: -------------------------------------------------------------------------------- 1 | import { BindableProperty } from "../../core/BindableProperty"; 2 | import { IClassOf } from "../../core/types"; 3 | import { AtomControl } from "./AtomControl"; 4 | 5 | export class AtomTemplateControl extends AtomControl { 6 | 7 | public contentTemplate: IClassOf; 8 | 9 | private content: AtomControl; 10 | 11 | public onPropertyChanged(name: string): void { 12 | if (name === "contentTemplate") { 13 | this.createContent(); 14 | } 15 | } 16 | 17 | public onUpdateUI(): void { 18 | super.onUpdateUI(); 19 | if (this.content) { 20 | return; 21 | } 22 | if (this.contentTemplate) { 23 | this.createContent(); 24 | } 25 | } 26 | 27 | protected preCreate() { 28 | this.contentTemplate = null; 29 | this.content = null; 30 | } 31 | 32 | protected createContent(): void { 33 | const t = this.contentTemplate; 34 | if (!t) { 35 | return; 36 | } 37 | 38 | const tc = this.content; 39 | if (tc) { 40 | tc.dispose(); 41 | this.content = null; 42 | } 43 | 44 | const ntc = this.content = new (t)(this.app); 45 | 46 | this.append(ntc); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/view-model/Delay.ts: -------------------------------------------------------------------------------- 1 | import { AtomViewModel } from "./AtomViewModel"; 2 | import { registerInit, viewModelInitFunc } from "./baseTypes"; 3 | /** 4 | * Setups a timer and disposes automatically when view model is destroyed. This will execute 5 | * given function only once unless `repeat` argument is `true`. 6 | * @param delayInSeconds delay in seconds 7 | * @param repeat repeat at given delay 8 | */ 9 | export default function Delay(delayInSeconds: number, repeat: boolean = false): viewModelInitFunc { 10 | return (target: AtomViewModel, key: string | symbol): void => { 11 | registerInit(target, (vm) => { 12 | // tslint:disable-next-line: ban-types 13 | const fx: Function = (vm as any)[key]; 14 | const afx = () => { 15 | vm.app.runAsync(() => fx.apply(vm)); 16 | }; 17 | const dx = delayInSeconds * 1000; 18 | const id = repeat 19 | ? setInterval(afx, dx) 20 | : setTimeout(afx, dx); 21 | const d = { 22 | dispose() { 23 | if (repeat) { 24 | clearInterval(id); 25 | } else { 26 | clearTimeout(id); 27 | } 28 | } 29 | }; 30 | vm.registerDisposable(d); 31 | }); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/view-model/bindPromise.ts: -------------------------------------------------------------------------------- 1 | import { AtomBinder } from "../core/AtomBinder"; 2 | import { NavigationService, NotifyType } from "../services/NavigationService"; 3 | import { AtomViewModel } from "./AtomViewModel"; 4 | /** 5 | * Use this method to create an object/array that will refresh 6 | * when promise is resolved 7 | */ 8 | export default function bindPromise( 9 | vm: AtomViewModel, 10 | p: Promise, 11 | value: any, 12 | displayError: boolean | ((e) => void) = true): T { 13 | p.then((v) => { 14 | if (Array.isArray(v)) { 15 | const a = value as any; 16 | (a as any[]).replace(v as any); 17 | } else { 18 | for (const key in v) { 19 | if (v.hasOwnProperty(key)) { 20 | const element = v[key]; 21 | value[key] = element; 22 | AtomBinder.refreshValue(value, key); 23 | } 24 | } 25 | } 26 | }).catch((e) => { 27 | if (displayError) { 28 | if (typeof displayError === "function") { 29 | displayError(e); 30 | } else { 31 | const n = vm.app.resolve(NavigationService) as NavigationService; 32 | n.notify(e, "Error", NotifyType.Error); 33 | } 34 | } 35 | }); 36 | return value; 37 | } 38 | -------------------------------------------------------------------------------- /src/web/services/NotificationPopup.tsx: -------------------------------------------------------------------------------- 1 | import XNode from "../../core/XNode"; 2 | import styled from "../../style/styled"; 3 | import { PopupWindow } from "./PopupService"; 4 | 5 | const css = styled.css ` 6 | padding: 5px; 7 | font-size: larger; 8 | & .error { 9 | color: red; 10 | border-color: red; 11 | } 12 | & .warning { 13 | background-color: lightyellow; 14 | } 15 | `.installLocal(); 16 | 17 | export default function NotificationPopup({ 18 | message, 19 | type 20 | }): typeof PopupWindow { 21 | 22 | return class Notification extends PopupWindow { 23 | 24 | public create(): void { 25 | if(message instanceof XNode) { 26 | this.render(
32 | { message } 33 |
); 34 | return; 35 | } 36 | this.render(
); 44 | } 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@web-atoms/core", 3 | "version": "2.2.194", 4 | "description": "Web Atoms Core", 5 | "main": "index.js", 6 | "scripts": { 7 | "test1": "node ./node_modules/@web-atoms/unit-test/index.js ./dist/tests", 8 | "test": "istanbul cover ./node_modules/@web-atoms/unit-test/index.js ./dist/tests", 9 | "posttest": "remap-istanbul -i ./coverage/coverage.json -t html -o ./html-report", 10 | "postversion": "git push --follow-tags" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/neurospeech/web-atoms-core.git" 15 | }, 16 | "keywords": [ 17 | "web", 18 | "atoms", 19 | "core" 20 | ], 21 | "author": "Akash Kava", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/neurospeech/web-atoms-core/issues" 25 | }, 26 | "homepage": "https://github.com/neurospeech/web-atoms-core#readme", 27 | "devDependencies": { 28 | "@web-atoms/module-loader": "^2.1.18", 29 | "@web-atoms/unit-test": "^1.0.32", 30 | "colors": "^1.4.0", 31 | "istanbul": "^0.4.5", 32 | "jsdom": "^15.2.1", 33 | "path": "^0.12.7", 34 | "remap-istanbul": "^0.11.1", 35 | "test-dom": "^1.0.0", 36 | "tslib": "^2.4.1", 37 | "xmlhttprequest": "^1.8.0" 38 | }, 39 | "dependencies": { 40 | "@web-atoms/date-time": "^1.1.1", 41 | "reflect-metadata": "^0.1.14" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/web/controls/AtomViewStack.ts: -------------------------------------------------------------------------------- 1 | import { BindableProperty } from "../../core/BindableProperty"; 2 | import { AtomControl } from "./AtomControl"; 3 | 4 | export class AtomViewStack extends AtomControl { 5 | 6 | public selectedIndex: number; 7 | 8 | public children: HTMLElement[]; 9 | 10 | public current: HTMLElement; 11 | 12 | public append(e: HTMLElement | Text | AtomControl): AtomControl { 13 | const ee = e instanceof AtomControl ? (e as AtomControl).element : e as HTMLElement; 14 | ((ee as any) as HTMLElement)._logicalParent = this.element; 15 | this.children = this.children || []; 16 | const index = this.children.length; 17 | this.children.push(e instanceof AtomControl ? (e as AtomControl).element : e as HTMLElement); 18 | if (this.selectedIndex === undefined) { 19 | this.selectedIndex = 0; 20 | } 21 | 22 | const style = ee.style; 23 | style.position = "absolute"; 24 | style.top = style.left = style.right = style.bottom = "0"; 25 | 26 | this.bind(ee, "styleVisibility", [["selectedIndex"]], false, 27 | (v) => v === index ? "visible" : "hidden" ); 28 | 29 | this.element.appendChild(ee); 30 | return this; 31 | } 32 | 33 | protected preCreate() { 34 | this.selectedIndex = -1; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/web/styles/AtomListBoxStyle.ts: -------------------------------------------------------------------------------- 1 | import { BindableProperty } from "../../core/BindableProperty"; 2 | import { AtomStyle } from "./AtomStyle"; 3 | import { AtomTheme } from "./AtomTheme"; 4 | import { IStyleDeclaration } from "./IStyleDeclaration"; 5 | 6 | export class AtomListBoxStyle extends AtomStyle { 7 | 8 | public padding: number; 9 | 10 | public get root(): IStyleDeclaration { 11 | return { 12 | subclasses: { 13 | " .item": this.item, 14 | " .selected-item": this.selectedItem 15 | } 16 | }; 17 | } 18 | 19 | public get theme(): AtomTheme { 20 | return this.styleSheet as AtomTheme; 21 | } 22 | 23 | public get item(): IStyleDeclaration { 24 | return { 25 | backgroundColor: this.theme.bgColor, 26 | color: this.theme.color, 27 | padding: (this.padding || this.theme.padding) + "px", 28 | borderRadius: (this.padding || this.theme.padding) + "px", 29 | cursor: "pointer" 30 | }; 31 | } 32 | 33 | public get selectedItem(): IStyleDeclaration { 34 | return { 35 | ... this.item, 36 | backgroundColor: this.theme.selectedBgColor, 37 | color: this.theme.selectedColor, 38 | cursor: "pointer" 39 | }; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/core/SingleInvoker.ts: -------------------------------------------------------------------------------- 1 | import DITransient from "../di/DITransient"; 2 | import TransientDisposable from "./TransientDisposable"; 3 | import { IDisposable } from "./types"; 4 | 5 | @DITransient() 6 | export default class SingleInvoker extends TransientDisposable { 7 | 8 | private keys = new Map(); 9 | 10 | public dispose() { 11 | for (const [key, index] of this.keys.entries()) { 12 | clearTimeout(index); 13 | } 14 | this.keys.clear(); 15 | } 16 | 17 | // tslint:disable-next-line: ban-types 18 | public invoke(key: string, fx: Function, delay: number = 100): void { 19 | const keys = this.keys; 20 | const e = keys.get(key); 21 | if (e) { 22 | clearTimeout(e); 23 | } 24 | keys.set(key, setTimeout(() => { 25 | keys.delete(key); 26 | fx(); 27 | }, delay)); 28 | } 29 | 30 | // tslint:disable-next-line: ban-types 31 | public queue(fx: Function, delay: number = 1, key?: string ): void { 32 | key ??= fx.toString(); 33 | const keys = this.keys; 34 | const e = keys.get(key); 35 | if (e) { 36 | clearTimeout(e); 37 | } 38 | keys.set(key, setTimeout(() => { 39 | keys.delete(key); 40 | fx(); 41 | }, delay)); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "All Build", 8 | "dependsOn":[ 9 | "TypeScript Build Watch", 10 | "Web Atoms Dev Server", 11 | ], 12 | "problemMatcher": [], 13 | "group": { 14 | "kind": "build", 15 | "isDefault": true 16 | } 17 | }, 18 | { 19 | "label": "TypeScript Build Watch", 20 | "type": "typescript", 21 | "tsconfig": "tsconfig.json", 22 | "option": "watch", 23 | }, 24 | { 25 | "label": "Web Atoms Dev Server", 26 | "type": "shell", 27 | "command": "wads", 28 | "args": [ 29 | "https://test.webatoms.in" 30 | ], 31 | "problemMatcher": [] 32 | }, 33 | { 34 | "label": "Web Atoms Dev Server Alpha", 35 | "command": "node", 36 | "args": [ 37 | "D:\\git\\akash\\github\\web-atoms\\dev-server\\index.js", 38 | "https://test.webatoms.in" 39 | ], 40 | "problemMatcher": [] 41 | } 42 | ] 43 | } -------------------------------------------------------------------------------- /src/tests/view-model/CancelTokenFactoryTest.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Category from "@web-atoms/unit-test/dist/Category"; 3 | import Test from "@web-atoms/unit-test/dist/Test"; 4 | import TestItem from "@web-atoms/unit-test/dist/TestItem"; 5 | import { Atom } from "../../Atom"; 6 | import CancelTokenFactory from "../../core/CancelTokenFactory"; 7 | import { Inject } from "../../di/Inject"; 8 | import { AtomTest } from "../../unit/AtomTest"; 9 | import { AtomViewModel, waitForReady } from "../../view-model/AtomViewModel"; 10 | 11 | class CVM extends AtomViewModel { 12 | 13 | @Inject 14 | private cancelTokenFactory: CancelTokenFactory; 15 | 16 | public async list(): Promise { 17 | await Atom.delay(10, this.cancelTokenFactory.newToken("list")); 18 | } 19 | 20 | } 21 | 22 | @Category("Cancel Token Factory") 23 | export default class CancelTokenFactoryTest extends AtomTest { 24 | 25 | @Test 26 | public async vmTest(): Promise { 27 | 28 | const vm = this.app.resolve(CVM, true); 29 | 30 | await waitForReady(vm); 31 | 32 | await vm.list(); 33 | 34 | const p = vm.list(); 35 | 36 | const p2 = vm.list(); 37 | 38 | Assert.throwsAsync("cancelled", async () => await p); 39 | 40 | await p2; 41 | 42 | vm.dispose(); 43 | 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/web/images/close-button-hover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/tests/core/AtomOnceTest.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Test from "@web-atoms/unit-test/dist/Test"; 3 | import { Atom } from "../../Atom"; 4 | import { AtomOnce } from "../../core/AtomOnce"; 5 | import { AtomTest } from "../../unit/AtomTest"; 6 | 7 | // export class AtomOnceTest extends AtomTest { 8 | 9 | // private a: AtomOnce = new AtomOnce(); 10 | 11 | // private isRunning: boolean = false; 12 | 13 | // @Test 14 | // public async promise(): Promise { 15 | // await this.runPromise(false); 16 | // Assert.isFalse((this.a as any).isRunning); 17 | // await this.runPromise(true); 18 | // Assert.isFalse((this.a as any).isRunning); 19 | // } 20 | 21 | // private async runPromise(fail: boolean): Promise { 22 | // // tslint:disable-next-line: no-debugger 23 | // debugger; 24 | // if (this.isRunning) { 25 | // throw new Error("Already running"); 26 | // } 27 | // this.isRunning = true; 28 | // this.a.run(() => this.run(fail)); 29 | // this.isRunning = false; 30 | // } 31 | 32 | // private async run(fail: boolean): Promise { 33 | // await Atom.delay(1); 34 | // this.a.run(() => this.runPromise(fail)); 35 | // if (fail) { 36 | // throw new Error("failed"); 37 | // } 38 | // } 39 | 40 | // } 41 | -------------------------------------------------------------------------------- /src/web/controls/AtomToggleButtonBar.ts: -------------------------------------------------------------------------------- 1 | import { App } from "../../App"; 2 | import { AtomToggleButtonBarStyle } from "../styles/AtomToggleButtonBarStyle"; 3 | import { AtomControl } from "./AtomControl"; 4 | import { AtomItemsControl } from "./AtomItemsControl"; 5 | import { AtomListBox } from "./AtomListBox"; 6 | 7 | export class AtomToggleButtonBar extends AtomListBox { 8 | 9 | constructor(app: App, e?: HTMLElement) { 10 | super(app, e || document.createElement("ul")); 11 | } 12 | 13 | protected preCreate(): void { 14 | super.preCreate(); 15 | this.allowMultipleSelection = false; 16 | this.allowSelectFirst = true; 17 | this.itemTemplate = AtomToggleButtonBarItemTemplate; 18 | this.defaultControlStyle = AtomToggleButtonBarStyle; 19 | this.registerItemClick(); 20 | this.runAfterInit(() => this.setElementClass(this.element, { 21 | [this.controlStyle.name]: 1, 22 | "atom-toggle-button-bar": 1 23 | }, true )); 24 | } 25 | 26 | } 27 | 28 | class AtomToggleButtonBarItemTemplate extends AtomControl { 29 | 30 | constructor(app: App, e?: HTMLElement) { 31 | super(app, e || document.createElement("li")); 32 | } 33 | 34 | protected create(): void { 35 | this.bind(this.element, "text", [["data"]], false, (v) => { 36 | const p = this.parent as AtomItemsControl; 37 | return v[p.labelPath]; 38 | }); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/tests/di/tests.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Category from "@web-atoms/unit-test/dist/Category"; 3 | import Test from "@web-atoms/unit-test/dist/Test"; 4 | import TestItem from "@web-atoms/unit-test/dist/TestItem"; 5 | import { App } from "../../App"; 6 | import { Inject } from "../../di/Inject"; 7 | import { ServiceCollection } from "../../di/ServiceCollection"; 8 | import { ServiceProvider } from "../../di/ServiceProvider"; 9 | 10 | class GlobalClass { 11 | 12 | public id: number; 13 | 14 | constructor() { 15 | this.id = (new Date()).getTime(); 16 | } 17 | 18 | } 19 | 20 | class DependentService { 21 | 22 | constructor( 23 | @Inject public g: GlobalClass, 24 | @Inject public sp: ServiceProvider 25 | ) { 26 | 27 | } 28 | 29 | } 30 | 31 | @Category("DI") 32 | export class TestCase extends TestItem { 33 | 34 | @Test 35 | public singleton(): void { 36 | 37 | const app = new App(); 38 | 39 | ServiceCollection.instance.registerSingleton(GlobalClass); 40 | ServiceCollection.instance.registerSingleton(DependentService); 41 | 42 | const g1 = app.get(GlobalClass); 43 | const g2 = app.get(GlobalClass); 44 | 45 | Assert.equals(g1, g2); 46 | 47 | const ds = app.get(DependentService); 48 | Assert.equals(ds.g, g1); 49 | 50 | Assert.equals(ds.sp, app); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/di/TypeKey.ts: -------------------------------------------------------------------------------- 1 | import AtomMap from "../core/AtomMap"; 2 | 3 | export class TypeKey { 4 | 5 | public static get(c: any) { 6 | return c; 7 | // for (const iterator of this.keys) { 8 | // if (iterator.c === c) { 9 | // return iterator.key; 10 | // } 11 | // } 12 | // const key = `${c.name || "key"}${this.keys.length}`; 13 | // this.keys.push({ c, key}); 14 | // return key; 15 | // return TypeKey.keys.getOrCreate(c, (c1) => { 16 | // const key = `${c1.name || "key"}${TypeKey.keys.size}`; 17 | // return key; 18 | // }); 19 | } 20 | 21 | // private static keys: AtomMap = new AtomMap(); 22 | 23 | public static getName(c: any) { 24 | return TypeKey.keys.getOrCreate(c, (c1) => { 25 | const key = `${c1.name || "key"}${TypeKey.keys.size}`; 26 | return key; 27 | }); 28 | } 29 | 30 | private static keys: AtomMap = new AtomMap(); 31 | 32 | } 33 | 34 | // if (Map !== undefined) { 35 | 36 | // const map = new Map(); 37 | 38 | // const oldGet = TypeKey.get; 39 | 40 | // TypeKey.get = (c: any): string => { 41 | // const v = map.get(c); 42 | // if (!v) { 43 | // return v; 44 | // } 45 | // const v1 = oldGet(c); 46 | // map.set(c, v1); 47 | // return v1; 48 | // }; 49 | // } 50 | -------------------------------------------------------------------------------- /src/view-model/Disposable.ts: -------------------------------------------------------------------------------- 1 | import { AtomViewModel } from "./AtomViewModel"; 2 | 3 | const Disposable = (target: AtomViewModel, key: string) => { 4 | // property value 5 | const iVal: any = target[key]; 6 | 7 | const keyName: string = "_" + key; 8 | const disposableKey: string = "_$_disposable" + key; 9 | 10 | target[keyName] = iVal; 11 | if (iVal) { 12 | target[disposableKey] = target.registerDisposable(iVal); 13 | } 14 | 15 | // property getter 16 | const getter: () => any = function(): any { 17 | return this[keyName]; 18 | }; 19 | 20 | // property setter 21 | const setter: (v: any) => void = function(newVal: any): void { 22 | const oldValue = this[keyName]; 23 | // tslint:disable-next-line:triple-equals 24 | if (oldValue == newVal) { 25 | return; 26 | } 27 | 28 | const oldDisposable = this[disposableKey]; 29 | if (oldDisposable && oldDisposable.dispose) { 30 | oldDisposable.dispose(); 31 | } 32 | 33 | this[keyName] = newVal; 34 | }; 35 | 36 | // delete property 37 | if (delete target[key]) { 38 | 39 | // create new property with getter and setter 40 | Object.defineProperty(target, key, { 41 | get: getter, 42 | set: setter, 43 | enumerable: true, 44 | configurable: true 45 | }); 46 | 47 | } 48 | }; 49 | 50 | export default Disposable; 51 | -------------------------------------------------------------------------------- /src/core/AtomDispatcher.ts: -------------------------------------------------------------------------------- 1 | export class AtomDispatcher { 2 | 3 | // public static instance: AtomDispatcher = new AtomDispatcher(); 4 | public paused: boolean; 5 | public head = null; 6 | public tail = null; 7 | 8 | public onTimeout(): void { 9 | if (this.paused) { 10 | return; 11 | } 12 | if (!this.head) { 13 | return; 14 | } 15 | const item = this.head; 16 | this.head = item.next; 17 | item.next = null; 18 | if (!this.head) { 19 | this.tail = null; 20 | } 21 | 22 | item(); 23 | setTimeout(() => { 24 | this.onTimeout(); 25 | }, 1); 26 | } 27 | 28 | public pause(): void { 29 | this.paused = true; 30 | } 31 | 32 | public start(): void { 33 | this.paused = false; 34 | setTimeout(() => { 35 | this.onTimeout(); 36 | }, 1); 37 | } 38 | 39 | public callLater(f: () => void) { 40 | 41 | if (this.tail) { 42 | this.tail.next = f; 43 | this.tail = f; 44 | } else { 45 | 46 | this.head = f; 47 | this.tail = f; 48 | } 49 | if (!this.paused) { 50 | this.start(); 51 | } 52 | } 53 | 54 | public waitForAll(): Promise { 55 | return new Promise((resolve, reject) => { 56 | this.callLater(() => { 57 | resolve(); 58 | }); 59 | }); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/web/controls/AtomContentControl.ts: -------------------------------------------------------------------------------- 1 | import { AtomBinder } from "../../core/AtomBinder"; 2 | import { AtomStyle } from "../styles/AtomStyle"; 3 | import { IStyleDeclaration } from "../styles/IStyleDeclaration"; 4 | import { AtomControl } from "./AtomControl"; 5 | 6 | export class AtomContentControl extends AtomControl { 7 | 8 | private mContent: AtomControl; 9 | public get content(): AtomControl { 10 | return this.mContent; 11 | } 12 | 13 | public set content(c: AtomControl) { 14 | const old = this.mContent; 15 | if (old) { 16 | old.element.remove(); 17 | } 18 | this.mContent = c; 19 | if (c) { 20 | this.element.appendChild(c.element); 21 | const style = c.element.style; 22 | c.invalidate(); 23 | } 24 | AtomBinder.refreshValue(this, "content"); 25 | } 26 | 27 | protected preCreate(): void { 28 | super.preCreate(); 29 | this.defaultControlStyle = AtomContentStyle; 30 | this.runAfterInit(() => { 31 | this.element.classList.add(this.controlStyle.name); 32 | }); 33 | } 34 | } 35 | 36 | export class AtomContentStyle extends AtomStyle { 37 | 38 | public get root(): IStyleDeclaration { 39 | return { 40 | subclasses: { 41 | " > *": { 42 | position: "absolute", 43 | top: "0", 44 | left: "0", 45 | right: "0", 46 | bottom: "0", 47 | } 48 | } 49 | }; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/web/samples/tabs/views/Page1.tsx: -------------------------------------------------------------------------------- 1 | import { App } from "../../../../App"; 2 | import Bind from "../../../../core/Bind"; 3 | import { BindableProperty } from "../../../../core/BindableProperty"; 4 | import XNode from "../../../../core/XNode"; 5 | import { Inject } from "../../../../di/Inject"; 6 | import { AtomWindowViewModel } from "../../../../view-model/AtomWindowViewModel"; 7 | import { AtomGridView } from "../../../controls/AtomGridView"; 8 | import { AtomListBox } from "../../../controls/AtomListBox"; 9 | import { MovieService } from "../../MovieService"; 10 | 11 | export default class Page1 extends AtomGridView { 12 | protected create(): void { 13 | 14 | this.viewModel = this.resolve(Page1ViewModel); 15 | 16 | this.columns = "45%,*,45%"; 17 | this.rows = "45%,*,45%"; 18 | 19 | this.render( 20 | 23 | 24 | x.data.label)}> 25 | 26 | ); 27 | } 28 | } 29 | 30 | class Page1ViewModel extends AtomWindowViewModel { 31 | 32 | @BindableProperty 33 | public message: string; 34 | 35 | @BindableProperty 36 | public items: any; 37 | 38 | constructor( 39 | @Inject app: App, 40 | @Inject public readonly movieService: MovieService 41 | ) { 42 | super(app); 43 | } 44 | 45 | public async init(): Promise { 46 | this.items = await this.movieService.countryList(); 47 | this.closeWarning = "Are you sure you want to close this?"; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/web/controls/AtomNotification.tsx: -------------------------------------------------------------------------------- 1 | import Bind from "../../core/Bind"; 2 | import { BindableProperty } from "../../core/BindableProperty"; 3 | import XNode from "../../core/XNode"; 4 | import AtomNotificationStyle from "../styles/AtomNotificationStyle"; 5 | import { AtomControl } from "./AtomControl"; 6 | 7 | export default class AtomNotification extends AtomControl { 8 | 9 | @BindableProperty 10 | public timeout: number = 5000; 11 | 12 | private timeoutKey: any = null; 13 | 14 | public onPropertyChanged(name: keyof AtomNotification): void { 15 | switch (name) { 16 | case "timeout": 17 | this.setupTimeout(); 18 | break; 19 | } 20 | } 21 | 22 | public create(): void { 23 | this.defaultControlStyle = AtomNotificationStyle; 24 | this.render(
this.viewModel.message )} 26 | timeout={Bind.oneWay(() => this.viewModel.timeout || 5000)} 27 | styleClass={Bind.oneWay(() => ({ 28 | [this.controlStyle.name]: 1, 29 | error: this.viewModel.type && /error/i.test(this.viewModel.type), 30 | warning: this.viewModel.type && /warn/i.test(this.viewModel.type), 31 | }))} 32 | />); 33 | } 34 | 35 | protected setupTimeout(): void { 36 | if (this.timeoutKey) { 37 | clearTimeout(this.timeoutKey); 38 | } 39 | this.timeoutKey = setTimeout(() => { 40 | if (this.element) { 41 | this.app.broadcast(`atom-window-close:${(this as any).id}`, ""); 42 | } 43 | }, this.timeout); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/core/PropertyMap.ts: -------------------------------------------------------------------------------- 1 | import { TypeKey } from "../di/TypeKey"; 2 | 3 | export interface IPropertyMap { 4 | [key: string]: boolean; 5 | } 6 | 7 | export class PropertyMap { 8 | 9 | // tslint:disable-next-line:ban-types 10 | public static from(o: any): PropertyMap { 11 | const c = Object.getPrototypeOf(o); 12 | const key = TypeKey.get(c); 13 | const map = PropertyMap.map; 14 | const m = map[key] || (map[key] = PropertyMap.createMap(o)); 15 | return m; 16 | } 17 | 18 | private static map: { [key: string]: PropertyMap } = {}; 19 | 20 | private static createMap(c: any): PropertyMap { 21 | const map: IPropertyMap = {}; 22 | const nameList: string[] = []; 23 | while (c) { 24 | const names = Object.getOwnPropertyNames(c); 25 | for (const name of names) { 26 | if (/hasOwnProperty|constructor|toString|isValid|errors/i.test(name)) { 27 | continue; 28 | } 29 | // // map[name] = Object.getOwnPropertyDescriptor(c, name) ? true : false; 30 | // const pd = Object.getOwnPropertyDescriptor(c, name); 31 | // // tslint:disable-next-line:no-console 32 | // console.log(`${name} = ${c.enumerable}`); 33 | map[name] = true; 34 | nameList.push(name); 35 | } 36 | c = Object.getPrototypeOf(c); 37 | } 38 | const m = new PropertyMap(); 39 | m.map = map; 40 | m.names = nameList; 41 | return m; 42 | } 43 | 44 | public names: string[]; 45 | 46 | public map: IPropertyMap; 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/web/styles/AtomAlertWindowStyle.ts: -------------------------------------------------------------------------------- 1 | import { Atom } from "../../Atom"; 2 | import Colors from "../../core/Colors"; 3 | import { AtomWindowStyle } from "./AtomWindowStyle"; 4 | import { IStyleDeclaration } from "./IStyleDeclaration"; 5 | 6 | export default class AtomAlertWindowStyle extends AtomWindowStyle { 7 | 8 | public get titleHost(): IStyleDeclaration { 9 | return { 10 | ... this.getBaseProperty(AtomAlertWindowStyle, "titleHost"), 11 | color: Colors.black, 12 | backgroundColor: Colors.white 13 | }; 14 | } 15 | 16 | public get contentPresenter(): IStyleDeclaration { 17 | return { 18 | ... this.getBaseProperty(AtomAlertWindowStyle, "contentPresenter"), 19 | padding: "0px 10px 30px 10px", 20 | textAlign: "center", 21 | color: Colors.rgba(51, 51, 51) 22 | }; 23 | } 24 | 25 | public get commandBar(): IStyleDeclaration { 26 | return { 27 | ... this.getBaseProperty(AtomAlertWindowStyle, "commandBar"), 28 | textAlign: "center", 29 | subclasses: { 30 | " button": this.buttonStyle, 31 | " .yes-button": { 32 | backgroundColor: Colors.rgba(0, 128, 0) 33 | }, 34 | " .no-button": { 35 | backgroundColor: Colors.rgba(255, 0, 0) 36 | } 37 | 38 | } 39 | }; 40 | } 41 | 42 | public get buttonStyle(): IStyleDeclaration { 43 | return { 44 | border: "none", 45 | color: Colors.white, 46 | width: "50%", 47 | height: "40px" 48 | }; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/tests/web/view-models/test.ts: -------------------------------------------------------------------------------- 1 | import { App } from "../../../App"; 2 | import { BindableProperty } from "../../../core/BindableProperty"; 3 | import Assert from "@web-atoms/unit-test/dist/Assert"; 4 | import Category from "@web-atoms/unit-test/dist/Category"; 5 | import Test from "@web-atoms/unit-test/dist/Test"; 6 | import TestItem from "@web-atoms/unit-test/dist/TestItem"; 7 | import { AtomViewModel, waitForReady, Watch } from "../../../view-model/AtomViewModel"; 8 | 9 | interface ICustomer { 10 | firstName: string; 11 | lastName: string; 12 | } 13 | 14 | class TestViewModel extends AtomViewModel { 15 | 16 | public errorText: string; 17 | 18 | @BindableProperty 19 | public customer: ICustomer; 20 | 21 | @Watch 22 | public get error(): string { 23 | if (this.customer) { 24 | if (!this.customer.firstName) { 25 | return "Firstname missing"; 26 | } 27 | if (!this.customer.lastName) { 28 | return "Lastname missing"; 29 | } 30 | } 31 | return "Data missing"; 32 | } 33 | 34 | @Watch 35 | public setError(): void { 36 | this.errorText = this.error; 37 | } 38 | } 39 | 40 | @Category("ViewModel") 41 | export class ViewModelTestCase extends TestItem { 42 | 43 | @Test 44 | public async watchTest(): Promise { 45 | 46 | const app = new App(); 47 | 48 | const tvm = new TestViewModel(app); 49 | await waitForReady(tvm); 50 | 51 | tvm.customer = { firstName: "", lastName: "Tss"}; 52 | 53 | Assert.equals("Firstname missing", tvm.errorText); 54 | 55 | tvm.customer = null; 56 | 57 | Assert.equals("Data missing", tvm.errorText); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/core/BindableProperty.ts: -------------------------------------------------------------------------------- 1 | import { AtomBinder } from "./AtomBinder"; 2 | import { INotifyPropertyChanging } from "./types"; 3 | 4 | /** 5 | * Use this decorator only to watch property changes in `onPropertyChanged` method. 6 | * This decorator also makes enumerable property. 7 | * 8 | * Do not use this on anything except UI control 9 | * @param target control 10 | * @param key name of property 11 | */ 12 | export function BindableProperty(target: any, key: string): any { 13 | // property value 14 | const iVal: any = target[key]; 15 | 16 | const keyName: string = "_" + key; 17 | 18 | target[keyName] = iVal; 19 | 20 | // property getter 21 | const getter: () => any = function(): any { 22 | // console.log(`Get: ${key} => ${_val}`); 23 | return this[keyName]; 24 | }; 25 | 26 | // property setter 27 | const setter: (v: any) => void = function(newVal: any): void { 28 | // console.log(`Set: ${key} => ${newVal}`); 29 | const oldValue = this[keyName]; 30 | // tslint:disable-next-line:triple-equals 31 | if (oldValue === undefined ? oldValue === newVal : oldValue == newVal) { 32 | return; 33 | } 34 | 35 | const ce = this as INotifyPropertyChanging; 36 | if (ce.onPropertyChanging) { 37 | ce.onPropertyChanging(key, oldValue, newVal); 38 | } 39 | 40 | this[keyName] = newVal; 41 | 42 | AtomBinder.refreshValue(this, key); 43 | }; 44 | 45 | // delete property 46 | if (delete target[key]) { 47 | 48 | // create new property with getter and setter 49 | Object.defineProperty(target, key, { 50 | get: getter, 51 | set: setter, 52 | enumerable: true, 53 | configurable: true 54 | }); 55 | 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/web/services/MarkdownService.ts: -------------------------------------------------------------------------------- 1 | import DISingleton from "../../di/DISingleton"; 2 | import { RegisterSingleton } from "../../di/RegisterSingleton"; 3 | import { Register } from "../../di/Register"; 4 | 5 | type Processor = [string, string, (s: string, e: string, t: string) => string ]; 6 | 7 | type Exp = [ RegExp, Processor?, Processor?, Processor?, Processor?, Processor?, Processor? ] | 8 | [ RegExp , (t: string) => string ] | 9 | [ RegExp , string ]; 10 | 11 | const regExps: Exp[] = [ 12 | [ /(\_{3})([^\_]+)(\_{3})/gmi, "$2" ], 13 | [ /(\_{2})([^\_]+)(\_{2})/gmi, "$2" ], 14 | [ /(\_{1})([^\_]+)(\_{1})/gmi, "$2" ], 15 | [ /(\*{3})([^\*]+)(\*{3})/gmi, "$2" ], 16 | [ /(\*{2})([^\*]+)(\*{2})/gmi, "$2" ], 17 | [ /(\*{1})([^\*]+)(\*{1})/gmi, "$2" ], 18 | [ /(\#{5})\s([^\n]+)/gmi, "
$2
"], 19 | [ /(\#{4})\s([^\n]+)/gmi, "

$2

"], 20 | [ /(\#{3})\s([^\n]+)/gmi, "

$2

"], 21 | [ /(\#{2})\s([^\n]+)/gmi, "

$2

"], 22 | [ /(\#{1})\s([^\n]+)/gmi, "

$2

"], 23 | [ /\n+/gmi, (t) => `
` ] 24 | ]; 25 | 26 | @DISingleton() 27 | export default class MarkdownService { 28 | 29 | public static instance: MarkdownService = new MarkdownService(); 30 | 31 | public toHtml(text: string): string { 32 | for (const iterator of regExps) { 33 | const reg = iterator[0]; 34 | if (iterator.length === 2) { 35 | const re = iterator[1]; 36 | if (typeof re === "string" || typeof re === "function") { 37 | text = text.replace(reg, re as any); 38 | } 39 | } 40 | } 41 | return text; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/web/samples/demo/views/MovieListViewModel.ts: -------------------------------------------------------------------------------- 1 | import { App } from "../../../../App"; 2 | import { AtomBinder } from "../../../../core/AtomBinder"; 3 | import { BindableProperty } from "../../../../core/BindableProperty"; 4 | import { Inject } from "../../../../di/Inject"; 5 | import { AtomViewModel, Validate } from "../../../../view-model/AtomViewModel"; 6 | import { WindowService } from "../../../../web/services/WindowService"; 7 | 8 | export interface IMovie { 9 | label: string; 10 | value?: string; 11 | category: string; 12 | } 13 | 14 | export class MovieListViewModel extends AtomViewModel { 15 | 16 | @BindableProperty 17 | public movies: IMovie[] = [ 18 | { label: "First", category: "None" }, 19 | { label: "True Lies", category: "Action" }, 20 | { label: "Jurassic Park", category: "Adventure" }, 21 | { label: "Big", category: "Kids" }, 22 | { label: "Inception", category: "Suspense" }, 23 | { label: "Last", category: "None" }, 24 | ]; 25 | 26 | @BindableProperty 27 | public selectedMovie: IMovie; 28 | 29 | constructor( 30 | @Inject app: App, 31 | @Inject private windowService: WindowService) { 32 | super(app); 33 | } 34 | 35 | @Validate 36 | public get errorSelectedMovie(): string { 37 | return this.selectedMovie ? "" : "Please select any movie"; 38 | } 39 | 40 | public onItemClick(data: IMovie): void { 41 | this.selectedMovie = data; 42 | } 43 | 44 | public async onDelete(data: IMovie): Promise { 45 | if (! (await this.windowService.confirm("Are you sure you want to delete?", "Confirm"))) { 46 | return; 47 | } 48 | AtomBinder.removeItem(this.movies, data); 49 | await this.windowService.alert("Movie deleted successfully."); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/node.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: actions/setup-node@v3.2.0 14 | with: 15 | node-version: 16 16 | registry-url: https://registry.npmjs.org/ 17 | - run: npm install 18 | - run: npm install -g codecov 19 | - run: npm install -D istanbul remap-istanbul @web-atoms/ts-to-systemjs 20 | - run: tsc 21 | # - run: node ./node_modules/@web-atoms/ts-to-systemjs/index.js 22 | # - run: typedoc --mode modules --out ./docs --tsconfig ./tsconfig.json src --excludeExternals --module umd --name web-atoms-core --excludePrivate --excludeNotExported --exclude "tests/**/*.ts" 23 | # - run: node ./node_modules/.bin/istanbul cover ./node_modules/@web-atoms/unit-test/index.js ./dist/test.js 24 | - run: npm run test 25 | - run: node ./node_modules/.bin/remap-istanbul -i ./coverage/coverage.json -t json -o ./coverage/coverage.json 26 | - run: codecov 27 | env: 28 | CODECOV_TOKEN: ${{secrets.codecov_token}} 29 | - run: node ./node_modules/@web-atoms/ts-to-systemjs/index.js 30 | - run: npm publish --access public 31 | env: 32 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 33 | # - run: npm install -D typescript typedocs 34 | # - run: npx typedoc --mode modules --out ./docs --tsconfig ./tsconfig.json src --excludeExternals --module umd --name @web-atoms/core --excludePrivate --excludeNotExported --exclude "**/tests/**/*" 35 | # - run: node ./core-docs/change-name.js 36 | # - run: npm publish --access public 37 | # env: 38 | # NODE_AUTH_TOKEN: ${{secrets.npm_token}} 39 | # - run: node ./core-docs/undo.js 40 | 41 | -------------------------------------------------------------------------------- /src/web/images/close-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/tests/AppTest.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Test from "@web-atoms/unit-test/dist/Test"; 3 | import { App } from "../App"; 4 | import { Atom } from "../Atom"; 5 | import { CancelToken } from "../core/types"; 6 | import { MockNavigationService } from "../services/MockNavigationService"; 7 | import { NavigationService } from "../services/NavigationService"; 8 | import { AtomTest } from "../unit/AtomTest"; 9 | 10 | class TestApp extends App { 11 | 12 | constructor() { 13 | super(); 14 | this.put(NavigationService, this.resolve(MockNavigationService)); 15 | } 16 | 17 | protected onReady(f: () => void): void { 18 | f(); 19 | } 20 | 21 | } 22 | 23 | export class AppTest extends AtomTest { 24 | 25 | @Test 26 | public async broadcastReceiveTest(): Promise { 27 | 28 | const app = this.app; 29 | 30 | await this.delay(100); 31 | 32 | let message = null; 33 | 34 | const d = app.subscribe("channel", (c, m) => { 35 | message = m; 36 | }); 37 | 38 | const d2 = app.subscribe("channel", (c, m) => { 39 | message = m; 40 | }); 41 | 42 | app.broadcast("channel", "hello world"); 43 | 44 | Assert.equals("hello world", message); 45 | 46 | app.broadcast("nothing", "nothing"); 47 | 48 | app.main(); 49 | 50 | d.dispose(); 51 | d2.dispose(); 52 | 53 | await Atom.delay(100); 54 | } 55 | 56 | @Test 57 | public async runAsync(): Promise { 58 | const app = this.app; 59 | 60 | app.runAsync( async () => { 61 | await this.delay(10); 62 | }); 63 | 64 | const ct = new CancelToken(); 65 | ct.cancel(); 66 | 67 | app.runAsync( async () => await Atom.delay(10, ct)); 68 | 69 | await Atom.delay(100); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/view-model/Once.ts: -------------------------------------------------------------------------------- 1 | import { App } from "../App"; 2 | import { AtomViewModel } from "./AtomViewModel"; 3 | import { registerInit } from "./baseTypes"; 4 | 5 | const timerSymbol = Symbol(); 6 | 7 | const Once = (timeInMS: number = 100) => 8 | (target: AtomViewModel, key: string | symbol): void => { 9 | registerInit(target, (vm) => { 10 | // tslint:disable-next-line: ban-types 11 | const oldMethod = vm[key] as Function; 12 | let keyTimer = vm[timerSymbol]; 13 | if (typeof keyTimer === "undefined") { 14 | keyTimer = {}; 15 | vm[timerSymbol] = keyTimer; 16 | } 17 | 18 | let running = false; 19 | 20 | // tslint:disable-next-line:only-arrow-functions 21 | vm[key] = ( ... a: any[]) => { 22 | if (running) { 23 | return; 24 | } 25 | const pending = keyTimer[key]; 26 | if (pending) { 27 | clearTimeout(pending); 28 | } 29 | keyTimer[key] = setTimeout((... b: any[]) => { 30 | running = true; 31 | delete keyTimer[key]; 32 | const r = oldMethod.apply(vm, b); 33 | if (r && r.then) { 34 | r.then(() => { 35 | running = false; 36 | }, (e) => { 37 | running = false; 38 | // tslint:disable-next-line: no-console 39 | console.warn(e); 40 | }); 41 | } else { 42 | running = false; 43 | } 44 | }, timeInMS, ... a); 45 | }; 46 | 47 | }); 48 | }; 49 | 50 | export default Once; 51 | -------------------------------------------------------------------------------- /src/xf/XFApp.ts: -------------------------------------------------------------------------------- 1 | import * as A from "../App"; 2 | import { AtomBridge } from "../core/AtomBridge"; 3 | import { BusyIndicatorService } from "../services/BusyIndicatorService"; 4 | import { NavigationService } from "../services/NavigationService"; 5 | import { AtomStyleSheet } from "../web/styles/AtomStyleSheet"; 6 | import XFBusyIndicatorService from "./services/XFBusyIndicatorService"; 7 | import XFNavigationService from "./services/XFNavigationService"; 8 | 9 | declare var bridge: any; 10 | 11 | export default class XFApp extends A.App { 12 | 13 | private mLastStyle: string = null; 14 | 15 | private mRoot: any; 16 | public get root(): any { 17 | return this.mRoot; 18 | } 19 | 20 | public set root(v: any) { 21 | this.mRoot = v; 22 | bridge.setRoot(v.element); 23 | } 24 | 25 | constructor() { 26 | super(); 27 | AtomBridge.instance = bridge; 28 | this.put(NavigationService, this.resolve(XFNavigationService)); 29 | this.put(BusyIndicatorService, this.resolve(XFBusyIndicatorService)); 30 | this.put(AtomStyleSheet, new AtomStyleSheet(this, "WA_")); 31 | 32 | const s = bridge.subscribe((channel, data) => { 33 | this.broadcast(channel, data); 34 | }); 35 | 36 | // register for messaging... 37 | const oldDispose = this.dispose; 38 | this.dispose = () => { 39 | s.dispose(); 40 | oldDispose.call(this); 41 | }; 42 | } 43 | 44 | public updateDefaultStyle(textContent: string) { 45 | if (!textContent) { return; } 46 | if (this.mLastStyle && this.mLastStyle === textContent) { 47 | return; 48 | } 49 | this.mLastStyle = textContent; 50 | bridge.updateDefaultStyle(textContent); 51 | } 52 | 53 | public broadcast(channel: string, data: any) { 54 | super.broadcast(channel, data); 55 | bridge.broadcast(channel, data); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/di/ServiceCollection.ts: -------------------------------------------------------------------------------- 1 | import { ArrayHelper } from "../core/types"; 2 | import { ServiceProvider } from "./ServiceProvider"; 3 | import { TypeKey } from "./TypeKey"; 4 | 5 | export type ServiceFactory = (sp: ServiceProvider) => any; 6 | 7 | export enum Scope { 8 | Global = 1, 9 | Scoped = 2, 10 | Transient = 3 11 | } 12 | 13 | export class ServiceDescription { 14 | 15 | constructor( 16 | public id: string, 17 | public scope: Scope, 18 | public type: any, 19 | public factory: ServiceFactory 20 | ) { 21 | this.factory = this.factory || ((sp) => { 22 | return (sp as any).create(type); 23 | }); 24 | } 25 | 26 | } 27 | 28 | export class ServiceCollection { 29 | 30 | public static instance: ServiceCollection = new ServiceCollection(); 31 | 32 | private registrations: ServiceDescription[] = []; 33 | 34 | private ids: number = 1; 35 | 36 | public register( 37 | type: any, 38 | factory: ServiceFactory, 39 | scope: Scope = Scope.Transient, 40 | id?: string): ServiceDescription { 41 | ArrayHelper.remove(this.registrations, (r) => id ? r.id === id : r.type === type); 42 | if (!id) { 43 | id = TypeKey.get(type); 44 | this.ids ++; 45 | } 46 | const sd = new ServiceDescription(id, scope, type, factory); 47 | this.registrations.push(sd); 48 | return sd; 49 | } 50 | 51 | public registerScoped(type: any, factory?: ServiceFactory, id?: string): ServiceDescription { 52 | return this.register(type, factory, Scope.Scoped, id); 53 | } 54 | 55 | public registerSingleton(type: any, factory?: ServiceFactory, id?: string): ServiceDescription { 56 | return this.register(type, factory, Scope.Global, id); 57 | } 58 | 59 | public get(type: any): ServiceDescription { 60 | return this.registrations.find( (s) => s.id === type || s.type === type); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/tests/web/core/AtomUITest.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Test from "@web-atoms/unit-test/dist/Test"; 3 | import { AtomUI } from "../../../web/core/AtomUI"; 4 | import AtomWebTest from "../../../unit/AtomWebTest"; 5 | 6 | export class AtomUITest extends AtomWebTest { 7 | 8 | @Test 9 | public newIndex(): void { 10 | const div = document.createElement("div"); 11 | div.id = "a"; 12 | 13 | const aid = AtomUI.assignID(div); 14 | 15 | Assert.equals("a", aid); 16 | 17 | const b = document.createElement("div"); 18 | const bid = AtomUI.assignID(b); 19 | 20 | Assert.equals("__waID" + 1002, bid); 21 | } 22 | 23 | @Test 24 | public parseUrl(): void { 25 | 26 | const a = AtomUI.parseUrl("a&=c&int=123&true=true&false=false&float=1.2"); 27 | 28 | Assert.equals(true, a.true); 29 | Assert.equals(false, a.false); 30 | Assert.equals(123, a.int); 31 | Assert.equals(1.2, a.float); 32 | } 33 | 34 | @Test 35 | public screenOffsetTest(): void { 36 | 37 | const e = {} as any; 38 | 39 | const child1 = {} as any; 40 | 41 | e.offsetLeft = 20; 42 | e.offsetTop = 20; 43 | e.offsetWidth = 80; 44 | e.offsetHeight = 80; 45 | 46 | child1.offsetLeft = 20; 47 | child1.offsetTop = 20; 48 | child1.offsetWidth = 60; 49 | child1.offsetHeight = 60; 50 | 51 | child1.offsetParent = e; 52 | 53 | const a = AtomUI.screenOffset(child1); 54 | 55 | Assert.equals(40, a.x); 56 | 57 | const ap = AtomUI.screenOffset(e); 58 | 59 | Assert.equals(20, ap.x); 60 | } 61 | 62 | @Test 63 | public toNumber(): void { 64 | Assert.equals(0, AtomUI.toNumber("")); 65 | Assert.equals(0, AtomUI.toNumber(null)); 66 | Assert.equals(0, AtomUI.toNumber("0")); 67 | // Assert.equals(1, AtomUI.toNumber(1 as any)); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/web/images/CloseButtonHoverDataUrl.ts: -------------------------------------------------------------------------------- 1 | import WebImage from "../../core/WebImage"; 2 | 3 | // tslint:disable 4 | 5 | const base64 = ["PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCEtLSBHZW5lcmF0b3I6", 6 | "IEFkb2JlIElsbHVzdHJhdG9yIDE5LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246", 7 | "IDYuMDAgQnVpbGQgMCkgIC0tPg0KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4bWxucz0i", 8 | "aHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8x", 9 | "OTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCINCgkgdmlld0JveD0iMCAwIDUxMiA1MTIiIHN0eWxlPSJl", 10 | "bmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDUxMiA1MTI7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxj", 11 | "aXJjbGUgc3R5bGU9ImZpbGw6I0UyMUIxQjsiIGN4PSIyNTYiIGN5PSIyNTYiIHI9IjI1NiIvPg0KPHBh", 12 | "dGggc3R5bGU9ImZpbGw6I0M0MDYwNjsiIGQ9Ik01MTAuMjgsMjg1LjMwNEwzNjcuOTEyLDE0Mi45MzZM", 13 | "MTUwLjI0OCwzNjguNjA4bDE0MC45MjgsMTQwLjkyOA0KCUM0MDYuMzUyLDQ5My42OTYsNDk3LjA1Niw0", 14 | "MDEuMjg4LDUxMC4yOCwyODUuMzA0eiIvPg0KPGc+DQoJPHBhdGggc3R5bGU9ImZpbGw6I0ZGRkZGRjsi", 15 | "IGQ9Ik0zNTQuMzc2LDM3MS41MzZjLTUuMTIsMC0xMC4yMzItMS45NTItMTQuMTQ0LTUuODU2TDE0Ni40", 16 | "MDgsMTcxLjg0OA0KCQljLTcuODE2LTcuODE2LTcuODE2LTIwLjQ3MiwwLTI4LjI4czIwLjQ3Mi03Ljgx", 17 | "NiwyOC4yOCwwTDM2OC41MiwzMzcuNGM3LjgxNiw3LjgxNiw3LjgxNiwyMC40NzIsMCwyOC4yOA0KCQlD", 18 | "MzY0LjYwOCwzNjkuNTg0LDM1OS40OTYsMzcxLjUzNiwzNTQuMzc2LDM3MS41MzZ6Ii8+DQoJPHBhdGgg", 19 | "c3R5bGU9ImZpbGw6I0ZGRkZGRjsiIGQ9Ik0xNjAuNTQ0LDM3MS41MzZjLTUuMTIsMC0xMC4yMzItMS45", 20 | "NTItMTQuMTQ0LTUuODU2Yy03LjgxNi03LjgxNi03LjgxNi0yMC40NzIsMC0yOC4yOA0KCQlsMTkzLjgz", 21 | "Mi0xOTMuODMyYzcuODE2LTcuODE2LDIwLjQ3Mi03LjgxNiwyOC4yOCwwczcuODE2LDIwLjQ3MiwwLDI4", 22 | "LjI4TDE3NC42ODgsMzY1LjY4DQoJCUMxNzAuNzg0LDM2OS41ODQsMTY1LjY2NCwzNzEuNTM2LDE2MC41", 23 | "NDQsMzcxLjUzNnoiLz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0K", 24 | "PC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4N", 25 | "CjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4N", 26 | "CjwvZz4NCjwvc3ZnPg0K"]; 27 | 28 | export default new WebImage(`data:image/svg+xml;base64,${base64.join("")}`); 29 | -------------------------------------------------------------------------------- /src/di/Register.ts: -------------------------------------------------------------------------------- 1 | import { DI } from "../core/types"; 2 | import { IMockOrInject } from "./IMockOrInject"; 3 | import { Scope, ServiceCollection } from "./ServiceCollection"; 4 | 5 | export interface IServiceDef { 6 | id?: string; 7 | scope: Scope; 8 | for?: any; 9 | mockOrInject?: IMockOrInject; 10 | } 11 | 12 | declare var global: any; 13 | declare var window: any; 14 | 15 | const globalNS = (typeof global === "undefined") ? window : global; 16 | 17 | function evalGlobal(path: string | any) { 18 | if (typeof path === "string") { 19 | let r = globalNS; 20 | for (const iterator of path.split(".")) { 21 | r = r[iterator]; 22 | if (r === undefined || r === null) { 23 | return r; 24 | } 25 | } 26 | return r; 27 | } 28 | return path; 29 | } 30 | 31 | export function Register(def: IServiceDef): ((t: any) => void); 32 | export function Register(id: string, scope: Scope): ((t: any) => void); 33 | export function Register(id: string | IServiceDef, scope?: Scope): ((t: any) => void) { 34 | return (target: any) => { 35 | if (typeof id === "object") { 36 | if (scope) { 37 | id.scope = scope; 38 | } 39 | ServiceCollection.instance.register(id.for || target, id.for ? (sp) => (sp as any).create(target) : null, 40 | id.scope || Scope.Transient, id.id); 41 | 42 | if (id.mockOrInject) { 43 | if (id.mockOrInject.inject) { 44 | DI.inject(target, id.mockOrInject.inject); 45 | } else if (id.mockOrInject.mock) { 46 | DI.mockType(target, id.mockOrInject.mock); 47 | } else if (id.mockOrInject.globalVar) { 48 | ServiceCollection.instance.register( 49 | id.for || target, (sp) => evalGlobal(id.mockOrInject.globalVar), 50 | id.scope || Scope.Global, id.id); 51 | } 52 | } 53 | 54 | return; 55 | } 56 | ServiceCollection.instance.register(target, null, scope, id); 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/view-model/bindUrlParameter.ts: -------------------------------------------------------------------------------- 1 | import { AtomDisposableList } from "../core/AtomDisposableList"; 2 | import { AtomOnce } from "../core/AtomOnce"; 3 | import { AtomUri } from "../core/AtomUri"; 4 | import { IDisposable } from "../core/types"; 5 | import { AtomViewModel } from "./AtomViewModel"; 6 | /** 7 | * Binds property of View Model to URL Parameter, it can read query string as well, 8 | * however it will only persist value in hash 9 | * @param vm View Model 10 | * @param name property name of View Model 11 | * @param urlParameter url parameter name 12 | */ 13 | export default function bindUrlParameter(vm: AtomViewModel, name: string, urlParameter: string): IDisposable { 14 | if (!name) { 15 | return; 16 | } 17 | if (!urlParameter) { 18 | return; 19 | } 20 | const a = vm as any; 21 | const paramDisposables = (a.mUrlParameters || (a.mUrlParameters = {})); 22 | const old = paramDisposables[name]; 23 | if (old) { 24 | old.dispose(); 25 | paramDisposables[name] = null; 26 | } 27 | const disposables = new AtomDisposableList(); 28 | const updater = new AtomOnce(); 29 | disposables.add(a.setupWatch([ 30 | ["app", "url", "hash", urlParameter], 31 | ["app", "url", "query", urlParameter] 32 | ], (hash, query) => { 33 | updater.run(() => { 34 | const value = hash || query; 35 | if (value) { 36 | // tslint:disable-next-line:triple-equals 37 | if (value != vm[name]) { 38 | vm[name] = value; 39 | } 40 | } 41 | }); 42 | })); 43 | disposables.add(a.setupWatch([[name]], (value) => { 44 | updater.run(() => { 45 | const url = vm.app.url || (vm.app.url = new AtomUri("")); 46 | url.hash[urlParameter] = value; 47 | vm.app.syncUrl(); 48 | }); 49 | })); 50 | paramDisposables[name] = disposables; 51 | if (vm.app.url) { 52 | const v = vm.app.url.hash[urlParameter] || vm.app.url.query[urlParameter]; 53 | if (v) { 54 | vm[name] = v; 55 | } 56 | } 57 | return vm.registerDisposable(disposables); 58 | } 59 | -------------------------------------------------------------------------------- /src/core/AtomOnce.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * AtomOnce will execute given method only once and it will prevent recursive calls. 3 | * This is important when you want to update source and destination and prevent recursive calls. 4 | * @example 5 | * private valueOnce: AtomOnce = new AtomOnce(); 6 | * 7 | * private onValueChanged(): void { 8 | * valueOnce.run(()=> { 9 | * this.value = compute(this.target); 10 | * }); 11 | * } 12 | * 13 | * private onTargetChanged(): void { 14 | * valueOnce.run(() => { 15 | * this.target = computeInverse(this.value); 16 | * }); 17 | * } 18 | */ 19 | export class AtomOnce { 20 | 21 | private isRunning: boolean; 22 | /** 23 | * AtomOnce will execute given method only once and it will prevent recursive calls. 24 | * This is important when you want to update source and destination and prevent recursive calls. 25 | * @example 26 | * private valueOnce: AtomOnce = new AtomOnce(); 27 | * 28 | * private onValueChanged(): void { 29 | * valueOnce.run(()=> { 30 | * this.value = compute(this.target); 31 | * }); 32 | * } 33 | * 34 | * private onTargetChanged(): void { 35 | * valueOnce.run(() => { 36 | * this.target = computeInverse(this.value); 37 | * }); 38 | * } 39 | */ 40 | public run(f: () => any): void { 41 | if (this.isRunning) { 42 | return; 43 | } 44 | let isAsync: boolean = false; 45 | try { 46 | this.isRunning = true; 47 | const p = f() as Promise; 48 | if (p && p.then && p.catch) { 49 | isAsync = true; 50 | p.then(() => { 51 | this.isRunning = false; 52 | }).catch(() => { 53 | this.isRunning = false; 54 | }); 55 | } 56 | } finally { 57 | if (!isAsync) { 58 | this.isRunning = false; 59 | } 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/web/styles/AtomTheme.ts: -------------------------------------------------------------------------------- 1 | import { AtomWindowStyle } from "./AtomWindowStyle"; 2 | 3 | import { AtomPopupStyle } from "./AtomPopupStyle"; 4 | 5 | import { App } from "../../App"; 6 | import { BindableProperty } from "../../core/BindableProperty"; 7 | import Color from "../../core/Color"; 8 | import Colors, { ColorItem } from "../../core/Colors"; 9 | import { IDisposable, INotifyPropertyChanging } from "../../core/types"; 10 | import { Inject } from "../../di/Inject"; 11 | import { RegisterSingleton } from "../../di/RegisterSingleton"; 12 | import { NavigationService } from "../../services/NavigationService"; 13 | import { AtomListBox } from "../controls/AtomListBox"; 14 | import { AtomWindow } from "../controls/AtomWindow"; 15 | import { AtomStyleSheet } from "../styles/AtomStyleSheet"; 16 | import { AtomListBoxStyle } from "./AtomListBoxStyle"; 17 | 18 | @RegisterSingleton 19 | export class AtomTheme extends AtomStyleSheet 20 | implements 21 | INotifyPropertyChanging, 22 | IDisposable { 23 | 24 | public bgColor: ColorItem = Colors.white; 25 | 26 | public color: ColorItem = Colors.gray; 27 | 28 | public hoverColor: ColorItem = Colors.lightGray; 29 | 30 | public activeColor: ColorItem = Colors.lightBlue; 31 | 32 | public selectedBgColor: ColorItem = Colors.blue; 33 | 34 | public selectedColor: ColorItem = Colors.white; 35 | 36 | public padding: number = 5; 37 | 38 | // public readonly window = this.createStyle(AtomWindow, AtomWindowStyle, "window"); 39 | 40 | // public readonly popup = this.createNamedStyle(AtomPopupStyle, "popup"); 41 | 42 | constructor( 43 | @Inject app: App, 44 | @Inject private navigationService: NavigationService) { 45 | super(app, "atom-theme"); 46 | 47 | setTimeout(() => { 48 | window.addEventListener("resize", () => { 49 | setTimeout(() => { 50 | this.pushUpdate(); 51 | }, 10); 52 | }); 53 | document.body.addEventListener("resize", () => { 54 | setTimeout(() => { 55 | this.pushUpdate(); 56 | }, 10); 57 | }); 58 | }, 1000); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/core/StringHelper.ts: -------------------------------------------------------------------------------- 1 | export class StringHelper { 2 | 3 | public static escapeRegExp(text: string) { 4 | return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 5 | } 6 | 7 | public static createContainsRegExp(text) { 8 | return new RegExp(this.escapeRegExp(text), "i"); 9 | } 10 | 11 | public static createContainsAnyWordRegExp(text: string) { 12 | return new RegExp(text.split(/\s+/g).map((x) => `(${this.escapeRegExp(x)})`).join("|"), "i"); 13 | } 14 | 15 | public static containsIgnoreCase(source: string, test: string) { 16 | if (!source) { 17 | return false; 18 | } 19 | if (!test) { 20 | return true; 21 | } 22 | return this.createContainsRegExp(test).test(source); 23 | } 24 | 25 | public static containsAnyWordIgnoreCase(source: string, test: string) { 26 | if (!source) { 27 | return false; 28 | } 29 | if (!test) { 30 | return true; 31 | } 32 | return this.createContainsAnyWordRegExp(test).test(source); 33 | } 34 | 35 | public static fromCamelToHyphen(input: string): string { 36 | return input.replace( /([a-z])([A-Z])/g, "$1-$2" ).toLowerCase(); 37 | } 38 | 39 | public static fromCamelToUnderscore(input: string): string { 40 | return input.replace( /([a-z])([A-Z])/g, "$1_$2" ).toLowerCase(); 41 | } 42 | 43 | public static fromCamelToPascal(input: string): string { 44 | return input[0].toUpperCase() + input.substr(1); 45 | } 46 | 47 | public static fromHyphenToCamel(input: string): string { 48 | return input.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); 49 | } 50 | 51 | public static fromUnderscoreToCamel(input: string): string { 52 | return input.replace(/\_([a-z])/g, (g) => g[1].toUpperCase()); 53 | } 54 | 55 | public static fromPascalToCamel(input: string): string { 56 | return input[0].toLowerCase() + input.substr(1); 57 | } 58 | 59 | public static fromPascalToTitleCase(s: string) { 60 | return s.replace(/([A-Z])/gm, (x, g, i) => i ? " " + x : x); 61 | }; 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/web/controls/AtomViewPager.ts: -------------------------------------------------------------------------------- 1 | import { AtomBinder } from "../../core/AtomBinder"; 2 | import { AtomLoader } from "../../core/AtomLoader"; 3 | import { AtomUri } from "../../core/AtomUri"; 4 | import { BindableProperty } from "../../core/BindableProperty"; 5 | import { IClassOf } from "../../core/types"; 6 | import { JsonService } from "../../services/JsonService"; 7 | import { AtomContentControl } from "./AtomContentControl"; 8 | import { AtomControl } from "./AtomControl"; 9 | import { AtomItemsControl } from "./AtomItemsControl"; 10 | 11 | export class AtomViewPager extends AtomItemsControl { 12 | 13 | public dispose(e?: HTMLElement): void { 14 | if (!e) { 15 | for (const iterator of this.items) { 16 | iterator.dispose(); 17 | } 18 | } 19 | this.selectedItem = null; 20 | super.dispose(e); 21 | } 22 | 23 | public onCollectionChanged(): void { 24 | // do nothing... 25 | } 26 | 27 | protected create(): void { 28 | super.create(); 29 | 30 | const eStyle = this.element.style; 31 | eStyle.position = "absolute"; 32 | eStyle.left = eStyle.right = eStyle.bottom = eStyle.top = "0"; 33 | 34 | const cc = new AtomContentControl(this.app); 35 | this.append(cc); 36 | const style = cc.element.style; 37 | style.position = "absolute"; 38 | style.top = style.left = style.right = style.bottom = "0"; 39 | 40 | cc.bind(cc.element, "content", [["this", "selectedItem"]], false, (si) => { 41 | if (!si) { 42 | return undefined; 43 | } 44 | if (si.view) { 45 | return si.view; 46 | } 47 | this.app.runAsync( async () => { 48 | const { view: ctrl } = await AtomLoader.loadView(new AtomUri(si.value), this.app, false); 49 | si.view = ctrl; 50 | ctrl.element._logicalParent = this.element; 51 | AtomBinder.refreshValue(this, "selectedItem"); 52 | si.dispose = () => { 53 | ctrl.dispose(); 54 | ctrl.element._logicalParent = null; 55 | }; 56 | }); 57 | return undefined; 58 | }, this); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/tests/core/ColorTests.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Test from "@web-atoms/unit-test/dist/Test"; 3 | import TestItem from "@web-atoms/unit-test/dist/TestItem"; 4 | import Colors, { ColorItem } from "../../core/Colors"; 5 | 6 | export class TestCase extends TestItem { 7 | 8 | @Test 9 | public parse(): void { 10 | Assert.equals("#ff0000", Colors.red.colorCode); 11 | Assert.equals("#ff0000", Colors.parse("red").colorCode); 12 | Assert.equals("#ff0000", Colors.rgba(255, 0, 0).colorCode); 13 | Assert.equals("#ff0000", Colors.rgba(255, 0, 0).colorCode); 14 | Assert.equals("#ff0000", Colors.rgba(255, 0, 0).toString()); 15 | 16 | let ci = new ColorItem(0, 255, 0); 17 | Assert.equals("#00ff00", ci.colorCode); 18 | 19 | ci = new ColorItem(0, 255, 0, 0); 20 | Assert.equals("rgba(0,255,0,0)", ci.colorCode); 21 | 22 | ci = new ColorItem(0, 255, 0, 0.5); 23 | Assert.equals("rgba(0,255,0,0.5)", ci.colorCode); 24 | 25 | const r = Colors.parse("#ff0000"); 26 | Assert.equals(255, r.red); 27 | Assert.equals(0, r.green); 28 | Assert.equals(0, r.blue); 29 | 30 | const white = Colors.parse("#fff"); 31 | Assert.equals(255, white.red); 32 | Assert.equals(255, white.blue); 33 | Assert.equals(255, white.green); 34 | 35 | const whiteWithAlpha = Colors.parse("#ff0000ff"); 36 | Assert.equals(255, whiteWithAlpha.red); 37 | Assert.equals(0, whiteWithAlpha.green); 38 | Assert.equals(0, whiteWithAlpha.blue); 39 | 40 | const rgba = Colors.parse("rgba(100,200,255,0.5)"); 41 | Assert.equals(100, rgba.red); 42 | Assert.equals(200, rgba.green); 43 | Assert.equals(255, rgba.blue); 44 | Assert.equals(0.5, rgba.alpha); 45 | 46 | const rgba1 = rgba.withAlphaPercent(0.2); 47 | Assert.equals(0.2, rgba1.alpha); 48 | 49 | const rgb = Colors.parse("rgb(100,200,255)"); 50 | Assert.equals(100, rgb.red); 51 | Assert.equals(200, rgb.green); 52 | Assert.equals(255, rgb.blue); 53 | 54 | Assert.throws("Invalid color format aaa", () => Colors.parse("aaa")); 55 | 56 | Assert.throws("Unknown color format aaa", () => new ColorItem("aaa")); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/web/styles/AtomStyleSheet.ts: -------------------------------------------------------------------------------- 1 | import { App } from "../../App"; 2 | import { IClassOf, INameValuePairs, INotifyPropertyChanging } from "../../core/types"; 3 | import { TypeKey } from "../../di/TypeKey"; 4 | import { AtomStyle } from "./AtomStyle"; 5 | 6 | export class AtomStyleSheet implements INotifyPropertyChanging { 7 | 8 | public styleElement: any; 9 | 10 | public styles: {[key: string]: AtomStyle} = {}; 11 | 12 | private lastUpdateId: any = 0; 13 | 14 | private isAttaching: boolean = false; 15 | 16 | constructor(public readonly app: App, public readonly name: string) { 17 | this.pushUpdate(0); 18 | } 19 | 20 | public getNamedStyle(c: IClassOf): AtomStyle { 21 | const name = TypeKey.getName(c); 22 | return this.createNamedStyle(c, name); 23 | } 24 | 25 | public createNamedStyle(c: IClassOf, name: string, updateTimeout?: number): T { 26 | const style = this.styles[name] = new (c)(this, `${this.name}-${name}`); 27 | style.build(); 28 | this.pushUpdate(updateTimeout); 29 | return style; 30 | } 31 | 32 | public onPropertyChanging(name: string, newValue: any, oldValue: any): void { 33 | this.pushUpdate(); 34 | } 35 | 36 | public pushUpdate(delay: number = 1): void { 37 | if (this.isAttaching) { 38 | return; 39 | } 40 | 41 | // special case for Xamarin Forms 42 | if (delay === 0) { 43 | this.attach(); 44 | return; 45 | } 46 | if (this.lastUpdateId) { 47 | clearTimeout(this.lastUpdateId); 48 | } 49 | this.lastUpdateId = setTimeout(() => { 50 | this.attach(); 51 | }, delay); 52 | } 53 | 54 | public dispose(): void { 55 | if (this.styleElement) { 56 | this.styleElement.remove(); 57 | } 58 | } 59 | 60 | public attach(): void { 61 | this.isAttaching = true; 62 | const text = []; 63 | for (const key in this.styles) { 64 | if (this.styles.hasOwnProperty(key)) { 65 | const element = this.styles[key]; 66 | text.push(element.toString()); 67 | } 68 | } 69 | const textContent = text.join("\n"); 70 | this.app.updateDefaultStyle(textContent); 71 | this.isAttaching = false; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // // tslint:disable:no-console 2 | // // tslint:disable:ordered-imports 3 | 4 | // declare var require: any; 5 | // declare var __dirname: any; 6 | 7 | // // import unit test modules here 8 | // import TestRunner from "@web-atoms/unit-test/dist/TestRunner"; 9 | // // tslint:disable-next-line:no-var-requires 10 | // const { statSync, readdirSync } = require("fs") as any; 11 | // // tslint:disable-next-line:no-var-requires 12 | // const path = require("path"); 13 | 14 | // // tslint:disable-next-line: no-var-requires 15 | // const Module = require("module"); 16 | // // tslint:disable-next-line: ban-types 17 | // const oldr: Function = Module.prototype.require; 18 | // const r = function(name) { 19 | // if (/\.(svg|jpg|gif|png)$/i.test(name)) { 20 | // return name; 21 | // } 22 | // return oldr.call(this, name); 23 | // }; 24 | // r.resolve = (oldr as any).resolve; 25 | // Module.prototype.require = r; 26 | 27 | // // import "./tests/AtomClassTest"; 28 | // // import "./tests/AppTest"; 29 | // // import "./tests/core/ColorTests"; 30 | // // import "./tests/core/AtomBinderTest"; 31 | // // import "./tests/core/StringHelperTests"; 32 | // // import "./tests/core/PropertyBinderTest"; 33 | // // import "./tests/services/JsonServiceTest"; 34 | // // import "./tests/web/window/WindowTest"; 35 | 36 | // declare var global: any; 37 | // (global as any).CustomEvent = function CustomEvent(type: string, p?: any) { 38 | // const e = document.createEvent("CustomEvent"); 39 | // const pe = p ? { ... p } : {}; 40 | // e.initCustomEvent(type, pe.bubble, pe.cancelable, pe.detail); 41 | // return e; 42 | // }; 43 | 44 | // function loadScripts(start) { 45 | // for (const item of readdirSync(start)) { 46 | // const file = `${start}/${item}`; 47 | // // const file = item; 48 | // const s = statSync(file); 49 | // if (s.isDirectory()) { 50 | // loadScripts(file); 51 | // continue; 52 | // } 53 | 54 | // if (file.endsWith(".js")) { 55 | // const md = file.substr(0, file.length - 3); 56 | // require("." + md); 57 | // } 58 | // } 59 | // } 60 | 61 | // loadScripts("./dist/tests"); 62 | 63 | // const instance: TestRunner = TestRunner.instance; 64 | 65 | // // export Atom; 66 | // declare var process: any; 67 | 68 | // instance.run().then(() => { 69 | // process.exit(); 70 | // }).catch( (e) => { 71 | // console.error(e.message); 72 | // if (e.stack) { 73 | // console.error(e.stack); 74 | // } 75 | // process.exit(1); 76 | // }); 77 | -------------------------------------------------------------------------------- /src/web/controls/AtomListBox.ts: -------------------------------------------------------------------------------- 1 | import { BindableProperty } from "../../core/BindableProperty"; 2 | import { AtomUI, ChildEnumerator } from "../../web/core/AtomUI"; 3 | import { AtomListBoxStyle } from "../styles/AtomListBoxStyle"; 4 | import { AtomControl } from "./AtomControl"; 5 | import { AtomItemsControl } from "./AtomItemsControl"; 6 | 7 | export class AtomListBox extends AtomItemsControl { 8 | 9 | public selectItemOnClick: boolean; 10 | 11 | protected preCreate(): void { 12 | this.selectItemOnClick = true; 13 | super.preCreate(); 14 | this.defaultControlStyle = AtomListBoxStyle; 15 | this.registerItemClick(); 16 | this.runAfterInit(() => this.setElementClass(this.element, { 17 | [this.controlStyle.name]: 1, 18 | "atom-list-box": 1 19 | })); 20 | } 21 | 22 | protected registerItemClick(): void { 23 | this.bindEvent(this.element, "click", (e) => { 24 | const p = this.atomParent(e.target as HTMLElement); 25 | if (p === this) { 26 | return; 27 | } 28 | if (p.element._logicalParent === this.element) { 29 | // this is child.. 30 | const data = p.data; 31 | if (!data) { 32 | return; 33 | } 34 | if (this.selectItemOnClick) { 35 | this.toggleSelection(data); 36 | const ce = new CustomEvent("selectionChanged", { 37 | bubbles: false, 38 | cancelable: false, 39 | detail: data }); 40 | this.element.dispatchEvent(ce); 41 | } 42 | } 43 | }); 44 | } 45 | 46 | protected createChild(df: DocumentFragment, data: any): AtomControl { 47 | const child = super.createChild(df, data); 48 | child.bind(child.element, "styleClass", 49 | [ 50 | ["this", "version"], 51 | ["data"], 52 | ["this", "selectedItems"] 53 | ], 54 | false, 55 | (version, itemData, selectedItems: any[]) => { 56 | return { 57 | "list-item": true, 58 | "item": true, 59 | "selected-item": selectedItems 60 | && selectedItems.find((x) => x === itemData), 61 | "selected-list-item": selectedItems 62 | && selectedItems.find((x) => x === itemData) 63 | }; 64 | }, 65 | this); 66 | return child; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/web/images/ButtonDataUrl.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable 2 | import WebImage from "../../core/WebImage"; 3 | 4 | const base64 = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCEtLSBHZW5lcmF0b3I6"+ 5 | "IEFkb2JlIElsbHVzdHJhdG9yIDE5LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246"+ 6 | "IDYuMDAgQnVpbGQgMCkgIC0tPg0KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJDYXBhXzEiIHhtbG5zPSJo"+ 7 | "dHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5"+ 8 | "OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgNDU1Ljk5MiA0NTUuOTkyIiBz"+ 9 | "dHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0NTUuOTkyIDQ1NS45OTI7IiB4bWw6c3BhY2U9"+ 10 | "InByZXNlcnZlIj4NCjxnPg0KCTxnPg0KCQk8Zz4NCgkJCTxnPg0KCQkJCTxwYXRoIHN0eWxlPSJmaWxs"+ 11 | "OiMwMTAwMDI7IiBkPSJNMjI3Ljk5NiwwQzEwMi4wODEsMCwwLDEwMi4wODEsMCwyMjcuOTk2YzAsMTI1"+ 12 | "Ljk0NSwxMDIuMDgxLDIyNy45OTYsMjI3Ljk5NiwyMjcuOTk2DQoJCQkJCWMxMjUuOTQ1LDAsMjI3Ljk5"+ 13 | "Ni0xMDIuMDUxLDIyNy45OTYtMjI3Ljk5NkM0NTUuOTkyLDEwMi4wODEsMzUzLjk0MSwwLDIyNy45OTYs"+ 14 | "MHogTTIyNy45OTYsNDI1LjU5Mw0KCQkJCQljLTEwOC45NTIsMC0xOTcuNTk3LTg4LjY0NS0xOTcuNTk3"+ 15 | "LTE5Ny41OTdTMTE5LjA0NCwzMC4zOTksMjI3Ljk5NiwzMC4zOTlzMTk3LjU5Nyw4OC42NDUsMTk3LjU5"+ 16 | "NywxOTcuNTk3DQoJCQkJCVMzMzYuOTQ4LDQyNS41OTMsMjI3Ljk5Niw0MjUuNTkzeiIvPg0KCQkJCTxw"+ 17 | "YXRoIHN0eWxlPSJmaWxsOiMwMTAwMDI7IiBkPSJNMzEyLjE0MiwxMjIuMzU4bC04My41MzgsODMuNTY4"+ 18 | "bC03NC45NjUtODMuNTY4Yy01LjkyOC01LjkyOC0xNS41NjUtNS45MjgtMjEuNDkyLDANCgkJCQkJYy01"+ 19 | "LjkyOCw1LjkyOC01LjkyOCwxNS41NjUsMCwyMS40OTJsNzQuOTY1LDgzLjU2OGwtODQuNzIzLDg0Ljcy"+ 20 | "M2MtNS45MjgsNS45MjgtNS45MjgsMTUuNTk1LDAsMjEuNDkyDQoJCQkJCWM1LjkyOCw1LjkyOCwxNS41"+ 21 | "NjUsNS45MjgsMjEuNDkyLDBsODMuNTY4LTgzLjUzOGw3NC45NjUsODMuNTM4YzUuODk3LDUuOTI4LDE1"+ 22 | "LjU2NSw1LjkyOCwyMS40NjIsMA0KCQkJCQljNS45MjgtNS44OTgsNS45MjgtMTUuNTY1LDAtMjEuNDky"+ 23 | "bC03NC45OTUtODMuNTM4bDg0LjcyMy04NC43NTRjNS45MjgtNS45MjgsNS45MjgtMTUuNTY1LDAtMjEu"+ 24 | "NDkyDQoJCQkJCUMzMjcuNjc2LDExNi40MywzMTguMDcsMTE2LjQzLDMxMi4xNDIsMTIyLjM1OHoiLz4N"+ 25 | "CgkJCTwvZz4NCgkJPC9nPg0KCTwvZz4NCgk8Zz4NCgk8L2c+DQoJPGc+DQoJPC9nPg0KCTxnPg0KCTwv"+ 26 | "Zz4NCgk8Zz4NCgk8L2c+DQoJPGc+DQoJPC9nPg0KCTxnPg0KCTwvZz4NCgk8Zz4NCgk8L2c+DQoJPGc+"+ 27 | "DQoJPC9nPg0KCTxnPg0KCTwvZz4NCgk8Zz4NCgk8L2c+DQoJPGc+DQoJPC9nPg0KCTxnPg0KCTwvZz4N"+ 28 | "Cgk8Zz4NCgk8L2c+DQoJPGc+DQoJPC9nPg0KCTxnPg0KCTwvZz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+"+ 29 | "DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9n"+ 30 | "Pg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxn"+ 31 | "Pg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjwvc3ZnPg0K"; 32 | 33 | export default new WebImage(`data:image/svg+xml;base64,${base64}`); 34 | -------------------------------------------------------------------------------- /src/web/images/CloseButtonDataUrl.ts: -------------------------------------------------------------------------------- 1 | import WebImage from "../../core/WebImage"; 2 | 3 | // tslint:disable 4 | 5 | const base64 = ["PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCEtLSBHZW5lcmF0b3I6", 6 | "IEFkb2JlIElsbHVzdHJhdG9yIDE5LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246", 7 | "IDYuMDAgQnVpbGQgMCkgIC0tPg0KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJDYXBhXzEiIHhtbG5zPSJo", 8 | "dHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5", 9 | "OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgNDU1Ljk5MiA0NTUuOTkyIiBz", 10 | "dHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0NTUuOTkyIDQ1NS45OTI7IiB4bWw6c3BhY2U9", 11 | "InByZXNlcnZlIj4NCjxnPg0KCTxnPg0KCQk8Zz4NCgkJCTxnPg0KCQkJCTxwYXRoIHN0eWxlPSJmaWxs", 12 | "OiMwMTAwMDI7IiBkPSJNMjI3Ljk5NiwwQzEwMi4wODEsMCwwLDEwMi4wODEsMCwyMjcuOTk2YzAsMTI1", 13 | "Ljk0NSwxMDIuMDgxLDIyNy45OTYsMjI3Ljk5NiwyMjcuOTk2DQoJCQkJCWMxMjUuOTQ1LDAsMjI3Ljk5", 14 | "Ni0xMDIuMDUxLDIyNy45OTYtMjI3Ljk5NkM0NTUuOTkyLDEwMi4wODEsMzUzLjk0MSwwLDIyNy45OTYs", 15 | "MHogTTIyNy45OTYsNDI1LjU5Mw0KCQkJCQljLTEwOC45NTIsMC0xOTcuNTk3LTg4LjY0NS0xOTcuNTk3", 16 | "LTE5Ny41OTdTMTE5LjA0NCwzMC4zOTksMjI3Ljk5NiwzMC4zOTlzMTk3LjU5Nyw4OC42NDUsMTk3LjU5", 17 | "NywxOTcuNTk3DQoJCQkJCVMzMzYuOTQ4LDQyNS41OTMsMjI3Ljk5Niw0MjUuNTkzeiIvPg0KCQkJCTxw", 18 | "YXRoIHN0eWxlPSJmaWxsOiMwMTAwMDI7IiBkPSJNMzEyLjE0MiwxMjIuMzU4bC04My41MzgsODMuNTY4", 19 | "bC03NC45NjUtODMuNTY4Yy01LjkyOC01LjkyOC0xNS41NjUtNS45MjgtMjEuNDkyLDANCgkJCQkJYy01", 20 | "LjkyOCw1LjkyOC01LjkyOCwxNS41NjUsMCwyMS40OTJsNzQuOTY1LDgzLjU2OGwtODQuNzIzLDg0Ljcy", 21 | "M2MtNS45MjgsNS45MjgtNS45MjgsMTUuNTk1LDAsMjEuNDkyDQoJCQkJCWM1LjkyOCw1LjkyOCwxNS41", 22 | "NjUsNS45MjgsMjEuNDkyLDBsODMuNTY4LTgzLjUzOGw3NC45NjUsODMuNTM4YzUuODk3LDUuOTI4LDE1", 23 | "LjU2NSw1LjkyOCwyMS40NjIsMA0KCQkJCQljNS45MjgtNS44OTgsNS45MjgtMTUuNTY1LDAtMjEuNDky", 24 | "bC03NC45OTUtODMuNTM4bDg0LjcyMy04NC43NTRjNS45MjgtNS45MjgsNS45MjgtMTUuNTY1LDAtMjEu", 25 | "NDkyDQoJCQkJCUMzMjcuNjc2LDExNi40MywzMTguMDcsMTE2LjQzLDMxMi4xNDIsMTIyLjM1OHoiLz4N", 26 | "CgkJCTwvZz4NCgkJPC9nPg0KCTwvZz4NCgk8Zz4NCgk8L2c+DQoJPGc+DQoJPC9nPg0KCTxnPg0KCTwv", 27 | "Zz4NCgk8Zz4NCgk8L2c+DQoJPGc+DQoJPC9nPg0KCTxnPg0KCTwvZz4NCgk8Zz4NCgk8L2c+DQoJPGc+", 28 | "DQoJPC9nPg0KCTxnPg0KCTwvZz4NCgk8Zz4NCgk8L2c+DQoJPGc+DQoJPC9nPg0KCTxnPg0KCTwvZz4N", 29 | "Cgk8Zz4NCgk8L2c+DQoJPGc+DQoJPC9nPg0KCTxnPg0KCTwvZz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+", 30 | "DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9n", 31 | "Pg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxn", 32 | "Pg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjwvc3ZnPg0K"]; 33 | 34 | export default new WebImage(`data:image/svg+xml;base64,${base64.join("")}`); 35 | -------------------------------------------------------------------------------- /src/tests/core/RouteTest.ts: -------------------------------------------------------------------------------- 1 | import Test from "@web-atoms/unit-test/dist/Test"; 2 | import { AtomTest } from "../../unit/AtomTest"; 3 | import Route from "../../core/Route"; 4 | import Assert from "@web-atoms/unit-test/dist/Assert"; 5 | 6 | export default class RouteTest extends AtomTest { 7 | 8 | @Test 9 | public parse() { 10 | let r = Route.create("/public/jobs/{id?}"); 11 | let p = r.matches("/public/jobs/3"); 12 | Assert.equals("3", p.id); 13 | 14 | p = r.matches("/public/jobs/a%20b"); 15 | Assert.equals("a b", p.id); 16 | 17 | r = Route.create("/public/jobs/{id:number?}"); 18 | p = r.matches("/public/jobs/3"); 19 | Assert.equals(3, p.id); 20 | 21 | Assert.isNull(r.matches("/public/jobs/a")); 22 | 23 | Assert.isNotNull(r.matches("/public/jobs")); 24 | } 25 | 26 | 27 | @Test 28 | public parseMultiple() { 29 | let r = Route.create("/feed/posts/{id}/{c}"); 30 | let p = r.matches("/feed/posts/3/5"); 31 | Assert.equals("3", p.id); 32 | Assert.equals("5", p.c); 33 | 34 | Assert.isNull(r.matches("/feed/posts/4")); 35 | 36 | } 37 | 38 | @Test 39 | public parseMultipleOptional() { 40 | let r = Route.create("/feed/posts/{id}/{c?}"); 41 | let p = r.matches("/feed/posts/3/5"); 42 | Assert.equals("3", p.id); 43 | Assert.equals("5", p.c); 44 | 45 | p = r.matches("/feed/posts/4"); 46 | Assert.equals("4", p.id); 47 | Assert.equals(void 0, p.c); 48 | 49 | } 50 | 51 | @Test 52 | public parseAll() { 53 | let r = Route.create("/preview/{*all}"); 54 | let p = r.matches("/preview/posts/3/5"); 55 | Assert.equals("posts/3/5", p.all); 56 | } 57 | 58 | @Test 59 | public parseQueryString() { 60 | let r = Route.create("/feed/post/{id?}", ["a"]); 61 | let p = r.matches("/feed/post/3", new URLSearchParams("a=4")); 62 | Assert.equals("3", p.id); 63 | Assert.equals("4", p.a); 64 | p = r.matches("/feed/posts/3", new URLSearchParams("a=4")); 65 | Assert.isNull(p); 66 | 67 | let url = r.substitute({ id: 2, a: 9}); 68 | Assert.equals("/feed/post/2?a=9&", url); 69 | 70 | r = Route.create("/feed/post/{id?}", ["a={anchor}"]); 71 | p = r.matches("/feed/post/3", new URLSearchParams("a=4")); 72 | Assert.equals("3", p.id); 73 | Assert.equals("4", p.anchor); 74 | p = r.matches("/feed/posts/3", new URLSearchParams("a=4")); 75 | Assert.isNull(p); 76 | 77 | url = r.substitute({ id: 2, anchor: 9}); 78 | Assert.equals("/feed/post/2?a=9&", url); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/web/controls/AtomGridSplitter.ts: -------------------------------------------------------------------------------- 1 | import { BindableProperty } from "../../core/BindableProperty"; 2 | import { IDisposable, IRect } from "../../core/types"; 3 | import { AtomControl } from "./AtomControl"; 4 | import { AtomGridView } from "./AtomGridView"; 5 | 6 | /** 7 | * Grid Splitter can only be added inside a Grid 8 | */ 9 | export class AtomGridSplitter extends AtomControl { 10 | 11 | public direction: "vertical" | "horizontal"; 12 | 13 | public dragging: boolean = false; 14 | 15 | protected preCreate() { 16 | this.direction = "vertical"; 17 | this.dragging = false; 18 | } 19 | 20 | protected create(): void { 21 | this.bind(this.element, "styleCursor", [["direction"]], false, 22 | (v) => v === "vertical" ? "ew-resize" : "ns-resize"); 23 | 24 | this.bind(this.element, "styleBackgroundColor", [["dragging"]], false, 25 | (v) => v ? "blue" : "lightgray"); 26 | const style = this.element.style; 27 | style.position = "absolute"; 28 | style.left = style.top = style.bottom = style.right = "0"; 29 | 30 | this.bindEvent(this.element, "mousedown", (e: MouseEvent) => { 31 | 32 | e.preventDefault(); 33 | 34 | this.dragging = true; 35 | 36 | const parent = this.parent as AtomGridView; 37 | 38 | const isVertical = this.direction === "vertical"; 39 | 40 | const disposables: IDisposable[] = []; 41 | 42 | const rect: IRect = { x: e.screenX, y: e.screenY }; 43 | 44 | const {column, row} = AtomGridView.getCellInfo(this.element); 45 | 46 | const ss = document.createElement("style"); 47 | ss.textContent = "iframe { pointer-events: none }"; 48 | document.head.appendChild(ss); 49 | 50 | disposables.push({ 51 | dispose: () => ss.remove() 52 | }); 53 | 54 | disposables.push(this.bindEvent(document.body, "mousemove", (me: MouseEvent) => { 55 | 56 | // do drag.... 57 | const { screenX, screenY } = me; 58 | 59 | const dx = screenX - rect.x; 60 | const dy = screenY - rect.y; 61 | 62 | if (isVertical) { 63 | parent.resize("column", column, dx); 64 | } else { 65 | parent.resize("row", row, dy); 66 | } 67 | 68 | rect.x = screenX; 69 | rect.y = screenY; 70 | 71 | })); 72 | 73 | disposables.push(this.bindEvent(document.body, "mouseup", (mup) => { 74 | // stop 75 | this.dragging = false; 76 | for (const iterator of disposables) { 77 | iterator.dispose(); 78 | } 79 | })); 80 | 81 | }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/web/controls/AtomComboBox.ts: -------------------------------------------------------------------------------- 1 | import { App } from "../../App"; 2 | import { Inject } from "../../di/Inject"; 3 | import { AtomControl } from "./AtomControl"; 4 | import { AtomItemsControl } from "./AtomItemsControl"; 5 | 6 | export class AtomComboBox extends AtomItemsControl { 7 | 8 | private isChanging: boolean; 9 | 10 | constructor(@Inject app: App, e?: HTMLElement) { 11 | super(app, e || document.createElement("select")); 12 | this.allowMultipleSelection = false; 13 | } 14 | 15 | public onCollectionChanged(key: string, index: number, item: any): void { 16 | super.onCollectionChanged(key, index, item); 17 | try { 18 | this.isChanging = true; 19 | const se = this.element as HTMLSelectElement; 20 | se.selectedIndex = this.selectedIndex; 21 | } finally { 22 | this.isChanging = false; 23 | } 24 | } 25 | 26 | public updateSelectionBindings(): void { 27 | super.updateSelectionBindings(); 28 | 29 | try { 30 | if (this.isChanging) { 31 | return; 32 | } 33 | this.isChanging = true; 34 | const se = this.element as HTMLSelectElement; 35 | se.selectedIndex = this.selectedIndex; 36 | } finally { 37 | this.isChanging = false; 38 | } 39 | } 40 | 41 | protected preCreate(): void { 42 | super.preCreate(); 43 | this.itemTemplate = AtomComboBoxItemTemplate; 44 | this.runAfterInit(() => { 45 | this.bindEvent(this.element, "change", (s) => { 46 | if (this.isChanging) { 47 | return; 48 | } 49 | try { 50 | this.isChanging = true; 51 | const index = (this.element as HTMLSelectElement).selectedIndex; 52 | if (index === -1) { 53 | this.selectedItems.clear(); 54 | return; 55 | } 56 | this.selectedItem = this.items[index]; 57 | // this.selectedIndex = (this.element as HTMLSelectElement).selectedIndex; 58 | } finally { 59 | this.isChanging = false; 60 | } 61 | }); 62 | }); 63 | 64 | } 65 | } 66 | 67 | class AtomComboBoxItemTemplate extends AtomControl { 68 | 69 | constructor(app: App, e?: HTMLElement) { 70 | super(app, e || document.createElement("option")); 71 | } 72 | 73 | protected create(): void { 74 | this.bind(this.element, "text", [["data"]], false , 75 | (v) => { 76 | const ip = this.element._templateParent as AtomItemsControl; 77 | return v[ip.labelPath]; 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Action Status](https://github.com/web-atoms/core/workflows/Build/badge.svg)](https://github.com/web-atoms/core/actions) [![npm version](https://badge.fury.io/js/%40web-atoms%2Fcore.svg)](https://badge.fury.io/js/%40web-atoms%2Fcore) [![codecov](https://codecov.io/gh/web-atoms/core/branch/master/graph/badge.svg)](https://codecov.io/gh/web-atoms/core) 2 | 3 | # Web-Atoms Core 4 | Lightweight JavaScript framework with MVU Pattern with Data Binding in JSX. 5 | 6 | > Note, MVVM is now deprecated, we have realized that MVVM often adds more code then the benefits. Since JavaScript allows mixin, its easy to incorporate 7 | > reusable logic with mixin rather than MVVM. MVU pattern is better suitable for faster development. 8 | 9 | ## Web Features 10 | 1. Data Binding, simple arrow functions to bind the UI elements 11 | 2. Styled Support 12 | 3. AtomRepeater - Lightweight List Control to manage list of items 13 | 4. Chips control 14 | 5. Dual View support (Mobile and Desktop) 15 | 6. Smallest syntax 16 | 7. Faster rendering 17 | 8. Simple Data Validations 18 | 9. RetroFit inspired REST API Support 19 | 10. No additional build configurations 20 | 11. Event re routing, it helps in reducing number of event listeners on page. 21 | 12. UMD and SystemJS Module Loader 22 | 13. Packer, to pack all JavaScript in single module along with dynamic module loader support 23 | 14. FetchBuilder, fetch builder allows you to build REST request in fluent way and execute them with single `await`. 24 | 25 | ## Web Controls 26 | 1. ComboBox (wrapper for SELECT element) 27 | 2. AtomControl (base class for all other controls) 28 | 3. AtomRepeater 29 | 4. PopupWindow, PopupWindowPage 30 | 31 | ## Services 32 | 1. WindowService - to host AtomWindow and retrieve result 33 | 2. RestService - RetroFit kind of service for simple ajax 34 | 3. BrowserService - An abstract navigation service for Web and Xamarin 35 | 36 | ## Development guidelines 37 | 1. Use TypeScript `module` pattern 38 | 2. Do not use `namespace` 39 | 3. Organize single module in single TypeScript file 40 | 4. Import only required module and retain naming convention 41 | 5. Use default export for UI component 42 | 6. No `Atom.get` and `Atom.set` 43 | 7. Do not use underscore `_` anywhere, not in field name not in get/set 44 | 8. Do not use `set_name` method name, instead use `get name()` and `set name(v: T)` syntax for properties. 45 | 46 | 47 | ## How to run unit tests? 48 | 49 | 1. Import test class `src\test.ts` 50 | 2. Run `node .\dist\test.js` 51 | 52 | ## How to get code coverage? 53 | 54 | 1. Install istanbul, `npm install istanbul --save-dev` 55 | 2. Install remap-istanbul, `npm install remap-istanbul` 56 | 3. Cover Run, `.\node_modules\.bin\istanbul.cmd cover .\dist\test.js` 57 | 4. Report Run, `.\node_modules\.bin\remap-istanbul -i .\coverage\coverage.json -t html -o html-report` 58 | 5. Open generated html-report on browser 59 | -------------------------------------------------------------------------------- /src/web/styles/CSS.ts: -------------------------------------------------------------------------------- 1 | import { AtomStyleRules } from "../../style/StyleRule"; 2 | import { IStyleDeclaration } from "./IStyleDeclaration"; 3 | 4 | let styleId = 1; 5 | 6 | function fromCamelToHyphen(input: string): string { 7 | return input.replace( /([a-z])([A-Z])/g, "$1-$2" ).toLowerCase(); 8 | } 9 | 10 | function createStyleText(name: string, styles: IStyleDeclaration): string[] { 11 | const styleList: any[] = []; 12 | const subclasses = []; 13 | for (const key in styles) { 14 | if (styles.hasOwnProperty(key)) { 15 | if (/^(\_\$\_|className$|toString$)/i.test(key)) { 16 | continue; 17 | } 18 | const element = styles[key]; 19 | if (element === undefined || element === null) { 20 | continue; 21 | } 22 | const keyName = fromCamelToHyphen(key); 23 | if (key === "subclasses") { 24 | const n = name; 25 | for (const subclassKey in element) { 26 | if (element.hasOwnProperty(subclassKey)) { 27 | const ve = element[subclassKey]; 28 | subclasses.push( ... createStyleText(`${n}${subclassKey}`, ve)); 29 | } 30 | } 31 | } else { 32 | if (element.url) { 33 | styleList.push(`${keyName}: url(${element})`); 34 | } else { 35 | styleList.push(`${keyName}: ${element}`); 36 | } 37 | } 38 | } 39 | } 40 | const cname = fromCamelToHyphen(name); 41 | 42 | const styleClassName = `${cname}`; 43 | 44 | if (styleList.length) { 45 | return [`${styleClassName} {\r\n${styleList.join(";\r\n")}; }`, ... subclasses]; 46 | } 47 | return subclasses; 48 | } 49 | 50 | /** 51 | * It will add custom stylesheet to the document and 52 | * it will create a new unique scope class if selectorName was not provided 53 | * @param style AtomStyleRules | IStyleDeclaration 54 | * @param selectorName name of the selector (only use for CustomElement, do not use for components) 55 | * @returns string 56 | */ 57 | export default function CSS(style: IStyleDeclaration | AtomStyleRules, selectorName?: string): string { 58 | let styleName = ""; 59 | if (style instanceof AtomStyleRules) { 60 | styleName = style.name || ""; 61 | if (styleName) { 62 | styleName = "-" + styleName; 63 | } 64 | style = style.style; 65 | } 66 | let className = selectorName; 67 | if (!selectorName) { 68 | className = `wa-style-${styleId++}${styleName}`; 69 | selectorName = `.${className}`; 70 | } 71 | const s = document.createElement("style"); 72 | s.id = selectorName; 73 | const list = createStyleText(selectorName, style); 74 | s.textContent = list.join("\r\n"); 75 | document.head.appendChild(s); 76 | return className; 77 | } 78 | -------------------------------------------------------------------------------- /src/tests/services/CacheServiceTest.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Test from "@web-atoms/unit-test/dist/Test"; 3 | import { Atom } from "../../Atom"; 4 | import CacheService from "../../services/CacheService"; 5 | import { AtomTest } from "../../unit/AtomTest"; 6 | 7 | export class CacheServiceTest extends AtomTest { 8 | 9 | @Test 10 | public async ttlTest(): Promise { 11 | const c = new CacheService(this.app); 12 | 13 | let i = 0; 14 | 15 | const f = async (): Promise => { 16 | return await c.getOrCreate("a", async (ci) => { 17 | await Atom.delay(10); 18 | ci.ttlSeconds = 100 / 1000; 19 | return "A" + i; 20 | }); 21 | }; 22 | 23 | let r = await f(); 24 | i ++; 25 | 26 | Assert.equals("A0", r); 27 | 28 | await Atom.delay(5); 29 | 30 | r = await f(); 31 | Assert.equals("A0", r); 32 | 33 | await Atom.delay(200); 34 | 35 | r = await f(); 36 | Assert.equals("A1", r); 37 | 38 | } 39 | 40 | @Test 41 | public async ttlSecondsMethod(): Promise { 42 | const c = new CacheService(this.app); 43 | 44 | let i = 0; 45 | 46 | const n = "0"; 47 | 48 | const f = async (): Promise => { 49 | return await c.getOrCreate("a", async (ci) => { 50 | await Atom.delay(10); 51 | ci.ttlSeconds = (x: string) => x.endsWith(n) ? (100 / 1000) : 0; 52 | return "A" + i; 53 | }); 54 | }; 55 | 56 | let r = await f(); 57 | i ++; 58 | 59 | Assert.equals("A0", r); 60 | 61 | await Atom.delay(5); 62 | 63 | r = await f(); 64 | Assert.equals("A0", r); 65 | 66 | await Atom.delay(200); 67 | 68 | r = await f(); 69 | Assert.equals("A1", r); 70 | 71 | i ++; 72 | r = await f(); 73 | Assert.equals("A2", r); 74 | } 75 | 76 | @Test 77 | public async remove(): Promise { 78 | const c = new CacheService(this.app); 79 | 80 | let i = 0; 81 | 82 | const f = async (): Promise => { 83 | return await c.getOrCreate("a", async (ci) => { 84 | await Atom.delay(10); 85 | ci.ttlSeconds = 100 / 1000; 86 | return "A" + i; 87 | }); 88 | }; 89 | 90 | let r = await f(); 91 | 92 | Assert.equals("A0", r); 93 | 94 | await Atom.delay(10); 95 | 96 | c.remove("a"); 97 | 98 | i++; 99 | 100 | r = await f(); 101 | 102 | Assert.equals("A1", r); 103 | 104 | Assert.isNull(c.remove("aa")); 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/web/services/WebBusyIndicatorService.ts: -------------------------------------------------------------------------------- 1 | import { App } from "../../App"; 2 | import { IDisposable } from "../../core/types"; 3 | import { Inject } from "../../di/Inject"; 4 | import { RegisterSingleton } from "../../di/RegisterSingleton"; 5 | import { BusyIndicatorService } from "../../services/BusyIndicatorService"; 6 | import { NavigationService } from "../../services/NavigationService"; 7 | import { AtomControl } from "../controls/AtomControl"; 8 | import { cssNumberToString } from "../styles/StyleBuilder"; 9 | import { WindowService } from "./WindowService"; 10 | 11 | @RegisterSingleton 12 | export class WebBusyIndicatorService extends BusyIndicatorService { 13 | 14 | @Inject 15 | private navigationService: NavigationService; 16 | 17 | @Inject 18 | private app: App; 19 | 20 | private zIndex: number = 50000; 21 | 22 | private indicators: number = 0; 23 | 24 | public createIndicator(): IDisposable { 25 | 26 | const host = document.createElement("div"); 27 | const popup = new AtomControl(this.app, host); 28 | host.className = "indicator-host"; 29 | 30 | const span = document.createElement("i"); 31 | 32 | const divStyle = host.style; 33 | divStyle.position = "absolute"; 34 | divStyle.overflow = "hidden"; 35 | divStyle.left = divStyle.right = divStyle.bottom = divStyle.top = "0"; 36 | divStyle.zIndex = (this.zIndex ++) + ""; 37 | const spanStyle = span.style; 38 | spanStyle.position = "absolute"; 39 | spanStyle.margin = "auto"; 40 | spanStyle.width = "16px"; 41 | spanStyle.height = "16px"; 42 | spanStyle.overflow = "hidden"; 43 | spanStyle.maxHeight = "100%"; 44 | spanStyle.maxWidth = "100%"; 45 | spanStyle.left = spanStyle.right = spanStyle.bottom = spanStyle.top = "0"; 46 | // span.src = ModuleFiles.src.web.images.busy_gif; 47 | span.className = "fas fa-spinner fa-spin"; 48 | 49 | host.appendChild(span); 50 | 51 | const ws = this.navigationService as WindowService; 52 | 53 | const e = ws.getHostForElement(); 54 | 55 | if (e) { 56 | e.appendChild(host); 57 | 58 | } else { 59 | document.body.appendChild(host); 60 | ws.refreshScreen(); 61 | popup.bind(host, "styleLeft", [["this", "scrollLeft"]], false, cssNumberToString, ws.screen); 62 | popup.bind(host, "styleTop", [["this", "scrollTop"]], false, cssNumberToString, ws.screen); 63 | popup.bind(host, "styleWidth", [["this", "width"]], false, cssNumberToString, ws.screen); 64 | popup.bind(host, "styleHeight", [["this", "height"]], false, cssNumberToString, ws.screen); 65 | } 66 | 67 | popup.registerDisposable({ 68 | dispose: () => { 69 | host.remove(); 70 | } 71 | }); 72 | 73 | return popup; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/services/CacheService.ts: -------------------------------------------------------------------------------- 1 | import { App } from "../App"; 2 | import { Atom } from "../Atom"; 3 | import DISingleton from "../di/DISingleton"; 4 | import { Inject } from "../di/Inject"; 5 | 6 | export type CacheSeconds = number | ((result: T) => number); 7 | export interface ICacheEntry { 8 | 9 | /** 10 | * Cache Key, must be unique 11 | */ 12 | key: string; 13 | 14 | /** 15 | * Time to Live in seconds, after given ttl 16 | * object will be removed from cache 17 | */ 18 | ttlSeconds?: CacheSeconds; 19 | 20 | // /** 21 | // * Not supported yet 22 | // */ 23 | // expires?: Date; 24 | 25 | /** 26 | * Cached value 27 | */ 28 | value?: Promise; 29 | 30 | } 31 | 32 | interface IFinalCacheEntry extends ICacheEntry { 33 | 34 | finalTTL?: number; 35 | 36 | timeout?: any; 37 | 38 | } 39 | 40 | @DISingleton() 41 | export default class CacheService { 42 | 43 | private cache: { [key: string]: IFinalCacheEntry } = {}; 44 | 45 | constructor(@Inject private app: App) { 46 | } 47 | 48 | public remove(key: string): any { 49 | const v = this.cache[key]; 50 | if (v) { 51 | this.clear(v); 52 | return v.value; 53 | } 54 | return null; 55 | } 56 | 57 | public async getOrCreate( 58 | key: string, 59 | task: (cacheEntry?: ICacheEntry) => Promise ): Promise { 60 | const c = this.cache[key] || (this.cache[key] = { 61 | key, 62 | finalTTL: 3600 63 | }); 64 | if (!c.value) { 65 | c.value = task(c); 66 | } 67 | let v: any = null; 68 | try { 69 | v = await c.value; 70 | } catch (e) { 71 | this.clear(c); 72 | throw e; 73 | } 74 | if (c.ttlSeconds !== undefined) { 75 | if (typeof c.ttlSeconds === "number") { 76 | c.finalTTL = c.ttlSeconds; 77 | } else { 78 | c.finalTTL = c.ttlSeconds(v); 79 | } 80 | } 81 | if (c.timeout) { 82 | clearTimeout(c.timeout); 83 | } 84 | if (c.finalTTL) { 85 | this.cache[key] = c; 86 | c.timeout = setTimeout(() => { 87 | c.timeout = 0; 88 | this.clear(c); 89 | }, c.finalTTL * 1000); 90 | } else { 91 | // this is the case where we do not want to store 92 | this.clear(c); 93 | } 94 | return await c.value; 95 | } 96 | 97 | private clear(ci: IFinalCacheEntry): void { 98 | if (ci.timeout) { 99 | clearTimeout(ci.timeout); 100 | ci.timeout = 0; 101 | } 102 | this.cache[ci.key] = null; 103 | delete this.cache[ci.key]; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/tests/di/InjectTest.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Test from "@web-atoms/unit-test/dist/Test"; 3 | import DISingleton from "../../di/DISingleton"; 4 | import { Inject } from "../../di/Inject"; 5 | import { AtomTest } from "../../unit/AtomTest"; 6 | 7 | export default class InjectTest extends AtomTest { 8 | 9 | @Test 10 | public propertyInjection(): void { 11 | 12 | const vm = this.app.resolve(VM, true) as VM; 13 | 14 | const first = vm.propertyService.time; 15 | 16 | const vm2 = this.app.resolve(VM, true) as VM; 17 | 18 | const second = vm2.propertyService.time; 19 | 20 | Assert.equals(first, second); 21 | 22 | } 23 | 24 | @Test 25 | public propertyInjectionWithConstructor(): void { 26 | const vm = this.app.resolve(VM2, true) as VM2; 27 | 28 | const first = vm.propertyService.time; 29 | 30 | const vm2 = this.app.resolve(VM2, true) as VM2; 31 | 32 | const second = vm2.propertyService.time; 33 | 34 | Assert.equals(first, second); 35 | } 36 | 37 | @Test 38 | public inheritedPropertyTest(): void { 39 | const bvm = this.app.resolve(BaseVM, true) as BaseVM; 40 | 41 | const bvm2 = this.app.resolve(BaseVM, true) as BaseVM; 42 | 43 | const bf = bvm.service.time; 44 | const bf2 = bvm.service.time; 45 | 46 | Assert.equals(bf, bf2); 47 | 48 | // child 49 | 50 | const cvm = this.app.resolve(ChildVM, true) as ChildVM; 51 | const cvm2 = this.app.resolve(ChildVM, true) as ChildVM; 52 | 53 | const cf = cvm.service.time; 54 | const cf2 = cvm2.service.time; 55 | 56 | Assert.equals(cf, cf2); 57 | 58 | const cs = cvm.propertyService.time; 59 | const cs2 = cvm2.propertyService.time; 60 | 61 | Assert.equals(cs, cs2); 62 | } 63 | } 64 | 65 | @DISingleton() 66 | class Service { 67 | public time: number; 68 | 69 | constructor() { 70 | this.time = (new Date()).getTime(); 71 | } 72 | 73 | } 74 | 75 | @DISingleton() 76 | class PropertyService { 77 | 78 | public time: number; 79 | 80 | constructor() { 81 | this.time = (new Date()).getTime(); 82 | } 83 | } 84 | 85 | class VM { 86 | 87 | @Inject 88 | public readonly propertyService: PropertyService; 89 | 90 | // constructor(@Inject private service: Service) { 91 | 92 | // } 93 | 94 | } 95 | 96 | class VM2 { 97 | 98 | @Inject 99 | public readonly propertyService: PropertyService; 100 | 101 | constructor(@Inject private service: Service) { 102 | 103 | } 104 | 105 | } 106 | 107 | class BaseVM { 108 | 109 | @Inject public service: Service; 110 | 111 | } 112 | 113 | class ChildVM extends BaseVM { 114 | 115 | @Inject public propertyService: PropertyService; 116 | 117 | } 118 | -------------------------------------------------------------------------------- /generated-cert-1: -------------------------------------------------------------------------------- 1 | {"key":"-----BEGIN RSA PRIVATE KEY-----\r\nMIIEpAIBAAKCAQEAx2KW0kCrqfs581FXqZvCHb7zofQLhArQXeP3OTbepnwg+Ukv\r\n1rwFu/aRkBrjkYm/oGahvmJzZAPUAKoJYhH3PyLXUrV7LR0xeT2xxdT76soO2Zdl\r\naXvBGu6Ya0O/2ojWLpSnd3Dn5fvdLZ7CzMqwGG9thqx2Lsl2BHJz70pU6BIW9Afg\r\nsgvz9T0wfLIlov1/XXPs5krD/DPJMrDt+zBU7XvV9gai55WFpqPUuNCk9IK4QxfK\r\nfJ9NJ85snOtHSJW+jorrsYxsSXPp+9BAdryRwFxF34OpdKI0e6KhuO751pIviSOS\r\niOWdieEvZbHGfmiKEXLdyW/bwMUlKQ7zY/aXNQIDAQABAoIBAQCypHvbkAP+vdBT\r\nWLtBhQPsoO0rWzyCJypAJYOZaLce1bK5iEBrlnVQzv/m7KCfApuF/M7IhO88Wk21\r\n9qOHke9ES1Q/j8j4R+LO7V79kPHRkQ6zGHnNhAsltUctPDuGcvnsLFbLpoawQfUO\r\n7eM9mBElI5kvhBAzpV+vZljHuoLOlemEzk/z6nohuNpZ/p7llUEREGW1w92xoG81\r\nrqk4ogbDSkno2AE2aELTaXg/LgW7nEqfEZR05VU975n/vbbJ4NU27q9toHBu6aDd\r\nQvy8GWruc4sMp28lyVE/8qx40K3BudkYzF1aIAoGJsfjPUqF0zqryzCLIEOqCE8a\r\n1Gcqozg1AoGBAOqkim3WFiykiJeXNZ2Tqofo0Vw6D+YzsAZmrAs1995r1DsX9/dC\r\nNUdGVdCRRz874gGzetyihOcHg4SawlXrS0KuqD27r3ypNB+Z6M2n5a6cR+D0an1p\r\niMzSufhOmaXnH2e0qIuYeOXsbx7YRhz2kParp9rg+UHfrDwuGrUaRORHAoGBANmI\r\ngNDBDMIzTarrmDZFJhxXj+yAXTBKEYF4UR3N8GCLh4DVtFPbOJuxktGinZVXB8bL\r\njMhdYn7XIwB19MEczZ/q6Ee9i11qfAjWF43OuJUg0QZ0cPuKSvdkCufgFccXibfO\r\nG+WSZpHBD6o835oeUMQPn4sPxGourvjTzkHA8dKjAoGAaWLrlbdxEC3593P/rb2T\r\n+yTzW2Psni0a41Ub8pETuf9ePAhg49oFyfRqOJn3kQwZT0BIb25DGOzEAjvsCuD6\r\nVYHSqJ9yiyAH/CWJbUz6mPkyQ3QjnB5ZRf1jb4YF9oCfF1oJ1WDu8/3ETus+WmXX\r\n6CJi6qje6tpGJmVis3KP/KMCgYBATb6udvK7kYjbBqvHFyfN5wBvi/6AINUN7bAk\r\n3FS7ZWOX7RRSWZJhS9u3xpdIpyJwXIlwTVKpZhU9tKC2WTpblIg3dMt2wNyLjCYI\r\nUFx2EO5ZNyCS6u2ANf2XT8GASe/2+qF6eo2Bdo2X6Ei8+UssueWSqQWJ0eT9PzdQ\r\nbqXNewKBgQDc/KEv4N0M/kpZjODs0b2KYFQ0N4toUWzPXTy6q3vXYP5xknZAHFN3\r\nD/BFjBt7eTHs1QJxijD80eHppMWLWoyIc8sf6zIvecYPOj+pd5eyia7gRQX03pIb\r\n5fOWBvdFWjSGqD2n9IRsI6x4UXsvr0G49D3FQ7PedEz5Ws3MZgfskQ==\r\n-----END RSA PRIVATE KEY-----\r\n","cert":"-----BEGIN CERTIFICATE-----\r\nMIIDlzCCAn+gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBjjEZMBcGA1UEAxMQZGV2\r\nLndlYi1hdG9tcy5pbjELMAkGA1UEBhMCSU4xFDASBgNVBAgTC01haGFyYXNodHJh\r\nMRQwEgYDVQQHEwtOYXZpIE11bWJhaTEpMCcGA1UEChMgTmV1cm9TcGVlY2ggVGVj\r\naG5vbG9naWVzIFB2dCBMdGQxDTALBgNVBAsTBFRlc3QwHhcNMjAwNTIxMDYwNjA2\r\nWhcNNDAwNTIxMDYwNjA2WjCBjjEZMBcGA1UEAxMQZGV2LndlYi1hdG9tcy5pbjEL\r\nMAkGA1UEBhMCSU4xFDASBgNVBAgTC01haGFyYXNodHJhMRQwEgYDVQQHEwtOYXZp\r\nIE11bWJhaTEpMCcGA1UEChMgTmV1cm9TcGVlY2ggVGVjaG5vbG9naWVzIFB2dCBM\r\ndGQxDTALBgNVBAsTBFRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB\r\nAQDHYpbSQKup+znzUVepm8IdvvOh9AuECtBd4/c5Nt6mfCD5SS/WvAW79pGQGuOR\r\nib+gZqG+YnNkA9QAqgliEfc/ItdStXstHTF5PbHF1Pvqyg7Zl2Vpe8Ea7phrQ7/a\r\niNYulKd3cOfl+90tnsLMyrAYb22GrHYuyXYEcnPvSlToEhb0B+CyC/P1PTB8siWi\r\n/X9dc+zmSsP8M8kysO37MFTte9X2BqLnlYWmo9S40KT0grhDF8p8n00nzmyc60dI\r\nlb6OiuuxjGxJc+n70EB2vJHAXEXfg6l0ojR7oqG47vnWki+JI5KI5Z2J4S9lscZ+\r\naIoRct3Jb9vAxSUpDvNj9pc1AgMBAAEwDQYJKoZIhvcNAQEFBQADggEBALzYGI3g\r\nyaUQq/8jrBBgF5OzJTmKRmCpR9ojBruVkE9qrD9e3cddjapLn8WlM0LmOTptGGeg\r\np7nR8kV7qUcENWb31h3IPVlKbIwveB8QS3/JBOrvQgasmDEWrlqMvJ80DN/hWjqU\r\nSJinJFo4/ZjLoVxSWCtKdtDPI9VjnKXwJZ65DuqfhVBut8Qv/CymeHl8G9ALNTSI\r\nCXb+ZTGcViOlfmwLh7z9f1CLgfx2++/c6HSqGFs2WFd2lnGVkm5g6wJYlI/BCsrY\r\n9O4Wb30Be/lCFinONlsNENfs0S3T2pNHWDeJDaLwxOYv0E6ZVeuK3GfXd7cbpqPp\r\nr7srz0iOQP8Ua4s=\r\n-----END CERTIFICATE-----\r\n"} -------------------------------------------------------------------------------- /src/view-model/AtomWindowViewModel.ts: -------------------------------------------------------------------------------- 1 | import { AtomBinder } from "../core/AtomBinder"; 2 | import { BindableProperty } from "../core/BindableProperty"; 3 | import { NavigationService } from "../services/NavigationService"; 4 | import { AtomViewModel } from "./AtomViewModel"; 5 | 6 | /** 7 | * This view model should be used with WindowService to create and open window. 8 | * 9 | * This view model has `close` and `cancel` methods. `close` method will 10 | * close the window and will resolve the given result in promise. `cancel` 11 | * will reject the given promise. 12 | * 13 | * @example 14 | * 15 | * @Inject windowService: NavigationService 16 | * var result = await 17 | * windowService.openPage( 18 | * ModuleFiles.views.NewWindow, 19 | * { 20 | * title: "Edit Object", 21 | * data: { 22 | * id: 4 23 | * } 24 | * }); 25 | * 26 | * 27 | * 28 | * class NewTaskWindowViewModel extends AtomWindowViewModel{ 29 | * 30 | * .... 31 | * save(){ 32 | * 33 | * // close and send result 34 | * this.close(task); 35 | * 36 | * } 37 | * .... 38 | * 39 | * } 40 | * 41 | * @export 42 | * @class AtomWindowViewModel 43 | * @extends {AtomViewModel} 44 | */ 45 | export class AtomWindowViewModel extends AtomViewModel { 46 | 47 | public title: string; 48 | 49 | public closeWarning: string; 50 | 51 | /** 52 | * windowName will be set to generated html tag id, you can use this 53 | * to mock AtomWindowViewModel in testing. 54 | * 55 | * When window is closed or cancelled, view model only broadcasts 56 | * `atom-window-close:${this.windowName}`, you can listen for 57 | * such message. 58 | * 59 | * @type {string} 60 | * @memberof AtomWindowViewModel 61 | */ 62 | public windowName: string; 63 | 64 | /** 65 | * This will broadcast `atom-window-close:windowName`. 66 | * WindowService will close the window on receipt of such message and 67 | * it will resolve the promise with given result. 68 | * 69 | * this.close(someResult); 70 | * 71 | * @param {*} [result] 72 | * @memberof AtomWindowViewModel 73 | */ 74 | public close(result?: any): void { 75 | this.app.broadcast(`atom-window-close:${this.windowName}`, result); 76 | } 77 | 78 | /** 79 | * This will return true if this view model is safe to cancel and close 80 | */ 81 | public async cancel(): Promise { 82 | if (this.closeWarning) { 83 | const navigationService = this.app.resolve(NavigationService); 84 | if (! await navigationService.confirm(this.closeWarning, "Are you sure?")) { 85 | return false; 86 | } 87 | } 88 | this.app.broadcast(`atom-window-cancel:${this.windowName}`, "cancelled"); 89 | return true; 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/web/samples/demo/views/MovieList.ts: -------------------------------------------------------------------------------- 1 | import { App } from "../../../../App"; 2 | import { AtomControl } from "../../../controls/AtomControl"; 3 | import { AtomGridSplitter } from "../../../controls/AtomGridSplitter"; 4 | import { AtomGridView } from "../../../controls/AtomGridView"; 5 | import { AtomItemsControl } from "../../../controls/AtomItemsControl"; 6 | 7 | export class MovieList extends AtomGridView { 8 | 9 | protected create(): void { 10 | 11 | const style = this.element.style; 12 | style.position = "absolute"; 13 | style.left = style.right = style.top = style.bottom = "0"; 14 | 15 | this.setPrimitiveValue(this.element, "columns", "30%,5,*"); 16 | this.setPrimitiveValue(this.element, "rows", "30,*"); 17 | 18 | const header = document.createElement("header"); 19 | header.textContent = "Header"; 20 | this.setPrimitiveValue(header, "cell", "0:3,0"); 21 | this.append(header); 22 | 23 | const ul = new AtomItemsControl(this.app, document.createElement("ul")); 24 | this.append(ul); 25 | ul.itemTemplate = MovieListItemTemplate; 26 | ul.bind(ul.element, "items", [["viewModel", "movies"]], false); 27 | ul.bind(ul.element, "selectedItem", [["viewModel", "selectedMovie"]], true); 28 | 29 | ul.setPrimitiveValue(ul.element, "cell", "0,1"); 30 | 31 | // const e = document.createElement("span"); 32 | // this.append(e); 33 | // e.style.color = "red"; 34 | // this.bind(e, "text", [["viewModel", "errorSelectedMovie"]]); 35 | 36 | const right = document.createElement("div"); 37 | right.textContent = "right"; 38 | right.style.backgroundColor = "green"; 39 | this.setPrimitiveValue(right, "cell", "2,1"); 40 | this.append(right); 41 | 42 | const splitter = new AtomGridSplitter(this.app); 43 | this.append(splitter); 44 | splitter.setPrimitiveValue(splitter.element, "cell", "1,1"); 45 | } 46 | } 47 | 48 | class MovieListItemTemplate extends AtomControl { 49 | 50 | constructor(app: App, e?: HTMLElement) { 51 | super(app, e || document.createElement("li")); 52 | } 53 | 54 | protected create(): void { 55 | this.element.style.margin = "2px"; 56 | const span = document.createElement("span"); 57 | this.append(span); 58 | this.bind(span, "text", [["data", "label"], ["data", "category"]], false, 59 | (label, category) => `${label} (${category})`); 60 | this.bind(span, "styleFontWeight", 61 | [["data"], ["viewModel", "selectedMovie"]], false, 62 | (d, s) => { 63 | return d === s ? "bold" : ""; 64 | }); 65 | this.bindEvent(span, "click", (e) => { 66 | this.viewModel.onItemClick(this.data); 67 | }); 68 | 69 | const button = document.createElement("button"); 70 | this.append(button); 71 | button.textContent = "Delete"; 72 | button.style.marginLeft = "10px"; 73 | this.bindEvent(button, "click", (e) => { 74 | this.viewModel.onDelete(this.data); 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/tests/AtomClassTest.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Test from "@web-atoms/unit-test/dist/Test"; 3 | import { Atom } from "../Atom"; 4 | import { CancelToken } from "../core/types"; 5 | import { AtomTest } from "../unit/AtomTest"; 6 | 7 | export class AtomClassTest extends AtomTest { 8 | 9 | @Test 10 | public async postAsync(): Promise { 11 | const r = await Atom.postAsync(this.app, async () => { 12 | await Atom.delay(100); 13 | return "test"; 14 | }); 15 | 16 | Assert.equals("test", r); 17 | 18 | try { 19 | await Atom.postAsync(this.app, async () => { 20 | await Atom.delay(1); 21 | throw new Error("error"); 22 | }); 23 | } catch (e) { 24 | Assert.equals("error", e.message); 25 | } 26 | } 27 | 28 | @Test 29 | public encode(): void { 30 | 31 | let url = Atom.encodeParameters({ a: { b: null }, c: 1 }); 32 | 33 | Assert.equals(`a=%7B%22b%22%3Anull%7D&c=1`, url); 34 | 35 | url = Atom.encodeParameters({ a: null, d: undefined, c: 1 }); 36 | Assert.equals(`c=1`, url); 37 | 38 | url = Atom.encodeParameters({ a: null, d: undefined, c: new Date(Date.UTC(2001, 0, 1, 0, 0, 0, 0))}); 39 | Assert.equals(`c=2001-01-01T00%3A00%3A00.000Z`, url); 40 | } 41 | 42 | @Test 43 | public url(): void { 44 | 45 | let url = Atom.url(null); 46 | Assert.isNull(url); 47 | 48 | url = Atom.url("a", { b: "c" }); 49 | Assert.equals("a?b=c", url); 50 | 51 | url = Atom.url("a?b=c", { d: "e" }); 52 | Assert.equals("a?b=c&d=e", url); 53 | 54 | url = Atom.url("a", null, { d: "e" }); 55 | Assert.equals("a#d=e", url); 56 | 57 | url = Atom.url("a#b=c", null, { d: "e" }); 58 | Assert.equals("a#b=c&d=e", url); 59 | } 60 | 61 | @Test 62 | public async atomDelay(): Promise { 63 | await Atom.delay(10); 64 | 65 | const ct = new CancelToken(); 66 | const p = Atom.delay(10, ct); 67 | 68 | ct.cancel(); 69 | 70 | try { 71 | await p; 72 | } catch (e) { 73 | Assert.equals("cancelled", e.message); 74 | } 75 | 76 | try { 77 | await Atom.delay(0, ct); 78 | } catch (e) { 79 | Assert.equals("cancelled", e.message); 80 | } 81 | } 82 | 83 | @Test 84 | public getMethod(): void { 85 | 86 | Assert.isUndefined(Atom.get({}, "a")); 87 | 88 | Assert.isNull(Atom.get({a: null}, "a")); 89 | 90 | Assert.isNull(Atom.get(null, "a")); 91 | 92 | Assert.isUndefined(Atom.get(undefined, "a")); 93 | 94 | Assert.equals("a", Atom.get({ a: {b: "a"}}, "a.b")); 95 | 96 | Assert.isUndefined(Atom.get({a: {}}, "a.b")); 97 | 98 | Assert.isNull(Atom.get({a: {b: null}}, "a.b")); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/tests/web/controls/AtomWindowTest.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Test from "@web-atoms/unit-test/dist/Test"; 3 | import { Atom } from "../../../Atom"; 4 | import { CancelToken } from "../../../core/types"; 5 | import { JsonService } from "../../../services/JsonService"; 6 | import { NavigationService } from "../../../services/NavigationService"; 7 | import { AtomTest } from "../../../unit/AtomTest"; 8 | import AtomWebTest from "../../../unit/AtomWebTest"; 9 | import { AtomControl } from "../../../web/controls/AtomControl"; 10 | import { AtomWindow } from "../../../web/controls/AtomWindow"; 11 | import { WindowService } from "../../../web/services/WindowService"; 12 | 13 | function createEvent(name, ... a: any[]): T { 14 | const e = document.createEvent(name); 15 | e.initEvent.apply(e, a); 16 | return e as any as T; 17 | } 18 | 19 | declare var global: any; 20 | 21 | export class AtomWindowTest extends AtomWebTest { 22 | 23 | constructor() { 24 | super(); 25 | const ws = new WindowService(this.app, this.app.resolve(JsonService)); 26 | this.app.put(NavigationService, ws); 27 | this.app.put(WindowService, ws); 28 | } 29 | 30 | @Test 31 | public async drag(): Promise { 32 | 33 | Assert.throwsAsync("cancelled", async () => { 34 | const ns = this.app.resolve(NavigationService) as NavigationService; 35 | 36 | const ct = new CancelToken(); 37 | const p = ns.openPage(SampleWindow, null, { cancelToken: ct }); 38 | 39 | await this.app.waitForPendingCalls(); 40 | 41 | window.dispatchEvent(createEvent("mouseevent", "mousedown", true, false)); 42 | window.dispatchEvent(createEvent("mouseevent", "mousemove", true, false)); 43 | window.dispatchEvent(createEvent("mouseevent", "mouseup", true, false)); 44 | 45 | setTimeout(() => { 46 | ct.cancel(); 47 | }, 100); 48 | 49 | await p; 50 | }); 51 | } 52 | 53 | // @Test 54 | public async alert(): Promise { 55 | const ns = this.app.resolve(NavigationService) as NavigationService; 56 | const p = ns.alert("Test"); 57 | 58 | await this.app.waitForPendingCalls(); 59 | 60 | await Atom.delay(100); 61 | 62 | await this.app.waitForPendingCalls(); 63 | // get yes button... 64 | const e = global.window.document.getElementsByClassName("yes-button")[0] as any; 65 | 66 | setTimeout(() => { 67 | e.click(); 68 | }, (100)); 69 | // e.click(); 70 | 71 | await p; 72 | } 73 | 74 | } 75 | 76 | class SampleWindow extends AtomWindow { 77 | 78 | protected create(): void { 79 | this.windowTemplate = SampleWindowTemplate; 80 | } 81 | 82 | } 83 | 84 | class SampleWindowTemplate extends AtomControl { 85 | 86 | protected create(): void { 87 | this.element.className = "sample_window"; 88 | this.element.textContent = "sample"; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/core/AtomMap.ts: -------------------------------------------------------------------------------- 1 | // if (typeof Map === "undefined") { 2 | 3 | // interface IKeyValuePair { 4 | // key: K; 5 | // value: V; 6 | // } 7 | 8 | // class AtomMap { 9 | 10 | // public get size(): number { 11 | // return this.map.length; 12 | // } 13 | 14 | // private map: Array> = []; 15 | 16 | // public clear(): void { 17 | // this.map.length = 0; 18 | // } 19 | 20 | // public delete(key: K): boolean { 21 | // return this.map.remove((x) => x.key === key); 22 | // } 23 | // public forEach(callbackfn: (value: V, key: K, map: Map) => void, thisArg?: any): void { 24 | // for (const iterator of this.map) { 25 | // callbackfn.call(thisArg, iterator.value, iterator.key, this); 26 | // } 27 | // } 28 | // public get(key: K): V { 29 | // const item = this.getItem(key, false); 30 | // return item ? item.value : undefined; 31 | // } 32 | // public has(key: K): boolean { 33 | // return this.map.find((x) => x.key === key) != null; 34 | // } 35 | // public set(key: K, value: V): this { 36 | // const item = this.getItem(key, true); 37 | // item.value = value; 38 | // return this; 39 | // } 40 | // // public [Symbol.iterator](): IterableIterator<[K, V]> { 41 | // // throw new Error("Method not implemented."); 42 | // // } 43 | // // public keys(): IterableIterator { 44 | // // throw new Error("Method not implemented."); 45 | // // } 46 | // // public values(): IterableIterator { 47 | // // throw new Error("Method not implemented."); 48 | // // } 49 | // // public get [Symbol.toStringTag](): string { 50 | // // return "[Map]"; 51 | // // } 52 | 53 | // private getItem(key: K, create: boolean = false): IKeyValuePair { 54 | // for (const iterator of this.map) { 55 | // if (iterator.key === key) { 56 | // return iterator; 57 | // } 58 | // } 59 | // if (create) { 60 | // const r = { key, value: undefined }; 61 | // this.map.push(r); 62 | // return r; 63 | // } 64 | // } 65 | 66 | // } 67 | 68 | // // tslint:disable-next-line:no-string-literal 69 | // (window as any)["Map"] = AtomMap; 70 | 71 | // } 72 | 73 | declare global { 74 | // tslint:disable-next-line:interface-name 75 | interface Map { 76 | getOrCreate(key: K, factory: (key: K) => V): V; 77 | } 78 | } 79 | 80 | // tslint:disable-next-line:only-arrow-functions 81 | Map.prototype.getOrCreate = function(key: any, factory: (key: any) => any): any { 82 | let item = this.get(key); 83 | if (item === undefined) { 84 | item = factory(key); 85 | this.set(key, item); 86 | } 87 | return item; 88 | }; 89 | 90 | export default Map; 91 | -------------------------------------------------------------------------------- /src/tests/core/AtomListTests.ts: -------------------------------------------------------------------------------- 1 | import { AtomList } from "../../core/AtomList"; 2 | import Assert from "@web-atoms/unit-test/dist/Assert"; 3 | import Test from "@web-atoms/unit-test/dist/Test"; 4 | import TestItem from "@web-atoms/unit-test/dist/TestItem"; 5 | 6 | export class AtomListTest extends TestItem { 7 | 8 | @Test 9 | public remove(): void { 10 | const list = new AtomList(); 11 | 12 | Assert.isFalse(list.remove(4)); 13 | 14 | Assert.isFalse(list.remove((item) => 5)); 15 | 16 | list.addAll([1, 2]); 17 | 18 | Assert.isFalse(list.remove(4)); 19 | 20 | Assert.isFalse(list.remove((item) => item === 5)); 21 | 22 | Assert.isTrue(list.remove(1)); 23 | 24 | Assert.isTrue(list.remove((item) => item === 2)); 25 | 26 | Assert.isEmpty(list.length); 27 | } 28 | 29 | @Test 30 | public removeMultiple(): void { 31 | const list = [1, 2, 3, 4, 5]; 32 | 33 | // remove all even numbers... 34 | list.remove((x) => x <= 2); 35 | 36 | Assert.equals(3, list.length); 37 | } 38 | 39 | @Test 40 | public removeMultipleWithFirst(): void { 41 | const list = [1, 2, 3, 4, 5]; 42 | 43 | // remove all even numbers... 44 | list.remove((x) => (x % 2) === 0); 45 | 46 | Assert.equals(3, list.length); 47 | } 48 | 49 | @Test 50 | public insert(): void { 51 | const list = new AtomList(); 52 | 53 | list.addAll([1, 2]); 54 | 55 | list.insert(1, 5); 56 | 57 | Assert.equals(5, list[1]); 58 | } 59 | 60 | @Test 61 | public wrap(): void { 62 | const list = [1, 2]; 63 | 64 | const d = list.watch((x) => { 65 | const a = x as [any, string, number, any]; 66 | Assert.isTrue(typeof a[1] === "string"); 67 | }, true); 68 | 69 | list.add(1); 70 | } 71 | 72 | @Test 73 | public replace(): void { 74 | 75 | const list = new AtomList(); 76 | 77 | list.add(2); 78 | 79 | const r = [1, 2]; 80 | 81 | list.replace(r); 82 | 83 | Assert.equals(2, r.length); 84 | } 85 | 86 | @Test 87 | public replacePaging(): void { 88 | const list = new AtomList(); 89 | 90 | const r = [1, 2] as any; 91 | r.total = 10; 92 | 93 | list.addAll(r); 94 | 95 | const a = [4, 5] as any; 96 | a.total = 10; 97 | 98 | list.replace(a, 2, 2); 99 | 100 | Assert.equals(2, list.start); 101 | Assert.equals(2, list.size); 102 | Assert.equals(10, list.total); 103 | 104 | list.next(); 105 | 106 | Assert.equals(4, list.start); 107 | 108 | list.prev(); 109 | 110 | Assert.equals(2, list.start); 111 | 112 | list.prev(); 113 | 114 | Assert.equals(0, list.start); 115 | 116 | list.prev(); 117 | 118 | Assert.equals(0, list.start); 119 | 120 | list.start = 0; 121 | 122 | list.size = 2; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/web/styles/AtomToggleButtonBarStyle.ts: -------------------------------------------------------------------------------- 1 | import { BindableProperty } from "../../core/BindableProperty"; 2 | import { AtomListBoxStyle } from "./AtomListBoxStyle"; 3 | import { AtomStyleSheet } from "./AtomStyleSheet"; 4 | import { AtomTheme } from "./AtomTheme"; 5 | import { IStyleDeclaration } from "./IStyleDeclaration"; 6 | 7 | export class AtomToggleButtonBarStyle extends AtomListBoxStyle { 8 | 9 | public toggleColor: string = "blue"; 10 | 11 | public get root(): IStyleDeclaration { 12 | return { 13 | // tslint:disable-next-line: no-string-literal 14 | ... this.getBaseProperty(AtomToggleButtonBarStyle , "root"), 15 | display: "inline-block", 16 | paddingInlineStart: 0, 17 | margin: 0 18 | }; 19 | } 20 | 21 | public get item(): IStyleDeclaration { 22 | return { 23 | // tslint:disable-next-line:no-string-literal 24 | ... this.getBaseProperty(AtomToggleButtonBarStyle , "item"), 25 | borderRadius: 0, 26 | display: "inline-block", 27 | border: "1px solid", 28 | borderLeft: "none", 29 | color: this.toggleColor, 30 | borderColor: this.toggleColor, 31 | cursor: "pointer", 32 | subclasses: { 33 | ":first-child": { 34 | borderTopLeftRadius: `${this.padding || this.theme.padding}px`, 35 | borderBottomLeftRadius: `${this.padding || this.theme.padding}px`, 36 | borderTopRightRadius: 0, 37 | borderBottomRightRadius: 0, 38 | borderLeft: "1px solid" 39 | }, 40 | ":last-child": { 41 | borderTopLeftRadius: 0, 42 | borderBottomLeftRadius: 0, 43 | borderTopRightRadius: `${this.padding || this.theme.padding}px`, 44 | borderBottomRightRadius: `${this.padding || this.theme.padding}px` 45 | } 46 | } 47 | }; 48 | } 49 | 50 | public get selectedItem(): IStyleDeclaration { 51 | return { 52 | ... this.getBaseProperty(AtomToggleButtonBarStyle, "selectedItem"), 53 | borderRadius: 0, 54 | display: "inline-block", 55 | border: "1px solid", 56 | borderLeft: "none", 57 | borderColor: this.toggleColor, 58 | cursor: "pointer", 59 | subclasses: { 60 | ":first-child": { 61 | borderTopLeftRadius: `${this.padding || this.theme.padding}px`, 62 | borderBottomLeftRadius: `${this.padding || this.theme.padding}px`, 63 | borderTopRightRadius: 0, 64 | borderBottomRightRadius: 0 65 | }, 66 | ":last-child": { 67 | borderTopLeftRadius: 0, 68 | borderBottomLeftRadius: 0, 69 | borderTopRightRadius: `${this.padding || this.theme.padding}px`, 70 | borderBottomRightRadius: `${this.padding || this.theme.padding}px` 71 | } 72 | } 73 | }; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/core/InheritedProperty.ts: -------------------------------------------------------------------------------- 1 | import type { AtomControl } from "../web/controls/AtomControl"; 2 | import { AtomBinder } from "./AtomBinder"; 3 | 4 | const cache = {}; 5 | 6 | function getSymbolKey(name: string) { 7 | return cache[name] ??= Symbol(name); 8 | } 9 | 10 | function refreshInherited(ac: AtomControl, key: any, storageKey: any) { 11 | const e = ac.element; 12 | if (!e) { 13 | // control is disposed !! 14 | return; 15 | } 16 | AtomBinder.refreshValue(ac, key); 17 | let start = e.firstElementChild as HTMLElement; 18 | if (!start) { 19 | return; 20 | } 21 | const stack = [start]; 22 | while (stack.length) { 23 | start = stack.pop(); 24 | while (start) { 25 | let firstChild = start.firstElementChild as HTMLElement; 26 | const childControl = start.atomControl; 27 | if (childControl) { 28 | if (childControl[storageKey] === undefined) { 29 | AtomBinder.refreshValue(childControl, key); 30 | } else { 31 | // we will not refresh this element 32 | firstChild = void 0; 33 | } 34 | } 35 | if (firstChild) { 36 | stack.push(firstChild); 37 | } 38 | start = start.nextElementSibling as HTMLElement; 39 | } 40 | } 41 | } 42 | 43 | export function getOwnInheritedProperty(target: any, key: string) { 44 | return target[getSymbolKey(key)]; 45 | } 46 | 47 | /** 48 | * Use this decorator only to watch property changes in `onPropertyChanged` method. 49 | * This decorator also makes enumerable property. 50 | * 51 | * Do not use this on anything except UI control 52 | * @param target control 53 | * @param key name of property 54 | */ 55 | export function InheritedProperty(target: any, key: string): any { 56 | // property value 57 | const iVal: any = target[key]; 58 | 59 | const keyName = getSymbolKey(key); 60 | 61 | target[keyName] = iVal; 62 | 63 | // property getter 64 | const getter: () => any = function(): any { 65 | let start = this; 66 | do { 67 | const p = start[keyName]; 68 | if (typeof p !== "undefined") { 69 | return p; 70 | } 71 | if (!start.element) { 72 | break; 73 | } 74 | start = start.parent; 75 | } while (start); 76 | return undefined; 77 | }; 78 | 79 | // property setter 80 | const setter: (v: any) => void = function(newVal: any): void { 81 | const oldValue = this[keyName]; 82 | 83 | if (oldValue && oldValue.dispose) { 84 | oldValue.dispose(); 85 | } 86 | 87 | this[keyName] = newVal; 88 | 89 | refreshInherited(this, key, keyName); 90 | }; 91 | 92 | // delete property 93 | if (delete target[key]) { 94 | 95 | // create new property with getter and setter 96 | Object.defineProperty(target, key, { 97 | get: getter, 98 | set: setter, 99 | enumerable: true, 100 | configurable: true 101 | }); 102 | 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/web/styles/StyleBuilder.ts: -------------------------------------------------------------------------------- 1 | import Color from "../../core/Color"; 2 | import { BorderStyleType, IStyleDeclaration } from "./IStyleDeclaration"; 3 | 4 | export type CssNumber = number | string; 5 | 6 | export function cssNumberToString(n: CssNumber, unit: string = "px"): string { 7 | if (typeof n === "number") { 8 | if (n === 0) { 9 | return n + ""; 10 | } 11 | 12 | return n + unit; 13 | } 14 | return n; 15 | 16 | } 17 | 18 | export default class StyleBuilder { 19 | 20 | public static get newStyle(): StyleBuilder { 21 | return new StyleBuilder(); 22 | } 23 | 24 | private constructor(private style?: IStyleDeclaration) { 25 | this.style = this.style || {}; 26 | } 27 | 28 | public toStyle(): IStyleDeclaration { 29 | return this.style; 30 | } 31 | 32 | public size( 33 | width: CssNumber, 34 | height: CssNumber 35 | ): StyleBuilder { 36 | width = cssNumberToString(width); 37 | height = cssNumberToString(height); 38 | return this.merge({ 39 | width, 40 | height 41 | }); 42 | } 43 | 44 | public roundBorder(radius: CssNumber): StyleBuilder { 45 | radius = cssNumberToString(radius); 46 | return this.merge({ 47 | borderRadius: radius, 48 | padding: radius 49 | }); 50 | } 51 | 52 | public border( 53 | borderWidth: CssNumber, 54 | borderColor: Color, 55 | borderStyle: BorderStyleType = "solid"): StyleBuilder { 56 | borderWidth = cssNumberToString(borderWidth); 57 | return this.merge({ 58 | borderWidth, 59 | borderStyle, 60 | borderColor 61 | }); 62 | } 63 | 64 | public center(width: CssNumber, height: CssNumber): StyleBuilder { 65 | width = cssNumberToString(width); 66 | height = cssNumberToString(height); 67 | return this.merge({ 68 | position: "absolute", 69 | left: 0, 70 | right: 0, 71 | top: 0, 72 | bottom: 0, 73 | width, 74 | height, 75 | margin: "auto" 76 | }); 77 | } 78 | 79 | public absolute( 80 | left: CssNumber, 81 | top: CssNumber, 82 | right?: CssNumber, 83 | bottom?: CssNumber): StyleBuilder { 84 | left = cssNumberToString(left); 85 | top = cssNumberToString(top); 86 | if (right !== undefined) { 87 | right = cssNumberToString(right); 88 | bottom = cssNumberToString(bottom); 89 | return this.merge ({ 90 | position: "absolute", 91 | left, 92 | top, 93 | right, 94 | bottom 95 | }); 96 | } 97 | return this.merge({ 98 | position: "absolute", 99 | left, 100 | top 101 | }); 102 | } 103 | 104 | private merge(style: IStyleDeclaration): StyleBuilder { 105 | return new StyleBuilder({ 106 | ... this.style, 107 | ... style 108 | }); 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/core/AtomUri.ts: -------------------------------------------------------------------------------- 1 | import { AtomUI } from "../web/core/AtomUI"; 2 | import { INameValuePairs, INameValues } from "./types"; 3 | 4 | export class AtomUri { 5 | public protocol: string; 6 | public path: string; 7 | public query: INameValues; 8 | public hash: INameValues; 9 | // public scheme: string; 10 | public host: string; 11 | public port: string; 12 | 13 | public get pathAndQuery() { 14 | const q: string[] = []; 15 | const h: string[] = []; 16 | for (const key in this.query) { 17 | if (this.query.hasOwnProperty(key)) { 18 | const element = this.query[key]; 19 | if (element === undefined || element === null) { 20 | continue; 21 | } 22 | q.push(`${encodeURIComponent(key)}=${encodeURIComponent(element.toString())}`); 23 | } 24 | } 25 | for (const key in this.hash) { 26 | if (this.hash.hasOwnProperty(key)) { 27 | const element = this.hash[key]; 28 | if (element === undefined || element === null) { 29 | continue; 30 | } 31 | h.push(`${encodeURIComponent(key)}=${encodeURIComponent(element.toString())}`); 32 | } 33 | } 34 | const query = q.length ? "?" + q.join("&") : ""; 35 | const hash = h.length ? "#" + h.join("&") : ""; 36 | let path: string = this.path || "/"; 37 | if (path.startsWith("/")) { 38 | path = path.substr(1); 39 | } 40 | return `${path}${query}${hash}`; 41 | } 42 | 43 | /** 44 | * 45 | */ 46 | constructor(url: string) { 47 | let path: string ; 48 | let query: string = ""; 49 | let hash: string = ""; 50 | let t: string[] = url.split("?"); 51 | path = t[0]; 52 | if (t.length === 2) { 53 | query = t[1] || ""; 54 | 55 | t = query.split("#"); 56 | query = t[0]; 57 | hash = t[1] || ""; 58 | } else { 59 | t = path.split("#"); 60 | path = t[0]; 61 | hash = t[1] || ""; 62 | } 63 | 64 | // extract protocol and domain... 65 | 66 | let scheme: string = ""; 67 | let host: string = ""; 68 | let port: string = ""; 69 | 70 | let i: number = path.indexOf("//"); 71 | if (i !== -1) { 72 | scheme = path.substr(0, i); 73 | path = path.substr(i + 2); 74 | i = path.indexOf("/"); 75 | if (i !== -1) { 76 | host = path.substr(0, i); 77 | path = path.substr(i + 1); 78 | t = host.split(":"); 79 | if (t.length > 1) { 80 | host = t[0]; 81 | port = t[1]; 82 | } 83 | } 84 | } 85 | this.host = host; 86 | this.protocol = scheme; 87 | this.port = port; 88 | this.path = path; 89 | this.query = AtomUI.parseUrl(query); 90 | this.hash = AtomUI.parseUrl(hash); 91 | } 92 | 93 | public toString(): string { 94 | 95 | const port = this.port ? ":" + this.port : ""; 96 | return `${this.protocol}//${this.host}${port}/${this.pathAndQuery}`; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/tests/web/window/WindowTest.tsx: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Test from "@web-atoms/unit-test/dist/Test"; 3 | import TestItem from "@web-atoms/unit-test/dist/TestItem"; 4 | import XNode, { IMergedControl } from "../../../core/XNode"; 5 | import { MockNavigationService } from "../../../services/MockNavigationService"; 6 | import { NavigationService } from "../../../services/NavigationService"; 7 | import AtomWebTest from "../../../unit/AtomWebTest"; 8 | import { AtomComboBox } from "../../../web/controls/AtomComboBox"; 9 | import { AtomPageLink } from "../../../web/controls/AtomPageLink"; 10 | 11 | export class TestCase extends AtomWebTest { 12 | 13 | @Test 14 | public async noWindowRegistered(): Promise { 15 | 16 | // const A = XNode.attach(AtomPageLink, "button"); 17 | 18 | // const B = XNode.prepare("ABCD") as (a?: Partial) => XNode; 19 | 20 | // const BB = XNode.attach(AtomPageLink, B); 21 | 22 | // const d = ; 23 | 24 | const nav = this.navigationService; 25 | 26 | try { 27 | await nav.openPage("WinA"); 28 | Assert.throw("Should not reach here"); 29 | } catch (e) { 30 | Assert.isTrue(/no window registered for/i.test(e)); 31 | } 32 | } 33 | 34 | @Test 35 | public async removeExpectedWindow(): Promise { 36 | const nav = this.navigationService; 37 | 38 | const d = nav.expectWindow("WinA", (v) => null); 39 | 40 | d.dispose(); 41 | 42 | await this.noWindowRegistered(); 43 | } 44 | 45 | @Test 46 | public async confirmTest(): Promise { 47 | const nav = this.navigationService; 48 | 49 | nav.expectConfirm("C1", (mv) => true); 50 | 51 | Assert.isTrue( await nav.confirm("C1") ); 52 | 53 | nav.expectConfirm("C2", (vm) => false); 54 | 55 | Assert.isFalse( await nav.confirm("C2")); 56 | } 57 | 58 | @Test 59 | public async openWindowError(): Promise { 60 | const nav = this.navigationService; 61 | 62 | nav.expectWindow("WinA", (vm) => { 63 | throw new Error("Error"); 64 | }); 65 | 66 | try { 67 | await nav.openPage("WinA"); 68 | Assert.throw("Should not reach here"); 69 | } catch (e) { 70 | Assert.isTrue(e.message === "Error"); 71 | } 72 | } 73 | 74 | @Test 75 | public async expectFailure(): Promise { 76 | const nav = this.navigationService; 77 | 78 | nav.expectAlert("Hello"); 79 | 80 | try { 81 | nav.assert(); 82 | Assert.throw("Should not reach here"); 83 | } catch (e) { 84 | Assert.isTrue(/expected windows did not open/i.test(e)); 85 | } 86 | 87 | await nav.alert("Hello"); 88 | } 89 | 90 | @Test 91 | public async expectWindow(): Promise { 92 | // task: pending 93 | 94 | const nav = this.navigationService; 95 | 96 | nav.expectWindow("WinA", (vm) => { 97 | return "result"; 98 | }); 99 | 100 | const result = await nav.openPage("WinA"); 101 | 102 | Assert.equals("result", result); 103 | 104 | nav.assert(); 105 | 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/tests/web/controls/AtomGridViewTests.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Category from "@web-atoms/unit-test/dist/Category"; 3 | import Test from "@web-atoms/unit-test/dist/Test"; 4 | import TestItem from "@web-atoms/unit-test/dist/TestItem"; 5 | import { App } from "../../../App"; 6 | import { Atom } from "../../../Atom"; 7 | import { AtomTest } from "../../../unit/AtomTest"; 8 | import AtomWebTest from "../../../unit/AtomWebTest"; 9 | import { AtomGridView } from "../../../web/controls/AtomGridView"; 10 | 11 | @Category("Grid view") 12 | export class TestCase extends AtomWebTest { 13 | 14 | // @Test("Grid Test") 15 | // public async test(): Promise { 16 | 17 | // const gv = new AtomGridView(this.app, document.createElement("section")); 18 | // gv.columns = "20,*,50"; 19 | // gv.rows = "10,*,30%"; 20 | // gv.element.style.width = "1000px"; 21 | // gv.element.style.height = "1000px"; 22 | 23 | // const header = document.createElement("header"); 24 | // const headerAny = header as any; 25 | // headerAny.cell = "0:3,0"; 26 | 27 | // gv.append(header); 28 | 29 | // const footer = document.createElement("footer"); 30 | // const footerAny = footer as any; 31 | // footerAny.cell = "0:3,2"; 32 | 33 | // gv.append(footer); 34 | 35 | // const left = document.createElement("div"); 36 | // const leftAny = left as any; 37 | // leftAny.cell = "0,1"; 38 | 39 | // gv.append(left); 40 | 41 | // const right = document.createElement("div"); 42 | // const rightAny = right as any; 43 | // rightAny.cell = "2,1"; 44 | 45 | // gv.append(right); 46 | 47 | // const fill = document.createElement("div"); 48 | // const fillAny = fill as any; 49 | // fillAny.cell = "1,1"; 50 | 51 | // gv.append(fill); 52 | 53 | // gv.invalidate(); 54 | 55 | // await Atom.delay(10); 56 | 57 | // await this.app.waitForPendingCalls(); 58 | 59 | // const hps = header.parentElement.style; 60 | 61 | // Assert.equals("0px", hps.left); 62 | // Assert.equals("0px", hps.top); 63 | // Assert.equals("1000px" , hps.width); 64 | // Assert.equals("10px", hps.height); 65 | 66 | // const lps = left.parentElement.style; 67 | 68 | // Assert.equals("0px", lps.left); 69 | // Assert.equals("10px", lps.top); 70 | // Assert.equals("20px" , lps.width); 71 | // Assert.equals("690px", lps.height); 72 | 73 | // const rps = right.parentElement.style; 74 | 75 | // Assert.equals("950px", rps.left); 76 | // Assert.equals("10px", rps.top); 77 | // Assert.equals("50px" , rps.width); 78 | // Assert.equals("690px", rps.height); 79 | 80 | // const fillps = fill.parentElement.style; 81 | 82 | // Assert.equals("20px", fillps.left); 83 | // Assert.equals("10px", fillps.top); 84 | // Assert.equals("930px" , fillps.width); 85 | // Assert.equals("690px", fillps.height); 86 | 87 | // const footerps = footer.parentElement.style; 88 | 89 | // Assert.equals("0px", footerps.left); 90 | // Assert.equals("700px", footerps.top); 91 | // Assert.equals("1000px" , footerps.width); 92 | // Assert.equals("300px", footerps.height); 93 | // } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/tests/web/controls/AtomControlPropertiesTest.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Test from "@web-atoms/unit-test/dist/Test"; 3 | import TestItem from "@web-atoms/unit-test/dist/TestItem"; 4 | import Markdown from "../../../core/Markdown"; 5 | import { AtomControl } from "../../../web/controls/AtomControl"; 6 | import AtomWebTest from "../../../unit/AtomWebTest"; 7 | 8 | class TestViewModel { 9 | 10 | public name: any = ""; 11 | } 12 | 13 | class InputControl extends AtomControl { 14 | 15 | public create(): void { 16 | const input = document.createElement("input"); 17 | this.viewModel = this.resolve(TestViewModel); 18 | this.append(input); 19 | 20 | this.runAfterInit(() => { 21 | this.setLocalValue(input, "autofocus", true); 22 | }); 23 | } 24 | 25 | } 26 | 27 | class FormattedControl extends AtomControl { 28 | 29 | public create(): void { 30 | this.viewModel = this.resolve(TestViewModel); 31 | this.bind(this.element, "formattedText", [["viewModel", "name"]]); 32 | } 33 | } 34 | 35 | class ImageSrcControl extends AtomControl { 36 | 37 | public create(): void { 38 | this.viewModel = this.resolve(TestViewModel); 39 | this.bind(this.element, "src", [["viewModel", "name"]]); 40 | } 41 | } 42 | 43 | class SetClassControl extends AtomControl { 44 | 45 | public create(): void { 46 | this.viewModel = this.resolve(TestViewModel); 47 | this.bind(this.element, "class", [["viewModel", "name"]]); 48 | } 49 | } 50 | 51 | export default class AtomControlPropertiesTest extends AtomWebTest { 52 | 53 | // @Test 54 | // public async autoFocus(): Promise { 55 | // const root = document.createElement("div"); 56 | // const control = new InputControl(this.app, root); 57 | 58 | // await this.app.waitForPendingCalls(); 59 | 60 | // const input = control.element.firstElementChild as HTMLInputElement; 61 | // Assert.equals(input, document.activeElement); 62 | 63 | // } 64 | 65 | @Test 66 | public async formattedText(): Promise { 67 | const control = new FormattedControl(this.app); 68 | 69 | await this.app.waitForPendingCalls(); 70 | 71 | control.viewModel.name = Markdown.from("Akash **Kava**"); 72 | 73 | Assert.equals("Akash Kava", control.element.innerHTML); 74 | 75 | } 76 | 77 | @Test 78 | public async src(): Promise { 79 | const control = new ImageSrcControl(this.app, document.createElement("img")); 80 | 81 | await this.app.waitForPendingCalls(); 82 | 83 | control.viewModel.name = "/a.jpg"; 84 | 85 | const img = control.element as HTMLImageElement; 86 | 87 | Assert.equals("/a.jpg", img.src); 88 | 89 | control.viewModel.name = "http://a/a.jpg"; 90 | 91 | Assert.equals("//a/a.jpg", img.src); 92 | } 93 | 94 | @Test 95 | public async setClass(): Promise { 96 | 97 | const control = new SetClassControl(this.app); 98 | await this.app.waitForPendingCalls(); 99 | 100 | const vm = control.viewModel as TestViewModel; 101 | 102 | vm.name = { a: 1, b: 1, c: 0 }; 103 | 104 | Assert.equals("a b", control.element.className); 105 | 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/tests/view-model/ActionTest.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Category from "@web-atoms/unit-test/dist/Category"; 3 | import Test from "@web-atoms/unit-test/dist/Test"; 4 | import { Atom } from "../../Atom"; 5 | import { CancelToken } from "../../core/types"; 6 | import DISingleton from "../../di/DISingleton"; 7 | import { Inject, InjectedTypes } from "../../di/Inject"; 8 | import Action from "../../view-model/Action"; 9 | import { AtomViewModel, Validate } from "../../view-model/AtomViewModel"; 10 | import AtomWebTest from "../../unit/AtomWebTest"; 11 | 12 | interface IUser { 13 | name?: string; 14 | email?: string; 15 | } 16 | 17 | @DISingleton() 18 | class RemoteService { 19 | public async signUp(user: IUser): Promise { 20 | await Atom.delay(100); 21 | if (!/\@/i.test(user.email)) { 22 | throw new Error("Invalid email address"); 23 | } 24 | return `Success ${user.name}`; 25 | } 26 | } 27 | 28 | class ActionViewModel extends AtomViewModel { 29 | 30 | public model: IUser = { 31 | name: "", 32 | email: "" 33 | }; 34 | 35 | public result: string; 36 | 37 | @Validate 38 | public get errorName(): string { 39 | return this.model.name ? "" : "Name is required"; 40 | } 41 | 42 | @Inject 43 | private remoteService: RemoteService; 44 | 45 | @Action({ confirm: "Are you sure you want to cancel"}) 46 | public async cancel(): Promise { 47 | await Atom.delay(10); 48 | this.model.name = ""; 49 | this.model.email = ""; 50 | } 51 | 52 | @Action({ 53 | success: "Operation completed successfully", 54 | validate: true, 55 | successMode: "alert" 56 | }) 57 | public async signUp(): Promise { 58 | this.result = await this.remoteService.signUp(this.model); 59 | } 60 | 61 | } 62 | 63 | @Category("View Model Action") 64 | export default class ActionTest extends AtomWebTest { 65 | 66 | @Test 67 | public async validate(): Promise { 68 | const vm = await this.createViewModel(ActionViewModel); 69 | this.navigationService.expectAlert("Please enter correct information"); 70 | 71 | await vm.signUp(); 72 | } 73 | 74 | @Test 75 | public async exception(): Promise { 76 | const vm = await this.createViewModel(ActionViewModel); 77 | vm.model.name = "a"; 78 | vm.model.email = "a"; 79 | this.navigationService.expectAlert("Error: Invalid email address"); 80 | 81 | await vm.signUp(); 82 | } 83 | 84 | @Test 85 | public async success(): Promise { 86 | const vm = await this.createViewModel(ActionViewModel); 87 | vm.model.name = "a"; 88 | vm.model.email = "a@a"; 89 | this.navigationService.expectAlert("Operation completed successfully"); 90 | await vm.signUp(); 91 | Assert.equals("Success a", vm.result); 92 | } 93 | 94 | @Test 95 | public async confirm(): Promise { 96 | const vm = await this.createViewModel(ActionViewModel); 97 | vm.model.name = "a"; 98 | vm.model.email = "a@a"; 99 | this.navigationService.expectConfirm("Are you sure you want to cancel", () => true); 100 | await vm.cancel(); 101 | Assert.equals("", vm.model.name); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/tests/view-model/ParentViewModelTest.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Test from "@web-atoms/unit-test/dist/Test"; 3 | import { AtomTest } from "../../unit/AtomTest"; 4 | import { AtomViewModel, Validate } from "../../view-model/AtomViewModel"; 5 | 6 | export default class ParentViewModelTest extends AtomTest { 7 | 8 | @Test 9 | public async validation(): Promise { 10 | 11 | const parent = new DataViewModel(this.app); 12 | 13 | const child = new DataViewModel(this.app); 14 | child.parent = parent; 15 | 16 | await this.app.waitForPendingCalls(); 17 | 18 | Assert.isEmpty(parent.errorName); 19 | 20 | Assert.isEmpty(child.errorName); 21 | 22 | parent.model.name = "a"; 23 | 24 | Assert.isFalse(parent.isValid); 25 | 26 | Assert.equals("Name cannot be empty", child.errorName); 27 | 28 | } 29 | 30 | @Test 31 | public async validationWithDisposableChild(): Promise { 32 | 33 | const parent = new DataViewModel(this.app); 34 | 35 | const child = new DataViewModel(this.app); 36 | child.parent = parent; 37 | 38 | await this.app.waitForPendingCalls(); 39 | 40 | Assert.isEmpty(parent.errorName); 41 | 42 | Assert.isEmpty(child.errorName); 43 | 44 | parent.model.name = "a"; 45 | 46 | Assert.isFalse(parent.isValid); 47 | 48 | Assert.equals("Name cannot be empty", child.errorName); 49 | 50 | child.dispose(); 51 | 52 | Assert.isTrue(parent.isValid); 53 | 54 | } 55 | 56 | @Test 57 | public async validationWithDirtyChild(): Promise { 58 | 59 | const parent = new DataViewModel(this.app); 60 | 61 | const child = new DataViewModel(this.app); 62 | child.parent = parent; 63 | 64 | await this.app.waitForPendingCalls(); 65 | 66 | Assert.isEmpty(parent.errorName); 67 | 68 | Assert.isEmpty(child.errorName); 69 | 70 | parent.model.name = "a"; 71 | 72 | Assert.isFalse(parent.isValid); 73 | 74 | Assert.equals("Name cannot be empty", child.errorName); 75 | 76 | child.parent = null; 77 | 78 | Assert.isTrue(parent.isValid); 79 | 80 | } 81 | 82 | @Test 83 | public async validationWithMultipleChild(): Promise { 84 | const parent = new DataViewModel(this.app); 85 | 86 | const child = new DataViewModel(this.app); 87 | child.parent = parent; 88 | 89 | const child2 = new DataViewModel(this.app); 90 | child2.parent = parent; 91 | 92 | Assert.equals(parent, child2.parent); 93 | 94 | await this.app.waitForPendingCalls(); 95 | 96 | Assert.isEmpty(parent.errorName); 97 | 98 | Assert.isEmpty(child.errorName); 99 | 100 | parent.model.name = "a"; 101 | 102 | child2.model.name = "a"; 103 | 104 | Assert.isFalse(parent.isValid); 105 | 106 | Assert.equals("Name cannot be empty", child.errorName); 107 | 108 | child.dispose(); 109 | 110 | Assert.isTrue(parent.isValid); 111 | } 112 | } 113 | 114 | class DataViewModel extends AtomViewModel { 115 | 116 | public model: any = {}; 117 | 118 | @Validate 119 | public get errorName(): string { 120 | return this.model.name ? "" : "Name cannot be empty"; 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/tests/web/controls/AtomControlStyleTest.ts: -------------------------------------------------------------------------------- 1 | import Assert from "@web-atoms/unit-test/dist/Assert"; 2 | import Category from "@web-atoms/unit-test/dist/Category"; 3 | import Test from "@web-atoms/unit-test/dist/Test"; 4 | import AtomWebTest from "../../../unit/AtomWebTest"; 5 | import { AtomViewModel } from "../../../view-model/AtomViewModel"; 6 | import { AtomControl } from "../../../web/controls/AtomControl"; 7 | import { AtomStyle } from "../../../web/styles/AtomStyle"; 8 | import { IStyleDeclaration } from "../../../web/styles/IStyleDeclaration"; 9 | 10 | class TestControl extends AtomControl { 11 | 12 | public create(): void { 13 | this.defaultControlStyle = TestStyle; 14 | 15 | this.runAfterInit(() => { 16 | this.element.className = this.controlStyle.name; 17 | }); 18 | } 19 | 20 | } 21 | 22 | class TestStyle extends AtomStyle { 23 | 24 | public get root(): IStyleDeclaration { 25 | return { 26 | padding: "5px" 27 | }; 28 | } 29 | 30 | } 31 | 32 | class InheritedStyle extends TestStyle { 33 | 34 | public get root(): IStyleDeclaration { 35 | return { 36 | padding: "5px" 37 | }; 38 | } 39 | 40 | } 41 | 42 | class InheritedTestControl extends TestControl { 43 | public create(): void { 44 | this.defaultControlStyle = TestStyle; 45 | this.bind(this.element, "class", [["this", "controlStyle", "name"]], false, null, this); 46 | } 47 | } 48 | 49 | class TestViewModel extends AtomViewModel { 50 | 51 | public model: any; 52 | 53 | } 54 | 55 | class StyleElementClass extends AtomControl { 56 | 57 | public create(): void { 58 | this.viewModel = this.resolve(TestViewModel); 59 | this.bind(this.element, "styleClass", [["viewModel", "model"]]); 60 | } 61 | } 62 | 63 | @Category("AtomControl Style") 64 | export default class AtomControlStyleTest extends AtomWebTest { 65 | 66 | @Test 67 | public async defaultStyle(): Promise { 68 | const tc = new TestControl(this.app); 69 | 70 | await this.app.waitForPendingCalls(); 71 | 72 | Assert.isTrue(tc.controlStyle instanceof TestStyle); 73 | Assert.equals( tc.controlStyle.name, tc.element.className); 74 | } 75 | 76 | @Test 77 | public async inheritedStyle(): Promise { 78 | const tc = new InheritedTestControl(this.app); 79 | 80 | await this.app.waitForPendingCalls(); 81 | 82 | Assert.isTrue(tc.controlStyle instanceof TestStyle); 83 | Assert.equals( tc.controlStyle.name, tc.element.className); 84 | } 85 | 86 | @Test 87 | public async styleChange(): Promise { 88 | const tc = new InheritedTestControl(this.app); 89 | 90 | await this.app.waitForPendingCalls(); 91 | 92 | tc.controlStyle = InheritedStyle as any; 93 | 94 | Assert.isTrue(tc.controlStyle instanceof InheritedStyle); 95 | Assert.equals( tc.controlStyle.name, tc.element.className); 96 | } 97 | 98 | @Test 99 | public async styleClass(): Promise { 100 | const tc = new StyleElementClass(this.app); 101 | 102 | await this.app.waitForPendingCalls(); 103 | 104 | const vm = tc.viewModel as TestViewModel; 105 | vm.model = { a: 1, b: 2 }; 106 | 107 | Assert.equals(tc.element.className, "a b"); 108 | 109 | vm.model = { a: 0, b: 1 }; 110 | Assert.equals(tc.element.className, "b"); 111 | } 112 | } 113 | --------------------------------------------------------------------------------