diff --git a/$index.ts b/$index.ts index 65de40c..0d972c1 100644 --- a/$index.ts +++ b/$index.ts @@ -19,6 +19,7 @@ import { $Textarea } from "./lib/node/$Textarea"; import { $Util } from "./lib/$Util"; import { $HTMLElement } from "./lib/node/$HTMLElement"; import { $Async } from "./lib/node/$Async"; +import { $Video } from "./lib/node/$Video"; export type $ = typeof $; export function $(query: `::${string}`): E[]; @@ -88,6 +89,7 @@ export namespace $ { 'option': $Option, 'optgroup': $OptGroup, 'textarea': $Textarea, + 'video': $Video, 'async': $Async, } export type TagNameElementMapType = typeof TagNameElementMap; @@ -112,6 +114,7 @@ export namespace $ { : H extends HTMLOptionElement ? $Option : H extends HTMLOptGroupElement ? $OptGroup : H extends HTMLTextAreaElement ? $Textarea + : H extends HTMLVideoElement ? $Video : $Container; /** @@ -142,27 +145,27 @@ export namespace $ { * @param handle callback when param `value` is $State object. * @returns */ - export function set( + export function set( object: O, key: K, value: O[K] extends (...args: any) => any - ? (undefined | $StateArgument>) + ? (undefined | [$StateArgument>]) : (undefined | $StateArgument), handle?: ($state: $State) => any) { if (value === undefined) return; if (value instanceof $State) { value.use(object, key); - if (object[key] instanceof Function) (object[key] as Function)(value) + if (object[key] instanceof Function) (object[key] as Function)(...value.value) else object[key] = value.value; if (handle) handle(value); return; } - if (object[key] instanceof Function) (object[key] as Function)(value); + if (object[key] instanceof Function) (object[key] as Function)(...value as any); else object[key] = value as any; } - export function state(value: T, options?: $StateOption) { - return new $State(value, options) + export function state(value: T, options?: $StateOption ? K : T>) { + return new $State(value, options as $StateOption) as T extends $State ? $State : $State; } export async function resize(object: Blob, size: number): Promise { diff --git a/index.ts b/index.ts index c90f8dd..ddc9752 100644 --- a/index.ts +++ b/index.ts @@ -26,7 +26,7 @@ declare global { Array.prototype.detype = function (this: O[], ...types: T[]) { return this.filter(item => { if (!types.length) return item !== undefined; - else for (const type of types) if (typeof item !== typeof type) return false; else return true + else for (const type of types) if (typeof item !== typeof type) return true; else return false; }) as Exclude[]; } export * from "./$index"; @@ -47,4 +47,6 @@ export * from "./lib/node/$OptGroup"; export * from "./lib/node/$Textarea"; export * from "./lib/node/$Image"; export * from "./lib/node/$Async"; -export * from "./lib/node/$Document"; \ No newline at end of file +export * from "./lib/node/$Document"; +export * from "./lib/node/$Media"; +export * from "./lib/node/$Video"; \ No newline at end of file diff --git a/lib/$NodeManager.ts b/lib/$NodeManager.ts index 2af381e..48c4325 100644 --- a/lib/$NodeManager.ts +++ b/lib/$NodeManager.ts @@ -3,32 +3,33 @@ import { $Node } from "./node/$Node"; import { $Text } from "./node/$Text"; export class $NodeManager { - $container: $Container; - $elementList = new Set<$Node> + readonly $container: $Container; + readonly childList = new Set<$Node> constructor(container: $Container) { this.$container = container; } - add(element: $Node | string) { - if (typeof element === 'string') { - const text = new $Text(element); - this.$elementList.add(text); - (text as Mutable<$Node>).parent = this.$container; - } else { - this.$elementList.add(element); + add(element: $Node, position = -1) { + if (position === -1 || this.childList.size - 1 === position) { + this.childList.add(element); (element as Mutable<$Node>).parent = this.$container; + } else { + const children = [...this.childList] + children.splice(position, 0, element); + this.childList.clear(); + children.forEach(child => this.childList.add(child)); } } remove(element: $Node) { - if (!this.$elementList.has(element)) return this; - this.$elementList.delete(element); + if (!this.childList.has(element)) return this; + this.childList.delete(element); (element as Mutable<$Node>).parent = undefined; return this; } removeAll(render = true) { - this.$elementList.forEach(ele => this.remove(ele)); + this.childList.forEach(ele => this.remove(ele)); if (render) this.render(); } @@ -36,8 +37,8 @@ export class $NodeManager { const array = this.array array.splice(array.indexOf(target), 1, replace); target.remove(); - this.$elementList.clear(); - array.forEach(node => this.$elementList.add(node)); + this.childList.clear(); + array.forEach(node => this.childList.add(node)); return this; } @@ -60,7 +61,7 @@ export class $NodeManager { } } - get array() {return [...this.$elementList.values()]}; + get array() {return [...this.childList.values()]}; get dom() {return this.$container.dom} } \ No newline at end of file diff --git a/lib/$State.ts b/lib/$State.ts index 92d9458..9d48a3b 100644 --- a/lib/$State.ts +++ b/lib/$State.ts @@ -2,28 +2,35 @@ export interface $StateOption { format: (value: T) => string; } export class $State { - readonly value!: T; + protected _value!: T | $State; readonly attributes = new Map>(); + readonly linkStates = new Set<$State>; options: Partial<$StateOption> = {} constructor(value: T, options?: $StateOption) { this.set(value); if (options) this.options = options; } - set(value: T) { - (this as Mutable<$State>).value = value; + set(value: T | $State) { + this._value = value; + if (value instanceof $State) value.linkStates.add(this as any); + this.update(); + this.linkStates.forEach($state => $state.update()); + } + + protected update() { // update element content for eatch attributes for (const [node, attrList] of this.attributes.entries()) { for (const attr of attrList) { //@ts-expect-error if (node[attr] instanceof Function) { //@ts-expect-error - if (this.options.format) node[attr](this.options.format(value)) + if (this.options.format) node[attr](this.options.format(this.value)) //@ts-expect-error - else node[attr](value) + else node[attr](this.value) } else if (attr in node) { //@ts-expect-error - node[attr] = value + node[attr] = this.value } } } @@ -56,6 +63,10 @@ export class $State { } return data; } + + get value(): T { + return this._value instanceof $State ? this._value.value as T : this._value; + } }; -export type $StateArgument = T | $State | undefined; \ No newline at end of file +export type $StateArgument = $State | undefined | (T extends (infer R)[] ? R : T); \ No newline at end of file diff --git a/lib/node/$Container.ts b/lib/node/$Container.ts index 4706276..ac4ba9b 100644 --- a/lib/node/$Container.ts +++ b/lib/node/$Container.ts @@ -20,18 +20,22 @@ export class $Container extends $HTMLElemen this.insert(children); })} + private __position_cursor = 0; /**Insert element to this element */ - insert(children: $ContainerContentBuilder): this { return $.fluent(this, arguments, () => this, () => { + insert(children: $ContainerContentBuilder, position = -1): this { return $.fluent(this, arguments, () => this, () => { if (children instanceof Function) children = children(this); children = $.orArrayResolve(children); + this.__position_cursor = position < 0 ? this.children.array.length + position : position; for (const child of children) { - if (child === undefined) continue; - if (child instanceof Array) this.insert(child) + if (child === undefined || child === null) continue; + if (child instanceof Array) this.insert(child, this.__position_cursor); + else if (typeof child === 'string') this.children.add(new $Text(child), position); 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.add(ele, position); + } else this.children.add(child, position); + this.__position_cursor += 1; } this.children.render(); })} @@ -61,4 +65,4 @@ export class $Container extends $HTMLElemen } export type $ContainerContentBuilder

= OrMatrix<$ContainerContentType> | (($node: P) => OrMatrix<$ContainerContentType>) -export type $ContainerContentType = $Node | string | undefined | $State \ No newline at end of file +export type $ContainerContentType = $Node | string | undefined | $State | null \ No newline at end of file diff --git a/lib/node/$Media.ts b/lib/node/$Media.ts new file mode 100644 index 0000000..a3d62cb --- /dev/null +++ b/lib/node/$Media.ts @@ -0,0 +1,106 @@ +import { $State, $StateArgument } from "../$State"; +import { $Element, $ElementOptions } from "./$Element"; + +export interface $MediaOptions extends $ElementOptions {} +export class $Media extends $Element { + constructor(tagname: string, options?: $MediaOptions) { + super(tagname, options); + } + + autoplay(): boolean; + autoplay(autoplay: $StateArgument): this; + autoplay(autoplay?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.autoplay, () => $.set(this.dom, 'autoplay', autoplay))} + + get buffered() { return this.dom.buffered } + + controls(): boolean; + controls(controls: $StateArgument): this; + controls(controls?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.controls, () => $.set(this.dom, 'controls', controls))} + + crossOrigin(): string | null; + crossOrigin(crossOrigin: $StateArgument): this; + crossOrigin(crossOrigin?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.crossOrigin, () => $.set(this.dom, 'crossOrigin', crossOrigin))} + + get currentSrc() { return this.dom.currentSrc }; + + currentTime(): number; + currentTime(currentTime: $StateArgument): this; + currentTime(currentTime?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.currentTime, () => $.set(this.dom, 'currentTime', currentTime))} + + defaultMuted(): boolean; + defaultMuted(defaultMuted: $StateArgument): this; + defaultMuted(defaultMuted?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.defaultMuted, () => $.set(this.dom, 'defaultMuted', defaultMuted))} + + defaultPlaybackRate(): number; + defaultPlaybackRate(defaultPlaybackRate: $StateArgument): this; + defaultPlaybackRate(defaultPlaybackRate?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.defaultPlaybackRate, () => $.set(this.dom, 'defaultPlaybackRate', defaultPlaybackRate))} + + disableRemotePlayback(): boolean; + disableRemotePlayback(disableRemotePlayback: $StateArgument): this; + disableRemotePlayback(disableRemotePlayback?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.disableRemotePlayback, () => $.set(this.dom, 'disableRemotePlayback', disableRemotePlayback))} + + get duration() { return this.dom.duration } + get ended() { return this.dom.ended } + get error() { return this.dom.error } + + loop(): boolean; + loop(loop: $StateArgument): this; + loop(loop?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.loop, () => $.set(this.dom, 'loop', loop))} + + mediaKeys(): MediaKeys | null; + mediaKeys(mediaKeys: $StateArgument<[MediaKeys | null]>): this; + mediaKeys(mediaKeys?: $StateArgument<[MediaKeys | null]>) { return $.fluent(this, arguments, () => this.dom.mediaKeys, () => $.set(this.dom, 'setMediaKeys', [mediaKeys]))} + + muted(): boolean; + muted(muted: $StateArgument): this; + muted(muted?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.muted, () => $.set(this.dom, 'muted', muted))} + + get networkState() { return this.dom.networkState } + get paused() { return this.dom.paused } + + playbackRate(): number; + playbackRate(playbackRate: $StateArgument): this; + playbackRate(playbackRate?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.playbackRate, () => $.set(this.dom, 'playbackRate', playbackRate))} + + get played() { return this.dom.played } + + preload(): this['dom']['preload']; + preload(preload: $StateArgument): this; + preload(preload?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.preload, () => $.set(this.dom, 'preload', preload))} + + preservesPitch(): boolean; + preservesPitch(preservesPitch: $StateArgument): this; + preservesPitch(preservesPitch?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.preservesPitch, () => $.set(this.dom, 'preservesPitch', preservesPitch))} + + get readyState() { return this.dom.readyState } + get remote() { return this.dom.remote } + get seekable() { return this.dom.seekable } + get seeking() { return this.dom.seeking } + + sinkId(): string; + sinkId(sinkId: $StateArgument<[string]>): this; + sinkId(sinkId?: $StateArgument<[string]>) { return $.fluent(this, arguments, () => this.dom.sinkId, () => $.set(this.dom, 'setSinkId', [sinkId]))} + + src(): string; + src(src: $StateArgument): this; + src(src?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.src, () => $.set(this.dom, 'src', src))} + + srcObject(): MediaProvider | null; + srcObject(srcObject: $StateArgument): this; + srcObject(srcObject?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.srcObject, () => $.set(this.dom, 'srcObject', srcObject))} + + get textTracks() { return this.dom.textTracks } + + volume(): number; + volume(volume: $StateArgument): this; + volume(volume?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.volume, () => $.set(this.dom, 'volume', volume))} + + addTextTrack(kind: TextTrackKind, label?: string, language?: string) { return this.dom.addTextTrack(kind, label, language)} + canPlayType(type: string) { return this.dom.canPlayType(type) } + fastSeek(time: number) { this.dom.fastSeek(time); return this } + load() { this.dom.load(); return this } + pause() { this.dom.pause(); return this } + async play() { await this.dom.play(); return this} + + get isPlaying() { return this.currentTime() > 0 && !this.paused && !this.ended && this.readyState > 2 } +} \ No newline at end of file diff --git a/lib/node/$Video.ts b/lib/node/$Video.ts new file mode 100644 index 0000000..f7a1d3c --- /dev/null +++ b/lib/node/$Video.ts @@ -0,0 +1,37 @@ +import { $StateArgument } from "../$State"; +import { $Media, $MediaOptions } from "./$Media"; + +export interface $VideoOptions extends $MediaOptions {} +export class $Video extends $Media { + constructor(options?: $VideoOptions) { + super('video', options) + } + + disablePictureInPicture(): boolean; + disablePictureInPicture(disablePictureInPicture: $StateArgument): this; + disablePictureInPicture(disablePictureInPicture?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.disablePictureInPicture, () => $.set(this.dom, 'disablePictureInPicture', disablePictureInPicture))} + + height(): number; + height(height: $StateArgument): this; + height(height?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.height, () => $.set(this.dom, 'height', height))} + + width(): number; + width(width: $StateArgument): this; + width(width?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.width, () => $.set(this.dom, 'width', width))} + + playsInline(): boolean; + playsInline(playsInline: $StateArgument): this; + playsInline(playsInline?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.playsInline, () => $.set(this.dom, 'playsInline', playsInline))} + + poster(): string; + poster(poster: $StateArgument): this; + poster(poster?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.poster, () => $.set(this.dom, 'poster', poster))} + + get videoHeight() { return this.dom.videoHeight } + get videoWidth() { return this.dom.videoWidth } + + cancelVideoFrameCallback(handle: number) { this.dom.cancelVideoFrameCallback(handle); return this } + getVideoPlaybackQuality() { return this.dom.getVideoPlaybackQuality() } + requestPictureInPicture() { return this.dom.requestPictureInPicture() } + requestVideoFrameCallback(callback: VideoFrameRequestCallback) { return this.dom.requestVideoFrameCallback(callback) } +} \ No newline at end of file diff --git a/package.json b/package.json index fa25bde..7f68cda 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,21 @@ { "name": "elexis", "description": "Build Web in Native JavaScript Syntax", - "version": "0.2.3", + "version": "0.2.4", "author": { "name": "defaultkavy", "email": "defaultkavy@gmail.com", - "url": "https://github.com/defaultkavy" + "url": "https://git.defaultkavy.com/defaultkavy" }, "repository": { "type": "git", - "url": "git+https://github.com/defaultkavy/elexis.git" + "url": "git+https://git.defaultkavy.com/defaultkavy/elexis.git" }, "module": "index.ts", "bugs": { - "url": "https://github.com/defaultkavy/elexis/issues" + "url": "https://git.defaultkavy.com/defaultkavy/elexis/issues" }, - "homepage": "https://github.com/defaultkavy/elexis", + "homepage": "https://git.defaultkavy.com/defaultkavy/elexis", "keywords": ["web", "front-end", "lib", "fluent", "framework"], "license": "ISC", "type": "module"