change: $NodeManager.$elementList rename to childList.
update: $Container.insert() and $NodeManager.add() now can insert element with position.
add: $Video element is avaliable.
add: $Media element is avaliable.
update: $State now can set with other $State object.
change: The host of this repository is changed.
This commit is contained in:
defaultkavy 2024-08-29 02:27:14 +08:00
parent 72f617df3c
commit f614ecd5f5
Signed by: defaultkavy
GPG Key ID: 59B28939E8FDD2CD
8 changed files with 205 additions and 41 deletions

View File

@ -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 $<E extends $Element = $Element>(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<H>;
/**
@ -142,27 +145,27 @@ export namespace $ {
* @param handle callback when param `value` is $State object.
* @returns
*/
export function set<O extends Object, K extends keyof O, V>(
export function set<O extends Object, K extends keyof O>(
object: O,
key: K,
value: O[K] extends (...args: any) => any
? (undefined | $StateArgument<Parameters<O[K]>>)
? (undefined | [$StateArgument<Parameters<O[K]>>])
: (undefined | $StateArgument<O[K]>),
handle?: ($state: $State<O[K]>) => 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<T>(value: T, options?: $StateOption<T>) {
return new $State<T>(value, options)
export function state<T>(value: T, options?: $StateOption<T extends $State<infer K> ? K : T>) {
return new $State<T>(value, options as $StateOption<T>) as T extends $State<infer K> ? $State<K> : $State<T>;
}
export async function resize(object: Blob, size: number): Promise<string> {

View File

@ -26,7 +26,7 @@ declare global {
Array.prototype.detype = function <T extends undefined | null, O>(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<O, T>[];
}
export * from "./$index";
@ -48,3 +48,5 @@ export * from "./lib/node/$Textarea";
export * from "./lib/node/$Image";
export * from "./lib/node/$Async";
export * from "./lib/node/$Document";
export * from "./lib/node/$Media";
export * from "./lib/node/$Video";

View File

@ -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}
}

View File

@ -2,28 +2,35 @@ export interface $StateOption<T> {
format: (value: T) => string;
}
export class $State<T> {
readonly value!: T;
protected _value!: T | $State<T>;
readonly attributes = new Map<Object, Set<string | number | symbol>>();
readonly linkStates = new Set<$State<T>>;
options: Partial<$StateOption<T>> = {}
constructor(value: T, options?: $StateOption<T>) {
this.set(value);
if (options) this.options = options;
}
set(value: T) {
(this as Mutable<$State<T>>).value = value;
set(value: T | $State<T>) {
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<T> {
}
return data;
}
get value(): T {
return this._value instanceof $State ? this._value.value as T : this._value;
}
};
export type $StateArgument<T> = T | $State<T> | undefined;
export type $StateArgument<T> = $State<T> | undefined | (T extends (infer R)[] ? R : T);

View File

@ -20,18 +20,22 @@ export class $Container<H extends HTMLElement = HTMLElement> extends $HTMLElemen
this.insert(children);
})}
private __position_cursor = 0;
/**Insert element to this element */
insert(children: $ContainerContentBuilder<this>): this { return $.fluent(this, arguments, () => this, () => {
insert(children: $ContainerContentBuilder<this>, 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<H extends HTMLElement = HTMLElement> extends $HTMLElemen
}
export type $ContainerContentBuilder<P extends $Container> = OrMatrix<$ContainerContentType> | (($node: P) => OrMatrix<$ContainerContentType>)
export type $ContainerContentType = $Node | string | undefined | $State<any>
export type $ContainerContentType = $Node | string | undefined | $State<any> | null

106
lib/node/$Media.ts Normal file
View File

@ -0,0 +1,106 @@
import { $State, $StateArgument } from "../$State";
import { $Element, $ElementOptions } from "./$Element";
export interface $MediaOptions extends $ElementOptions {}
export class $Media<H extends HTMLMediaElement> extends $Element<H> {
constructor(tagname: string, options?: $MediaOptions) {
super(tagname, options);
}
autoplay(): boolean;
autoplay(autoplay: $StateArgument<boolean>): this;
autoplay(autoplay?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.autoplay, () => $.set<HTMLMediaElement, 'autoplay'>(this.dom, 'autoplay', autoplay))}
get buffered() { return this.dom.buffered }
controls(): boolean;
controls(controls: $StateArgument<boolean>): this;
controls(controls?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.controls, () => $.set<HTMLMediaElement, 'controls'>(this.dom, 'controls', controls))}
crossOrigin(): string | null;
crossOrigin(crossOrigin: $StateArgument<string | null>): this;
crossOrigin(crossOrigin?: $StateArgument<string | null>) { return $.fluent(this, arguments, () => this.dom.crossOrigin, () => $.set<HTMLMediaElement, 'crossOrigin'>(this.dom, 'crossOrigin', crossOrigin))}
get currentSrc() { return this.dom.currentSrc };
currentTime(): number;
currentTime(currentTime: $StateArgument<number>): this;
currentTime(currentTime?: $StateArgument<number>) { return $.fluent(this, arguments, () => this.dom.currentTime, () => $.set<HTMLMediaElement, 'currentTime'>(this.dom, 'currentTime', currentTime))}
defaultMuted(): boolean;
defaultMuted(defaultMuted: $StateArgument<boolean>): this;
defaultMuted(defaultMuted?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.defaultMuted, () => $.set<HTMLMediaElement, 'defaultMuted'>(this.dom, 'defaultMuted', defaultMuted))}
defaultPlaybackRate(): number;
defaultPlaybackRate(defaultPlaybackRate: $StateArgument<number>): this;
defaultPlaybackRate(defaultPlaybackRate?: $StateArgument<number>) { return $.fluent(this, arguments, () => this.dom.defaultPlaybackRate, () => $.set<HTMLMediaElement, 'defaultPlaybackRate'>(this.dom, 'defaultPlaybackRate', defaultPlaybackRate))}
disableRemotePlayback(): boolean;
disableRemotePlayback(disableRemotePlayback: $StateArgument<boolean>): this;
disableRemotePlayback(disableRemotePlayback?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.disableRemotePlayback, () => $.set<HTMLMediaElement, 'disableRemotePlayback'>(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<boolean>): this;
loop(loop?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.loop, () => $.set<HTMLMediaElement, 'loop'>(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<HTMLMediaElement, 'setMediaKeys'>(this.dom, 'setMediaKeys', [mediaKeys]))}
muted(): boolean;
muted(muted: $StateArgument<boolean>): this;
muted(muted?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.muted, () => $.set<HTMLMediaElement, 'muted'>(this.dom, 'muted', muted))}
get networkState() { return this.dom.networkState }
get paused() { return this.dom.paused }
playbackRate(): number;
playbackRate(playbackRate: $StateArgument<number>): this;
playbackRate(playbackRate?: $StateArgument<number>) { return $.fluent(this, arguments, () => this.dom.playbackRate, () => $.set<HTMLMediaElement, 'playbackRate'>(this.dom, 'playbackRate', playbackRate))}
get played() { return this.dom.played }
preload(): this['dom']['preload'];
preload(preload: $StateArgument<this['dom']['preload']>): this;
preload(preload?: $StateArgument<this['dom']['preload']>) { return $.fluent(this, arguments, () => this.dom.preload, () => $.set<HTMLMediaElement, 'preload'>(this.dom, 'preload', preload))}
preservesPitch(): boolean;
preservesPitch(preservesPitch: $StateArgument<boolean>): this;
preservesPitch(preservesPitch?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.preservesPitch, () => $.set<HTMLMediaElement, 'preservesPitch'>(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<HTMLMediaElement, 'setSinkId'>(this.dom, 'setSinkId', [sinkId]))}
src(): string;
src(src: $StateArgument<string>): this;
src(src?: $StateArgument<string>) { return $.fluent(this, arguments, () => this.dom.src, () => $.set<HTMLMediaElement, 'src'>(this.dom, 'src', src))}
srcObject(): MediaProvider | null;
srcObject(srcObject: $StateArgument<MediaProvider | null>): this;
srcObject(srcObject?: $StateArgument<MediaProvider | null>) { return $.fluent(this, arguments, () => this.dom.srcObject, () => $.set<HTMLMediaElement, 'srcObject'>(this.dom, 'srcObject', srcObject))}
get textTracks() { return this.dom.textTracks }
volume(): number;
volume(volume: $StateArgument<number>): this;
volume(volume?: $StateArgument<number>) { return $.fluent(this, arguments, () => this.dom.volume, () => $.set<HTMLMediaElement, 'volume'>(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 }
}

37
lib/node/$Video.ts Normal file
View File

@ -0,0 +1,37 @@
import { $StateArgument } from "../$State";
import { $Media, $MediaOptions } from "./$Media";
export interface $VideoOptions extends $MediaOptions {}
export class $Video extends $Media<HTMLVideoElement> {
constructor(options?: $VideoOptions) {
super('video', options)
}
disablePictureInPicture(): boolean;
disablePictureInPicture(disablePictureInPicture: $StateArgument<boolean>): this;
disablePictureInPicture(disablePictureInPicture?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.disablePictureInPicture, () => $.set(this.dom, 'disablePictureInPicture', disablePictureInPicture))}
height(): number;
height(height: $StateArgument<number>): this;
height(height?: $StateArgument<number>) { return $.fluent(this, arguments, () => this.dom.height, () => $.set(this.dom, 'height', height))}
width(): number;
width(width: $StateArgument<number>): this;
width(width?: $StateArgument<number>) { return $.fluent(this, arguments, () => this.dom.width, () => $.set(this.dom, 'width', width))}
playsInline(): boolean;
playsInline(playsInline: $StateArgument<boolean>): this;
playsInline(playsInline?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.playsInline, () => $.set(this.dom, 'playsInline', playsInline))}
poster(): string;
poster(poster: $StateArgument<string>): this;
poster(poster?: $StateArgument<string>) { 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) }
}

View File

@ -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"