tagu-tagu 1.0.0
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 +35 -0
- package/dist/bundle.min.js +1 -0
- package/package.json +35 -0
- package/src/Elements.ts +169 -0
- package/src/Modify.ts +219 -0
- package/src/State.ts +38 -0
- package/src/Tag.ts +34 -0
- package/src/data/data.ts +160 -0
- package/src/data/useBinding.ts +10 -0
- package/src/flow/ControlFlow.ts +6 -0
- package/src/flow/For.ts +82 -0
- package/src/flow/If.ts +61 -0
- package/src/flow/Switch.ts +76 -0
- package/src/flow/SwitchBlockState.ts +31 -0
- package/src/index.ts +8 -0
- package/src/initializeChildBlock.ts +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# tag-tag
|
|
2
|
+
|
|
3
|
+
A lightweight helper for vanilla `HTMLElement`. No config, no jsx — only clean javascript.
|
|
4
|
+
|
|
5
|
+
## `tag-tag` is
|
|
6
|
+
|
|
7
|
+
just a helper for `HTMLElement`:
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
import {button} from "tag-tag";
|
|
11
|
+
|
|
12
|
+
const myButton = button("Click Me!"); // `HTMLButtonElement`
|
|
13
|
+
document.body.appendChild(myButton);
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
with reactivity!
|
|
17
|
+
|
|
18
|
+
```javascript
|
|
19
|
+
import {button, span, Modify, useState} from "tag-tag";
|
|
20
|
+
|
|
21
|
+
const count = useState(4);
|
|
22
|
+
|
|
23
|
+
function decrementCount() {
|
|
24
|
+
count.set(count.get() - 1);
|
|
25
|
+
}
|
|
26
|
+
function incrementCount() {
|
|
27
|
+
count.set(count.get() + 1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Modify(document.body, [
|
|
31
|
+
button("-", { on: { click: decrementCount } }),// `HTMLButtonElement`
|
|
32
|
+
span(count),// `HTMLSpanElement`
|
|
33
|
+
button("+", { on: { click: incrementCount } }),// `HTMLButtonElement`
|
|
34
|
+
]);
|
|
35
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var S=class{node2Data=new WeakMap;addCallbacks(t,n){if(!n)return;this.node2DescendantCallbacks.has(t)||this.node2DescendantCallbacks.set(t,{});let o=this.node2DescendantCallbacks.get(t);k(o,n);let i=this.node2Data.get(t);w(o,i)}setDataRecord(t,n){n&&this.node2Data.set(t,n)}resolveCallbacks(t,n){let o=(l,u)=>{if(w(u,this.node2Data.get(l)),!!Object.keys(u).length){if(!l.parentElement){this.node2DescendantCallbacks.has(l)||this.node2DescendantCallbacks.set(l,{});let m=this.node2DescendantCallbacks.get(l);m&&k(m,u);return}o(l.parentElement,u)}},i=this.node2DescendantCallbacks.get(n);i&&o(t,i)}node2DescendantCallbacks=new WeakMap};function w(e,t){for(let n in t)if(n in e){for(let o of e[n])o(t[n]);delete e[n]}}var c=new S;function b(e,t){c.addCallbacks(e,$(t)),c.setDataRecord(e,A(t))}function $(e){return H(e,t=>typeof t=="function",t=>[t])}function A(e){return H(e,t=>typeof t!="function")}function H(e,t,n=o=>o){if(!e)return;let o={};for(let i in e){let l=e[i];t(l)&&(o[i]=n(l))}if(Object.keys(o).length)return o}function k(e,t){for(let n in t)e[n]||(e[n]=[]),e[n].push(...t[n])}var x=class{constructor(t,n){this.key=t;this.map=n}};var d=class{next=null;firstNode=null};var p=class{#e;constructor(t){this.#e=t}get=()=>this.#e;set(t){this.#e=t,this.#n("change")}#n(t){this.#t.dispatchEvent(new Event(t))}#t=new EventTarget;on(t,n){this.#t.addEventListener(t,n)}};function D(e){return new p(e)}function R(e,t){let n=new p(t()),o=()=>{n.set(t())};for(let i of e)i.on("change",o);return n}function z(e,t){let n=V(t);j(n);for(let o of n)O(e,o)}function O(e,t){t instanceof d?t.run(e):(c.resolveCallbacks(e,t),e.appendChild(t))}function V(e){return e.map(t=>{if(typeof t=="string"||t instanceof p){let n=document.createTextNode("");return y(t,o=>{n.textContent=o}),n}return t})}function j(e){for(let t=0;t<e.length;t++){let n=e[t];n instanceof d&&(n.next=e[t+1]??null)}}function B(e){let t=e.next;return t===null?null:t instanceof Node?t:t.firstNode?t.firstNode:B(t)}function T(e){let t=B(e);return t?.parentElement?t:null}function y(e,t){if(typeof e=="string")t(e);else{let n=()=>{t(e.get())};n(),e.on("change",n)}}function g(e,t,n){t instanceof x?b(e,{[t.key]:o=>{let l=o instanceof p?R([o],()=>t.map(o.get())):t.map(o);y(l,n)}}):y(t,n)}function K(e,t){t!==void 0&&g(e,t,n=>{e.innerHTML=n})}function F(e,t){t!==void 0&&g(e,t,n=>{e.textContent=n})}function q(e,t){let n=e.style;if(n instanceof CSSStyleDeclaration)for(let o in t){let i=t[o];g(e,i,l=>n.setProperty(o,l))}}function P(e,t){for(let n in t){let o=t[n];g(e,o,i=>{i?e.setAttribute(n,i):e.removeAttribute(n)})}}function G(e,t){for(let n in t){let o=t[n];g(e,o,i=>{e[n]=i})}}function U(e,t){for(let n in t){let o=e.querySelector(n);o&&C(o,t[n])}}function W(e,t){for(let n in t){let o=e.querySelectorAll(n);for(let i of o)C(i,t[n])}}function Q(e,t){for(let n in t){let i=t[n];i&&(typeof i=="function"?e.addEventListener(n,i):e.addEventListener(n,i.listener,i.options))}}function C(e,t){e&&(typeof t=="string"||t instanceof p?F(e,t):Array.isArray(t)?z(e,t):typeof t=="function"?t(e):(K(e,t.html),F(e,t.text),P(e,t.attr),G(e,t.prop),q(e,t.css),U(e,t.$),W(e,t.$$),Q(e,t.on),b(e,t.data)))}function h(e,...t){let n=typeof e=="string"?document.querySelector(e):e;for(let o of t)C(n,o);return n}function ue(e,...t){let n=document.createElementNS("http://www.w3.org/2000/svg",e);return h(n,...t)}function r(e,...t){let n=document.createElement(e);return h(n,...t),n}function fe(e,...t){let n=L({html:e}).children[0];return h(n,...t)}function Ee(...e){return r("h1",...e)}function Te(...e){return r("h2",...e)}function ge(...e){return r("h3",...e)}function xe(...e){return r("h4",...e)}function ye(...e){return r("h5",...e)}function he(...e){return r("h6",...e)}function ve(...e){return r("p",...e)}function Se(...e){return r("section",...e)}function be(...e){return r("button",...e)}function Ce(...e){return r("span",...e)}function Le(e){function t(n,o){return`${n} {${Object.keys(o).map(i=>`${i}: ${o[i]};`).join("")}}`}return r("style",[Object.keys(e).map(n=>t(n,e[n])).join("")])}function L(...e){return r("div",...e)}function Me(...e){return L({css:{display:"flex"}},...e)}function Ne(...e){return r("input",...e)}function Ie(...e){return r("textarea",...e)}function we(...e){return r("select",...e)}function ke(...e){return r("option",...e)}function He(...e){return r("br",...e)}function De(...e){return r("tr",...e)}function Re(...e){return r("td",...e)}function ze(...e){return r("b",...e)}function Be(...e){return r("label",...e)}function Fe(...e){return r("a",...e)}function $e(...e){return r("blockquote",...e)}function Ae(...e){return r("li",...e)}function Oe(...e){return r("ol",...e)}function Ve(...e){return r("ul",...e)}function je(...e){return r("audio",...e)}function Ke(...e){return r("video",...e)}function qe(...e){return r("img",...e)}function Pe(...e){return r("canvas",...e)}function Ge(...e){return r("iframe",...e)}function Ue(...e){return r("form",...e)}function We(...e){return r("table",...e)}function Qe(...e){return r("tbody",...e)}function Je(...e){return r("hr",...e)}function et(e,t){return new M(e,t)}var M=class extends d{constructor(n,o){super();this.list=n;this.map=o}run(n){let o=new Map,i=new Map,l=()=>{let u=[];for(let a of this.list.get())o.has(a)||u.push(a);let m=[],s=new Set(this.list.get());for(let a of i.keys()){let f=i.get(a);f&&!s.has(f)&&m.push(a)}for(let a of u){let f=this.map(a),v=typeof f=="string"?document.createTextNode(f):f;c.resolveCallbacks(n,v),i.set(v,a),o.set(a,v)}for(let a of m){a.parentNode?.removeChild(a);let f=i.get(a);i.delete(a),f&&o.delete(f)}for(let a of[...i.keys()])a.parentElement?.removeChild(a);let E=T(this);for(let a of this.list.get())n.insertBefore(o.get(a),E);this.firstNode=o.get(this.list.get()[0])??null};l(),this.list.on("change",()=>{l()})}};function rt(e,t,n){return new N(e,t,n)}var N=class extends d{#e;#n;#t;constructor(t,n,o){super(),this.#e=t,this.#n=n,this.#t=o}run(t){let n,o,i=()=>{let l=T(this);this.#e.get()?(n||(n=this.#n()),c.resolveCallbacks(t,n),this.firstNode=n,o?.remove(),t.insertBefore(n,l)):(o||(o=this.#t?.()),o&&c.resolveCallbacks(t,o),this.firstNode=o??null,n?.remove(),o&&t.insertBefore(o,l))};i(),this.#e.on("change",i)}};function dt(e,t,n){return new I(e,t,n)}var I=class extends d{#e;#n;#t;constructor(t,n,o){super(),this.#e=t,this.#n=n,this.#t=o}run(t){let n=new Map,o=new Map;for(let s of this.#n)o.set(s.case,s);let i,l,u=s=>{let E=o.get(s);if(E){if(!n.has(s)){let a=E.show();n.set(s,a)}return n.get(s)}return this.#t&&!l&&(l=this.#t()),l},m=()=>{let s=this.#e.get(),E=T(this),a=u(s);a&&c.resolveCallbacks(t,a),i?.remove(),a&&t.insertBefore(a,E),i=a};m(),this.#e.on("change",m)}};function pt(e,t){let n=D(void 0),o=()=>{let i;for(let l of t)e.get()===l.case&&(i=l);n.set(i)};return e.on("change",o),o(),n}export{Me as FlexDiv,et as For,M as ForMap,R as FromStates,r as Html,rt as If,N as IfFlow,h as Modify,p as State,ue as Svg,dt as Switch,pt as SwitchBlockState,I as SwitchFlow,fe as Tag,Fe as a,y as applyStringOrState,je as audio,ze as b,$e as blockquote,He as br,be as button,Pe as canvas,L as div,Ue as form,Ee as h1,Te as h2,ge as h3,xe as h4,ye as h5,he as h6,Je as hr,Ge as iframe,qe as img,Ne as input,Be as label,Ae as li,Oe as ol,ke as option,ve as p,Se as section,we as select,Ce as span,Le as style,We as table,Qe as tbody,Re as td,Ie as textarea,De as tr,Ve as ul,D as useState,Ke as video};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tagu-tagu",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A lightweight helper for vanilla `HTMLElement`.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"vanilla"
|
|
7
|
+
],
|
|
8
|
+
"homepage": "https://github.com/DoTheSimplest/tag-tag#readme",
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/DoTheSimplest/tag-tag/issues"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/DoTheSimplest/tag-tag.git"
|
|
15
|
+
},
|
|
16
|
+
"author": "DoTheSimmplest",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "src/index.ts",
|
|
19
|
+
"files": [
|
|
20
|
+
"src",
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test:browser": "vitest",
|
|
25
|
+
"build": "tsx scripts/build.ts"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@biomejs/biome": "2.3.8",
|
|
29
|
+
"@vitest/browser-playwright": "^4.0.15",
|
|
30
|
+
"esbuild": "^0.27.2",
|
|
31
|
+
"tsx": "^4.21.0",
|
|
32
|
+
"typescript": "^5.9.3",
|
|
33
|
+
"vitest": "^4.0.15"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/Elements.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { ElementInitializer } from "./Modify";
|
|
2
|
+
import { Html } from "./Tag";
|
|
3
|
+
|
|
4
|
+
export function h1(...initializers: ElementInitializer<HTMLHeadingElement>[]) {
|
|
5
|
+
return Html("h1", ...initializers);
|
|
6
|
+
}
|
|
7
|
+
export function h2(...initializers: ElementInitializer<HTMLHeadingElement>[]) {
|
|
8
|
+
return Html("h2", ...initializers);
|
|
9
|
+
}
|
|
10
|
+
export function h3(...initializers: ElementInitializer<HTMLHeadingElement>[]) {
|
|
11
|
+
return Html("h3", ...initializers);
|
|
12
|
+
}
|
|
13
|
+
export function h4(...initializers: ElementInitializer<HTMLHeadingElement>[]) {
|
|
14
|
+
return Html("h4", ...initializers);
|
|
15
|
+
}
|
|
16
|
+
export function h5(...initializers: ElementInitializer<HTMLHeadingElement>[]) {
|
|
17
|
+
return Html("h5", ...initializers);
|
|
18
|
+
}
|
|
19
|
+
export function h6(...initializers: ElementInitializer<HTMLHeadingElement>[]) {
|
|
20
|
+
return Html("h6", ...initializers);
|
|
21
|
+
}
|
|
22
|
+
export function p(...initializers: ElementInitializer<HTMLParagraphElement>[]) {
|
|
23
|
+
return Html("p", ...initializers);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function section(...initializers: ElementInitializer<HTMLElement>[]) {
|
|
27
|
+
return Html("section", ...initializers);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function button(
|
|
31
|
+
...initializers: ElementInitializer<HTMLButtonElement>[]
|
|
32
|
+
) {
|
|
33
|
+
return Html("button", ...initializers);
|
|
34
|
+
}
|
|
35
|
+
export function span(...initializers: ElementInitializer<HTMLSpanElement>[]) {
|
|
36
|
+
return Html("span", ...initializers);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function style(styles: Record<string, Record<string, string>>) {
|
|
40
|
+
function createStyleText(
|
|
41
|
+
selector: string,
|
|
42
|
+
properties: Record<string, string>,
|
|
43
|
+
) {
|
|
44
|
+
return `${selector} {${Object.keys(properties)
|
|
45
|
+
.map((property) => `${property}: ${properties[property]};`)
|
|
46
|
+
.join("")}}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return Html("style", [
|
|
50
|
+
Object.keys(styles)
|
|
51
|
+
.map((selector) => createStyleText(selector, styles[selector]))
|
|
52
|
+
.join(""),
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function div(...initializers: ElementInitializer<HTMLDivElement>[]) {
|
|
57
|
+
return Html("div", ...initializers);
|
|
58
|
+
}
|
|
59
|
+
export function FlexDiv(...initializers: ElementInitializer<HTMLDivElement>[]) {
|
|
60
|
+
return div({ css: { display: "flex" } }, ...initializers);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function input(...initializers: ElementInitializer<HTMLInputElement>[]) {
|
|
64
|
+
return Html("input", ...initializers);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function textarea(
|
|
68
|
+
...initializers: ElementInitializer<HTMLTextAreaElement>[]
|
|
69
|
+
) {
|
|
70
|
+
return Html("textarea", ...initializers);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function select(
|
|
74
|
+
...initializers: ElementInitializer<HTMLSelectElement>[]
|
|
75
|
+
) {
|
|
76
|
+
return Html("select", ...initializers);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function option(
|
|
80
|
+
...initializers: ElementInitializer<HTMLOptionElement>[]
|
|
81
|
+
) {
|
|
82
|
+
return Html("option", ...initializers);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function br(...initializers: ElementInitializer<HTMLBRElement>[]) {
|
|
86
|
+
return Html("br", ...initializers);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function tr(...initializers: ElementInitializer<HTMLTableRowElement>[]) {
|
|
90
|
+
return Html("tr", ...initializers);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function td(
|
|
94
|
+
...initializers: ElementInitializer<HTMLTableCellElement>[]
|
|
95
|
+
) {
|
|
96
|
+
return Html("td", ...initializers);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function b(...initializers: ElementInitializer<HTMLElement>[]) {
|
|
100
|
+
return Html("b", ...initializers);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function label(...initializers: ElementInitializer<HTMLLabelElement>[]) {
|
|
104
|
+
return Html("label", ...initializers);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function a(...initializers: ElementInitializer<HTMLAnchorElement>[]) {
|
|
108
|
+
return Html("a", ...initializers);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function blockquote(
|
|
112
|
+
...initializers: ElementInitializer<HTMLQuoteElement>[]
|
|
113
|
+
) {
|
|
114
|
+
return Html("blockquote", ...initializers);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function li(...initializers: ElementInitializer<HTMLLIElement>[]) {
|
|
118
|
+
return Html("li", ...initializers);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function ol(...initializers: ElementInitializer<HTMLOListElement>[]) {
|
|
122
|
+
return Html("ol", ...initializers);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function ul(...initializers: ElementInitializer<HTMLUListElement>[]) {
|
|
126
|
+
return Html("ul", ...initializers);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function audio(...initializers: ElementInitializer<HTMLAudioElement>[]) {
|
|
130
|
+
return Html("audio", ...initializers);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function video(...initializers: ElementInitializer<HTMLVideoElement>[]) {
|
|
134
|
+
return Html("video", ...initializers);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function img(...initializers: ElementInitializer<HTMLImageElement>[]) {
|
|
138
|
+
return Html("img", ...initializers);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function canvas(
|
|
142
|
+
...initializers: ElementInitializer<HTMLCanvasElement>[]
|
|
143
|
+
) {
|
|
144
|
+
return Html("canvas", ...initializers);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function iframe(
|
|
148
|
+
...initializers: ElementInitializer<HTMLIFrameElement>[]
|
|
149
|
+
) {
|
|
150
|
+
return Html("iframe", ...initializers);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function form(...initializers: ElementInitializer<HTMLFormElement>[]) {
|
|
154
|
+
return Html("form", ...initializers);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function table(...initializers: ElementInitializer<HTMLTableElement>[]) {
|
|
158
|
+
return Html("table", ...initializers);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function tbody(
|
|
162
|
+
...initializers: ElementInitializer<HTMLTableSectionElement>[]
|
|
163
|
+
) {
|
|
164
|
+
return Html("tbody", ...initializers);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function hr(...initializers: ElementInitializer<HTMLHRElement>[]) {
|
|
168
|
+
return Html("hr", ...initializers);
|
|
169
|
+
}
|
package/src/Modify.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { type DataRecord, initializeData } from "./data/data";
|
|
2
|
+
import { Binding } from "./data/useBinding";
|
|
3
|
+
import { type ChildType, initializeChildBlock } from "./initializeChildBlock";
|
|
4
|
+
import { FromStates, State } from "./State";
|
|
5
|
+
|
|
6
|
+
type EventListenerType<
|
|
7
|
+
TEventType2Event,
|
|
8
|
+
TEventType extends keyof TEventType2Event,
|
|
9
|
+
> = (event: TEventType2Event[TEventType]) => any;
|
|
10
|
+
|
|
11
|
+
type EventListenerRecord<TEventType2Event> = {
|
|
12
|
+
[TEventType in keyof TEventType2Event]?:
|
|
13
|
+
| EventListenerType<TEventType2Event, TEventType>
|
|
14
|
+
| {
|
|
15
|
+
listener: EventListenerType<TEventType2Event, TEventType>;
|
|
16
|
+
options: boolean | AddEventListenerOptions;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type $Record = Record<
|
|
21
|
+
string,
|
|
22
|
+
ElementInitializer<Element, HTMLElementEventMap | SVGElementEventMap>
|
|
23
|
+
>;
|
|
24
|
+
|
|
25
|
+
type ElementPropertyInitializer<TEventType2Event> = {
|
|
26
|
+
html?: string | State | Binding;
|
|
27
|
+
text?: string | State | Binding;
|
|
28
|
+
attr?: Record<string, string | State | Binding>;
|
|
29
|
+
prop?: Record<string, any | State | Binding>;
|
|
30
|
+
css?: Record<string, string | State | Binding>;
|
|
31
|
+
on?: EventListenerRecord<TEventType2Event>;
|
|
32
|
+
$?: $Record;
|
|
33
|
+
$$?: $Record;
|
|
34
|
+
data?: DataRecord;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type ElementInitializer<
|
|
38
|
+
TElement,
|
|
39
|
+
TEventType2Event = HTMLElementEventMap,
|
|
40
|
+
> =
|
|
41
|
+
| string
|
|
42
|
+
| State
|
|
43
|
+
| ElementPropertyInitializer<TEventType2Event>
|
|
44
|
+
| ChildType[]
|
|
45
|
+
| ((element: TElement) => void);
|
|
46
|
+
|
|
47
|
+
export function applyStringOrState(
|
|
48
|
+
value: string | State,
|
|
49
|
+
initialize: (text: string) => void,
|
|
50
|
+
) {
|
|
51
|
+
if (typeof value === "string") {
|
|
52
|
+
initialize(value);
|
|
53
|
+
} else {
|
|
54
|
+
const update = () => {
|
|
55
|
+
initialize(value.get());
|
|
56
|
+
};
|
|
57
|
+
update();
|
|
58
|
+
value.on("change", update);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function applyStringOrStateOrBinding(
|
|
63
|
+
element: Node,
|
|
64
|
+
value: string | State | Binding,
|
|
65
|
+
initialize: (text: string) => void,
|
|
66
|
+
) {
|
|
67
|
+
if (value instanceof Binding) {
|
|
68
|
+
initializeData(element, {
|
|
69
|
+
[value.key]: (data: any) => {
|
|
70
|
+
const isState = data instanceof State;
|
|
71
|
+
const stringOrState = isState
|
|
72
|
+
? FromStates([data], () => value.map(data.get()))
|
|
73
|
+
: value.map(data);
|
|
74
|
+
|
|
75
|
+
applyStringOrState(stringOrState, initialize);
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
applyStringOrState(value, initialize);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function initializeHtml(
|
|
84
|
+
element: Element,
|
|
85
|
+
html: string | State | Binding | undefined,
|
|
86
|
+
) {
|
|
87
|
+
if (html !== undefined) {
|
|
88
|
+
applyStringOrStateOrBinding(element, html, (text) => {
|
|
89
|
+
element.innerHTML = text;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function initializeText(
|
|
95
|
+
node: Node,
|
|
96
|
+
text: string | State | Binding | undefined,
|
|
97
|
+
) {
|
|
98
|
+
if (text !== undefined) {
|
|
99
|
+
applyStringOrStateOrBinding(node, text, (text) => {
|
|
100
|
+
node.textContent = text;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function initializeStyle(
|
|
106
|
+
element: Element,
|
|
107
|
+
css: Record<string, string | State | Binding> | undefined,
|
|
108
|
+
) {
|
|
109
|
+
const style = (element as any).style;
|
|
110
|
+
if (!(style instanceof CSSStyleDeclaration)) return;
|
|
111
|
+
|
|
112
|
+
for (const propName in css) {
|
|
113
|
+
const value = css[propName];
|
|
114
|
+
applyStringOrStateOrBinding(element, value, (text) =>
|
|
115
|
+
style.setProperty(propName, text),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function initializeAttributes(
|
|
121
|
+
element: Element,
|
|
122
|
+
attr: Record<string, string | State | Binding> | undefined,
|
|
123
|
+
) {
|
|
124
|
+
for (const attrName in attr) {
|
|
125
|
+
const value = attr[attrName];
|
|
126
|
+
applyStringOrStateOrBinding(element, value, (text) => {
|
|
127
|
+
if (!text) element.removeAttribute(attrName);
|
|
128
|
+
else {
|
|
129
|
+
element.setAttribute(attrName, text);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function initializeProps(
|
|
136
|
+
element: Node,
|
|
137
|
+
prop: Record<string, any | State | Binding> | undefined,
|
|
138
|
+
) {
|
|
139
|
+
for (const key in prop) {
|
|
140
|
+
const value = prop[key];
|
|
141
|
+
applyStringOrStateOrBinding(element, value, (resolvedValue) => {
|
|
142
|
+
(element as any)[key] = resolvedValue;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function initialize$(element: Element, $: $Record | undefined) {
|
|
148
|
+
for (const selector in $) {
|
|
149
|
+
const selected = element.querySelector(selector);
|
|
150
|
+
if (selected) initialize(selected, $[selector]);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function initialize$$(element: Element, $$: $Record | undefined) {
|
|
155
|
+
for (const selector in $$) {
|
|
156
|
+
const selectedItems = element.querySelectorAll(selector);
|
|
157
|
+
for (const selected of selectedItems) {
|
|
158
|
+
initialize(selected, $$[selector]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function initializeEventListeners<TEventType2Event>(
|
|
164
|
+
element: Node,
|
|
165
|
+
on: EventListenerRecord<TEventType2Event> | undefined,
|
|
166
|
+
) {
|
|
167
|
+
for (const eventName in on) {
|
|
168
|
+
const type = eventName as keyof TEventType2Event;
|
|
169
|
+
const listener = on[type];
|
|
170
|
+
if (!listener) continue;
|
|
171
|
+
if (typeof listener === "function") {
|
|
172
|
+
element.addEventListener(eventName, listener as EventListener);
|
|
173
|
+
} else {
|
|
174
|
+
element.addEventListener(
|
|
175
|
+
eventName,
|
|
176
|
+
listener.listener as EventListener,
|
|
177
|
+
listener.options,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function initialize<TElement extends Element, TEventType2Event>(
|
|
184
|
+
element: TElement | null,
|
|
185
|
+
initializer: ElementInitializer<TElement, TEventType2Event>,
|
|
186
|
+
) {
|
|
187
|
+
if (!element) return;
|
|
188
|
+
if (typeof initializer === "string" || initializer instanceof State) {
|
|
189
|
+
initializeText(element, initializer);
|
|
190
|
+
} else if (Array.isArray(initializer)) {
|
|
191
|
+
initializeChildBlock(element, initializer);
|
|
192
|
+
} else if (typeof initializer === "function") {
|
|
193
|
+
initializer(element);
|
|
194
|
+
} else {
|
|
195
|
+
initializeHtml(element, initializer.html);
|
|
196
|
+
initializeText(element, initializer.text);
|
|
197
|
+
initializeAttributes(element, initializer.attr);
|
|
198
|
+
initializeProps(element, initializer.prop);
|
|
199
|
+
initializeStyle(element, initializer.css);
|
|
200
|
+
initialize$(element, initializer.$);
|
|
201
|
+
initialize$$(element, initializer.$$);
|
|
202
|
+
initializeEventListeners(element, initializer.on);
|
|
203
|
+
initializeData(element, initializer.data);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function Modify<T extends Element>(
|
|
208
|
+
elementOrSelector: T | string,
|
|
209
|
+
...initializers: ElementInitializer<T>[]
|
|
210
|
+
) {
|
|
211
|
+
const element =
|
|
212
|
+
typeof elementOrSelector === "string"
|
|
213
|
+
? (document.querySelector(elementOrSelector) as T)
|
|
214
|
+
: elementOrSelector;
|
|
215
|
+
for (const initializer of initializers) {
|
|
216
|
+
initialize(element, initializer);
|
|
217
|
+
}
|
|
218
|
+
return element;
|
|
219
|
+
}
|
package/src/State.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class State<T = any> {
|
|
2
|
+
#value: T;
|
|
3
|
+
constructor(value: T) {
|
|
4
|
+
this.#value = value;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
get = () => this.#value;
|
|
8
|
+
set(value: T) {
|
|
9
|
+
this.#value = value;
|
|
10
|
+
|
|
11
|
+
this.#dispatch("change");
|
|
12
|
+
}
|
|
13
|
+
#dispatch(event: string) {
|
|
14
|
+
this.#listeners.dispatchEvent(new Event(event));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#listeners = new EventTarget();
|
|
18
|
+
on(event: StateEventType, listener: () => void) {
|
|
19
|
+
this.#listeners.addEventListener(event, listener);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type StateEventType = "change";
|
|
24
|
+
|
|
25
|
+
export function useState<T>(value: T) {
|
|
26
|
+
return new State<T>(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function FromStates<T>(states: State[], createValue: () => T) {
|
|
30
|
+
const result = new State<T>(createValue());
|
|
31
|
+
const update = () => {
|
|
32
|
+
result.set(createValue());
|
|
33
|
+
};
|
|
34
|
+
for (const state of states) {
|
|
35
|
+
state.on("change", update);
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
package/src/Tag.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { div } from "./Elements";
|
|
2
|
+
import { type ElementInitializer, Modify } from "./Modify";
|
|
3
|
+
|
|
4
|
+
export type SvgElementInitializer =
|
|
5
|
+
| {
|
|
6
|
+
attr?: Record<string, string>;
|
|
7
|
+
css?: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
| Element[];
|
|
10
|
+
|
|
11
|
+
export function Svg<K extends keyof SVGElementTagNameMap>(
|
|
12
|
+
name: K,
|
|
13
|
+
...initializers: SvgElementInitializer[]
|
|
14
|
+
): SVGElementTagNameMap[K] {
|
|
15
|
+
const result = document.createElementNS("http://www.w3.org/2000/svg", name);
|
|
16
|
+
return Modify(result, ...initializers);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Html<K extends keyof HTMLElementTagNameMap>(
|
|
20
|
+
tagName: K,
|
|
21
|
+
...initializers: ElementInitializer<HTMLElementTagNameMap[K]>[]
|
|
22
|
+
) {
|
|
23
|
+
const result = document.createElement(tagName);
|
|
24
|
+
Modify(result, ...initializers);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function Tag(
|
|
29
|
+
html: string,
|
|
30
|
+
...initializers: ElementInitializer<Element>[]
|
|
31
|
+
) {
|
|
32
|
+
const result = div({ html: html }).children[0];
|
|
33
|
+
return Modify(result, ...initializers);
|
|
34
|
+
}
|
package/src/data/data.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
export type DataCallback = (data: any) => void;
|
|
2
|
+
|
|
3
|
+
export class NodeData {
|
|
4
|
+
node2Data = new WeakMap<Node, Record<string, any>>();
|
|
5
|
+
|
|
6
|
+
addCallbacks(
|
|
7
|
+
element: Node,
|
|
8
|
+
callbackRecord: Record<string, DataCallback[]> | undefined,
|
|
9
|
+
) {
|
|
10
|
+
if (!callbackRecord) return;
|
|
11
|
+
if (!this.node2DescendantCallbacks.has(element)) {
|
|
12
|
+
this.node2DescendantCallbacks.set(element, {});
|
|
13
|
+
}
|
|
14
|
+
const originalCallbackRecord = this.node2DescendantCallbacks.get(element)!;
|
|
15
|
+
appendCallbacksRecord(originalCallbackRecord, callbackRecord);
|
|
16
|
+
const dataRecord = this.node2Data.get(element);
|
|
17
|
+
resolveCallbacksByData(originalCallbackRecord, dataRecord);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
setDataRecord(element: Node, dataRecord: Record<string, any> | undefined) {
|
|
21
|
+
dataRecord && this.node2Data.set(element, dataRecord);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
resolveCallbacks(element: Node, child: Node) {
|
|
25
|
+
/**
|
|
26
|
+
* Bubble up callbacks record until it reaches root.
|
|
27
|
+
* if it finds data, call callbacks and remove them.
|
|
28
|
+
* if it reaches root, append root callbacks
|
|
29
|
+
*/
|
|
30
|
+
const bubbleUp = (
|
|
31
|
+
ancestor: Node,
|
|
32
|
+
callbacksRecord: Record<string, DataCallback[]>,
|
|
33
|
+
) => {
|
|
34
|
+
resolveCallbacksByData(callbacksRecord, this.node2Data.get(ancestor));
|
|
35
|
+
|
|
36
|
+
// If callbacks are empty (resolved), finish bubbling up
|
|
37
|
+
if (!Object.keys(callbacksRecord).length) return;
|
|
38
|
+
|
|
39
|
+
// If it is root,
|
|
40
|
+
// append callbacks
|
|
41
|
+
if (!ancestor.parentElement) {
|
|
42
|
+
if (!this.node2DescendantCallbacks.has(ancestor)) {
|
|
43
|
+
this.node2DescendantCallbacks.set(ancestor, {});
|
|
44
|
+
}
|
|
45
|
+
const rootCallbacks = this.node2DescendantCallbacks.get(ancestor);
|
|
46
|
+
rootCallbacks && appendCallbacksRecord(rootCallbacks, callbacksRecord);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
bubbleUp(ancestor.parentElement, callbacksRecord);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const callbacks = this.node2DescendantCallbacks.get(child);
|
|
54
|
+
if (callbacks) {
|
|
55
|
+
bubbleUp(element, callbacks);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
node2DescendantCallbacks = new WeakMap<
|
|
60
|
+
Node,
|
|
61
|
+
Record<string, DataCallback[]>
|
|
62
|
+
>();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve callbacks.
|
|
67
|
+
* Resolved keys are deleted
|
|
68
|
+
*/
|
|
69
|
+
function resolveCallbacksByData(
|
|
70
|
+
callbacksRecord: Record<string, DataCallback[]>,
|
|
71
|
+
dataRecord: Record<string, any> | undefined,
|
|
72
|
+
) {
|
|
73
|
+
for (const key in dataRecord) {
|
|
74
|
+
if (key in callbacksRecord) {
|
|
75
|
+
for (const callback of callbacksRecord[key]) {
|
|
76
|
+
callback(dataRecord[key]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
delete callbacksRecord[key];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type DataRecord = Record<string, DataCallback | any>;
|
|
85
|
+
export const nodeData = new NodeData();
|
|
86
|
+
export function initializeData(element: Node, data: DataRecord | undefined) {
|
|
87
|
+
nodeData.addCallbacks(element, extractCallbackRecord(data));
|
|
88
|
+
nodeData.setDataRecord(element, extractDataValueRecord(data));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function extractCallbackRecord(
|
|
92
|
+
record: DataRecord | undefined,
|
|
93
|
+
): Record<string, DataCallback[]> | undefined {
|
|
94
|
+
return extractRecordFromDataRecord(
|
|
95
|
+
record,
|
|
96
|
+
(value) => typeof value === "function",
|
|
97
|
+
(callback) => [callback],
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function extractDataValueRecord(
|
|
102
|
+
record: DataRecord | undefined,
|
|
103
|
+
): Record<string, any> | undefined {
|
|
104
|
+
return extractRecordFromDataRecord(
|
|
105
|
+
record,
|
|
106
|
+
(value) => typeof value !== "function",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function extractRecordFromDataRecord(
|
|
111
|
+
record: DataRecord | undefined,
|
|
112
|
+
predicate: (value: any) => boolean,
|
|
113
|
+
map = (value: any) => value,
|
|
114
|
+
) {
|
|
115
|
+
if (!record) return;
|
|
116
|
+
|
|
117
|
+
const result = {} as Record<string, any>;
|
|
118
|
+
for (const key in record) {
|
|
119
|
+
const value = record[key];
|
|
120
|
+
if (predicate(value)) {
|
|
121
|
+
result[key] = map(value);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!Object.keys(result).length) return;
|
|
126
|
+
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function createDescendantCallbacks(
|
|
131
|
+
record: Record<string, DataCallback> | undefined,
|
|
132
|
+
) {
|
|
133
|
+
return extractRecordFromDataRecord(
|
|
134
|
+
record,
|
|
135
|
+
() => true,
|
|
136
|
+
(value) => [value],
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function appendCallbacksRecord(
|
|
141
|
+
record1: Record<string, DataCallback[]>,
|
|
142
|
+
record2: Record<string, DataCallback[]> | undefined,
|
|
143
|
+
) {
|
|
144
|
+
for (const key in record2) {
|
|
145
|
+
if (!record1[key]) {
|
|
146
|
+
record1[key] = [];
|
|
147
|
+
}
|
|
148
|
+
record1[key].push(...record2[key]);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function findData(node: Node | null, key: string) {
|
|
153
|
+
if (!node) return;
|
|
154
|
+
|
|
155
|
+
const record = nodeData.node2Data.get(node);
|
|
156
|
+
if (record && key in record) {
|
|
157
|
+
return record[key];
|
|
158
|
+
}
|
|
159
|
+
return findData(node.parentElement, key);
|
|
160
|
+
}
|
package/src/flow/For.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { nodeData } from "../data/data";
|
|
2
|
+
import { getNextNodeSibling } from "../initializeChildBlock";
|
|
3
|
+
import type { State } from "../State";
|
|
4
|
+
import { ControlFlow } from "./ControlFlow";
|
|
5
|
+
|
|
6
|
+
export function For<T>(
|
|
7
|
+
array: State<readonly T[]>,
|
|
8
|
+
map: (value: T) => Node | string,
|
|
9
|
+
): ControlFlow {
|
|
10
|
+
return new ForMap(array, map);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class ForMap<T> extends ControlFlow {
|
|
14
|
+
constructor(
|
|
15
|
+
private list: State<readonly T[]>,
|
|
16
|
+
private map: (value: T) => Node | string,
|
|
17
|
+
) {
|
|
18
|
+
super();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
run(element: Element) {
|
|
22
|
+
const model2View = new Map<T, Node>();
|
|
23
|
+
const view2Model = new Map<Node, T>();
|
|
24
|
+
|
|
25
|
+
// When list is updated, check diffs.
|
|
26
|
+
// Create UIs for added models and delete UIs for deleted models.
|
|
27
|
+
const updateListUI = () => {
|
|
28
|
+
// Added models
|
|
29
|
+
const itemsAdded = [] as T[];
|
|
30
|
+
for (const item of this.list.get()) {
|
|
31
|
+
if (!model2View.has(item)) itemsAdded.push(item);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// UIs to remove
|
|
35
|
+
const viewsToRemove = [] as Node[];
|
|
36
|
+
const listSet = new Set<T>(this.list.get());
|
|
37
|
+
for (const view of view2Model.keys()) {
|
|
38
|
+
const oldItem = view2Model.get(view);
|
|
39
|
+
if (oldItem && !listSet.has(oldItem)) viewsToRemove.push(view);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Add UIs for added models
|
|
43
|
+
for (const item of itemsAdded) {
|
|
44
|
+
const mapped = this.map(item);
|
|
45
|
+
const resolved =
|
|
46
|
+
typeof mapped === "string" ? document.createTextNode(mapped) : mapped;
|
|
47
|
+
|
|
48
|
+
// data
|
|
49
|
+
nodeData.resolveCallbacks(element, resolved);
|
|
50
|
+
|
|
51
|
+
// To insert `item`
|
|
52
|
+
view2Model.set(resolved, item);
|
|
53
|
+
model2View.set(item, resolved);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Remove UIs
|
|
57
|
+
for (const view of viewsToRemove) {
|
|
58
|
+
view.parentNode?.removeChild(view);
|
|
59
|
+
const model = view2Model.get(view);
|
|
60
|
+
view2Model.delete(view);
|
|
61
|
+
model && model2View.delete(model);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Sort
|
|
65
|
+
for (const child of [...view2Model.keys()]) {
|
|
66
|
+
// child might not be appended yet. (so we need `?`)
|
|
67
|
+
child.parentElement?.removeChild(child);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const next = getNextNodeSibling(this);
|
|
71
|
+
for (const model of this.list.get()) {
|
|
72
|
+
element.insertBefore(model2View.get(model)!, next);
|
|
73
|
+
}
|
|
74
|
+
this.firstNode = model2View.get(this.list.get()[0]) ?? null;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
updateListUI();
|
|
78
|
+
this.list.on("change", () => {
|
|
79
|
+
updateListUI();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/flow/If.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { nodeData } from "../data/data";
|
|
2
|
+
import { getNextNodeSibling } from "../initializeChildBlock";
|
|
3
|
+
import type { State } from "../State";
|
|
4
|
+
import { ControlFlow } from "./ControlFlow";
|
|
5
|
+
|
|
6
|
+
export function If(
|
|
7
|
+
condition: State<boolean>,
|
|
8
|
+
show: () => Element,
|
|
9
|
+
showElse?: () => Element,
|
|
10
|
+
): ControlFlow {
|
|
11
|
+
return new IfFlow(condition, show, showElse);
|
|
12
|
+
}
|
|
13
|
+
export class IfFlow extends ControlFlow {
|
|
14
|
+
#condition: State<boolean>;
|
|
15
|
+
#createThen: () => Element;
|
|
16
|
+
#createElse?: () => Element;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
condition: State<boolean>,
|
|
20
|
+
create: () => Element,
|
|
21
|
+
createElse?: () => Element,
|
|
22
|
+
) {
|
|
23
|
+
super();
|
|
24
|
+
this.#condition = condition;
|
|
25
|
+
this.#createThen = create;
|
|
26
|
+
this.#createElse = createElse;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
run(element: Element) {
|
|
30
|
+
let child: Element;
|
|
31
|
+
let elseChild: Element | undefined;
|
|
32
|
+
|
|
33
|
+
const update = () => {
|
|
34
|
+
const next = getNextNodeSibling(this);
|
|
35
|
+
|
|
36
|
+
if (this.#condition.get()) {
|
|
37
|
+
if (!child) child = this.#createThen();
|
|
38
|
+
|
|
39
|
+
// data
|
|
40
|
+
nodeData.resolveCallbacks(element, child);
|
|
41
|
+
// insert `child`
|
|
42
|
+
this.firstNode = child;
|
|
43
|
+
elseChild?.remove();
|
|
44
|
+
element.insertBefore(child, next);
|
|
45
|
+
} else {
|
|
46
|
+
if (!elseChild) elseChild = this.#createElse?.();
|
|
47
|
+
|
|
48
|
+
// data
|
|
49
|
+
elseChild && nodeData.resolveCallbacks(element, elseChild);
|
|
50
|
+
// insert `elseChild`
|
|
51
|
+
this.firstNode = elseChild ?? null;
|
|
52
|
+
child?.remove();
|
|
53
|
+
elseChild && element.insertBefore(elseChild, next);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
update();
|
|
58
|
+
|
|
59
|
+
this.#condition.on("change", update);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { nodeData } from "../data/data";
|
|
2
|
+
import { getNextNodeSibling } from "../initializeChildBlock";
|
|
3
|
+
import type { State } from "../State";
|
|
4
|
+
import { ControlFlow } from "./ControlFlow";
|
|
5
|
+
import type { SwitchSection } from "./SwitchBlockState";
|
|
6
|
+
|
|
7
|
+
export function Switch<T>(
|
|
8
|
+
value: State<T>,
|
|
9
|
+
sections: SwitchSection<T>[],
|
|
10
|
+
createDefault?: () => Element,
|
|
11
|
+
): ControlFlow {
|
|
12
|
+
return new SwitchFlow(value, sections, createDefault);
|
|
13
|
+
}
|
|
14
|
+
export class SwitchFlow<T> extends ControlFlow {
|
|
15
|
+
#value: State<T>;
|
|
16
|
+
#sections: SwitchSection<T>[];
|
|
17
|
+
#createDefault?: () => Element;
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
value: State<T>,
|
|
21
|
+
sections: SwitchSection<T>[],
|
|
22
|
+
createDefault?: () => Element,
|
|
23
|
+
) {
|
|
24
|
+
super();
|
|
25
|
+
this.#value = value;
|
|
26
|
+
this.#sections = sections;
|
|
27
|
+
this.#createDefault = createDefault;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
run(element: Element) {
|
|
31
|
+
const value2Element = new Map<T, Element>();
|
|
32
|
+
const value2Section = new Map<T, SwitchSection<T>>();
|
|
33
|
+
|
|
34
|
+
for (const section of this.#sections) {
|
|
35
|
+
value2Section.set(section.case, section);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let currentElement: Element | undefined;
|
|
39
|
+
let defaultElement: Element | undefined;
|
|
40
|
+
|
|
41
|
+
const getElementFromValue = (value: T) => {
|
|
42
|
+
const section = value2Section.get(value);
|
|
43
|
+
if (section) {
|
|
44
|
+
// If element is not created yet, create and cache
|
|
45
|
+
if (!value2Element.has(value)) {
|
|
46
|
+
const newElement = section.show();
|
|
47
|
+
value2Element.set(value, newElement);
|
|
48
|
+
}
|
|
49
|
+
return value2Element.get(value)!;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ensure default
|
|
53
|
+
if (this.#createDefault && !defaultElement) {
|
|
54
|
+
defaultElement = this.#createDefault();
|
|
55
|
+
}
|
|
56
|
+
return defaultElement;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const update = () => {
|
|
60
|
+
const value = this.#value.get();
|
|
61
|
+
const nextNode = getNextNodeSibling(this);
|
|
62
|
+
|
|
63
|
+
const newElement = getElementFromValue(value);
|
|
64
|
+
// data
|
|
65
|
+
newElement && nodeData.resolveCallbacks(element, newElement);
|
|
66
|
+
// insert `newElement`
|
|
67
|
+
currentElement?.remove();
|
|
68
|
+
newElement && element.insertBefore(newElement, nextNode);
|
|
69
|
+
currentElement = newElement;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
update();
|
|
73
|
+
|
|
74
|
+
this.#value.on("change", update);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type State, useState } from '../State';
|
|
2
|
+
|
|
3
|
+
export type SwitchSection<TCase, TElement = Element> = {
|
|
4
|
+
case: TCase;
|
|
5
|
+
show: () => TElement;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function SwitchBlockState<TCase, TElement = Element>(
|
|
9
|
+
value: State<TCase>,
|
|
10
|
+
sections: SwitchSection<TCase, TElement>[],
|
|
11
|
+
) {
|
|
12
|
+
const result = useState<SwitchSection<TCase, TElement> | undefined>(
|
|
13
|
+
undefined,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const update = () => {
|
|
17
|
+
let activated: SwitchSection<TCase, TElement> | undefined;
|
|
18
|
+
|
|
19
|
+
for (const section of sections) {
|
|
20
|
+
if (value.get() === section.case) {
|
|
21
|
+
activated = section;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
result.set(activated);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
value.on('change', update);
|
|
28
|
+
|
|
29
|
+
update();
|
|
30
|
+
return result;
|
|
31
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { nodeData } from "./data/data";
|
|
2
|
+
import { ControlFlow } from "./flow/ControlFlow";
|
|
3
|
+
import { applyStringOrState } from "./Modify";
|
|
4
|
+
import { State } from "./State";
|
|
5
|
+
|
|
6
|
+
export type ChildType = Node | string | State | ControlFlow;
|
|
7
|
+
export function initializeChildBlock(element: Element, children: ChildType[]) {
|
|
8
|
+
const resolvedChildren = resolveTextNode(children);
|
|
9
|
+
connectNeighbours(resolvedChildren);
|
|
10
|
+
|
|
11
|
+
for (const child of resolvedChildren) {
|
|
12
|
+
initializeChild(element, child);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function initializeChild(element: Element, child: ControlFlow | Node) {
|
|
17
|
+
if (child instanceof ControlFlow) {
|
|
18
|
+
child.run(element);
|
|
19
|
+
} else {
|
|
20
|
+
nodeData.resolveCallbacks(element, child);
|
|
21
|
+
element.appendChild(child);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveTextNode(children: ChildType[]): (Node | ControlFlow)[] {
|
|
26
|
+
return children.map((c) => {
|
|
27
|
+
if (typeof c === "string" || c instanceof State) {
|
|
28
|
+
const textNode = document.createTextNode("");
|
|
29
|
+
applyStringOrState(c, (text) => {
|
|
30
|
+
textNode.textContent = text;
|
|
31
|
+
});
|
|
32
|
+
return textNode;
|
|
33
|
+
}
|
|
34
|
+
return c;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function connectNeighbours(children: (Node | ControlFlow)[]) {
|
|
39
|
+
for (let i = 0; i < children.length; i++) {
|
|
40
|
+
const child = children[i];
|
|
41
|
+
if (child instanceof ControlFlow) {
|
|
42
|
+
child.next = children[i + 1] ?? null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getNextNodeSiblingVirtual(flow: ControlFlow) {
|
|
48
|
+
const next = flow.next;
|
|
49
|
+
if (next === null) return null;
|
|
50
|
+
if (next instanceof Node) return next;
|
|
51
|
+
|
|
52
|
+
if (next.firstNode) return next.firstNode;
|
|
53
|
+
return getNextNodeSiblingVirtual(next);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getNextNodeSibling(flow: ControlFlow) {
|
|
57
|
+
const virtual = getNextNodeSiblingVirtual(flow);
|
|
58
|
+
return virtual?.parentElement ? virtual : null;
|
|
59
|
+
}
|