fix: declare global not working.
fix: Array.detype() return empty array. new: RouteRecord, Route with path resolve function. change: $ElementManager rename to $NodeManager. add: $Node.hide() and $Node.show(). fix: $Node parent undefined. add: create element with 'label', 'input'. new: $.state() to binding data in element. new: $Form, $Button. fix: Router resolve path id bug
This commit is contained in:
parent
8fc0ab538c
commit
8e37f65f4f
61
$index.ts
61
$index.ts
@ -1,14 +1,18 @@
|
|||||||
import { $Node } from "./index";
|
import { $Node, $State } from "./index";
|
||||||
import { $Anchor } from "./lib/$Anchor";
|
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 { $Container } from "./lib/$Container";
|
||||||
import { $Element } from "./lib/$Element";
|
import { $Element } from "./lib/$Element";
|
||||||
import { $Input } from "./lib/$Input";
|
|
||||||
import { $Label } from "./lib/$Label";
|
import { $Label } from "./lib/$Label";
|
||||||
import { Router } from "./lib/Router/Router";
|
import { Router } from "./lib/Router/Router";
|
||||||
|
export type $ = typeof $;
|
||||||
export function $<K extends keyof $.TagNameTypeMap>(resolver: K): $.TagNameTypeMap[K];
|
export function $<K extends keyof $.TagNameTypeMap>(resolver: K): $.TagNameTypeMap[K];
|
||||||
export function $<K extends string>(resolver: K): $Container;
|
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): $.HTMLElementTo$ElementMap<H>;
|
||||||
|
export function $<H extends Element>(element: H): $Element;
|
||||||
|
export function $<H extends EventTarget>(element: H): $Element;
|
||||||
export function $(resolver: any) {
|
export function $(resolver: any) {
|
||||||
if (typeof resolver === 'string') {
|
if (typeof resolver === 'string') {
|
||||||
if (resolver in $.TagNameElementMap) {
|
if (resolver in $.TagNameElementMap) {
|
||||||
@ -17,17 +21,22 @@ export function $(resolver: any) {
|
|||||||
case $Element: return new $Element(resolver);
|
case $Element: return new $Element(resolver);
|
||||||
case $Anchor: return new $Anchor();
|
case $Anchor: return new $Anchor();
|
||||||
case $Container: return new $Container(resolver);
|
case $Container: return new $Container(resolver);
|
||||||
|
case $Input: return new $Input();
|
||||||
|
case $Label: return new $Label();
|
||||||
|
case $Form: return new $Form();
|
||||||
|
case $Button: return new $Button();
|
||||||
}
|
}
|
||||||
} else return new $Container(resolver);
|
} else return new $Container(resolver);
|
||||||
}
|
}
|
||||||
if (resolver instanceof HTMLElement) {
|
if (resolver instanceof HTMLElement || resolver instanceof Text) {
|
||||||
if (resolver.$) return resolver.$;
|
if (resolver.$) return resolver.$;
|
||||||
else throw new Error('HTMLElement PROPERTY $ MISSING');
|
else return $Node.from(resolver);
|
||||||
}
|
}
|
||||||
|
throw '$: NOT SUPPORT TARGET ELEMENT TYPE'
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace $ {
|
export namespace $ {
|
||||||
export let anchorHandler: null | ((url: URL, e: Event) => void) = null;
|
export let anchorHandler: null | ((url: string, e: Event) => void) = null;
|
||||||
export let anchorPreventDefault: boolean = false;
|
export let anchorPreventDefault: boolean = false;
|
||||||
export const routers = new Set<Router>;
|
export const routers = new Set<Router>;
|
||||||
export const TagNameElementMap = {
|
export const TagNameElementMap = {
|
||||||
@ -48,7 +57,10 @@ export namespace $ {
|
|||||||
'ul': $Container,
|
'ul': $Container,
|
||||||
'dl': $Container,
|
'dl': $Container,
|
||||||
'li': $Container,
|
'li': $Container,
|
||||||
'input': $Input
|
'input': $Input,
|
||||||
|
'label': $Label,
|
||||||
|
'button': $Button,
|
||||||
|
'form': $Form
|
||||||
}
|
}
|
||||||
export type TagNameTypeMap = {
|
export type TagNameTypeMap = {
|
||||||
[key in keyof typeof $.TagNameElementMap]: InstanceType<typeof $.TagNameElementMap[key]>;
|
[key in keyof typeof $.TagNameElementMap]: InstanceType<typeof $.TagNameElementMap[key]>;
|
||||||
@ -60,6 +72,8 @@ export namespace $ {
|
|||||||
H extends HTMLLabelElement ? $Label
|
H extends HTMLLabelElement ? $Label
|
||||||
: H extends HTMLInputElement ? $Input
|
: H extends HTMLInputElement ? $Input
|
||||||
: H extends HTMLAnchorElement ? $Anchor
|
: H extends HTMLAnchorElement ? $Anchor
|
||||||
|
: H extends HTMLButtonElement ? $Button
|
||||||
|
: H extends HTMLFormElement ? $Form
|
||||||
: $Element<H>;
|
: $Element<H>;
|
||||||
|
|
||||||
export function fluent<T, A, V>(instance: T, args: IArguments, value: () => V, action: (...args: any[]) => void) {
|
export function fluent<T, A, V>(instance: T, args: IArguments, value: () => V, action: (...args: any[]) => void) {
|
||||||
@ -74,7 +88,7 @@ export namespace $ {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function mixin(target: any, constructors: OrArray<any>) {
|
export function mixin(target: any, constructors: OrArray<any>) {
|
||||||
$.multableResolve(constructors).forEach(constructor => {
|
multableResolve(constructors).forEach(constructor => {
|
||||||
Object.getOwnPropertyNames(constructor.prototype).forEach(name => {
|
Object.getOwnPropertyNames(constructor.prototype).forEach(name => {
|
||||||
if (name === 'constructor') return;
|
if (name === 'constructor') return;
|
||||||
Object.defineProperty(
|
Object.defineProperty(
|
||||||
@ -90,18 +104,20 @@ export namespace $ {
|
|||||||
export function set<O, K extends keyof O>(object: O, key: K, value: any) {
|
export function set<O, K extends keyof O>(object: O, key: K, value: any) {
|
||||||
if (value !== undefined) object[key] = value;
|
if (value !== undefined) object[key] = value;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
$.builder = builder
|
|
||||||
|
|
||||||
/**Build multiple element in once. */
|
export function state<T>(value: T) {
|
||||||
function builder<F extends BuildNodeFunction, R extends ReturnType<F>>(bulder: F, params: [...Parameters<F>][], callback?: BuilderSelfFunction<R>): R[]
|
return new $State<T>(value)
|
||||||
function builder<F extends BuildNodeFunction, R extends ReturnType<F>>(bulder: [F, ...Parameters<F>], size: number, callback?: BuilderSelfFunction<R>): R[]
|
}
|
||||||
function builder<F extends BuildNodeFunction, R extends ReturnType<F>>(bulder: [F, ...Parameters<F>], options: ($Node | string | BuilderSelfFunction<R>)[]): R[]
|
|
||||||
function builder<K extends $.SelfTypeTagName>(tagname: K, size: number, callback?: BuilderSelfFunction<$.TagNameTypeMap[K]>): $.TagNameTypeMap[K][]
|
/**Build multiple element in once. */
|
||||||
function builder<K extends $.SelfTypeTagName>(tagname: K, callback: BuilderSelfFunction<$.TagNameTypeMap[K]>[]): $.TagNameTypeMap[K][]
|
export function builder<F extends BuildNodeFunction, R extends ReturnType<F>>(bulder: F, params: [...Parameters<F>][], callback?: BuilderSelfFunction<R>): R[]
|
||||||
function builder<K extends $.ContainerTypeTagName>(tagname: K, size: number, callback?: BuilderSelfFunction<$.TagNameTypeMap[K]>): $.TagNameTypeMap[K][]
|
export function builder<F extends BuildNodeFunction, R extends ReturnType<F>>(bulder: [F, ...Parameters<F>], size: number, callback?: BuilderSelfFunction<R>): R[]
|
||||||
function builder<K extends $.ContainerTypeTagName>(tagname: K, options: ($Node | string | BuilderSelfFunction<$.TagNameTypeMap[K]>)[]): $.TagNameTypeMap[K][]
|
export function builder<F extends BuildNodeFunction, R extends ReturnType<F>>(bulder: [F, ...Parameters<F>], options: ($Node | string | BuilderSelfFunction<R>)[]): R[]
|
||||||
function builder(tagname: any, resolver: any, callback?: BuilderSelfFunction<any>) {
|
export function builder<K extends $.SelfTypeTagName>(tagname: K, size: number, callback?: BuilderSelfFunction<$.TagNameTypeMap[K]>): $.TagNameTypeMap[K][]
|
||||||
|
export function builder<K extends $.SelfTypeTagName>(tagname: K, callback: BuilderSelfFunction<$.TagNameTypeMap[K]>[]): $.TagNameTypeMap[K][]
|
||||||
|
export function builder<K extends $.ContainerTypeTagName>(tagname: K, size: number, callback?: BuilderSelfFunction<$.TagNameTypeMap[K]>): $.TagNameTypeMap[K][]
|
||||||
|
export function builder<K extends $.ContainerTypeTagName>(tagname: K, options: ($Node | string | BuilderSelfFunction<$.TagNameTypeMap[K]>)[]): $.TagNameTypeMap[K][]
|
||||||
|
export function builder(tagname: any, resolver: any, callback?: BuilderSelfFunction<any>) {
|
||||||
if (typeof resolver === 'number') {
|
if (typeof resolver === 'number') {
|
||||||
return Array(resolver).fill('').map(v => {
|
return Array(resolver).fill('').map(v => {
|
||||||
const ele = isTuppleBuilder(tagname) ? tagname[0](...tagname.slice(1) as []) : $(tagname);
|
const ele = isTuppleBuilder(tagname) ? tagname[0](...tagname.slice(1) as []) : $(tagname);
|
||||||
@ -126,9 +142,8 @@ function builder(tagname: any, resolver: any, callback?: BuilderSelfFunction<any
|
|||||||
if (target instanceof Array && target[0] instanceof Function) return true;
|
if (target instanceof Array && target[0] instanceof Function) return true;
|
||||||
else return false;
|
else return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
type BuildNodeFunction = (...args: any[]) => $Node;
|
type BuildNodeFunction = (...args: any[]) => $Node;
|
||||||
type BuilderSelfFunction<K extends $Node> = (self: K) => void
|
type BuilderSelfFunction<K extends $Node> = (self: K) => void;
|
||||||
|
|
||||||
//@ts-expect-error
|
|
||||||
globalThis.$ = $;
|
globalThis.$ = $;
|
21
global.d.ts
vendored
21
global.d.ts
vendored
@ -1,21 +0,0 @@
|
|||||||
import { $ as fluentx } from "./$index";
|
|
||||||
import { $Element } from "./lib/$Element";
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
const $ = fluentx;
|
|
||||||
type OrMatrix<T> = T | OrMatrix<T>[];
|
|
||||||
type OrArray<T> = T | T[];
|
|
||||||
type OrPromise<T> = T | Promise<T>;
|
|
||||||
type Mutable<T> = {
|
|
||||||
-readonly [k in keyof T]: T[k];
|
|
||||||
};
|
|
||||||
type Types = 'string' | 'number' | 'boolean' | 'object' | 'symbol' | 'bigint' | 'function' | 'undefined'
|
|
||||||
type Autocapitalize = 'none' | 'off' | 'sentences' | 'on' | 'words' | 'characters';
|
|
||||||
type SelectionDirection = "forward" | "backward" | "none";
|
|
||||||
type InputType = "button" | "checkbox" | "color" | "date" | "datetime-local" | "email" | "file" | "hidden" | "image" | "month" | "number" | "password" | "radio" | "range" | "reset" | "search" | "submit" | "tel" | "text" | "time" | "url" | "week";
|
|
||||||
type TextDirection = 'ltr' | 'rtl' | 'auto' | '';
|
|
||||||
|
|
||||||
interface HTMLElement {
|
|
||||||
$: $Element;
|
|
||||||
}
|
|
||||||
}
|
|
25
index.ts
25
index.ts
@ -1,11 +1,29 @@
|
|||||||
declare global {
|
declare global {
|
||||||
|
var $: import('./$index').$;
|
||||||
interface Array<T> {
|
interface Array<T> {
|
||||||
detype<F extends undefined | null, O>(...types: F[]): Array<Exclude<T, F>>
|
detype<F extends undefined | null, O>(...types: F[]): Array<Exclude<T, F>>
|
||||||
}
|
}
|
||||||
|
type OrMatrix<T> = T | OrMatrix<T>[];
|
||||||
|
type OrArray<T> = T | T[];
|
||||||
|
type OrPromise<T> = T | Promise<T>;
|
||||||
|
type Mutable<T> = {
|
||||||
|
-readonly [k in keyof T]: T[k];
|
||||||
|
};
|
||||||
|
type Types = 'string' | 'number' | 'boolean' | 'object' | 'symbol' | 'bigint' | 'function' | 'undefined'
|
||||||
|
type Autocapitalize = 'none' | 'off' | 'sentences' | 'on' | 'words' | 'characters';
|
||||||
|
type SelectionDirection = "forward" | "backward" | "none";
|
||||||
|
type InputType = "button" | "checkbox" | "color" | "date" | "datetime-local" | "email" | "file" | "hidden" | "image" | "month" | "number" | "password" | "radio" | "range" | "reset" | "search" | "submit" | "tel" | "text" | "time" | "url" | "week";
|
||||||
|
type ButtonType = "submit" | "reset" | "button" | "menu";
|
||||||
|
type TextDirection = 'ltr' | 'rtl' | 'auto' | '';
|
||||||
|
|
||||||
|
interface Node {
|
||||||
|
$: import('./lib/$Node').$Node;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Array.prototype.detype = function <T extends undefined | null, O>(this: O[], ...types: T[]) {
|
Array.prototype.detype = function <T extends undefined | null, O>(this: O[], ...types: T[]) {
|
||||||
return this.filter(item => {
|
return this.filter(item => {
|
||||||
for (const type of types) typeof item !== typeof type
|
if (!types.length) return item !== undefined;
|
||||||
|
else for (const type of types) if (typeof item !== typeof type) return false; else return true
|
||||||
}) as Exclude<O, T>[];
|
}) as Exclude<O, T>[];
|
||||||
}
|
}
|
||||||
export * from "./$index";
|
export * from "./$index";
|
||||||
@ -14,7 +32,10 @@ export * from "./lib/Router/Router";
|
|||||||
export * from "./lib/$Node";
|
export * from "./lib/$Node";
|
||||||
export * from "./lib/$Anchor";
|
export * from "./lib/$Anchor";
|
||||||
export * from "./lib/$Element";
|
export * from "./lib/$Element";
|
||||||
export * from "./lib/$ElementManager";
|
export * from "./lib/$NodeManager";
|
||||||
export * from "./lib/$Text";
|
export * from "./lib/$Text";
|
||||||
export * from "./lib/$Container";
|
export * from "./lib/$Container";
|
||||||
|
export * from "./lib/$Button";
|
||||||
|
export * from "./lib/$Form";
|
||||||
export * from "./lib/$EventManager";
|
export * from "./lib/$EventManager";
|
||||||
|
export * from "./lib/$State";
|
@ -8,13 +8,13 @@ export class $Anchor extends $Container<HTMLAnchorElement> {
|
|||||||
// Link Handler event
|
// Link Handler event
|
||||||
this.dom.addEventListener('click', e => {
|
this.dom.addEventListener('click', e => {
|
||||||
if ($.anchorPreventDefault) e.preventDefault();
|
if ($.anchorPreventDefault) e.preventDefault();
|
||||||
if ($.anchorHandler) $.anchorHandler(this.href(), e)
|
if ($.anchorHandler && !!this.href()) $.anchorHandler(this.href(), e)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/**Set URL of anchor element. */
|
/**Set URL of anchor element. */
|
||||||
href(): URL;
|
href(): string;
|
||||||
href(url: string | undefined): this;
|
href(url: string | undefined): this;
|
||||||
href(url?: string | undefined) { return $.fluent(this, arguments, () => new URL(this.dom.href), () => {if (url) this.dom.href = url}) }
|
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 */
|
/**Link open with this window, new tab or other */
|
||||||
target(): string;
|
target(): string;
|
||||||
target(target: $AnchorTarget | undefined): this;
|
target(target: $AnchorTarget | undefined): this;
|
||||||
|
22
lib/$Button.ts
Normal file
22
lib/$Button.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { $Container, $ContainerOptions } from "./$Container";
|
||||||
|
import { FormElementMethod, $FormElementMethod } from "./$Form";
|
||||||
|
export interface $ButtonOptions extends $ContainerOptions {}
|
||||||
|
//@ts-expect-error
|
||||||
|
export interface $Button extends $FormElementMethod {}
|
||||||
|
@FormElementMethod
|
||||||
|
export class $Button extends $Container<HTMLButtonElement> {
|
||||||
|
constructor(options?: $ButtonOptions) {
|
||||||
|
super('button', options);
|
||||||
|
}
|
||||||
|
|
||||||
|
disabled(): boolean;
|
||||||
|
disabled(disabled: boolean): this;
|
||||||
|
disabled(disabled?: boolean) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))}
|
||||||
|
|
||||||
|
type(): ButtonType;
|
||||||
|
type(type: ButtonType): this;
|
||||||
|
type(type?: ButtonType) { return $.fluent(this, arguments, () => this.dom.type as ButtonType, () => $.set(this.dom, 'type', type))}
|
||||||
|
|
||||||
|
checkValidity() { return this.dom.checkValidity() }
|
||||||
|
reportValidity() { return this.dom.reportValidity() }
|
||||||
|
}
|
@ -1,11 +1,12 @@
|
|||||||
import { $Element, $ElementOptions } from "./$Element";
|
import { $Element, $ElementOptions } from "./$Element";
|
||||||
import { $ElementManager } from "./$ElementManager";
|
import { $NodeManager } from "./$NodeManager";
|
||||||
import { $Node } from "./$Node";
|
import { $Node } from "./$Node";
|
||||||
|
import { $State } from "./$State";
|
||||||
|
|
||||||
export interface $ContainerOptions extends $ElementOptions {}
|
export interface $ContainerOptions extends $ElementOptions {}
|
||||||
|
|
||||||
export class $Container<H extends HTMLElement = HTMLElement> extends $Element<H> {
|
export class $Container<H extends HTMLElement = HTMLElement> extends $Element<H> {
|
||||||
readonly children: $ElementManager = new $ElementManager(this);
|
readonly children: $NodeManager = new $NodeManager(this);
|
||||||
constructor(tagname: string, options?: $ContainerOptions) {
|
constructor(tagname: string, options?: $ContainerOptions) {
|
||||||
super(tagname, options)
|
super(tagname, options)
|
||||||
}
|
}
|
||||||
@ -13,13 +14,14 @@ export class $Container<H extends HTMLElement = HTMLElement> extends $Element<H>
|
|||||||
/**Replace element to this element.
|
/**Replace element to this element.
|
||||||
* @example Element.content([$('div')])
|
* @example Element.content([$('div')])
|
||||||
* Element.content('Hello World')*/
|
* Element.content('Hello World')*/
|
||||||
content(children: OrMatrix<$Node | string | undefined>): this { return $.fluent(this, arguments, () => this, () => {
|
content(children: $ContainerContentBuilder<this>): this { return $.fluent(this, arguments, () => this, () => {
|
||||||
this.children.removeAll();
|
this.children.removeAll();
|
||||||
this.insert(children);
|
this.insert(children);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
/**Insert element to this element */
|
/**Insert element to this element */
|
||||||
insert(children: OrMatrix<$Node | string | undefined>): this { return $.fluent(this, arguments, () => this, () => {
|
insert(children: $ContainerContentBuilder<this>): this { return $.fluent(this, arguments, () => this, () => {
|
||||||
|
if (children instanceof Function) children = children(this);
|
||||||
children = $.multableResolve(children);
|
children = $.multableResolve(children);
|
||||||
for (const child of children) {
|
for (const child of children) {
|
||||||
if (child === undefined) return;
|
if (child === undefined) return;
|
||||||
@ -29,3 +31,6 @@ export class $Container<H extends HTMLElement = HTMLElement> extends $Element<H>
|
|||||||
this.children.render();
|
this.children.render();
|
||||||
})}
|
})}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type $ContainerContentBuilder<P extends $Container> = OrMatrix<$ContainerContentType> | (($node: P) => OrMatrix<$ContainerContentType>)
|
||||||
|
export type $ContainerContentType = $Node | string | undefined | $State<any>
|
@ -23,7 +23,7 @@ export class $Element<H extends HTMLElement = HTMLElement> extends $Node<H> {
|
|||||||
/**Replace id of element. @example Element.id('customId');*/
|
/**Replace id of element. @example Element.id('customId');*/
|
||||||
id(): string;
|
id(): string;
|
||||||
id(name: string | undefined): this;
|
id(name: string | undefined): this;
|
||||||
id(name?: string | undefined): this | string {return $.fluent(this, arguments, () => this.dom.id, () => {if (name) this.dom.id === name})}
|
id(name?: string | undefined): this | string {return $.fluent(this, arguments, () => this.dom.id, () => $.set(this.dom, 'id', name))}
|
||||||
|
|
||||||
/**Replace list of class name to element. @example Element.class('name1', 'name2') */
|
/**Replace list of class name to element. @example Element.class('name1', 'name2') */
|
||||||
class(): DOMTokenList;
|
class(): DOMTokenList;
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
import { $Container } from "./$Container";
|
|
||||||
import { $Element } from "./$Element";
|
|
||||||
import { $Node } from "./$Node";
|
|
||||||
import { $Text } from "./$Text";
|
|
||||||
|
|
||||||
export class $ElementManager {
|
|
||||||
#container: $Container;
|
|
||||||
#dom: HTMLElement;
|
|
||||||
elementList = new Set<$Node>
|
|
||||||
constructor(container: $Container) {
|
|
||||||
this.#container = container;
|
|
||||||
this.#dom = this.#container.dom
|
|
||||||
}
|
|
||||||
|
|
||||||
add(element: $Node | string) {
|
|
||||||
if (typeof element === 'string') {
|
|
||||||
const text = new $Text(element);
|
|
||||||
this.elementList.add(text);
|
|
||||||
} else {
|
|
||||||
this.elementList.add(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
remove(element: $Node) {
|
|
||||||
this.elementList.delete(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeAll() {
|
|
||||||
this.elementList.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const [domList, nodeList] = [this.array.map(node => node.dom), Array.from(this.#dom.childNodes)];
|
|
||||||
// Rearrange
|
|
||||||
while (nodeList.length || domList.length) {
|
|
||||||
const [node, dom] = [nodeList.at(0), domList.at(0)];
|
|
||||||
if (!dom) { node?.remove(); nodeList.shift()}
|
|
||||||
else if (!node) { this.#dom.append(dom); domList.shift();}
|
|
||||||
else if (dom !== node) { this.#dom.insertBefore(dom, node); domList.shift();}
|
|
||||||
else {domList.shift(); nodeList.shift();}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get array() {return [...this.elementList.values()]};
|
|
||||||
}
|
|
84
lib/$Form.ts
Normal file
84
lib/$Form.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { $Container, $ContainerOptions } from "./$Container";
|
||||||
|
import { $Util } from "./$Util";
|
||||||
|
export interface $FormOptions extends $ContainerOptions {}
|
||||||
|
export class $Form extends $Container<HTMLFormElement> {
|
||||||
|
constructor(options?: $FormOptions) {
|
||||||
|
super('form', options);
|
||||||
|
}
|
||||||
|
|
||||||
|
autocomplete(): AutoFillBase;
|
||||||
|
autocomplete(autocomplete: AutoFill): this;
|
||||||
|
autocomplete(autocomplete?: AutoFill) { return $.fluent(this, arguments, () => this.dom.autocomplete, () => $.set(this.dom, 'autocomplete', autocomplete))}
|
||||||
|
|
||||||
|
action(): string;
|
||||||
|
action(action: string): this;
|
||||||
|
action(action?: string) { return $.fluent(this, arguments, () => this.dom.formAction, () => $.set(this.dom, 'formAction', action))}
|
||||||
|
|
||||||
|
enctype(): string;
|
||||||
|
enctype(enctype: string): this;
|
||||||
|
enctype(enctype?: string) { return $.fluent(this, arguments, () => this.dom.formEnctype, () => $.set(this.dom, 'formEnctype', enctype))}
|
||||||
|
|
||||||
|
method(): string;
|
||||||
|
method(method: string): this;
|
||||||
|
method(method?: string) { return $.fluent(this, arguments, () => this.dom.formMethod, () => $.set(this.dom, 'formMethod', method))}
|
||||||
|
|
||||||
|
noValidate(): boolean;
|
||||||
|
noValidate(boolean: boolean): this;
|
||||||
|
noValidate(boolean?: boolean) { return $.fluent(this, arguments, () => this.dom.formNoValidate, () => $.set(this.dom, 'formNoValidate', boolean))}
|
||||||
|
|
||||||
|
acceptCharset(): string;
|
||||||
|
acceptCharset(acceptCharset: string): this;
|
||||||
|
acceptCharset(acceptCharset?: string) { return $.fluent(this, arguments, () => this.dom.acceptCharset, () => $.set(this.dom, 'acceptCharset', acceptCharset))}
|
||||||
|
|
||||||
|
target(): string;
|
||||||
|
target(target: string): this;
|
||||||
|
target(target?: string) { return $.fluent(this, arguments, () => this.dom.formTarget, () => $.set(this.dom, 'formTarget', target))}
|
||||||
|
|
||||||
|
requestSubmit() { this.dom.requestSubmit(); return this }
|
||||||
|
reset(): this { this.dom.reset(); return this }
|
||||||
|
submit() { this.dom.submit(); return this }
|
||||||
|
checkValidity() { return this.dom.checkValidity() }
|
||||||
|
reportValidity() { return this.dom.reportValidity() }
|
||||||
|
|
||||||
|
get length() { return this.dom.length }
|
||||||
|
get elements() { return Array.from(this.dom.elements).map(ele => $(ele)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormElementMethod(target: any) { return $Util.mixin(target, $FormElementMethod) }
|
||||||
|
export abstract class $FormElementMethod {
|
||||||
|
abstract dom: HTMLButtonElement | HTMLInputElement;
|
||||||
|
|
||||||
|
formAction(): string;
|
||||||
|
formAction(action: string): this;
|
||||||
|
formAction(action?: string) { return $.fluent(this, arguments, () => this.dom.formAction, () => $.set(this.dom, 'formAction', action))}
|
||||||
|
|
||||||
|
formEnctype(): string;
|
||||||
|
formEnctype(enctype: string): this;
|
||||||
|
formEnctype(enctype?: string) { return $.fluent(this, arguments, () => this.dom.formEnctype, () => $.set(this.dom, 'formEnctype', enctype))}
|
||||||
|
|
||||||
|
formMethod(): string;
|
||||||
|
formMethod(method: string): this;
|
||||||
|
formMethod(method?: string) { return $.fluent(this, arguments, () => this.dom.formMethod, () => $.set(this.dom, 'formMethod', method))}
|
||||||
|
|
||||||
|
formNoValidate(): boolean;
|
||||||
|
formNoValidate(boolean: boolean): this;
|
||||||
|
formNoValidate(boolean?: boolean) { return $.fluent(this, arguments, () => this.dom.formNoValidate, () => $.set(this.dom, 'formNoValidate', boolean))}
|
||||||
|
|
||||||
|
formTarget(): string;
|
||||||
|
formTarget(target: string): this;
|
||||||
|
formTarget(target?: string) { return $.fluent(this, arguments, () => this.dom.formTarget, () => $.set(this.dom, 'formTarget', target))}
|
||||||
|
|
||||||
|
name(): string;
|
||||||
|
name(name: string): this;
|
||||||
|
name(name?: string) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))}
|
||||||
|
|
||||||
|
value(): string;
|
||||||
|
value(value: string): this;
|
||||||
|
value(value?: string) { 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)) }
|
||||||
|
get validationMessage() { return this.dom.validationMessage }
|
||||||
|
get validity() { return this.dom.validity }
|
||||||
|
get willValidate() { return this.dom.willValidate }
|
||||||
|
}
|
@ -1,12 +1,15 @@
|
|||||||
import { $Element, $ElementOptions } from "./$Element";
|
import { $Element, $ElementOptions } from "./$Element";
|
||||||
|
import { $FormElementMethod, FormElementMethod } from "./$Form";
|
||||||
|
|
||||||
export interface $InputOptions extends $ElementOptions {}
|
export interface $InputOptions extends $ElementOptions {}
|
||||||
|
//@ts-expect-error
|
||||||
|
export interface $Input extends $FormElementMethod {}
|
||||||
|
@FormElementMethod
|
||||||
export class $Input extends $Element<HTMLInputElement> {
|
export class $Input extends $Element<HTMLInputElement> {
|
||||||
constructor(options: $InputOptions) {
|
constructor(options?: $InputOptions) {
|
||||||
super('input', options);
|
super('input', options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
accept(): string[]
|
accept(): string[]
|
||||||
accept(...filetype: string[]): this
|
accept(...filetype: string[]): this
|
||||||
accept(...filetype: string[]) { return $.fluent(this, arguments, () => this.dom.accept.split(','), () => this.dom.accept = filetype.toString() )}
|
accept(...filetype: string[]) { return $.fluent(this, arguments, () => this.dom.accept.split(','), () => this.dom.accept = filetype.toString() )}
|
||||||
@ -27,27 +30,6 @@ export class $Input extends $Element<HTMLInputElement> {
|
|||||||
width(wdith: number): this;
|
width(wdith: number): this;
|
||||||
width(width?: number) { return $.fluent(this, arguments, () => this.dom.width, () => $.set(this.dom, 'width', width))}
|
width(width?: number) { return $.fluent(this, arguments, () => this.dom.width, () => $.set(this.dom, 'width', width))}
|
||||||
|
|
||||||
formAction(): string;
|
|
||||||
formAction(action: string): this;
|
|
||||||
formAction(action?: string) { return $.fluent(this, arguments, () => this.dom.formAction, () => $.set(this.dom, 'formAction', action))}
|
|
||||||
|
|
||||||
formEnctype(): string;
|
|
||||||
formEnctype(enctype: string): this;
|
|
||||||
formEnctype(enctype?: string) { return $.fluent(this, arguments, () => this.dom.formEnctype, () => $.set(this.dom, 'formEnctype', enctype))}
|
|
||||||
|
|
||||||
formMethod(): string;
|
|
||||||
formMethod(method: string): this;
|
|
||||||
formMethod(method?: string) { return $.fluent(this, arguments, () => this.dom.formMethod, () => $.set(this.dom, 'formMethod', method))}
|
|
||||||
|
|
||||||
formNoValidate(): boolean;
|
|
||||||
formNoValidate(boolean: boolean): this;
|
|
||||||
formNoValidate(boolean?: boolean) { return $.fluent(this, arguments, () => this.dom.formNoValidate, () => $.set(this.dom, 'formNoValidate', boolean))}
|
|
||||||
|
|
||||||
formTarget(): string;
|
|
||||||
formTarget(target: string): this;
|
|
||||||
formTarget(target?: string) { return $.fluent(this, arguments, () => this.dom.formTarget, () => $.set(this.dom, 'formTarget', target))}
|
|
||||||
|
|
||||||
|
|
||||||
checked(): boolean;
|
checked(): boolean;
|
||||||
checked(boolean: boolean): this;
|
checked(boolean: boolean): this;
|
||||||
checked(boolean?: boolean) { return $.fluent(this, arguments, () => this.dom.checked, () => $.set(this.dom, 'checked', boolean))}
|
checked(boolean?: boolean) { return $.fluent(this, arguments, () => this.dom.checked, () => $.set(this.dom, 'checked', boolean))}
|
||||||
@ -92,10 +74,6 @@ export class $Input extends $Element<HTMLInputElement> {
|
|||||||
multiple(multiple: boolean): this;
|
multiple(multiple: boolean): this;
|
||||||
multiple(multiple?: boolean) { return $.fluent(this, arguments, () => this.dom.multiple, () => $.set(this.dom, 'multiple', multiple))}
|
multiple(multiple?: boolean) { return $.fluent(this, arguments, () => this.dom.multiple, () => $.set(this.dom, 'multiple', multiple))}
|
||||||
|
|
||||||
name(): string;
|
|
||||||
name(name: string): this;
|
|
||||||
name(name?: string) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))}
|
|
||||||
|
|
||||||
pattern(): string;
|
pattern(): string;
|
||||||
pattern(pattern: string): this;
|
pattern(pattern: string): this;
|
||||||
pattern(pattern?: string) { return $.fluent(this, arguments, () => this.dom.pattern, () => $.set(this.dom, 'pattern', pattern))}
|
pattern(pattern?: string) { return $.fluent(this, arguments, () => this.dom.pattern, () => $.set(this.dom, 'pattern', pattern))}
|
||||||
@ -140,10 +118,6 @@ export class $Input extends $Element<HTMLInputElement> {
|
|||||||
type(type: InputType): this;
|
type(type: InputType): this;
|
||||||
type(type?: InputType) { return $.fluent(this, arguments, () => this.dom.type, () => $.set(this.dom, 'type', type))}
|
type(type?: InputType) { return $.fluent(this, arguments, () => this.dom.type, () => $.set(this.dom, 'type', type))}
|
||||||
|
|
||||||
value(): string;
|
|
||||||
value(value: string): this;
|
|
||||||
value(value?: string) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value))}
|
|
||||||
|
|
||||||
valueAsDate(): Date | null;
|
valueAsDate(): Date | null;
|
||||||
valueAsDate(date: Date | null): this;
|
valueAsDate(date: Date | null): this;
|
||||||
valueAsDate(date?: Date | null) { return $.fluent(this, arguments, () => this.dom.valueAsDate, () => $.set(this.dom, 'valueAsDate', date))}
|
valueAsDate(date?: Date | null) { return $.fluent(this, arguments, () => this.dom.valueAsDate, () => $.set(this.dom, 'valueAsDate', date))}
|
||||||
@ -173,9 +147,5 @@ export class $Input extends $Element<HTMLInputElement> {
|
|||||||
checkValidity() { return this.dom.checkValidity() }
|
checkValidity() { return this.dom.checkValidity() }
|
||||||
reportValidity() { return this.dom.reportValidity() }
|
reportValidity() { return this.dom.reportValidity() }
|
||||||
get files() { return this.dom.files }
|
get files() { return this.dom.files }
|
||||||
get labels() { return Array.from(this.dom.labels ?? []).map(label => $(label)) }
|
|
||||||
get validationMessage() { return this.dom.validationMessage }
|
|
||||||
get validity() { return this.dom.validity }
|
|
||||||
get webkitEntries() { return this.dom.webkitEntries }
|
get webkitEntries() { return this.dom.webkitEntries }
|
||||||
get willValidate() { return this.dom.willValidate }
|
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { $Container, $ContainerOptions } from "./$Container";
|
import { $Container, $ContainerOptions } from "./$Container";
|
||||||
export interface $LabelOptions extends $ContainerOptions {}
|
export interface $LabelOptions extends $ContainerOptions {}
|
||||||
export class $Label extends $Container<HTMLLabelElement> {
|
export class $Label extends $Container<HTMLLabelElement> {
|
||||||
constructor(options: $LabelOptions) {
|
constructor(options?: $LabelOptions) {
|
||||||
super('label', options);
|
super('label', options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
55
lib/$Node.ts
55
lib/$Node.ts
@ -1,25 +1,62 @@
|
|||||||
|
import { $Text } from "../index";
|
||||||
import { $Container } from "./$Container";
|
import { $Container } from "./$Container";
|
||||||
|
|
||||||
export abstract class $Node<N extends Node = Node> {
|
export abstract class $Node<N extends Node = Node> {
|
||||||
readonly parent?: $Container;
|
readonly parent?: $Container;
|
||||||
abstract readonly dom: N;
|
abstract readonly dom: N;
|
||||||
constructor() {
|
readonly hidden: boolean = false;
|
||||||
|
private domEvents: {[key: string]: Map<Function, Function>} = {};
|
||||||
|
|
||||||
|
on<K extends keyof HTMLElementEventMap>(type: K, callback: (event: Event, $node: this) => void, options?: AddEventListenerOptions | boolean) {
|
||||||
|
if (!this.domEvents[type]) this.domEvents[type] = new Map()
|
||||||
|
const middleCallback = (e: Event) => callback(e, this);
|
||||||
|
this.domEvents[type].set(callback, middleCallback)
|
||||||
|
this.dom.addEventListener(type, middleCallback, options)
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
on<K extends keyof HTMLElementEventMap>(type: K, callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean) {
|
off<K extends keyof HTMLElementEventMap>(type: K, callback: (event: Event, $node: this) => void, options?: AddEventListenerOptions | boolean) {
|
||||||
this.dom.addEventListener(type, callback, options)
|
const middleCallback = this.domEvents[type]?.get(callback);
|
||||||
|
if (middleCallback) this.dom.removeEventListener(type, middleCallback as EventListener, options)
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
off<K extends keyof HTMLElementEventMap>(type: K, callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean) {
|
once<K extends keyof HTMLElementEventMap>(type: K, callback: (event: Event, $node: this) => void, options?: AddEventListenerOptions | boolean) {
|
||||||
this.dom.removeEventListener(type, callback, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
once<K extends keyof HTMLElementEventMap>(type: K, callback: (event: Event) => void, options?: AddEventListenerOptions | boolean) {
|
|
||||||
const onceFn = (event: Event) => {
|
const onceFn = (event: Event) => {
|
||||||
this.dom.removeEventListener(type, onceFn, options)
|
this.dom.removeEventListener(type, onceFn, options)
|
||||||
callback(event);
|
callback(event, this);
|
||||||
};
|
};
|
||||||
this.dom.addEventListener(type, onceFn, options)
|
this.dom.addEventListener(type, onceFn, options)
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
show() { (this as Mutable<$Node>).hidden = false; this.parent?.children.render(); return this; }
|
||||||
|
hide() {
|
||||||
|
(this as Mutable<$Node>).hidden = true;
|
||||||
|
this.parent?.children.render();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
contains(target: $Node | EventTarget | Node) {
|
||||||
|
if (target instanceof $Node) return this.dom.contains(target.dom);
|
||||||
|
else if (target instanceof EventTarget) return this.dom.contains($(target).dom)
|
||||||
|
else return this.dom.contains(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
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'
|
||||||
}
|
}
|
||||||
}
|
}
|
60
lib/$NodeManager.ts
Normal file
60
lib/$NodeManager.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { $Container } from "./$Container";
|
||||||
|
import { $Node } from "./$Node";
|
||||||
|
import { $Text } from "./$Text";
|
||||||
|
import { $State } from "./$State";
|
||||||
|
|
||||||
|
export class $NodeManager {
|
||||||
|
#container: $Container;
|
||||||
|
#dom: HTMLElement;
|
||||||
|
elementList = new Set<$Node>
|
||||||
|
constructor(container: $Container) {
|
||||||
|
this.#container = container;
|
||||||
|
this.#dom = this.#container.dom
|
||||||
|
}
|
||||||
|
|
||||||
|
add(element: $Node | string | $State<any>) {
|
||||||
|
if (typeof element === 'string') {
|
||||||
|
const text = new $Text(element);
|
||||||
|
(text as Mutable<$Text>).parent = this.#container;
|
||||||
|
this.elementList.add(text);
|
||||||
|
} else if (element instanceof $State) {
|
||||||
|
if (typeof element.value === 'string') {
|
||||||
|
const ele = new $Text(element.value);
|
||||||
|
element.contents.add(ele);
|
||||||
|
(ele as Mutable<$Text>).parent = this.#container;
|
||||||
|
this.elementList.add(ele);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(element as Mutable<$Node>).parent = this.#container;
|
||||||
|
this.elementList.add(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(element: $Node) {
|
||||||
|
if (!this.elementList.has(element)) return;
|
||||||
|
this.elementList.delete(element);
|
||||||
|
(element as Mutable<$Node>).parent = undefined;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAll() {
|
||||||
|
this.elementList.forEach(ele => this.remove(ele))
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const [domList, nodeList] = [this.array.map(node => node.dom), Array.from(this.#dom.childNodes)];
|
||||||
|
// Rearrange
|
||||||
|
while (nodeList.length || domList.length) {
|
||||||
|
const [node, dom] = [nodeList.at(0), domList.at(0)];
|
||||||
|
if (!dom) { node?.remove(); nodeList.shift()}
|
||||||
|
else if (!node) { if (!dom.$.hidden) this.#dom.append(dom); domList.shift();}
|
||||||
|
else if (dom !== node) { if (!dom.$.hidden) this.#dom.insertBefore(dom, node); domList.shift();}
|
||||||
|
else {
|
||||||
|
if (dom.$.hidden) this.#dom.removeChild(dom);
|
||||||
|
domList.shift(); nodeList.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get array() {return [...this.elementList.values()]};
|
||||||
|
}
|
22
lib/$State.ts
Normal file
22
lib/$State.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { $Node } from "./$Node";
|
||||||
|
import { $Text } from "./$Text";
|
||||||
|
|
||||||
|
export class $State<T> {
|
||||||
|
readonly value: T;
|
||||||
|
readonly contents = new Set<$Node>();
|
||||||
|
constructor(value: T) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
set(value: T) {
|
||||||
|
(this as Mutable<$State<T>>).value = value;
|
||||||
|
for (const content of this.contents.values()) {
|
||||||
|
if (content instanceof $Text) {
|
||||||
|
content.content(`${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return `${this.value}`
|
||||||
|
}
|
||||||
|
};
|
@ -5,5 +5,10 @@ export class $Text extends $Node<Text> {
|
|||||||
constructor(data: string) {
|
constructor(data: string) {
|
||||||
super();
|
super();
|
||||||
this.dom = new Text(data);
|
this.dom = new Text(data);
|
||||||
|
this.dom.$ = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
content(): string;
|
||||||
|
content(text: string): this;
|
||||||
|
content(text?: string) { return $.fluent(this, arguments, () => this.dom.textContent, () => $.set(this.dom, 'textContent', text))}
|
||||||
}
|
}
|
36
lib/$Util.ts
Normal file
36
lib/$Util.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { $State } from "./$State";
|
||||||
|
|
||||||
|
export namespace $Util {
|
||||||
|
export function fluent<T, A, V>(instance: T, args: IArguments, value: () => V, action: (...args: any[]) => void) {
|
||||||
|
if (!args.length) return value();
|
||||||
|
action();
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function multableResolve<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 => {
|
||||||
|
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 set<O, K extends keyof O>(object: O, key: K, value: any) {
|
||||||
|
if (value !== undefined) object[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function state<T>(value: T) {
|
||||||
|
return new $State<T>(value)
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +1,39 @@
|
|||||||
|
import { $EventManager, $EventMethod, EventMethod } from "../$EventManager";
|
||||||
import { $Node } from "../$Node";
|
import { $Node } from "../$Node";
|
||||||
|
export class Route<Path extends string | PathResolverFn> {
|
||||||
export class Route<Path extends string = string> {
|
path: string | PathResolverFn;
|
||||||
path: string;
|
builder: (params: PathParamResolver<Path>, record: RouteRecord) => $Node | string;
|
||||||
builder: (path: PathParams<Path>) => string | $Node;
|
constructor(path: Path, builder: (params: PathParamResolver<Path>, record: RouteRecord) => $Node | string) {
|
||||||
constructor(path: Path, builder: (params: PathParams<Path>) => $Node | string) {
|
|
||||||
if (!path.startsWith('/')) throw new Error('PATH SHOULD START WITH /')
|
|
||||||
this.path = path;
|
this.path = path;
|
||||||
this.builder = builder;
|
this.builder = builder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type PathParams<Path> = Path extends `${infer Segment}/${infer Rest}`
|
type PathParams<Path> = Path extends `${infer Segment}/${infer Rest}`
|
||||||
? Segment extends `:${infer Param}` ? Record<Param, string> & PathParams<Rest> : PathParams<Rest>
|
? Segment extends `${string}:${infer Param}` ? Record<Param, string> & PathParams<Rest> : PathParams<Rest>
|
||||||
: Path extends `:${infer Param}` ? Record<Param,string> : {}
|
: Path extends `${string}:${infer Param}` ? Record<Param,string> : {}
|
||||||
|
|
||||||
type A = PathParams<'/:userId/post/:postId'>
|
export type PathResolverFn = (path: string) => undefined | string;
|
||||||
|
|
||||||
|
type PathParamResolver<P extends PathResolverFn | string> = P extends PathResolverFn
|
||||||
|
? undefined : PathParams<P>
|
||||||
|
|
||||||
|
// type PathResolverRecord<P extends PathResolverFn> = {
|
||||||
|
// [key in keyof ReturnType<P>]: ReturnType<P>[key]
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
export interface RouteRecord extends $EventMethod<RouteRecordEventMap> {};
|
||||||
|
@EventMethod
|
||||||
|
export class RouteRecord {
|
||||||
|
id: string;
|
||||||
|
readonly content?: $Node;
|
||||||
|
events = new $EventManager<RouteRecordEventMap>().register('open')
|
||||||
|
constructor(id: string) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RouteRecordEventMap {
|
||||||
|
'open': [path: string, record: RouteRecord]
|
||||||
|
}
|
@ -1,13 +1,12 @@
|
|||||||
import { $Container } from "../$Container";
|
import { $Container } from "../$Container";
|
||||||
import { $EventManager, $EventMethod, EventMethod } from "../$EventManager";
|
import { $EventManager, $EventMethod, EventMethod } from "../$EventManager";
|
||||||
import { $Node } from "../$Node";
|
|
||||||
import { $Text } from "../$Text";
|
import { $Text } from "../$Text";
|
||||||
import { Route } from "./Route";
|
import { PathResolverFn, Route, RouteRecord } from "./Route";
|
||||||
export interface Router extends $EventMethod<RouterEventMap> {};
|
export interface Router extends $EventMethod<RouterEventMap> {};
|
||||||
@EventMethod
|
@EventMethod
|
||||||
export class Router {
|
export class Router {
|
||||||
routeMap = new Map<string, Route<any>>();
|
routeMap = new Map<string | PathResolverFn, Route<any>>();
|
||||||
contentMap = new Map<string, $Node>();
|
recordMap = new Map<string, RouteRecord>();
|
||||||
view: $Container;
|
view: $Container;
|
||||||
index: number = 0;
|
index: number = 0;
|
||||||
events = new $EventManager<RouterEventMap>().register('pathchange', 'notfound');
|
events = new $EventManager<RouterEventMap>().register('pathchange', 'notfound');
|
||||||
@ -15,7 +14,6 @@ export class Router {
|
|||||||
constructor(basePath: string, view: $Container) {
|
constructor(basePath: string, view: $Container) {
|
||||||
this.basePath = basePath;
|
this.basePath = basePath;
|
||||||
this.view = view
|
this.view = view
|
||||||
$.routers.add(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**Add route to Router. @example Router.addRoute(new Route('/', 'Hello World')) */
|
/**Add route to Router. @example Router.addRoute(new Route('/', 'Hello World')) */
|
||||||
@ -35,13 +33,13 @@ export class Router {
|
|||||||
}
|
}
|
||||||
addEventListener('popstate', this.popstate)
|
addEventListener('popstate', this.popstate)
|
||||||
this.resolvePath();
|
this.resolvePath();
|
||||||
|
$.routers.add(this);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**Open path */
|
/**Open path */
|
||||||
open(path: string | URL) {
|
open(path: string) {
|
||||||
if (path instanceof URL) path = path.pathname;
|
if (path === location.href) return this;
|
||||||
if (path === location.pathname) return this;
|
|
||||||
this.index += 1;
|
this.index += 1;
|
||||||
const routeData: RouteData = { index: this.index };
|
const routeData: RouteData = { index: this.index };
|
||||||
history.pushState(routeData, '', path);
|
history.pushState(routeData, '', path);
|
||||||
@ -51,7 +49,14 @@ export class Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**Back to previous page */
|
/**Back to previous page */
|
||||||
back() { history.back(); }
|
back() { history.back(); return this }
|
||||||
|
|
||||||
|
replace(path: string) {
|
||||||
|
history.replaceState({index: this.index}, '', path)
|
||||||
|
$.routers.forEach(router => router.resolvePath());
|
||||||
|
this.events.fire('pathchange', path, 'Forward');
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
private popstate = (() => {
|
private popstate = (() => {
|
||||||
// Forward
|
// Forward
|
||||||
@ -67,38 +72,56 @@ export class Router {
|
|||||||
if (!path.startsWith(this.basePath)) return;
|
if (!path.startsWith(this.basePath)) return;
|
||||||
path = path.replace(this.basePath, '/').replace('//', '/')
|
path = path.replace(this.basePath, '/').replace('//', '/')
|
||||||
let found = false;
|
let found = false;
|
||||||
const openCached = () => {
|
const openCached = (pathId: string) => {
|
||||||
const cacheContent = this.contentMap.get(path);
|
const record = this.recordMap.get(pathId);
|
||||||
if (cacheContent) {
|
if (record) {
|
||||||
this.view.content(cacheContent);
|
|
||||||
found = true;
|
found = true;
|
||||||
|
if (record.content && this.view.contains(record.content)) return true;
|
||||||
|
this.view.content(record.content);
|
||||||
|
record.events.fire('open', path, record);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const create = (content: $Node | string) => {
|
const create = (pathId: string, route: Route<any>, data: any) => {
|
||||||
|
const record = new RouteRecord(pathId);
|
||||||
|
let content = route.builder(data, record);
|
||||||
if (typeof content === 'string') content = new $Text(content);
|
if (typeof content === 'string') content = new $Text(content);
|
||||||
this.contentMap.set(path, content)
|
(record as Mutable<RouteRecord>).content = content;
|
||||||
|
this.recordMap.set(pathId, record);
|
||||||
this.view.content(content);
|
this.view.content(content);
|
||||||
|
record.events.fire('open', path, record);
|
||||||
found = true;
|
found = true;
|
||||||
}
|
}
|
||||||
for (const route of this.routeMap.values()) {
|
for (const [pathResolver, route] of this.routeMap.entries()) {
|
||||||
const [_routeParts, _pathParts] = [route.path.split('/').map(p => `/${p}`), path.split('/').map(p => `/${p}`)];
|
// PathResolverFn
|
||||||
|
if (pathResolver instanceof Function) {
|
||||||
|
const routeId = pathResolver(path)
|
||||||
|
if (routeId) { if (!openCached(routeId)) create(routeId, route, undefined) }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// string
|
||||||
|
const [_routeParts, _pathParts] = [pathResolver.split('/').map(p => `/${p}`), path.split('/').map(p => `/${p}`)];
|
||||||
_routeParts.shift(); _pathParts.shift();
|
_routeParts.shift(); _pathParts.shift();
|
||||||
const data = {};
|
const data = {};
|
||||||
|
let pathString = '';
|
||||||
for (let i = 0; i < _pathParts.length; i++) {
|
for (let i = 0; i < _pathParts.length; i++) {
|
||||||
const [routePart, pathPart] = [_routeParts.at(i), _pathParts.at(i)];
|
const [routePart, pathPart] = [_routeParts.at(i), _pathParts.at(i)];
|
||||||
if (!routePart || !pathPart) continue;
|
if (!routePart || !pathPart) continue;
|
||||||
if (routePart === pathPart) {
|
if (routePart === pathPart) {
|
||||||
|
pathString += pathPart;
|
||||||
if (routePart === _routeParts.at(-1)) {
|
if (routePart === _routeParts.at(-1)) {
|
||||||
if (!openCached()) create(route.builder(data));
|
if (!openCached(pathString)) create(pathString, route, data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (routePart.includes(':')) {
|
else if (routePart.includes(':')) {
|
||||||
Object.assign(data, {[routePart.split(':')[1]]: pathPart.replace('/', '')})
|
const [prefix, param] = routePart.split(':');
|
||||||
|
if (!pathPart.startsWith(prefix)) return;
|
||||||
|
Object.assign(data, {[param]: pathPart.replace('/', '')})
|
||||||
|
pathString += pathPart;
|
||||||
if (routePart === _routeParts.at(-1)) {
|
if (routePart === _routeParts.at(-1)) {
|
||||||
if (!openCached()) create(route.builder(data));
|
if (!openCached(pathString)) create(pathString, route, data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"main": "index.ts",
|
|
||||||
"author": {
|
"author": {
|
||||||
"name": "defaultkavy",
|
"name": "defaultkavy",
|
||||||
"email": "defaultkavy@gmail.com",
|
"email": "defaultkavy@gmail.com",
|
||||||
|
Loading…
Reference in New Issue
Block a user