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:
defaultkavy 2024-04-23 18:18:43 +08:00
parent 4b3c32c88d
commit b3f28dbf86
32 changed files with 418 additions and 297 deletions

102
$index.ts
View File

@ -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) {
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);
const prop = object[key];
if (prop instanceof Function) prop(value.value);
if (object[key] instanceof Function) (object[key] as Function)(value)
else object[key] = value.value;
return;
}
object[key] = value as any;
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[]
@ -238,4 +249,3 @@ export namespace $ {
type BuildNodeFunction = (...args: any[]) => $Node;
type BuilderSelfFunction<K extends $Node> = (self: K) => void;
globalThis.$ = $;

View File

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

View File

@ -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);

View File

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

View File

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

View File

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

View File

@ -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}`
}
@ -26,3 +37,5 @@ export class $State<T> {
else this.attributes.set($node, new Set<string | number | symbol>().add(attrName))
}
};
export type $StateArgument<T> = T | $State<T | undefined>;

View File

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

View File

@ -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
View 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'))
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View 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
View 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
View 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);
}
}

View File

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

View File

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

View File

@ -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);

View File

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

View File

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

View File

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