From 4c078c26b6c7b491f400e326d36a4b79fa2b3308 Mon Sep 17 00:00:00 2001 From: defaultkavy Date: Thu, 17 Oct 2024 11:54:28 +0800 Subject: [PATCH] v0.3.0 - 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() --- $index.ts | 11 ++- README.md | 5 +- index.ts | 20 ++++- lib/$EventManager.ts | 57 +++---------- lib/$EventTarget.ts | 50 +++++++++++ lib/$FocusManager.ts | 175 +++++++++++++++++++++++++++++++++++++++ lib/$KeyboardManager.ts | 52 ++++++++++++ lib/$NodeManager.ts | 14 ++-- lib/$PointerManager.ts | 105 +++++++++++++++++++++++ lib/$State.ts | 44 +++++----- lib/$Util.ts | 13 +-- lib/$Window.ts | 9 ++ lib/node/$Anchor.ts | 5 +- lib/node/$Container.ts | 27 +++--- lib/node/$Element.ts | 10 ++- lib/node/$HTMLElement.ts | 8 +- lib/node/$Media.ts | 7 +- lib/node/$Node.ts | 69 +++++++-------- package.json | 2 +- 19 files changed, 545 insertions(+), 138 deletions(-) create mode 100644 lib/$EventTarget.ts create mode 100644 lib/$FocusManager.ts create mode 100644 lib/$KeyboardManager.ts create mode 100644 lib/$PointerManager.ts create mode 100644 lib/$Window.ts diff --git a/$index.ts b/$index.ts index 844621b..5440f37 100644 --- a/$index.ts +++ b/$index.ts @@ -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 { $Document } from "./lib/node/$Document" import { $Anchor } from "./lib/node/$Anchor"; @@ -19,6 +19,8 @@ import { $Util } from "./lib/$Util"; import { $HTMLElement } from "./lib/node/$HTMLElement"; import { $Async } from "./lib/node/$Async"; import { $Video } from "./lib/node/$Video"; +import { $Window } from "./lib/$Window"; +import { $KeyboardManager } from "./lib/$KeyboardManager"; export type $ = typeof $; export function $(query: `::${string}`): E[]; @@ -26,6 +28,7 @@ export function $(query: `:${string}`): E | null; export function $(element: null): null; export function $(resolver: K): $.TagNameTypeMap[K]; export function $(resolver: K): $Container; +export function $(window: Window): $Window; export function $(htmlElement: H): $.$HTMLElementMap; export function $(element: H): $Element; export function $(node: N): N; @@ -51,6 +54,7 @@ export function $(resolver: any) { if (resolver.$) return resolver.$; else return $Util.from(resolver); } + if (resolver instanceof Window) { return $Window.$ } throw `$: NOT SUPPORT TARGET ELEMENT TYPE ('${resolver}')` } export namespace $ { @@ -238,7 +242,10 @@ export namespace $ { return $.TagNameElementMap; } - export function events(...eventname: N[]) { return new $EventManager<{[keys in N]: any[]}>().register(...eventname) } + export function events() { return new $EventManager } + 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(fn: () => T): T { return fn() } } diff --git a/README.md b/README.md index 58afe2b..940f62c 100644 --- a/README.md +++ b/README.md @@ -86,5 +86,6 @@ value$.set(0) ``` ## Extensions -1. [@elexis/router](https://github.com/elexisjs/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. \ No newline at end of file +1. [@elexis/router](https://git.defaultkavy.com/elexis/router): Router for Single Page App. +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. \ No newline at end of file diff --git a/index.ts b/index.ts index 6ff3bea..fb6eeab 100644 --- a/index.ts +++ b/index.ts @@ -3,6 +3,10 @@ declare global { interface Array { detype(...types: F[]): Array> } + interface Set { + get array(): T[] + sort(handler: ((a: T, b: T) => number) | undefined): T[]; + } type OrMatrix = T | OrMatrix[]; type OrArray = T | T[]; type OrPromise = T | Promise; @@ -29,17 +33,27 @@ Array.prototype.detype = function (this: O[], ...types: T[]) { else for (const type of types) if (typeof item !== typeof type) return true; else return false; }) as Exclude[]; } +Object.defineProperties(Set.prototype, { + array: { get: function (this: Set) { return Array.from(this)} } +}) +Set.prototype.sort = function (this: Set, handler: ((a: T, b: T) => number) | undefined) { return this.array.sort(handler)} 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/$Anchor"; export * from "./lib/node/$Element"; -export * from "./lib/$NodeManager"; +export * from "./lib/node/$HTMLElement"; export * from "./lib/node/$Text"; export * from "./lib/node/$Container"; export * from "./lib/node/$Button"; export * from "./lib/node/$Form"; -export * from "./lib/$EventManager"; -export * from "./lib/$State"; export * from "./lib/node/$Select"; export * from "./lib/node/$Option"; export * from "./lib/node/$OptGroup"; diff --git a/lib/$EventManager.ts b/lib/$EventManager.ts index df54dcb..72b707d 100644 --- a/lib/$EventManager.ts +++ b/lib/$EventManager.ts @@ -1,65 +1,32 @@ -export abstract class $EventMethod { - abstract events: $EventManager; - //@ts-expect-error - on(type: K, callback: (...args: EM[K]) => any) { this.events.on(type, callback); return this } - //@ts-expect-error - off(type: K, callback: (...args: EM[K]) => any) { this.events.off(type, callback); return this } - //@ts-expect-error - once(type: K, callback: (...args: EM[K]) => any) { this.events.once(type, callback); return this } -} -export class $EventManager { - eventMap = new Map(); - 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 } +export class $EventManager { + private eventMap = new Map>(); //@ts-expect-error fire(type: K, ...args: EM[K]) { - const event = this.get(type) - //@ts-expect-error - if (event instanceof $Event) event.fire(...args); + this.eventMap.get(type as string)?.forEach(fn => fn(...args as [])); return this } //@ts-expect-error on(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 } //@ts-expect-error off(type: K, callback: (...args: EM[K]) => any) { - this.get(type).delete(callback); + this.eventMap.get(type as string)?.delete(callback); return this } //@ts-expect-error once(type: K, callback: (...args: EM[K]) => any) { - //@ts-expect-error - const onceFn = (...args: EM[K]) => { - this.get(type).delete(onceFn); + const onceFn = (...args: []) => { + this.eventMap.get(type as string)?.delete(onceFn); //@ts-expect-error 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; } - - get(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() - 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) } -} \ No newline at end of file + +export interface $EventMap {} \ No newline at end of file diff --git a/lib/$EventTarget.ts b/lib/$EventTarget.ts new file mode 100644 index 0000000..7ffc90d --- /dev/null +++ b/lib/$EventTarget.ts @@ -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}> = {}; + readonly events = new $EventManager<$EM>(); + abstract dom: EventTarget + + //@ts-expect-error + on(type: K | K[], callback: (...args: $EM[K]) => any): this; + on(type: K | K[], callback: (event: EM[K], $node: this) => any, options?: AddEventListenerOptions | boolean): this; + on(type: K | K[], callback: (event: Event, $node: this) => any, options?: AddEventListenerOptions | boolean): this; + on(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(type: K, callback: (...args: $EM[K]) => any): this; + off(type: K, callback: (event: EM[K], $node: this) => any, options?: AddEventListenerOptions | boolean): this; + off(type: K, callback: (event: Event, $node: this) => any, options?: AddEventListenerOptions | boolean): this; + off(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(type: K, callback: (...args: $EM[K]) => any): this; + once(type: K, callback: (event: EM[K], $node: this) => any, options?: AddEventListenerOptions | boolean): this; + once(type: K, callback: (event: Event, $node: this) => any, options?: AddEventListenerOptions | boolean): this; + once(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 } +} \ No newline at end of file diff --git a/lib/$FocusManager.ts b/lib/$FocusManager.ts new file mode 100644 index 0000000..782e71d --- /dev/null +++ b/lib/$FocusManager.ts @@ -0,0 +1,175 @@ +import { $Element } from ".."; + +export class $FocusManager { + layerMap = new Map(); + 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)) } +} \ No newline at end of file diff --git a/lib/$KeyboardManager.ts b/lib/$KeyboardManager.ts new file mode 100644 index 0000000..4f98a33 --- /dev/null +++ b/lib/$KeyboardManager.ts @@ -0,0 +1,52 @@ +import { $EventTarget } from "./$EventTarget"; +import { $Util } from "./$Util"; + +export class $KeyboardManager { + keyMap = new Map(); + 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, 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, 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, callback: $KeyboardEventHandler) { this.assigns(keys, 'keydown', callback); return this; } + keyup(keys: OrArray, callback: $KeyboardEventHandler) { this.assigns(keys, 'keyup', callback); return this; } + keypress(keys: OrArray, 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>}; \ No newline at end of file diff --git a/lib/$NodeManager.ts b/lib/$NodeManager.ts index 48c4325..791b3b4 100644 --- a/lib/$NodeManager.ts +++ b/lib/$NodeManager.ts @@ -1,6 +1,5 @@ import { $Container } from "./node/$Container"; import { $Node } from "./node/$Node"; -import { $Text } from "./node/$Text"; export class $NodeManager { readonly $container: $Container; @@ -12,13 +11,13 @@ export class $NodeManager { add(element: $Node, position = -1) { if (position === -1 || this.childList.size - 1 === position) { this.childList.add(element); - (element as Mutable<$Node>).parent = this.$container; } else { const children = [...this.childList] children.splice(position, 0, element); this.childList.clear(); children.forEach(child => this.childList.add(child)); } + (element as Mutable<$Node>).parent = this.$container; } remove(element: $Node) { @@ -39,6 +38,7 @@ export class $NodeManager { target.remove(); this.childList.clear(); array.forEach(node => this.childList.add(node)); + (replace as Mutable<$Node>).parent = this.$container; return this; } @@ -49,18 +49,22 @@ export class $NodeManager { while (nodeList.length || domList.length) { // while nodeList or domList has item const [node, dom] = [nodeList.at(0), domList.at(0)]; if (!dom) { if (node && !appendedNodeList.includes(node)) node.remove(); nodeList.shift()} - else if (!node) { if (!dom.$.__hidden) this.dom.append(dom); domList.shift();} + else if (!node) { if (!dom.$.hide()) this.dom.append(dom); domList.shift();} 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(); } else { - if (dom.$.__hidden) this.dom.removeChild(dom); + if (dom.$.hide()) this.dom.removeChild(dom); domList.shift(); nodeList.shift(); } } } + indexOf(target: $Node) { + return this.array.indexOf(target); + } + get array() {return [...this.childList.values()]}; get dom() {return this.$container.dom} diff --git a/lib/$PointerManager.ts b/lib/$PointerManager.ts new file mode 100644 index 0000000..e724c02 --- /dev/null +++ b/lib/$PointerManager.ts @@ -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(); + 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' \ No newline at end of file diff --git a/lib/$State.ts b/lib/$State.ts index 9d48a3b..80dc557 100644 --- a/lib/$State.ts +++ b/lib/$State.ts @@ -17,6 +17,16 @@ export class $State { 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() { // update element content for eatch attributes for (const [node, attrList] of this.attributes.entries()) { @@ -36,37 +46,31 @@ export class $State { } } - 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(object: O, attrName: K) { const attrList = this.attributes.get(object) if (attrList) attrList.add(attrName); else this.attributes.set(object, new Set().add(attrName)) } + convert(fn: (value: T) => string) { + return new $State(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 { if (this.value instanceof $State) return this.value.toJSON(); if (this.value instanceof Object) return $State.toJSON(this.value); 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 = $State | undefined | (T extends (infer R)[] ? R : T); \ No newline at end of file diff --git a/lib/$Util.ts b/lib/$Util.ts index 10947b9..4b00335 100644 --- a/lib/$Util.ts +++ b/lib/$Util.ts @@ -42,14 +42,17 @@ export namespace $Util { export function from(element: Node): $Node { if (element.$) return element.$; 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); else if (element instanceof HTMLElement) { const instance = $.TagNameElementMap[element.tagName.toLowerCase() as keyof typeof $.TagNameElementMap]; - const $node = instance === $Container - //@ts-expect-error - ? new instance(element.tagName, {dom: element}) - //@ts-expect-error - : new instance({dom: element} as any); + const $node = !instance + ? new $Container(element.tagName, {dom: element}) + : instance === $Container + //@ts-expect-error + ? 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)) { $node.children.add($(childnode as any)); } diff --git a/lib/$Window.ts b/lib/$Window.ts new file mode 100644 index 0000000..9472d87 --- /dev/null +++ b/lib/$Window.ts @@ -0,0 +1,9 @@ +import { $EventManager, $EventMap } from "./$EventManager"; +import { $EventTarget } from "./$EventTarget"; + +export class $Window extends $EventTarget { + static $ = new $Window(); + readonly dom = window; +} + +export interface $WindowEventMap extends $EventMap {} \ No newline at end of file diff --git a/lib/node/$Anchor.ts b/lib/node/$Anchor.ts index 9160bef..cd5bb87 100644 --- a/lib/node/$Anchor.ts +++ b/lib/node/$Anchor.ts @@ -1,3 +1,4 @@ +import { $StateArgument } from "../$State"; import { $Container, $ContainerOptions } from "./$Container"; export interface AnchorOptions extends $ContainerOptions {} @@ -15,8 +16,8 @@ export class $Anchor extends $Container { } /**Set URL of anchor element. */ href(): string; - href(url: string | undefined): this; - href(url?: string | undefined) { return $.fluent(this, arguments, () => this.dom.href, () => {if (url) this.dom.href = url}) } + href(url: $StateArgument): this; + href(url?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.href, () => $.set(this.dom, 'href', url)) } /**Link open with this window, new tab or other */ target(): $AnchorTarget | undefined; target(target: $AnchorTarget | undefined): this; diff --git a/lib/node/$Container.ts b/lib/node/$Container.ts index f8cb1bc..403dccb 100644 --- a/lib/node/$Container.ts +++ b/lib/node/$Container.ts @@ -1,12 +1,12 @@ -import { $Element, $ElementOptions } from "./$Element"; +import { $Element } from "./$Element"; import { $NodeManager } from "../$NodeManager"; import { $Node } from "./$Node"; import { $State, $StateArgument } from "../$State"; import { $Text } from "./$Text"; -import { $HTMLElement, $HTMLElementOptions } from "./$HTMLElement"; +import { $HTMLElement, $HTMLElementEventMap, $HTMLElementOptions } from "./$HTMLElement"; export interface $ContainerOptions extends $HTMLElementOptions {} -export class $Container extends $HTMLElement { +export class $Container extends $HTMLElement { readonly children: $NodeManager = new $NodeManager(this); constructor(tagname: string, options?: $ContainerOptions) { super(tagname, options) @@ -23,7 +23,11 @@ export class $Container extends $HTMLElemen private __position_cursor = 0; /**Insert element to this element */ insert(children: $ContainerContentBuilder, 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); // 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; @@ -53,11 +57,12 @@ export class $Container extends $HTMLElemen return this; } - //**Query selector one of child element */ - $(query: string): E | null { return $(this.dom.querySelector(query)) as E | null } - - //**Query selector of child elements */ - $all(query: string): E[] { return Array.from(this.dom.querySelectorAll(query)).map($dom => $($dom) as E) } + $(query: `::${string}`): E[]; + $(query: `:${string}`): E | null; + $(query: string) { + if (query.startsWith('::')) return Array.from(document.querySelectorAll(query.replace(/^::/, ''))).map(dom => $(dom)); + else if (query.startsWith(':')) return $(document.querySelector(query.replace(/^:/, ''))); + } get scrollHeight() { return this.dom.scrollHeight } get scrollWidth() { return this.dom.scrollWidth } @@ -73,4 +78,6 @@ export class $Container extends $HTMLElemen export type $ContainerContentBuilder

= $ContainerContentGroup | (($node: P) => OrPromise<$ContainerContentGroup>) export type $ContainerContentGroup = OrMatrix> -export type $ContainerContentType = $Node | string | undefined | $State | null \ No newline at end of file +export type $ContainerContentType = $Node | string | undefined | $State | null + +export interface $ContainerEventMap extends $HTMLElementEventMap {} \ No newline at end of file diff --git a/lib/node/$Element.ts b/lib/node/$Element.ts index 9045482..5d3f885 100644 --- a/lib/node/$Element.ts +++ b/lib/node/$Element.ts @@ -1,4 +1,4 @@ -import { $Node } from "./$Node"; +import { $Node, $NodeEventMap } from "./$Node"; export interface $ElementOptions { id?: string; @@ -7,7 +7,7 @@ export interface $ElementOptions { tagname?: string; } -export class $Element extends $Node { +export class $Element extends $Node { readonly dom: H; private static_classes = new Set(); constructor(tagname: string, options?: $ElementOptions) { @@ -81,7 +81,7 @@ export class $Element extends tabIndex(tabIndex: number): this; 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; } animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions, callback?: (animation: Animation) => void) { @@ -111,4 +111,6 @@ export class $Element extends } } -export type $DOMRect = Omit; \ No newline at end of file +export type $DOMRect = Omit; + +export interface $ElementEventMap extends $NodeEventMap {} \ No newline at end of file diff --git a/lib/node/$HTMLElement.ts b/lib/node/$HTMLElement.ts index cac0bc9..8bdb38c 100644 --- a/lib/node/$HTMLElement.ts +++ b/lib/node/$HTMLElement.ts @@ -1,7 +1,7 @@ -import { $Element, $ElementOptions } from "./$Element"; +import { $Element, $ElementEventMap, $ElementOptions } from "./$Element"; export interface $HTMLElementOptions extends $ElementOptions {} -export class $HTMLElement extends $Element { +export class $HTMLElement extends $Element { constructor(tagname: string, options?: $HTMLElementOptions) { super(tagname, options) } @@ -62,4 +62,6 @@ export class $HTMLElement extends $Element< get offsetParent() { return $(this.dom.offsetParent) } get offsetTop() { return this.dom.offsetTop } get offsetWidth() { return this.dom.offsetWidth } -} \ No newline at end of file +} + +export interface $HTMLElementEventMap extends $ElementEventMap {} \ No newline at end of file diff --git a/lib/node/$Media.ts b/lib/node/$Media.ts index a3d62cb..9185baa 100644 --- a/lib/node/$Media.ts +++ b/lib/node/$Media.ts @@ -1,8 +1,9 @@ -import { $State, $StateArgument } from "../$State"; -import { $Element, $ElementOptions } from "./$Element"; +import { $StateArgument } from "../$State"; +import { $ElementOptions } from "./$Element"; +import { $HTMLElement } from "./$HTMLElement"; export interface $MediaOptions extends $ElementOptions {} -export class $Media extends $Element { +export class $Media extends $HTMLElement { constructor(tagname: string, options?: $MediaOptions) { super(tagname, options); } diff --git a/lib/node/$Node.ts b/lib/node/$Node.ts index b9968ca..7bf30d7 100644 --- a/lib/node/$Node.ts +++ b/lib/node/$Node.ts @@ -1,41 +1,20 @@ -import { $, $Element, $State, $Text } from "../../index"; -import { $Container } from "./$Container"; +import { $EventTarget } from "../$EventTarget"; +import { $, $Element, $EventManager, $State, $HTMLElement, $Container } from "../../index"; -export abstract class $Node { +export abstract class $Node extends $EventTarget<$EM, EM> { abstract readonly dom: N; - readonly __hidden: boolean = false; - private domEvents: {[key: string]: Map} = {}; + protected __$property__ = { + hidden: false, + coordinate: undefined as $NodeCoordinate | undefined + } readonly parent?: $Container; - on(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(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(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(hide?: boolean | $State, render?: boolean): this; - hide(hide?: boolean | $State, render = true) { return $.fluent(this, arguments, () => this.__hidden, () => { + hide(hide?: boolean | $State, render = true) { return $.fluent(this, arguments, () => this.__$property__.hidden, () => { if (hide === undefined) return; - if (hide instanceof $State) { (this as Mutable<$Node>).__hidden = hide.value; hide.use(this, 'hide')} - else (this as Mutable<$Node>).__hidden = hide; + if (hide instanceof $State) { this.__$property__.hidden = hide.value; hide.use(this, 'hide')} + else this.__$property__.hidden = hide; if (render) this.parent?.children.render(); return this; })} @@ -59,8 +38,13 @@ export abstract class $Node { 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); } + isElement(): this is $Element { if (this instanceof $Element) return true; else return false; @@ -69,4 +53,23 @@ export abstract class $Node { if (this instanceof $Element) return this; else return null; } -} \ No newline at end of file + 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]; +}; \ No newline at end of file diff --git a/package.json b/package.json index 55207f9..7286b71 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "elexis", "description": "Build Web in Native JavaScript Syntax", - "version": "0.2.5", + "version": "0.3.0", "author": { "name": "defaultkavy", "email": "defaultkavy@gmail.com",