diff --git a/$index.ts b/$index.ts index d276df6..8ba0151 100644 --- a/$index.ts +++ b/$index.ts @@ -7,13 +7,21 @@ 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"; export type $ = typeof $; +export function $(element: null): null; export function $(resolver: K): $.TagNameTypeMap[K]; export function $(resolver: K): $Container; export function $(htmlElement: H): $.HTMLElementTo$ElementMap; export function $(element: H): $Element; 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 (typeof resolver === 'string') { if (resolver in $.TagNameElementMap) { const instance = $.TagNameElementMap[resolver as keyof typeof $.TagNameElementMap] @@ -25,6 +33,9 @@ export function $(resolver: any) { case $Label: return new $Label(); case $Form: return new $Form(); case $Button: return new $Button(); + case $Image: return new $Image(); + case $Canvas: return new $Canvas(); + case $Dialog: return new $Dialog(); } } else return new $Container(resolver); } @@ -60,7 +71,10 @@ export namespace $ { 'input': $Input, 'label': $Label, 'button': $Button, - 'form': $Form + 'form': $Form, + 'img': $Image, + 'dialog': $Dialog, + 'canvas': $Canvas } export type TagNameTypeMap = { [key in keyof typeof $.TagNameElementMap]: InstanceType; @@ -74,6 +88,10 @@ export namespace $ { : H extends HTMLAnchorElement ? $Anchor : H extends HTMLButtonElement ? $Button : H extends HTMLFormElement ? $Form + : H extends HTMLImageElement ? $Image + : H extends HTMLFormElement ? $Form + : H extends HTMLCanvasElement ? $Canvas + : H extends HTMLDialogElement ? $Dialog : $Element; export function fluent(instance: T, args: IArguments, value: () => V, action: (...args: any[]) => void) { @@ -101,14 +119,48 @@ export namespace $ { return target; } - export function set(object: O, key: K, value: any) { - if (value !== undefined) object[key] = value; + export function set(object: O, key: K, value: any, methodKey?: string) { + if (value === undefined) return; + if (value instanceof $State && object instanceof Node) { + value.use(object.$, methodKey ?? key as any); + object[key] = value.value; + return; + } + object[key] = value; } export function state(value: T) { return new $State(value) } + export async function resize(object: Blob, size: number): Promise { + return new Promise(resolve => { + const reader = new FileReader(); + reader.onload = (e) => { + const $img = $('img'); + $img.once('load', e => { + const $canvas = $('canvas'); + const context = $canvas.getContext('2d'); + const ratio = $img.height() / $img.width(); + const [w, h] = [ + ratio > 1 ? size / ratio : size, + ratio > 1 ? size : size * ratio, + ] + $canvas.height(h).width(w); + context?.drawImage($img.dom, 0, 0, w, h); + resolve($canvas.toDataURL(object.type)) + }) + if (!e.target) throw "$.resize(): e.target is null"; + $img.src(e.target.result as string); + } + reader.readAsDataURL(object); + }) + } + + export function rem(amount: number = 1) { + return parseInt(getComputedStyle(document.documentElement).fontSize) * amount + } + /**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[] diff --git a/index.ts b/index.ts index 2964192..0ff39b1 100644 --- a/index.ts +++ b/index.ts @@ -15,7 +15,9 @@ declare global { 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' | ''; - + type ImageDecoding = "async" | "sync" | "auto"; + type ImageLoading = "eager" | "lazy"; + type ContructorType = { new (...args: any[]): T } interface Node { $: import('./lib/$Node').$Node; } diff --git a/lib/$Button.ts b/lib/$Button.ts index 0fa09e2..06de082 100644 --- a/lib/$Button.ts +++ b/lib/$Button.ts @@ -1,5 +1,6 @@ import { $Container, $ContainerOptions } from "./$Container"; import { FormElementMethod, $FormElementMethod } from "./$Form"; +import { $State } from "./$State"; export interface $ButtonOptions extends $ContainerOptions {} //@ts-expect-error export interface $Button extends $FormElementMethod {} @@ -10,8 +11,8 @@ export class $Button extends $Container { } disabled(): boolean; - disabled(disabled: boolean): this; - disabled(disabled?: boolean) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))} + disabled(disabled: boolean | $State): this; + disabled(disabled?: boolean | $State) { 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/$Canvas.ts new file mode 100644 index 0000000..eb46e8e --- /dev/null +++ b/lib/$Canvas.ts @@ -0,0 +1,29 @@ +import { $Container, $ContainerOptions } from "./$Container"; +export interface $CanvasOptions extends $ContainerOptions {} +export class $Canvas extends $Container { + constructor(options?: $CanvasOptions) { + super('canvas', options); + } + + height(): number; + height(height?: number): this; + height(height?: number) { return $.fluent(this, arguments, () => this.dom.height, () => { $.set(this.dom, 'height', height)}) } + + width(): number; + width(width?: number): this; + width(width?: number) { return $.fluent(this, arguments, () => this.dom.width, () => { $.set(this.dom, 'width', width)}) } + + captureStream(frameRequestRate?: number) { return this.dom.captureStream(frameRequestRate) } + + getContext(contextId: "2d", options?: CanvasRenderingContext2DSettings): CanvasRenderingContext2D | null; + getContext(contextId: "bitmaprenderer", options?: ImageBitmapRenderingContextSettings): ImageBitmapRenderingContext | null; + getContext(contextId: "webgl", options?: WebGLContextAttributes): WebGLRenderingContext | null; + getContext(contextId: "webgl2", options?: WebGLContextAttributes): WebGL2RenderingContext | null; + getContext(contextId: string, options?: any): RenderingContext | null { return this.dom.getContext(contextId); } + + toBlob(callback: BlobCallback, type?: string, quality?: any) { this.dom.toBlob(callback, type, quality); return this;} + + toDataURL(type?: string, quality?: any) { return this.dom.toDataURL(type, quality) } + + transferControlToOffscreen() { return this.dom.transferControlToOffscreen() } +} \ No newline at end of file diff --git a/lib/$Container.ts b/lib/$Container.ts index a5860df..dd2eca7 100644 --- a/lib/$Container.ts +++ b/lib/$Container.ts @@ -2,6 +2,7 @@ import { $Element, $ElementOptions } from "./$Element"; import { $NodeManager } from "./$NodeManager"; import { $Node } from "./$Node"; import { $State } from "./$State"; +import { $Text } from "./$Text"; export interface $ContainerOptions extends $ElementOptions {} @@ -24,9 +25,13 @@ export class $Container extends $Element if (children instanceof Function) children = children(this); children = $.multableResolve(children); for (const child of children) { - if (child === undefined) return; + if (child === undefined) continue; if (child instanceof Array) this.insert(child) - else this.children.add(child); + else if (child instanceof $State) { + const ele = new $Text(child.toString()); + child.use(ele, 'content'); + this.children.add(ele); + } else this.children.add(child); } this.children.render(); })} diff --git a/lib/$Dialog.ts b/lib/$Dialog.ts new file mode 100644 index 0000000..d0f8f21 --- /dev/null +++ b/lib/$Dialog.ts @@ -0,0 +1,19 @@ +import { $Container, $ContainerOptions } from "./$Container"; +export interface $DialogOptions extends $ContainerOptions {} +export class $Dialog extends $Container { + constructor(options?: $DialogOptions) { + super('dialog', options); + } + + open(): boolean; + open(open?: boolean): this; + open(open?: boolean) { return $.fluent(this, arguments, () => this.dom.open, () => $.set(this.dom, 'open', open)) } + + returnValue(): string; + returnValue(returnValue?: string): this; + returnValue(returnValue?: string) { return $.fluent(this, arguments, () => this.dom.returnValue, () => $.set(this.dom, 'returnValue', returnValue)) } + + close() { this.dom.close(); return this; } + show() { this.dom.show(); return this; } + showModal() { this.dom.showModal(); return this; } +} \ No newline at end of file diff --git a/lib/$Element.ts b/lib/$Element.ts index 1a28f73..0acab4d 100644 --- a/lib/$Element.ts +++ b/lib/$Element.ts @@ -38,32 +38,69 @@ export class $Element extends $Node { css(): CSSStyleDeclaration css(style: Partial): this; css(style?: Partial) { return $.fluent(this, arguments, () => this.dom.style, () => {Object.assign(this.dom.style, style)})} - - /**Remove this element from parent */ - remove() { - this.parent?.children.remove(this); - (this as Mutable).parent = undefined; - this.dom.remove(); - return this; - } autocapitalize(): Autocapitalize; - autocapitalize(autocapitalize: Autocapitalize): this; + 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): 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): 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): 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): 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))} + + 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; } + + animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions, callback?: (animation: Animation) => void) { + const animation = this.dom.animate(keyframes, options); + if (callback) callback(animation); + return this; + } + + 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 } } \ No newline at end of file diff --git a/lib/$Form.ts b/lib/$Form.ts index c622eaf..863feba 100644 --- a/lib/$Form.ts +++ b/lib/$Form.ts @@ -1,4 +1,5 @@ import { $Container, $ContainerOptions } from "./$Container"; +import { $State } from "./$State"; import { $Util } from "./$Util"; export interface $FormOptions extends $ContainerOptions {} export class $Form extends $Container { @@ -7,31 +8,31 @@ export class $Form extends $Container { } autocomplete(): AutoFillBase; - autocomplete(autocomplete: AutoFill): this; + autocomplete(autocomplete: AutoFill | undefined): 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 | undefined): 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 | undefined): 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 | undefined): 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 | undefined): 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 | undefined): 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 | undefined): this; target(target?: string) { return $.fluent(this, arguments, () => this.dom.formTarget, () => $.set(this.dom, 'formTarget', target))} requestSubmit() { this.dom.requestSubmit(); return this } @@ -49,32 +50,32 @@ export abstract class $FormElementMethod { abstract dom: HTMLButtonElement | HTMLInputElement; formAction(): string; - formAction(action: string): this; + formAction(action: string | undefined): 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 | undefined): 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 | undefined): 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 | undefined): 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 | undefined): 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))} + name(name?: string | $State): this; + name(name?: string | $State) { 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))} + 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 labels() { return Array.from(this.dom.labels ?? []).map(label => $(label)) } diff --git a/lib/$Image.ts b/lib/$Image.ts new file mode 100644 index 0000000..96b5c08 --- /dev/null +++ b/lib/$Image.ts @@ -0,0 +1,85 @@ +import { $Element, $ElementOptions } from "./$Element"; +import { $State } from "./$State"; +export interface $ImageOptions extends $ElementOptions {} +export class $Image extends $Element { + constructor(options?: $ImageOptions) { + super('img', options); + } + + /**HTMLImageElement base property */ + alt(): string; + alt(alt: string): this; + alt(alt?: string) { return $.fluent(this, arguments, () => this.dom.alt, () => $.set(this.dom, 'alt', alt))} + + /**HTMLImageElement base property */ + crossOrigin(): string | null; + crossOrigin(crossOrigin: string | null): this; + crossOrigin(crossOrigin?: string | null) { return $.fluent(this, arguments, () => this.dom.crossOrigin, () => $.set(this.dom, 'crossOrigin', crossOrigin))} + + /**HTMLImageElement base property */ + decoding(): ImageDecoding; + decoding(decoding: ImageDecoding): this; + decoding(decoding?: ImageDecoding) { return $.fluent(this, arguments, () => this.dom.decoding, () => $.set(this.dom, 'decoding', decoding))} + + /**HTMLImageElement base property */ + height(): number; + height(height: number): this; + height(height?: number) { return $.fluent(this, arguments, () => this.dom.height, () => $.set(this.dom, 'height', height))} + + /**HTMLImageElement base property */ + isMap(): boolean; + isMap(isMap: boolean): this; + isMap(isMap?: boolean) { return $.fluent(this, arguments, () => this.dom.isMap, () => $.set(this.dom, 'isMap', isMap))} + + /**HTMLImageElement base property */ + loading(): ImageLoading; + loading(loading: ImageLoading): this; + loading(loading?: ImageLoading) { return $.fluent(this, arguments, () => this.dom.loading, () => $.set(this.dom, 'loading', loading))} + + /**HTMLImageElement base property */ + referrerPolicy(): string; + referrerPolicy(referrerPolicy: string): this; + referrerPolicy(referrerPolicy?: string) { return $.fluent(this, arguments, () => this.dom.referrerPolicy, () => $.set(this.dom, 'referrerPolicy', referrerPolicy))} + + /**HTMLImageElement base property */ + sizes(): string; + sizes(sizes: string): this; + sizes(sizes?: string) { return $.fluent(this, arguments, () => this.dom.sizes, () => $.set(this.dom, 'sizes', sizes))} + + /**HTMLImageElement base property */ + src(): string; + src(src?: string | $State): this; + src(src?: string | $State) { return $.fluent(this, arguments, () => this.dom.src, () => $.set(this.dom, 'src', src))} + + /**HTMLImageElement base property */ + srcset(): string; + srcset(srcset: string): this; + srcset(srcset?: string) { return $.fluent(this, arguments, () => this.dom.srcset, () => $.set(this.dom, 'srcset', srcset))} + + /**HTMLImageElement base property */ + useMap(): string; + useMap(useMap: string): this; + useMap(useMap?: string) { return $.fluent(this, arguments, () => this.dom.useMap, () => $.set(this.dom, 'useMap', useMap))} + + /**HTMLImageElement base property */ + width(): number; + width(width: number): this; + width(width?: number) { return $.fluent(this, arguments, () => this.dom.width, () => $.set(this.dom, 'width', width))} + + /**HTMLImageElement base method */ + decode() { return this.dom.decode() } + + /**HTMLImageElement base property */ + get complete() { return this.dom.complete } + /**HTMLImageElement base property */ + get currentSrc() { return this.dom.currentSrc } + /**HTMLImageElement base property */ + get naturalHeight() { return this.dom.naturalHeight } + /**HTMLImageElement base property */ + get naturalWidth() { return this.dom.naturalWidth } + /**HTMLImageElement base property */ + get x() { return this.dom.x } + /**HTMLImageElement base property */ + get y() { return this.dom.y } + +} \ No newline at end of file diff --git a/lib/$Input.ts b/lib/$Input.ts index a43e7e5..6b75279 100644 --- a/lib/$Input.ts +++ b/lib/$Input.ts @@ -79,7 +79,7 @@ export class $Input extends $Element { pattern(pattern?: string) { return $.fluent(this, arguments, () => this.dom.pattern, () => $.set(this.dom, 'pattern', pattern))} placeholder(): string; - placeholder(placeholder: string): this; + placeholder(placeholder?: string): this; placeholder(placeholder?: string) { return $.fluent(this, arguments, () => this.dom.placeholder, () => $.set(this.dom, 'placeholder', placeholder))} readOnly(): boolean; diff --git a/lib/$Label.ts b/lib/$Label.ts index de6f2ca..1d4a937 100644 --- a/lib/$Label.ts +++ b/lib/$Label.ts @@ -6,8 +6,8 @@ export class $Label extends $Container { } for(): string; - for(name: string): this; - for(name?: string | undefined) { return $.fluent(this, arguments, () => this.dom.htmlFor, () => {if (name) this.dom.htmlFor = name}) } + for(name?: string): this; + for(name?: string) { return $.fluent(this, arguments, () => this.dom.htmlFor, () => { $.set(this.dom, 'htmlFor', name, 'for')}) } get form() { return this.dom.form } get control() { return this.dom.control } diff --git a/lib/$Node.ts b/lib/$Node.ts index 84242bb..10ca1bc 100644 --- a/lib/$Node.ts +++ b/lib/$Node.ts @@ -1,48 +1,68 @@ -import { $Text } from "../index"; +import { $State, $Text } from "../index"; import { $Container } from "./$Container"; export abstract class $Node { readonly parent?: $Container; abstract readonly dom: N; - readonly hidden: boolean = false; + readonly $hidden: boolean = false; private domEvents: {[key: string]: Map} = {}; - on(type: K, callback: (event: Event, $node: this) => void, options?: AddEventListenerOptions | boolean) { + on(type: K, callback: (event: HTMLElementEventMap[K], $node: this) => void, options?: AddEventListenerOptions | boolean) { if (!this.domEvents[type]) this.domEvents[type] = new Map() - const middleCallback = (e: Event) => callback(e, this); + const middleCallback = (e: Event) => callback(e as HTMLElementEventMap[K], this); this.domEvents[type].set(callback, middleCallback) this.dom.addEventListener(type, middleCallback, options) return this; } - off(type: K, callback: (event: Event, $node: this) => void, options?: AddEventListenerOptions | boolean) { + off(type: K, callback: (event: HTMLElementEventMap[K], $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; } - once(type: K, callback: (event: Event, $node: this) => void, options?: AddEventListenerOptions | boolean) { + once(type: K, callback: (event: HTMLElementEventMap[K], $node: this) => void, options?: AddEventListenerOptions | boolean) { const onceFn = (event: Event) => { this.dom.removeEventListener(type, onceFn, options) - callback(event, this); + callback(event as HTMLElementEventMap[K], 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; + hide(): boolean; + hide(hide?: boolean | $State): this; + hide(hide?: boolean | $State) { return $.fluent(this, arguments, () => this.$hidden, () => { + if (hide === undefined) return; + if (hide instanceof $State) { (this as Mutable<$Node>).$hidden = hide.value; hide.use(this, 'hide')} + else (this as Mutable<$Node>).$hidden = hide; this.parent?.children.render(); return this; + })} + + /**Remove this element from parent */ + remove() { + this.parent?.children.remove(this).render(); + return this; } - contains(target: $Node | EventTarget | Node) { + /**Replace $Node with this element */ + replace($node: $Node) { + this.parent?.children.replace(this, $node).render(); + return this; + } + + contains(target: $Node | EventTarget | Node | null) { + 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) else return this.dom.contains(target) } + self(callback: ($node: this) => void) { callback(this); return this; } + + inDOM() { return document.contains(this.dom); } + static from(element: HTMLElement | Text): $Node { if (element.$) return element.$; else if (element instanceof HTMLElement) { diff --git a/lib/$NodeManager.ts b/lib/$NodeManager.ts index ac6f232..8d35ef3 100644 --- a/lib/$NodeManager.ts +++ b/lib/$NodeManager.ts @@ -1,7 +1,6 @@ import { $Container } from "./$Container"; import { $Node } from "./$Node"; import { $Text } from "./$Text"; -import { $State } from "./$State"; export class $NodeManager { #container: $Container; @@ -12,18 +11,11 @@ export class $NodeManager { this.#dom = this.#container.dom } - add(element: $Node | string | $State) { + 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 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); @@ -31,26 +23,42 @@ export class $NodeManager { } remove(element: $Node) { - if (!this.elementList.has(element)) return; + if (!this.elementList.has(element)) return this; this.elementList.delete(element); (element as Mutable<$Node>).parent = undefined; - + return this; } removeAll() { this.elementList.forEach(ele => this.remove(ele)) } + replace(target: $Node, replace: $Node) { + const array = this.array.map(node => { + if (node === target) return replace; + else return node; + }) + 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; + } + render() { const [domList, nodeList] = [this.array.map(node => node.dom), Array.from(this.#dom.childNodes)]; + const appendedNodeList: Node[] = []; // appended node list // Rearrange - while (nodeList.length || domList.length) { + while (nodeList.length || domList.length) { // while nodeList or domList has item 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();} + if (!dom) { if (node && !appendedNodeList.includes(node)) node.remove(); nodeList.shift()} + else if (!node) { if (!dom.$.$hidden) this.#dom.append(dom); domList.shift();} + else if (dom !== node) { + if (!dom.$.$hidden) { this.#dom.insertBefore(dom, node); appendedNodeList.push(dom) } + domList.shift(); + } else { - if (dom.$.hidden) this.#dom.removeChild(dom); + if (dom.$.$hidden) this.#dom.removeChild(dom); domList.shift(); nodeList.shift(); } } diff --git a/lib/$State.ts b/lib/$State.ts index 78acedc..10b5d9e 100644 --- a/lib/$State.ts +++ b/lib/$State.ts @@ -1,17 +1,17 @@ import { $Node } from "./$Node"; -import { $Text } from "./$Text"; export class $State { readonly value: T; - readonly contents = new Set<$Node>(); + readonly attributes = new Map<$Node, Set>(); 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}`); + for (const [node, attrList] of this.attributes.entries()) { + for (const attr of attrList) { + //@ts-expect-error + if (node[attr] instanceof Function) node[attr](value) } } } @@ -19,4 +19,10 @@ export class $State { toString(): string { return `${this.value}` } + + use($node: T, attrName: K) { + const attrList = this.attributes.get($node) + if (attrList) attrList.add(attrName); + else this.attributes.set($node, new Set().add(attrName)) + } }; \ No newline at end of file diff --git a/lib/Router/Route.ts b/lib/Router/Route.ts index 567311c..5aa23da 100644 --- a/lib/Router/Route.ts +++ b/lib/Router/Route.ts @@ -2,8 +2,8 @@ import { $EventManager, $EventMethod, EventMethod } from "../$EventManager"; import { $Node } from "../$Node"; export class Route { path: string | PathResolverFn; - builder: (params: PathParamResolver, record: RouteRecord) => $Node | string; - constructor(path: Path, builder: (params: PathParamResolver, record: RouteRecord) => $Node | string) { + builder: (req: RouteRequest) => $Node | string; + constructor(path: Path, builder: (req: RouteRequest) => $Node | string) { this.path = path; this.builder = builder; } @@ -36,4 +36,9 @@ export class RouteRecord { export interface RouteRecordEventMap { 'open': [path: string, record: RouteRecord] +} + +export interface RouteRequest { + params: PathParamResolver, + record: RouteRecord, } \ No newline at end of file diff --git a/lib/Router/Router.ts b/lib/Router/Router.ts index 904d523..7476683 100644 --- a/lib/Router/Router.ts +++ b/lib/Router/Router.ts @@ -32,8 +32,9 @@ export class Router { this.index = history.state.index } addEventListener('popstate', this.popstate) - this.resolvePath(); $.routers.add(this); + this.resolvePath(); + this.events.fire('pathchange', location.href, 'Forward'); return this; } @@ -53,7 +54,7 @@ export class Router { replace(path: string) { history.replaceState({index: this.index}, '', path) - $.routers.forEach(router => router.resolvePath()); + $.routers.forEach(router => router.resolvePath(path)); this.events.fire('pathchange', path, 'Forward'); return this; } @@ -85,7 +86,7 @@ export class Router { } const create = (pathId: string, route: Route, data: any) => { const record = new RouteRecord(pathId); - let content = route.builder(data, record); + let content = route.builder({params: data, record: record}); if (typeof content === 'string') content = new $Text(content); (record as Mutable).content = content; this.recordMap.set(pathId, record); @@ -117,8 +118,8 @@ export class Router { } else if (routePart.includes(':')) { const [prefix, param] = routePart.split(':'); - if (!pathPart.startsWith(prefix)) return; - Object.assign(data, {[param]: pathPart.replace('/', '')}) + if (!pathPart.startsWith(prefix)) continue; + Object.assign(data, {[param]: pathPart.replace(prefix, '')}) pathString += pathPart; if (routePart === _routeParts.at(-1)) { if (!openCached(pathString)) create(pathString, route, data); diff --git a/package.json b/package.json index 6d2b443..e344b52 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "fluentx", "description": "Fast, fluent, simple web builder", - "version": "0.0.1", + "version": "0.0.2", "type": "module", "module": "index.ts", "author": {