new: $View
change: Router.view => $View
new: $Select, $Option, $OptGroup, $Textarea, $View
add: $ query selector
add: $Container.clear() .$() .$all()
change: $Element.options => .setOptions
remove: EventMethod function
This commit is contained in:
defaultkavy 2024-04-20 20:46:11 +08:00
parent 0b3ca308d6
commit b51edda800
15 changed files with 328 additions and 43 deletions

View File

@ -10,7 +10,14 @@ import { Router } from "./lib/Router/Router";
import { $Image } from "./lib/$Image"; import { $Image } from "./lib/$Image";
import { $Canvas } from "./lib/$Canvas"; import { $Canvas } from "./lib/$Canvas";
import { $Dialog } from "./lib/$Dialog"; 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";
export type $ = typeof $; 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 $(element: null): null;
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;
@ -23,7 +30,9 @@ export function $(resolver: any) {
if (typeof resolver === 'undefined') return resolver; if (typeof resolver === 'undefined') return resolver;
if (resolver === null) return resolver; if (resolver === null) return resolver;
if (typeof resolver === 'string') { if (typeof resolver === 'string') {
if (resolver in $.TagNameElementMap) { 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] const instance = $.TagNameElementMap[resolver as keyof typeof $.TagNameElementMap]
switch (instance) { switch (instance) {
case $Element: return new $Element(resolver); case $Element: return new $Element(resolver);
@ -36,6 +45,11 @@ export function $(resolver: any) {
case $Image: return new $Image(); case $Image: return new $Image();
case $Canvas: return new $Canvas(); case $Canvas: return new $Canvas();
case $Dialog: return new $Dialog(); case $Dialog: return new $Dialog();
case $View: return new $View();
case $Select: return new $Select();
case $Option: return new $Option();
case $OptGroup: return new $OptGroup();
case $Textarea: return new $Textarea();
} }
} else return new $Container(resolver); } else return new $Container(resolver);
} }
@ -45,7 +59,6 @@ export function $(resolver: any) {
} }
throw '$: NOT SUPPORT TARGET ELEMENT TYPE' throw '$: NOT SUPPORT TARGET ELEMENT TYPE'
} }
export namespace $ { export namespace $ {
export let anchorHandler: null | ((url: string, e: Event) => void) = null; export let anchorHandler: null | ((url: string, e: Event) => void) = null;
export let anchorPreventDefault: boolean = false; export let anchorPreventDefault: boolean = false;
@ -74,7 +87,12 @@ export namespace $ {
'form': $Form, 'form': $Form,
'img': $Image, 'img': $Image,
'dialog': $Dialog, 'dialog': $Dialog,
'canvas': $Canvas 'canvas': $Canvas,
'view': $View,
'select': $Select,
'option': $Option,
'optgroup': $OptGroup,
'textarea': $Textarea
} }
export type TagNameTypeMap = { export type TagNameTypeMap = {
[key in keyof typeof $.TagNameElementMap]: InstanceType<typeof $.TagNameElementMap[key]>; [key in keyof typeof $.TagNameElementMap]: InstanceType<typeof $.TagNameElementMap[key]>;
@ -92,7 +110,11 @@ export namespace $ {
: H extends HTMLFormElement ? $Form : H extends HTMLFormElement ? $Form
: H extends HTMLCanvasElement ? $Canvas : H extends HTMLCanvasElement ? $Canvas
: H extends HTMLDialogElement ? $Dialog : H extends HTMLDialogElement ? $Dialog
: $Element<H>; : H extends HTMLSelectElement ? $Select
: H extends HTMLOptionElement ? $Option
: H extends HTMLOptGroupElement ? $OptGroup
: H extends HTMLTextAreaElement ? $Textarea
: $Container<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) {
if (!args.length) return value(); if (!args.length) return value();
@ -199,3 +221,4 @@ export namespace $ {
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;
globalThis.$ = $; globalThis.$ = $;

View File

@ -42,3 +42,8 @@ export * from "./lib/$Button";
export * from "./lib/$Form"; export * from "./lib/$Form";
export * from "./lib/$EventManager"; export * from "./lib/$EventManager";
export * from "./lib/$State"; export * from "./lib/$State";
export * from "./lib/$View";
export * from "./lib/$Select";
export * from "./lib/$Option";
export * from "./lib/$OptGroup";
export * from "./lib/$Textarea";

View File

@ -35,6 +35,18 @@ export class $Container<H extends HTMLElement = HTMLElement> extends $Element<H>
} }
this.children.render(); this.children.render();
})} })}
/**Remove all children elemetn from this element */
clear() {
this.children.removeAll();
return this;
}
//**Query selector one of child element */
$<E extends $Element>(query: string) { return $(this.dom.querySelector(query)) as E | null }
//**Query selector of child elements */
$all<E extends $Element>(query: string) { return Array.from(this.dom.querySelectorAll(query)).map($dom => $($dom) as E) }
} }
export type $ContainerContentBuilder<P extends $Container> = OrMatrix<$ContainerContentType> | (($node: P) => OrMatrix<$ContainerContentType>) export type $ContainerContentBuilder<P extends $Container> = OrMatrix<$ContainerContentType> | (($node: P) => OrMatrix<$ContainerContentType>)

View File

@ -12,10 +12,10 @@ export class $Element<H extends HTMLElement = HTMLElement> extends $Node<H> {
super(); super();
this.dom = document.createElement(tagname) as H; this.dom = document.createElement(tagname) as H;
this.dom.$ = this; this.dom.$ = this;
this.options(options); this.setOptions(options);
} }
options(options: $ElementOptions | undefined) { setOptions(options: $ElementOptions | undefined) {
this.id(options?.id) this.id(options?.id)
if (options && options.class) this.class(...options.class) if (options && options.class) this.class(...options.class)
return this; return this;

View File

@ -1,4 +1,3 @@
export function EventMethod<T>(target: T) {return $.mixin(target, $EventMethod)}
export abstract class $EventMethod<EM> { export abstract class $EventMethod<EM> {
abstract events: $EventManager<EM>; abstract events: $EventManager<EM>;
//@ts-expect-error //@ts-expect-error

View File

@ -34,13 +34,13 @@ export class $Input extends $Element<HTMLInputElement> {
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))}
max(): string; max(): number;
max(max: string): this; max(max: number): this;
max(max?: string) { return $.fluent(this, arguments, () => this.dom.max, () => $.set(this.dom, 'max', max))} max(max?: number) { return $.fluent(this, arguments, () => this.dom.max === '' ? null : parseInt(this.dom.min), () => $.set(this.dom, 'max', max?.toString()))}
min(): string; min(): number;
min(min: string): this; min(min: number): this;
min(min?: string) { return $.fluent(this, arguments, () => this.dom.min, () => $.set(this.dom, 'min', min))} min(min?: number) { return $.fluent(this, arguments, () => this.dom.min === '' ? null : parseInt(this.dom.min), () => $.set(this.dom, 'min', min?.toString()))}
maxLength(): number; maxLength(): number;
maxLength(maxLength: number): this; maxLength(maxLength: number): this;
@ -110,9 +110,9 @@ export class $Input extends $Element<HTMLInputElement> {
src(src: string): this; src(src: string): this;
src(src?: string) { return $.fluent(this, arguments, () => this.dom.src, () => $.set(this.dom, 'src', src))} src(src?: string) { return $.fluent(this, arguments, () => this.dom.src, () => $.set(this.dom, 'src', src))}
step(): string; step(): number;
step(step: string): this; step(step: number): this;
step(step?: string) { return $.fluent(this, arguments, () => this.dom.step, () => $.set(this.dom, 'step', step))} step(step?: number) { return $.fluent(this, arguments, () => Number(this.dom.step), () => $.set(this.dom, 'step', step?.toString()))}
type(): InputType; type(): InputType;
type(type: InputType): this; type(type: InputType): this;

View File

@ -4,11 +4,9 @@ import { $Text } from "./$Text";
export class $NodeManager { export class $NodeManager {
#container: $Container; #container: $Container;
#dom: HTMLElement;
elementList = new Set<$Node> elementList = new Set<$Node>
constructor(container: $Container) { constructor(container: $Container) {
this.#container = container; this.#container = container;
this.#dom = this.#container.dom
} }
add(element: $Node | string) { add(element: $Node | string) {
@ -47,23 +45,25 @@ export class $NodeManager {
} }
render() { render() {
const [domList, nodeList] = [this.array.map(node => node.dom), Array.from(this.#dom.childNodes)]; const [domList, nodeList] = [this.array.map(node => node.dom), Array.from(this.dom.childNodes)];
const appendedNodeList: Node[] = []; // appended node list const appendedNodeList: Node[] = []; // appended node list
// Rearrange // Rearrange
while (nodeList.length || domList.length) { // while nodeList or domList has item while (nodeList.length || domList.length) { // while nodeList or domList has item
const [node, dom] = [nodeList.at(0), domList.at(0)]; const [node, dom] = [nodeList.at(0), domList.at(0)];
if (!dom) { if (node && !appendedNodeList.includes(node)) node.remove(); nodeList.shift()} 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) { 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(); domList.shift();
} }
else { else {
if (dom.$.__hidden) this.#dom.removeChild(dom); if (dom.$.__hidden) this.dom.removeChild(dom);
domList.shift(); nodeList.shift(); domList.shift(); nodeList.shift();
} }
} }
} }
get array() {return [...this.elementList.values()]}; get array() {return [...this.elementList.values()]};
get dom() {return this.#container.dom}
} }

17
lib/$OptGroup.ts Normal file
View File

@ -0,0 +1,17 @@
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))}
}

37
lib/$Option.ts Normal file
View File

@ -0,0 +1,37 @@
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 }
}

47
lib/$Select.ts Normal file
View File

@ -0,0 +1,47 @@
import { $Container, $ContainerOptions } from "./$Container";
import { $FormElementMethod, FormElementMethod } from "./$Form";
import { $OptGroup } from "./$OptGroup";
import { $Option } from "./$Option";
import { $State } from "./$State";
export interface $SelectOptions extends $ContainerOptions {}
//@ts-expect-error
export interface $Select extends $FormElementMethod {}
@FormElementMethod
export class $Select extends $Container<HTMLSelectElement> {
constructor() {
super('select')
}
add(option: $SelectContentType | OrMatrix<$SelectContentType>) {
this.insert(option);
return this;
}
item(index: number) { return $(this.dom.item(index)) }
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))}
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))}
required(): boolean;
required(required: boolean): this;
required(required?: boolean) { return $.fluent(this, arguments, () => this.dom.required, () => $.set(this.dom, 'required', required))}
autocomplete(): AutoFill;
autocomplete(autocomplete: AutoFill): this;
autocomplete(autocomplete?: AutoFill) { return $.fluent(this, arguments, () => this.dom.autocomplete, () => $.set(this.dom, 'autocomplete', autocomplete))}
get length() { return this.dom.length }
get size() { return this.dom.size }
get options() { return Array.from(this.dom.options).map($option => $($option)) }
get selectedIndex() { return this.dom.selectedIndex }
get selectedOptions() { return Array.from(this.dom.selectedOptions).map($option => $($option)) }
}
export type $SelectContentType = $Option | $OptGroup | undefined;

100
lib/$Textarea.ts Normal file
View File

@ -0,0 +1,100 @@
import { $Container, $ContainerOptions } from "./$Container";
import { $State } from "./$State";
export interface $TextareaOptions extends $ContainerOptions {}
export class $Textarea extends $Container<HTMLTextAreaElement> {
constructor(options?: $TextareaOptions) {
super('textarea', options);
}
cols(): number;
cols(cols: number): this;
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))}
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))}
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))}
maxLength(): number;
maxLength(maxLength: number): this;
maxLength(maxLength?: number) { return $.fluent(this, arguments, () => this.dom.maxLength, () => $.set(this.dom, 'maxLength', maxLength))}
minLength(): number;
minLength(minLength: number): this;
minLength(minLength?: number) { return $.fluent(this, arguments, () => this.dom.minLength, () => $.set(this.dom, 'minLength', minLength))}
autocomplete(): AutoFill;
autocomplete(autocomplete: AutoFill): this;
autocomplete(autocomplete?: AutoFill) { return $.fluent(this, arguments, () => this.dom.autocomplete, () => $.set(this.dom, 'autocomplete', autocomplete))}
defaultValue(): string;
defaultValue(defaultValue: string): this;
defaultValue(defaultValue?: string) { return $.fluent(this, arguments, () => this.dom.defaultValue, () => $.set(this.dom, 'defaultValue', defaultValue))}
dirName(): string;
dirName(dirName: string): this;
dirName(dirName?: string) { return $.fluent(this, arguments, () => this.dom.dirName, () => $.set(this.dom, 'dirName', dirName))}
disabled(): boolean;
disabled(disabled: boolean): this;
disabled(disabled?: boolean) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))}
placeholder(): string;
placeholder(placeholder?: string): this;
placeholder(placeholder?: string) { return $.fluent(this, arguments, () => this.dom.placeholder, () => $.set(this.dom, 'placeholder', placeholder))}
readOnly(): boolean;
readOnly(readOnly: boolean): this;
readOnly(readOnly?: boolean) { return $.fluent(this, arguments, () => this.dom.readOnly, () => $.set(this.dom, 'readOnly', readOnly))}
required(): boolean;
required(required: boolean): this;
required(required?: boolean) { return $.fluent(this, arguments, () => this.dom.required, () => $.set(this.dom, 'required', required))}
selectionDirection(): SelectionDirection;
selectionDirection(selectionDirection: SelectionDirection): this;
selectionDirection(selectionDirection?: SelectionDirection) { return $.fluent(this, arguments, () => this.dom.selectionDirection, () => $.set(this.dom, 'selectionDirection', selectionDirection))}
selectionEnd(): number;
selectionEnd(selectionEnd: number): this;
selectionEnd(selectionEnd?: number) { return $.fluent(this, arguments, () => this.dom.selectionEnd, () => $.set(this.dom, 'selectionEnd', selectionEnd))}
selectionStart(): number;
selectionStart(selectionStart: number): this;
selectionStart(selectionStart?: number) { return $.fluent(this, arguments, () => this.dom.selectionStart, () => $.set(this.dom, 'selectionStart', selectionStart))}
type(): InputType;
type(type: InputType): this;
type(type?: InputType) { return $.fluent(this, arguments, () => this.dom.type, () => $.set(this.dom, 'type', type))}
inputMode(): InputMode;
inputMode(mode: InputMode): this;
inputMode(mode?: InputMode) { return $.fluent(this, arguments, () => this.dom.inputMode as InputMode, () => $.set(this.dom, 'inputMode', mode))}
select() { this.dom.select(); return this }
setCustomValidity(error: string) { this.dom.setCustomValidity(error); return this }
setRangeText(replacement: string): this;
setRangeText(replacement: string, start: number, end: number, selectionMode?: SelectionMode): this;
setRangeText(replacement: string, start?: number, end?: number, selectionMode?: SelectionMode) {
if (typeof start === 'number' && typeof end === 'number') this.dom.setRangeText(replacement, start, end, selectionMode)
this.dom.setRangeText(replacement);
return this
}
setSelectionRange(start: number | null, end: number | null, direction?: SelectionDirection) { this.dom.setSelectionRange(start, end, direction); return this }
checkValidity() { return this.dom.checkValidity() }
reportValidity() { return this.dom.reportValidity() }
get validationMessage() { return this.dom.validationMessage }
get validity() { return this.dom.validity }
get form() { return this.dom.form ? $(this.dom.form) : null }
get labels() { return Array.from(this.dom.labels ?? []).map(label => $(label)) }
}

41
lib/$View.ts Normal file
View File

@ -0,0 +1,41 @@
import { $Container, $ContainerOptions } from "./$Container";
import { $EventManager } from "./$EventManager";
import { $Node } from "./$Node";
export interface $ViewOptions extends $ContainerOptions {}
export class $View extends $Container {
protected view_cache = new Map<string, $Node>();
event = new $EventManager<$ViewEventMap>().register('switch')
content_id: string | null = null;
constructor(options?: $ViewOptions) {
super('view', options);
}
setView(id: string, $node: $Node) {
this.view_cache.set(id, $node);
return this;
}
deleteView(id: string) {
this.view_cache.delete(id);
return this;
}
deleteAllView() {
this.view_cache.clear();
return this;
}
switchView(id: string) {
const target_content = this.view_cache.get(id);
if (target_content === undefined) throw '$View.switch(): target content is undefined';
this.content(target_content);
this.content_id = id;
this.event.fire('switch', id);
return this;
}
}
export interface $ViewEventMap {
'switch': [id: string]
}

View File

@ -1,11 +1,11 @@
import { $EventManager, $EventMethod, EventMethod } from "../$EventManager"; import { $EventManager, $EventMethod } from "../$EventManager";
import { $Node } from "../$Node"; import { $Node } from "../$Node";
export class Route<Path extends string | PathResolverFn> { export class Route<Path extends string | PathResolverFn> {
path: string | PathResolverFn; path: string | PathResolverFn;
builder: (req: RouteRequest<Path>) => $Node | string; builder: (req: RouteRequest<Path>) => RouteContent;
constructor(path: Path, builder: (req: RouteRequest<Path>) => $Node | string) { constructor(path: Path, builder: ((req: RouteRequest<Path>) => RouteContent) | RouteContent) {
this.path = path; this.path = path;
this.builder = builder; this.builder = builder instanceof Function ? builder : (req: RouteRequest<Path>) => builder;
} }
} }
@ -24,7 +24,6 @@ type PathParamResolver<P extends PathResolverFn | string> = P extends PathResolv
export interface RouteRecord extends $EventMethod<RouteRecordEventMap> {}; export interface RouteRecord extends $EventMethod<RouteRecordEventMap> {};
@EventMethod
export class RouteRecord { export class RouteRecord {
id: string; id: string;
readonly content?: $Node; readonly content?: $Node;
@ -33,7 +32,7 @@ export class RouteRecord {
this.id = id; this.id = id;
} }
} }
$.mixin(RouteRecord, $EventMethod)
export interface RouteRecordEventMap { export interface RouteRecordEventMap {
'open': [{path: string, record: RouteRecord}]; 'open': [{path: string, record: RouteRecord}];
'load': [{path: string, record: RouteRecord}]; 'load': [{path: string, record: RouteRecord}];
@ -44,3 +43,5 @@ export interface RouteRequest<Path extends PathResolverFn | string> {
record: RouteRecord, record: RouteRecord,
loaded: () => void; loaded: () => void;
} }
export type RouteContent = $Node | string | void;

View File

@ -1,19 +1,18 @@
import { $Container } from "../$Container"; import { $EventManager, $EventMethod } from "../$EventManager";
import { $EventManager, $EventMethod, EventMethod } from "../$EventManager";
import { $Text } from "../$Text"; import { $Text } from "../$Text";
import { $View } from "../$View";
import { PathResolverFn, Route, RouteRecord } from "./Route"; import { PathResolverFn, Route, RouteRecord } from "./Route";
export interface Router extends $EventMethod<RouterEventMap> {}; export interface Router extends $EventMethod<RouterEventMap> {};
@EventMethod
export class Router { export class Router {
routeMap = new Map<string | PathResolverFn, Route<any>>(); routeMap = new Map<string | PathResolverFn, Route<any>>();
recordMap = new Map<string, RouteRecord>(); recordMap = new Map<string, RouteRecord>();
view: $Container; $view: $View;
index: number = 0; index: number = 0;
events = new $EventManager<RouterEventMap>().register('pathchange', 'notfound', 'load'); events = new $EventManager<RouterEventMap>().register('pathchange', 'notfound', 'load');
basePath: string; basePath: string;
constructor(basePath: string, view: $Container) { constructor(basePath: string, view?: $View) {
this.basePath = basePath; this.basePath = basePath;
this.view = view this.$view = view ?? new $View();
} }
/**Add route to Router. @example Router.addRoute(new Route('/', 'Hello World')) */ /**Add route to Router. @example Router.addRoute(new Route('/', 'Hello World')) */
@ -41,7 +40,7 @@ export class Router {
/**Open path */ /**Open path */
open(path: string | undefined) { open(path: string | undefined) {
if (path === undefined) return; if (path === undefined) return;
if (path === location.href) return this; if (path === location.pathname) return this;
this.index += 1; this.index += 1;
const routeData: RouteData = { index: this.index, data: {} }; const routeData: RouteData = { index: this.index, data: {} };
history.pushState(routeData, '', path); history.pushState(routeData, '', path);
@ -53,7 +52,9 @@ export class Router {
/**Back to previous page */ /**Back to previous page */
back() { history.back(); return this } back() { history.back(); return this }
replace(path: string) { replace(path: string | undefined) {
if (path === undefined) return;
if (path === location.pathname) return this;
history.replaceState({index: this.index}, '', path) history.replaceState({index: this.index}, '', path)
$.routers.forEach(router => router.resolvePath(path)); $.routers.forEach(router => router.resolvePath(path));
this.events.fire('pathchange', {path, navigation: 'Forward'}); this.events.fire('pathchange', {path, navigation: 'Forward'});
@ -84,7 +85,7 @@ export class Router {
const record = this.recordMap.get(pathId); const record = this.recordMap.get(pathId);
if (record) { if (record) {
found = true; found = true;
if (record.content && !this.view.contains(record.content)) this.view.content(record.content); if (record.content && !this.$view.contains(record.content)) this.$view.switchView(pathId);
record.events.fire('open', {path, record}); record.events.fire('open', {path, record});
return true; return true;
} }
@ -101,9 +102,10 @@ export class Router {
} }
}); });
if (typeof content === 'string') content = new $Text(content); if (typeof content === 'string') content = new $Text(content);
if (content === undefined) return;
(record as Mutable<RouteRecord>).content = content; (record as Mutable<RouteRecord>).content = content;
this.recordMap.set(pathId, record); this.recordMap.set(pathId, record);
this.view.content(content); this.$view.setView(pathId, content).switchView(pathId);
record.events.fire('open', {path, record}); record.events.fire('open', {path, record});
found = true; found = true;
} }
@ -146,10 +148,11 @@ export class Router {
let preventDefaultState = false; let preventDefaultState = false;
const preventDefault = () => preventDefaultState = true; const preventDefault = () => preventDefaultState = true;
this.events.fire('notfound', {path, preventDefault}); this.events.fire('notfound', {path, preventDefault});
if (!preventDefaultState) this.view.children.removeAll(); if (!preventDefaultState) this.$view.clear();
} }
} }
} }
$.mixin(Router, $EventMethod);
interface RouterEventMap { interface RouterEventMap {
pathchange: [{path: string, navigation: 'Back' | 'Forward'}]; pathchange: [{path: string, navigation: 'Back' | 'Forward'}];
notfound: [{path: string, preventDefault: () => void}]; notfound: [{path: string, preventDefault: () => void}];

View File

@ -1,7 +1,7 @@
{ {
"name": "fluentx", "name": "fluentx",
"description": "Fast, fluent, simple web builder", "description": "Fast, fluent, simple web builder",
"version": "0.0.3", "version": "0.0.4",
"type": "module", "type": "module",
"module": "index.ts", "module": "index.ts",
"author": { "author": {