diff --git a/$index.ts b/$index.ts index 7efc19f..d276df6 100644 --- a/$index.ts +++ b/$index.ts @@ -1,14 +1,18 @@ -import { $Node } from "./index"; +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 { $Input } from "./lib/$Input"; import { $Label } from "./lib/$Label"; import { Router } from "./lib/Router/Router"; - +export type $ = typeof $; export function $(resolver: K): $.TagNameTypeMap[K]; export function $(resolver: K): $Container; -export function $(htmlElement: H): $.HTMLElementTo$ElementMap +export function $(htmlElement: H): $.HTMLElementTo$ElementMap; +export function $(element: H): $Element; +export function $(element: H): $Element; export function $(resolver: any) { if (typeof resolver === 'string') { if (resolver in $.TagNameElementMap) { @@ -17,17 +21,22 @@ export function $(resolver: any) { case $Element: return new $Element(resolver); case $Anchor: return new $Anchor(); case $Container: return new $Container(resolver); + case $Input: return new $Input(); + case $Label: return new $Label(); + case $Form: return new $Form(); + case $Button: return new $Button(); } } else return new $Container(resolver); } - if (resolver instanceof HTMLElement) { + if (resolver instanceof HTMLElement || resolver instanceof Text) { if (resolver.$) return resolver.$; - else throw new Error('HTMLElement PROPERTY $ MISSING'); + else return $Node.from(resolver); } + throw '$: NOT SUPPORT TARGET ELEMENT TYPE' } export namespace $ { - export let anchorHandler: null | ((url: URL, e: Event) => void) = null; + export let anchorHandler: null | ((url: string, e: Event) => void) = null; export let anchorPreventDefault: boolean = false; export const routers = new Set; export const TagNameElementMap = { @@ -48,7 +57,10 @@ export namespace $ { 'ul': $Container, 'dl': $Container, 'li': $Container, - 'input': $Input + 'input': $Input, + 'label': $Label, + 'button': $Button, + 'form': $Form } export type TagNameTypeMap = { [key in keyof typeof $.TagNameElementMap]: InstanceType; @@ -60,75 +72,78 @@ export namespace $ { H extends HTMLLabelElement ? $Label : H extends HTMLInputElement ? $Input : H extends HTMLAnchorElement ? $Anchor + : H extends HTMLButtonElement ? $Button + : H extends HTMLFormElement ? $Form : $Element; - + export function fluent(instance: T, args: IArguments, value: () => V, action: (...args: any[]) => void) { if (!args.length) return value(); action(); return instance; } - + export function multableResolve(multable: OrArray) { if (multable instanceof Array) return multable; else return [multable]; } - + export function mixin(target: any, constructors: OrArray) { - $.multableResolve(constructors).forEach(constructor => { - Object.getOwnPropertyNames(constructor.prototype).forEach(name => { + multableResolve(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) + target.prototype, + name, + Object.getOwnPropertyDescriptor(constructor.prototype, name) || Object.create(null) ) - }) + }) }) return target; } - + export function set(object: O, key: K, value: any) { if (value !== undefined) object[key] = value; } -} -$.builder = builder - -/**Build multiple element in once. */ -function builder>(bulder: F, params: [...Parameters][], callback?: BuilderSelfFunction): R[] -function builder>(bulder: [F, ...Parameters], size: number, callback?: BuilderSelfFunction): R[] -function builder>(bulder: [F, ...Parameters], options: ($Node | string | BuilderSelfFunction)[]): R[] -function builder(tagname: K, size: number, callback?: BuilderSelfFunction<$.TagNameTypeMap[K]>): $.TagNameTypeMap[K][] -function builder(tagname: K, callback: BuilderSelfFunction<$.TagNameTypeMap[K]>[]): $.TagNameTypeMap[K][] -function builder(tagname: K, size: number, callback?: BuilderSelfFunction<$.TagNameTypeMap[K]>): $.TagNameTypeMap[K][] -function builder(tagname: K, options: ($Node | string | BuilderSelfFunction<$.TagNameTypeMap[K]>)[]): $.TagNameTypeMap[K][] -function builder(tagname: any, resolver: any, callback?: BuilderSelfFunction) { - if (typeof resolver === 'number') { - return Array(resolver).fill('').map(v => { - const ele = isTuppleBuilder(tagname) ? tagname[0](...tagname.slice(1) as []) : $(tagname); - if (callback) callback(ele); - return ele - }); + + export function state(value: T) { + return new $State(value) } - else { - const eleArray = []; - for (const item of resolver) { - const ele = tagname instanceof Function ? tagname(...item) // tagname is function, item is params - : isTuppleBuilder(tagname) ? tagname[0](...tagname.slice(1) as []) - : $(tagname); - if (item instanceof Function) { item(ele) } - else if (item instanceof $Node || typeof item === 'string') { ele.content(item) } - eleArray.push(ele); + + /**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[] + export function builder>(bulder: [F, ...Parameters], options: ($Node | string | BuilderSelfFunction)[]): R[] + export function builder(tagname: K, size: number, callback?: BuilderSelfFunction<$.TagNameTypeMap[K]>): $.TagNameTypeMap[K][] + export function builder(tagname: K, callback: BuilderSelfFunction<$.TagNameTypeMap[K]>[]): $.TagNameTypeMap[K][] + export function builder(tagname: K, size: number, callback?: BuilderSelfFunction<$.TagNameTypeMap[K]>): $.TagNameTypeMap[K][] + export function builder(tagname: K, options: ($Node | string | BuilderSelfFunction<$.TagNameTypeMap[K]>)[]): $.TagNameTypeMap[K][] + export function builder(tagname: any, resolver: any, callback?: BuilderSelfFunction) { + if (typeof resolver === 'number') { + return Array(resolver).fill('').map(v => { + const ele = isTuppleBuilder(tagname) ? tagname[0](...tagname.slice(1) as []) : $(tagname); + if (callback) callback(ele); + return ele + }); + } + else { + const eleArray = []; + for (const item of resolver) { + const ele = tagname instanceof Function ? tagname(...item) // tagname is function, item is params + : isTuppleBuilder(tagname) ? tagname[0](...tagname.slice(1) as []) + : $(tagname); + if (item instanceof Function) { item(ele) } + else if (item instanceof $Node || typeof item === 'string') { ele.content(item) } + eleArray.push(ele); + } + return eleArray; + } + + function isTuppleBuilder(target: any): target is [BuildNodeFunction, ...any] { + if (target instanceof Array && target[0] instanceof Function) return true; + else return false; } - return eleArray; - } - - function isTuppleBuilder(target: any): target is [BuildNodeFunction, ...any] { - if (target instanceof Array && target[0] instanceof Function) return true; - else return false; } } type BuildNodeFunction = (...args: any[]) => $Node; -type BuilderSelfFunction = (self: K) => void - -//@ts-expect-error +type BuilderSelfFunction = (self: K) => void; globalThis.$ = $; \ No newline at end of file diff --git a/global.d.ts b/global.d.ts deleted file mode 100644 index 529298a..0000000 --- a/global.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { $ as fluentx } from "./$index"; -import { $Element } from "./lib/$Element"; - -declare global { - const $ = fluentx; - type OrMatrix = T | OrMatrix[]; - type OrArray = T | T[]; - type OrPromise = T | Promise; - type Mutable = { - -readonly [k in keyof T]: T[k]; - }; - type Types = 'string' | 'number' | 'boolean' | 'object' | 'symbol' | 'bigint' | 'function' | 'undefined' - type Autocapitalize = 'none' | 'off' | 'sentences' | 'on' | 'words' | 'characters'; - type SelectionDirection = "forward" | "backward" | "none"; - type InputType = "button" | "checkbox" | "color" | "date" | "datetime-local" | "email" | "file" | "hidden" | "image" | "month" | "number" | "password" | "radio" | "range" | "reset" | "search" | "submit" | "tel" | "text" | "time" | "url" | "week"; - type TextDirection = 'ltr' | 'rtl' | 'auto' | ''; - - interface HTMLElement { - $: $Element; - } -} diff --git a/index.ts b/index.ts index 9048085..2964192 100644 --- a/index.ts +++ b/index.ts @@ -1,11 +1,29 @@ declare global { + var $: import('./$index').$; interface Array { detype(...types: F[]): Array> } + type OrMatrix = T | OrMatrix[]; + type OrArray = T | T[]; + type OrPromise = T | Promise; + type Mutable = { + -readonly [k in keyof T]: T[k]; + }; + type Types = 'string' | 'number' | 'boolean' | 'object' | 'symbol' | 'bigint' | 'function' | 'undefined' + type Autocapitalize = 'none' | 'off' | 'sentences' | 'on' | 'words' | 'characters'; + type SelectionDirection = "forward" | "backward" | "none"; + type InputType = "button" | "checkbox" | "color" | "date" | "datetime-local" | "email" | "file" | "hidden" | "image" | "month" | "number" | "password" | "radio" | "range" | "reset" | "search" | "submit" | "tel" | "text" | "time" | "url" | "week"; + type ButtonType = "submit" | "reset" | "button" | "menu"; + type TextDirection = 'ltr' | 'rtl' | 'auto' | ''; + + interface Node { + $: import('./lib/$Node').$Node; + } } Array.prototype.detype = function (this: O[], ...types: T[]) { return this.filter(item => { - for (const type of types) typeof item !== typeof type + if (!types.length) return item !== undefined; + else for (const type of types) if (typeof item !== typeof type) return false; else return true }) as Exclude[]; } export * from "./$index"; @@ -14,7 +32,10 @@ export * from "./lib/Router/Router"; export * from "./lib/$Node"; export * from "./lib/$Anchor"; export * from "./lib/$Element"; -export * from "./lib/$ElementManager"; +export * from "./lib/$NodeManager"; export * from "./lib/$Text"; export * from "./lib/$Container"; -export * from "./lib/$EventManager"; \ No newline at end of file +export * from "./lib/$Button"; +export * from "./lib/$Form"; +export * from "./lib/$EventManager"; +export * from "./lib/$State"; \ No newline at end of file diff --git a/lib/$Anchor.ts b/lib/$Anchor.ts index ac11d4d..3a42aae 100644 --- a/lib/$Anchor.ts +++ b/lib/$Anchor.ts @@ -8,13 +8,13 @@ export class $Anchor extends $Container { // Link Handler event this.dom.addEventListener('click', e => { if ($.anchorPreventDefault) e.preventDefault(); - if ($.anchorHandler) $.anchorHandler(this.href(), e) + if ($.anchorHandler && !!this.href()) $.anchorHandler(this.href(), e) }) } /**Set URL of anchor element. */ - href(): URL; + href(): string; href(url: string | undefined): this; - href(url?: string | undefined) { return $.fluent(this, arguments, () => new URL(this.dom.href), () => {if (url) this.dom.href = url}) } + 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(target: $AnchorTarget | undefined): this; diff --git a/lib/$Button.ts b/lib/$Button.ts new file mode 100644 index 0000000..0fa09e2 --- /dev/null +++ b/lib/$Button.ts @@ -0,0 +1,22 @@ +import { $Container, $ContainerOptions } from "./$Container"; +import { FormElementMethod, $FormElementMethod } from "./$Form"; +export interface $ButtonOptions extends $ContainerOptions {} +//@ts-expect-error +export interface $Button extends $FormElementMethod {} +@FormElementMethod +export class $Button extends $Container { + constructor(options?: $ButtonOptions) { + super('button', options); + } + + disabled(): boolean; + disabled(disabled: boolean): this; + disabled(disabled?: boolean) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))} + + type(): ButtonType; + type(type: ButtonType): this; + type(type?: ButtonType) { return $.fluent(this, arguments, () => this.dom.type as ButtonType, () => $.set(this.dom, 'type', type))} + + checkValidity() { return this.dom.checkValidity() } + reportValidity() { return this.dom.reportValidity() } +} \ No newline at end of file diff --git a/lib/$Container.ts b/lib/$Container.ts index c819df8..a5860df 100644 --- a/lib/$Container.ts +++ b/lib/$Container.ts @@ -1,11 +1,12 @@ import { $Element, $ElementOptions } from "./$Element"; -import { $ElementManager } from "./$ElementManager"; +import { $NodeManager } from "./$NodeManager"; import { $Node } from "./$Node"; +import { $State } from "./$State"; export interface $ContainerOptions extends $ElementOptions {} export class $Container extends $Element { - readonly children: $ElementManager = new $ElementManager(this); + readonly children: $NodeManager = new $NodeManager(this); constructor(tagname: string, options?: $ContainerOptions) { super(tagname, options) } @@ -13,13 +14,14 @@ export class $Container extends $Element /**Replace element to this element. * @example Element.content([$('div')]) * Element.content('Hello World')*/ - content(children: OrMatrix<$Node | string | undefined>): this { return $.fluent(this, arguments, () => this, () => { + content(children: $ContainerContentBuilder): this { return $.fluent(this, arguments, () => this, () => { this.children.removeAll(); this.insert(children); })} /**Insert element to this element */ - insert(children: OrMatrix<$Node | string | undefined>): this { return $.fluent(this, arguments, () => this, () => { + insert(children: $ContainerContentBuilder): this { return $.fluent(this, arguments, () => this, () => { + if (children instanceof Function) children = children(this); children = $.multableResolve(children); for (const child of children) { if (child === undefined) return; @@ -29,3 +31,6 @@ export class $Container extends $Element this.children.render(); })} } + +export type $ContainerContentBuilder

= OrMatrix<$ContainerContentType> | (($node: P) => OrMatrix<$ContainerContentType>) +export type $ContainerContentType = $Node | string | undefined | $State \ No newline at end of file diff --git a/lib/$Element.ts b/lib/$Element.ts index 840cc2c..1a28f73 100644 --- a/lib/$Element.ts +++ b/lib/$Element.ts @@ -23,7 +23,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, () => {if (name) this.dom.id === name})} + id(name?: string | undefined): this | string {return $.fluent(this, arguments, () => this.dom.id, () => $.set(this.dom, 'id', name))} /**Replace list of class name to element. @example Element.class('name1', 'name2') */ class(): DOMTokenList; diff --git a/lib/$ElementManager.ts b/lib/$ElementManager.ts deleted file mode 100644 index 4f875db..0000000 --- a/lib/$ElementManager.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { $Container } from "./$Container"; -import { $Element } from "./$Element"; -import { $Node } from "./$Node"; -import { $Text } from "./$Text"; - -export class $ElementManager { - #container: $Container; - #dom: HTMLElement; - elementList = new Set<$Node> - constructor(container: $Container) { - this.#container = container; - this.#dom = this.#container.dom - } - - add(element: $Node | string) { - if (typeof element === 'string') { - const text = new $Text(element); - this.elementList.add(text); - } else { - this.elementList.add(element); - } - } - - remove(element: $Node) { - this.elementList.delete(element); - } - - removeAll() { - this.elementList.clear(); - } - - render() { - const [domList, nodeList] = [this.array.map(node => node.dom), Array.from(this.#dom.childNodes)]; - // Rearrange - while (nodeList.length || domList.length) { - const [node, dom] = [nodeList.at(0), domList.at(0)]; - if (!dom) { node?.remove(); nodeList.shift()} - else if (!node) { this.#dom.append(dom); domList.shift();} - else if (dom !== node) { this.#dom.insertBefore(dom, node); domList.shift();} - else {domList.shift(); nodeList.shift();} - } - } - - get array() {return [...this.elementList.values()]}; -} \ No newline at end of file diff --git a/lib/$Form.ts b/lib/$Form.ts new file mode 100644 index 0000000..c622eaf --- /dev/null +++ b/lib/$Form.ts @@ -0,0 +1,84 @@ +import { $Container, $ContainerOptions } from "./$Container"; +import { $Util } from "./$Util"; +export interface $FormOptions extends $ContainerOptions {} +export class $Form extends $Container { + constructor(options?: $FormOptions) { + super('form', options); + } + + autocomplete(): AutoFillBase; + autocomplete(autocomplete: AutoFill): this; + autocomplete(autocomplete?: AutoFill) { return $.fluent(this, arguments, () => this.dom.autocomplete, () => $.set(this.dom, 'autocomplete', autocomplete))} + + action(): string; + action(action: string): this; + action(action?: string) { return $.fluent(this, arguments, () => this.dom.formAction, () => $.set(this.dom, 'formAction', action))} + + enctype(): string; + enctype(enctype: string): this; + enctype(enctype?: string) { return $.fluent(this, arguments, () => this.dom.formEnctype, () => $.set(this.dom, 'formEnctype', enctype))} + + method(): string; + method(method: string): this; + method(method?: string) { return $.fluent(this, arguments, () => this.dom.formMethod, () => $.set(this.dom, 'formMethod', method))} + + noValidate(): boolean; + noValidate(boolean: boolean): this; + noValidate(boolean?: boolean) { return $.fluent(this, arguments, () => this.dom.formNoValidate, () => $.set(this.dom, 'formNoValidate', boolean))} + + acceptCharset(): string; + acceptCharset(acceptCharset: string): this; + acceptCharset(acceptCharset?: string) { return $.fluent(this, arguments, () => this.dom.acceptCharset, () => $.set(this.dom, 'acceptCharset', acceptCharset))} + + target(): string; + target(target: string): this; + target(target?: string) { return $.fluent(this, arguments, () => this.dom.formTarget, () => $.set(this.dom, 'formTarget', target))} + + requestSubmit() { this.dom.requestSubmit(); return this } + reset(): this { this.dom.reset(); return this } + submit() { this.dom.submit(); return this } + checkValidity() { return this.dom.checkValidity() } + reportValidity() { return this.dom.reportValidity() } + + get length() { return this.dom.length } + get elements() { return Array.from(this.dom.elements).map(ele => $(ele)) } +} + +export function FormElementMethod(target: any) { return $Util.mixin(target, $FormElementMethod) } +export abstract class $FormElementMethod { + abstract dom: HTMLButtonElement | HTMLInputElement; + + formAction(): string; + formAction(action: string): this; + formAction(action?: string) { return $.fluent(this, arguments, () => this.dom.formAction, () => $.set(this.dom, 'formAction', action))} + + formEnctype(): string; + formEnctype(enctype: string): this; + formEnctype(enctype?: string) { return $.fluent(this, arguments, () => this.dom.formEnctype, () => $.set(this.dom, 'formEnctype', enctype))} + + formMethod(): string; + formMethod(method: string): this; + formMethod(method?: string) { return $.fluent(this, arguments, () => this.dom.formMethod, () => $.set(this.dom, 'formMethod', method))} + + formNoValidate(): boolean; + formNoValidate(boolean: boolean): this; + formNoValidate(boolean?: boolean) { return $.fluent(this, arguments, () => this.dom.formNoValidate, () => $.set(this.dom, 'formNoValidate', boolean))} + + formTarget(): string; + formTarget(target: string): this; + formTarget(target?: string) { return $.fluent(this, arguments, () => this.dom.formTarget, () => $.set(this.dom, 'formTarget', target))} + + name(): string; + name(name: string): this; + name(name?: string) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))} + + value(): string; + value(value: string): this; + value(value?: string) { 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)) } + get validationMessage() { return this.dom.validationMessage } + get validity() { return this.dom.validity } + get willValidate() { return this.dom.willValidate } +} \ No newline at end of file diff --git a/lib/$Input.ts b/lib/$Input.ts index 49880a4..a43e7e5 100644 --- a/lib/$Input.ts +++ b/lib/$Input.ts @@ -1,11 +1,14 @@ import { $Element, $ElementOptions } from "./$Element"; +import { $FormElementMethod, FormElementMethod } from "./$Form"; export interface $InputOptions extends $ElementOptions {} +//@ts-expect-error +export interface $Input extends $FormElementMethod {} +@FormElementMethod export class $Input extends $Element { - constructor(options: $InputOptions) { + constructor(options?: $InputOptions) { super('input', options); } - accept(): string[] accept(...filetype: string[]): this @@ -27,27 +30,6 @@ export class $Input extends $Element { width(wdith: number): this; width(width?: number) { return $.fluent(this, arguments, () => this.dom.width, () => $.set(this.dom, 'width', width))} - formAction(): string; - formAction(action: string): this; - formAction(action?: string) { return $.fluent(this, arguments, () => this.dom.formAction, () => $.set(this.dom, 'formAction', action))} - - formEnctype(): string; - formEnctype(enctype: string): this; - formEnctype(enctype?: string) { return $.fluent(this, arguments, () => this.dom.formEnctype, () => $.set(this.dom, 'formEnctype', enctype))} - - formMethod(): string; - formMethod(method: string): this; - formMethod(method?: string) { return $.fluent(this, arguments, () => this.dom.formMethod, () => $.set(this.dom, 'formMethod', method))} - - formNoValidate(): boolean; - formNoValidate(boolean: boolean): this; - formNoValidate(boolean?: boolean) { return $.fluent(this, arguments, () => this.dom.formNoValidate, () => $.set(this.dom, 'formNoValidate', boolean))} - - formTarget(): string; - formTarget(target: string): this; - formTarget(target?: string) { return $.fluent(this, arguments, () => this.dom.formTarget, () => $.set(this.dom, 'formTarget', target))} - - checked(): boolean; checked(boolean: boolean): this; checked(boolean?: boolean) { return $.fluent(this, arguments, () => this.dom.checked, () => $.set(this.dom, 'checked', boolean))} @@ -92,10 +74,6 @@ export class $Input extends $Element { multiple(multiple: boolean): this; multiple(multiple?: boolean) { return $.fluent(this, arguments, () => this.dom.multiple, () => $.set(this.dom, 'multiple', multiple))} - name(): string; - name(name: string): this; - name(name?: string) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))} - pattern(): string; pattern(pattern: string): this; pattern(pattern?: string) { return $.fluent(this, arguments, () => this.dom.pattern, () => $.set(this.dom, 'pattern', pattern))} @@ -140,10 +118,6 @@ export class $Input extends $Element { type(type: InputType): this; type(type?: InputType) { return $.fluent(this, arguments, () => this.dom.type, () => $.set(this.dom, 'type', type))} - value(): string; - value(value: string): this; - value(value?: string) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value))} - valueAsDate(): Date | null; valueAsDate(date: Date | null): this; valueAsDate(date?: Date | null) { return $.fluent(this, arguments, () => this.dom.valueAsDate, () => $.set(this.dom, 'valueAsDate', date))} @@ -173,9 +147,5 @@ export class $Input extends $Element { checkValidity() { return this.dom.checkValidity() } reportValidity() { return this.dom.reportValidity() } get files() { return this.dom.files } - get labels() { return Array.from(this.dom.labels ?? []).map(label => $(label)) } - get validationMessage() { return this.dom.validationMessage } - get validity() { return this.dom.validity } get webkitEntries() { return this.dom.webkitEntries } - get willValidate() { return this.dom.willValidate } } \ No newline at end of file diff --git a/lib/$Label.ts b/lib/$Label.ts index 9e249d6..de6f2ca 100644 --- a/lib/$Label.ts +++ b/lib/$Label.ts @@ -1,7 +1,7 @@ import { $Container, $ContainerOptions } from "./$Container"; export interface $LabelOptions extends $ContainerOptions {} export class $Label extends $Container { - constructor(options: $LabelOptions) { + constructor(options?: $LabelOptions) { super('label', options); } diff --git a/lib/$Node.ts b/lib/$Node.ts index 471e815..84242bb 100644 --- a/lib/$Node.ts +++ b/lib/$Node.ts @@ -1,25 +1,62 @@ +import { $Text } from "../index"; import { $Container } from "./$Container"; export abstract class $Node { readonly parent?: $Container; abstract readonly dom: N; - constructor() { + readonly hidden: boolean = false; + private domEvents: {[key: string]: Map} = {}; + on(type: K, callback: (event: Event, $node: this) => void, options?: AddEventListenerOptions | boolean) { + if (!this.domEvents[type]) this.domEvents[type] = new Map() + const middleCallback = (e: Event) => callback(e, this); + this.domEvents[type].set(callback, middleCallback) + this.dom.addEventListener(type, middleCallback, options) + return this; } - on(type: K, callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean) { - this.dom.addEventListener(type, callback, options) + off(type: K, callback: (event: Event, $node: this) => void, options?: AddEventListenerOptions | boolean) { + const middleCallback = this.domEvents[type]?.get(callback); + if (middleCallback) this.dom.removeEventListener(type, middleCallback as EventListener, options) + return this; } - off(type: K, callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean) { - this.dom.removeEventListener(type, callback, options) - } - - once(type: K, callback: (event: Event) => void, options?: AddEventListenerOptions | boolean) { + once(type: K, callback: (event: Event, $node: this) => void, options?: AddEventListenerOptions | boolean) { const onceFn = (event: Event) => { this.dom.removeEventListener(type, onceFn, options) - callback(event); + callback(event, this); }; this.dom.addEventListener(type, onceFn, options) + return this; + } + + show() { (this as Mutable<$Node>).hidden = false; this.parent?.children.render(); return this; } + hide() { + (this as Mutable<$Node>).hidden = true; + this.parent?.children.render(); + return this; + } + + contains(target: $Node | EventTarget | Node) { + if (target instanceof $Node) return this.dom.contains(target.dom); + else if (target instanceof EventTarget) return this.dom.contains($(target).dom) + else return this.dom.contains(target) + } + + 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' } } \ No newline at end of file diff --git a/lib/$NodeManager.ts b/lib/$NodeManager.ts new file mode 100644 index 0000000..ac6f232 --- /dev/null +++ b/lib/$NodeManager.ts @@ -0,0 +1,60 @@ +import { $Container } from "./$Container"; +import { $Node } from "./$Node"; +import { $Text } from "./$Text"; +import { $State } from "./$State"; + +export class $NodeManager { + #container: $Container; + #dom: HTMLElement; + elementList = new Set<$Node> + constructor(container: $Container) { + this.#container = container; + this.#dom = this.#container.dom + } + + add(element: $Node | string | $State) { + if (typeof element === 'string') { + const text = new $Text(element); + (text as Mutable<$Text>).parent = this.#container; + this.elementList.add(text); + } else if (element instanceof $State) { + if (typeof element.value === 'string') { + const ele = new $Text(element.value); + element.contents.add(ele); + (ele as Mutable<$Text>).parent = this.#container; + this.elementList.add(ele); + } + } else { + (element as Mutable<$Node>).parent = this.#container; + this.elementList.add(element); + } + } + + remove(element: $Node) { + if (!this.elementList.has(element)) return; + this.elementList.delete(element); + (element as Mutable<$Node>).parent = undefined; + + } + + removeAll() { + this.elementList.forEach(ele => this.remove(ele)) + } + + render() { + const [domList, nodeList] = [this.array.map(node => node.dom), Array.from(this.#dom.childNodes)]; + // Rearrange + while (nodeList.length || domList.length) { + const [node, dom] = [nodeList.at(0), domList.at(0)]; + if (!dom) { node?.remove(); nodeList.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); domList.shift();} + else { + if (dom.$.hidden) this.#dom.removeChild(dom); + domList.shift(); nodeList.shift(); + } + } + } + + get array() {return [...this.elementList.values()]}; +} \ No newline at end of file diff --git a/lib/$State.ts b/lib/$State.ts new file mode 100644 index 0000000..78acedc --- /dev/null +++ b/lib/$State.ts @@ -0,0 +1,22 @@ +import { $Node } from "./$Node"; +import { $Text } from "./$Text"; + +export class $State { + readonly value: T; + readonly contents = new Set<$Node>(); + constructor(value: T) { + this.value = value; + } + set(value: T) { + (this as Mutable<$State>).value = value; + for (const content of this.contents.values()) { + if (content instanceof $Text) { + content.content(`${value}`); + } + } + } + + toString(): string { + return `${this.value}` + } +}; \ No newline at end of file diff --git a/lib/$Text.ts b/lib/$Text.ts index ca93696..d9f7018 100644 --- a/lib/$Text.ts +++ b/lib/$Text.ts @@ -5,5 +5,10 @@ export class $Text extends $Node { constructor(data: string) { super(); this.dom = new Text(data); + this.dom.$ = this; } + + content(): string; + content(text: string): this; + content(text?: string) { return $.fluent(this, arguments, () => this.dom.textContent, () => $.set(this.dom, 'textContent', text))} } \ No newline at end of file diff --git a/lib/$Util.ts b/lib/$Util.ts new file mode 100644 index 0000000..7f5d166 --- /dev/null +++ b/lib/$Util.ts @@ -0,0 +1,36 @@ +import { $State } from "./$State"; + +export namespace $Util { + export function fluent(instance: T, args: IArguments, value: () => V, action: (...args: any[]) => void) { + if (!args.length) return value(); + action(); + return instance; + } + + export function multableResolve(multable: OrArray) { + if (multable instanceof Array) return multable; + else return [multable]; + } + + export function mixin(target: any, constructors: OrArray) { + multableResolve(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 set(object: O, key: K, value: any) { + if (value !== undefined) object[key] = value; + } + + export function state(value: T) { + return new $State(value) + } +} \ No newline at end of file diff --git a/lib/Router/Route.ts b/lib/Router/Route.ts index e7c395f..567311c 100644 --- a/lib/Router/Route.ts +++ b/lib/Router/Route.ts @@ -1,17 +1,39 @@ +import { $EventManager, $EventMethod, EventMethod } from "../$EventManager"; import { $Node } from "../$Node"; - -export class Route { - path: string; - builder: (path: PathParams) => string | $Node; - constructor(path: Path, builder: (params: PathParams) => $Node | string) { - if (!path.startsWith('/')) throw new Error('PATH SHOULD START WITH /') +export class Route { + path: string | PathResolverFn; + builder: (params: PathParamResolver, record: RouteRecord) => $Node | string; + constructor(path: Path, builder: (params: PathParamResolver, record: RouteRecord) => $Node | string) { this.path = path; this.builder = builder; } } type PathParams = Path extends `${infer Segment}/${infer Rest}` - ? Segment extends `:${infer Param}` ? Record & PathParams : PathParams - : Path extends `:${infer Param}` ? Record : {} + ? Segment extends `${string}:${infer Param}` ? Record & PathParams : PathParams + : Path extends `${string}:${infer Param}` ? Record : {} -type A = PathParams<'/:userId/post/:postId'> \ No newline at end of file +export type PathResolverFn = (path: string) => undefined | string; + +type PathParamResolver

= P extends PathResolverFn +? undefined : PathParams

+ +// type PathResolverRecord

= { +// [key in keyof ReturnType

]: ReturnType

[key] +// } + + +export interface RouteRecord extends $EventMethod {}; +@EventMethod +export class RouteRecord { + id: string; + readonly content?: $Node; + events = new $EventManager().register('open') + constructor(id: string) { + this.id = id; + } +} + +export interface RouteRecordEventMap { + 'open': [path: string, record: RouteRecord] +} \ No newline at end of file diff --git a/lib/Router/Router.ts b/lib/Router/Router.ts index 8a44bfe..904d523 100644 --- a/lib/Router/Router.ts +++ b/lib/Router/Router.ts @@ -1,13 +1,12 @@ import { $Container } from "../$Container"; import { $EventManager, $EventMethod, EventMethod } from "../$EventManager"; -import { $Node } from "../$Node"; import { $Text } from "../$Text"; -import { Route } from "./Route"; +import { PathResolverFn, Route, RouteRecord } from "./Route"; export interface Router extends $EventMethod {}; @EventMethod export class Router { - routeMap = new Map>(); - contentMap = new Map(); + routeMap = new Map>(); + recordMap = new Map(); view: $Container; index: number = 0; events = new $EventManager().register('pathchange', 'notfound'); @@ -15,7 +14,6 @@ export class Router { constructor(basePath: string, view: $Container) { this.basePath = basePath; this.view = view - $.routers.add(this); } /**Add route to Router. @example Router.addRoute(new Route('/', 'Hello World')) */ @@ -35,13 +33,13 @@ export class Router { } addEventListener('popstate', this.popstate) this.resolvePath(); + $.routers.add(this); return this; } /**Open path */ - open(path: string | URL) { - if (path instanceof URL) path = path.pathname; - if (path === location.pathname) return this; + open(path: string) { + if (path === location.href) return this; this.index += 1; const routeData: RouteData = { index: this.index }; history.pushState(routeData, '', path); @@ -51,7 +49,14 @@ export class Router { } /**Back to previous page */ - back() { history.back(); } + back() { history.back(); return this } + + replace(path: string) { + history.replaceState({index: this.index}, '', path) + $.routers.forEach(router => router.resolvePath()); + this.events.fire('pathchange', path, 'Forward'); + return this; + } private popstate = (() => { // Forward @@ -67,38 +72,56 @@ export class Router { if (!path.startsWith(this.basePath)) return; path = path.replace(this.basePath, '/').replace('//', '/') let found = false; - const openCached = () => { - const cacheContent = this.contentMap.get(path); - if (cacheContent) { - this.view.content(cacheContent); + const openCached = (pathId: string) => { + 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); + record.events.fire('open', path, record); return true; } return false; } - const create = (content: $Node | string) => { + const create = (pathId: string, route: Route, data: any) => { + const record = new RouteRecord(pathId); + let content = route.builder(data, record); if (typeof content === 'string') content = new $Text(content); - this.contentMap.set(path, content) + (record as Mutable).content = content; + this.recordMap.set(pathId, record); this.view.content(content); + record.events.fire('open', path, record); found = true; } - for (const route of this.routeMap.values()) { - const [_routeParts, _pathParts] = [route.path.split('/').map(p => `/${p}`), path.split('/').map(p => `/${p}`)]; + for (const [pathResolver, route] of this.routeMap.entries()) { + // PathResolverFn + if (pathResolver instanceof Function) { + const routeId = pathResolver(path) + if (routeId) { if (!openCached(routeId)) create(routeId, route, undefined) } + continue; + } + // string + const [_routeParts, _pathParts] = [pathResolver.split('/').map(p => `/${p}`), path.split('/').map(p => `/${p}`)]; _routeParts.shift(); _pathParts.shift(); const data = {}; + let pathString = ''; for (let i = 0; i < _pathParts.length; i++) { const [routePart, pathPart] = [_routeParts.at(i), _pathParts.at(i)]; if (!routePart || !pathPart) continue; if (routePart === pathPart) { + pathString += pathPart; if (routePart === _routeParts.at(-1)) { - if (!openCached()) create(route.builder(data)); + if (!openCached(pathString)) create(pathString, route, data); return; } } else if (routePart.includes(':')) { - Object.assign(data, {[routePart.split(':')[1]]: pathPart.replace('/', '')}) + const [prefix, param] = routePart.split(':'); + if (!pathPart.startsWith(prefix)) return; + Object.assign(data, {[param]: pathPart.replace('/', '')}) + pathString += pathPart; if (routePart === _routeParts.at(-1)) { - if (!openCached()) create(route.builder(data)); + if (!openCached(pathString)) create(pathString, route, data); return; } } diff --git a/package.json b/package.json index 8982296..6d2b443 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "version": "0.0.1", "type": "module", "module": "index.ts", - "main": "index.ts", "author": { "name": "defaultkavy", "email": "defaultkavy@gmail.com",