amateras 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/ext/css/$CSSDeclaration.ts +12 -0
- package/ext/css/$CSSMediaRule.ts +13 -0
- package/ext/css/$CSSRule.ts +7 -0
- package/ext/css/$CSSStyleRule.ts +20 -0
- package/ext/css/css.ts +101 -0
- package/ext/html/html.ts +59 -0
- package/ext/html/node/$HTMLElement.ts +7 -0
- package/ext/html/node/type.ts +96 -0
- package/package.json +37 -0
- package/src/core.ts +84 -0
- package/src/global.ts +22 -0
- package/src/index.ts +3 -0
- package/src/lib/assign.ts +37 -0
- package/src/lib/assignHelper.ts +18 -0
- package/src/lib/randomId.ts +9 -0
- package/src/lib/sleep.ts +3 -0
- package/src/node/$Element.ts +59 -0
- package/src/node/$Node.ts +80 -0
- package/src/node/node.ts +11 -0
- package/src/structure/Signal.ts +40 -0
- package/tsconfig.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Amateras
|
|
2
|
+
Amateras is a DOM Utility library.
|
|
3
|
+
|
|
4
|
+
## Build DOM tree in JS
|
|
5
|
+
```ts
|
|
6
|
+
import 'amateras';
|
|
7
|
+
|
|
8
|
+
$(document.body).content([
|
|
9
|
+
$('h1').attr({ class: 'title' }).content('Hello, World')
|
|
10
|
+
])
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Style in JS
|
|
14
|
+
```ts
|
|
15
|
+
import 'amateras';
|
|
16
|
+
import 'amateras/css';
|
|
17
|
+
|
|
18
|
+
const paragraphStyle = $.css({
|
|
19
|
+
border: '2px solid black',
|
|
20
|
+
padding: '0.4rem',
|
|
21
|
+
textAlign: 'center'
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
$('p').css(paragraphStyle).content([
|
|
25
|
+
'Amateras is a ',
|
|
26
|
+
$('span').css({ color: 'blue', fontWeight: 700 }).content('DOM Utility Library.')
|
|
27
|
+
])
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## DOM Operating
|
|
31
|
+
```ts
|
|
32
|
+
import 'amateras';
|
|
33
|
+
|
|
34
|
+
const count$ = $.signal(0);
|
|
35
|
+
|
|
36
|
+
$(document.body).content([
|
|
37
|
+
$('button').content('Click me')
|
|
38
|
+
.class('class1', 'class2')
|
|
39
|
+
.on('click', () => count$(oldValue => oldValue + 1)),
|
|
40
|
+
$('p').content($`Clicked ${count$} times.`)
|
|
41
|
+
])
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## State Management
|
|
45
|
+
```ts
|
|
46
|
+
import 'amateras'
|
|
47
|
+
|
|
48
|
+
const count$ = $.signal(0);
|
|
49
|
+
const doubleCount$ = $.compute(() => count() * 2);
|
|
50
|
+
|
|
51
|
+
setInterval(() => count$(oldValue => oldValue + 1), 1000);
|
|
52
|
+
|
|
53
|
+
$(document.body).content([
|
|
54
|
+
$('p').content($`Count: ${count$}`),
|
|
55
|
+
$('p').content($`Double: ${doubleCount$}`)
|
|
56
|
+
])
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Custom Components
|
|
60
|
+
```ts
|
|
61
|
+
import 'amateras';
|
|
62
|
+
import 'amateras/html';
|
|
63
|
+
|
|
64
|
+
function NameCard(name: string, avatarURL: string) {
|
|
65
|
+
return $('div')
|
|
66
|
+
.css({
|
|
67
|
+
borderRadius: '1rem',
|
|
68
|
+
background: '#1e1e1e',
|
|
69
|
+
display: 'flex',
|
|
70
|
+
overflow: 'hidden'
|
|
71
|
+
})
|
|
72
|
+
.content([
|
|
73
|
+
$('img').src(avatarURL),
|
|
74
|
+
$('div').css({ padding: '1rem' }).content([
|
|
75
|
+
$('h2').css({ color: '#e1e1e1' }).content(name)
|
|
76
|
+
])
|
|
77
|
+
])
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
$(document.body).content([
|
|
81
|
+
$(NameCard, 'The dancing man', 'https://media.tenor.com/x8v1oNUOmg4AAAAM/rickroll-roll.gif')
|
|
82
|
+
])
|
|
83
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export class $CSSDeclaration {
|
|
2
|
+
key: string;
|
|
3
|
+
value: string;
|
|
4
|
+
constructor(key: string, value: string) {
|
|
5
|
+
this.key = key;
|
|
6
|
+
this.value = value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
get property() {
|
|
10
|
+
return this.key.replaceAll(/([A-Z])/g, (_, s1) => `-${s1}`).toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { $CSSRule } from "#css/$CSSRule";
|
|
2
|
+
|
|
3
|
+
export class $CSSMediaRule extends $CSSRule {
|
|
4
|
+
condition: string;
|
|
5
|
+
constructor(condition: string) {
|
|
6
|
+
super();
|
|
7
|
+
this.condition = condition;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
toString() {
|
|
11
|
+
return `@media ${this.condition} { ${[...this.rules.values()].map(rule => rule.toString()).join('\n')} }`
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { $CSSDeclaration } from "#css/$CSSDeclaration";
|
|
2
|
+
import { $CSSRule } from "#css/$CSSRule";
|
|
3
|
+
|
|
4
|
+
export class $CSSStyleRule extends $CSSRule {
|
|
5
|
+
context: string[] = [];
|
|
6
|
+
declarations = new Map<string, $CSSDeclaration>();
|
|
7
|
+
className: string = '';
|
|
8
|
+
constructor(context: string[] = []) {
|
|
9
|
+
super();
|
|
10
|
+
this.context = context;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
toString() {
|
|
14
|
+
return `${this.selector} { ${[...this.declarations.values()].map(dec => `${dec.property}: ${dec.value};`).join('\n')} }`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get selector() {
|
|
18
|
+
return this.context.join(' ');
|
|
19
|
+
}
|
|
20
|
+
}
|
package/ext/css/css.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { $CSSDeclaration } from "#css/$CSSDeclaration";
|
|
2
|
+
import { $CSSMediaRule } from "#css/$CSSMediaRule";
|
|
3
|
+
import { $CSSRule } from "#css/$CSSRule";
|
|
4
|
+
import { $CSSStyleRule } from "#css/$CSSStyleRule";
|
|
5
|
+
import { randomId } from "#lib/randomId";
|
|
6
|
+
import { $Element } from "#node/$Element";
|
|
7
|
+
|
|
8
|
+
declare module '#core' {
|
|
9
|
+
export namespace $ {
|
|
10
|
+
export function css(options: $CSSOptions): $CSSStyleRule
|
|
11
|
+
export function CSS(options: $CSSSelector | $CSSMediaSelector): void
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare module '#node/$Element' {
|
|
16
|
+
export interface $Element {
|
|
17
|
+
css(...options: $CSSOptions[]): this;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
Object.defineProperty($, 'css', {
|
|
22
|
+
value: function (options: $CSSOptions) {
|
|
23
|
+
if (options instanceof $CSSRule) return options;
|
|
24
|
+
const className = $CSS.generateClassName();
|
|
25
|
+
const rule = $CSS.createStyleRule(options, [className]);
|
|
26
|
+
rule.className = className;
|
|
27
|
+
return $CSS.insertRule( rule );
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
Object.defineProperty($, 'CSS', {
|
|
32
|
+
value: function (options: $CSSSelector | $CSSMediaRule) {
|
|
33
|
+
return Object.entries(options).map(([selector, declarations]) => {
|
|
34
|
+
return $CSS.insertRule( $CSS.createRule(selector, declarations) );
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
Object.defineProperty($Element.prototype, 'css', {
|
|
40
|
+
value: function (this: $Element, ...options: $CSSOptions[]) {
|
|
41
|
+
options.forEach(options => {
|
|
42
|
+
const rule = $.css(options);
|
|
43
|
+
this.addClass(rule.context[0]?.slice(1) as string);
|
|
44
|
+
})
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
export namespace $CSS {
|
|
50
|
+
export const stylesheet = new CSSStyleSheet();
|
|
51
|
+
const generatedIds = new Set<string>();
|
|
52
|
+
document.adoptedStyleSheets.push($CSS.stylesheet);
|
|
53
|
+
export function generateClassName(): string {
|
|
54
|
+
const id = randomId();
|
|
55
|
+
if (generatedIds.has(id)) return generateClassName();
|
|
56
|
+
generatedIds.add(id);
|
|
57
|
+
return `.${id}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createStyleRule<T extends $CSSRule>(options: T, context?: string[]): T;
|
|
61
|
+
export function createStyleRule<T extends $CSSOptions>(options: T, context?: string[]): $CSSStyleRule;
|
|
62
|
+
export function createStyleRule<T extends $CSSOptions>(options: T, context: string[] = []) {
|
|
63
|
+
if (options instanceof $CSSRule) return options;
|
|
64
|
+
const rule = new $CSSStyleRule(context);
|
|
65
|
+
for (const [key, value] of Object.entries(options)) {
|
|
66
|
+
if (value === undefined) continue;
|
|
67
|
+
if (value instanceof Object) rule.rules.add( createRule(key, value, context) )
|
|
68
|
+
else {
|
|
69
|
+
const declaration = new $CSSDeclaration(key, value);
|
|
70
|
+
rule.declarations.set(declaration.key, declaration);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return rule;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createRule(selector: string, options: $CSSOptions, context: string[] = []) {
|
|
77
|
+
if (selector.startsWith('@media')) return createMediaRule(selector.replace('@media ', ''), options, context);
|
|
78
|
+
else return createStyleRule(options, [...context, selector],);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function createMediaRule(key: string, options: $CSSOptions, context: string[] = []) {
|
|
82
|
+
const rule = new $CSSMediaRule(key);
|
|
83
|
+
rule.rules.add(createStyleRule(options, context));
|
|
84
|
+
return rule;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function insertRule(rule: $CSSRule) {
|
|
88
|
+
if (rule instanceof $CSSStyleRule && !CSS.supports(`selector(${rule.selector})`)) return rule;
|
|
89
|
+
$CSS.stylesheet.insertRule(rule.toString(), $CSS.stylesheet.cssRules.length);
|
|
90
|
+
if (rule instanceof $CSSMediaRule) return;
|
|
91
|
+
rule.rules.forEach(rule => insertRule(rule))
|
|
92
|
+
return rule;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type $CSSOptions = $CSSDeclarations | $CSSSelector | $CSSRule | $CSSMediaSelector;
|
|
97
|
+
type $CSSDeclarations = { [key in keyof CSSStyleDeclaration]?: string | number }
|
|
98
|
+
type $CSSSelector = { [key: string & {}]: $CSSOptions }
|
|
99
|
+
type $CSSMediaSelector = { [key: `@${string}`]: $CSSOptions }
|
|
100
|
+
|
|
101
|
+
console.debug(document.adoptedStyleSheets[0])
|
package/ext/html/html.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import '#core';
|
|
2
|
+
import { $Node } from '#node/$Node';
|
|
3
|
+
import { $HTMLElement } from './node/$HTMLElement';
|
|
4
|
+
import './node/type';
|
|
5
|
+
import { assignHelper } from '#lib/assignHelper';
|
|
6
|
+
export * from './node/type';
|
|
7
|
+
export * from './node/$HTMLElement';
|
|
8
|
+
|
|
9
|
+
// create element classes
|
|
10
|
+
export const [
|
|
11
|
+
$Input,
|
|
12
|
+
$Anchor,
|
|
13
|
+
$Image,
|
|
14
|
+
$Canvas,
|
|
15
|
+
$Dialog,
|
|
16
|
+
$Form,
|
|
17
|
+
$Label,
|
|
18
|
+
$Media,
|
|
19
|
+
$Select,
|
|
20
|
+
$Option,
|
|
21
|
+
$OptGroup,
|
|
22
|
+
$TextArea,
|
|
23
|
+
] = [
|
|
24
|
+
$HTMLElementBuilder<HTMLInputElement>('input'),
|
|
25
|
+
$HTMLElementBuilder<HTMLAnchorElement>('a'),
|
|
26
|
+
$HTMLElementBuilder<HTMLImageElement>('img'),
|
|
27
|
+
$HTMLElementBuilder<HTMLCanvasElement>('canvas'),
|
|
28
|
+
$HTMLElementBuilder<HTMLDialogElement>('dialog'),
|
|
29
|
+
$HTMLElementBuilder<HTMLFormElement>('form'),
|
|
30
|
+
$HTMLElementBuilder<HTMLLabelElement>('label'),
|
|
31
|
+
$HTMLElementBuilder<HTMLMediaElement>('media'),
|
|
32
|
+
$HTMLElementBuilder<HTMLSelectElement>('select'),
|
|
33
|
+
$HTMLElementBuilder<HTMLOptionElement>('option'),
|
|
34
|
+
$HTMLElementBuilder<HTMLOptGroupElement>('optgroup'),
|
|
35
|
+
$HTMLElementBuilder<HTMLTextAreaElement>('textarea'),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
const targets: [object: Constructor<Node>, target: Constructor<$Node>, tagname?: string][] = [
|
|
39
|
+
[HTMLElement, $HTMLElement],
|
|
40
|
+
[HTMLInputElement, $Input, 'input'],
|
|
41
|
+
[HTMLAnchorElement, $Anchor, 'a'],
|
|
42
|
+
[HTMLImageElement, $Image, 'img'],
|
|
43
|
+
[HTMLCanvasElement, $Canvas, 'canvas'],
|
|
44
|
+
[HTMLDialogElement, $Dialog, 'dialog'],
|
|
45
|
+
[HTMLFormElement, $Form, 'form'],
|
|
46
|
+
[HTMLLabelElement, $Label, 'label'],
|
|
47
|
+
[HTMLMediaElement, $Media, 'media'],
|
|
48
|
+
[HTMLSelectElement, $Select, 'select'],
|
|
49
|
+
[HTMLOptionElement, $Option, 'option'],
|
|
50
|
+
[HTMLOptGroupElement, $OptGroup, 'optgroup'],
|
|
51
|
+
[HTMLTextAreaElement, $TextArea, 'textarea'],
|
|
52
|
+
];
|
|
53
|
+
assignHelper(targets);
|
|
54
|
+
|
|
55
|
+
function $HTMLElementBuilder<Ele extends HTMLElement>(tagName: string) {
|
|
56
|
+
return class extends $HTMLElement<Ele> {
|
|
57
|
+
constructor() { super(tagName) }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { $HTMLElement } from "./$HTMLElement";
|
|
2
|
+
import type { Signal } from "#structure/Signal";
|
|
3
|
+
|
|
4
|
+
type $Parameter<T> = T | undefined | Signal<T> | Signal<T | undefined>
|
|
5
|
+
|
|
6
|
+
declare module '#node/$Element' {
|
|
7
|
+
export interface $Element<Ele extends Element> {
|
|
8
|
+
/** {@link Element.attributes} */
|
|
9
|
+
readonly attributes: NamedNodeMap;
|
|
10
|
+
/** {@link Element.clientHeight} */
|
|
11
|
+
readonly clientHeight: number;
|
|
12
|
+
/** {@link Element.clientLeft} */
|
|
13
|
+
readonly clientLeft: number;
|
|
14
|
+
/** {@link Element.clientTop} */
|
|
15
|
+
readonly clientTop: number;
|
|
16
|
+
/** {@link Element.clientWidth} */
|
|
17
|
+
readonly clientWidth: number;
|
|
18
|
+
/** {@link Element.currentCSSZoom} */
|
|
19
|
+
readonly currentCSSZoom: number;
|
|
20
|
+
/** {@link Element.localName} */
|
|
21
|
+
readonly localName: string;
|
|
22
|
+
/** {@link Element.namespaceURI} */
|
|
23
|
+
readonly namespaceURI: string | null;
|
|
24
|
+
/** {@link Element.prefix} */
|
|
25
|
+
readonly prefix: string | null;
|
|
26
|
+
/** {@link Element.ownerDocument} */
|
|
27
|
+
readonly ownerDocument: Document;
|
|
28
|
+
/** {@link Element.scrollHeight} */
|
|
29
|
+
readonly scrollHeight: number;
|
|
30
|
+
/** {@link Element.scrollWidth} */
|
|
31
|
+
readonly scrollWidth: number;
|
|
32
|
+
/** {@link Element.shadowRoot} */
|
|
33
|
+
readonly shadowRoot: ShadowRoot | null;
|
|
34
|
+
/** {@link Element.tagName} */
|
|
35
|
+
readonly tagName: string;
|
|
36
|
+
|
|
37
|
+
/** {@link Element.classList} */
|
|
38
|
+
classList(): DOMTokenList;
|
|
39
|
+
classList(value: $Parameter<string>): this;
|
|
40
|
+
/** {@link Element.className} */
|
|
41
|
+
className(): string;
|
|
42
|
+
className(value: $Parameter<string>): this;
|
|
43
|
+
/** {@link Element.id} */
|
|
44
|
+
id(): string;
|
|
45
|
+
id(id: $Parameter<string>): this;
|
|
46
|
+
/** {@link Element.innerHTML} */
|
|
47
|
+
innerHTML(): string;
|
|
48
|
+
innerHTML(innerHTML: $Parameter<string>): this;
|
|
49
|
+
/** {@link Element.outerHTML} */
|
|
50
|
+
outerHTML(): string;
|
|
51
|
+
outerHTML(outerHTML: $Parameter<string>): this;
|
|
52
|
+
/** {@link Element.part} */
|
|
53
|
+
part(): DOMTokenList;
|
|
54
|
+
part(part: $Parameter<string>): this;
|
|
55
|
+
/** {@link Element.scrollLeft} */
|
|
56
|
+
scrollLeft(): number;
|
|
57
|
+
scrollLeft(scrollLeft: $Parameter<number>): this;
|
|
58
|
+
/** {@link Element.scrollTop} */
|
|
59
|
+
scrollTop(): number;
|
|
60
|
+
scrollTop(scrollTop: $Parameter<number>): this;
|
|
61
|
+
/** {@link Element.slot} */
|
|
62
|
+
slot(): string;
|
|
63
|
+
slot(slot: $Parameter<string>): this;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface $Input extends $HTMLElement<HTMLInputElement> {}
|
|
68
|
+
export interface $Anchor extends $HTMLElement<HTMLAnchorElement> {}
|
|
69
|
+
export interface $Image extends $HTMLElement<HTMLImageElement> {
|
|
70
|
+
src(src: string): this;
|
|
71
|
+
src(): string;
|
|
72
|
+
}
|
|
73
|
+
export interface $Canvas extends $HTMLElement<HTMLCanvasElement> {}
|
|
74
|
+
export interface $Dialog extends $HTMLElement<HTMLDialogElement> {}
|
|
75
|
+
export interface $Form extends $HTMLElement<HTMLFormElement> {}
|
|
76
|
+
export interface $Label extends $HTMLElement<HTMLLabelElement> {}
|
|
77
|
+
export interface $Media extends $HTMLElement<HTMLMediaElement> {}
|
|
78
|
+
export interface $Select extends $HTMLElement<HTMLSelectElement> {}
|
|
79
|
+
export interface $Option extends $HTMLElement<HTMLOptionElement> {}
|
|
80
|
+
export interface $OptGroup extends $HTMLElement<HTMLOptGroupElement> {}
|
|
81
|
+
export interface $TextArea extends $HTMLElement<HTMLTextAreaElement> {}
|
|
82
|
+
|
|
83
|
+
declare module '#core' {
|
|
84
|
+
export function $(tagname: 'input'): $Input
|
|
85
|
+
export function $(tagname: 'anchor'): $Anchor
|
|
86
|
+
export function $(tagname: 'img'): $Image
|
|
87
|
+
export function $(tagname: 'dialog'): $Dialog
|
|
88
|
+
export function $(tagname: 'form'): $Form
|
|
89
|
+
export function $(tagname: 'label'): $Label
|
|
90
|
+
export function $(tagname: 'media'): $Media
|
|
91
|
+
export function $(tagname: 'select'): $Select
|
|
92
|
+
export function $(tagname: 'option'): $Option
|
|
93
|
+
export function $(tagname: 'otpgroup'): $OptGroup
|
|
94
|
+
export function $(tagname: 'textarea'): $TextArea
|
|
95
|
+
export function $(tagname: string): $HTMLElement
|
|
96
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "amateras",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Amateras is a DOM Utility library.",
|
|
5
|
+
"module": "index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "defaultkavy",
|
|
9
|
+
"email": "defaultkavy@gmail.com",
|
|
10
|
+
"url": "https://github.com/defaultkavy"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/defaultkavy/amateras/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/defaultkavy/amateras",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"imports": {
|
|
18
|
+
"#core": "./src/core.ts",
|
|
19
|
+
"#node/*": "./src/node/*.ts",
|
|
20
|
+
"#lib/*": "./src/lib/*.ts",
|
|
21
|
+
"#structure/*": "./src/structure/*.ts",
|
|
22
|
+
"#html": "./ext/html/html.ts",
|
|
23
|
+
"#css": "./ext/css/css.ts",
|
|
24
|
+
"#css/*": "./ext/css/*.ts"
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./src/index.ts",
|
|
28
|
+
"./core": "./src/core.ts",
|
|
29
|
+
"./node": "./src/node/node.ts",
|
|
30
|
+
"./node/*": "./src/node/*.ts",
|
|
31
|
+
"./lib/*": "./src/lib/*.ts",
|
|
32
|
+
"./structure/*": "./src/structure/*.ts",
|
|
33
|
+
"./html": "./ext/html/html.ts",
|
|
34
|
+
"./css": "./ext/css/css.ts",
|
|
35
|
+
"./css/*": "./ext/css/*.ts"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/core.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import './global';
|
|
2
|
+
import { Signal } from "#structure/Signal";
|
|
3
|
+
import { $Element } from "#node/$Element";
|
|
4
|
+
import { $Node, type $NodeContentTypes } from '#node/$Node';
|
|
5
|
+
import '#node/node';
|
|
6
|
+
|
|
7
|
+
const tagNameMap: {[key: string]: Constructor<$Node>} = {}
|
|
8
|
+
export function $<K extends (...args: any[]) => $Node>(fn: K, ...args: Parameters<K>): ReturnType<K>;
|
|
9
|
+
export function $<K extends $NodeContentTypes | undefined | void, F extends () => K, P extends Parameters<F>>(fn: F, ...args: any[]): K;
|
|
10
|
+
export function $<K extends $Node, T extends Constructor<K>, P extends ConstructorParameters<T>>(construct: T, ...args: P): K;
|
|
11
|
+
export function $<K extends TemplateStringsArray>(string: K, ...values: any[]): $NodeContentTypes[];
|
|
12
|
+
export function $<K extends $Node>($node: K): K;
|
|
13
|
+
export function $<K extends Element>(element: K): $Element<K>;
|
|
14
|
+
export function $<K extends keyof HTMLElementTagNameMap>(tagname: K): $Element<HTMLElementTagNameMap[K]>
|
|
15
|
+
export function $(tagname: string): $Element<HTMLElement>
|
|
16
|
+
export function $(resolver: string | HTMLElement | $Node | Function | TemplateStringsArray, ...args: any[]) {
|
|
17
|
+
if (resolver instanceof $Node) return resolver;
|
|
18
|
+
if (typeof resolver === 'string' && tagNameMap[resolver]) return new tagNameMap[resolver]();
|
|
19
|
+
if (typeof resolver === 'function')
|
|
20
|
+
if (resolver.prototype?.constructor) return resolver.prototype.constructor(...args);
|
|
21
|
+
else return resolver(...args);
|
|
22
|
+
if (resolver instanceof Array) {
|
|
23
|
+
const iterate = args.values();
|
|
24
|
+
return resolver.map(str => [str ?? undefined, iterate.next().value]).flat().filter(item => item);
|
|
25
|
+
}
|
|
26
|
+
if (resolver instanceof Node && resolver.$ instanceof $Node) return resolver.$;
|
|
27
|
+
return new $Element(resolver);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export namespace $ {
|
|
31
|
+
type SignalProcess<T> = T extends Array<any> ? {} : T extends object ? { [key in keyof T as `${string & key}$`]: SignalFunction<T[key]> } : {};
|
|
32
|
+
export type SignalFunction<T> = ({(value: ((newValue: T) => T) | T): SignalFunction<T>, (): T}) & { signal: Signal<T> } & SignalProcess<T>;
|
|
33
|
+
export function signal<T>(value: T): SignalFunction<T>
|
|
34
|
+
export function signal<T>(value: T) {
|
|
35
|
+
const signal = new Signal<T>(value);
|
|
36
|
+
const signalFn = function (newValue?: T) {
|
|
37
|
+
if (!arguments.length) return signal.value();
|
|
38
|
+
if (newValue !== undefined) signal.value(newValue);
|
|
39
|
+
return signalFn;
|
|
40
|
+
}
|
|
41
|
+
Object.defineProperty(signalFn, 'signal', { value: signal });
|
|
42
|
+
if (value instanceof Object) {
|
|
43
|
+
for (const [key, val] of Object.entries(value)) {
|
|
44
|
+
const val$ = $.signal(val);
|
|
45
|
+
val$.signal.subscribe(newValue => { value[key as keyof typeof value] = newValue; signal.emit() });
|
|
46
|
+
Object.defineProperty(signalFn, `${key}$`, {value: val$});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return signalFn as unknown as SignalFunction<T>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type ComputeFunction<T> = ({(): T}) & { signal: Signal<T> };
|
|
53
|
+
export function compute<T>(process: () => T) {
|
|
54
|
+
let subscribed = false;
|
|
55
|
+
const signalFn: SignalFunction<any> = $.signal(null);
|
|
56
|
+
function computeFn() {
|
|
57
|
+
if (!subscribed) return signalFn(subscribe())();
|
|
58
|
+
else return signalFn(process())();
|
|
59
|
+
}
|
|
60
|
+
function subscribe () {
|
|
61
|
+
const signalHandler = (signal: Signal<any>) => {
|
|
62
|
+
signal.subscribe(() => signalFn(process()))
|
|
63
|
+
}
|
|
64
|
+
Signal.listeners.add(signalHandler);
|
|
65
|
+
const result = process();
|
|
66
|
+
Signal.listeners.delete(signalHandler);
|
|
67
|
+
subscribed = true;
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
Object.defineProperty(computeFn, 'signal', { value: signalFn.signal });
|
|
71
|
+
return computeFn as ComputeFunction<T>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function registerTagName(tagname: string, $node: Constructor<$Node>) {
|
|
75
|
+
tagNameMap[tagname] = $node;
|
|
76
|
+
return $;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function orArrayResolver<T>(item: OrArray<T>): T[] {
|
|
80
|
+
return item instanceof Array ? item : [item];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export type $ = typeof $;
|
|
84
|
+
globalThis.$ = $;
|
package/src/global.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { $Element } from '#node/$Element';
|
|
2
|
+
import type { $Node } from '#node/$Node';
|
|
3
|
+
import * as core from '#core';
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
export import $ = core.$;
|
|
7
|
+
type Nullish = null | undefined;
|
|
8
|
+
type OrArray<T> = T | T[];
|
|
9
|
+
type OrMatrix<T> = T | OrMatrix<T>[];
|
|
10
|
+
type OrPromise<T> = T | Promise<T>;
|
|
11
|
+
type OrNullish<T> = T | Nullish;
|
|
12
|
+
type Constructor<T> = { new (...args: any[]): T }
|
|
13
|
+
type Prettify<T> = {
|
|
14
|
+
[K in keyof T]: T[K];
|
|
15
|
+
} & {};
|
|
16
|
+
interface Node {
|
|
17
|
+
readonly $: $Node
|
|
18
|
+
}
|
|
19
|
+
interface Element {
|
|
20
|
+
readonly $: $Element
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Signal } from "../structure/Signal";
|
|
2
|
+
|
|
3
|
+
export function assign(target: any, {set, get, fn}: {
|
|
4
|
+
set?: string[],
|
|
5
|
+
get?: string[],
|
|
6
|
+
fn?: string[]
|
|
7
|
+
}) {
|
|
8
|
+
const filterAndMap = (type: 'get' | 'set' | 'fn', arr: string[] | undefined) => {
|
|
9
|
+
return arr?.map(prop => [type, prop]) ?? []
|
|
10
|
+
}
|
|
11
|
+
const list = [...filterAndMap('get', get), ...filterAndMap('set', set), ...filterAndMap('fn', fn)] as [string, string][];
|
|
12
|
+
for (const [type, prop] of list) {
|
|
13
|
+
Object.defineProperty(target.prototype, prop, {
|
|
14
|
+
...(type === 'get' ? {
|
|
15
|
+
get() { return this.node[prop as any] }
|
|
16
|
+
} : {
|
|
17
|
+
writable: true,
|
|
18
|
+
...(type === 'set' ? {
|
|
19
|
+
// set
|
|
20
|
+
value: function (this, args: any) {
|
|
21
|
+
if (!arguments.length) return this.node[prop];
|
|
22
|
+
const set = (value: any) => value !== undefined && (this.node[prop] = value);
|
|
23
|
+
if (args instanceof Signal) args = args.subscribe(set).value();
|
|
24
|
+
set(args)
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
} : {
|
|
28
|
+
// fn
|
|
29
|
+
value: function (this, ...args : any[]) {
|
|
30
|
+
return this.node[prop](...args) ?? this;
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
}),
|
|
34
|
+
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { $Node } from "#node/$Node";
|
|
2
|
+
import { assign } from "./assign";
|
|
3
|
+
|
|
4
|
+
export function assignHelper(targets: [object: Constructor<Node>, target: Constructor<$Node>, tagname?: string][]) {
|
|
5
|
+
for (const [object, target, tagname] of targets) {
|
|
6
|
+
const {set, get, fn} = { set: [] as string[], get: [] as string[], fn: [] as string[] }
|
|
7
|
+
// assign native object properties to target
|
|
8
|
+
for (const [prop, value] of Object.entries(Object.getOwnPropertyDescriptors(object.prototype))) {
|
|
9
|
+
if (prop in target.prototype) continue;
|
|
10
|
+
if (value.get && !value.set) get.push(prop);
|
|
11
|
+
else if (value.value) fn.push(prop);
|
|
12
|
+
else if (value.get && value.set) set.push(prop);
|
|
13
|
+
}
|
|
14
|
+
assign(target, {set, get, fn})
|
|
15
|
+
// register tagname
|
|
16
|
+
if (tagname) $.registerTagName(tagname, target)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const LOWER = 'abcdefghijklmnopqrstuvwxyz';
|
|
2
|
+
const UPPER = LOWER.toUpperCase();
|
|
3
|
+
export function randomId(options?: {length?: number, case?: 'any' | 'lower' | 'upper'}): string {
|
|
4
|
+
options = {length: 5, case: 'any', ...options};
|
|
5
|
+
const char = options.case === 'any' ? LOWER + UPPER : options.case === 'lower' ? LOWER : UPPER;
|
|
6
|
+
return Array.from({length: options.length as number}, (_, i) => {
|
|
7
|
+
const rand = Math.round(Math.random() * char.length); return char[rand]
|
|
8
|
+
}).join('');
|
|
9
|
+
}
|
package/src/lib/sleep.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Signal } from "#structure/Signal";
|
|
2
|
+
import { $Node } from "#node/$Node";
|
|
3
|
+
|
|
4
|
+
export class $Element<Ele extends Element = Element> extends $Node {
|
|
5
|
+
listeners = new Map<Function, (event: Event) => void>;
|
|
6
|
+
declare node: Ele
|
|
7
|
+
constructor(resolver: Ele | string) {
|
|
8
|
+
super(resolver instanceof Element ? resolver : document.createElement(resolver) as unknown as Ele)
|
|
9
|
+
//@ts-expect-error
|
|
10
|
+
this.node.$ = this;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
attr(): {[key: string]: string};
|
|
14
|
+
attr(obj: {[key: string]: string | number | boolean | Signal<any>}): this;
|
|
15
|
+
attr(obj?: {[key: string]: string | number | boolean | Signal<any>}) {
|
|
16
|
+
if (!arguments.length) return Object.fromEntries(Array.from(this.node.attributes).map(attr => [attr.name, attr.value]));
|
|
17
|
+
if (obj) for (let [key, value] of Object.entries(obj)) {
|
|
18
|
+
const set = (value: any) => value !== undefined && this.node.setAttribute(key, `${value}`)
|
|
19
|
+
if (value instanceof Signal) value = value.subscribe(set).value();
|
|
20
|
+
set(value);
|
|
21
|
+
}
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class(...token: string[]) {
|
|
26
|
+
this.node.classList = token.join(' ');
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
addClass(...token: string[]) {
|
|
31
|
+
this.node.classList.add(...token);
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
removeClass(...token: string[]) {
|
|
36
|
+
this.node.classList.remove(...token);
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
use(callback: ($ele: this) => void) {
|
|
41
|
+
callback(this);
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
on<K extends keyof HTMLElementEventMap>(type: K, listener: ($node: this, event: Event) => void, options?: boolean | AddEventListenerOptions) {
|
|
46
|
+
const handler = (event: Event) => listener(this, event);
|
|
47
|
+
this.listeners.set(listener, handler);
|
|
48
|
+
this.node.addEventListener(type, handler, options);
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
off<K extends keyof HTMLElementEventMap>(type: K, listener: ($node: this, event: Event) => void, options?: boolean | EventListenerOptions) {
|
|
53
|
+
const handler = this.listeners.get(listener);
|
|
54
|
+
if (handler) this.node.removeEventListener(type, handler, options);
|
|
55
|
+
this.listeners.delete(listener);
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
once() {}
|
|
59
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Signal } from "#structure/Signal";
|
|
2
|
+
|
|
3
|
+
export class $Node {
|
|
4
|
+
node: Node;
|
|
5
|
+
constructor(node: Node) {
|
|
6
|
+
this.node = node;
|
|
7
|
+
//@ts-expect-error
|
|
8
|
+
this.node.$ = this;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
content(children: $NodeContentResolver<this>) {
|
|
12
|
+
this.node.childNodes.forEach(node => node.remove());
|
|
13
|
+
return this.insert(children);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
insert(resolver: $NodeContentResolver<this>, position = -1) {
|
|
17
|
+
if (resolver instanceof Function) {
|
|
18
|
+
const content = resolver(this);
|
|
19
|
+
if (content instanceof Promise) content.then(content => $Node.insertChild(this.node, content, position))
|
|
20
|
+
else $Node.insertChild(this.node, content, position);
|
|
21
|
+
} else $Node.insertChild(this.node, resolver, position);
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
await<T>(promise: Promise<T>, callback: ($node: this, result: T) => void): this {
|
|
26
|
+
promise.then(result => callback(this, result));
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
replace($node: $NodeContentTypes | undefined | null) {
|
|
31
|
+
if (!$node) return this;
|
|
32
|
+
this.node.parentNode?.replaceChild($Node.processContent($node), this.node);
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static insertChild(node: Node, children: OrArray<OrPromise<$NodeContentTypes>>, position = -1) {
|
|
37
|
+
children = $.orArrayResolver(children);
|
|
38
|
+
// get child node at position
|
|
39
|
+
const positionChild = Array.from(node.childNodes).filter(node => node.nodeType !== node.TEXT_NODE).at(position);
|
|
40
|
+
// insert node helper function for depend position
|
|
41
|
+
const append = (child: Node | undefined | null) => {
|
|
42
|
+
if (!child) return;
|
|
43
|
+
if (!positionChild) node.appendChild(child);
|
|
44
|
+
else node.insertBefore(child, position < 0 ? positionChild.nextSibling : positionChild);
|
|
45
|
+
}
|
|
46
|
+
// process nodes
|
|
47
|
+
for (const child of children) if (child !== undefined) append($Node.processContent(child))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static processContent(content: $NodeContentTypes): Node;
|
|
51
|
+
static processContent(content: undefined | null): undefined | null;
|
|
52
|
+
static processContent(content: OrPromise<undefined | null>): undefined | null;
|
|
53
|
+
static processContent(content: OrPromise<$NodeContentTypes>): Node;
|
|
54
|
+
static processContent(content: OrPromise<$NodeContentTypes | undefined | null>): Node | undefined | null
|
|
55
|
+
static processContent(content: OrPromise<$NodeContentTypes | undefined | null>) {
|
|
56
|
+
if (content === undefined) return undefined;
|
|
57
|
+
if (content === null) return null;
|
|
58
|
+
// is $Element
|
|
59
|
+
if (content instanceof $Node) return content.node;
|
|
60
|
+
// is Promise
|
|
61
|
+
if (content instanceof Promise) {
|
|
62
|
+
const async = $('async').await(content, ($async, $child) => $async.replace($child));
|
|
63
|
+
return async.node;
|
|
64
|
+
}
|
|
65
|
+
// is SignalFunction
|
|
66
|
+
if (content instanceof Function && (content as $.SignalFunction<any>).signal instanceof Signal) {
|
|
67
|
+
const text = new Text();
|
|
68
|
+
const set = (value: any) => text.textContent = value instanceof Object ? JSON.stringify(value) : value;
|
|
69
|
+
(content as $.SignalFunction<any>).signal.subscribe(set);
|
|
70
|
+
set((content as $.SignalFunction<any>)());
|
|
71
|
+
return text;
|
|
72
|
+
}
|
|
73
|
+
// is string | number | boolean
|
|
74
|
+
return new Text(`${content}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type $NodeContentTypes = $Node | string | number | boolean | $.SignalFunction<any>;
|
|
79
|
+
export type $NodeContentHandler<T extends $Node> = ($node: T) => OrPromise<OrArray<$NodeContentTypes>>;
|
|
80
|
+
export type $NodeContentResolver<T extends $Node> = $NodeContentHandler<T> | OrArray<OrPromise<$NodeContentTypes>>
|
package/src/node/node.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export class Signal<T> {
|
|
2
|
+
#value: T;
|
|
3
|
+
subscribers = new Set<(value: T) => void>();
|
|
4
|
+
static listeners = new Set<(signal: Signal<any>) => void>();
|
|
5
|
+
constructor(value: T) {
|
|
6
|
+
this.#value = value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
value(): T;
|
|
10
|
+
value(newValue: T): this;
|
|
11
|
+
value(callback: (oldValue: T) => T): this;
|
|
12
|
+
value(resolver?: T | ((oldValue: T) => T)) {
|
|
13
|
+
if (!arguments.length) {
|
|
14
|
+
Signal.listeners.forEach(fn => fn(this));
|
|
15
|
+
return this.#value;
|
|
16
|
+
}
|
|
17
|
+
if (resolver instanceof Function) this.value(resolver(this.#value));
|
|
18
|
+
else if (resolver !== undefined) {
|
|
19
|
+
this.#value = resolver;
|
|
20
|
+
this.emit();
|
|
21
|
+
}
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
emit() {
|
|
26
|
+
this.subscribers.forEach(subs => subs(this.#value))
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
subscribe(callback: (value: T) => void) {
|
|
31
|
+
this.subscribers.add(callback);
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
unsubscribe(callback: (value: T) => void) {
|
|
36
|
+
this.subscribers.delete(callback);
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext", "DOM"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
|
|
23
|
+
// Some stricter flags (disabled by default)
|
|
24
|
+
"noUnusedLocals": false,
|
|
25
|
+
"noUnusedParameters": false,
|
|
26
|
+
"noPropertyAccessFromIndexSignature": false
|
|
27
|
+
},
|
|
28
|
+
"exclude": ["dist"]
|
|
29
|
+
}
|