diff --git a/lib/$Element.ts b/lib/$Element.ts index 0acab4d..0edad20 100644 --- a/lib/$Element.ts +++ b/lib/$Element.ts @@ -7,6 +7,7 @@ export interface $ElementOptions { export class $Element extends $Node { readonly dom: H; + private static_classes = new Set(); constructor(tagname: string, options?: $ElementOptions) { super(); this.dom = document.createElement(tagname) as H; @@ -28,17 +29,38 @@ export class $Element extends $Node { /**Replace list of class name to element. @example Element.class('name1', 'name2') */ class(): DOMTokenList; class(...name: (string | undefined)[]): this; - class(...name: (string | undefined)[]): this | DOMTokenList {return $.fluent(this, arguments, () => this.dom.classList, () => {this.dom.className = ''; this.dom.classList.add(...name.detype())})} + class(...name: (string | undefined)[]): this | DOMTokenList {return $.fluent(this, arguments, () => this.dom.classList, () => {this.dom.classList.forEach(n => this.static_classes.has(n) ?? this.dom.classList.remove(n)); this.dom.classList.add(...name.detype())})} /**Add class name to dom. */ addClass(...name: (string | undefined)[]): this {return $.fluent(this, arguments, () => this, () => {this.dom.classList.add(...name.detype())})} /**Remove class name from dom */ removeClass(...name: (string | undefined)[]): this {return $.fluent(this, arguments, () => this, () => {this.dom.classList.remove(...name.detype())})} + staticClass(): Set; + staticClass(...name: (string | undefined)[]): this; + staticClass(...name: (string | undefined)[]) {return $.fluent(this, arguments, () => this.static_classes, () => {this.removeClass(...this.static_classes); this.static_classes.clear(); this.addStaticClass(...name);})} + addStaticClass(...name: (string | undefined)[]) {return $.fluent(this, arguments, () => this, () => {name.detype().forEach(n => this.static_classes.add(n)); this.addClass(...name)})} + removeStaticClass(...name: (string | undefined)[]) {return $.fluent(this, arguments, () => this, () => {name.detype().forEach(n => this.static_classes.delete(n)); this.removeClass(...name)})} + /**Modify css of element. */ css(): CSSStyleDeclaration css(style: Partial): this; css(style?: Partial) { return $.fluent(this, arguments, () => this.dom.style, () => {Object.assign(this.dom.style, style)})} + attribute(qualifiedName: string | undefined): string | null; + attribute(qualifiedName: string | undefined, value?: string | number | boolean): this; + attribute(qualifiedName: string | undefined, value?: string | number | boolean): this | string | null { + if (!arguments.length) return null; + if (arguments.length === 1) { + if (qualifiedName === undefined) return null; + return this.dom.getAttribute(qualifiedName); + } + if (arguments.length === 2) { + if (qualifiedName && value) this.dom.setAttribute(qualifiedName, `${value}`); + return this; + } + return this; + } + autocapitalize(): Autocapitalize; autocapitalize(autocapitalize?: Autocapitalize): this; autocapitalize(autocapitalize?: Autocapitalize) { return $.fluent(this, arguments, () => this.dom.autocapitalize, () => $.set(this.dom, 'autocapitalize', autocapitalize))} diff --git a/lib/$Node.ts b/lib/$Node.ts index 10ca1bc..227fb96 100644 --- a/lib/$Node.ts +++ b/lib/$Node.ts @@ -1,10 +1,10 @@ -import { $State, $Text } from "../index"; +import { $Element, $State, $Text } from "../index"; import { $Container } from "./$Container"; export abstract class $Node { readonly parent?: $Container; abstract readonly dom: N; - readonly $hidden: boolean = false; + readonly __hidden: boolean = false; private domEvents: {[key: string]: Map} = {}; on(type: K, callback: (event: HTMLElementEventMap[K], $node: this) => void, options?: AddEventListenerOptions | boolean) { @@ -32,10 +32,10 @@ export abstract class $Node { hide(): boolean; hide(hide?: boolean | $State): this; - hide(hide?: boolean | $State) { return $.fluent(this, arguments, () => this.$hidden, () => { + hide(hide?: boolean | $State) { return $.fluent(this, arguments, () => this.__hidden, () => { if (hide === undefined) return; - if (hide instanceof $State) { (this as Mutable<$Node>).$hidden = hide.value; hide.use(this, 'hide')} - else (this as Mutable<$Node>).$hidden = hide; + if (hide instanceof $State) { (this as Mutable<$Node>).__hidden = hide.value; hide.use(this, 'hide')} + else (this as Mutable<$Node>).__hidden = hide; this.parent?.children.render(); return this; })} @@ -60,8 +60,11 @@ export abstract class $Node { } self(callback: ($node: this) => void) { callback(this); return this; } - inDOM() { return document.contains(this.dom); } + isElement() { + if (this instanceof $Element) return this; + else return undefined; + } static from(element: HTMLElement | Text): $Node { if (element.$) return element.$; diff --git a/lib/$NodeManager.ts b/lib/$NodeManager.ts index 8d35ef3..8c2888a 100644 --- a/lib/$NodeManager.ts +++ b/lib/$NodeManager.ts @@ -52,13 +52,13 @@ export class $NodeManager { while (nodeList.length || domList.length) { // while nodeList or domList has item const [node, dom] = [nodeList.at(0), domList.at(0)]; if (!dom) { if (node && !appendedNodeList.includes(node)) node.remove(); nodeList.shift()} - else if (!node) { if (!dom.$.$hidden) this.#dom.append(dom); domList.shift();} + else if (!node) { if (!dom.$.__hidden) this.#dom.append(dom); domList.shift();} else if (dom !== node) { - if (!dom.$.$hidden) { this.#dom.insertBefore(dom, node); appendedNodeList.push(dom) } + if (!dom.$.__hidden) { this.#dom.insertBefore(dom, node); appendedNodeList.push(dom) } domList.shift(); } else { - if (dom.$.$hidden) this.#dom.removeChild(dom); + if (dom.$.__hidden) this.#dom.removeChild(dom); domList.shift(); nodeList.shift(); } } diff --git a/lib/Router/Route.ts b/lib/Router/Route.ts index 5aa23da..69c4984 100644 --- a/lib/Router/Route.ts +++ b/lib/Router/Route.ts @@ -28,17 +28,19 @@ export interface RouteRecord extends $EventMethod {}; export class RouteRecord { id: string; readonly content?: $Node; - events = new $EventManager().register('open') + events = new $EventManager().register('open', 'load') constructor(id: string) { this.id = id; } } export interface RouteRecordEventMap { - 'open': [path: string, record: RouteRecord] + 'open': [path: string, record: RouteRecord]; + 'load': [path: string, record: RouteRecord]; } export interface RouteRequest { params: PathParamResolver, record: RouteRecord, + loaded: () => void; } \ No newline at end of file diff --git a/lib/Router/Router.ts b/lib/Router/Router.ts index 7476683..9a19acd 100644 --- a/lib/Router/Router.ts +++ b/lib/Router/Router.ts @@ -9,7 +9,7 @@ export class Router { recordMap = new Map(); view: $Container; index: number = 0; - events = new $EventManager().register('pathchange', 'notfound'); + events = new $EventManager().register('pathchange', 'notfound', 'load'); basePath: string; constructor(basePath: string, view: $Container) { this.basePath = basePath; @@ -26,7 +26,7 @@ export class Router { /**Start listen to the path change */ listen() { if (!history.state || 'index' in history.state === false) { - const routeData: RouteData = {index: this.index} + const routeData: RouteData = {index: this.index, data: {}} history.replaceState(routeData, '') } else { this.index = history.state.index @@ -39,10 +39,11 @@ export class Router { } /**Open path */ - open(path: string) { + open(path: string | undefined) { + if (path === undefined) return; if (path === location.href) return this; this.index += 1; - const routeData: RouteData = { index: this.index }; + const routeData: RouteData = { index: this.index, data: {} }; history.pushState(routeData, '', path); $.routers.forEach(router => router.resolvePath()) this.events.fire('pathchange', path, 'Forward'); @@ -59,6 +60,12 @@ export class Router { return this; } + setStateData(key: string, value: any) { + if (history.state.data === undefined) history.state.data = {}; + history.state.data[key] = value; + return this; + } + private popstate = (() => { // Forward if (history.state.index > this.index) { } @@ -77,8 +84,7 @@ export class Router { const record = this.recordMap.get(pathId); if (record) { found = true; - if (record.content && this.view.contains(record.content)) return true; - this.view.content(record.content); + if (record.content && !this.view.contains(record.content)) this.view.content(record.content); record.events.fire('open', path, record); return true; } @@ -86,7 +92,14 @@ export class Router { } const create = (pathId: string, route: Route, data: any) => { const record = new RouteRecord(pathId); - let content = route.builder({params: data, record: record}); + let content = route.builder({ + params: data, + record: record, + loaded: () => { + record.events.fire('load', pathId, record); + this.events.fire('load', pathId); + } + }); if (typeof content === 'string') content = new $Text(content); (record as Mutable).content = content; this.recordMap.set(pathId, record); @@ -134,10 +147,11 @@ export class Router { } interface RouterEventMap { pathchange: [path: string, navigation: 'Back' | 'Forward']; - notfound: [path: string] + notfound: [path: string]; + load: [path: string]; } type RouteData = { index: number; - data?: any; + data: {[key: string]: any}; } \ No newline at end of file