update: $Async.await() allow $ContainerContentType object.
update: $Container.insert() and .content() can handler Promise object and Async function.
update: $Select.value() will sync value with $State.value when update.
update: Array.prototype.detype will exclude `undefined` and `void` automatically.
new: $.events function, create EventManager in faster way.
fork: move $View to extensions repository
new: $.call function, just a simple function caller.
This commit is contained in:
defaultkavy 2024-10-03 23:21:55 +08:00
parent c5b99d8835
commit a57246e6e1
Signed by: defaultkavy
GPG Key ID: DFBB22C4E69D7826
12 changed files with 73 additions and 83 deletions

View File

@ -1,4 +1,4 @@
import { $State, $StateArgument, $StateOption } from "./index";
import { $EventManager, $State, $StateArgument, $StateOption } from "./index";
import { $Node } from "./lib/node/$Node"
import { $Document } from "./lib/node/$Document"
import { $Anchor } from "./lib/node/$Anchor";
@ -11,7 +11,6 @@ import { $Label } from "./lib/node/$Label";
import { $Image } from "./lib/node/$Image";
import { $Canvas } from "./lib/node/$Canvas";
import { $Dialog } from "./lib/node/$Dialog";
import { $View } from "./lib/node/$View";
import { $Select } from "./lib/node/$Select";
import { $Option } from "./lib/node/$Option";
import { $OptGroup } from "./lib/node/$OptGroup";
@ -56,8 +55,9 @@ export function $(resolver: any) {
}
export namespace $ {
export let anchorHandler: null | (($a: $Anchor, e: Event) => void) = null;
export let anchorPreventDefault: boolean = false;
export const TagNameElementMap = {
'html': $Container,
'head': $Container,
'document': $Document,
'body': $Container,
'a': $Anchor,
@ -84,7 +84,6 @@ export namespace $ {
'img': $Image,
'dialog': $Dialog,
'canvas': $Canvas,
'view': $View,
'select': $Select,
'option': $Option,
'optgroup': $OptGroup,
@ -192,9 +191,7 @@ export namespace $ {
})
}
export function rem(amount: number = 1) {
return parseInt(getComputedStyle(document.documentElement).fontSize) * amount
}
export function rem(amount: number = 1) { return parseInt(getComputedStyle(document.documentElement).fontSize) * amount }
export function html(html: string) {
const body = new DOMParser().parseFromString(html, 'text/html').body;
@ -240,6 +237,10 @@ export namespace $ {
Object.assign($.TagNameElementMap, {[string]: node});
return $.TagNameElementMap;
}
export function events<N extends string>(...eventname: N[]) { return new $EventManager<{[keys in N]: any[]}>().register(...eventname) }
export function call<T>(fn: () => T): T { return fn() }
}
type BuildNodeFunction = (...args: any[]) => $Node;
type BuilderSelfFunction<K extends $Node> = (self: K) => void;

View File

@ -1,7 +1,7 @@
declare global {
var $: import('./$index').$;
interface Array<T> {
detype<F extends undefined | null, O>(...types: F[]): Array<Exclude<T, F>>
detype<F extends any, O>(...types: F[]): Array<Exclude<T, F | undefined | void>>
}
type OrMatrix<T> = T | OrMatrix<T>[];
type OrArray<T> = T | T[];
@ -23,11 +23,11 @@ declare global {
$: import('./lib/node/$Node').$Node;
}
}
Array.prototype.detype = function <T extends undefined | null, O>(this: O[], ...types: T[]) {
Array.prototype.detype = function <T extends any, 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 true; else return false;
}) as Exclude<O, T>[];
}) as Exclude<O, T | undefined | void>[];
}
export * from "./$index";
export * from "./lib/node/$Node";
@ -40,7 +40,6 @@ export * from "./lib/node/$Button";
export * from "./lib/node/$Form";
export * from "./lib/$EventManager";
export * from "./lib/$State";
export * from "./lib/node/$View";
export * from "./lib/node/$Select";
export * from "./lib/node/$Option";
export * from "./lib/node/$OptGroup";

View File

@ -8,7 +8,7 @@ export abstract class $EventMethod<EM> {
once<K extends keyof EM>(type: K, callback: (...args: EM[K]) => any) { this.events.once(type, callback); return this }
}
export class $EventManager<EM> {
private eventMap = new Map<string, $Event>();
eventMap = new Map<string, $Event>();
register(...names: string[]) {
names.forEach(name => {
const event = new $Event(name);

View File

@ -7,8 +7,10 @@ export class $Anchor extends $Container<HTMLAnchorElement> {
super('a', options);
// Link Handler event
this.dom.addEventListener('click', e => {
if ($.anchorPreventDefault) e.preventDefault();
if ($.anchorHandler && !!this.href()) $.anchorHandler(this, e)
if ($.anchorHandler && !!this.href()) {
e.preventDefault();
$.anchorHandler(this, e);
}
})
}
/**Set URL of anchor element. */

View File

@ -1,5 +1,7 @@
import { $Container, $ContainerOptions } from "./$Container";
import { $State } from "../$State";
import { $Container, $ContainerContentType, $ContainerOptions } from "./$Container";
import { $Node } from "./$Node";
import { $Text } from "./$Text";
export interface $AsyncNodeOptions extends $ContainerOptions {}
export class $Async<N extends $Node = $Node> extends $Container {
#loaded: boolean = false;
@ -7,14 +9,22 @@ export class $Async<N extends $Node = $Node> extends $Container {
super('async', options)
}
await<T extends $Node = $Node>($node: Promise<T>) {
$node.then($node => this._loaded($node));
await<T extends $Node>($node: Promise<T | $ContainerContentType> | (($self: this) => Promise<T | $ContainerContentType>)) {
if ($node instanceof Function) $node(this).then($node => this._loaded($node));
else $node.then($node => this._loaded($node));
return this as $Async<T>
}
protected _loaded($node: $Node) {
protected _loaded($node: $ContainerContentType) {
this.#loaded = true;
this.replace($node)
if (typeof $node === 'string') this.replace(new $Text($node));
else if ($node instanceof $State) {
const ele = new $Text($node.toString());
$node.use(ele, 'content');
this.replace(ele);
}
else if ($node === null || $node === undefined) this.replace(new $Text(String($node)));
else this.replace($node)
this.dom.dispatchEvent(new Event('load'))
}

View File

@ -22,22 +22,29 @@ export class $Container<H extends HTMLElement = HTMLElement> extends $HTMLElemen
private __position_cursor = 0;
/**Insert element to this element */
insert(children: $ContainerContentBuilder<this>, position = -1): this { return $.fluent(this, arguments, () => this, () => {
if (children instanceof Function) children = children(this);
insert(children: $ContainerContentBuilder<this>, position = -1): this { return $.fluent(this, arguments, () => this, async () => {
if (children instanceof Function) children = await children(this); // resolve function
children = $.orArrayResolve(children);
// Set position cursor depend negative or positive number, position will count from last index when position is negative.
this.__position_cursor = position < 0 ? this.children.array.length + position : position;
for (const child of children) {
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);
if (child === undefined || child === null) continue; // skip
if (child instanceof Array) this.insert(child, this.__position_cursor); // insert element group at this position
else if (typeof child === 'string') this.children.add(new $Text(child), position); // turn string into $Text element
else if (child instanceof $State) {
const ele = new $Text(child.toString());
child.use(ele, 'content');
const ele = new $Text(child.toString()); // turn $State object into $Text element
child.use(ele, 'content'); // bind $Text elelment and function name to $State
this.children.add(ele, position);
} else this.children.add(child, position);
this.__position_cursor += 1;
}
this.children.render();
else if (child instanceof Promise) {
const $Async = (await import('./$Async')).$Async; // import $Async avoid extends error
const ele = new $Async().await(child) // using $Async.await resolve promise element
this.children.add(ele, position); // insert $Async element at this position, leave a position for promised element
}
else this.children.add(child, position); // insert $Node element directly
this.__position_cursor += 1; // increase position count
}
this.children.render(); // start to render dom tree
})}
/**Remove all children elemetn from this element */
@ -64,5 +71,6 @@ export class $Container<H extends HTMLElement = HTMLElement> extends $HTMLElemen
scrollLeft(scrollLeft?: $StateArgument<number> | undefined) { return $.fluent(this, arguments, () => this.dom.scrollLeft, () => $.set(this.dom, 'scrollLeft', scrollLeft as any))}
}
export type $ContainerContentBuilder<P extends $Container> = OrMatrix<$ContainerContentType> | (($node: P) => OrMatrix<$ContainerContentType>)
export type $ContainerContentBuilder<P extends $Container> = $ContainerContentGroup | (($node: P) => OrPromise<$ContainerContentGroup>)
export type $ContainerContentGroup = OrMatrix<OrPromise<$ContainerContentType>>
export type $ContainerContentType = $Node | string | undefined | $State<any> | null

View File

@ -4,6 +4,7 @@ export interface $ElementOptions {
id?: string;
class?: string[];
dom?: HTMLElement | SVGElement;
tagname?: string;
}
export class $Element<H extends HTMLElement | SVGElement = HTMLElement> extends $Node<H> {
@ -19,7 +20,7 @@ export class $Element<H extends HTMLElement | SVGElement = HTMLElement> extends
private createDom(tagname: string, options?: $ElementOptions) {
if (options?.dom) return options.dom;
if (tagname === 'svg') return document.createElementNS("http://www.w3.org/2000/svg", "svg");
return document.createElement(tagname);
return document.createElement(options?.tagname ?? tagname);
}
@ -85,8 +86,8 @@ export class $Element<H extends HTMLElement | SVGElement = HTMLElement> extends
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;
if (callback) animation.onfinish = () => callback(animation);
return animation;
}
getAnimations(options?: GetAnimationsOptions) { return this.dom.getAnimations(options) }

View File

@ -1,10 +1,10 @@
import { $Element, $ElementOptions } from "./$Element";
import { $State, $StateArgument } from "../$State";
import { $HTMLElementAPIFilter, $HTMLElementAPIs } from "../$ElementTemplate";
import { $Util } from "../$Util";
import { $HTMLElement, $HTMLElementOptions } from "./$HTMLElement";
export interface $InputOptions extends $ElementOptions {}
export class $Input<T extends string | number = string> extends $Element<HTMLInputElement> {
export interface $InputOptions extends $HTMLElementOptions {}
export class $Input<T extends string | number = string> extends $HTMLElement<HTMLInputElement> {
constructor(options?: $InputOptions) {
super('input', options);
}
@ -117,7 +117,6 @@ export class $Input<T extends string | number = string> extends $Element<HTMLInp
export interface $Input extends $HTMLElementAPIFilter<$Input, 'checkValidity' | 'reportValidity' | 'autocomplete' | 'name' | 'form' | 'required' | 'validationMessage' | 'validity' | 'willValidate' | 'formAction' | 'formEnctype' | 'formMethod' | 'formNoValidate' | 'formTarget'> {}
$Util.mixin($Input, $HTMLElementAPIs.create('checkValidity', 'reportValidity', 'autocomplete', 'name', 'form', 'required', 'validationMessage', 'validity', 'willValidate', 'formAction', 'formEnctype', 'formMethod', 'formNoValidate', 'formTarget'))
export class $NumberInput extends $Input<number> {
constructor(options?: $InputOptions) {
super(options)
@ -169,7 +168,7 @@ export class $FileInput extends $Input<string> {
}
static from($input: $Input) {
return $.mixin($Input, this) as $CheckInput;
return $.mixin($Input, this) as $FileInput;
}
multiple(): boolean;
@ -182,3 +181,4 @@ export class $FileInput extends $Input<string> {
}
export type $InputType<T extends InputType> = T extends 'number' ? $NumberInput : T extends 'radio' | 'checkbox' ? $CheckInput : T extends 'file' ? $FileInput : $Input<string>;
$Util.mixin($Input, [$NumberInput, $CheckInput, $FileInput])

View File

@ -65,4 +65,8 @@ export abstract class $Node<N extends Node = Node> {
if (this instanceof $Element) return true;
else return false;
}
get element(): $Element | null {
if (this instanceof $Element) return this;
else return null;
}
}

View File

@ -1,7 +1,7 @@
import { $Container, $ContainerOptions } from "./$Container";
import { $OptGroup } from "./$OptGroup";
import { $Option } from "./$Option";
import { $StateArgument } from "../$State";
import { $State, $StateArgument } from "../$State";
import { $HTMLElementAPIFilter, $HTMLElementAPIs } from "../$ElementTemplate";
import { $Util } from "../$Util";
@ -31,7 +31,13 @@ export class $Select extends $Container<HTMLSelectElement> {
value(): string;
value(value?: $StateArgument<string> | undefined): this;
value(value?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value))}
value(value?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value as $State<string> | string, (value$) => {
this.on('input', () => {
if (value$.attributes.has(this.dom) === false) return;
if (typeof value$.value === 'string') (value$ as $State<string>).set(`${this.value()}`)
if (typeof value$.value === 'number') (value$ as unknown as $State<number>).set(Number(this.value()))
})
}))}
get labels() { return Array.from(this.dom.labels ?? []).map(label => $(label)) }
}

View File

@ -1,41 +0,0 @@
import { $Container, $ContainerOptions } from "./$Container";
import { $EventManager } from "../$EventManager";
import { $Node } from "./$Node";
export interface $ViewOptions extends $ContainerOptions {}
export class $View extends $Container {
protected view_cache = new Map<string, $Node>();
event = new $EventManager<$ViewEventMap>().register('switch')
content_id: string | null = null;
constructor(options?: $ViewOptions) {
super('view', options);
}
setView(id: string, $node: $Node) {
this.view_cache.set(id, $node);
return this;
}
deleteView(id: string) {
this.view_cache.delete(id);
return this;
}
deleteAllView() {
this.view_cache.clear();
return this;
}
switchView(id: string) {
const target_content = this.view_cache.get(id);
if (target_content === undefined) return this;
this.content(target_content);
this.content_id = id;
this.event.fire('switch', id);
return this;
}
}
export interface $ViewEventMap {
'switch': [id: string]
}

View File

@ -1,7 +1,7 @@
{
"name": "elexis",
"description": "Build Web in Native JavaScript Syntax",
"version": "0.2.4",
"version": "0.2.5",
"author": {
"name": "defaultkavy",
"email": "defaultkavy@gmail.com",