diff --git a/$index.ts b/$index.ts index d71e95d..80b13c1 100644 --- a/$index.ts +++ b/$index.ts @@ -1,41 +1,47 @@ -import { $Node, $State } from "./index"; -import { $Anchor } from "./lib/$Anchor"; -import { $Button } from "./lib/$Button"; -import { $Form } from "./lib/$Form"; -import { $Input } from "./lib/$Input"; -import { $Container } from "./lib/$Container"; -import { $Element } from "./lib/$Element"; -import { $Label } from "./lib/$Label"; -import { Router } from "./lib/Router/Router"; -import { $Image } from "./lib/$Image"; -import { $Canvas } from "./lib/$Canvas"; -import { $Dialog } from "./lib/$Dialog"; -import { $View } from "./lib/$View"; -import { $Select } from "./lib/$Select"; -import { $Option } from "./lib/$Option"; -import { $OptGroup } from "./lib/$OptGroup"; -import { $Textarea } from "./lib/$Textarea"; +import { $Node, $State, $StateOption } from "./index"; +import { $Anchor } from "./lib/node/$Anchor"; +import { $Button } from "./lib/node/$Button"; +import { $Form } from "./lib/node/$Form"; +import { $Input } from "./lib/node/$Input"; +import { $Container } from "./lib/node/$Container"; +import { $Element } from "./lib/node/$Element"; +import { $Label } from "./lib/node/$Label"; +import { Router } from "./lib/router/Router"; +import { $Image } from "./lib/node/$Image"; +import { $Canvas } from "./lib/node/$Canvas"; +import { $Dialog } from "./lib/node/$Dialog"; +import { $View } from "./lib/node/$View"; +import { $Select } from "./lib/node/$Select"; +import { $Option } from "./lib/node/$Option"; +import { $OptGroup } from "./lib/node/$OptGroup"; +import { $Textarea } from "./lib/node/$Textarea"; +import { $Util } from "./lib/$Util"; +import { $HTMLElement } from "./lib/node/$HTMLElement"; +import { $AsyncNode } from "./lib/node/$AsyncNode"; + export type $ = typeof $; export function $(query: `::${string}`): E[]; export function $(query: `:${string}`): E | null; export function $(element: null): null; export function $(resolver: K): $.TagNameTypeMap[K]; export function $(resolver: K): $Container; -export function $(htmlElement: H): $.HTMLElementTo$ElementMap; +export function $(htmlElement: H): $.$HTMLElementMap; export function $(element: H): $Element; +export function $(node: N): N; export function $(element: H): $Element; export function $(element: null | HTMLElement | EventTarget): $Element | null; export function $(element: undefined): undefined; export function $(resolver: any) { if (typeof resolver === 'undefined') return resolver; if (resolver === null) return resolver; + if (resolver instanceof $Node) return resolver; if (typeof resolver === 'string') { if (resolver.startsWith('::')) return Array.from(document.querySelectorAll(resolver.replace(/^::/, ''))).map(dom => $(dom)); else if (resolver.startsWith(':')) return $(document.querySelector(resolver.replace(/^:/, ''))); else if (resolver in $.TagNameElementMap) { const instance = $.TagNameElementMap[resolver as keyof typeof $.TagNameElementMap] switch (instance) { - case $Element: return new $Element(resolver); + case $HTMLElement: return new $HTMLElement(resolver); case $Anchor: return new $Anchor(); case $Container: return new $Container(resolver); case $Input: return new $Input(); @@ -50,20 +56,22 @@ export function $(resolver: any) { case $Option: return new $Option(); case $OptGroup: return new $OptGroup(); case $Textarea: return new $Textarea(); + case $AsyncNode: return new $AsyncNode(); } } else return new $Container(resolver); } - if (resolver instanceof HTMLElement || resolver instanceof Text) { + if (resolver instanceof HTMLElement || resolver instanceof Text || resolver instanceof SVGElement) { if (resolver.$) return resolver.$; - else return $Node.from(resolver); + else return $Util.from(resolver); } - throw '$: NOT SUPPORT TARGET ELEMENT TYPE' + throw `$: NOT SUPPORT TARGET ELEMENT TYPE ('${resolver}')` } export namespace $ { - export let anchorHandler: null | ((url: string, e: Event) => void) = null; + export let anchorHandler: null | (($a: $Anchor, e: Event) => void) = null; export let anchorPreventDefault: boolean = false; export const routers = new Set; export const TagNameElementMap = { + 'body': $Container, 'a': $Anchor, 'p': $Container, 'pre': $Container, @@ -92,7 +100,8 @@ export namespace $ { 'select': $Select, 'option': $Option, 'optgroup': $OptGroup, - 'textarea': $Textarea + 'textarea': $Textarea, + 'async': $AsyncNode, } export type TagNameTypeMap = { [key in keyof typeof $.TagNameElementMap]: InstanceType; @@ -100,7 +109,7 @@ export namespace $ { export type ContainerTypeTagName = Exclude; export type SelfTypeTagName = 'input'; - export type HTMLElementTo$ElementMap = + export type $HTMLElementMap = H extends HTMLLabelElement ? $Label : H extends HTMLInputElement ? $Input : H extends HTMLAnchorElement ? $Anchor @@ -116,6 +125,10 @@ export namespace $ { : H extends HTMLTextAreaElement ? $Textarea : $Container; + export function open(path: string | URL | undefined) { return Router.open(path) } + export function replace(path: string | URL | undefined) { return Router.replace(path) } + export function back() { return Router.back() } + /** * A helper for fluent method design. Return the `instance` object when arguments length not equal 0. Otherwise, return the `value`. * @param instance The object to return when arguments length not equal 0. @@ -135,41 +148,34 @@ export namespace $ { else return [multable]; } - export function mixin(target: any, constructors: OrArray) { - orArrayResolve(constructors).forEach(constructor => { - Object.getOwnPropertyNames(constructor.prototype).forEach(name => { - if (name === 'constructor') return; - Object.defineProperty( - target.prototype, - name, - Object.getOwnPropertyDescriptor(constructor.prototype, name) || Object.create(null) - ) - }) - }) - return target; - } + export function mixin(target: any, constructors: OrArray) { return $Util.mixin(target, constructors) } /** - * A helper for $State.set() which apply value to target. + * A helper for undefined able value and $State.set() which apply value to target. * @param object Target object. * @param key The key of target object. * @param value Value of target property or parameter of method(Using Tuple to apply parameter). * @param methodKey Variant key name when apply value on $State.set() * @returns */ - export function set(object: O, key: K, value: O[K] extends (...args: any) => any ? (Parameters | $State>) : O[K] | undefined | $State, methodKey?: string) { - if (value === undefined) return; - if (value instanceof $State && object instanceof Node) { - value.use(object.$, methodKey ?? key as any); - const prop = object[key]; - if (prop instanceof Function) prop(value.value); - else object[key] = value.value; - return; - } - object[key] = value as any; + export function set( + object: O, key: K, + value: O[K] extends (...args: any) => any + ? (Parameters | $State | undefined>) + : (O[K] | undefined | $State), + methodKey?: string) { + if (value === undefined) return; + if (value instanceof $State && object instanceof Node) { + value.use(object.$, methodKey ?? key as any); + if (object[key] instanceof Function) (object[key] as Function)(value) + else object[key] = value.value; + return; + } + if (object[key] instanceof Function) (object[key] as Function)(value); + else object[key] = value as any; } - export function state(value: T) { - return new $State(value) + export function state(value: T, options?: $StateOption) { + return new $State(value, options) } export async function resize(object: Blob, size: number): Promise { @@ -200,6 +206,11 @@ export namespace $ { return parseInt(getComputedStyle(document.documentElement).fontSize) * amount } + export function html(html: string) { + const body = new DOMParser().parseFromString(html, 'text/html').body; + return Array.from(body.children).map(child => $(child)) + } + /**Build multiple element in once. */ export function builder>(bulder: F, params: [...Parameters][], callback?: BuilderSelfFunction): R[] export function builder>(bulder: [F, ...Parameters], size: number, callback?: BuilderSelfFunction): R[] @@ -237,5 +248,4 @@ export namespace $ { } type BuildNodeFunction = (...args: any[]) => $Node; type BuilderSelfFunction = (self: K) => void; -globalThis.$ = $; - +globalThis.$ = $; \ No newline at end of file diff --git a/index.ts b/index.ts index 0122ef7..2e65871 100644 --- a/index.ts +++ b/index.ts @@ -20,7 +20,7 @@ declare global { type ImageLoading = "eager" | "lazy"; type ContructorType = { new (...args: any[]): T } interface Node { - $: import('./lib/$Node').$Node; + $: import('./lib/node/$Node').$Node; } } Array.prototype.detype = function (this: O[], ...types: T[]) { @@ -30,20 +30,22 @@ Array.prototype.detype = function (this: O[], ... }) as Exclude[]; } export * from "./$index"; -export * from "./lib/Router/Route"; -export * from "./lib/Router/Router"; -export * from "./lib/$Node"; -export * from "./lib/$Anchor"; -export * from "./lib/$Element"; +export * from "./lib/router/Route"; +export * from "./lib/router/Router"; +export * from "./lib/node/$Node"; +export * from "./lib/node/$Anchor"; +export * from "./lib/node/$Element"; export * from "./lib/$NodeManager"; -export * from "./lib/$Text"; -export * from "./lib/$Container"; -export * from "./lib/$Button"; -export * from "./lib/$Form"; +export * from "./lib/node/$Text"; +export * from "./lib/node/$Container"; +export * from "./lib/node/$Button"; +export * from "./lib/node/$Form"; export * from "./lib/$EventManager"; export * from "./lib/$State"; -export * from "./lib/$View"; -export * from "./lib/$Select"; -export * from "./lib/$Option"; -export * from "./lib/$OptGroup"; -export * from "./lib/$Textarea"; \ No newline at end of file +export * from "./lib/node/$View"; +export * from "./lib/node/$Select"; +export * from "./lib/node/$Option"; +export * from "./lib/node/$OptGroup"; +export * from "./lib/node/$Textarea"; +export * from "./lib/node/$Image"; +export * from "./lib/node/$AsyncNode"; \ No newline at end of file diff --git a/lib/$EventManager.ts b/lib/$EventManager.ts index f6bc676..35cc005 100644 --- a/lib/$EventManager.ts +++ b/lib/$EventManager.ts @@ -1,11 +1,11 @@ export abstract class $EventMethod { abstract events: $EventManager; //@ts-expect-error - on(type: K, callback: (...args: EM[K]) => void) { this.events.on(type, callback); return this } + on(type: K, callback: (...args: EM[K]) => any) { this.events.on(type, callback); return this } //@ts-expect-error - off(type: K, callback: (...args: EM[K]) => void) { this.events.off(type, callback); return this } + off(type: K, callback: (...args: EM[K]) => any) { this.events.off(type, callback); return this } //@ts-expect-error - once(type: K, callback: (...args: EM[K]) => void) { this.events.once(type, callback); return this } + once(type: K, callback: (...args: EM[K]) => any) { this.events.once(type, callback); return this } } export class $EventManager { private eventMap = new Map(); @@ -25,17 +25,17 @@ export class $EventManager { return this } //@ts-expect-error - on(type: K, callback: (...args: EM[K]) => void) { + on(type: K, callback: (...args: EM[K]) => any) { this.get(type).add(callback); return this } //@ts-expect-error - off(type: K, callback: (...args: EM[K]) => void) { + off(type: K, callback: (...args: EM[K]) => any) { this.get(type).delete(callback); return this } //@ts-expect-error - once(type: K, callback: (...args: EM[K]) => void) { + once(type: K, callback: (...args: EM[K]) => any) { //@ts-expect-error const onceFn = (...args: EM[K]) => { this.get(type).delete(onceFn); diff --git a/lib/$NodeManager.ts b/lib/$NodeManager.ts index b6c211e..20a257f 100644 --- a/lib/$NodeManager.ts +++ b/lib/$NodeManager.ts @@ -1,6 +1,6 @@ -import { $Container } from "./$Container"; -import { $Node } from "./$Node"; -import { $Text } from "./$Text"; +import { $Container } from "./node/$Container"; +import { $Node } from "./node/$Node"; +import { $Text } from "./node/$Text"; export class $NodeManager { #container: $Container; @@ -12,10 +12,8 @@ export class $NodeManager { add(element: $Node | string) { if (typeof element === 'string') { const text = new $Text(element); - (text as Mutable<$Text>).parent = this.#container; this.elementList.add(text); } else { - (element as Mutable<$Node>).parent = this.#container; this.elementList.add(element); } } @@ -23,7 +21,6 @@ export class $NodeManager { remove(element: $Node) { if (!this.elementList.has(element)) return this; this.elementList.delete(element); - (element as Mutable<$Node>).parent = undefined; return this; } @@ -39,8 +36,6 @@ export class $NodeManager { }) this.elementList.clear(); array.forEach(node => this.elementList.add(node)); - (target as Mutable<$Node>).parent = undefined; - (replace as Mutable<$Node>).parent = this.#container; return this; } diff --git a/lib/$OptGroup.ts b/lib/$OptGroup.ts deleted file mode 100644 index 342d4a6..0000000 --- a/lib/$OptGroup.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { $Container, $ContainerOptions } from "./$Container"; -import { $State } from "./$State"; - -export interface $OptGroupOptions extends $ContainerOptions {} -export class $OptGroup extends $Container { - constructor(options?: $OptGroupOptions) { - super('optgroup', options); - } - - disabled(): boolean; - disabled(disabled: boolean | $State): this; - disabled(disabled?: boolean | $State) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))} - - label(): string; - label(label: string | $State): this; - label(label?: string | $State) { return $.fluent(this, arguments, () => this.dom.label, () => $.set(this.dom, 'label', label))} -} \ No newline at end of file diff --git a/lib/$Option.ts b/lib/$Option.ts deleted file mode 100644 index e8093e4..0000000 --- a/lib/$Option.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { $Container, $ContainerOptions } from "./$Container"; -import { $State } from "./$State"; - -export interface $OptionOptions extends $ContainerOptions {} -export class $Option extends $Container { - constructor(options?: $OptionOptions) { - super('option', options); - } - - defaultSelected(): boolean; - defaultSelected(defaultSelected: boolean | $State): this; - defaultSelected(defaultSelected?: boolean | $State) { return $.fluent(this, arguments, () => this.dom.defaultSelected, () => $.set(this.dom, 'defaultSelected', defaultSelected))} - - disabled(): boolean; - disabled(disabled: boolean | $State): this; - disabled(disabled?: boolean | $State) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))} - - label(): string; - label(label: string | $State): this; - label(label?: string | $State) { return $.fluent(this, arguments, () => this.dom.label, () => $.set(this.dom, 'label', label))} - - selected(): boolean; - selected(selected: boolean | $State): this; - selected(selected?: boolean | $State) { return $.fluent(this, arguments, () => this.dom.selected, () => $.set(this.dom, 'selected', selected))} - - text(): string; - text(text: string | $State): this; - text(text?: string | $State) { return $.fluent(this, arguments, () => this.dom.text, () => $.set(this.dom, 'text', text))} - - value(): string; - value(value: string | $State): this; - value(value?: string | $State) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value))} - - get form() { return this.dom.form ? $(this.dom.form) : null } - get index() { return this.dom.index } - -} \ No newline at end of file diff --git a/lib/$State.ts b/lib/$State.ts index 10b5d9e..c622b70 100644 --- a/lib/$State.ts +++ b/lib/$State.ts @@ -1,22 +1,33 @@ -import { $Node } from "./$Node"; +import { $Node } from "./node/$Node"; +export interface $StateOption { + format: (value: T) => string; +} export class $State { readonly value: T; readonly attributes = new Map<$Node, Set>(); - constructor(value: T) { + options: Partial<$StateOption> = {} + constructor(value: T, options?: $StateOption) { this.value = value; + if (options) this.options = options; } set(value: T) { (this as Mutable<$State>).value = value; for (const [node, attrList] of this.attributes.entries()) { for (const attr of attrList) { //@ts-expect-error - if (node[attr] instanceof Function) node[attr](value) + if (node[attr] instanceof Function) { + //@ts-expect-error + if (this.options.format) node[attr](this.options.format(value)) + //@ts-expect-error + else node[attr](value) + } } } } toString(): string { + if (this.options.format) return this.options.format(this.value); return `${this.value}` } @@ -25,4 +36,6 @@ export class $State { if (attrList) attrList.add(attrName); else this.attributes.set($node, new Set().add(attrName)) } -}; \ No newline at end of file +}; + +export type $StateArgument = T | $State; \ No newline at end of file diff --git a/lib/$Util.ts b/lib/$Util.ts index 7f5d166..0d06872 100644 --- a/lib/$Util.ts +++ b/lib/$Util.ts @@ -1,4 +1,8 @@ import { $State } from "./$State"; +import { $Container } from "./node/$Container"; +import { $Node } from "./node/$Node"; +import { $SVGElement } from "./node/$SVGElement"; +import { $Text } from "./node/$Text"; export namespace $Util { export function fluent(instance: T, args: IArguments, value: () => V, action: (...args: any[]) => void) { @@ -7,13 +11,13 @@ export namespace $Util { return instance; } - export function multableResolve(multable: OrArray) { + export function orArrayResolve(multable: OrArray) { if (multable instanceof Array) return multable; else return [multable]; } export function mixin(target: any, constructors: OrArray) { - multableResolve(constructors).forEach(constructor => { + orArrayResolve(constructors).forEach(constructor => { Object.getOwnPropertyNames(constructor.prototype).forEach(name => { if (name === 'constructor') return; Object.defineProperty( @@ -33,4 +37,26 @@ export namespace $Util { export function state(value: T) { return new $State(value) } + + export function from(element: HTMLElement | Text | Node): $Node { + if (element.$) return element.$; + if (element.nodeName.toLowerCase() === 'body') return new $Container('body', {dom: element as HTMLBodyElement}); + else if (element instanceof HTMLElement) { + const instance = $.TagNameElementMap[element.tagName.toLowerCase() as keyof typeof $.TagNameElementMap]; + const $node = instance === $Container ? new instance(element.tagName, {dom: element}) : new instance({dom: element} as any); + if ($node instanceof $Container) for (const childnode of Array.from($node.dom.childNodes)) { + $node.children.add($(childnode)); + } + return $node as $Node; + } + else if (element instanceof Text) { + const node = new $Text(element.textContent ?? '') as Mutable<$Node>; + node.dom = element; + return node as $Node; + } + else if (element instanceof SVGElement) { + if (element.tagName.toLowerCase() === 'svg') {return new $SVGElement('svg', {dom: element}) }; + } + throw `$NODE.FROM: NOT SUPPORT TARGET ELEMENT TYPE (${element.nodeName})` + } } \ No newline at end of file diff --git a/lib/$Anchor.ts b/lib/node/$Anchor.ts similarity index 83% rename from lib/$Anchor.ts rename to lib/node/$Anchor.ts index 3a42aae..87bf5fe 100644 --- a/lib/$Anchor.ts +++ b/lib/node/$Anchor.ts @@ -8,7 +8,7 @@ export class $Anchor extends $Container { // Link Handler event this.dom.addEventListener('click', e => { if ($.anchorPreventDefault) e.preventDefault(); - if ($.anchorHandler && !!this.href()) $.anchorHandler(this.href(), e) + if ($.anchorHandler && !!this.href()) $.anchorHandler(this, e) }) } /**Set URL of anchor element. */ @@ -16,9 +16,9 @@ export class $Anchor extends $Container { href(url: string | undefined): this; href(url?: string | undefined) { return $.fluent(this, arguments, () => this.dom.href, () => {if (url) this.dom.href = url}) } /**Link open with this window, new tab or other */ - target(): string; + target(): $AnchorTarget | undefined; target(target: $AnchorTarget | undefined): this; - target(target?: $AnchorTarget | undefined) { return $.fluent(this, arguments, () => this.dom.target, () => {if (target) this.dom.target = target}) } + target(target?: $AnchorTarget | undefined) { return $.fluent(this, arguments, () => (this.dom.target ?? undefined) as $AnchorTarget | undefined, () => {if (target) this.dom.target = target}) } } export type $AnchorTarget = '_blank' | '_self' | '_parent' | '_top'; \ No newline at end of file diff --git a/lib/node/$AsyncNode.ts b/lib/node/$AsyncNode.ts new file mode 100644 index 0000000..5f233ec --- /dev/null +++ b/lib/node/$AsyncNode.ts @@ -0,0 +1,22 @@ +import { $Node } from "./$Node"; + +export class $AsyncNode extends $Node { + dom: Node = document.createElement('async'); + loaded: boolean = false; + constructor($node?: Promise) { + super() + this.dom.$ = this; + if ($node) $node.then($node => this._loaded($node)); + } + + await($node: Promise) { + $node.then($node => this._loaded($node)); + return this as $AsyncNode + } + + protected _loaded($node: $Node) { + this.loaded = true; + this.replace($node) + this.dom.dispatchEvent(new Event('load')) + } +} \ No newline at end of file diff --git a/lib/$Button.ts b/lib/node/$Button.ts similarity index 86% rename from lib/$Button.ts rename to lib/node/$Button.ts index d575d6f..8accab0 100644 --- a/lib/$Button.ts +++ b/lib/node/$Button.ts @@ -1,5 +1,5 @@ import { $Container, $ContainerOptions } from "./$Container"; -import { $State } from "./$State"; +import { $State, $StateArgument } from "../$State"; export interface $ButtonOptions extends $ContainerOptions {} export class $Button extends $Container { constructor(options?: $ButtonOptions) { @@ -7,8 +7,8 @@ export class $Button extends $Container { } disabled(): boolean; - disabled(disabled: boolean | $State): this; - disabled(disabled?: boolean | $State) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))} + disabled(disabled: $StateArgument): this; + disabled(disabled?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))} type(): ButtonType; type(type: ButtonType): this; diff --git a/lib/$Canvas.ts b/lib/node/$Canvas.ts similarity index 100% rename from lib/$Canvas.ts rename to lib/node/$Canvas.ts diff --git a/lib/$Container.ts b/lib/node/$Container.ts similarity index 88% rename from lib/$Container.ts rename to lib/node/$Container.ts index 9d6177f..06b2b66 100644 --- a/lib/$Container.ts +++ b/lib/node/$Container.ts @@ -1,12 +1,12 @@ import { $Element, $ElementOptions } from "./$Element"; -import { $NodeManager } from "./$NodeManager"; +import { $NodeManager } from "../$NodeManager"; import { $Node } from "./$Node"; -import { $State } from "./$State"; +import { $State } from "../$State"; import { $Text } from "./$Text"; +import { $HTMLElement, $HTMLElementOptions } from "./$HTMLElement"; -export interface $ContainerOptions extends $ElementOptions {} - -export class $Container extends $Element { +export interface $ContainerOptions extends $HTMLElementOptions {} +export class $Container extends $HTMLElement { readonly children: $NodeManager = new $NodeManager(this); constructor(tagname: string, options?: $ContainerOptions) { super(tagname, options) diff --git a/lib/$Dialog.ts b/lib/node/$Dialog.ts similarity index 100% rename from lib/$Dialog.ts rename to lib/node/$Dialog.ts diff --git a/lib/$Element.ts b/lib/node/$Element.ts similarity index 54% rename from lib/$Element.ts rename to lib/node/$Element.ts index 2de8f0b..3537a38 100644 --- a/lib/$Element.ts +++ b/lib/node/$Element.ts @@ -3,18 +3,26 @@ import { $Node } from "./$Node"; export interface $ElementOptions { id?: string; class?: string[]; + dom?: HTMLElement | SVGElement; } -export class $Element extends $Node { +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; + this.dom = this.createDom(tagname, options) as H; this.dom.$ = this; this.setOptions(options); } + private createDom(tagname: string, options?: $ElementOptions) { + if (options?.dom) return options.dom; + if (tagname === 'svg') return document.createElementNS("http://www.w3.org/2000/svg", "svg"); + return document.createElement(tagname); + + } + setOptions(options: $ElementOptions | undefined) { this.id(options?.id) if (options && options.class) this.class(...options.class) @@ -24,7 +32,7 @@ export class $Element extends $Node { /**Replace id of element. @example Element.id('customId');*/ id(): string; id(name: string | undefined): this; - id(name?: string | undefined): this | string {return $.fluent(this, arguments, () => this.dom.id, () => $.set(this.dom, 'id', name))} + id(name?: string | undefined): this | string {return $.fluent(this, arguments, () => this.dom.id, () => $.set(this.dom, 'id', name as any))} /**Replace list of class name to element. @example Element.class('name1', 'name2') */ class(): DOMTokenList; @@ -46,74 +54,32 @@ export class $Element extends $Node { css(style: Partial): this; css(style?: Partial) { return $.fluent(this, arguments, () => this.dom.style, () => {Object.assign(this.dom.style, style)})} + /** + * Get or set attribute from this element. + * @param qualifiedName Attribute name + * @param value Attribute value. Set `null` will remove attribute. + */ 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 { + attribute(qualifiedName: string | undefined, value?: string | number | boolean | null): this; + attribute(qualifiedName: string | undefined, value?: string | number | boolean | null): 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}`); + if (!qualifiedName) return this; + if (value === null) this.dom.removeAttribute(qualifiedName); + else if (value !== undefined) 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))} - - dir(): TextDirection; - dir(dir?: TextDirection): this; - dir(dir?: TextDirection) { return $.fluent(this, arguments, () => this.dom.dir, () => $.set(this.dom, 'dir', dir))} - - innerText(): string; - innerText(text?: string): this; - innerText(text?: string) { return $.fluent(this, arguments, () => this.dom.innerText, () => $.set(this.dom, 'innerText', text))} - - title(): string; - title(title?: string): this; - title(title?: string) { return $.fluent(this, arguments, () => this.dom.title, () => $.set(this.dom, 'title', title))} - - translate(): boolean; - translate(translate?: boolean): this; - translate(translate?: boolean) { return $.fluent(this, arguments, () => this.dom.translate, () => $.set(this.dom, 'translate', translate))} - - popover(): string | null; - popover(popover?: string | null): this; - popover(popover?: string | null) { return $.fluent(this, arguments, () => this.dom.popover, () => $.set(this.dom, 'popover', popover))} - - spellcheck(): boolean; - spellcheck(spellcheck?: boolean): this; - spellcheck(spellcheck?: boolean) { return $.fluent(this, arguments, () => this.dom.spellcheck, () => $.set(this.dom, 'spellcheck', spellcheck))} - - inert(): boolean; - inert(inert?: boolean): this; - inert(inert?: boolean) { return $.fluent(this, arguments, () => this.dom.inert, () => $.set(this.dom, 'inert', inert))} - - lang(): string; - lang(lang?: string): this; - lang(lang?: string) { return $.fluent(this, arguments, () => this.dom.lang, () => $.set(this.dom, 'lang', lang))} - - draggable(): boolean; - draggable(draggable?: boolean): this; - draggable(draggable?: boolean) { return $.fluent(this, arguments, () => this.dom.draggable, () => $.set(this.dom, 'draggable', draggable))} - - hidden(): boolean; - hidden(hidden?: boolean): this; - hidden(hidden?: boolean) { return $.fluent(this, arguments, () => this.dom.hidden, () => $.set(this.dom, 'hidden', hidden))} - tabIndex(): number; tabIndex(tabIndex: number): this; - tabIndex(tabIndex?: number) { return $.fluent(this, arguments, () => this.dom.tabIndex, () => $.set(this.dom, 'tabIndex', tabIndex))} + tabIndex(tabIndex?: number) { return $.fluent(this, arguments, () => this.dom.tabIndex, () => $.set(this.dom, 'tabIndex', tabIndex as any))} - click() { this.dom.click(); return this; } - attachInternals() { return this.dom.attachInternals(); } - hidePopover() { this.dom.hidePopover(); return this; } - showPopover() { this.dom.showPopover(); return this; } - togglePopover() { this.dom.togglePopover(); return this; } focus() { this.dom.focus(); return this; } blur() { this.dom.blur(); return this; } @@ -125,11 +91,5 @@ export class $Element extends $Node { getAnimations(options?: GetAnimationsOptions) { return this.dom.getAnimations(options) } - get accessKeyLabel() { return this.dom.accessKeyLabel } - get offsetHeight() { return this.dom.offsetHeight } - get offsetLeft() { return this.dom.offsetLeft } - get offsetParent() { return $(this.dom.offsetParent) } - get offsetTop() { return this.dom.offsetTop } - get offsetWidth() { return this.dom.offsetWidth } get dataset() { return this.dom.dataset } } \ No newline at end of file diff --git a/lib/$Form.ts b/lib/node/$Form.ts similarity index 97% rename from lib/$Form.ts rename to lib/node/$Form.ts index d39881d..208b8db 100644 --- a/lib/$Form.ts +++ b/lib/node/$Form.ts @@ -1,6 +1,4 @@ import { $Container, $ContainerOptions } from "./$Container"; -import { $State } from "./$State"; -import { $Util } from "./$Util"; export interface $FormOptions extends $ContainerOptions {} export class $Form extends $Container { constructor(options?: $FormOptions) { diff --git a/lib/node/$HTMLElement.ts b/lib/node/$HTMLElement.ts new file mode 100644 index 0000000..cac0bc9 --- /dev/null +++ b/lib/node/$HTMLElement.ts @@ -0,0 +1,65 @@ +import { $Element, $ElementOptions } from "./$Element"; + +export interface $HTMLElementOptions extends $ElementOptions {} +export class $HTMLElement extends $Element { + constructor(tagname: string, options?: $HTMLElementOptions) { + super(tagname, options) + } + + autocapitalize(): Autocapitalize; + autocapitalize(autocapitalize?: Autocapitalize): this; + autocapitalize(autocapitalize?: Autocapitalize) { return $.fluent(this, arguments, () => this.dom.autocapitalize, () => $.set(this.dom, 'autocapitalize', autocapitalize as any))} + + innerText(): string; + innerText(text?: string): this; + innerText(text?: string) { return $.fluent(this, arguments, () => this.dom.innerText, () => $.set(this.dom, 'innerText', text as any))} + + title(): string; + title(title?: string): this; + title(title?: string) { return $.fluent(this, arguments, () => this.dom.title, () => $.set(this.dom, 'title', title as any))} + + dir(): TextDirection; + dir(dir?: TextDirection): this; + dir(dir?: TextDirection) { return $.fluent(this, arguments, () => this.dom.dir, () => $.set(this.dom, 'dir', dir as any))} + + translate(): boolean; + translate(translate?: boolean): this; + translate(translate?: boolean) { return $.fluent(this, arguments, () => this.dom.translate, () => $.set(this.dom, 'translate', translate as any))} + + popover(): string | null; + popover(popover?: string | null): this; + popover(popover?: string | null) { return $.fluent(this, arguments, () => this.dom.popover, () => $.set(this.dom, 'popover', popover as any))} + + spellcheck(): boolean; + spellcheck(spellcheck?: boolean): this; + spellcheck(spellcheck?: boolean) { return $.fluent(this, arguments, () => this.dom.spellcheck, () => $.set(this.dom, 'spellcheck', spellcheck as any))} + + inert(): boolean; + inert(inert?: boolean): this; + inert(inert?: boolean) { return $.fluent(this, arguments, () => this.dom.inert, () => $.set(this.dom, 'inert', inert as any))} + + lang(): string; + lang(lang?: string): this; + lang(lang?: string) { return $.fluent(this, arguments, () => this.dom.lang, () => $.set(this.dom, 'lang', lang as any))} + + draggable(): boolean; + draggable(draggable?: boolean): this; + draggable(draggable?: boolean) { return $.fluent(this, arguments, () => this.dom.draggable, () => $.set(this.dom, 'draggable', draggable as any))} + + hidden(): boolean; + hidden(hidden?: boolean): this; + hidden(hidden?: boolean) { return $.fluent(this, arguments, () => this.dom.hidden, () => $.set(this.dom, 'hidden', hidden as any))} + + click() { this.dom.click(); return this; } + attachInternals() { return this.dom.attachInternals(); } + hidePopover() { this.dom.hidePopover(); return this; } + showPopover() { this.dom.showPopover(); return this; } + togglePopover() { this.dom.togglePopover(); return this; } + + get accessKeyLabel() { return this.dom.accessKeyLabel } + get offsetHeight() { return this.dom.offsetHeight } + get offsetLeft() { return this.dom.offsetLeft } + get offsetParent() { return $(this.dom.offsetParent) } + get offsetTop() { return this.dom.offsetTop } + get offsetWidth() { return this.dom.offsetWidth } +} \ No newline at end of file diff --git a/lib/$Image.ts b/lib/node/$Image.ts similarity index 89% rename from lib/$Image.ts rename to lib/node/$Image.ts index 96b5c08..096f049 100644 --- a/lib/$Image.ts +++ b/lib/node/$Image.ts @@ -1,11 +1,19 @@ -import { $Element, $ElementOptions } from "./$Element"; -import { $State } from "./$State"; -export interface $ImageOptions extends $ElementOptions {} -export class $Image extends $Element { +import { $HTMLElement, $HTMLElementOptions } from "./$HTMLElement"; +import { $State } from "../$State"; +export interface $ImageOptions extends $HTMLElementOptions {} +export class $Image extends $HTMLElement { constructor(options?: $ImageOptions) { super('img', options); } + async load(src: string): Promise<$Image> { + return new Promise(resolve => { + const $img = this.once('load', () => { + resolve($img) + }).src(src) + }) + } + /**HTMLImageElement base property */ alt(): string; alt(alt: string): this; diff --git a/lib/$Input.ts b/lib/node/$Input.ts similarity index 95% rename from lib/$Input.ts rename to lib/node/$Input.ts index 10a7502..9f22844 100644 --- a/lib/$Input.ts +++ b/lib/node/$Input.ts @@ -1,5 +1,5 @@ import { $Element, $ElementOptions } from "./$Element"; -import { $State } from "./$State"; +import { $State, $StateArgument } from "../$State"; export interface $InputOptions extends $ElementOptions {} export class $Input extends $Element { @@ -172,12 +172,12 @@ export class $Input extends $Element { formTarget(target?: string) { return $.fluent(this, arguments, () => this.dom.formTarget, () => $.set(this.dom, 'formTarget', target))} name(): string; - name(name?: string | $State): this; - name(name?: string | $State) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))} + name(name?: $StateArgument | undefined): this; + name(name?: $StateArgument | undefined) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))} value(): string; - value(value?: string | $State): this; - value(value?: string | $State) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value))} + value(value: $StateArgument | undefined): this; + value(value?: $StateArgument | undefined) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value))} get form() { return this.dom.form ? $(this.dom.form) : null } get labels() { return Array.from(this.dom.labels ?? []).map(label => $(label)) } diff --git a/lib/$Label.ts b/lib/node/$Label.ts similarity index 100% rename from lib/$Label.ts rename to lib/node/$Label.ts diff --git a/lib/$Node.ts b/lib/node/$Node.ts similarity index 69% rename from lib/$Node.ts rename to lib/node/$Node.ts index 227fb96..59d0194 100644 --- a/lib/$Node.ts +++ b/lib/node/$Node.ts @@ -1,13 +1,12 @@ -import { $Element, $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; private domEvents: {[key: string]: Map} = {}; - on(type: K, callback: (event: HTMLElementEventMap[K], $node: this) => void, options?: AddEventListenerOptions | boolean) { + on(type: K, callback: (event: HTMLElementEventMap[K], $node: this) => any, options?: AddEventListenerOptions | boolean) { if (!this.domEvents[type]) this.domEvents[type] = new Map() const middleCallback = (e: Event) => callback(e as HTMLElementEventMap[K], this); this.domEvents[type].set(callback, middleCallback) @@ -15,13 +14,13 @@ export abstract class $Node { return this; } - off(type: K, callback: (event: HTMLElementEventMap[K], $node: this) => void, options?: AddEventListenerOptions | boolean) { + off(type: K, callback: (event: HTMLElementEventMap[K], $node: this) => any, options?: AddEventListenerOptions | boolean) { const middleCallback = this.domEvents[type]?.get(callback); if (middleCallback) this.dom.removeEventListener(type, middleCallback as EventListener, options) return this; } - once(type: K, callback: (event: HTMLElementEventMap[K], $node: this) => void, options?: AddEventListenerOptions | boolean) { + once(type: K, callback: (event: HTMLElementEventMap[K], $node: this) => any, options?: AddEventListenerOptions | boolean) { const onceFn = (event: Event) => { this.dom.removeEventListener(type, onceFn, options) callback(event as HTMLElementEventMap[K], this); @@ -52,7 +51,7 @@ export abstract class $Node { return this; } - contains(target: $Node | EventTarget | Node | null) { + contains(target: $Node | EventTarget | Node | null): boolean { if (!target) return false; if (target instanceof $Node) return this.dom.contains(target.dom); else if (target instanceof EventTarget) return this.dom.contains($(target).dom) @@ -61,25 +60,12 @@ export abstract class $Node { self(callback: ($node: this) => void) { callback(this); return this; } inDOM() { return document.contains(this.dom); } - isElement() { + isElement(): $Element | undefined { if (this instanceof $Element) return this; else return undefined; } - static from(element: HTMLElement | Text): $Node { - if (element.$) return element.$; - else if (element instanceof HTMLElement) { - const node = $(element.tagName) as Mutable<$Node>; - node.dom = element; - if (element.parentElement) node.parent = $(element.parentElement) as $Container; - return node as $Node; - } - else if (element instanceof Text) { - const node = new $Text(element.textContent ?? '') as Mutable<$Node>; - node.dom = element; - if (element.parentElement) node.parent = $(element.parentElement) as $Container; - return node as $Node; - } - throw '$NODE.FROM: NOT SUPPORT TARGET ELEMENT TYPE' + get parent() { + return this.dom.parentElement?.$ as $Container | undefined; } } \ No newline at end of file diff --git a/lib/node/$OptGroup.ts b/lib/node/$OptGroup.ts new file mode 100644 index 0000000..a99034a --- /dev/null +++ b/lib/node/$OptGroup.ts @@ -0,0 +1,17 @@ +import { $Container, $ContainerOptions } from "./$Container"; +import { $State, $StateArgument } from "../$State"; + +export interface $OptGroupOptions extends $ContainerOptions {} +export class $OptGroup extends $Container { + constructor(options?: $OptGroupOptions) { + super('optgroup', options); + } + + disabled(): boolean; + disabled(disabled: $StateArgument | undefined): this; + disabled(disabled?: $StateArgument | undefined) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))} + + label(): string; + label(label: $StateArgument | undefined): this; + label(label?: $StateArgument | undefined) { return $.fluent(this, arguments, () => this.dom.label, () => $.set(this.dom, 'label', label))} +} \ No newline at end of file diff --git a/lib/node/$Option.ts b/lib/node/$Option.ts new file mode 100644 index 0000000..eabf21b --- /dev/null +++ b/lib/node/$Option.ts @@ -0,0 +1,37 @@ +import { $Container, $ContainerOptions } from "./$Container"; +import { $State, $StateArgument } from "../$State"; + +export interface $OptionOptions extends $ContainerOptions {} +export class $Option extends $Container { + constructor(options?: $OptionOptions) { + super('option', options); + } + + defaultSelected(): boolean; + defaultSelected(defaultSelected: $StateArgument | undefined): this; + defaultSelected(defaultSelected?: $StateArgument | undefined) { return $.fluent(this, arguments, () => this.dom.defaultSelected, () => $.set(this.dom, 'defaultSelected', defaultSelected))} + + disabled(): boolean; + disabled(disabled: $StateArgument | undefined): this; + disabled(disabled?: $StateArgument | undefined) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))} + + label(): string; + label(label: $StateArgument | undefined): this; + label(label?: $StateArgument | undefined) { return $.fluent(this, arguments, () => this.dom.label, () => $.set(this.dom, 'label', label))} + + selected(): boolean; + selected(selected: $StateArgument | undefined): this; + selected(selected?: $StateArgument | undefined) { return $.fluent(this, arguments, () => this.dom.selected, () => $.set(this.dom, 'selected', selected))} + + text(): string; + text(text: $StateArgument | undefined): this; + text(text?: $StateArgument | undefined) { return $.fluent(this, arguments, () => this.dom.text, () => $.set(this.dom, 'text', text))} + + value(): string; + value(value: $StateArgument | undefined): this; + value(value?: $StateArgument | undefined) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value))} + + get form() { return this.dom.form ? $(this.dom.form) : null } + get index() { return this.dom.index } + +} \ No newline at end of file diff --git a/lib/node/$SVGElement.ts b/lib/node/$SVGElement.ts new file mode 100644 index 0000000..4128b46 --- /dev/null +++ b/lib/node/$SVGElement.ts @@ -0,0 +1,8 @@ +import { $Element, $ElementOptions } from "./$Element" + +export interface $SVGOptions extends $ElementOptions {} +export class $SVGElement extends $Element { + constructor(tagname: string, options?: $SVGOptions) { + super(tagname, options); + } +} \ No newline at end of file diff --git a/lib/$Select.ts b/lib/node/$Select.ts similarity index 64% rename from lib/$Select.ts rename to lib/node/$Select.ts index 6d917e5..b72dbac 100644 --- a/lib/$Select.ts +++ b/lib/node/$Select.ts @@ -1,11 +1,11 @@ import { $Container, $ContainerOptions } from "./$Container"; import { $OptGroup } from "./$OptGroup"; import { $Option } from "./$Option"; -import { $State } from "./$State"; +import { $State, $StateArgument } from "../$State"; export interface $SelectOptions extends $ContainerOptions {} export class $Select extends $Container { - constructor() { + constructor(options?: $SelectOptions) { super('select') } @@ -18,12 +18,12 @@ export class $Select extends $Container { namedItem(name: string) { return $(this.dom.namedItem(name)) } disabled(): boolean; - disabled(disabled: boolean | $State): this; - disabled(disabled?: boolean | $State) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))} + disabled(disabled: $StateArgument | undefined): this; + disabled(disabled?: $StateArgument | undefined) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))} multiple(): boolean; - multiple(multiple: boolean | $State): this; - multiple(multiple?: boolean | $State) { return $.fluent(this, arguments, () => this.dom.multiple, () => $.set(this.dom, 'multiple', multiple))} + multiple(multiple: $StateArgument | undefined): this; + multiple(multiple?: $StateArgument | undefined) { return $.fluent(this, arguments, () => this.dom.multiple, () => $.set(this.dom, 'multiple', multiple))} required(): boolean; required(required: boolean): this; @@ -40,12 +40,12 @@ export class $Select extends $Container { get selectedOptions() { return Array.from(this.dom.selectedOptions).map($option => $($option)) } name(): string; - name(name?: string | $State): this; - name(name?: string | $State) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))} + name(name?: $StateArgument | undefined): this; + name(name?: $StateArgument | undefined) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))} value(): string; - value(value?: string | $State): this; - value(value?: string | $State) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value))} + value(value?: $StateArgument | undefined): this; + value(value?: $StateArgument | undefined) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value))} get form() { return this.dom.form ? $(this.dom.form) : null } get labels() { return Array.from(this.dom.labels ?? []).map(label => $(label)) } diff --git a/lib/$Text.ts b/lib/node/$Text.ts similarity index 100% rename from lib/$Text.ts rename to lib/node/$Text.ts diff --git a/lib/$Textarea.ts b/lib/node/$Textarea.ts similarity index 87% rename from lib/$Textarea.ts rename to lib/node/$Textarea.ts index 7378e61..a195dce 100644 --- a/lib/$Textarea.ts +++ b/lib/node/$Textarea.ts @@ -1,5 +1,5 @@ import { $Container, $ContainerOptions } from "./$Container"; -import { $State } from "./$State"; +import { $StateArgument } from "../$State"; export interface $TextareaOptions extends $ContainerOptions {} export class $Textarea extends $Container { @@ -12,16 +12,16 @@ export class $Textarea extends $Container { cols(cols?: number) { return $.fluent(this, arguments, () => this.dom.cols, () => $.set(this.dom, 'cols', cols))} name(): string; - name(name?: string | $State): this; - name(name?: string | $State) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))} + name(name?: $StateArgument | undefined): this; + name(name?: $StateArgument | undefined) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))} wrap(): string; - wrap(wrap?: string | $State): this; - wrap(wrap?: string | $State) { return $.fluent(this, arguments, () => this.dom.wrap, () => $.set(this.dom, 'wrap', wrap))} + wrap(wrap?: $StateArgument | undefined): this; + wrap(wrap?: $StateArgument | undefined) { return $.fluent(this, arguments, () => this.dom.wrap, () => $.set(this.dom, 'wrap', wrap))} value(): string; - value(value?: string | $State): this; - value(value?: string | $State) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value))} + value(value?: $StateArgument | undefined): this; + value(value?: $StateArgument | undefined) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value))} maxLength(): number; maxLength(maxLength: number): this; diff --git a/lib/$View.ts b/lib/node/$View.ts similarity index 87% rename from lib/$View.ts rename to lib/node/$View.ts index c5e64ca..75ae1d6 100644 --- a/lib/$View.ts +++ b/lib/node/$View.ts @@ -1,5 +1,5 @@ import { $Container, $ContainerOptions } from "./$Container"; -import { $EventManager } from "./$EventManager"; +import { $EventManager } from "../$EventManager"; import { $Node } from "./$Node"; export interface $ViewOptions extends $ContainerOptions {} @@ -28,7 +28,7 @@ export class $View extends $Container { switchView(id: string) { const target_content = this.view_cache.get(id); - if (target_content === undefined) throw '$View.switch(): target content is undefined'; + if (target_content === undefined) return this; this.content(target_content); this.content_id = id; this.event.fire('switch', id); diff --git a/lib/Router/README.md b/lib/router/README.md similarity index 100% rename from lib/Router/README.md rename to lib/router/README.md diff --git a/lib/Router/Route.ts b/lib/router/Route.ts similarity index 90% rename from lib/Router/Route.ts rename to lib/router/Route.ts index fc82115..5a08e4d 100644 --- a/lib/Router/Route.ts +++ b/lib/router/Route.ts @@ -1,5 +1,6 @@ import { $EventManager, $EventMethod } from "../$EventManager"; -import { $Node } from "../$Node"; +import { $Node } from "../node/$Node"; +import { $Util } from "../$Util"; export class Route { path: string | PathResolverFn; builder: (req: RouteRequest) => RouteContent; @@ -32,7 +33,7 @@ export class RouteRecord { this.id = id; } } -$.mixin(RouteRecord, $EventMethod) +$Util.mixin(RouteRecord, $EventMethod) export interface RouteRecordEventMap { 'open': [{path: string, record: RouteRecord}]; 'load': [{path: string, record: RouteRecord}]; diff --git a/lib/Router/Router.ts b/lib/router/Router.ts similarity index 61% rename from lib/Router/Router.ts rename to lib/router/Router.ts index 1412aae..043cf1e 100644 --- a/lib/Router/Router.ts +++ b/lib/router/Router.ts @@ -1,15 +1,18 @@ import { $EventManager, $EventMethod } from "../$EventManager"; -import { $Text } from "../$Text"; -import { $View } from "../$View"; +import { $Text } from "../node/$Text"; +import { $Util } from "../$Util"; +import { $View } from "../node/$View"; import { PathResolverFn, Route, RouteRecord } from "./Route"; export interface Router extends $EventMethod {}; export class Router { routeMap = new Map>(); recordMap = new Map(); $view: $View; - index: number = 0; - events = new $EventManager().register('pathchange', 'notfound', 'load'); + static index: number = 0; + static events = new $EventManager().register('pathchange', 'notfound', 'load'); + events = new $EventManager().register('notfound', 'load'); basePath: string; + static currentPath: URL = new URL(location.href); constructor(basePath: string, view?: $View) { this.basePath = basePath; this.$view = view ?? new $View(); @@ -25,39 +28,54 @@ export class Router { /**Start listen to the path change */ listen() { if (!history.state || 'index' in history.state === false) { - const routeData: RouteData = {index: this.index, data: {}} + const routeData: RouteData = {index: Router.index, data: {}} history.replaceState(routeData, '') } else { - this.index = history.state.index + Router.index = history.state.index } addEventListener('popstate', this.popstate) $.routers.add(this); this.resolvePath(); - this.events.fire('pathchange', {path: location.href, navigation: 'Forward'}); + Router.events.fire('pathchange', {prevURL: undefined, nextURL: Router.currentPath, navigation: 'Forward'}); return this; } - /**Open path */ - open(path: string | undefined) { - if (path === undefined) return; - if (path === location.pathname) return this; + /**Open URL */ + static open(url: string | URL | undefined) { + if (url === undefined) return this; + url = new URL(url); + if (url.origin !== location.origin) return this; + if (url.href === location.href) return this; + const prevPath = Router.currentPath; this.index += 1; const routeData: RouteData = { index: this.index, data: {} }; - history.pushState(routeData, '', path); + history.pushState(routeData, '', url); + Router.currentPath = new URL(location.href); $.routers.forEach(router => router.resolvePath()) - this.events.fire('pathchange', {path, navigation: 'Forward'}); + Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: 'Forward'}); return this; } /**Back to previous page */ - back() { history.back(); return this } + static back() { + const prevPath = Router.currentPath; + history.back(); + Router.currentPath = new URL(location.href); + Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: 'Back'}); + return this + } - replace(path: string | undefined) { - if (path === undefined) return; - if (path === location.pathname) return this; - history.replaceState({index: this.index}, '', path) - $.routers.forEach(router => router.resolvePath(path)); - this.events.fire('pathchange', {path, navigation: 'Forward'}); + static replace(url: string | URL | undefined) { + if (url === undefined) return this; + if (typeof url === 'string' && !url.startsWith(location.origin)) url = location.origin + url; + url = new URL(url); + if (url.origin !== location.origin) return this; + if (url.href === location.href) return this; + const prevPath = Router.currentPath; + history.replaceState({index: Router.index}, '', url) + Router.currentPath = new URL(location.href); + $.routers.forEach(router => router.resolvePath(url.pathname)); + Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: 'Forward'}); return this; } @@ -69,12 +87,14 @@ export class Router { private popstate = (() => { // Forward - if (history.state.index > this.index) { } + if (history.state.index > Router.index) { } // Back - else if (history.state.index < this.index) { } - this.index = history.state.index; + else if (history.state.index < Router.index) { } + const prevPath = Router.currentPath; + Router.index = history.state.index; this.resolvePath(); - this.events.fire('pathchange', {path: location.pathname, navigation: 'Forward'}); + Router.currentPath = new URL(location.href); + Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: 'Forward'}); }).bind(this) private resolvePath(path = location.pathname) { @@ -151,14 +171,21 @@ export class Router { if (!preventDefaultState) this.$view.clear(); } } + + static on(type: K, callback: (...args: RouterGlobalEventMap[K]) => any) { this.events.on(type, callback); return this } + static off(type: K, callback: (...args: RouterGlobalEventMap[K]) => any) { this.events.off(type, callback); return this } + static once(type: K, callback: (...args: RouterGlobalEventMap[K]) => any) { this.events.once(type, callback); return this } } -$.mixin(Router, $EventMethod); +$Util.mixin(Router, $EventMethod); interface RouterEventMap { - pathchange: [{path: string, navigation: 'Back' | 'Forward'}]; - notfound: [{path: string, preventDefault: () => void}]; + notfound: [{path: string, preventDefault: () => any}]; load: [{path: string}]; } +interface RouterGlobalEventMap { + pathchange: [{prevURL?: URL, nextURL: URL, navigation: 'Back' | 'Forward'}]; +} + type RouteData = { index: number; data: {[key: string]: any}; diff --git a/package.json b/package.json index 66b5c61..0aaa293 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "fluentx", "description": "Fast, fluent, simple web builder", - "version": "0.0.6", + "version": "0.0.7", "type": "module", "module": "index.ts", "author": {