v0.0.7
change: Router.open(), .replace() .back(), .events change to static add: $.open(), $.replace(), $.back() add: $.html() convert html string to $Element update: $State support output format update: $Util.from() convert children node to $Element change: $Node.parent change to getter change: using $StateArgument<T> new: $SVGElement, $HTMLElement new: $AsyncNode add: $Image.load()
This commit is contained in:
parent
4b3c32c88d
commit
b3f28dbf86
116
$index.ts
116
$index.ts
@ -1,41 +1,47 @@
|
||||
import { $Node, $State } from "./index";
|
||||
import { $Anchor } from "./lib/$Anchor";
|
||||
import { $Button } from "./lib/$Button";
|
||||
import { $Form } from "./lib/$Form";
|
||||
import { $Input } from "./lib/$Input";
|
||||
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";
|
||||
import { $View } from "./lib/$View";
|
||||
import { $Select } from "./lib/$Select";
|
||||
import { $Option } from "./lib/$Option";
|
||||
import { $OptGroup } from "./lib/$OptGroup";
|
||||
import { $Textarea } from "./lib/$Textarea";
|
||||
import { $Node, $State, $StateOption } from "./index";
|
||||
import { $Anchor } from "./lib/node/$Anchor";
|
||||
import { $Button } from "./lib/node/$Button";
|
||||
import { $Form } from "./lib/node/$Form";
|
||||
import { $Input } from "./lib/node/$Input";
|
||||
import { $Container } from "./lib/node/$Container";
|
||||
import { $Element } from "./lib/node/$Element";
|
||||
import { $Label } from "./lib/node/$Label";
|
||||
import { Router } from "./lib/router/Router";
|
||||
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";
|
||||
import { $Textarea } from "./lib/node/$Textarea";
|
||||
import { $Util } from "./lib/$Util";
|
||||
import { $HTMLElement } from "./lib/node/$HTMLElement";
|
||||
import { $AsyncNode } from "./lib/node/$AsyncNode";
|
||||
|
||||
export type $ = typeof $;
|
||||
export function $<E extends $Element = $Element>(query: `::${string}`): E[];
|
||||
export function $<E extends $Element = $Element>(query: `:${string}`): E | null;
|
||||
export function $(element: null): null;
|
||||
export function $<K extends keyof $.TagNameTypeMap>(resolver: K): $.TagNameTypeMap[K];
|
||||
export function $<K extends string>(resolver: K): $Container;
|
||||
export function $<H extends HTMLElement>(htmlElement: H): $.HTMLElementTo$ElementMap<H>;
|
||||
export function $<H extends HTMLElement>(htmlElement: H): $.$HTMLElementMap<H>;
|
||||
export function $<H extends Element>(element: H): $Element;
|
||||
export function $<N extends $Node>(node: N): N;
|
||||
export function $<H extends EventTarget>(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 (resolver instanceof $Node) return resolver;
|
||||
if (typeof resolver === 'string') {
|
||||
if (resolver.startsWith('::')) return Array.from(document.querySelectorAll(resolver.replace(/^::/, ''))).map(dom => $(dom));
|
||||
else if (resolver.startsWith(':')) return $(document.querySelector(resolver.replace(/^:/, '')));
|
||||
else if (resolver in $.TagNameElementMap) {
|
||||
const instance = $.TagNameElementMap[resolver as keyof typeof $.TagNameElementMap]
|
||||
switch (instance) {
|
||||
case $Element: return new $Element(resolver);
|
||||
case $HTMLElement: return new $HTMLElement(resolver);
|
||||
case $Anchor: return new $Anchor();
|
||||
case $Container: return new $Container(resolver);
|
||||
case $Input: return new $Input();
|
||||
@ -50,20 +56,22 @@ export function $(resolver: any) {
|
||||
case $Option: return new $Option();
|
||||
case $OptGroup: return new $OptGroup();
|
||||
case $Textarea: return new $Textarea();
|
||||
case $AsyncNode: return new $AsyncNode();
|
||||
}
|
||||
} else return new $Container(resolver);
|
||||
}
|
||||
if (resolver instanceof HTMLElement || resolver instanceof Text) {
|
||||
if (resolver instanceof HTMLElement || resolver instanceof Text || resolver instanceof SVGElement) {
|
||||
if (resolver.$) return resolver.$;
|
||||
else return $Node.from(resolver);
|
||||
else return $Util.from(resolver);
|
||||
}
|
||||
throw '$: NOT SUPPORT TARGET ELEMENT TYPE'
|
||||
throw `$: NOT SUPPORT TARGET ELEMENT TYPE ('${resolver}')`
|
||||
}
|
||||
export namespace $ {
|
||||
export let anchorHandler: null | ((url: string, e: Event) => void) = null;
|
||||
export let anchorHandler: null | (($a: $Anchor, e: Event) => void) = null;
|
||||
export let anchorPreventDefault: boolean = false;
|
||||
export const routers = new Set<Router>;
|
||||
export const TagNameElementMap = {
|
||||
'body': $Container,
|
||||
'a': $Anchor,
|
||||
'p': $Container,
|
||||
'pre': $Container,
|
||||
@ -92,7 +100,8 @@ export namespace $ {
|
||||
'select': $Select,
|
||||
'option': $Option,
|
||||
'optgroup': $OptGroup,
|
||||
'textarea': $Textarea
|
||||
'textarea': $Textarea,
|
||||
'async': $AsyncNode,
|
||||
}
|
||||
export type TagNameTypeMap = {
|
||||
[key in keyof typeof $.TagNameElementMap]: InstanceType<typeof $.TagNameElementMap[key]>;
|
||||
@ -100,7 +109,7 @@ export namespace $ {
|
||||
export type ContainerTypeTagName = Exclude<keyof TagNameTypeMap, 'input'>;
|
||||
export type SelfTypeTagName = 'input';
|
||||
|
||||
export type HTMLElementTo$ElementMap<H extends HTMLElement> =
|
||||
export type $HTMLElementMap<H extends HTMLElement> =
|
||||
H extends HTMLLabelElement ? $Label
|
||||
: H extends HTMLInputElement ? $Input
|
||||
: H extends HTMLAnchorElement ? $Anchor
|
||||
@ -116,6 +125,10 @@ export namespace $ {
|
||||
: H extends HTMLTextAreaElement ? $Textarea
|
||||
: $Container<H>;
|
||||
|
||||
export function open(path: string | URL | undefined) { return Router.open(path) }
|
||||
export function replace(path: string | URL | undefined) { return Router.replace(path) }
|
||||
export function back() { return Router.back() }
|
||||
|
||||
/**
|
||||
* A helper for fluent method design. Return the `instance` object when arguments length not equal 0. Otherwise, return the `value`.
|
||||
* @param instance The object to return when arguments length not equal 0.
|
||||
@ -135,41 +148,34 @@ export namespace $ {
|
||||
else return [multable];
|
||||
}
|
||||
|
||||
export function mixin(target: any, constructors: OrArray<any>) {
|
||||
orArrayResolve(constructors).forEach(constructor => {
|
||||
Object.getOwnPropertyNames(constructor.prototype).forEach(name => {
|
||||
if (name === 'constructor') return;
|
||||
Object.defineProperty(
|
||||
target.prototype,
|
||||
name,
|
||||
Object.getOwnPropertyDescriptor(constructor.prototype, name) || Object.create(null)
|
||||
)
|
||||
})
|
||||
})
|
||||
return target;
|
||||
}
|
||||
export function mixin(target: any, constructors: OrArray<any>) { return $Util.mixin(target, constructors) }
|
||||
/**
|
||||
* A helper for $State.set() which apply value to target.
|
||||
* A helper for undefined able value and $State.set() which apply value to target.
|
||||
* @param object Target object.
|
||||
* @param key The key of target object.
|
||||
* @param value Value of target property or parameter of method(Using Tuple to apply parameter).
|
||||
* @param methodKey Variant key name when apply value on $State.set()
|
||||
* @returns
|
||||
*/
|
||||
export function set<O, K extends keyof O>(object: O, key: K, value: O[K] extends (...args: any) => any ? (Parameters<O[K]> | $State<Parameters<O[K]>>) : O[K] | undefined | $State<O[K]>, methodKey?: string) {
|
||||
if (value === undefined) return;
|
||||
if (value instanceof $State && object instanceof Node) {
|
||||
value.use(object.$, methodKey ?? key as any);
|
||||
const prop = object[key];
|
||||
if (prop instanceof Function) prop(value.value);
|
||||
else object[key] = value.value;
|
||||
return;
|
||||
}
|
||||
object[key] = value as any;
|
||||
export function set<O, K extends keyof O>(
|
||||
object: O, key: K,
|
||||
value: O[K] extends (...args: any) => any
|
||||
? (Parameters<O[K]> | $State<Parameters<O[K]> | undefined>)
|
||||
: (O[K] | undefined | $State<O[K] | undefined>),
|
||||
methodKey?: string) {
|
||||
if (value === undefined) return;
|
||||
if (value instanceof $State && object instanceof Node) {
|
||||
value.use(object.$, methodKey ?? key as any);
|
||||
if (object[key] instanceof Function) (object[key] as Function)(value)
|
||||
else object[key] = value.value;
|
||||
return;
|
||||
}
|
||||
if (object[key] instanceof Function) (object[key] as Function)(value);
|
||||
else object[key] = value as any;
|
||||
}
|
||||
|
||||
export function state<T>(value: T) {
|
||||
return new $State<T>(value)
|
||||
export function state<T>(value: T, options?: $StateOption<T>) {
|
||||
return new $State<T>(value, options)
|
||||
}
|
||||
|
||||
export async function resize(object: Blob, size: number): Promise<string> {
|
||||
@ -200,6 +206,11 @@ export namespace $ {
|
||||
return parseInt(getComputedStyle(document.documentElement).fontSize) * amount
|
||||
}
|
||||
|
||||
export function html(html: string) {
|
||||
const body = new DOMParser().parseFromString(html, 'text/html').body;
|
||||
return Array.from(body.children).map(child => $(child))
|
||||
}
|
||||
|
||||
/**Build multiple element in once. */
|
||||
export function builder<F extends BuildNodeFunction, R extends ReturnType<F>>(bulder: F, params: [...Parameters<F>][], callback?: BuilderSelfFunction<R>): R[]
|
||||
export function builder<F extends BuildNodeFunction, R extends ReturnType<F>>(bulder: [F, ...Parameters<F>], size: number, callback?: BuilderSelfFunction<R>): R[]
|
||||
@ -237,5 +248,4 @@ export namespace $ {
|
||||
}
|
||||
type BuildNodeFunction = (...args: any[]) => $Node;
|
||||
type BuilderSelfFunction<K extends $Node> = (self: K) => void;
|
||||
globalThis.$ = $;
|
||||
|
||||
globalThis.$ = $;
|
32
index.ts
32
index.ts
@ -20,7 +20,7 @@ declare global {
|
||||
type ImageLoading = "eager" | "lazy";
|
||||
type ContructorType<T> = { new (...args: any[]): T }
|
||||
interface Node {
|
||||
$: import('./lib/$Node').$Node;
|
||||
$: import('./lib/node/$Node').$Node;
|
||||
}
|
||||
}
|
||||
Array.prototype.detype = function <T extends undefined | null, O>(this: O[], ...types: T[]) {
|
||||
@ -30,20 +30,22 @@ Array.prototype.detype = function <T extends undefined | null, O>(this: O[], ...
|
||||
}) as Exclude<O, T>[];
|
||||
}
|
||||
export * from "./$index";
|
||||
export * from "./lib/Router/Route";
|
||||
export * from "./lib/Router/Router";
|
||||
export * from "./lib/$Node";
|
||||
export * from "./lib/$Anchor";
|
||||
export * from "./lib/$Element";
|
||||
export * from "./lib/router/Route";
|
||||
export * from "./lib/router/Router";
|
||||
export * from "./lib/node/$Node";
|
||||
export * from "./lib/node/$Anchor";
|
||||
export * from "./lib/node/$Element";
|
||||
export * from "./lib/$NodeManager";
|
||||
export * from "./lib/$Text";
|
||||
export * from "./lib/$Container";
|
||||
export * from "./lib/$Button";
|
||||
export * from "./lib/$Form";
|
||||
export * from "./lib/node/$Text";
|
||||
export * from "./lib/node/$Container";
|
||||
export * from "./lib/node/$Button";
|
||||
export * from "./lib/node/$Form";
|
||||
export * from "./lib/$EventManager";
|
||||
export * from "./lib/$State";
|
||||
export * from "./lib/$View";
|
||||
export * from "./lib/$Select";
|
||||
export * from "./lib/$Option";
|
||||
export * from "./lib/$OptGroup";
|
||||
export * from "./lib/$Textarea";
|
||||
export * from "./lib/node/$View";
|
||||
export * from "./lib/node/$Select";
|
||||
export * from "./lib/node/$Option";
|
||||
export * from "./lib/node/$OptGroup";
|
||||
export * from "./lib/node/$Textarea";
|
||||
export * from "./lib/node/$Image";
|
||||
export * from "./lib/node/$AsyncNode";
|
@ -1,11 +1,11 @@
|
||||
export abstract class $EventMethod<EM> {
|
||||
abstract events: $EventManager<EM>;
|
||||
//@ts-expect-error
|
||||
on<K extends keyof EM>(type: K, callback: (...args: EM[K]) => void) { this.events.on(type, callback); return this }
|
||||
on<K extends keyof EM>(type: K, callback: (...args: EM[K]) => any) { this.events.on(type, callback); return this }
|
||||
//@ts-expect-error
|
||||
off<K extends keyof EM>(type: K, callback: (...args: EM[K]) => void) { this.events.off(type, callback); return this }
|
||||
off<K extends keyof EM>(type: K, callback: (...args: EM[K]) => any) { this.events.off(type, callback); return this }
|
||||
//@ts-expect-error
|
||||
once<K extends keyof EM>(type: K, callback: (...args: EM[K]) => void) { this.events.once(type, callback); return this }
|
||||
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>();
|
||||
@ -25,17 +25,17 @@ export class $EventManager<EM> {
|
||||
return this
|
||||
}
|
||||
//@ts-expect-error
|
||||
on<K extends keyof EM>(type: K, callback: (...args: EM[K]) => void) {
|
||||
on<K extends keyof EM>(type: K, callback: (...args: EM[K]) => any) {
|
||||
this.get(type).add(callback);
|
||||
return this
|
||||
}
|
||||
//@ts-expect-error
|
||||
off<K extends keyof EM>(type: K, callback: (...args: EM[K]) => void) {
|
||||
off<K extends keyof EM>(type: K, callback: (...args: EM[K]) => any) {
|
||||
this.get(type).delete(callback);
|
||||
return this
|
||||
}
|
||||
//@ts-expect-error
|
||||
once<K extends keyof EM>(type: K, callback: (...args: EM[K]) => void) {
|
||||
once<K extends keyof EM>(type: K, callback: (...args: EM[K]) => any) {
|
||||
//@ts-expect-error
|
||||
const onceFn = (...args: EM[K]) => {
|
||||
this.get(type).delete(onceFn);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { $Container } from "./$Container";
|
||||
import { $Node } from "./$Node";
|
||||
import { $Text } from "./$Text";
|
||||
import { $Container } from "./node/$Container";
|
||||
import { $Node } from "./node/$Node";
|
||||
import { $Text } from "./node/$Text";
|
||||
|
||||
export class $NodeManager {
|
||||
#container: $Container;
|
||||
@ -12,10 +12,8 @@ export class $NodeManager {
|
||||
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 {
|
||||
(element as Mutable<$Node>).parent = this.#container;
|
||||
this.elementList.add(element);
|
||||
}
|
||||
}
|
||||
@ -23,7 +21,6 @@ export class $NodeManager {
|
||||
remove(element: $Node) {
|
||||
if (!this.elementList.has(element)) return this;
|
||||
this.elementList.delete(element);
|
||||
(element as Mutable<$Node>).parent = undefined;
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -39,8 +36,6 @@ export class $NodeManager {
|
||||
})
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -1,17 +0,0 @@
|
||||
import { $Container, $ContainerOptions } from "./$Container";
|
||||
import { $State } from "./$State";
|
||||
|
||||
export interface $OptGroupOptions extends $ContainerOptions {}
|
||||
export class $OptGroup extends $Container<HTMLOptGroupElement> {
|
||||
constructor(options?: $OptGroupOptions) {
|
||||
super('optgroup', options);
|
||||
}
|
||||
|
||||
disabled(): boolean;
|
||||
disabled(disabled: boolean | $State<boolean>): this;
|
||||
disabled(disabled?: boolean | $State<boolean>) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))}
|
||||
|
||||
label(): string;
|
||||
label(label: string | $State<string>): this;
|
||||
label(label?: string | $State<string>) { return $.fluent(this, arguments, () => this.dom.label, () => $.set(this.dom, 'label', label))}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
import { $Container, $ContainerOptions } from "./$Container";
|
||||
import { $State } from "./$State";
|
||||
|
||||
export interface $OptionOptions extends $ContainerOptions {}
|
||||
export class $Option extends $Container<HTMLOptionElement> {
|
||||
constructor(options?: $OptionOptions) {
|
||||
super('option', options);
|
||||
}
|
||||
|
||||
defaultSelected(): boolean;
|
||||
defaultSelected(defaultSelected: boolean | $State<boolean>): this;
|
||||
defaultSelected(defaultSelected?: boolean | $State<boolean>) { return $.fluent(this, arguments, () => this.dom.defaultSelected, () => $.set(this.dom, 'defaultSelected', defaultSelected))}
|
||||
|
||||
disabled(): boolean;
|
||||
disabled(disabled: boolean | $State<boolean>): this;
|
||||
disabled(disabled?: boolean | $State<boolean>) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))}
|
||||
|
||||
label(): string;
|
||||
label(label: string | $State<string>): this;
|
||||
label(label?: string | $State<string>) { return $.fluent(this, arguments, () => this.dom.label, () => $.set(this.dom, 'label', label))}
|
||||
|
||||
selected(): boolean;
|
||||
selected(selected: boolean | $State<boolean>): this;
|
||||
selected(selected?: boolean | $State<boolean>) { return $.fluent(this, arguments, () => this.dom.selected, () => $.set(this.dom, 'selected', selected))}
|
||||
|
||||
text(): string;
|
||||
text(text: string | $State<string>): this;
|
||||
text(text?: string | $State<string>) { return $.fluent(this, arguments, () => this.dom.text, () => $.set(this.dom, 'text', text))}
|
||||
|
||||
value(): string;
|
||||
value(value: string | $State<string>): this;
|
||||
value(value?: string | $State<string>) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value))}
|
||||
|
||||
get form() { return this.dom.form ? $(this.dom.form) : null }
|
||||
get index() { return this.dom.index }
|
||||
|
||||
}
|
@ -1,22 +1,33 @@
|
||||
import { $Node } from "./$Node";
|
||||
import { $Node } from "./node/$Node";
|
||||
|
||||
export interface $StateOption<T> {
|
||||
format: (value: T) => string;
|
||||
}
|
||||
export class $State<T> {
|
||||
readonly value: T;
|
||||
readonly attributes = new Map<$Node, Set<string | number | symbol>>();
|
||||
constructor(value: T) {
|
||||
options: Partial<$StateOption<T>> = {}
|
||||
constructor(value: T, options?: $StateOption<T>) {
|
||||
this.value = value;
|
||||
if (options) this.options = options;
|
||||
}
|
||||
set(value: T) {
|
||||
(this as Mutable<$State<T>>).value = 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)
|
||||
if (node[attr] instanceof Function) {
|
||||
//@ts-expect-error
|
||||
if (this.options.format) node[attr](this.options.format(value))
|
||||
//@ts-expect-error
|
||||
else node[attr](value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
if (this.options.format) return this.options.format(this.value);
|
||||
return `${this.value}`
|
||||
}
|
||||
|
||||
@ -25,4 +36,6 @@ export class $State<T> {
|
||||
if (attrList) attrList.add(attrName);
|
||||
else this.attributes.set($node, new Set<string | number | symbol>().add(attrName))
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export type $StateArgument<T> = T | $State<T | undefined>;
|
30
lib/$Util.ts
30
lib/$Util.ts
@ -1,4 +1,8 @@
|
||||
import { $State } from "./$State";
|
||||
import { $Container } from "./node/$Container";
|
||||
import { $Node } from "./node/$Node";
|
||||
import { $SVGElement } from "./node/$SVGElement";
|
||||
import { $Text } from "./node/$Text";
|
||||
|
||||
export namespace $Util {
|
||||
export function fluent<T, A, V>(instance: T, args: IArguments, value: () => V, action: (...args: any[]) => void) {
|
||||
@ -7,13 +11,13 @@ export namespace $Util {
|
||||
return instance;
|
||||
}
|
||||
|
||||
export function multableResolve<T>(multable: OrArray<T>) {
|
||||
export function orArrayResolve<T>(multable: OrArray<T>) {
|
||||
if (multable instanceof Array) return multable;
|
||||
else return [multable];
|
||||
}
|
||||
|
||||
export function mixin(target: any, constructors: OrArray<any>) {
|
||||
multableResolve(constructors).forEach(constructor => {
|
||||
orArrayResolve(constructors).forEach(constructor => {
|
||||
Object.getOwnPropertyNames(constructor.prototype).forEach(name => {
|
||||
if (name === 'constructor') return;
|
||||
Object.defineProperty(
|
||||
@ -33,4 +37,26 @@ export namespace $Util {
|
||||
export function state<T>(value: T) {
|
||||
return new $State<T>(value)
|
||||
}
|
||||
|
||||
export function from(element: HTMLElement | Text | Node): $Node {
|
||||
if (element.$) return element.$;
|
||||
if (element.nodeName.toLowerCase() === 'body') return new $Container('body', {dom: element as HTMLBodyElement});
|
||||
else if (element instanceof HTMLElement) {
|
||||
const instance = $.TagNameElementMap[element.tagName.toLowerCase() as keyof typeof $.TagNameElementMap];
|
||||
const $node = instance === $Container ? new instance(element.tagName, {dom: element}) : new instance({dom: element} as any);
|
||||
if ($node instanceof $Container) for (const childnode of Array.from($node.dom.childNodes)) {
|
||||
$node.children.add($(childnode));
|
||||
}
|
||||
return $node as $Node;
|
||||
}
|
||||
else if (element instanceof Text) {
|
||||
const node = new $Text(element.textContent ?? '') as Mutable<$Node>;
|
||||
node.dom = element;
|
||||
return node as $Node;
|
||||
}
|
||||
else if (element instanceof SVGElement) {
|
||||
if (element.tagName.toLowerCase() === 'svg') {return new $SVGElement('svg', {dom: element}) };
|
||||
}
|
||||
throw `$NODE.FROM: NOT SUPPORT TARGET ELEMENT TYPE (${element.nodeName})`
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ export class $Anchor extends $Container<HTMLAnchorElement> {
|
||||
// Link Handler event
|
||||
this.dom.addEventListener('click', e => {
|
||||
if ($.anchorPreventDefault) e.preventDefault();
|
||||
if ($.anchorHandler && !!this.href()) $.anchorHandler(this.href(), e)
|
||||
if ($.anchorHandler && !!this.href()) $.anchorHandler(this, e)
|
||||
})
|
||||
}
|
||||
/**Set URL of anchor element. */
|
||||
@ -16,9 +16,9 @@ export class $Anchor extends $Container<HTMLAnchorElement> {
|
||||
href(url: string | undefined): this;
|
||||
href(url?: string | undefined) { return $.fluent(this, arguments, () => this.dom.href, () => {if (url) this.dom.href = url}) }
|
||||
/**Link open with this window, new tab or other */
|
||||
target(): string;
|
||||
target(): $AnchorTarget | undefined;
|
||||
target(target: $AnchorTarget | undefined): this;
|
||||
target(target?: $AnchorTarget | undefined) { return $.fluent(this, arguments, () => this.dom.target, () => {if (target) this.dom.target = target}) }
|
||||
target(target?: $AnchorTarget | undefined) { return $.fluent(this, arguments, () => (this.dom.target ?? undefined) as $AnchorTarget | undefined, () => {if (target) this.dom.target = target}) }
|
||||
}
|
||||
|
||||
export type $AnchorTarget = '_blank' | '_self' | '_parent' | '_top';
|
22
lib/node/$AsyncNode.ts
Normal file
22
lib/node/$AsyncNode.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { $Node } from "./$Node";
|
||||
|
||||
export class $AsyncNode<N extends $Node = $Node> extends $Node {
|
||||
dom: Node = document.createElement('async');
|
||||
loaded: boolean = false;
|
||||
constructor($node?: Promise<N>) {
|
||||
super()
|
||||
this.dom.$ = this;
|
||||
if ($node) $node.then($node => this._loaded($node));
|
||||
}
|
||||
|
||||
await<T extends $Node = $Node>($node: Promise<T>) {
|
||||
$node.then($node => this._loaded($node));
|
||||
return this as $AsyncNode<T>
|
||||
}
|
||||
|
||||
protected _loaded($node: $Node) {
|
||||
this.loaded = true;
|
||||
this.replace($node)
|
||||
this.dom.dispatchEvent(new Event('load'))
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { $Container, $ContainerOptions } from "./$Container";
|
||||
import { $State } from "./$State";
|
||||
import { $State, $StateArgument } from "../$State";
|
||||
export interface $ButtonOptions extends $ContainerOptions {}
|
||||
export class $Button extends $Container<HTMLButtonElement> {
|
||||
constructor(options?: $ButtonOptions) {
|
||||
@ -7,8 +7,8 @@ export class $Button extends $Container<HTMLButtonElement> {
|
||||
}
|
||||
|
||||
disabled(): boolean;
|
||||
disabled(disabled: boolean | $State<boolean>): this;
|
||||
disabled(disabled?: boolean | $State<boolean>) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))}
|
||||
disabled(disabled: $StateArgument<boolean>): this;
|
||||
disabled(disabled?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))}
|
||||
|
||||
type(): ButtonType;
|
||||
type(type: ButtonType): this;
|
@ -1,12 +1,12 @@
|
||||
import { $Element, $ElementOptions } from "./$Element";
|
||||
import { $NodeManager } from "./$NodeManager";
|
||||
import { $NodeManager } from "../$NodeManager";
|
||||
import { $Node } from "./$Node";
|
||||
import { $State } from "./$State";
|
||||
import { $State } from "../$State";
|
||||
import { $Text } from "./$Text";
|
||||
import { $HTMLElement, $HTMLElementOptions } from "./$HTMLElement";
|
||||
|
||||
export interface $ContainerOptions extends $ElementOptions {}
|
||||
|
||||
export class $Container<H extends HTMLElement = HTMLElement> extends $Element<H> {
|
||||
export interface $ContainerOptions extends $HTMLElementOptions {}
|
||||
export class $Container<H extends HTMLElement = HTMLElement> extends $HTMLElement<H> {
|
||||
readonly children: $NodeManager = new $NodeManager(this);
|
||||
constructor(tagname: string, options?: $ContainerOptions) {
|
||||
super(tagname, options)
|
@ -3,18 +3,26 @@ import { $Node } from "./$Node";
|
||||
export interface $ElementOptions {
|
||||
id?: string;
|
||||
class?: string[];
|
||||
dom?: HTMLElement | SVGElement;
|
||||
}
|
||||
|
||||
export class $Element<H extends HTMLElement = HTMLElement> extends $Node<H> {
|
||||
export class $Element<H extends HTMLElement | SVGElement = HTMLElement> extends $Node<H> {
|
||||
readonly dom: H;
|
||||
private static_classes = new Set<string>();
|
||||
constructor(tagname: string, options?: $ElementOptions) {
|
||||
super();
|
||||
this.dom = document.createElement(tagname) as H;
|
||||
this.dom = this.createDom(tagname, options) as H;
|
||||
this.dom.$ = this;
|
||||
this.setOptions(options);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
setOptions(options: $ElementOptions | undefined) {
|
||||
this.id(options?.id)
|
||||
if (options && options.class) this.class(...options.class)
|
||||
@ -24,7 +32,7 @@ export class $Element<H extends HTMLElement = HTMLElement> extends $Node<H> {
|
||||
/**Replace id of element. @example Element.id('customId');*/
|
||||
id(): string;
|
||||
id(name: string | undefined): this;
|
||||
id(name?: string | undefined): this | string {return $.fluent(this, arguments, () => this.dom.id, () => $.set(this.dom, 'id', name))}
|
||||
id(name?: string | undefined): this | string {return $.fluent(this, arguments, () => this.dom.id, () => $.set(this.dom, 'id', name as any))}
|
||||
|
||||
/**Replace list of class name to element. @example Element.class('name1', 'name2') */
|
||||
class(): DOMTokenList;
|
||||
@ -46,74 +54,32 @@ export class $Element<H extends HTMLElement = HTMLElement> extends $Node<H> {
|
||||
css(style: Partial<CSSStyleDeclaration>): this;
|
||||
css(style?: Partial<CSSStyleDeclaration>) { return $.fluent(this, arguments, () => this.dom.style, () => {Object.assign(this.dom.style, style)})}
|
||||
|
||||
/**
|
||||
* Get or set attribute from this element.
|
||||
* @param qualifiedName Attribute name
|
||||
* @param value Attribute value. Set `null` will remove attribute.
|
||||
*/
|
||||
attribute(qualifiedName: string | undefined): string | null;
|
||||
attribute(qualifiedName: string | undefined, value?: string | number | boolean): this;
|
||||
attribute(qualifiedName: string | undefined, value?: string | number | boolean): this | string | null {
|
||||
attribute(qualifiedName: string | undefined, value?: string | number | boolean | null): this;
|
||||
attribute(qualifiedName: string | undefined, value?: string | number | boolean | null): this | string | null {
|
||||
if (!arguments.length) return null;
|
||||
if (arguments.length === 1) {
|
||||
if (qualifiedName === undefined) return null;
|
||||
return this.dom.getAttribute(qualifiedName);
|
||||
}
|
||||
if (arguments.length === 2) {
|
||||
if (qualifiedName && value) this.dom.setAttribute(qualifiedName, `${value}`);
|
||||
if (!qualifiedName) return this;
|
||||
if (value === null) this.dom.removeAttribute(qualifiedName);
|
||||
else if (value !== undefined) this.dom.setAttribute(qualifiedName, `${value}`);
|
||||
return this;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
autocapitalize(): Autocapitalize;
|
||||
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) { return $.fluent(this, arguments, () => this.dom.dir, () => $.set(this.dom, 'dir', dir))}
|
||||
|
||||
innerText(): string;
|
||||
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) { return $.fluent(this, arguments, () => this.dom.title, () => $.set(this.dom, 'title', title))}
|
||||
|
||||
translate(): boolean;
|
||||
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))}
|
||||
|
||||
tabIndex(): number;
|
||||
tabIndex(tabIndex: number): this;
|
||||
tabIndex(tabIndex?: number) { return $.fluent(this, arguments, () => this.dom.tabIndex, () => $.set(this.dom, 'tabIndex', tabIndex))}
|
||||
tabIndex(tabIndex?: number) { return $.fluent(this, arguments, () => this.dom.tabIndex, () => $.set(this.dom, 'tabIndex', tabIndex as any))}
|
||||
|
||||
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; }
|
||||
focus() { this.dom.focus(); return this; }
|
||||
blur() { this.dom.blur(); return this; }
|
||||
|
||||
@ -125,11 +91,5 @@ export class $Element<H extends HTMLElement = HTMLElement> extends $Node<H> {
|
||||
|
||||
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 }
|
||||
get dataset() { return this.dom.dataset }
|
||||
}
|
@ -1,6 +1,4 @@
|
||||
import { $Container, $ContainerOptions } from "./$Container";
|
||||
import { $State } from "./$State";
|
||||
import { $Util } from "./$Util";
|
||||
export interface $FormOptions extends $ContainerOptions {}
|
||||
export class $Form extends $Container<HTMLFormElement> {
|
||||
constructor(options?: $FormOptions) {
|
65
lib/node/$HTMLElement.ts
Normal file
65
lib/node/$HTMLElement.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { $Element, $ElementOptions } from "./$Element";
|
||||
|
||||
export interface $HTMLElementOptions extends $ElementOptions {}
|
||||
export class $HTMLElement<H extends HTMLElement = HTMLElement> extends $Element<H> {
|
||||
constructor(tagname: string, options?: $HTMLElementOptions) {
|
||||
super(tagname, options)
|
||||
}
|
||||
|
||||
autocapitalize(): Autocapitalize;
|
||||
autocapitalize(autocapitalize?: Autocapitalize): this;
|
||||
autocapitalize(autocapitalize?: Autocapitalize) { return $.fluent(this, arguments, () => this.dom.autocapitalize, () => $.set(this.dom, 'autocapitalize', autocapitalize as any))}
|
||||
|
||||
innerText(): string;
|
||||
innerText(text?: string): this;
|
||||
innerText(text?: string) { return $.fluent(this, arguments, () => this.dom.innerText, () => $.set(this.dom, 'innerText', text as any))}
|
||||
|
||||
title(): string;
|
||||
title(title?: string): this;
|
||||
title(title?: string) { return $.fluent(this, arguments, () => this.dom.title, () => $.set(this.dom, 'title', title as any))}
|
||||
|
||||
dir(): TextDirection;
|
||||
dir(dir?: TextDirection): this;
|
||||
dir(dir?: TextDirection) { return $.fluent(this, arguments, () => this.dom.dir, () => $.set(this.dom, 'dir', dir as any))}
|
||||
|
||||
translate(): boolean;
|
||||
translate(translate?: boolean): this;
|
||||
translate(translate?: boolean) { return $.fluent(this, arguments, () => this.dom.translate, () => $.set(this.dom, 'translate', translate as any))}
|
||||
|
||||
popover(): string | null;
|
||||
popover(popover?: string | null): this;
|
||||
popover(popover?: string | null) { return $.fluent(this, arguments, () => this.dom.popover, () => $.set(this.dom, 'popover', popover as any))}
|
||||
|
||||
spellcheck(): boolean;
|
||||
spellcheck(spellcheck?: boolean): this;
|
||||
spellcheck(spellcheck?: boolean) { return $.fluent(this, arguments, () => this.dom.spellcheck, () => $.set(this.dom, 'spellcheck', spellcheck as any))}
|
||||
|
||||
inert(): boolean;
|
||||
inert(inert?: boolean): this;
|
||||
inert(inert?: boolean) { return $.fluent(this, arguments, () => this.dom.inert, () => $.set(this.dom, 'inert', inert as any))}
|
||||
|
||||
lang(): string;
|
||||
lang(lang?: string): this;
|
||||
lang(lang?: string) { return $.fluent(this, arguments, () => this.dom.lang, () => $.set(this.dom, 'lang', lang as any))}
|
||||
|
||||
draggable(): boolean;
|
||||
draggable(draggable?: boolean): this;
|
||||
draggable(draggable?: boolean) { return $.fluent(this, arguments, () => this.dom.draggable, () => $.set(this.dom, 'draggable', draggable as any))}
|
||||
|
||||
hidden(): boolean;
|
||||
hidden(hidden?: boolean): this;
|
||||
hidden(hidden?: boolean) { return $.fluent(this, arguments, () => this.dom.hidden, () => $.set(this.dom, 'hidden', hidden as any))}
|
||||
|
||||
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; }
|
||||
|
||||
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 }
|
||||
}
|
@ -1,11 +1,19 @@
|
||||
import { $Element, $ElementOptions } from "./$Element";
|
||||
import { $State } from "./$State";
|
||||
export interface $ImageOptions extends $ElementOptions {}
|
||||
export class $Image extends $Element<HTMLImageElement> {
|
||||
import { $HTMLElement, $HTMLElementOptions } from "./$HTMLElement";
|
||||
import { $State } from "../$State";
|
||||
export interface $ImageOptions extends $HTMLElementOptions {}
|
||||
export class $Image extends $HTMLElement<HTMLImageElement> {
|
||||
constructor(options?: $ImageOptions) {
|
||||
super('img', options);
|
||||
}
|
||||
|
||||
async load(src: string): Promise<$Image> {
|
||||
return new Promise(resolve => {
|
||||
const $img = this.once('load', () => {
|
||||
resolve($img)
|
||||
}).src(src)
|
||||
})
|
||||
}
|
||||
|
||||
/**HTMLImageElement base property */
|
||||
alt(): string;
|
||||
alt(alt: string): this;
|
@ -1,5 +1,5 @@
|
||||
import { $Element, $ElementOptions } from "./$Element";
|
||||
import { $State } from "./$State";
|
||||
import { $State, $StateArgument } from "../$State";
|
||||
|
||||
export interface $InputOptions extends $ElementOptions {}
|
||||
export class $Input extends $Element<HTMLInputElement> {
|
||||
@ -172,12 +172,12 @@ export class $Input extends $Element<HTMLInputElement> {
|
||||
formTarget(target?: string) { return $.fluent(this, arguments, () => this.dom.formTarget, () => $.set(this.dom, 'formTarget', target))}
|
||||
|
||||
name(): string;
|
||||
name(name?: string | $State<string>): this;
|
||||
name(name?: string | $State<string>) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))}
|
||||
name(name?: $StateArgument<string> | undefined): this;
|
||||
name(name?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))}
|
||||
|
||||
value(): string;
|
||||
value(value?: string | $State<string>): this;
|
||||
value(value?: string | $State<string>) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value))}
|
||||
value(value: $StateArgument<string> | undefined): this;
|
||||
value(value?: $StateArgument<string> | undefined) { 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)) }
|
@ -1,13 +1,12 @@
|
||||
import { $Element, $State, $Text } from "../index";
|
||||
import { $, $Element, $State, $Text } from "../../index";
|
||||
import { $Container } from "./$Container";
|
||||
|
||||
export abstract class $Node<N extends Node = Node> {
|
||||
readonly parent?: $Container;
|
||||
abstract readonly dom: N;
|
||||
readonly __hidden: boolean = false;
|
||||
private domEvents: {[key: string]: Map<Function, Function>} = {};
|
||||
|
||||
on<K extends keyof HTMLElementEventMap>(type: K, callback: (event: HTMLElementEventMap[K], $node: this) => void, options?: AddEventListenerOptions | boolean) {
|
||||
on<K extends keyof HTMLElementEventMap>(type: K, callback: (event: HTMLElementEventMap[K], $node: this) => any, options?: AddEventListenerOptions | boolean) {
|
||||
if (!this.domEvents[type]) this.domEvents[type] = new Map()
|
||||
const middleCallback = (e: Event) => callback(e as HTMLElementEventMap[K], this);
|
||||
this.domEvents[type].set(callback, middleCallback)
|
||||
@ -15,13 +14,13 @@ export abstract class $Node<N extends Node = Node> {
|
||||
return this;
|
||||
}
|
||||
|
||||
off<K extends keyof HTMLElementEventMap>(type: K, callback: (event: HTMLElementEventMap[K], $node: this) => void, options?: AddEventListenerOptions | boolean) {
|
||||
off<K extends keyof HTMLElementEventMap>(type: K, callback: (event: HTMLElementEventMap[K], $node: this) => any, options?: AddEventListenerOptions | boolean) {
|
||||
const middleCallback = this.domEvents[type]?.get(callback);
|
||||
if (middleCallback) this.dom.removeEventListener(type, middleCallback as EventListener, options)
|
||||
return this;
|
||||
}
|
||||
|
||||
once<K extends keyof HTMLElementEventMap>(type: K, callback: (event: HTMLElementEventMap[K], $node: this) => void, options?: AddEventListenerOptions | boolean) {
|
||||
once<K extends keyof HTMLElementEventMap>(type: K, callback: (event: HTMLElementEventMap[K], $node: this) => any, options?: AddEventListenerOptions | boolean) {
|
||||
const onceFn = (event: Event) => {
|
||||
this.dom.removeEventListener(type, onceFn, options)
|
||||
callback(event as HTMLElementEventMap[K], this);
|
||||
@ -52,7 +51,7 @@ export abstract class $Node<N extends Node = Node> {
|
||||
return this;
|
||||
}
|
||||
|
||||
contains(target: $Node | EventTarget | Node | null) {
|
||||
contains(target: $Node | EventTarget | Node | null): boolean {
|
||||
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)
|
||||
@ -61,25 +60,12 @@ export abstract class $Node<N extends Node = Node> {
|
||||
|
||||
self(callback: ($node: this) => void) { callback(this); return this; }
|
||||
inDOM() { return document.contains(this.dom); }
|
||||
isElement() {
|
||||
isElement(): $Element | undefined {
|
||||
if (this instanceof $Element) return this;
|
||||
else return undefined;
|
||||
}
|
||||
|
||||
static from(element: HTMLElement | Text): $Node {
|
||||
if (element.$) return element.$;
|
||||
else if (element instanceof HTMLElement) {
|
||||
const node = $(element.tagName) as Mutable<$Node>;
|
||||
node.dom = element;
|
||||
if (element.parentElement) node.parent = $(element.parentElement) as $Container;
|
||||
return node as $Node;
|
||||
}
|
||||
else if (element instanceof Text) {
|
||||
const node = new $Text(element.textContent ?? '') as Mutable<$Node>;
|
||||
node.dom = element;
|
||||
if (element.parentElement) node.parent = $(element.parentElement) as $Container;
|
||||
return node as $Node;
|
||||
}
|
||||
throw '$NODE.FROM: NOT SUPPORT TARGET ELEMENT TYPE'
|
||||
get parent() {
|
||||
return this.dom.parentElement?.$ as $Container | undefined;
|
||||
}
|
||||
}
|
17
lib/node/$OptGroup.ts
Normal file
17
lib/node/$OptGroup.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { $Container, $ContainerOptions } from "./$Container";
|
||||
import { $State, $StateArgument } from "../$State";
|
||||
|
||||
export interface $OptGroupOptions extends $ContainerOptions {}
|
||||
export class $OptGroup extends $Container<HTMLOptGroupElement> {
|
||||
constructor(options?: $OptGroupOptions) {
|
||||
super('optgroup', options);
|
||||
}
|
||||
|
||||
disabled(): boolean;
|
||||
disabled(disabled: $StateArgument<boolean> | undefined): this;
|
||||
disabled(disabled?: $StateArgument<boolean> | undefined) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))}
|
||||
|
||||
label(): string;
|
||||
label(label: $StateArgument<string> | undefined): this;
|
||||
label(label?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.label, () => $.set(this.dom, 'label', label))}
|
||||
}
|
37
lib/node/$Option.ts
Normal file
37
lib/node/$Option.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { $Container, $ContainerOptions } from "./$Container";
|
||||
import { $State, $StateArgument } from "../$State";
|
||||
|
||||
export interface $OptionOptions extends $ContainerOptions {}
|
||||
export class $Option extends $Container<HTMLOptionElement> {
|
||||
constructor(options?: $OptionOptions) {
|
||||
super('option', options);
|
||||
}
|
||||
|
||||
defaultSelected(): boolean;
|
||||
defaultSelected(defaultSelected: $StateArgument<boolean> | undefined): this;
|
||||
defaultSelected(defaultSelected?: $StateArgument<boolean> | undefined) { return $.fluent(this, arguments, () => this.dom.defaultSelected, () => $.set(this.dom, 'defaultSelected', defaultSelected))}
|
||||
|
||||
disabled(): boolean;
|
||||
disabled(disabled: $StateArgument<boolean> | undefined): this;
|
||||
disabled(disabled?: $StateArgument<boolean> | undefined) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))}
|
||||
|
||||
label(): string;
|
||||
label(label: $StateArgument<string> | undefined): this;
|
||||
label(label?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.label, () => $.set(this.dom, 'label', label))}
|
||||
|
||||
selected(): boolean;
|
||||
selected(selected: $StateArgument<boolean> | undefined): this;
|
||||
selected(selected?: $StateArgument<boolean> | undefined) { return $.fluent(this, arguments, () => this.dom.selected, () => $.set(this.dom, 'selected', selected))}
|
||||
|
||||
text(): string;
|
||||
text(text: $StateArgument<string> | undefined): this;
|
||||
text(text?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.text, () => $.set(this.dom, 'text', text))}
|
||||
|
||||
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))}
|
||||
|
||||
get form() { return this.dom.form ? $(this.dom.form) : null }
|
||||
get index() { return this.dom.index }
|
||||
|
||||
}
|
8
lib/node/$SVGElement.ts
Normal file
8
lib/node/$SVGElement.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { $Element, $ElementOptions } from "./$Element"
|
||||
|
||||
export interface $SVGOptions extends $ElementOptions {}
|
||||
export class $SVGElement<S extends SVGElement> extends $Element<S> {
|
||||
constructor(tagname: string, options?: $SVGOptions) {
|
||||
super(tagname, options);
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
import { $Container, $ContainerOptions } from "./$Container";
|
||||
import { $OptGroup } from "./$OptGroup";
|
||||
import { $Option } from "./$Option";
|
||||
import { $State } from "./$State";
|
||||
import { $State, $StateArgument } from "../$State";
|
||||
|
||||
export interface $SelectOptions extends $ContainerOptions {}
|
||||
export class $Select extends $Container<HTMLSelectElement> {
|
||||
constructor() {
|
||||
constructor(options?: $SelectOptions) {
|
||||
super('select')
|
||||
}
|
||||
|
||||
@ -18,12 +18,12 @@ export class $Select extends $Container<HTMLSelectElement> {
|
||||
namedItem(name: string) { return $(this.dom.namedItem(name)) }
|
||||
|
||||
disabled(): boolean;
|
||||
disabled(disabled: boolean | $State<boolean>): this;
|
||||
disabled(disabled?: boolean | $State<boolean>) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))}
|
||||
disabled(disabled: $StateArgument<boolean> | undefined): this;
|
||||
disabled(disabled?: $StateArgument<boolean> | undefined) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))}
|
||||
|
||||
multiple(): boolean;
|
||||
multiple(multiple: boolean | $State<boolean>): this;
|
||||
multiple(multiple?: boolean | $State<boolean>) { return $.fluent(this, arguments, () => this.dom.multiple, () => $.set(this.dom, 'multiple', multiple))}
|
||||
multiple(multiple: $StateArgument<boolean> | undefined): this;
|
||||
multiple(multiple?: $StateArgument<boolean> | undefined) { return $.fluent(this, arguments, () => this.dom.multiple, () => $.set(this.dom, 'multiple', multiple))}
|
||||
|
||||
required(): boolean;
|
||||
required(required: boolean): this;
|
||||
@ -40,12 +40,12 @@ export class $Select extends $Container<HTMLSelectElement> {
|
||||
get selectedOptions() { return Array.from(this.dom.selectedOptions).map($option => $($option)) }
|
||||
|
||||
name(): string;
|
||||
name(name?: string | $State<string>): this;
|
||||
name(name?: string | $State<string>) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))}
|
||||
name(name?: $StateArgument<string> | undefined): this;
|
||||
name(name?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))}
|
||||
|
||||
value(): string;
|
||||
value(value?: string | $State<string>): this;
|
||||
value(value?: string | $State<string>) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value))}
|
||||
value(value?: $StateArgument<string> | undefined): this;
|
||||
value(value?: $StateArgument<string> | undefined) { 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)) }
|
@ -1,5 +1,5 @@
|
||||
import { $Container, $ContainerOptions } from "./$Container";
|
||||
import { $State } from "./$State";
|
||||
import { $StateArgument } from "../$State";
|
||||
|
||||
export interface $TextareaOptions extends $ContainerOptions {}
|
||||
export class $Textarea extends $Container<HTMLTextAreaElement> {
|
||||
@ -12,16 +12,16 @@ export class $Textarea extends $Container<HTMLTextAreaElement> {
|
||||
cols(cols?: number) { return $.fluent(this, arguments, () => this.dom.cols, () => $.set(this.dom, 'cols', cols))}
|
||||
|
||||
name(): string;
|
||||
name(name?: string | $State<string>): this;
|
||||
name(name?: string | $State<string>) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))}
|
||||
name(name?: $StateArgument<string> | undefined): this;
|
||||
name(name?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))}
|
||||
|
||||
wrap(): string;
|
||||
wrap(wrap?: string | $State<string>): this;
|
||||
wrap(wrap?: string | $State<string>) { return $.fluent(this, arguments, () => this.dom.wrap, () => $.set(this.dom, 'wrap', wrap))}
|
||||
wrap(wrap?: $StateArgument<string> | undefined): this;
|
||||
wrap(wrap?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.wrap, () => $.set(this.dom, 'wrap', wrap))}
|
||||
|
||||
value(): string;
|
||||
value(value?: string | $State<string>): this;
|
||||
value(value?: string | $State<string>) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value))}
|
||||
value(value?: $StateArgument<string> | undefined): this;
|
||||
value(value?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value))}
|
||||
|
||||
maxLength(): number;
|
||||
maxLength(maxLength: number): this;
|
@ -1,5 +1,5 @@
|
||||
import { $Container, $ContainerOptions } from "./$Container";
|
||||
import { $EventManager } from "./$EventManager";
|
||||
import { $EventManager } from "../$EventManager";
|
||||
import { $Node } from "./$Node";
|
||||
|
||||
export interface $ViewOptions extends $ContainerOptions {}
|
||||
@ -28,7 +28,7 @@ export class $View extends $Container {
|
||||
|
||||
switchView(id: string) {
|
||||
const target_content = this.view_cache.get(id);
|
||||
if (target_content === undefined) throw '$View.switch(): target content is undefined';
|
||||
if (target_content === undefined) return this;
|
||||
this.content(target_content);
|
||||
this.content_id = id;
|
||||
this.event.fire('switch', id);
|
@ -1,5 +1,6 @@
|
||||
import { $EventManager, $EventMethod } from "../$EventManager";
|
||||
import { $Node } from "../$Node";
|
||||
import { $Node } from "../node/$Node";
|
||||
import { $Util } from "../$Util";
|
||||
export class Route<Path extends string | PathResolverFn> {
|
||||
path: string | PathResolverFn;
|
||||
builder: (req: RouteRequest<Path>) => RouteContent;
|
||||
@ -32,7 +33,7 @@ export class RouteRecord {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
$.mixin(RouteRecord, $EventMethod)
|
||||
$Util.mixin(RouteRecord, $EventMethod)
|
||||
export interface RouteRecordEventMap {
|
||||
'open': [{path: string, record: RouteRecord}];
|
||||
'load': [{path: string, record: RouteRecord}];
|
@ -1,15 +1,18 @@
|
||||
import { $EventManager, $EventMethod } from "../$EventManager";
|
||||
import { $Text } from "../$Text";
|
||||
import { $View } from "../$View";
|
||||
import { $Text } from "../node/$Text";
|
||||
import { $Util } from "../$Util";
|
||||
import { $View } from "../node/$View";
|
||||
import { PathResolverFn, Route, RouteRecord } from "./Route";
|
||||
export interface Router extends $EventMethod<RouterEventMap> {};
|
||||
export class Router {
|
||||
routeMap = new Map<string | PathResolverFn, Route<any>>();
|
||||
recordMap = new Map<string, RouteRecord>();
|
||||
$view: $View;
|
||||
index: number = 0;
|
||||
events = new $EventManager<RouterEventMap>().register('pathchange', 'notfound', 'load');
|
||||
static index: number = 0;
|
||||
static events = new $EventManager<RouterGlobalEventMap>().register('pathchange', 'notfound', 'load');
|
||||
events = new $EventManager<RouterEventMap>().register('notfound', 'load');
|
||||
basePath: string;
|
||||
static currentPath: URL = new URL(location.href);
|
||||
constructor(basePath: string, view?: $View) {
|
||||
this.basePath = basePath;
|
||||
this.$view = view ?? new $View();
|
||||
@ -25,39 +28,54 @@ export class Router {
|
||||
/**Start listen to the path change */
|
||||
listen() {
|
||||
if (!history.state || 'index' in history.state === false) {
|
||||
const routeData: RouteData = {index: this.index, data: {}}
|
||||
const routeData: RouteData = {index: Router.index, data: {}}
|
||||
history.replaceState(routeData, '')
|
||||
} else {
|
||||
this.index = history.state.index
|
||||
Router.index = history.state.index
|
||||
}
|
||||
addEventListener('popstate', this.popstate)
|
||||
$.routers.add(this);
|
||||
this.resolvePath();
|
||||
this.events.fire('pathchange', {path: location.href, navigation: 'Forward'});
|
||||
Router.events.fire('pathchange', {prevURL: undefined, nextURL: Router.currentPath, navigation: 'Forward'});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**Open path */
|
||||
open(path: string | undefined) {
|
||||
if (path === undefined) return;
|
||||
if (path === location.pathname) return this;
|
||||
/**Open URL */
|
||||
static open(url: string | URL | undefined) {
|
||||
if (url === undefined) return this;
|
||||
url = new URL(url);
|
||||
if (url.origin !== location.origin) return this;
|
||||
if (url.href === location.href) return this;
|
||||
const prevPath = Router.currentPath;
|
||||
this.index += 1;
|
||||
const routeData: RouteData = { index: this.index, data: {} };
|
||||
history.pushState(routeData, '', path);
|
||||
history.pushState(routeData, '', url);
|
||||
Router.currentPath = new URL(location.href);
|
||||
$.routers.forEach(router => router.resolvePath())
|
||||
this.events.fire('pathchange', {path, navigation: 'Forward'});
|
||||
Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: 'Forward'});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**Back to previous page */
|
||||
back() { history.back(); return this }
|
||||
static back() {
|
||||
const prevPath = Router.currentPath;
|
||||
history.back();
|
||||
Router.currentPath = new URL(location.href);
|
||||
Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: 'Back'});
|
||||
return this
|
||||
}
|
||||
|
||||
replace(path: string | undefined) {
|
||||
if (path === undefined) return;
|
||||
if (path === location.pathname) return this;
|
||||
history.replaceState({index: this.index}, '', path)
|
||||
$.routers.forEach(router => router.resolvePath(path));
|
||||
this.events.fire('pathchange', {path, navigation: 'Forward'});
|
||||
static replace(url: string | URL | undefined) {
|
||||
if (url === undefined) return this;
|
||||
if (typeof url === 'string' && !url.startsWith(location.origin)) url = location.origin + url;
|
||||
url = new URL(url);
|
||||
if (url.origin !== location.origin) return this;
|
||||
if (url.href === location.href) return this;
|
||||
const prevPath = Router.currentPath;
|
||||
history.replaceState({index: Router.index}, '', url)
|
||||
Router.currentPath = new URL(location.href);
|
||||
$.routers.forEach(router => router.resolvePath(url.pathname));
|
||||
Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: 'Forward'});
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -69,12 +87,14 @@ export class Router {
|
||||
|
||||
private popstate = (() => {
|
||||
// Forward
|
||||
if (history.state.index > this.index) { }
|
||||
if (history.state.index > Router.index) { }
|
||||
// Back
|
||||
else if (history.state.index < this.index) { }
|
||||
this.index = history.state.index;
|
||||
else if (history.state.index < Router.index) { }
|
||||
const prevPath = Router.currentPath;
|
||||
Router.index = history.state.index;
|
||||
this.resolvePath();
|
||||
this.events.fire('pathchange', {path: location.pathname, navigation: 'Forward'});
|
||||
Router.currentPath = new URL(location.href);
|
||||
Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: 'Forward'});
|
||||
}).bind(this)
|
||||
|
||||
private resolvePath(path = location.pathname) {
|
||||
@ -151,14 +171,21 @@ export class Router {
|
||||
if (!preventDefaultState) this.$view.clear();
|
||||
}
|
||||
}
|
||||
|
||||
static on<K extends keyof RouterGlobalEventMap>(type: K, callback: (...args: RouterGlobalEventMap[K]) => any) { this.events.on(type, callback); return this }
|
||||
static off<K extends keyof RouterGlobalEventMap>(type: K, callback: (...args: RouterGlobalEventMap[K]) => any) { this.events.off(type, callback); return this }
|
||||
static once<K extends keyof RouterGlobalEventMap>(type: K, callback: (...args: RouterGlobalEventMap[K]) => any) { this.events.once(type, callback); return this }
|
||||
}
|
||||
$.mixin(Router, $EventMethod);
|
||||
$Util.mixin(Router, $EventMethod);
|
||||
interface RouterEventMap {
|
||||
pathchange: [{path: string, navigation: 'Back' | 'Forward'}];
|
||||
notfound: [{path: string, preventDefault: () => void}];
|
||||
notfound: [{path: string, preventDefault: () => any}];
|
||||
load: [{path: string}];
|
||||
}
|
||||
|
||||
interface RouterGlobalEventMap {
|
||||
pathchange: [{prevURL?: URL, nextURL: URL, navigation: 'Back' | 'Forward'}];
|
||||
}
|
||||
|
||||
type RouteData = {
|
||||
index: number;
|
||||
data: {[key: string]: any};
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "fluentx",
|
||||
"description": "Fast, fluent, simple web builder",
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"type": "module",
|
||||
"module": "index.ts",
|
||||
"author": {
|
||||
|
Loading…
Reference in New Issue
Block a user