publish
This commit is contained in:
commit
851f44c205
134
$index.ts
Normal file
134
$index.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import { $Node } from "./index";
|
||||||
|
import { $Anchor } from "./lib/$Anchor";
|
||||||
|
import { $Container } from "./lib/$Container";
|
||||||
|
import { $Element } from "./lib/$Element";
|
||||||
|
import { $Input } from "./lib/$Input";
|
||||||
|
import { $Label } from "./lib/$Label";
|
||||||
|
import { Router } from "./lib/Router/Router";
|
||||||
|
|
||||||
|
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 $(resolver: any) {
|
||||||
|
if (typeof resolver === 'string') {
|
||||||
|
if (resolver in $.TagNameElementMap) {
|
||||||
|
const instance = $.TagNameElementMap[resolver as keyof typeof $.TagNameElementMap]
|
||||||
|
switch (instance) {
|
||||||
|
case $Element: return new $Element(resolver);
|
||||||
|
case $Anchor: return new $Anchor();
|
||||||
|
case $Container: return new $Container(resolver);
|
||||||
|
}
|
||||||
|
} else return new $Container(resolver);
|
||||||
|
}
|
||||||
|
if (resolver instanceof HTMLElement) {
|
||||||
|
if (resolver.$) return resolver.$;
|
||||||
|
else throw new Error('HTMLElement PROPERTY $ MISSING');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace $ {
|
||||||
|
export let anchorHandler: null | ((url: URL, e: Event) => void) = null;
|
||||||
|
export let anchorPreventDefault: boolean = false;
|
||||||
|
export const routers = new Set<Router>;
|
||||||
|
export const TagNameElementMap = {
|
||||||
|
'a': $Anchor,
|
||||||
|
'p': $Container,
|
||||||
|
'pre': $Container,
|
||||||
|
'code': $Container,
|
||||||
|
'blockquote': $Container,
|
||||||
|
'strong': $Container,
|
||||||
|
'h1': $Container,
|
||||||
|
'h2': $Container,
|
||||||
|
'h3': $Container,
|
||||||
|
'h4': $Container,
|
||||||
|
'h5': $Container,
|
||||||
|
'h6': $Container,
|
||||||
|
'div': $Container,
|
||||||
|
'ol': $Container,
|
||||||
|
'ul': $Container,
|
||||||
|
'dl': $Container,
|
||||||
|
'li': $Container,
|
||||||
|
'input': $Input
|
||||||
|
}
|
||||||
|
export type TagNameTypeMap = {
|
||||||
|
[key in keyof typeof $.TagNameElementMap]: InstanceType<typeof $.TagNameElementMap[key]>;
|
||||||
|
};
|
||||||
|
export type ContainerTypeTagName = Exclude<keyof TagNameTypeMap, 'input'>;
|
||||||
|
export type SelfTypeTagName = 'input';
|
||||||
|
|
||||||
|
export type HTMLElementTo$ElementMap<H extends HTMLElement> =
|
||||||
|
H extends HTMLLabelElement ? $Label
|
||||||
|
: H extends HTMLInputElement ? $Input
|
||||||
|
: H extends HTMLAnchorElement ? $Anchor
|
||||||
|
: $Element<H>;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$.builder = builder
|
||||||
|
|
||||||
|
/**Build multiple element in once. */
|
||||||
|
function builder<F extends BuildNodeFunction, R extends ReturnType<F>>(bulder: F, params: [...Parameters<F>][], callback?: BuilderSelfFunction<R>): R[]
|
||||||
|
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][]
|
||||||
|
function builder<K extends $.SelfTypeTagName>(tagname: K, callback: BuilderSelfFunction<$.TagNameTypeMap[K]>[]): $.TagNameTypeMap[K][]
|
||||||
|
function builder<K extends $.ContainerTypeTagName>(tagname: K, size: number, callback?: BuilderSelfFunction<$.TagNameTypeMap[K]>): $.TagNameTypeMap[K][]
|
||||||
|
function builder<K extends $.ContainerTypeTagName>(tagname: K, options: ($Node | string | BuilderSelfFunction<$.TagNameTypeMap[K]>)[]): $.TagNameTypeMap[K][]
|
||||||
|
function builder(tagname: any, resolver: any, callback?: BuilderSelfFunction<any>) {
|
||||||
|
if (typeof resolver === 'number') {
|
||||||
|
return Array(resolver).fill('').map(v => {
|
||||||
|
const ele = isTuppleBuilder(tagname) ? tagname[0](...tagname.slice(1) as []) : $(tagname);
|
||||||
|
if (callback) callback(ele);
|
||||||
|
return ele
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const eleArray = [];
|
||||||
|
for (const item of resolver) {
|
||||||
|
const ele = tagname instanceof Function ? tagname(...item) // tagname is function, item is params
|
||||||
|
: isTuppleBuilder(tagname) ? tagname[0](...tagname.slice(1) as [])
|
||||||
|
: $(tagname);
|
||||||
|
if (item instanceof Function) { item(ele) }
|
||||||
|
else if (item instanceof $Node || typeof item === 'string') { ele.content(item) }
|
||||||
|
eleArray.push(ele);
|
||||||
|
}
|
||||||
|
return eleArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTuppleBuilder(target: any): target is [BuildNodeFunction, ...any] {
|
||||||
|
if (target instanceof Array && target[0] instanceof Function) return true;
|
||||||
|
else return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type BuildNodeFunction = (...args: any[]) => $Node;
|
||||||
|
type BuilderSelfFunction<K extends $Node> = (self: K) => void
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
globalThis.$ = $;
|
134
README.md
Normal file
134
README.md
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
# Fluent - A mordern way to build web.
|
||||||
|
Inspired by jQuery, but not selecting query anymore, just create it.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
```ts
|
||||||
|
import { $ } from 'fluent'
|
||||||
|
|
||||||
|
const $app = $('app').content([
|
||||||
|
$('h1').content('Hello World!')
|
||||||
|
])
|
||||||
|
|
||||||
|
document.body.append($app.dom) // render $app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Forget HTML, create any element just like this
|
||||||
|
```ts
|
||||||
|
$('a')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Yes, Fluent Method.
|
||||||
|
```ts
|
||||||
|
$('h1').class('amazing-title').css({color: 'red'}).content('Fluuuuuuuuuuuuent!')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Router? I got you.
|
||||||
|
```ts
|
||||||
|
const router = new Router('/')
|
||||||
|
// example.com
|
||||||
|
.addRoute(new Route('/', () => 'Welcome to your first page!'))
|
||||||
|
|
||||||
|
// example.com/user/anyusername
|
||||||
|
.addRoute(new Route('/user/:username', (params) => {
|
||||||
|
return $('div').content([
|
||||||
|
$('h1').content(params.username)
|
||||||
|
])
|
||||||
|
}))
|
||||||
|
|
||||||
|
.listen() // start resolve pathname and listen state change
|
||||||
|
```
|
||||||
|
|
||||||
|
## Single Page App
|
||||||
|
```ts
|
||||||
|
$.anchorPreventDefault = true;
|
||||||
|
$.anchorHandler = (url) => { router.open(url) }
|
||||||
|
|
||||||
|
$('a').href('/about').content('Click me will not reload page.')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Insert element(s) with condition
|
||||||
|
```ts
|
||||||
|
// Example 1
|
||||||
|
$('div').content([
|
||||||
|
$('h1').content(params.username),
|
||||||
|
// conditional
|
||||||
|
params.username === 'admin' ? $('span').content('Admin is here!') : undefined
|
||||||
|
])
|
||||||
|
|
||||||
|
// Example 2
|
||||||
|
$('div').content([
|
||||||
|
$('h1').content(params.username),
|
||||||
|
params.username === 'alien' ? [
|
||||||
|
// the elements in this array will insert to <div> when conditional is true
|
||||||
|
$('span').content('Warning'),
|
||||||
|
$('span').content('You are contacting with alien!')
|
||||||
|
] : undefined
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Replace or Insert
|
||||||
|
```ts
|
||||||
|
$('div').content(['1', '2', '3']) // 123
|
||||||
|
.content(['4']) // 4
|
||||||
|
// content method will replace children with elements
|
||||||
|
|
||||||
|
.insert(['5', '6', '7']) // 4567
|
||||||
|
// using insert method to avoid replacement
|
||||||
|
|
||||||
|
.class('class1, class2') // class1, class2
|
||||||
|
// class method is replacement method
|
||||||
|
|
||||||
|
.addClass('class3') // class1, class2, class3
|
||||||
|
// using addClass method
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multiple element builder
|
||||||
|
```ts
|
||||||
|
$('ul').content([
|
||||||
|
// create 10 <li> element with same content
|
||||||
|
$.builder('li', 10, ($li) => $li.content('Not a unique content of list item!'))
|
||||||
|
|
||||||
|
// create <li> element depend on array length
|
||||||
|
$.builder('li', [
|
||||||
|
// if insert a function,
|
||||||
|
// builder will callback this function after create this <li> element
|
||||||
|
($li) => $li.css({color: 'red'}).content('List item with customize style!'),
|
||||||
|
|
||||||
|
// if insert a string or element,
|
||||||
|
// builder will create <li> element and insert this into <li>
|
||||||
|
'List item with just text',
|
||||||
|
$('a').href('/').content('List item but with a link!')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Element builder with function
|
||||||
|
```ts
|
||||||
|
// This is a template function that return a <div> element
|
||||||
|
function UserCard(name: string, age: number) {
|
||||||
|
return $('div').content([
|
||||||
|
$('h2').content(name),
|
||||||
|
$('span').content(`${bio} year old`)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// A user data array
|
||||||
|
const userDataList = [
|
||||||
|
{ name: 'Amateras', age: 16 },
|
||||||
|
{ name: 'Tsukimi', age: 16},
|
||||||
|
{ name: 'Rei', age: 14},
|
||||||
|
{ name: 'Ichi', age: 14},
|
||||||
|
]
|
||||||
|
|
||||||
|
// This function will create 10 UserCard element with same name and age
|
||||||
|
// Using tuple [Function, ...args] to call function with paramerters
|
||||||
|
$.builder([UserCard, 'Shizuka', 16], 100)
|
||||||
|
|
||||||
|
// This function will create UserCard with the amount depend on array length
|
||||||
|
$.builder(
|
||||||
|
UserCard,
|
||||||
|
userDataList.map(userData => [userData.name, userData.age]))
|
||||||
|
|
||||||
|
// Same result as (prefer)
|
||||||
|
userDataList.map(userData => UserCard(userData.name, userData.age))
|
||||||
|
```
|
21
global.d.ts
vendored
Normal file
21
global.d.ts
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { $ as fluent } from "./$index";
|
||||||
|
import { $Element } from "./lib/$Element";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
const $ = fluent;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
20
index.ts
Normal file
20
index.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
declare global {
|
||||||
|
interface Array<T> {
|
||||||
|
detype<F extends undefined | null, O>(...types: F[]): Array<Exclude<T, F>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Array.prototype.detype = function <T extends undefined | null, O>(this: O[], ...types: T[]) {
|
||||||
|
return this.filter(item => {
|
||||||
|
for (const type of types) typeof item !== typeof type
|
||||||
|
}) 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/$ElementManager";
|
||||||
|
export * from "./lib/$Text";
|
||||||
|
export * from "./lib/$Container";
|
||||||
|
export * from "./lib/$EventManager";
|
24
lib/$Anchor.ts
Normal file
24
lib/$Anchor.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { $Container, $ContainerOptions } from "./$Container";
|
||||||
|
|
||||||
|
export interface AnchorOptions extends $ContainerOptions {}
|
||||||
|
|
||||||
|
export class $Anchor extends $Container<HTMLAnchorElement> {
|
||||||
|
constructor(options?: AnchorOptions) {
|
||||||
|
super('a', options);
|
||||||
|
// Link Handler event
|
||||||
|
this.dom.addEventListener('click', e => {
|
||||||
|
if ($.anchorPreventDefault) e.preventDefault();
|
||||||
|
if ($.anchorHandler) $.anchorHandler(this.href(), e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**Set URL of anchor element. */
|
||||||
|
href(): URL;
|
||||||
|
href(url: string | undefined): this;
|
||||||
|
href(url?: string | undefined) { return $.fluent(this, arguments, () => new URL(this.dom.href), () => {if (url) this.dom.href = url}) }
|
||||||
|
/**Link open with this window, new tab or other */
|
||||||
|
target(): string;
|
||||||
|
target(target: $AnchorTarget | undefined): this;
|
||||||
|
target(target?: $AnchorTarget | undefined) { return $.fluent(this, arguments, () => this.dom.target, () => {if (target) this.dom.target = target}) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type $AnchorTarget = '_blank' | '_self' | '_parent' | '_top';
|
31
lib/$Container.ts
Normal file
31
lib/$Container.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { $Element, $ElementOptions } from "./$Element";
|
||||||
|
import { $ElementManager } from "./$ElementManager";
|
||||||
|
import { $Node } from "./$Node";
|
||||||
|
|
||||||
|
export interface $ContainerOptions extends $ElementOptions {}
|
||||||
|
|
||||||
|
export class $Container<H extends HTMLElement = HTMLElement> extends $Element<H> {
|
||||||
|
readonly children: $ElementManager = new $ElementManager(this);
|
||||||
|
constructor(tagname: string, options?: $ContainerOptions) {
|
||||||
|
super(tagname, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**Replace element to this element.
|
||||||
|
* @example Element.content([$('div')])
|
||||||
|
* Element.content('Hello World')*/
|
||||||
|
content(children: OrMatrix<$Node | string | undefined>): this { return $.fluent(this, arguments, () => this, () => {
|
||||||
|
this.children.removeAll();
|
||||||
|
this.insert(children);
|
||||||
|
})}
|
||||||
|
|
||||||
|
/**Insert element to this element */
|
||||||
|
insert(children: OrMatrix<$Node | string | undefined>): this { return $.fluent(this, arguments, () => this, () => {
|
||||||
|
children = $.multableResolve(children);
|
||||||
|
for (const child of children) {
|
||||||
|
if (child === undefined) return;
|
||||||
|
if (child instanceof Array) this.insert(child)
|
||||||
|
else this.children.add(child);
|
||||||
|
}
|
||||||
|
this.children.render();
|
||||||
|
})}
|
||||||
|
}
|
69
lib/$Element.ts
Normal file
69
lib/$Element.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { $Node } from "./$Node";
|
||||||
|
|
||||||
|
export interface $ElementOptions {
|
||||||
|
id?: string;
|
||||||
|
class?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class $Element<H extends HTMLElement = HTMLElement> extends $Node<H> {
|
||||||
|
readonly dom: H;
|
||||||
|
constructor(tagname: string, options?: $ElementOptions) {
|
||||||
|
super();
|
||||||
|
this.dom = document.createElement(tagname) as H;
|
||||||
|
this.dom.$ = this;
|
||||||
|
this.options(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
options(options: $ElementOptions | undefined) {
|
||||||
|
this.id(options?.id)
|
||||||
|
if (options && options.class) this.class(...options.class)
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**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, () => {if (name) this.dom.id === name})}
|
||||||
|
|
||||||
|
/**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())})}
|
||||||
|
/**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())})}
|
||||||
|
|
||||||
|
/**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)})}
|
||||||
|
|
||||||
|
/**Remove this element from parent */
|
||||||
|
remove() {
|
||||||
|
this.parent?.children.remove(this);
|
||||||
|
(this as Mutable<this>).parent = undefined;
|
||||||
|
this.dom.remove();
|
||||||
|
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))}
|
||||||
|
}
|
45
lib/$ElementManager.ts
Normal file
45
lib/$ElementManager.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
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()]};
|
||||||
|
}
|
66
lib/$EventManager.ts
Normal file
66
lib/$EventManager.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
export function EventMethod<T>(target: T) {return $.mixin(target, $EventMethod)}
|
||||||
|
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 }
|
||||||
|
//@ts-expect-error
|
||||||
|
off<K extends keyof EM>(type: K, callback: (...args: EM[K]) => void) { 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 }
|
||||||
|
}
|
||||||
|
export class $EventManager<EM> {
|
||||||
|
private eventMap = new Map<string, $Event>();
|
||||||
|
register(...names: string[]) {
|
||||||
|
names.forEach(name => {
|
||||||
|
const event = new $Event(name);
|
||||||
|
this.eventMap.set(event.name, event);
|
||||||
|
})
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
delete(name: string) { this.eventMap.delete(name); return this }
|
||||||
|
//@ts-expect-error
|
||||||
|
fire<K extends keyof EM>(type: K, ...args: EM[K]) {
|
||||||
|
const event = this.get(type)
|
||||||
|
//@ts-expect-error
|
||||||
|
if (event instanceof $Event) event.fire(...args);
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
//@ts-expect-error
|
||||||
|
on<K extends keyof EM>(type: K, callback: (...args: EM[K]) => void) {
|
||||||
|
this.get(type).add(callback);
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
//@ts-expect-error
|
||||||
|
off<K extends keyof EM>(type: K, callback: (...args: EM[K]) => void) {
|
||||||
|
this.get(type).delete(callback);
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
//@ts-expect-error
|
||||||
|
once<K extends keyof EM>(type: K, callback: (...args: EM[K]) => void) {
|
||||||
|
//@ts-expect-error
|
||||||
|
const onceFn = (...args: EM[K]) => {
|
||||||
|
this.get(type).delete(onceFn);
|
||||||
|
//@ts-expect-error
|
||||||
|
callback(...args);
|
||||||
|
}
|
||||||
|
this.get(type).add(onceFn);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get<K extends keyof EM>(type: K) {
|
||||||
|
//@ts-expect-error
|
||||||
|
const event = this.eventMap.get(type);
|
||||||
|
if (!event) throw new Error('EVENT NOT EXIST')
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class $Event {
|
||||||
|
name: string;
|
||||||
|
private callbackList = new Set<Function>()
|
||||||
|
constructor(name: string) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
fire(...args: any[]) { this.callbackList.forEach(callback => callback(...args)) }
|
||||||
|
add(callback: Function) { this.callbackList.add(callback) }
|
||||||
|
delete(callback: Function) { this.callbackList.delete(callback) }
|
||||||
|
}
|
181
lib/$Input.ts
Normal file
181
lib/$Input.ts
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import { $Element, $ElementOptions } from "./$Element";
|
||||||
|
|
||||||
|
export interface $InputOptions extends $ElementOptions {}
|
||||||
|
export class $Input extends $Element<HTMLInputElement> {
|
||||||
|
constructor(options: $InputOptions) {
|
||||||
|
super('input', options);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
accept(): string[]
|
||||||
|
accept(...filetype: string[]): this
|
||||||
|
accept(...filetype: string[]) { return $.fluent(this, arguments, () => this.dom.accept.split(','), () => this.dom.accept = filetype.toString() )}
|
||||||
|
|
||||||
|
capture(): string;
|
||||||
|
capture(capture: string): this;
|
||||||
|
capture(capture?: string) { return $.fluent(this, arguments, () => this.dom.capture, () => $.set(this.dom, 'capture', capture))}
|
||||||
|
|
||||||
|
alt(): string;
|
||||||
|
alt(alt: string): this;
|
||||||
|
alt(alt?: string) { return $.fluent(this, arguments, () => this.dom.alt, () => $.set(this.dom, 'alt', alt))}
|
||||||
|
|
||||||
|
height(): number;
|
||||||
|
height(height: number): this;
|
||||||
|
height(height?: number) { return $.fluent(this, arguments, () => this.dom.height, () => $.set(this.dom, 'height', height))}
|
||||||
|
|
||||||
|
width(): number;
|
||||||
|
width(wdith: number): this;
|
||||||
|
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: boolean): this;
|
||||||
|
checked(boolean?: boolean) { return $.fluent(this, arguments, () => this.dom.checked, () => $.set(this.dom, 'checked', boolean))}
|
||||||
|
|
||||||
|
max(): string;
|
||||||
|
max(max: string): this;
|
||||||
|
max(max?: string) { return $.fluent(this, arguments, () => this.dom.max, () => $.set(this.dom, 'max', max))}
|
||||||
|
|
||||||
|
min(): string;
|
||||||
|
min(min: string): this;
|
||||||
|
min(min?: string) { return $.fluent(this, arguments, () => this.dom.min, () => $.set(this.dom, 'min', min))}
|
||||||
|
|
||||||
|
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))}
|
||||||
|
|
||||||
|
defaultChecked(): boolean;
|
||||||
|
defaultChecked(defaultChecked: boolean): this;
|
||||||
|
defaultChecked(defaultChecked?: boolean) { return $.fluent(this, arguments, () => this.dom.defaultChecked, () => $.set(this.dom, 'defaultChecked', defaultChecked))}
|
||||||
|
|
||||||
|
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))}
|
||||||
|
|
||||||
|
multiple(): boolean;
|
||||||
|
multiple(multiple: boolean): this;
|
||||||
|
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(pattern: string): this;
|
||||||
|
pattern(pattern?: string) { return $.fluent(this, arguments, () => this.dom.pattern, () => $.set(this.dom, 'pattern', pattern))}
|
||||||
|
|
||||||
|
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 | null;
|
||||||
|
selectionDirection(selectionDirection: SelectionDirection | null): this;
|
||||||
|
selectionDirection(selectionDirection?: SelectionDirection | null) { return $.fluent(this, arguments, () => this.dom.selectionDirection, () => $.set(this.dom, 'selectionDirection', selectionDirection))}
|
||||||
|
|
||||||
|
selectionEnd(): number | null;
|
||||||
|
selectionEnd(selectionEnd: number | null): this;
|
||||||
|
selectionEnd(selectionEnd?: number | null) { return $.fluent(this, arguments, () => this.dom.selectionEnd, () => $.set(this.dom, 'selectionEnd', selectionEnd))}
|
||||||
|
|
||||||
|
selectionStart(): number | null;
|
||||||
|
selectionStart(selectionStart: number | null): this;
|
||||||
|
selectionStart(selectionStart?: number | null) { return $.fluent(this, arguments, () => this.dom.selectionStart, () => $.set(this.dom, 'selectionStart', selectionStart))}
|
||||||
|
|
||||||
|
size(): number;
|
||||||
|
size(size: number): this;
|
||||||
|
size(size?: number) { return $.fluent(this, arguments, () => this.dom.size, () => $.set(this.dom, 'size', size))}
|
||||||
|
|
||||||
|
src(): string;
|
||||||
|
src(src: string): this;
|
||||||
|
src(src?: string) { return $.fluent(this, arguments, () => this.dom.src, () => $.set(this.dom, 'src', src))}
|
||||||
|
|
||||||
|
step(): string;
|
||||||
|
step(step: string): this;
|
||||||
|
step(step?: string) { return $.fluent(this, arguments, () => this.dom.step, () => $.set(this.dom, 'step', step))}
|
||||||
|
|
||||||
|
type(): InputType;
|
||||||
|
type(type: InputType): this;
|
||||||
|
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: Date | null): this;
|
||||||
|
valueAsDate(date?: Date | null) { return $.fluent(this, arguments, () => this.dom.valueAsDate, () => $.set(this.dom, 'valueAsDate', date))}
|
||||||
|
|
||||||
|
valueAsNumber(): number;
|
||||||
|
valueAsNumber(number: number): this;
|
||||||
|
valueAsNumber(number?: number) { return $.fluent(this, arguments, () => this.dom.valueAsNumber, () => $.set(this.dom, 'valueAsNumber', number))}
|
||||||
|
|
||||||
|
webkitdirectory(): boolean;
|
||||||
|
webkitdirectory(boolean: boolean): this;
|
||||||
|
webkitdirectory(boolean?: boolean) { return $.fluent(this, arguments, () => this.dom.webkitdirectory, () => $.set(this.dom, 'webkitdirectory', boolean))}
|
||||||
|
|
||||||
|
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 }
|
||||||
|
showPicker() { this.dom.showPicker(); return this }
|
||||||
|
stepDown() { this.dom.stepDown(); return this }
|
||||||
|
stepUp() { this.dom.stepUp(); return this }
|
||||||
|
|
||||||
|
checkValidity() { return this.dom.checkValidity() }
|
||||||
|
reportValidity() { return this.dom.reportValidity() }
|
||||||
|
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 willValidate() { return this.dom.willValidate }
|
||||||
|
}
|
14
lib/$Label.ts
Normal file
14
lib/$Label.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { $Container, $ContainerOptions } from "./$Container";
|
||||||
|
export interface $LabelOptions extends $ContainerOptions {}
|
||||||
|
export class $Label extends $Container<HTMLLabelElement> {
|
||||||
|
constructor(options: $LabelOptions) {
|
||||||
|
super('label', options);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(): string;
|
||||||
|
for(name: string): this;
|
||||||
|
for(name?: string | undefined) { return $.fluent(this, arguments, () => this.dom.htmlFor, () => {if (name) this.dom.htmlFor = name}) }
|
||||||
|
|
||||||
|
get form() { return this.dom.form }
|
||||||
|
get control() { return this.dom.control }
|
||||||
|
}
|
25
lib/$Node.ts
Normal file
25
lib/$Node.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { $Container } from "./$Container";
|
||||||
|
|
||||||
|
export abstract class $Node<N extends Node = Node> {
|
||||||
|
readonly parent?: $Container;
|
||||||
|
abstract readonly dom: N;
|
||||||
|
constructor() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
on<K extends keyof HTMLElementEventMap>(type: K, callback: EventListenerOrEventListenerObject | null, options?: AddEventListenerOptions | boolean) {
|
||||||
|
this.dom.addEventListener(type, callback, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
off<K extends keyof HTMLElementEventMap>(type: K, callback: EventListenerOrEventListenerObject | null, 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) => {
|
||||||
|
this.dom.removeEventListener(type, onceFn, options)
|
||||||
|
callback(event);
|
||||||
|
};
|
||||||
|
this.dom.addEventListener(type, onceFn, options)
|
||||||
|
}
|
||||||
|
}
|
9
lib/$Text.ts
Normal file
9
lib/$Text.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { $Node } from "./$Node";
|
||||||
|
|
||||||
|
export class $Text extends $Node<Text> {
|
||||||
|
dom: Text;
|
||||||
|
constructor(data: string) {
|
||||||
|
super();
|
||||||
|
this.dom = new Text(data);
|
||||||
|
}
|
||||||
|
}
|
17
lib/Router/Route.ts
Normal file
17
lib/Router/Route.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { $Node } from "../$Node";
|
||||||
|
|
||||||
|
export class Route<Path extends string = string> {
|
||||||
|
path: string;
|
||||||
|
builder: (path: PathParams<Path>) => string | $Node;
|
||||||
|
constructor(path: Path, builder: (params: PathParams<Path>) => $Node | string) {
|
||||||
|
if (!path.startsWith('/')) throw new Error('PATH SHOULD START WITH /')
|
||||||
|
this.path = path;
|
||||||
|
this.builder = builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PathParams<Path> = Path extends `${infer Segment}/${infer Rest}`
|
||||||
|
? Segment extends `:${infer Param}` ? Record<Param, string> & PathParams<Rest> : PathParams<Rest>
|
||||||
|
: Path extends `:${infer Param}` ? Record<Param,string> : {}
|
||||||
|
|
||||||
|
type A = PathParams<'/:userId/post/:postId'>
|
119
lib/Router/Router.ts
Normal file
119
lib/Router/Router.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { $Container } from "../$Container";
|
||||||
|
import { $EventManager, $EventMethod, EventMethod } from "../$EventManager";
|
||||||
|
import { $Node } from "../$Node";
|
||||||
|
import { $Text } from "../$Text";
|
||||||
|
import { Route } from "./Route";
|
||||||
|
export interface Router extends $EventMethod<RouterEventMap> {};
|
||||||
|
@EventMethod
|
||||||
|
export class Router {
|
||||||
|
routeMap = new Map<string, Route<any>>();
|
||||||
|
contentMap = new Map<string, $Node>();
|
||||||
|
view: $Container;
|
||||||
|
index: number = 0;
|
||||||
|
events = new $EventManager<RouterEventMap>().register('pathchange', 'notfound');
|
||||||
|
basePath: string;
|
||||||
|
constructor(basePath: string, view: $Container) {
|
||||||
|
this.basePath = basePath;
|
||||||
|
this.view = view
|
||||||
|
$.routers.add(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**Add route to Router. @example Router.addRoute(new Route('/', 'Hello World')) */
|
||||||
|
addRoute(routes: OrArray<Route<any>>) {
|
||||||
|
routes = $.multableResolve(routes);
|
||||||
|
for (const route of routes) this.routeMap.set(route.path, route);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**Start listen to the path change */
|
||||||
|
listen() {
|
||||||
|
if (!history.state || 'index' in history.state === false) {
|
||||||
|
const routeData: RouteData = {index: this.index}
|
||||||
|
history.replaceState(routeData, '')
|
||||||
|
} else {
|
||||||
|
this.index = history.state.index
|
||||||
|
}
|
||||||
|
addEventListener('popstate', this.popstate)
|
||||||
|
this.resolvePath();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**Open path */
|
||||||
|
open(path: string | URL) {
|
||||||
|
if (path instanceof URL) path = path.pathname;
|
||||||
|
if (path === location.pathname) return this;
|
||||||
|
this.index += 1;
|
||||||
|
const routeData: RouteData = { index: this.index };
|
||||||
|
history.pushState(routeData, '', path);
|
||||||
|
$.routers.forEach(router => router.resolvePath())
|
||||||
|
this.events.fire('pathchange', path, 'Forward');
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**Back to previous page */
|
||||||
|
back() { history.back(); }
|
||||||
|
|
||||||
|
private popstate = (() => {
|
||||||
|
// Forward
|
||||||
|
if (history.state.index > this.index) { }
|
||||||
|
// Back
|
||||||
|
else if (history.state.index < this.index) { }
|
||||||
|
this.index = history.state.index;
|
||||||
|
this.resolvePath();
|
||||||
|
this.events.fire('pathchange', location.pathname, 'Forward');
|
||||||
|
}).bind(this)
|
||||||
|
|
||||||
|
private resolvePath(path = location.pathname) {
|
||||||
|
if (!path.startsWith(this.basePath)) return;
|
||||||
|
path = path.replace(this.basePath, '/').replace('//', '/')
|
||||||
|
let found = false;
|
||||||
|
const openCached = () => {
|
||||||
|
const cacheContent = this.contentMap.get(path);
|
||||||
|
if (cacheContent) {
|
||||||
|
this.view.content(cacheContent);
|
||||||
|
found = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const create = (content: $Node | string) => {
|
||||||
|
if (typeof content === 'string') content = new $Text(content);
|
||||||
|
this.contentMap.set(path, content)
|
||||||
|
this.view.content(content);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
for (const route of this.routeMap.values()) {
|
||||||
|
const [_routeParts, _pathParts] = [route.path.split('/').map(p => `/${p}`), path.split('/').map(p => `/${p}`)];
|
||||||
|
_routeParts.shift(); _pathParts.shift();
|
||||||
|
const data = {};
|
||||||
|
for (let i = 0; i < _pathParts.length; i++) {
|
||||||
|
const [routePart, pathPart] = [_routeParts.at(i), _pathParts.at(i)];
|
||||||
|
if (!routePart || !pathPart) continue;
|
||||||
|
if (routePart === pathPart) {
|
||||||
|
if (routePart === _routeParts.at(-1)) {
|
||||||
|
if (!openCached()) create(route.builder(data));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (routePart.includes(':')) {
|
||||||
|
Object.assign(data, {[routePart.split(':')[1]]: pathPart.replace('/', '')})
|
||||||
|
if (routePart === _routeParts.at(-1)) {
|
||||||
|
if (!openCached()) create(route.builder(data));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) this.events.fire('notfound', path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interface RouterEventMap {
|
||||||
|
pathchange: [path: string, navigation: 'Back' | 'Forward'];
|
||||||
|
notfound: [path: string]
|
||||||
|
}
|
||||||
|
|
||||||
|
type RouteData = {
|
||||||
|
index: number;
|
||||||
|
data?: any;
|
||||||
|
}
|
23
package.json
Normal file
23
package.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "fluent.ts",
|
||||||
|
"description": "Front-end builder library",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"type": "module",
|
||||||
|
"module": "index.ts",
|
||||||
|
"main": "index.ts",
|
||||||
|
"author": {
|
||||||
|
"name": "defaultkavy",
|
||||||
|
"email": "defaultkavy@gmail.com",
|
||||||
|
"url": "https://github.com/defaultkavy"
|
||||||
|
},
|
||||||
|
"keywords": ["web", "front-end", "lib", "fluent", "framework"],
|
||||||
|
"homepage": "https://github.com/defaultkavy/fluent",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/defaultkavy/fluent.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/defaultkavy/fluent/issues"
|
||||||
|
},
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["dom", "ES2022"],
|
||||||
|
"module": "esnext",
|
||||||
|
"target": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"composite": true,
|
||||||
|
"strict": true,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"experimentalDecorators": true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user