Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
4c078c26b6 | |||
a57246e6e1 | |||
c5b99d8835 | |||
f614ecd5f5 | |||
72f617df3c | |||
7f8f599b8a | |||
4c143cad90 | |||
6454ddab48 |
50
$index.ts
50
$index.ts
@ -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)
|
||||
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
1
.gitignore
vendored
@ -1,3 +1,2 @@
|
||||
.npmignore
|
||||
bun.lockb
|
||||
node_modules
|
2
.npmignore
Normal file
2
.npmignore
Normal file
@ -0,0 +1,2 @@
|
||||
.gitignore
|
||||
assets
|
59
README.md
59
README.md
@ -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.
|
31
index.ts
31
index.ts
@ -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,24 +27,33 @@ 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";
|
||||
@ -48,3 +61,5 @@ export * from "./lib/node/$Textarea";
|
||||
export * from "./lib/node/$Image";
|
||||
export * from "./lib/node/$Async";
|
||||
export * from "./lib/node/$Document";
|
||||
export * from "./lib/node/$Media";
|
||||
export * from "./lib/node/$Video";
|
94
lib/$ElementTemplate.ts
Normal file
94
lib/$ElementTemplate.ts
Normal 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>
|
@ -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
50
lib/$EventTarget.ts
Normal 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
175
lib/$FocusManager.ts
Normal 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
52
lib/$KeyboardManager.ts
Normal 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>};
|
@ -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
105
lib/$PointerManager.ts
Normal 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'
|
@ -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);
|
@ -42,10 +42,14 @@ 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
|
||||
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);
|
||||
|
9
lib/$Window.ts
Normal file
9
lib/$Window.ts
Normal 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 {}
|
@ -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;
|
||||
|
@ -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'))
|
||||
}
|
||||
|
||||
|
@ -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'))
|
@ -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);
|
||||
}
|
||||
this.children.render();
|
||||
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(); // 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 {}
|
@ -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) }
|
||||
@ -111,3 +112,5 @@ export class $Element<H extends HTMLElement | SVGElement = HTMLElement> extends
|
||||
}
|
||||
|
||||
export type $DOMRect = Omit<DOMRect, 'toJSON'>;
|
||||
|
||||
export interface $ElementEventMap extends $NodeEventMap {}
|
@ -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'))
|
@ -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)
|
||||
}
|
||||
@ -63,3 +63,5 @@ export class $HTMLElement<H extends HTMLElement = HTMLElement> extends $Element<
|
||||
get offsetTop() { return this.dom.offsetTop }
|
||||
get offsetWidth() { return this.dom.offsetWidth }
|
||||
}
|
||||
|
||||
export interface $HTMLElementEventMap extends $ElementEventMap {}
|
@ -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;
|
||||
|
@ -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;
|
||||
@ -27,50 +42,14 @@ export class $Input extends $Element<HTMLInputElement> {
|
||||
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))}
|
||||
@ -107,14 +82,6 @@ export class $Input extends $Element<HTMLInputElement> {
|
||||
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;
|
||||
inputMode(mode?: InputMode) { return $.fluent(this, arguments, () => this.dom.inputMode as InputMode, () => $.set(this.dom, 'inputMode', mode))}
|
||||
@ -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()))}
|
||||
|
||||
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()))}
|
||||
|
||||
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))}
|
||||
|
||||
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))}
|
||||
|
||||
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 }
|
||||
step(): number;
|
||||
step(step: number): this;
|
||||
step(step?: number) { return $.fluent(this, arguments, () => Number(this.dom.step), () => $.set(this.dom, 'step', step?.toString()))}
|
||||
}
|
||||
|
||||
export class $CheckInput extends $Input<string> {
|
||||
constructor(options?: $InputOptions) {
|
||||
super(options)
|
||||
this.type('radio')
|
||||
}
|
||||
|
||||
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))}
|
||||
|
||||
defaultChecked(): boolean;
|
||||
defaultChecked(defaultChecked: boolean): this;
|
||||
defaultChecked(defaultChecked?: boolean) { return $.fluent(this, arguments, () => this.dom.defaultChecked, () => $.set(this.dom, 'defaultChecked', defaultChecked))}
|
||||
}
|
||||
|
||||
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])
|
@ -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
107
lib/node/$Media.ts
Normal 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 }
|
||||
}
|
@ -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];
|
||||
};
|
@ -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'))
|
@ -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))}
|
||||
@ -35,3 +29,6 @@ export class $Option extends $Container<HTMLOptionElement> {
|
||||
get index() { return this.dom.index }
|
||||
|
||||
}
|
||||
|
||||
export interface $Option extends $HTMLElementAPIFilter<$Option, 'form' | 'disabled' | 'label'> {}
|
||||
$Util.mixin($Option, $HTMLElementAPIs.create('form', 'disabled', 'label'))
|
@ -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;
|
@ -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))}
|
||||
@ -90,11 +68,8 @@ export class $Textarea extends $Container<HTMLTextAreaElement> {
|
||||
}
|
||||
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
37
lib/node/$Video.ts
Normal 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) }
|
||||
}
|
@ -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]
|
||||
}
|
12
package.json
12
package.json
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user