add: $Node.isElement()

add: Router/Route event - load
enhance: RouteData.data
add: Router.setStateData()
fix: route record open event not fire
rename: $Node.$hidden -> $Node.__hidden
add: $Element.static_classes, .stateClass(), .addStaticClass, .removeStaticClass, .attribute()
This commit is contained in:
defaultkavy 2024-03-16 13:33:05 +08:00
parent 658df2d8e6
commit 1489b1dba5
5 changed files with 62 additions and 21 deletions

View File

@ -7,6 +7,7 @@ export interface $ElementOptions {
export class $Element<H extends HTMLElement = 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;
@ -28,17 +29,38 @@ export class $Element<H extends HTMLElement = HTMLElement> extends $Node<H> {
/**Replace list of class name to element. @example Element.class('name1', 'name2') */
class(): DOMTokenList;
class(...name: (string | undefined)[]): this;
class(...name: (string | undefined)[]): this | DOMTokenList {return $.fluent(this, arguments, () => this.dom.classList, () => {this.dom.className = ''; this.dom.classList.add(...name.detype())})}
class(...name: (string | undefined)[]): this | DOMTokenList {return $.fluent(this, arguments, () => this.dom.classList, () => {this.dom.classList.forEach(n => this.static_classes.has(n) ?? this.dom.classList.remove(n)); this.dom.classList.add(...name.detype())})}
/**Add class name to dom. */
addClass(...name: (string | undefined)[]): this {return $.fluent(this, arguments, () => this, () => {this.dom.classList.add(...name.detype())})}
/**Remove class name from dom */
removeClass(...name: (string | undefined)[]): this {return $.fluent(this, arguments, () => this, () => {this.dom.classList.remove(...name.detype())})}
staticClass(): Set<string>;
staticClass(...name: (string | undefined)[]): this;
staticClass(...name: (string | undefined)[]) {return $.fluent(this, arguments, () => this.static_classes, () => {this.removeClass(...this.static_classes); this.static_classes.clear(); this.addStaticClass(...name);})}
addStaticClass(...name: (string | undefined)[]) {return $.fluent(this, arguments, () => this, () => {name.detype().forEach(n => this.static_classes.add(n)); this.addClass(...name)})}
removeStaticClass(...name: (string | undefined)[]) {return $.fluent(this, arguments, () => this, () => {name.detype().forEach(n => this.static_classes.delete(n)); this.removeClass(...name)})}
/**Modify css of element. */
css(): CSSStyleDeclaration
css(style: Partial<CSSStyleDeclaration>): this;
css(style?: Partial<CSSStyleDeclaration>) { return $.fluent(this, arguments, () => this.dom.style, () => {Object.assign(this.dom.style, style)})}
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 {
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}`);
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))}

View File

@ -1,10 +1,10 @@
import { $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;
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) {
@ -32,10 +32,10 @@ export abstract class $Node<N extends Node = Node> {
hide(): boolean;
hide(hide?: boolean | $State<boolean>): this;
hide(hide?: boolean | $State<boolean>) { return $.fluent(this, arguments, () => this.$hidden, () => {
hide(hide?: boolean | $State<boolean>) { return $.fluent(this, arguments, () => this.__hidden, () => {
if (hide === undefined) return;
if (hide instanceof $State) { (this as Mutable<$Node>).$hidden = hide.value; hide.use(this, 'hide')}
else (this as Mutable<$Node>).$hidden = hide;
if (hide instanceof $State) { (this as Mutable<$Node>).__hidden = hide.value; hide.use(this, 'hide')}
else (this as Mutable<$Node>).__hidden = hide;
this.parent?.children.render();
return this;
})}
@ -60,8 +60,11 @@ export abstract class $Node<N extends Node = Node> {
}
self(callback: ($node: this) => void) { callback(this); return this; }
inDOM() { return document.contains(this.dom); }
isElement() {
if (this instanceof $Element) return this;
else return undefined;
}
static from(element: HTMLElement | Text): $Node {
if (element.$) return element.$;

View File

@ -52,13 +52,13 @@ export class $NodeManager {
while (nodeList.length || domList.length) { // while nodeList or domList has item
const [node, dom] = [nodeList.at(0), domList.at(0)];
if (!dom) { if (node && !appendedNodeList.includes(node)) node.remove(); nodeList.shift()}
else if (!node) { if (!dom.$.$hidden) this.#dom.append(dom); domList.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); appendedNodeList.push(dom) }
if (!dom.$.__hidden) { this.#dom.insertBefore(dom, node); appendedNodeList.push(dom) }
domList.shift();
}
else {
if (dom.$.$hidden) this.#dom.removeChild(dom);
if (dom.$.__hidden) this.#dom.removeChild(dom);
domList.shift(); nodeList.shift();
}
}

View File

@ -28,17 +28,19 @@ export interface RouteRecord extends $EventMethod<RouteRecordEventMap> {};
export class RouteRecord {
id: string;
readonly content?: $Node;
events = new $EventManager<RouteRecordEventMap>().register('open')
events = new $EventManager<RouteRecordEventMap>().register('open', 'load')
constructor(id: string) {
this.id = id;
}
}
export interface RouteRecordEventMap {
'open': [path: string, record: RouteRecord]
'open': [path: string, record: RouteRecord];
'load': [path: string, record: RouteRecord];
}
export interface RouteRequest<Path extends PathResolverFn | string> {
params: PathParamResolver<Path>,
record: RouteRecord,
loaded: () => void;
}

View File

@ -9,7 +9,7 @@ export class Router {
recordMap = new Map<string, RouteRecord>();
view: $Container;
index: number = 0;
events = new $EventManager<RouterEventMap>().register('pathchange', 'notfound');
events = new $EventManager<RouterEventMap>().register('pathchange', 'notfound', 'load');
basePath: string;
constructor(basePath: string, view: $Container) {
this.basePath = basePath;
@ -26,7 +26,7 @@ export class Router {
/**Start listen to the path change */
listen() {
if (!history.state || 'index' in history.state === false) {
const routeData: RouteData = {index: this.index}
const routeData: RouteData = {index: this.index, data: {}}
history.replaceState(routeData, '')
} else {
this.index = history.state.index
@ -39,10 +39,11 @@ export class Router {
}
/**Open path */
open(path: string) {
open(path: string | undefined) {
if (path === undefined) return;
if (path === location.href) return this;
this.index += 1;
const routeData: RouteData = { index: this.index };
const routeData: RouteData = { index: this.index, data: {} };
history.pushState(routeData, '', path);
$.routers.forEach(router => router.resolvePath())
this.events.fire('pathchange', path, 'Forward');
@ -59,6 +60,12 @@ export class Router {
return this;
}
setStateData(key: string, value: any) {
if (history.state.data === undefined) history.state.data = {};
history.state.data[key] = value;
return this;
}
private popstate = (() => {
// Forward
if (history.state.index > this.index) { }
@ -77,8 +84,7 @@ export class Router {
const record = this.recordMap.get(pathId);
if (record) {
found = true;
if (record.content && this.view.contains(record.content)) return true;
this.view.content(record.content);
if (record.content && !this.view.contains(record.content)) this.view.content(record.content);
record.events.fire('open', path, record);
return true;
}
@ -86,7 +92,14 @@ export class Router {
}
const create = (pathId: string, route: Route<any>, data: any) => {
const record = new RouteRecord(pathId);
let content = route.builder({params: data, record: record});
let content = route.builder({
params: data,
record: record,
loaded: () => {
record.events.fire('load', pathId, record);
this.events.fire('load', pathId);
}
});
if (typeof content === 'string') content = new $Text(content);
(record as Mutable<RouteRecord>).content = content;
this.recordMap.set(pathId, record);
@ -134,10 +147,11 @@ export class Router {
}
interface RouterEventMap {
pathchange: [path: string, navigation: 'Back' | 'Forward'];
notfound: [path: string]
notfound: [path: string];
load: [path: string];
}
type RouteData = {
index: number;
data?: any;
data: {[key: string]: any};
}