v0.2.5
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:
parent
c5b99d8835
commit
a57246e6e1
15
$index.ts
15
$index.ts
@ -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;
|
||||
|
7
index.ts
7
index.ts
@ -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";
|
||||
|
@ -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);
|
||||
|
@ -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. */
|
||||
|
@ -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'))
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
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();
|
||||
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
|
@ -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) }
|
||||
|
@ -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])
|
@ -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;
|
||||
}
|
||||
}
|
@ -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)) }
|
||||
}
|
||||
|
@ -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]
|
||||
}
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user