- remove: class $EventMethod, $Event.
- change: $EventManager rewrite logic.
- change: most $Node base node element generic type have new EM(EventMap) parameter.
- new: $EventTarget.
- change: $Node extends $EventTarget.
- change: mix dom events and $EventManager events into $EventTarget.on().off().once().
- fix: $Container.insert() process synchronous when passing an not async function
- new: $Window element.
- fix: $(document.documentElement) throw error.
- new: $KeyboardManager, $FocusManager, $PointerManager.
- new: $ global methods:
  - $.events() return new $EventManager.
  - $.pointers() return new $PointerManager.
  - $.keys() return new $KeyboardManager.
  - $.focus() return new $FocusManager.
  - $.call()
- change: $Media extends $HTMLElement
- change: $Anchor.href() support $State parameter.
- new: $State.convert()
This commit is contained in:
defaultkavy 2024-10-17 11:54:28 +08:00
parent a57246e6e1
commit 4c078c26b6
Signed by: defaultkavy
GPG Key ID: DFBB22C4E69D7826
19 changed files with 545 additions and 138 deletions

View File

@ -1,4 +1,4 @@
import { $EventManager, $State, $StateArgument, $StateOption } from "./index"; import { $EventManager, $EventMap, $EventTarget, $FocusManager, $PointerManager, $State, $StateArgument, $StateOption } from "./index";
import { $Node } from "./lib/node/$Node" import { $Node } from "./lib/node/$Node"
import { $Document } from "./lib/node/$Document" import { $Document } from "./lib/node/$Document"
import { $Anchor } from "./lib/node/$Anchor"; import { $Anchor } from "./lib/node/$Anchor";
@ -19,6 +19,8 @@ import { $Util } from "./lib/$Util";
import { $HTMLElement } from "./lib/node/$HTMLElement"; import { $HTMLElement } from "./lib/node/$HTMLElement";
import { $Async } from "./lib/node/$Async"; import { $Async } from "./lib/node/$Async";
import { $Video } from "./lib/node/$Video"; import { $Video } from "./lib/node/$Video";
import { $Window } from "./lib/$Window";
import { $KeyboardManager } from "./lib/$KeyboardManager";
export type $ = typeof $; export type $ = typeof $;
export function $<E extends $Element = $Element>(query: `::${string}`): E[]; export function $<E extends $Element = $Element>(query: `::${string}`): E[];
@ -26,6 +28,7 @@ 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;
export function $(window: Window): $Window;
export function $<H extends HTMLElement>(htmlElement: H): $.$HTMLElementMap<H>; export function $<H extends HTMLElement>(htmlElement: H): $.$HTMLElementMap<H>;
export function $<H extends Element>(element: H): $Element; export function $<H extends Element>(element: H): $Element;
export function $<N extends $Node>(node: N): N; export function $<N extends $Node>(node: N): N;
@ -51,6 +54,7 @@ export function $(resolver: any) {
if (resolver.$) return resolver.$; if (resolver.$) return resolver.$;
else return $Util.from(resolver); else return $Util.from(resolver);
} }
if (resolver instanceof Window) { return $Window.$ }
throw `$: NOT SUPPORT TARGET ELEMENT TYPE ('${resolver}')` throw `$: NOT SUPPORT TARGET ELEMENT TYPE ('${resolver}')`
} }
export namespace $ { export namespace $ {
@ -238,7 +242,10 @@ export namespace $ {
return $.TagNameElementMap; return $.TagNameElementMap;
} }
export function events<N extends string>(...eventname: N[]) { return new $EventManager<{[keys in N]: any[]}>().register(...eventname) } export function events<EM extends $EventMap>() { return new $EventManager<EM> }
export function pointers($node: $Node) { return new $PointerManager($node) }
export function keys($target: $EventTarget) { return new $KeyboardManager($target) }
export function focus() { return new $FocusManager() }
export function call<T>(fn: () => T): T { return fn() } export function call<T>(fn: () => T): T { return fn() }
} }

View File

@ -86,5 +86,6 @@ value$.set(0)
``` ```
## Extensions ## Extensions
1. [@elexis/router](https://github.com/elexisjs/router): Router for Single Page App. 1. [@elexis/router](https://git.defaultkavy.com/elexis/router): Router for Single Page App.
2. [@elexis/layout](https://github.com/elexisjs/layout): Build waterfall/justified layout with automatic compute content size and position. 2. [@elexis/layout](https://git.defaultkavy.com/elexis/layout): Build waterfall/justified layout with automatic compute content size and position.
3. [@elexis/view](https://git.defaultkavy.com/elexis/view): Multiple content switch handler.

View File

@ -3,6 +3,10 @@ declare global {
interface Array<T> { interface Array<T> {
detype<F extends any, O>(...types: F[]): Array<Exclude<T, F | undefined | void>> detype<F extends any, O>(...types: F[]): Array<Exclude<T, F | undefined | void>>
} }
interface Set<T> {
get array(): T[]
sort(handler: ((a: T, b: T) => number) | undefined): T[];
}
type OrMatrix<T> = T | OrMatrix<T>[]; type OrMatrix<T> = T | OrMatrix<T>[];
type OrArray<T> = T | T[]; type OrArray<T> = T | T[];
type OrPromise<T> = T | Promise<T>; type OrPromise<T> = T | Promise<T>;
@ -29,17 +33,27 @@ Array.prototype.detype = function <T extends any, O>(this: O[], ...types: T[]) {
else for (const type of types) if (typeof item !== typeof type) return true; else return false; else for (const type of types) if (typeof item !== typeof type) return true; else return false;
}) as Exclude<O, T | undefined | void>[]; }) as Exclude<O, T | undefined | void>[];
} }
Object.defineProperties(Set.prototype, {
array: { get: function <T>(this: Set<T>) { return Array.from(this)} }
})
Set.prototype.sort = function <T>(this: Set<T>, handler: ((a: T, b: T) => number) | undefined) { return this.array.sort(handler)}
export * from "./$index"; export * from "./$index";
export * from "./lib/$NodeManager";
export * from "./lib/$EventManager";
export * from "./lib/$EventTarget";
export * from "./lib/$KeyboardManager";
export * from "./lib/$FocusManager";
export * from "./lib/$PointerManager";
export * from "./lib/$Window";
export * from "./lib/$State";
export * from "./lib/node/$Node"; export * from "./lib/node/$Node";
export * from "./lib/node/$Anchor"; export * from "./lib/node/$Anchor";
export * from "./lib/node/$Element"; export * from "./lib/node/$Element";
export * from "./lib/$NodeManager"; export * from "./lib/node/$HTMLElement";
export * from "./lib/node/$Text"; export * from "./lib/node/$Text";
export * from "./lib/node/$Container"; export * from "./lib/node/$Container";
export * from "./lib/node/$Button"; export * from "./lib/node/$Button";
export * from "./lib/node/$Form"; export * from "./lib/node/$Form";
export * from "./lib/$EventManager";
export * from "./lib/$State";
export * from "./lib/node/$Select"; export * from "./lib/node/$Select";
export * from "./lib/node/$Option"; export * from "./lib/node/$Option";
export * from "./lib/node/$OptGroup"; export * from "./lib/node/$OptGroup";

View File

@ -1,65 +1,32 @@
export abstract class $EventMethod<EM> { export class $EventManager<EM extends $EventMap> {
abstract events: $EventManager<EM>; private eventMap = new Map<string, Set<Function>>();
//@ts-expect-error
on<K extends keyof EM>(type: K, callback: (...args: EM[K]) => any) { this.events.on(type, callback); return this }
//@ts-expect-error
off<K extends keyof EM>(type: K, callback: (...args: EM[K]) => any) { this.events.off(type, callback); return this }
//@ts-expect-error
once<K extends keyof EM>(type: K, callback: (...args: EM[K]) => any) { this.events.once(type, callback); return this }
}
export class $EventManager<EM> {
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 //@ts-expect-error
fire<K extends keyof EM>(type: K, ...args: EM[K]) { fire<K extends keyof EM>(type: K, ...args: EM[K]) {
const event = this.get(type) this.eventMap.get(type as string)?.forEach(fn => fn(...args as []));
//@ts-expect-error
if (event instanceof $Event) event.fire(...args);
return this return this
} }
//@ts-expect-error //@ts-expect-error
on<K extends keyof EM>(type: K, callback: (...args: EM[K]) => any) { on<K extends keyof EM>(type: K, callback: (...args: EM[K]) => any) {
this.get(type).add(callback); const set = this.eventMap.get(type as string) ?? this.eventMap.set(type as string, new Set()).get(type as string);
set?.add(callback);
return this return this
} }
//@ts-expect-error //@ts-expect-error
off<K extends keyof EM>(type: K, callback: (...args: EM[K]) => any) { off<K extends keyof EM>(type: K, callback: (...args: EM[K]) => any) {
this.get(type).delete(callback); this.eventMap.get(type as string)?.delete(callback);
return this return this
} }
//@ts-expect-error //@ts-expect-error
once<K extends keyof EM>(type: K, callback: (...args: EM[K]) => any) { once<K extends keyof EM>(type: K, callback: (...args: EM[K]) => any) {
//@ts-expect-error const onceFn = (...args: []) => {
const onceFn = (...args: EM[K]) => { this.eventMap.get(type as string)?.delete(onceFn);
this.get(type).delete(onceFn);
//@ts-expect-error //@ts-expect-error
callback(...args); callback(...args);
} }
this.get(type).add(onceFn); const set = this.eventMap.get(type as string) ?? this.eventMap.set(type as string, new Set()).get(type as string)
set?.add(onceFn);
return this; return this;
} }
}
get<K extends keyof EM>(type: K) { export interface $EventMap {}
//@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) }
}

50
lib/$EventTarget.ts Normal file
View File

@ -0,0 +1,50 @@
import { $EventManager, $EventMap } from "./$EventManager";
export abstract class $EventTarget<$EM extends $EventMap = $EventMap, EM extends GlobalEventHandlersEventMap = GlobalEventHandlersEventMap> {
private domEvents: Partial<{[key in keyof EM]: Map<Function, Function>}> = {};
readonly events = new $EventManager<$EM>();
abstract dom: EventTarget
//@ts-expect-error
on<K extends keyof $EM>(type: K | K[], callback: (...args: $EM[K]) => any): this;
on<K extends keyof EM>(type: K | K[], callback: (event: EM[K], $node: this) => any, options?: AddEventListenerOptions | boolean): this;
on<K extends string>(type: K | K[], callback: (event: Event, $node: this) => any, options?: AddEventListenerOptions | boolean): this;
on<K extends keyof EM>(types: K | K[], callback: (event: EM[K], $node: this) => any, options?: AddEventListenerOptions | boolean) {
types = $.orArrayResolve(types);
for (const type of types) {
if (!this.domEvents[type]) this.domEvents[type] = new Map()
const handler = (e: Event) => { callback(e as EM[K], this); }
this.domEvents[type].set(callback, handler);
this.events.on(type as any, callback);
this.dom.addEventListener(type as string, handler, options);
}
return this;
}
//@ts-expect-error
off<K extends keyof $EM>(type: K, callback: (...args: $EM[K]) => any): this;
off<K extends keyof EM>(type: K, callback: (event: EM[K], $node: this) => any, options?: AddEventListenerOptions | boolean): this;
off<K extends string>(type: K, callback: (event: Event, $node: this) => any, options?: AddEventListenerOptions | boolean): this;
off<K extends keyof EM>(type: K, callback: (event: EM[K], $node: this) => any, options?: AddEventListenerOptions | boolean) {
const middleCallback = this.domEvents[type]?.get(callback);
if (middleCallback) this.dom.removeEventListener(type as string, middleCallback as EventListener, options);
this.events.off(type as any, callback);
return this;
}
//@ts-expect-error
once<K extends keyof $EM>(type: K, callback: (...args: $EM[K]) => any): this;
once<K extends keyof EM>(type: K, callback: (event: EM[K], $node: this) => any, options?: AddEventListenerOptions | boolean): this;
once<K extends string>(type: K, callback: (event: Event, $node: this) => any, options?: AddEventListenerOptions | boolean): this;
once<K extends keyof EM>(type: K, callback: (event: EM[K], $node: this) => any, options?: AddEventListenerOptions | boolean) {
const onceFn = (event: Event) => {
this.dom.removeEventListener(type as string, onceFn, options)
callback(event as EM[K], this);
};
this.dom.addEventListener(type as string, onceFn, options);
this.events.once(type as any, callback);
return this;
}
trigger(event: string) { this.dom.dispatchEvent(new Event(event)); return }
}

175
lib/$FocusManager.ts Normal file
View File

@ -0,0 +1,175 @@
import { $Element } from "..";
export class $FocusManager {
layerMap = new Map<number, $FocusLayer>();
currentLayer?: $FocusLayer;
historyList: $FocusLayer[] = [];
constructor() {}
layer(id: number) {
const layer = this.layerMap.get(id) ?? new $FocusLayer(id);
this.layerMap.set(layer.id, layer);
return layer;
}
next() { return this.select($FocusNavigation.Next) }
prev() { return this.select($FocusNavigation.Prev) }
up() { return this.select($FocusNavigation.Up) }
down() { return this.select($FocusNavigation.Down) }
right() { return this.select($FocusNavigation.Right) }
left() { return this.select($FocusNavigation.Left) }
blur() {
this.currentLayer?.blur();
return this;
}
select(navigation: $FocusNavigation) {
this.currentLayer = this.currentLayer ?? [...this.layerMap.values()].at(0);
if (!this.currentLayer) return this;
const $focused = this.currentLayer.currentFocus;
const eleList = this.currentLayer.elementSet.array;
if (!$focused) { this.currentLayer.focus(this.currentLayer.beforeBlur ?? eleList.at(0)); return this; }
const eleIndex = eleList.indexOf($focused)
switch (navigation) {
case $FocusNavigation.Next:
case $FocusNavigation.Prev: {
let targetIndex = navigation === 0 ? eleIndex + 1 : eleIndex - 1;
if (targetIndex === eleList.length && this.currentLayer.loop()) targetIndex = 0;
else if (targetIndex === -1 && !this.currentLayer.loop()) targetIndex = 0;
this.currentLayer.focus(eleList.at(targetIndex));
break;
}
case $FocusNavigation.Down:
case $FocusNavigation.Left:
case $FocusNavigation.Right:
case $FocusNavigation.Up: {
const focusedPosition = $focused.coordinate();
if (!focusedPosition) break;
const focusedCoordinate = $.call(() => {
switch (navigation) {
case $FocusNavigation.Up: return {y: focusedPosition.y, x: focusedPosition.x / 2}
case $FocusNavigation.Down: return {y: focusedPosition.y + focusedPosition.height, x: focusedPosition.x / 2}
case $FocusNavigation.Left: return {y: focusedPosition.y / 2, x: focusedPosition.x}
case $FocusNavigation.Right: return {y: focusedPosition.y / 2, x: focusedPosition.x + focusedPosition.width}
}
})
const eleInfoList = eleList.map($ele => {
if ($ele === $focused) return;
const elePosition = $ele.coordinate();
if (!elePosition) return;
const eleCoordinate = $.call(() => {
switch (navigation) {
case $FocusNavigation.Up: return {y: elePosition.y + elePosition.height, x: elePosition.x / 2};
case $FocusNavigation.Down: return {y: elePosition.y, x: elePosition.x / 2};
case $FocusNavigation.Left: return {y: elePosition.y / 2, x: elePosition.x + elePosition.width};
case $FocusNavigation.Right: return {y: elePosition.y / 2, x: elePosition.x};
}
})
return {
$ele, elePosition,
distance: Math.sqrt((eleCoordinate.x - focusedCoordinate.x) ** 2 + (eleCoordinate.y - focusedCoordinate.y) ** 2)
}
}).detype(undefined).filter(({elePosition}) => {
switch (navigation) {
case $FocusNavigation.Up: if (elePosition.y + elePosition.height >= focusedPosition.y) return false; break;
case $FocusNavigation.Down: if (elePosition.y <= focusedPosition.y + focusedPosition.height) return false; break;
case $FocusNavigation.Left: if (elePosition.x + elePosition.width >= focusedPosition.x) return false; break;
case $FocusNavigation.Right: if (elePosition.x <= focusedPosition.x + focusedPosition.width) return false; break;
}
return true;
})
const $target = eleInfoList.sort((a, b) => a.distance - b.distance).at(0)?.$ele;
this.currentLayer.focus($target);
}
}
return this;
}
}
export enum $FocusNavigation { Next, Prev, Up, Down, Right, Left }
export class $FocusLayer {
id: number;
elementSet = new Set<$Element>();
entrySet = new Set<$Element>();
beforeBlur?: $Element;
currentFocus?: $Element;
private __$property__ = {
loop: true,
scrollThreshold: 0
}
constructor(id: number) {
this.id = id
this.add = this.add.bind(this);
this.entry = this.entry.bind(this);
}
add($elements: OrArray<$Element>) {
$.orArrayResolve($elements).forEach($element => {
this.elementSet.add($element);
$element.tabIndex(0);
});
return this;
}
remove($element: $Element) {
this.elementSet.delete($element);
return this;
}
entry($elements: OrArray<$Element>) {
$.orArrayResolve($elements).forEach(this.entrySet.add.bind(this.entrySet))
return this;
}
focus($element: $Element | undefined) {
if (!$element) return this;
$element.hide(false);
const {scrollTop, scrollLeft} = document.documentElement;
const position = $.call(() => {
const rect = $element.domRect()
return {
left: rect.left + scrollLeft,
top: rect.top + scrollTop,
right: rect.right + scrollLeft,
bottom: rect.bottom + scrollTop,
height: rect.height,
width: rect.width
}
})
const {scrollThreshold} = this.__$property__;
this.blur();
this.currentFocus = $element;
if (scrollTop > position.top - scrollThreshold // scroll after item threshold
|| scrollTop > position.bottom + scrollThreshold
) document.documentElement.scrollTo({left: position.left - scrollThreshold, top: position.top - scrollThreshold});
if (scrollTop + innerHeight < position.top + scrollThreshold // scroll before item
|| scrollTop + innerHeight < position.bottom + scrollThreshold
) document.documentElement.scrollTo({left: position.left - scrollThreshold, top: (position.bottom - innerHeight) + scrollThreshold});
$element.attribute('focus', '')
$element.focus({preventScroll: true});
return this;
}
blur() {
if (!this.currentFocus) return this;
this.beforeBlur = this.currentFocus;
this.currentFocus.attribute('focus', null);
this.currentFocus?.blur();
this.currentFocus = undefined;
return this;
}
removeAll() {
this.elementSet.clear();
return this;
}
loop(): boolean;
loop(boolean: boolean): this;
loop(boolean?: boolean) { return $.fluent(this, arguments, () => this.__$property__.loop, () => $.set(this.__$property__, 'loop', boolean)) }
scrollThreshold(): number;
scrollThreshold(number: number): this;
scrollThreshold(number?: number) { return $.fluent(this, arguments, () => this.__$property__.scrollThreshold, () => $.set(this.__$property__, 'scrollThreshold', number)) }
}

52
lib/$KeyboardManager.ts Normal file
View File

@ -0,0 +1,52 @@
import { $EventTarget } from "./$EventTarget";
import { $Util } from "./$Util";
export class $KeyboardManager {
keyMap = new Map<string, $KeyboardEventMap>();
protected conditional?: ((event: KeyboardEvent) => boolean | undefined);
constructor($element: $EventTarget) {
$element.on('keydown', e => { if (this.conditional && !this.conditional(e)) return; this.keyMap.get(e.key)?.keydown.forEach(fn => fn(e)) })
$element.on('keyup', e => { if (this.conditional && !this.conditional(e)) return; this.keyMap.get(e.key)?.keyup.forEach(fn => fn(e)) })
$element.on('keypress', e => { if (this.conditional && !this.conditional(e)) return; this.keyMap.get(e.key)?.keypress.forEach(fn => fn(e)) })
}
if(callback: (event: KeyboardEvent) => boolean | undefined) {
this.conditional = callback;
return this;
}
assigns(keys: OrArray<string>, on: OrArray<$KeyboardEventType>, callback: $KeyboardEventHandler) {
keys = $Util.orArrayResolve(keys);
on = $Util.orArrayResolve(on);
for (const key of keys) {
const eventData: $KeyboardEventMap = this.keyMap.get(key) ?? {keydown: new Set(), keypress: new Set(), keyup: new Set()};
for (const event of on) {
eventData[event].add(callback);
}
this.keyMap.set(key, eventData);
}
return this;
}
unassign(keys: OrArray<string>, on?: OrArray<$KeyboardEventType>, callback?: (event: KeyboardEvent) => void) {
keys = $Util.orArrayResolve(keys);
on = on ? $Util.orArrayResolve(on) : ['keydown', 'keypress', 'keyup'];
for (const key of keys) {
const eventData: $KeyboardEventMap = this.keyMap.get(key) ?? {keydown: new Set(), keypress: new Set(), keyup: new Set()};
for (const event of on) {
if (callback) eventData[event].delete(callback);
else eventData[event].clear();
}
this.keyMap.set(key, eventData);
}
return this;
}
keydown(keys: OrArray<string>, callback: $KeyboardEventHandler) { this.assigns(keys, 'keydown', callback); return this; }
keyup(keys: OrArray<string>, callback: $KeyboardEventHandler) { this.assigns(keys, 'keyup', callback); return this; }
keypress(keys: OrArray<string>, callback: $KeyboardEventHandler) { this.assigns(keys, 'keypress', callback); return this; }
}
export type $KeyboardEventType = 'keydown' | 'keyup' | 'keypress';
export type $KeyboardEventHandler = (event: KeyboardEvent) => void;
type $KeyboardEventMap = {[key in $KeyboardEventType]: Set<$KeyboardEventHandler>};

View File

@ -1,6 +1,5 @@
import { $Container } from "./node/$Container"; import { $Container } from "./node/$Container";
import { $Node } from "./node/$Node"; import { $Node } from "./node/$Node";
import { $Text } from "./node/$Text";
export class $NodeManager { export class $NodeManager {
readonly $container: $Container; readonly $container: $Container;
@ -12,13 +11,13 @@ export class $NodeManager {
add(element: $Node, position = -1) { add(element: $Node, position = -1) {
if (position === -1 || this.childList.size - 1 === position) { if (position === -1 || this.childList.size - 1 === position) {
this.childList.add(element); this.childList.add(element);
(element as Mutable<$Node>).parent = this.$container;
} else { } else {
const children = [...this.childList] const children = [...this.childList]
children.splice(position, 0, element); children.splice(position, 0, element);
this.childList.clear(); this.childList.clear();
children.forEach(child => this.childList.add(child)); children.forEach(child => this.childList.add(child));
} }
(element as Mutable<$Node>).parent = this.$container;
} }
remove(element: $Node) { remove(element: $Node) {
@ -39,6 +38,7 @@ export class $NodeManager {
target.remove(); target.remove();
this.childList.clear(); this.childList.clear();
array.forEach(node => this.childList.add(node)); array.forEach(node => this.childList.add(node));
(replace as Mutable<$Node>).parent = this.$container;
return this; return this;
} }
@ -49,18 +49,22 @@ export class $NodeManager {
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.$.hide()) 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.$.hide()) { this.dom.insertBefore(dom, node); appendedNodeList.push(dom) }
domList.shift(); domList.shift();
} }
else { else {
if (dom.$.__hidden) this.dom.removeChild(dom); if (dom.$.hide()) this.dom.removeChild(dom);
domList.shift(); nodeList.shift(); domList.shift(); nodeList.shift();
} }
} }
} }
indexOf(target: $Node) {
return this.array.indexOf(target);
}
get array() {return [...this.childList.values()]}; get array() {return [...this.childList.values()]};
get dom() {return this.$container.dom} get dom() {return this.$container.dom}

105
lib/$PointerManager.ts Normal file
View File

@ -0,0 +1,105 @@
import { $EventManager, $EventMap } from "./$EventManager";
import { type $Node } from "./node/$Node";
export class $PointerManager extends $EventManager<$PointerManagerEventMap> {
$node: $Node;
map = new Map<number, $Pointer>();
constructor($node: $Node) {
super();
this.$node = $node;
this.$node.on('pointerdown', (e) => this.down(e))
this.$node.on('pointerup', (e) => this.up(e))
this.$node.on('pointermove', (e) => this.move(e))
this.$node.on('pointercancel', (e) => this.cancel(e))
}
protected down(e: PointerEvent) {
const pointer = new $Pointer(this, this.toData(e), $(e.target!))
this.map.set(pointer.id, pointer);
this.fire('down', pointer, e);
}
protected up(e: PointerEvent) {
const pointer = this.map.get(e.pointerId);
if (!pointer) return;
this.map.delete(e.pointerId);
this.fire('up', pointer, e);
}
protected move(e: PointerEvent) {
const pointer = this.map.get(e.pointerId);
if (!pointer) return;
this.map.set(pointer.id, pointer);
pointer.update(this.toData(e));
this.fire('move', pointer, e);
}
protected cancel(e: PointerEvent) {
const pointer = this.map.get(e.pointerId);
if (!pointer) return;
pointer.update(this.toData(e));
this.map.delete(pointer.id);
this.fire('cancel', pointer, e);
}
protected toData(e: PointerEvent): $PointerData {
return {
id: e.pointerId,
type: e.pointerType as PointerType,
width: e.width,
height: e.height,
x: e.x,
y: e.y,
movement_x: e.movementX,
movement_y: e.movementY
}
}
}
export interface $PointerManagerEventMap extends $EventMap {
up: [$Pointer, MouseEvent];
down: [$Pointer, MouseEvent];
move: [$Pointer, MouseEvent];
cancel: [$Pointer, MouseEvent];
}
export interface $Pointer extends $PointerData {}
export class $Pointer {
initial_x: number;
initial_y: number;
$target: $Node;
protected manager: $PointerManager;
constructor(manager: $PointerManager, data: $PointerData, target: $Node) {
Object.assign(this, data);
this.manager = manager;
this.$target = target;
this.initial_x = data.x;
this.initial_y = data.y;
}
get move_x() { return this.x - this.initial_x }
get move_y() { return this.y - this.initial_y }
update(data: $PointerData) {
Object.assign(this, data);
return this;
}
delete() {
this.manager.map.delete(this.id);
return this;
}
}
export interface $PointerData {
id: number;
type: PointerType;
width: number;
height: number;
x: number;
y: number;
movement_x: number;
movement_y: number;
}
export type PointerType = 'mouse' | 'pen' | 'touch'

View File

@ -17,6 +17,16 @@ export class $State<T> {
this.linkStates.forEach($state => $state.update()); this.linkStates.forEach($state => $state.update());
} }
static toJSON(object: Object): Object {
const data = {};
for (let [key, value] of Object.entries(object)) {
if (value instanceof $State) value = value.toJSON();
else if (value instanceof Object) $State.toJSON(value);
Object.assign(data, {[key]: value})
}
return data;
}
protected update() { protected update() {
// update element content for eatch attributes // update element content for eatch attributes
for (const [node, attrList] of this.attributes.entries()) { for (const [node, attrList] of this.attributes.entries()) {
@ -36,37 +46,31 @@ export class $State<T> {
} }
} }
toString(): string {
if (this.options.format) return this.options.format(this.value);
if (this.value instanceof Object) return JSON.stringify(this.toJSON());
return `${this.value}`
}
use<O extends Object, K extends keyof O>(object: O, attrName: K) { use<O extends Object, K extends keyof O>(object: O, attrName: K) {
const attrList = this.attributes.get(object) const attrList = this.attributes.get(object)
if (attrList) attrList.add(attrName); if (attrList) attrList.add(attrName);
else this.attributes.set(object, new Set<string | number | symbol>().add(attrName)) else this.attributes.set(object, new Set<string | number | symbol>().add(attrName))
} }
convert(fn: (value: T) => string) {
return new $State<T>(this as any, {format: fn});
}
get value(): T {
return this._value instanceof $State ? this._value.value as T : this._value;
}
toString(): string {
if (this.options.format) return this.options.format(this.value);
if (this.value instanceof Object) return JSON.stringify(this.toJSON());
return `${this.value}`
}
toJSON(): Object { toJSON(): Object {
if (this.value instanceof $State) return this.value.toJSON(); if (this.value instanceof $State) return this.value.toJSON();
if (this.value instanceof Object) return $State.toJSON(this.value); if (this.value instanceof Object) return $State.toJSON(this.value);
else return this.toString(); else return this.toString();
} }
static toJSON(object: Object): Object {
const data = {};
for (let [key, value] of Object.entries(object)) {
if (value instanceof $State) value = value.toJSON();
else if (value instanceof Object) $State.toJSON(value);
Object.assign(data, {[key]: value})
}
return data;
}
get value(): T {
return this._value instanceof $State ? this._value.value as T : this._value;
}
}; };
export type $StateArgument<T> = $State<T> | undefined | (T extends (infer R)[] ? R : T); export type $StateArgument<T> = $State<T> | undefined | (T extends (infer R)[] ? R : T);

View File

@ -42,14 +42,17 @@ export namespace $Util {
export function from(element: Node): $Node { export function from(element: Node): $Node {
if (element.$) return element.$; if (element.$) return element.$;
if (element.nodeName.toLowerCase() === 'body') return new $Container('body', {dom: element as HTMLBodyElement}); if (element.nodeName.toLowerCase() === 'body') return new $Container('body', {dom: element as HTMLBodyElement});
if (element.nodeName.toLowerCase() === 'head') return new $Container('head', {dom: element as HTMLHeadElement});
if (element.nodeName.toLowerCase() === '#document') return $Document.from(element as Document); if (element.nodeName.toLowerCase() === '#document') return $Document.from(element as Document);
else if (element instanceof HTMLElement) { else if (element instanceof HTMLElement) {
const instance = $.TagNameElementMap[element.tagName.toLowerCase() as keyof typeof $.TagNameElementMap]; const instance = $.TagNameElementMap[element.tagName.toLowerCase() as keyof typeof $.TagNameElementMap];
const $node = instance === $Container const $node = !instance
//@ts-expect-error ? new $Container(element.tagName, {dom: element})
? new instance(element.tagName, {dom: element}) : instance === $Container
//@ts-expect-error //@ts-expect-error
: new instance({dom: element} as any); ? new instance(element.tagName, {dom: element})
//@ts-expect-error
: new instance({dom: element} as any);
if ($node instanceof $Container) for (const childnode of Array.from($node.dom.childNodes)) { if ($node instanceof $Container) for (const childnode of Array.from($node.dom.childNodes)) {
$node.children.add($(childnode as any)); $node.children.add($(childnode as any));
} }

9
lib/$Window.ts Normal file
View File

@ -0,0 +1,9 @@
import { $EventManager, $EventMap } from "./$EventManager";
import { $EventTarget } from "./$EventTarget";
export class $Window<EM extends $WindowEventMap = $WindowEventMap> extends $EventTarget<EM, WindowEventMap> {
static $ = new $Window();
readonly dom = window;
}
export interface $WindowEventMap extends $EventMap {}

View File

@ -1,3 +1,4 @@
import { $StateArgument } from "../$State";
import { $Container, $ContainerOptions } from "./$Container"; import { $Container, $ContainerOptions } from "./$Container";
export interface AnchorOptions extends $ContainerOptions {} export interface AnchorOptions extends $ContainerOptions {}
@ -15,8 +16,8 @@ export class $Anchor extends $Container<HTMLAnchorElement> {
} }
/**Set URL of anchor element. */ /**Set URL of anchor element. */
href(): string; href(): string;
href(url: string | undefined): this; href(url: $StateArgument<string>): this;
href(url?: string | undefined) { return $.fluent(this, arguments, () => this.dom.href, () => {if (url) this.dom.href = url}) } href(url?: $StateArgument<string>) { return $.fluent(this, arguments, () => this.dom.href, () => $.set(this.dom, 'href', url)) }
/**Link open with this window, new tab or other */ /**Link open with this window, new tab or other */
target(): $AnchorTarget | undefined; target(): $AnchorTarget | undefined;
target(target: $AnchorTarget | undefined): this; target(target: $AnchorTarget | undefined): this;

View File

@ -1,12 +1,12 @@
import { $Element, $ElementOptions } from "./$Element"; import { $Element } from "./$Element";
import { $NodeManager } from "../$NodeManager"; import { $NodeManager } from "../$NodeManager";
import { $Node } from "./$Node"; import { $Node } from "./$Node";
import { $State, $StateArgument } from "../$State"; import { $State, $StateArgument } from "../$State";
import { $Text } from "./$Text"; import { $Text } from "./$Text";
import { $HTMLElement, $HTMLElementOptions } from "./$HTMLElement"; import { $HTMLElement, $HTMLElementEventMap, $HTMLElementOptions } from "./$HTMLElement";
export interface $ContainerOptions extends $HTMLElementOptions {} export interface $ContainerOptions extends $HTMLElementOptions {}
export class $Container<H extends HTMLElement = HTMLElement> extends $HTMLElement<H> { export class $Container<H extends HTMLElement = HTMLElement, EM extends $ContainerEventMap = $ContainerEventMap> extends $HTMLElement<H, EM> {
readonly children: $NodeManager = new $NodeManager(this); readonly children: $NodeManager = new $NodeManager(this);
constructor(tagname: string, options?: $ContainerOptions) { constructor(tagname: string, options?: $ContainerOptions) {
super(tagname, options) super(tagname, options)
@ -23,7 +23,11 @@ export class $Container<H extends HTMLElement = HTMLElement> extends $HTMLElemen
private __position_cursor = 0; private __position_cursor = 0;
/**Insert element to this element */ /**Insert element to this element */
insert(children: $ContainerContentBuilder<this>, position = -1): this { return $.fluent(this, arguments, () => this, async () => { insert(children: $ContainerContentBuilder<this>, position = -1): this { return $.fluent(this, arguments, () => this, async () => {
if (children instanceof Function) children = await children(this); // resolve function if (children instanceof Function) { // resolve function and promise
let cache = children(this);
if (cache instanceof Promise) children = await cache;
else children = cache;
} else if (children instanceof Promise) { children = await children }
children = $.orArrayResolve(children); children = $.orArrayResolve(children);
// Set position cursor depend negative or positive number, position will count from last index when position is negative. // Set position cursor depend negative or positive number, position will count from last index when position is negative.
this.__position_cursor = position < 0 ? this.children.array.length + position : position; this.__position_cursor = position < 0 ? this.children.array.length + position : position;
@ -53,11 +57,12 @@ export class $Container<H extends HTMLElement = HTMLElement> extends $HTMLElemen
return this; return this;
} }
//**Query selector one of child element */ $<E extends $Element = $Element>(query: `::${string}`): E[];
$<E extends $Element>(query: string): E | null { return $(this.dom.querySelector(query)) as E | null } $<E extends $Element = $Element>(query: `:${string}`): E | null;
$(query: string) {
//**Query selector of child elements */ if (query.startsWith('::')) return Array.from(document.querySelectorAll(query.replace(/^::/, ''))).map(dom => $(dom));
$all<E extends $Element>(query: string): E[] { return Array.from(this.dom.querySelectorAll(query)).map($dom => $($dom) as E) } else if (query.startsWith(':')) return $(document.querySelector(query.replace(/^:/, '')));
}
get scrollHeight() { return this.dom.scrollHeight } get scrollHeight() { return this.dom.scrollHeight }
get scrollWidth() { return this.dom.scrollWidth } get scrollWidth() { return this.dom.scrollWidth }
@ -74,3 +79,5 @@ export class $Container<H extends HTMLElement = HTMLElement> extends $HTMLElemen
export type $ContainerContentBuilder<P extends $Container> = $ContainerContentGroup | (($node: P) => OrPromise<$ContainerContentGroup>) export type $ContainerContentBuilder<P extends $Container> = $ContainerContentGroup | (($node: P) => OrPromise<$ContainerContentGroup>)
export type $ContainerContentGroup = OrMatrix<OrPromise<$ContainerContentType>> export type $ContainerContentGroup = OrMatrix<OrPromise<$ContainerContentType>>
export type $ContainerContentType = $Node | string | undefined | $State<any> | null export type $ContainerContentType = $Node | string | undefined | $State<any> | null
export interface $ContainerEventMap extends $HTMLElementEventMap {}

View File

@ -1,4 +1,4 @@
import { $Node } from "./$Node"; import { $Node, $NodeEventMap } from "./$Node";
export interface $ElementOptions { export interface $ElementOptions {
id?: string; id?: string;
@ -7,7 +7,7 @@ export interface $ElementOptions {
tagname?: string; tagname?: string;
} }
export class $Element<H extends HTMLElement | SVGElement = HTMLElement> extends $Node<H> { export class $Element<H extends HTMLElement | SVGElement = HTMLElement, $EM extends $ElementEventMap = $ElementEventMap, EM extends HTMLElementEventMap = HTMLElementEventMap> extends $Node<H, $EM, EM> {
readonly dom: H; readonly dom: H;
private static_classes = new Set<string>(); private static_classes = new Set<string>();
constructor(tagname: string, options?: $ElementOptions) { constructor(tagname: string, options?: $ElementOptions) {
@ -81,7 +81,7 @@ export class $Element<H extends HTMLElement | SVGElement = HTMLElement> extends
tabIndex(tabIndex: number): this; tabIndex(tabIndex: number): this;
tabIndex(tabIndex?: number) { return $.fluent(this, arguments, () => this.dom.tabIndex, () => $.set(this.dom, 'tabIndex', tabIndex as any))} tabIndex(tabIndex?: number) { return $.fluent(this, arguments, () => this.dom.tabIndex, () => $.set(this.dom, 'tabIndex', tabIndex as any))}
focus() { this.dom.focus(); return this; } focus(options?: FocusOptions) { this.dom.focus(options); return this; }
blur() { this.dom.blur(); return this; } blur() { this.dom.blur(); return this; }
animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions, callback?: (animation: Animation) => void) { animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions, callback?: (animation: Animation) => void) {
@ -112,3 +112,5 @@ export class $Element<H extends HTMLElement | SVGElement = HTMLElement> extends
} }
export type $DOMRect = Omit<DOMRect, 'toJSON'>; export type $DOMRect = Omit<DOMRect, 'toJSON'>;
export interface $ElementEventMap extends $NodeEventMap {}

View File

@ -1,7 +1,7 @@
import { $Element, $ElementOptions } from "./$Element"; import { $Element, $ElementEventMap, $ElementOptions } from "./$Element";
export interface $HTMLElementOptions extends $ElementOptions {} export interface $HTMLElementOptions extends $ElementOptions {}
export class $HTMLElement<H extends HTMLElement = HTMLElement> extends $Element<H> { export class $HTMLElement<H extends HTMLElement = HTMLElement, $EM extends $HTMLElementEventMap = $HTMLElementEventMap> extends $Element<H, $EM> {
constructor(tagname: string, options?: $HTMLElementOptions) { constructor(tagname: string, options?: $HTMLElementOptions) {
super(tagname, options) super(tagname, options)
} }
@ -63,3 +63,5 @@ export class $HTMLElement<H extends HTMLElement = HTMLElement> extends $Element<
get offsetTop() { return this.dom.offsetTop } get offsetTop() { return this.dom.offsetTop }
get offsetWidth() { return this.dom.offsetWidth } get offsetWidth() { return this.dom.offsetWidth }
} }
export interface $HTMLElementEventMap extends $ElementEventMap {}

View File

@ -1,8 +1,9 @@
import { $State, $StateArgument } from "../$State"; import { $StateArgument } from "../$State";
import { $Element, $ElementOptions } from "./$Element"; import { $ElementOptions } from "./$Element";
import { $HTMLElement } from "./$HTMLElement";
export interface $MediaOptions extends $ElementOptions {} export interface $MediaOptions extends $ElementOptions {}
export class $Media<H extends HTMLMediaElement> extends $Element<H> { export class $Media<H extends HTMLMediaElement> extends $HTMLElement<H> {
constructor(tagname: string, options?: $MediaOptions) { constructor(tagname: string, options?: $MediaOptions) {
super(tagname, options); super(tagname, options);
} }

View File

@ -1,41 +1,20 @@
import { $, $Element, $State, $Text } from "../../index"; import { $EventTarget } from "../$EventTarget";
import { $Container } from "./$Container"; import { $, $Element, $EventManager, $State, $HTMLElement, $Container } from "../../index";
export abstract class $Node<N extends Node = Node> { export abstract class $Node<N extends Node = Node, $EM extends $NodeEventMap = $NodeEventMap, EM extends GlobalEventHandlersEventMap = GlobalEventHandlersEventMap> extends $EventTarget<$EM, EM> {
abstract readonly dom: N; abstract readonly dom: N;
readonly __hidden: boolean = false; protected __$property__ = {
private domEvents: {[key: string]: Map<Function, Function>} = {}; hidden: false,
coordinate: undefined as $NodeCoordinate | undefined
}
readonly parent?: $Container; readonly parent?: $Container;
on<K extends keyof HTMLElementEventMap>(type: K, callback: (event: HTMLElementEventMap[K], $node: this) => any, options?: AddEventListenerOptions | boolean) {
if (!this.domEvents[type]) this.domEvents[type] = new Map()
const middleCallback = (e: Event) => callback(e as HTMLElementEventMap[K], this);
this.domEvents[type].set(callback, middleCallback)
this.dom.addEventListener(type, middleCallback, options)
return this;
}
off<K extends keyof HTMLElementEventMap>(type: K, callback: (event: HTMLElementEventMap[K], $node: this) => any, options?: AddEventListenerOptions | boolean) {
const middleCallback = this.domEvents[type]?.get(callback);
if (middleCallback) this.dom.removeEventListener(type, middleCallback as EventListener, options)
return this;
}
once<K extends keyof HTMLElementEventMap>(type: K, callback: (event: HTMLElementEventMap[K], $node: this) => any, options?: AddEventListenerOptions | boolean) {
const onceFn = (event: Event) => {
this.dom.removeEventListener(type, onceFn, options)
callback(event as HTMLElementEventMap[K], this);
};
this.dom.addEventListener(type, onceFn, options)
return this;
}
hide(): boolean; hide(): boolean;
hide(hide?: boolean | $State<boolean>, render?: boolean): this; hide(hide?: boolean | $State<boolean>, render?: boolean): this;
hide(hide?: boolean | $State<boolean>, render = true) { return $.fluent(this, arguments, () => this.__hidden, () => { hide(hide?: boolean | $State<boolean>, render = true) { return $.fluent(this, arguments, () => this.__$property__.hidden, () => {
if (hide === undefined) return; if (hide === undefined) return;
if (hide instanceof $State) { (this as Mutable<$Node>).__hidden = hide.value; hide.use(this, 'hide')} if (hide instanceof $State) { this.__$property__.hidden = hide.value; hide.use(this, 'hide')}
else (this as Mutable<$Node>).__hidden = hide; else this.__$property__.hidden = hide;
if (render) this.parent?.children.render(); if (render) this.parent?.children.render();
return this; return this;
})} })}
@ -59,8 +38,13 @@ export abstract class $Node<N extends Node = Node> {
else return this.dom.contains(target) else return this.dom.contains(target)
} }
self(callback: ($node: this) => void) { callback(this); return this; } coordinate(): $NodeCoordinate | undefined;
coordinate(coordinate: $NodeCoordinate): this;
coordinate(coordinate?: $NodeCoordinate) { return $.fluent(this, arguments, () => this.__$property__.coordinate, () => $.set(this.__$property__, 'coordinate', coordinate))}
self(callback: OrArray<($node: this) => void>) { $.orArrayResolve(callback).forEach(fn => fn(this)); return this; }
inDOM() { return document.contains(this.dom); } inDOM() { return document.contains(this.dom); }
isElement(): this is $Element { isElement(): this is $Element {
if (this instanceof $Element) return true; if (this instanceof $Element) return true;
else return false; else return false;
@ -69,4 +53,23 @@ export abstract class $Node<N extends Node = Node> {
if (this instanceof $Element) return this; if (this instanceof $Element) return this;
else return null; else return null;
} }
get htmlElement(): $HTMLElement | null {
if (this instanceof $HTMLElement) return this;
else return null;
}
} }
export interface $NodeCoordinate {
x: number;
y: number;
height: number;
width: number;
}
export interface $NodeEventMap {
}
type $HTMLElementEventMap<$N> = {
[keys in keyof HTMLElementEventMap]: [event: HTMLElementEventMap[keys], $this: $N];
};

View File

@ -1,7 +1,7 @@
{ {
"name": "elexis", "name": "elexis",
"description": "Build Web in Native JavaScript Syntax", "description": "Build Web in Native JavaScript Syntax",
"version": "0.2.5", "version": "0.3.0",
"author": { "author": {
"name": "defaultkavy", "name": "defaultkavy",
"email": "defaultkavy@gmail.com", "email": "defaultkavy@gmail.com",