Compare commits

...

8 Commits

Author SHA1 Message Date
4c078c26b6
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()
2024-10-17 11:54:28 +08:00
a57246e6e1
v0.2.5
update: $Async.await() allow $ContainerContentType object.
update: $Container.insert() and .content() can handler Promise object and Async function.
update: $Select.value() will sync value with $State.value when update.
update: Array.prototype.detype will exclude `undefined` and `void` automatically.
new: $.events function, create EventManager in faster way.
fork: move $View to extensions repository
new: $.call function, just a simple function caller.
2024-10-03 23:21:55 +08:00
c5b99d8835
update: README 2024-08-29 02:36:03 +08:00
f614ecd5f5
v0.2.4
change: $NodeManager.$elementList rename to childList.
update: $Container.insert() and $NodeManager.add() now can insert element with position.
add: $Video element is avaliable.
add: $Media element is avaliable.
update: $State now can set with other $State object.
change: The host of this repository is changed.
2024-08-29 02:27:14 +08:00
72f617df3c v0.2.3
fix: remove $State debug message
2024-05-15 18:54:44 +08:00
7f8f599b8a v0.2.2
new: $HTMLElementAPIs
optimize: collect utilty method of HTMLElement and merge into different $Element
2024-05-15 18:38:45 +08:00
4c143cad90 v0.2.1
fix: README image source
2024-04-26 18:56:23 +08:00
6454ddab48 v0.2.0
new: ElexisJS icons and logo
change: README.md
update: $Input.type() will convert input into Number/Check/File Input with several methods
update: $.set() [methodKey] parameter will change to [handle] function, handle() will be called when [value] parameter is $State.
add: $State .toJSON()
2024-04-26 18:41:29 +08:00
34 changed files with 1093 additions and 433 deletions

View File

@ -1,4 +1,4 @@
import { $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";
@ -11,7 +11,6 @@ import { $Label } from "./lib/node/$Label";
import { $Image } from "./lib/node/$Image";
import { $Canvas } from "./lib/node/$Canvas";
import { $Dialog } from "./lib/node/$Dialog";
import { $View } from "./lib/node/$View";
import { $Select } from "./lib/node/$Select";
import { $Option } from "./lib/node/$Option";
import { $OptGroup } from "./lib/node/$OptGroup";
@ -19,6 +18,9 @@ import { $Textarea } from "./lib/node/$Textarea";
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 $<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 $<K extends keyof $.TagNameTypeMap>(resolver: K): $.TagNameTypeMap[K];
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 Element>(element: H): $Element;
export function $<N extends $Node>(node: N): N;
@ -51,12 +54,14 @@ 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 $ {
export let anchorHandler: null | (($a: $Anchor, e: Event) => void) = null;
export let anchorPreventDefault: boolean = false;
export const TagNameElementMap = {
'html': $Container,
'head': $Container,
'document': $Document,
'body': $Container,
'a': $Anchor,
@ -83,11 +88,11 @@ export namespace $ {
'img': $Image,
'dialog': $Dialog,
'canvas': $Canvas,
'view': $View,
'select': $Select,
'option': $Option,
'optgroup': $OptGroup,
'textarea': $Textarea,
'video': $Video,
'async': $Async,
}
export type TagNameElementMapType = typeof TagNameElementMap;
@ -112,6 +117,7 @@ export namespace $ {
: H extends HTMLOptionElement ? $Option
: H extends HTMLOptGroupElement ? $OptGroup
: H extends HTMLTextAreaElement ? $Textarea
: H extends HTMLVideoElement ? $Video
: $Container<H>;
/**
@ -122,7 +128,7 @@ export namespace $ {
* @param action The action to execute when arguments length not equal 0.
* @returns
*/
export function fluent<T, A, V>(instance: T, args: IArguments, value: () => V, action: (...args: any[]) => void) {
export function fluent<T, A, V>(instance: T, args: IArguments, value: () => V, action: (...args: any[]) => void): T | V {
if (!args.length) return value();
action();
return instance;
@ -138,30 +144,31 @@ export namespace $ {
* A helper for undefined able value and $State.set() which apply value to target.
* @param object Target object.
* @param key The key of target object.
* @param value Value of target property or parameter of method(Using Tuple to apply parameter).
* @param methodKey Variant key name when apply value on $State.set()
* @param value Value of target property or parameter of method (Using Tuple to apply parameter).
* @param handle callback when param `value` is $State object.
* @returns
*/
export function set<O, K extends keyof O>(
export function set<O extends Object, K extends keyof O>(
object: O,
key: K,
value: O[K] extends (...args: any) => any
? (undefined | $StateArgument<Parameters<O[K]>>)
? (undefined | [$StateArgument<Parameters<O[K]>>])
: (undefined | $StateArgument<O[K]>),
methodKey?: string) {
handle?: ($state: $State<O[K]>) => any) {
if (value === undefined) return;
if (value instanceof $State && object instanceof Node) {
value.use(object.$, methodKey ?? key as any);
if (object[key] instanceof Function) (object[key] as Function)(value)
else object[key] = value.value;
if (value instanceof $State) {
value.use(object, key);
if (object[key] instanceof Function) (object[key] as Function)(...value.value)
else object[key] = value.value;
if (handle) handle(value);
return;
}
if (object[key] instanceof Function) (object[key] as Function)(value);
if (object[key] instanceof Function) (object[key] as Function)(...value as any);
else object[key] = value as any;
}
export function state<T>(value: T, options?: $StateOption<T>) {
return new $State<T>(value, options)
export function state<T>(value: T, options?: $StateOption<T extends $State<infer K> ? K : T>) {
return new $State<T>(value, options as $StateOption<T>) as T extends $State<infer K> ? $State<K> : $State<T>;
}
export async function resize(object: Blob, size: number): Promise<string> {
@ -188,9 +195,7 @@ export namespace $ {
})
}
export function rem(amount: number = 1) {
return parseInt(getComputedStyle(document.documentElement).fontSize) * amount
}
export function rem(amount: number = 1) { return parseInt(getComputedStyle(document.documentElement).fontSize) * amount }
export function html(html: string) {
const body = new DOMParser().parseFromString(html, 'text/html').body;
@ -236,6 +241,13 @@ export namespace $ {
Object.assign($.TagNameElementMap, {[string]: node});
return $.TagNameElementMap;
}
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() }
}
type BuildNodeFunction = (...args: any[]) => $Node;
type BuilderSelfFunction<K extends $Node> = (self: K) => void;

1
.gitignore vendored
View File

@ -1,3 +1,2 @@
.npmignore
bun.lockb
node_modules

2
.npmignore Normal file
View File

@ -0,0 +1,2 @@
.gitignore
assets

View File

@ -1,6 +1,11 @@
# ElexisJS
TypeScript First Web Framework, for Humans.
> ElexisJS is still in beta test now, some breaking changes might happen very often.
<picture style="display: flex; justify-content: center">
<source media="(prefers-color-scheme: dark)" srcset="https://git.defaultkavy.com/defaultkavy/elexis/raw/branch/assets/logo_light.png">
<source media="(prefers-color-scheme: light)" srcset="https://git.defaultkavy.com/defaultkavy/elexis/raw/branch/assets/logo_dark.png">
<img src="https://git.defaultkavy.com/defaultkavy/elexis/raw/branch/assets/logo_dark.png" alt="Elexis Logo">
</picture>
<p style="text-align: center">Build Web in Native JavaScript Syntax</p>
> ElexisJS is still in beta test now, some breaking changes might happened very often.
## What does ElexisJS bring to developer?
1. Write website with Native JavaScript syntax and full TypeScript development experiance, no more HTML or JSX.
@ -32,13 +37,55 @@ $('h1').class('title').css({color: 'red'})
```
## Build your first "Hello, world!" ElexisJS project
Let's try this in your entry file:
Let's try this code in your entry file:
```ts
$(document.body).content([
$('h1').class('title').content('Hello, world!')
])
```
In the first line, we create a `$Container` to using Elexis API on `document.body` element. Then we see a `content` method after the container object, this method mean the following elements will be the content of container.
We can pass an array into `content` method. In this array, we put a new `<h1>` element which have a class name "title" and text content "Hello, world!".
Run the code, we will get this body structure in DOM:
```html
<body>
<h1 class="title">Hello, world!</h1>
</body>
```
So far, we just simply do a hello world project that you can type less in HTML way, and these is not the point of ElexisJS. Let's figure out what ElexisJS will boost development speed in the following examples.
## Using `$State` to sync view and data changes
This line will create a `$State` value, usually we will put `$` sign behind variable name to mean this is a `$State` variable.
```ts
const number$ = $.state(42);
```
This `$State` value has been set a number `42`, which will become a number type `$State`. We can simply put this state value into any display content!
```ts
const value$ = $.state(42);
$(document.body).content([
$('input').type('number').value(value$),
$('p').content(['User input value: ', value$])
])
```
You will see the `<input>` element is fill with number `42`, and also `<p>` element will display `'User input value: 42'`. Now try to change input value in browser, the value text in `<p>` element will be synced by your input!
Using `set` method to set value of `$State`, all displayed content of `value$` will be synced.
```ts
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.
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.

View File

@ -1,7 +1,11 @@
declare global {
var $: import('./$index').$;
interface Array<T> {
detype<F extends undefined | null, O>(...types: F[]): Array<Exclude<T, F>>
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 OrArray<T> = T | T[];
@ -23,28 +27,39 @@ declare global {
$: import('./lib/node/$Node').$Node;
}
}
Array.prototype.detype = function <T extends undefined | null, O>(this: O[], ...types: T[]) {
Array.prototype.detype = function <T extends any, O>(this: O[], ...types: T[]) {
return this.filter(item => {
if (!types.length) return item !== undefined;
else for (const type of types) if (typeof item !== typeof type) return false; else return true
}) as Exclude<O, T>[];
else for (const type of types) if (typeof item !== typeof type) return true; else return false;
}) 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 "./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/$View";
export * from "./lib/node/$Select";
export * from "./lib/node/$Option";
export * from "./lib/node/$OptGroup";
export * from "./lib/node/$Textarea";
export * from "./lib/node/$Image";
export * from "./lib/node/$Async";
export * from "./lib/node/$Document";
export * from "./lib/node/$Document";
export * from "./lib/node/$Media";
export * from "./lib/node/$Video";

94
lib/$ElementTemplate.ts Normal file
View File

@ -0,0 +1,94 @@
import { $StateArgument } from "./$State";
import { $Form } from "./node/$Form";
export abstract class $HTMLElementAPIs<This = any> {
static create(...args: (keyof $HTMLElementAPIs)[]) {
const $template = class {};
Object.getOwnPropertyNames($HTMLElementAPIs.prototype).forEach(name => {
if (name === 'constructor') return;
if (!args.includes(name as keyof $HTMLElementAPIs)) return;
Object.defineProperty(
$template.prototype,
name,
Object.getOwnPropertyDescriptor($HTMLElementAPIs.prototype, name) || Object.create(null)
)
})
return $template;
}
disabled(): boolean;
disabled(disabled: $StateArgument<boolean>): This;
//@ts-ignore
disabled(disabled?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled)) }
//@ts-ignore
checkValidity(): boolean { return this.dom.checkValidity() }
//@ts-ignore
reportValidity(): boolean { return this.dom.reportValidity() }
formAction(): string;
formAction(action: string | undefined): This;
//@ts-ignore
formAction(action?: string) { return $.fluent(this, arguments, () => this.dom.formAction, () => $.set(this.dom, 'formAction', action))}
formEnctype(): string;
formEnctype(enctype: string | undefined): This;
//@ts-ignore
formEnctype(enctype?: string) { return $.fluent(this, arguments, () => this.dom.formEnctype, () => $.set(this.dom, 'formEnctype', enctype))}
formMethod(): string;
formMethod(method: string | undefined): This;
//@ts-ignore
formMethod(method?: string) { return $.fluent(this, arguments, () => this.dom.formMethod, () => $.set(this.dom, 'formMethod', method))}
formNoValidate(): boolean;
formNoValidate(boolean: boolean | undefined): This;
//@ts-ignore
formNoValidate(boolean?: boolean) { return $.fluent(this, arguments, () => this.dom.formNoValidate, () => $.set(this.dom, 'formNoValidate', boolean))}
formTarget(): string;
formTarget(target: string | undefined): This;
//@ts-ignore
formTarget(target?: string) { return $.fluent(this, arguments, () => this.dom.formTarget, () => $.set(this.dom, 'formTarget', target))}
autocomplete(): AutoFill;
//@ts-ignore
autocomplete(autocomplete: AutoFill | undefined): This;
//@ts-ignore
autocomplete(autocomplete?: AutoFill) { return $.fluent(this, arguments, () => this.dom.autocomplete as AutoFill, () => $.set(this.dom, 'autocomplete', autocomplete as AutoFillBase))}
name(): string;
name(name?: $StateArgument<string> | undefined): This;
//@ts-ignore
name(name?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))}
maxLength(): number;
maxLength(maxLength: number): This;
//@ts-ignore
maxLength(maxLength?: number) { return $.fluent(this, arguments, () => this.dom.maxLength, () => $.set(this.dom, 'maxLength', maxLength))}
minLength(): number;
minLength(minLength: number): This;
//@ts-ignore
minLength(minLength?: number) { return $.fluent(this, arguments, () => this.dom.minLength, () => $.set(this.dom, 'minLength', minLength))}
required(): boolean;
required(required: boolean): This;
//@ts-ignore
required(required?: boolean) { return $.fluent(this, arguments, () => this.dom.required, () => $.set(this.dom, 'required', required))}
label(): string;
label(label: $StateArgument<string> | undefined): This;
//@ts-ignore
label(label?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.label, () => $.set(this.dom, 'label', label))}
//@ts-ignore
get form(): $Form | null { return this.dom.form ? $(this.dom.form) : null }
//@ts-ignore
get validationMessage(): string { return this.dom.validationMessage }
//@ts-ignore
get validity(): ValidityState { return this.dom.validity }
//@ts-ignore
get willValidate(): boolean { return this.dom.willValidate }
}
export type $HTMLElementAPIFilter<This, M extends keyof $HTMLElementAPIs> = Pick<$HTMLElementAPIs<This>, M>

View File

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

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,34 +1,34 @@
import { $Container } from "./node/$Container";
import { $Node } from "./node/$Node";
import { $Text } from "./node/$Text";
export class $NodeManager {
$container: $Container;
$elementList = new Set<$Node>
readonly $container: $Container;
readonly childList = new Set<$Node>
constructor(container: $Container) {
this.$container = container;
}
add(element: $Node | string) {
if (typeof element === 'string') {
const text = new $Text(element);
this.$elementList.add(text);
(text as Mutable<$Node>).parent = this.$container;
add(element: $Node, position = -1) {
if (position === -1 || this.childList.size - 1 === position) {
this.childList.add(element);
} else {
this.$elementList.add(element);
(element as Mutable<$Node>).parent = this.$container;
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) {
if (!this.$elementList.has(element)) return this;
this.$elementList.delete(element);
if (!this.childList.has(element)) return this;
this.childList.delete(element);
(element as Mutable<$Node>).parent = undefined;
return this;
}
removeAll(render = true) {
this.$elementList.forEach(ele => this.remove(ele));
this.childList.forEach(ele => this.remove(ele));
if (render) this.render();
}
@ -36,8 +36,9 @@ export class $NodeManager {
const array = this.array
array.splice(array.indexOf(target), 1, replace);
target.remove();
this.$elementList.clear();
array.forEach(node => this.$elementList.add(node));
this.childList.clear();
array.forEach(node => this.childList.add(node));
(replace as Mutable<$Node>).parent = this.$container;
return this;
}
@ -48,19 +49,23 @@ 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();
}
}
}
get array() {return [...this.$elementList.values()]};
indexOf(target: $Node) {
return this.array.indexOf(target);
}
get array() {return [...this.childList.values()]};
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

@ -1,41 +1,76 @@
import { $Node } from "./node/$Node";
export interface $StateOption<T> {
format: (value: T) => string;
}
export class $State<T> {
readonly value: T;
readonly attributes = new Map<$Node, Set<string | number | symbol>>();
protected _value!: T | $State<T>;
readonly attributes = new Map<Object, Set<string | number | symbol>>();
readonly linkStates = new Set<$State<T>>;
options: Partial<$StateOption<T>> = {}
constructor(value: T, options?: $StateOption<T>) {
this.value = value;
this.set(value);
if (options) this.options = options;
}
set(value: T) {
(this as Mutable<$State<T>>).value = value;
set(value: T | $State<T>) {
this._value = value;
if (value instanceof $State) value.linkStates.add(this as any);
this.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() {
// update element content for eatch attributes
for (const [node, attrList] of this.attributes.entries()) {
for (const attr of attrList) {
//@ts-expect-error
if (node[attr] instanceof Function) {
//@ts-expect-error
if (this.options.format) node[attr](this.options.format(value))
if (this.options.format) node[attr](this.options.format(this.value))
//@ts-expect-error
else node[attr](value)
else node[attr](this.value)
}
else if (attr in node) {
//@ts-expect-error
node[attr] = this.value
}
}
}
}
use<O extends Object, K extends keyof O>(object: O, attrName: K) {
const attrList = this.attributes.get(object)
if (attrList) attrList.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}`
}
use<T extends $Node, K extends keyof T>($node: T, attrName: K) {
const attrList = this.attributes.get($node)
if (attrList) attrList.add(attrName);
else this.attributes.set($node, new Set<string | number | symbol>().add(attrName))
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();
}
};
export type $StateArgument<T> = T | $State<T | undefined>;
export type $StateArgument<T> = $State<T> | undefined | (T extends (infer R)[] ? R : T);

View File

@ -42,13 +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
? 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));
}

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";
export interface AnchorOptions extends $ContainerOptions {}
@ -7,14 +8,16 @@ export class $Anchor extends $Container<HTMLAnchorElement> {
super('a', options);
// Link Handler event
this.dom.addEventListener('click', e => {
if ($.anchorPreventDefault) e.preventDefault();
if ($.anchorHandler && !!this.href()) $.anchorHandler(this, e)
if ($.anchorHandler && !!this.href()) {
e.preventDefault();
$.anchorHandler(this, e);
}
})
}
/**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<string>): this;
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 */
target(): $AnchorTarget | undefined;
target(target: $AnchorTarget | undefined): this;

View File

@ -1,5 +1,7 @@
import { $Container, $ContainerOptions } from "./$Container";
import { $State } from "../$State";
import { $Container, $ContainerContentType, $ContainerOptions } from "./$Container";
import { $Node } from "./$Node";
import { $Text } from "./$Text";
export interface $AsyncNodeOptions extends $ContainerOptions {}
export class $Async<N extends $Node = $Node> extends $Container {
#loaded: boolean = false;
@ -7,14 +9,22 @@ export class $Async<N extends $Node = $Node> extends $Container {
super('async', options)
}
await<T extends $Node = $Node>($node: Promise<T>) {
$node.then($node => this._loaded($node));
await<T extends $Node>($node: Promise<T | $ContainerContentType> | (($self: this) => Promise<T | $ContainerContentType>)) {
if ($node instanceof Function) $node(this).then($node => this._loaded($node));
else $node.then($node => this._loaded($node));
return this as $Async<T>
}
protected _loaded($node: $Node) {
protected _loaded($node: $ContainerContentType) {
this.#loaded = true;
this.replace($node)
if (typeof $node === 'string') this.replace(new $Text($node));
else if ($node instanceof $State) {
const ele = new $Text($node.toString());
$node.use(ele, 'content');
this.replace(ele);
}
else if ($node === null || $node === undefined) this.replace(new $Text(String($node)));
else this.replace($node)
this.dom.dispatchEvent(new Event('load'))
}

View File

@ -1,39 +1,16 @@
import { $Container, $ContainerOptions } from "./$Container";
import { $State, $StateArgument } from "../$State";
import { $Util } from "../$Util";
import { $HTMLElementAPIFilter, $HTMLElementAPIs } from "../$ElementTemplate";
export interface $ButtonOptions extends $ContainerOptions {}
export class $Button extends $Container<HTMLButtonElement> {
constructor(options?: $ButtonOptions) {
super('button', options);
}
disabled(): boolean;
disabled(disabled: $StateArgument<boolean>): this;
disabled(disabled?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))}
type(): ButtonType;
type(type: ButtonType): this;
type(type?: ButtonType) { return $.fluent(this, arguments, () => this.dom.type as ButtonType, () => $.set(this.dom, 'type', type as any))}
checkValidity() { return this.dom.checkValidity() }
reportValidity() { return this.dom.reportValidity() }
}
formAction(): string;
formAction(action: string | undefined): this;
formAction(action?: string) { return $.fluent(this, arguments, () => this.dom.formAction, () => $.set(this.dom, 'formAction', action))}
formEnctype(): string;
formEnctype(enctype: string | undefined): this;
formEnctype(enctype?: string) { return $.fluent(this, arguments, () => this.dom.formEnctype, () => $.set(this.dom, 'formEnctype', enctype))}
formMethod(): string;
formMethod(method: string | undefined): this;
formMethod(method?: string) { return $.fluent(this, arguments, () => this.dom.formMethod, () => $.set(this.dom, 'formMethod', method))}
formNoValidate(): boolean;
formNoValidate(boolean: boolean | undefined): this;
formNoValidate(boolean?: boolean) { return $.fluent(this, arguments, () => this.dom.formNoValidate, () => $.set(this.dom, 'formNoValidate', boolean))}
formTarget(): string;
formTarget(target: string | undefined): this;
formTarget(target?: string) { return $.fluent(this, arguments, () => this.dom.formTarget, () => $.set(this.dom, 'formTarget', target))}
}
export interface $Button extends $HTMLElementAPIFilter<$Button, 'disabled' | 'checkValidity' | 'formAction' | 'formEnctype' | 'formMethod' | 'formNoValidate' | 'formTarget' | 'reportValidity'> {}
$Util.mixin($Button, $HTMLElementAPIs.create('disabled', 'checkValidity', 'formAction', 'formEnctype', 'formMethod', 'formNoValidate', 'formTarget', 'reportValidity'))

View File

@ -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<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);
constructor(tagname: string, options?: $ContainerOptions) {
super(tagname, options)
@ -20,20 +20,35 @@ export class $Container<H extends HTMLElement = HTMLElement> extends $HTMLElemen
this.insert(children);
})}
private __position_cursor = 0;
/**Insert element to this element */
insert(children: $ContainerContentBuilder<this>): this { return $.fluent(this, arguments, () => this, () => {
if (children instanceof Function) children = children(this);
insert(children: $ContainerContentBuilder<this>, position = -1): this { return $.fluent(this, arguments, () => this, async () => {
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;
for (const child of children) {
if (child === undefined) continue;
if (child instanceof Array) this.insert(child)
if (child === undefined || child === null) continue; // skip
if (child instanceof Array) this.insert(child, this.__position_cursor); // insert element group at this position
else if (typeof child === 'string') this.children.add(new $Text(child), position); // turn string into $Text element
else if (child instanceof $State) {
const ele = new $Text(child.toString());
child.use(ele, 'content');
this.children.add(ele);
} else this.children.add(child);
const ele = new $Text(child.toString()); // turn $State object into $Text element
child.use(ele, 'content'); // bind $Text elelment and function name to $State
this.children.add(ele, position);
}
else if (child instanceof Promise) {
const $Async = (await import('./$Async')).$Async; // import $Async avoid extends error
const ele = new $Async().await(child) // using $Async.await resolve promise element
this.children.add(ele, position); // insert $Async element at this position, leave a position for promised element
}
else this.children.add(child, position); // insert $Node element directly
this.__position_cursor += 1; // increase position count
}
this.children.render();
this.children.render(); // start to render dom tree
})}
/**Remove all children elemetn from this element */
@ -42,11 +57,12 @@ export class $Container<H extends HTMLElement = HTMLElement> extends $HTMLElemen
return this;
}
//**Query selector one of child element */
$<E extends $Element>(query: string): E | null { return $(this.dom.querySelector(query)) as E | null }
//**Query selector of child elements */
$all<E extends $Element>(query: string): E[] { return Array.from(this.dom.querySelectorAll(query)).map($dom => $($dom) as E) }
$<E extends $Element = $Element>(query: `::${string}`): E[];
$<E extends $Element = $Element>(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 }
@ -60,5 +76,8 @@ export class $Container<H extends HTMLElement = HTMLElement> extends $HTMLElemen
scrollLeft(scrollLeft?: $StateArgument<number> | undefined) { return $.fluent(this, arguments, () => this.dom.scrollLeft, () => $.set(this.dom, 'scrollLeft', scrollLeft as any))}
}
export type $ContainerContentBuilder<P extends $Container> = OrMatrix<$ContainerContentType> | (($node: P) => OrMatrix<$ContainerContentType>)
export type $ContainerContentType = $Node | string | undefined | $State<any>
export type $ContainerContentBuilder<P extends $Container> = $ContainerContentGroup | (($node: P) => OrPromise<$ContainerContentGroup>)
export type $ContainerContentGroup = OrMatrix<OrPromise<$ContainerContentType>>
export type $ContainerContentType = $Node | string | undefined | $State<any> | null
export interface $ContainerEventMap extends $HTMLElementEventMap {}

View File

@ -1,12 +1,13 @@
import { $Node } from "./$Node";
import { $Node, $NodeEventMap } from "./$Node";
export interface $ElementOptions {
id?: string;
class?: string[];
dom?: HTMLElement | SVGElement;
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;
private static_classes = new Set<string>();
constructor(tagname: string, options?: $ElementOptions) {
@ -19,7 +20,7 @@ export class $Element<H extends HTMLElement | SVGElement = HTMLElement> extends
private createDom(tagname: string, options?: $ElementOptions) {
if (options?.dom) return options.dom;
if (tagname === 'svg') return document.createElementNS("http://www.w3.org/2000/svg", "svg");
return document.createElement(tagname);
return document.createElement(options?.tagname ?? tagname);
}
@ -80,13 +81,13 @@ export class $Element<H extends HTMLElement | SVGElement = HTMLElement> 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) {
const animation = this.dom.animate(keyframes, options);
if (callback) callback(animation);
return this;
if (callback) animation.onfinish = () => callback(animation);
return animation;
}
getAnimations(options?: GetAnimationsOptions) { return this.dom.getAnimations(options) }
@ -110,4 +111,6 @@ 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,3 +1,5 @@
import { $HTMLElementAPIFilter, $HTMLElementAPIs } from "../$ElementTemplate";
import { $Util } from "../$Util";
import { $Container, $ContainerOptions } from "./$Container";
export interface $FormOptions extends $ContainerOptions {}
export class $Form extends $Container<HTMLFormElement> {
@ -5,10 +7,6 @@ export class $Form extends $Container<HTMLFormElement> {
super('form', options);
}
autocomplete(): AutoFill;
autocomplete(autocomplete: AutoFill | undefined): this;
autocomplete(autocomplete?: AutoFill) { return $.fluent(this, arguments, () => this.dom.autocomplete as AutoFill, () => $.set(this.dom, 'autocomplete', autocomplete as AutoFillBase))}
action(): string;
action(action: string | undefined): this;
action(action?: string) { return $.fluent(this, arguments, () => this.dom.formAction, () => $.set(this.dom, 'formAction', action))}
@ -36,9 +34,10 @@ export class $Form extends $Container<HTMLFormElement> {
requestSubmit() { this.dom.requestSubmit(); return this }
reset(): this { this.dom.reset(); return this }
submit() { this.dom.submit(); return this }
checkValidity() { return this.dom.checkValidity() }
reportValidity() { return this.dom.reportValidity() }
get length() { return this.dom.length }
get elements() { return Array.from(this.dom.elements).map(ele => $(ele)) }
}
}
export interface $Form extends $HTMLElementAPIFilter<$Form, 'checkValidity' | 'reportValidity' | 'autocomplete'> {}
$Util.mixin($Form, $HTMLElementAPIs.create('checkValidity', 'reportValidity', 'autocomplete'))

View File

@ -1,7 +1,7 @@
import { $Element, $ElementOptions } from "./$Element";
import { $Element, $ElementEventMap, $ElementOptions } from "./$Element";
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) {
super(tagname, options)
}
@ -62,4 +62,6 @@ export class $HTMLElement<H extends HTMLElement = HTMLElement> extends $Element<
get offsetParent() { return $(this.dom.offsetParent) }
get offsetTop() { return this.dom.offsetTop }
get offsetWidth() { return this.dom.offsetWidth }
}
}
export interface $HTMLElementEventMap extends $ElementEventMap {}

View File

@ -1,5 +1,5 @@
import { $HTMLElement, $HTMLElementOptions } from "./$HTMLElement";
import { $State } from "../$State";
import { $StateArgument } from "../$State";
export interface $ImageOptions extends $HTMLElementOptions {}
export class $Image extends $HTMLElement<HTMLImageElement> {
constructor(options?: $ImageOptions) {
@ -81,8 +81,8 @@ export class $Image extends $HTMLElement<HTMLImageElement> {
/**HTMLImageElement base property */
src(): string;
src(src?: string | $State<string | undefined>): this;
src(src?: string | $State<string | undefined>) { return $.fluent(this, arguments, () => this.dom.src, () => $.set(this.dom, 'src', src))}
src(src: $StateArgument<string>): this;
src(src?: $StateArgument<string>) { return $.fluent(this, arguments, () => this.dom.src, () => $.set(this.dom, 'src', src))}
/**HTMLImageElement base property */
srcset(): string;

View File

@ -1,15 +1,30 @@
import { $Element, $ElementOptions } from "./$Element";
import { $State, $StateArgument } from "../$State";
import { $HTMLElementAPIFilter, $HTMLElementAPIs } from "../$ElementTemplate";
import { $Util } from "../$Util";
import { $HTMLElement, $HTMLElementOptions } from "./$HTMLElement";
export interface $InputOptions extends $ElementOptions {}
export class $Input extends $Element<HTMLInputElement> {
export interface $InputOptions extends $HTMLElementOptions {}
export class $Input<T extends string | number = string> extends $HTMLElement<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() )}
value(): T;
value(value: $StateArgument<T>): this;
value(value?: $StateArgument<T>) { return $.fluent(this, arguments, () => {
if (this.type() === 'number') return Number(this.dom.value);
return this.dom.value as T;
}, () => $.set(this.dom, 'value', value as $State<string> | string, (value$) => {
this.on('input', () => {
if (value$.attributes.has(this.dom) === false) return;
if (typeof value$.value === 'string') (value$ as $State<string>).set(`${this.value()}`)
if (typeof value$.value === 'number') (value$ as unknown as $State<number>).set(Number(this.value()))
})
}))}
type(): InputType;
type<T extends InputType>(type: T): $InputType<T>;
type<T extends InputType>(type?: T) { return $.fluent(this, arguments, () => this.dom.type, () => $.set(this.dom, 'type', type)) as unknown as $InputType<T> | InputType}
capture(): string;
capture(capture: string): this;
@ -26,51 +41,15 @@ export class $Input extends $Element<HTMLInputElement> {
width(): number;
width(wdith: number): this;
width(width?: number) { return $.fluent(this, arguments, () => this.dom.width, () => $.set(this.dom, 'width', width))}
checked(): boolean;
checked(boolean: boolean): this;
checked(boolean?: boolean) { return $.fluent(this, arguments, () => this.dom.checked, () => $.set(this.dom, 'checked', boolean))}
max(): number;
max(max: number): this;
max(max?: number) { return $.fluent(this, arguments, () => this.dom.max === '' ? null : parseInt(this.dom.min), () => $.set(this.dom, 'max', max?.toString()))}
min(): number;
min(min: number): this;
min(min?: number) { return $.fluent(this, arguments, () => this.dom.min === '' ? null : parseInt(this.dom.min), () => $.set(this.dom, 'min', min?.toString()))}
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))}
pattern(): string;
pattern(pattern: string): this;
pattern(pattern?: string) { return $.fluent(this, arguments, () => this.dom.pattern, () => $.set(this.dom, 'pattern', pattern))}
@ -83,10 +62,6 @@ export class $Input extends $Element<HTMLInputElement> {
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))}
@ -106,14 +81,6 @@ export class $Input extends $Element<HTMLInputElement> {
src(): string;
src(src: string): this;
src(src?: string) { return $.fluent(this, arguments, () => this.dom.src, () => $.set(this.dom, 'src', src))}
step(): number;
step(step: number): this;
step(step?: number) { return $.fluent(this, arguments, () => Number(this.dom.step), () => $.set(this.dom, 'step', step?.toString()))}
type(): InputType;
type(type: InputType): this;
type(type?: InputType) { return $.fluent(this, arguments, () => this.dom.type, () => $.set(this.dom, 'type', type))}
inputMode(): InputMode;
inputMode(mode: InputMode): this;
@ -142,46 +109,76 @@ export class $Input extends $Element<HTMLInputElement> {
}
setSelectionRange(start: number | null, end: number | null, direction?: SelectionDirection) { this.dom.setSelectionRange(start, end, direction); return this }
showPicker() { this.dom.showPicker(); return this }
get files() { return this.dom.files }
get webkitEntries() { return this.dom.webkitEntries }
get labels() { return Array.from(this.dom.labels ?? []).map(label => $(label)) }
}
export interface $Input extends $HTMLElementAPIFilter<$Input, 'checkValidity' | 'reportValidity' | 'autocomplete' | 'name' | 'form' | 'required' | 'validationMessage' | 'validity' | 'willValidate' | 'formAction' | 'formEnctype' | 'formMethod' | 'formNoValidate' | 'formTarget'> {}
$Util.mixin($Input, $HTMLElementAPIs.create('checkValidity', 'reportValidity', 'autocomplete', 'name', 'form', 'required', 'validationMessage', 'validity', 'willValidate', 'formAction', 'formEnctype', 'formMethod', 'formNoValidate', 'formTarget'))
export class $NumberInput extends $Input<number> {
constructor(options?: $InputOptions) {
super(options)
this.type('number')
}
static from($input: $Input) {
return $.mixin($Input, this) as $NumberInput;
}
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 webkitEntries() { return this.dom.webkitEntries }
max(): number;
max(max: number): this;
max(max?: number) { return $.fluent(this, arguments, () => this.dom.max === '' ? null : parseInt(this.dom.min), () => $.set(this.dom, 'max', max?.toString()))}
formAction(): string;
formAction(action: string | undefined): this;
formAction(action?: string) { return $.fluent(this, arguments, () => this.dom.formAction, () => $.set(this.dom, 'formAction', action))}
formEnctype(): string;
formEnctype(enctype: string | undefined): this;
formEnctype(enctype?: string) { return $.fluent(this, arguments, () => this.dom.formEnctype, () => $.set(this.dom, 'formEnctype', enctype))}
min(): number;
min(min: number): this;
min(min?: number) { return $.fluent(this, arguments, () => this.dom.min === '' ? null : parseInt(this.dom.min), () => $.set(this.dom, 'min', min?.toString()))}
formMethod(): string;
formMethod(method: string | undefined): this;
formMethod(method?: string) { return $.fluent(this, arguments, () => this.dom.formMethod, () => $.set(this.dom, 'formMethod', method))}
step(): number;
step(step: number): this;
step(step?: number) { return $.fluent(this, arguments, () => Number(this.dom.step), () => $.set(this.dom, 'step', step?.toString()))}
}
formNoValidate(): boolean;
formNoValidate(boolean: boolean | undefined): this;
formNoValidate(boolean?: boolean) { return $.fluent(this, arguments, () => this.dom.formNoValidate, () => $.set(this.dom, 'formNoValidate', boolean))}
export class $CheckInput extends $Input<string> {
constructor(options?: $InputOptions) {
super(options)
this.type('radio')
}
formTarget(): string;
formTarget(target: string | undefined): this;
formTarget(target?: string) { return $.fluent(this, arguments, () => this.dom.formTarget, () => $.set(this.dom, 'formTarget', target))}
static from($input: $Input) {
return $.mixin($Input, this) as $CheckInput;
}
checked(): boolean;
checked(boolean: boolean): this;
checked(boolean?: boolean) { return $.fluent(this, arguments, () => this.dom.checked, () => $.set(this.dom, 'checked', boolean))}
name(): string;
name(name?: $StateArgument<string> | undefined): this;
name(name?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))}
value(): string;
value(value: $StateArgument<string> | undefined): this;
value(value?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value))}
defaultChecked(): boolean;
defaultChecked(defaultChecked: boolean): this;
defaultChecked(defaultChecked?: boolean) { return $.fluent(this, arguments, () => this.dom.defaultChecked, () => $.set(this.dom, 'defaultChecked', defaultChecked))}
}
get form() { return this.dom.form ? $(this.dom.form) : null }
get labels() { return Array.from(this.dom.labels ?? []).map(label => $(label)) }
get validationMessage() { return this.dom.validationMessage }
get validity() { return this.dom.validity }
get willValidate() { return this.dom.willValidate }
}
export class $FileInput extends $Input<string> {
constructor(options?: $InputOptions) {
super(options)
this.type('file')
}
static from($input: $Input) {
return $.mixin($Input, this) as $FileInput;
}
multiple(): boolean;
multiple(multiple: boolean): this;
multiple(multiple?: boolean) { return $.fluent(this, arguments, () => this.dom.multiple, () => $.set(this.dom, 'multiple', multiple))}
accept(): string[]
accept(...filetype: string[]): this
accept(...filetype: string[]) { return $.fluent(this, arguments, () => this.dom.accept.split(','), () => this.dom.accept = filetype.toString() )}
}
export type $InputType<T extends InputType> = T extends 'number' ? $NumberInput : T extends 'radio' | 'checkbox' ? $CheckInput : T extends 'file' ? $FileInput : $Input<string>;
$Util.mixin($Input, [$NumberInput, $CheckInput, $FileInput])

View File

@ -1,3 +1,6 @@
import { $HTMLElementAPIFilter, $HTMLElementAPIs } from "../$ElementTemplate";
import { $StateArgument } from "../$State";
import { $Util } from "../$Util";
import { $Container, $ContainerOptions } from "./$Container";
export interface $LabelOptions extends $ContainerOptions {}
export class $Label extends $Container<HTMLLabelElement> {
@ -6,9 +9,11 @@ export class $Label extends $Container<HTMLLabelElement> {
}
for(): string;
for(name?: string): this;
for(name?: string) { return $.fluent(this, arguments, () => this.dom.htmlFor, () => { $.set(this.dom, 'htmlFor', name, 'for')}) }
for(name: $StateArgument<string>): this;
for(name?: $StateArgument<string>) { return $.fluent(this, arguments, () => this.dom.htmlFor, () => { $.set(this.dom, 'htmlFor', name)}) }
get form() { return this.dom.form }
get control() { return this.dom.control }
}
}
export interface $Label extends $HTMLElementAPIFilter<$Label, 'form'> {}
$Util.mixin($Label, $HTMLElementAPIs.create('form',))

107
lib/node/$Media.ts Normal file
View File

@ -0,0 +1,107 @@
import { $StateArgument } from "../$State";
import { $ElementOptions } from "./$Element";
import { $HTMLElement } from "./$HTMLElement";
export interface $MediaOptions extends $ElementOptions {}
export class $Media<H extends HTMLMediaElement> extends $HTMLElement<H> {
constructor(tagname: string, options?: $MediaOptions) {
super(tagname, options);
}
autoplay(): boolean;
autoplay(autoplay: $StateArgument<boolean>): this;
autoplay(autoplay?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.autoplay, () => $.set<HTMLMediaElement, 'autoplay'>(this.dom, 'autoplay', autoplay))}
get buffered() { return this.dom.buffered }
controls(): boolean;
controls(controls: $StateArgument<boolean>): this;
controls(controls?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.controls, () => $.set<HTMLMediaElement, 'controls'>(this.dom, 'controls', controls))}
crossOrigin(): string | null;
crossOrigin(crossOrigin: $StateArgument<string | null>): this;
crossOrigin(crossOrigin?: $StateArgument<string | null>) { return $.fluent(this, arguments, () => this.dom.crossOrigin, () => $.set<HTMLMediaElement, 'crossOrigin'>(this.dom, 'crossOrigin', crossOrigin))}
get currentSrc() { return this.dom.currentSrc };
currentTime(): number;
currentTime(currentTime: $StateArgument<number>): this;
currentTime(currentTime?: $StateArgument<number>) { return $.fluent(this, arguments, () => this.dom.currentTime, () => $.set<HTMLMediaElement, 'currentTime'>(this.dom, 'currentTime', currentTime))}
defaultMuted(): boolean;
defaultMuted(defaultMuted: $StateArgument<boolean>): this;
defaultMuted(defaultMuted?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.defaultMuted, () => $.set<HTMLMediaElement, 'defaultMuted'>(this.dom, 'defaultMuted', defaultMuted))}
defaultPlaybackRate(): number;
defaultPlaybackRate(defaultPlaybackRate: $StateArgument<number>): this;
defaultPlaybackRate(defaultPlaybackRate?: $StateArgument<number>) { return $.fluent(this, arguments, () => this.dom.defaultPlaybackRate, () => $.set<HTMLMediaElement, 'defaultPlaybackRate'>(this.dom, 'defaultPlaybackRate', defaultPlaybackRate))}
disableRemotePlayback(): boolean;
disableRemotePlayback(disableRemotePlayback: $StateArgument<boolean>): this;
disableRemotePlayback(disableRemotePlayback?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.disableRemotePlayback, () => $.set<HTMLMediaElement, 'disableRemotePlayback'>(this.dom, 'disableRemotePlayback', disableRemotePlayback))}
get duration() { return this.dom.duration }
get ended() { return this.dom.ended }
get error() { return this.dom.error }
loop(): boolean;
loop(loop: $StateArgument<boolean>): this;
loop(loop?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.loop, () => $.set<HTMLMediaElement, 'loop'>(this.dom, 'loop', loop))}
mediaKeys(): MediaKeys | null;
mediaKeys(mediaKeys: $StateArgument<[MediaKeys | null]>): this;
mediaKeys(mediaKeys?: $StateArgument<[MediaKeys | null]>) { return $.fluent(this, arguments, () => this.dom.mediaKeys, () => $.set<HTMLMediaElement, 'setMediaKeys'>(this.dom, 'setMediaKeys', [mediaKeys]))}
muted(): boolean;
muted(muted: $StateArgument<boolean>): this;
muted(muted?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.muted, () => $.set<HTMLMediaElement, 'muted'>(this.dom, 'muted', muted))}
get networkState() { return this.dom.networkState }
get paused() { return this.dom.paused }
playbackRate(): number;
playbackRate(playbackRate: $StateArgument<number>): this;
playbackRate(playbackRate?: $StateArgument<number>) { return $.fluent(this, arguments, () => this.dom.playbackRate, () => $.set<HTMLMediaElement, 'playbackRate'>(this.dom, 'playbackRate', playbackRate))}
get played() { return this.dom.played }
preload(): this['dom']['preload'];
preload(preload: $StateArgument<this['dom']['preload']>): this;
preload(preload?: $StateArgument<this['dom']['preload']>) { return $.fluent(this, arguments, () => this.dom.preload, () => $.set<HTMLMediaElement, 'preload'>(this.dom, 'preload', preload))}
preservesPitch(): boolean;
preservesPitch(preservesPitch: $StateArgument<boolean>): this;
preservesPitch(preservesPitch?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.preservesPitch, () => $.set<HTMLMediaElement, 'preservesPitch'>(this.dom, 'preservesPitch', preservesPitch))}
get readyState() { return this.dom.readyState }
get remote() { return this.dom.remote }
get seekable() { return this.dom.seekable }
get seeking() { return this.dom.seeking }
sinkId(): string;
sinkId(sinkId: $StateArgument<[string]>): this;
sinkId(sinkId?: $StateArgument<[string]>) { return $.fluent(this, arguments, () => this.dom.sinkId, () => $.set<HTMLMediaElement, 'setSinkId'>(this.dom, 'setSinkId', [sinkId]))}
src(): string;
src(src: $StateArgument<string>): this;
src(src?: $StateArgument<string>) { return $.fluent(this, arguments, () => this.dom.src, () => $.set<HTMLMediaElement, 'src'>(this.dom, 'src', src))}
srcObject(): MediaProvider | null;
srcObject(srcObject: $StateArgument<MediaProvider | null>): this;
srcObject(srcObject?: $StateArgument<MediaProvider | null>) { return $.fluent(this, arguments, () => this.dom.srcObject, () => $.set<HTMLMediaElement, 'srcObject'>(this.dom, 'srcObject', srcObject))}
get textTracks() { return this.dom.textTracks }
volume(): number;
volume(volume: $StateArgument<number>): this;
volume(volume?: $StateArgument<number>) { return $.fluent(this, arguments, () => this.dom.volume, () => $.set<HTMLMediaElement, 'volume'>(this.dom, 'volume', volume))}
addTextTrack(kind: TextTrackKind, label?: string, language?: string) { return this.dom.addTextTrack(kind, label, language)}
canPlayType(type: string) { return this.dom.canPlayType(type) }
fastSeek(time: number) { this.dom.fastSeek(time); return this }
load() { this.dom.load(); return this }
pause() { this.dom.pause(); return this }
async play() { await this.dom.play(); return this}
get isPlaying() { return this.currentTime() > 0 && !this.paused && !this.ended && this.readyState > 2 }
}

View File

@ -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<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;
readonly __hidden: boolean = false;
private domEvents: {[key: string]: Map<Function, Function>} = {};
protected __$property__ = {
hidden: false,
coordinate: undefined as $NodeCoordinate | undefined
}
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(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 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,10 +38,38 @@ export abstract class $Node<N extends Node = 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;
}
}
get element(): $Element | null {
if (this instanceof $Element) return this;
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,5 +1,7 @@
import { $Container, $ContainerOptions } from "./$Container";
import { $State, $StateArgument } from "../$State";
import { $StateArgument } from "../$State";
import { $HTMLElementAPIFilter, $HTMLElementAPIs } from "../$ElementTemplate";
import { $Util } from "../$Util";
export interface $OptGroupOptions extends $ContainerOptions {}
export class $OptGroup extends $Container<HTMLOptGroupElement> {
@ -10,8 +12,7 @@ export class $OptGroup extends $Container<HTMLOptGroupElement> {
disabled(): boolean;
disabled(disabled: $StateArgument<boolean> | undefined): this;
disabled(disabled?: $StateArgument<boolean> | undefined) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))}
label(): string;
label(label: $StateArgument<string> | undefined): this;
label(label?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.label, () => $.set(this.dom, 'label', label))}
}
}
export interface $OptGroup extends $HTMLElementAPIFilter<$OptGroup, 'disabled' | 'label'> {}
$Util.mixin($OptGroup, $HTMLElementAPIs.create('disabled', 'label'))

View File

@ -1,5 +1,7 @@
import { $Container, $ContainerOptions } from "./$Container";
import { $State, $StateArgument } from "../$State";
import { $HTMLElementAPIFilter, $HTMLElementAPIs } from "../$ElementTemplate";
import { $Util } from "../$Util";
export interface $OptionOptions extends $ContainerOptions {}
export class $Option extends $Container<HTMLOptionElement> {
@ -11,14 +13,6 @@ export class $Option extends $Container<HTMLOptionElement> {
defaultSelected(defaultSelected: $StateArgument<boolean> | undefined): this;
defaultSelected(defaultSelected?: $StateArgument<boolean> | undefined) { return $.fluent(this, arguments, () => this.dom.defaultSelected, () => $.set(this.dom, 'defaultSelected', defaultSelected))}
disabled(): boolean;
disabled(disabled: $StateArgument<boolean> | undefined): this;
disabled(disabled?: $StateArgument<boolean> | undefined) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))}
label(): string;
label(label: $StateArgument<string> | undefined): this;
label(label?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.label, () => $.set(this.dom, 'label', label))}
selected(): boolean;
selected(selected: $StateArgument<boolean> | undefined): this;
selected(selected?: $StateArgument<boolean> | undefined) { return $.fluent(this, arguments, () => this.dom.selected, () => $.set(this.dom, 'selected', selected))}
@ -34,4 +28,7 @@ export class $Option extends $Container<HTMLOptionElement> {
get form() { return this.dom.form ? $(this.dom.form) : null }
get index() { return this.dom.index }
}
}
export interface $Option extends $HTMLElementAPIFilter<$Option, 'form' | 'disabled' | 'label'> {}
$Util.mixin($Option, $HTMLElementAPIs.create('form', 'disabled', 'label'))

View File

@ -2,6 +2,8 @@ import { $Container, $ContainerOptions } from "./$Container";
import { $OptGroup } from "./$OptGroup";
import { $Option } from "./$Option";
import { $State, $StateArgument } from "../$State";
import { $HTMLElementAPIFilter, $HTMLElementAPIs } from "../$ElementTemplate";
import { $Util } from "../$Util";
export interface $SelectOptions extends $ContainerOptions {}
export class $Select extends $Container<HTMLSelectElement> {
@ -17,41 +19,30 @@ export class $Select extends $Container<HTMLSelectElement> {
item(index: number) { return $(this.dom.item(index)) }
namedItem(name: string) { return $(this.dom.namedItem(name)) }
disabled(): boolean;
disabled(disabled: $StateArgument<boolean> | undefined): this;
disabled(disabled?: $StateArgument<boolean> | undefined) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))}
multiple(): boolean;
multiple(multiple: $StateArgument<boolean> | undefined): this;
multiple(multiple?: $StateArgument<boolean> | undefined) { return $.fluent(this, arguments, () => this.dom.multiple, () => $.set(this.dom, 'multiple', multiple))}
required(): boolean;
required(required: boolean): this;
required(required?: boolean) { return $.fluent(this, arguments, () => this.dom.required, () => $.set(this.dom, 'required', required))}
autocomplete(): AutoFill;
autocomplete(autocomplete: AutoFill): this;
autocomplete(autocomplete?: AutoFill) { return $.fluent(this, arguments, () => this.dom.autocomplete, () => $.set(this.dom, 'autocomplete', autocomplete))}
get length() { return this.dom.length }
get size() { return this.dom.size }
get options() { return Array.from(this.dom.options).map($option => $($option)) }
get selectedIndex() { return this.dom.selectedIndex }
get selectedOptions() { return Array.from(this.dom.selectedOptions).map($option => $($option)) }
name(): string;
name(name?: $StateArgument<string> | undefined): this;
name(name?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))}
value(): string;
value(value?: $StateArgument<string> | undefined): this;
value(value?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value))}
value(value?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value as $State<string> | string, (value$) => {
this.on('input', () => {
if (value$.attributes.has(this.dom) === false) return;
if (typeof value$.value === 'string') (value$ as $State<string>).set(`${this.value()}`)
if (typeof value$.value === 'number') (value$ as unknown as $State<number>).set(Number(this.value()))
})
}))}
get form() { return this.dom.form ? $(this.dom.form) : null }
get labels() { return Array.from(this.dom.labels ?? []).map(label => $(label)) }
get validationMessage() { return this.dom.validationMessage }
get validity() { return this.dom.validity }
get willValidate() { return this.dom.willValidate }
}
export interface $Select extends $HTMLElementAPIFilter<$Select, 'checkValidity' | 'reportValidity' | 'autocomplete' | 'name' | 'form' | 'required' | 'disabled' | 'validationMessage' | 'validity' | 'willValidate'> {}
$Util.mixin($Select, $HTMLElementAPIs.create('checkValidity', 'reportValidity', 'autocomplete', 'name', 'form', 'required', 'disabled', 'validationMessage', 'validity', 'willValidate'))
export type $SelectContentType = $Option | $OptGroup | undefined;

View File

@ -1,5 +1,7 @@
import { $Container, $ContainerOptions } from "./$Container";
import { $StateArgument } from "../$State";
import { $HTMLElementAPIFilter, $HTMLElementAPIs } from "../$ElementTemplate";
import { $Util } from "../$Util";
export interface $TextareaOptions extends $ContainerOptions {}
export class $Textarea extends $Container<HTMLTextAreaElement> {
@ -11,10 +13,6 @@ export class $Textarea extends $Container<HTMLTextAreaElement> {
cols(cols: number): this;
cols(cols?: number) { return $.fluent(this, arguments, () => this.dom.cols, () => $.set(this.dom, 'cols', cols))}
name(): string;
name(name?: $StateArgument<string> | undefined): this;
name(name?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))}
wrap(): string;
wrap(wrap?: $StateArgument<string> | undefined): this;
wrap(wrap?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.wrap, () => $.set(this.dom, 'wrap', wrap))}
@ -23,18 +21,6 @@ export class $Textarea extends $Container<HTMLTextAreaElement> {
value(value?: $StateArgument<string> | undefined): this;
value(value?: $StateArgument<string> | undefined) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value))}
maxLength(): number;
maxLength(maxLength: number): this;
maxLength(maxLength?: number) { return $.fluent(this, arguments, () => this.dom.maxLength, () => $.set(this.dom, 'maxLength', maxLength))}
minLength(): number;
minLength(minLength: number): this;
minLength(minLength?: number) { return $.fluent(this, arguments, () => this.dom.minLength, () => $.set(this.dom, 'minLength', minLength))}
autocomplete(): AutoFill;
autocomplete(autocomplete: AutoFill): this;
autocomplete(autocomplete?: AutoFill) { return $.fluent(this, arguments, () => this.dom.autocomplete, () => $.set(this.dom, 'autocomplete', autocomplete))}
defaultValue(): string;
defaultValue(defaultValue: string): this;
defaultValue(defaultValue?: string) { return $.fluent(this, arguments, () => this.dom.defaultValue, () => $.set(this.dom, 'defaultValue', defaultValue))}
@ -43,10 +29,6 @@ export class $Textarea extends $Container<HTMLTextAreaElement> {
dirName(dirName: string): this;
dirName(dirName?: string) { return $.fluent(this, arguments, () => this.dom.dirName, () => $.set(this.dom, 'dirName', dirName))}
disabled(): boolean;
disabled(disabled: boolean): this;
disabled(disabled?: boolean) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))}
placeholder(): string;
placeholder(placeholder?: string): this;
placeholder(placeholder?: string) { return $.fluent(this, arguments, () => this.dom.placeholder, () => $.set(this.dom, 'placeholder', placeholder))}
@ -55,10 +37,6 @@ export class $Textarea extends $Container<HTMLTextAreaElement> {
readOnly(readOnly: boolean): this;
readOnly(readOnly?: boolean) { return $.fluent(this, arguments, () => this.dom.readOnly, () => $.set(this.dom, 'readOnly', readOnly))}
required(): boolean;
required(required: boolean): this;
required(required?: boolean) { return $.fluent(this, arguments, () => this.dom.required, () => $.set(this.dom, 'required', required))}
selectionDirection(): SelectionDirection;
selectionDirection(selectionDirection: SelectionDirection): this;
selectionDirection(selectionDirection?: SelectionDirection) { return $.fluent(this, arguments, () => this.dom.selectionDirection, () => $.set(this.dom, 'selectionDirection', selectionDirection))}
@ -89,12 +67,9 @@ export class $Textarea extends $Container<HTMLTextAreaElement> {
return this
}
setSelectionRange(start: number | null, end: number | null, direction?: SelectionDirection) { this.dom.setSelectionRange(start, end, direction); return this }
checkValidity() { return this.dom.checkValidity() }
reportValidity() { return this.dom.reportValidity() }
get validationMessage() { return this.dom.validationMessage }
get validity() { return this.dom.validity }
get form() { return this.dom.form ? $(this.dom.form) : null }
get labels() { return Array.from(this.dom.labels ?? []).map(label => $(label)) }
}
}
export interface $Textarea extends $HTMLElementAPIFilter<$Textarea, 'checkValidity' | 'reportValidity' | 'autocomplete' | 'name' | 'form' | 'required' | 'disabled' | 'minLength' | 'maxLength' | 'validationMessage' | 'validity' | 'willValidate'> {}
$Util.mixin($Textarea, $HTMLElementAPIs.create('checkValidity', 'reportValidity', 'autocomplete', 'name', 'form', 'required', 'disabled', 'minLength', 'maxLength', 'validationMessage', 'validity', 'willValidate'))

37
lib/node/$Video.ts Normal file
View File

@ -0,0 +1,37 @@
import { $StateArgument } from "../$State";
import { $Media, $MediaOptions } from "./$Media";
export interface $VideoOptions extends $MediaOptions {}
export class $Video extends $Media<HTMLVideoElement> {
constructor(options?: $VideoOptions) {
super('video', options)
}
disablePictureInPicture(): boolean;
disablePictureInPicture(disablePictureInPicture: $StateArgument<boolean>): this;
disablePictureInPicture(disablePictureInPicture?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.disablePictureInPicture, () => $.set(this.dom, 'disablePictureInPicture', disablePictureInPicture))}
height(): number;
height(height: $StateArgument<number>): this;
height(height?: $StateArgument<number>) { return $.fluent(this, arguments, () => this.dom.height, () => $.set(this.dom, 'height', height))}
width(): number;
width(width: $StateArgument<number>): this;
width(width?: $StateArgument<number>) { return $.fluent(this, arguments, () => this.dom.width, () => $.set(this.dom, 'width', width))}
playsInline(): boolean;
playsInline(playsInline: $StateArgument<boolean>): this;
playsInline(playsInline?: $StateArgument<boolean>) { return $.fluent(this, arguments, () => this.dom.playsInline, () => $.set(this.dom, 'playsInline', playsInline))}
poster(): string;
poster(poster: $StateArgument<string>): this;
poster(poster?: $StateArgument<string>) { return $.fluent(this, arguments, () => this.dom.poster, () => $.set(this.dom, 'poster', poster))}
get videoHeight() { return this.dom.videoHeight }
get videoWidth() { return this.dom.videoWidth }
cancelVideoFrameCallback(handle: number) { this.dom.cancelVideoFrameCallback(handle); return this }
getVideoPlaybackQuality() { return this.dom.getVideoPlaybackQuality() }
requestPictureInPicture() { return this.dom.requestPictureInPicture() }
requestVideoFrameCallback(callback: VideoFrameRequestCallback) { return this.dom.requestVideoFrameCallback(callback) }
}

View File

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

View File

@ -1,21 +1,21 @@
{
"name": "elexis",
"description": "Web library design for JS/TS lover.",
"version": "0.1.0",
"description": "Build Web in Native JavaScript Syntax",
"version": "0.3.0",
"author": {
"name": "defaultkavy",
"email": "defaultkavy@gmail.com",
"url": "https://github.com/defaultkavy"
"url": "https://git.defaultkavy.com/defaultkavy"
},
"repository": {
"type": "git",
"url": "git+https://github.com/defaultkavy/elexis.git"
"url": "git+https://git.defaultkavy.com/defaultkavy/elexis.git"
},
"module": "index.ts",
"bugs": {
"url": "https://github.com/defaultkavy/elexis/issues"
"url": "https://git.defaultkavy.com/defaultkavy/elexis/issues"
},
"homepage": "https://github.com/defaultkavy/elexis",
"homepage": "https://git.defaultkavy.com/defaultkavy/elexis",
"keywords": ["web", "front-end", "lib", "fluent", "framework"],
"license": "ISC",
"type": "module"