@chronocide/hyper 0.5.0 → 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 +64 -11
- package/dist/hyper.d.ts +31 -9
- package/dist/hyper.js +198 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,25 +41,78 @@ document.body.appendChild(img);
|
|
|
41
41
|
|
|
42
42
|
### List
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
Keeping track of children within a list can be tedious. `list` caches state and compares each entry, only updating children if the data has changed.
|
|
45
45
|
|
|
46
|
-
**Note
|
|
46
|
+
**Note**: `list` only supports the following types:
|
|
47
|
+
- `string`
|
|
48
|
+
- `number`
|
|
49
|
+
- `boolean`
|
|
50
|
+
- `null`
|
|
51
|
+
- `array`
|
|
52
|
+
- `object`
|
|
53
|
+
|
|
54
|
+
Where `array` and `object` can only contain aforementioned types.
|
|
47
55
|
|
|
48
56
|
```ts
|
|
49
57
|
import h, { list } from '@chronocide/hyper';
|
|
50
58
|
|
|
51
|
-
|
|
59
|
+
const ul = h('ul')()(); // <ul></ul>
|
|
60
|
+
const li = (x: string) => h('li')()(x);
|
|
61
|
+
const update = list(li)(ul);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* <ul>
|
|
65
|
+
* <li>a</li> (render)
|
|
66
|
+
* <li>b</li> (render)
|
|
67
|
+
* <li>c</li> (render)
|
|
68
|
+
* </ul>
|
|
69
|
+
*/
|
|
70
|
+
update(['a', 'b', 'c']);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* <ul>
|
|
74
|
+
* <li>a</li>
|
|
75
|
+
* <li>b</li>
|
|
76
|
+
* <li>c</li>
|
|
77
|
+
* <li>d</li> (render)
|
|
78
|
+
* </ul>
|
|
79
|
+
*/
|
|
80
|
+
update(['a', 'b', 'c', 'd']);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* <ul>
|
|
84
|
+
* <li>a</li>
|
|
85
|
+
* <li>b</li> (render)
|
|
86
|
+
* <li>c</li>
|
|
87
|
+
* </ul>
|
|
88
|
+
*/
|
|
89
|
+
update(['a', 'c', 'c']);
|
|
90
|
+
```
|
|
52
91
|
|
|
53
|
-
|
|
54
|
-
planets.add('jupiter', { id: 'jupiter', name: 'Jupiter' });
|
|
55
|
-
planets.add('mars', { id: 'mars', name: 'Mars' });
|
|
56
|
-
planets.add('pluto', { id: 'pluto', name: 'Pluto' });
|
|
92
|
+
### Virtual
|
|
57
93
|
|
|
58
|
-
|
|
59
|
-
|
|
94
|
+
Virtualisation is a technique that improves list performance by limiting the amount of children rendered. By only rendering elements that are visible within a defined viewport, the size of the DOM can be significantly decreased.
|
|
95
|
+
|
|
96
|
+
`virtual` adds the neccesary inline styles and even listeners to make virtualisation possible, but does not add `aria` properties.
|
|
60
97
|
|
|
61
|
-
|
|
62
|
-
|
|
98
|
+
```ts
|
|
99
|
+
import h, { virtual } from '@chronocide/hyper';
|
|
100
|
+
|
|
101
|
+
type Planet = { id: string; name: string };
|
|
102
|
+
|
|
103
|
+
const ul = h('ul')()(); // <ul></ul>
|
|
104
|
+
const render = (planet: Planet) => h('li')({ id: planet.id })(planet.name);
|
|
105
|
+
const { update, scrollTo } = virtual(render)(ul);
|
|
106
|
+
|
|
107
|
+
const planets: Planet[] = [
|
|
108
|
+
{ id: 'jupiter', name: 'Jupiter' },
|
|
109
|
+
{ id: 'mars', name: 'Mars' },
|
|
110
|
+
{ id: 'pluto', name: 'Pluto' },
|
|
111
|
+
// ...
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
update(planets); // <ul><li id="jupiter">Jupiter</li><li id="mars">Mars</li><li id="pluto">Pluto</li></ul>
|
|
115
|
+
scrollTo(30); // Scroll to 30th item
|
|
63
116
|
```
|
|
64
117
|
|
|
65
118
|
## Testing
|
package/dist/hyper.d.ts
CHANGED
|
@@ -1,21 +1,43 @@
|
|
|
1
|
-
type
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
type CellOptions<T> = {
|
|
2
|
+
/** If empty, equal to container width */
|
|
3
|
+
width?: number | ((data: T, i: number, arr: T[]) => number | null);
|
|
4
|
+
/** If true, cells do not fill container width */
|
|
5
|
+
gap?: boolean;
|
|
6
|
+
height: number | ((data: T, i: number, arr: T[]) => number);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type Json = string | number | boolean | null | Json[] | {
|
|
10
|
+
[key: string]: Json;
|
|
11
|
+
};
|
|
4
12
|
|
|
5
13
|
declare class Env {
|
|
6
14
|
private _document;
|
|
15
|
+
private _window;
|
|
7
16
|
get document(): Document;
|
|
8
17
|
set document(document: Document);
|
|
18
|
+
get window(): Window;
|
|
19
|
+
set window(window: Window);
|
|
9
20
|
constructor();
|
|
10
21
|
}
|
|
11
22
|
|
|
23
|
+
type Attributes = Record<string, unknown>;
|
|
24
|
+
type HTMLAttributes = Attributes & {
|
|
25
|
+
style?: Record<string, string>;
|
|
26
|
+
};
|
|
27
|
+
type Child = Node | string;
|
|
28
|
+
type HTMLVoidElementTagName = 'area' | 'base' | 'br' | 'col' | 'embed' | 'hr' | 'img' | 'input' | 'link' | 'meta' | 'source' | 'track' | 'wbr';
|
|
29
|
+
|
|
12
30
|
declare const env: Env;
|
|
13
|
-
declare const _default: <T extends keyof HTMLElementTagNameMap>(tag: T) => <P extends
|
|
31
|
+
declare const _default: <T extends keyof HTMLElementTagNameMap>(tag: T) => <P extends HTMLAttributes>(attributes?: P) => (...children: T extends HTMLVoidElementTagName ? never[] : Child[]) => HTMLElementTagNameMap[T];
|
|
14
32
|
|
|
15
|
-
declare const svg: <T extends keyof SVGElementTagNameMap>(tag: T) => <P extends Attributes>(attributes?: P
|
|
16
|
-
declare const mathml: <T extends keyof MathMLElementEventMap>(tag: T) => <P extends Attributes>(attributes?: P
|
|
33
|
+
declare const svg: <T extends keyof SVGElementTagNameMap>(tag: T) => <P extends Attributes>(attributes?: P) => (...children: Child[]) => SVGElementTagNameMap[T];
|
|
34
|
+
declare const mathml: <T extends keyof MathMLElementEventMap>(tag: T) => <P extends Attributes>(attributes?: P) => (...children: Child[]) => MathMLElement;
|
|
17
35
|
declare const xml: (tag: string) => <P extends Attributes>(attributes?: P) => (...children: Child[]) => HTMLElement;
|
|
18
|
-
declare const list: <T extends
|
|
36
|
+
declare const list: <T extends Json>(render: (x: T, i: number, arr: T[]) => Element) => (root: Element) => (next: T[]) => void;
|
|
37
|
+
declare const virtual: <T>(cell: CellOptions<T>) => (render: (i: number) => HTMLElement) => (root: HTMLElement) => {
|
|
38
|
+
update: (next: T[]) => void;
|
|
39
|
+
scrollTo: (i: number) => void;
|
|
40
|
+
};
|
|
19
41
|
|
|
20
|
-
export { _default as default, env, list, mathml, svg, xml };
|
|
21
|
-
export type { Attributes, Child, HTMLVoidElementTagName };
|
|
42
|
+
export { _default as default, env, list, mathml, svg, virtual, xml };
|
|
43
|
+
export type { Attributes, CellOptions, Child, HTMLVoidElementTagName, Json };
|
package/dist/hyper.js
CHANGED
|
@@ -1,68 +1,227 @@
|
|
|
1
|
+
class Env {
|
|
2
|
+
_document;
|
|
3
|
+
_window;
|
|
4
|
+
get document() {
|
|
5
|
+
if (!this._document) throw new Error("Missing document");
|
|
6
|
+
return this._document;
|
|
7
|
+
}
|
|
8
|
+
set document(document2) {
|
|
9
|
+
this._document = document2;
|
|
10
|
+
}
|
|
11
|
+
get window() {
|
|
12
|
+
if (!this._window) throw new Error("Missing window");
|
|
13
|
+
return this._window;
|
|
14
|
+
}
|
|
15
|
+
set window(window2) {
|
|
16
|
+
this._window = window2;
|
|
17
|
+
}
|
|
18
|
+
constructor() {
|
|
19
|
+
this._document = typeof document === "undefined" ? null : document;
|
|
20
|
+
this._window = typeof window === "undefined" ? null : window;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
1
24
|
const maybe = (fn) => (x) => {
|
|
2
25
|
if (x === null || x === void 0) return null;
|
|
3
26
|
return fn(x);
|
|
4
27
|
};
|
|
28
|
+
const debounce = (env) => (fn) => {
|
|
29
|
+
let id;
|
|
30
|
+
return (...x) => {
|
|
31
|
+
if (id) env.window.cancelAnimationFrame(id);
|
|
32
|
+
id = env.window.requestAnimationFrame(() => fn(...x));
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const get = (arr) => (i) => arr[i] ?? null;
|
|
37
|
+
const fill = (n) => (fn) => {
|
|
38
|
+
const arr = Array.from({ length: n });
|
|
39
|
+
for (let i = 0; i < arr.length; i += 1) {
|
|
40
|
+
arr[i] = fn(i, arr);
|
|
41
|
+
}
|
|
42
|
+
return arr;
|
|
43
|
+
};
|
|
44
|
+
const bisectLeft = (arr) => (n) => {
|
|
45
|
+
let l = 0;
|
|
46
|
+
let r = arr.length;
|
|
47
|
+
while (l < r) {
|
|
48
|
+
const m = l + Math.floor((r - l) / 2);
|
|
49
|
+
if (arr[m] < n) {
|
|
50
|
+
l = m + 1;
|
|
51
|
+
} else {
|
|
52
|
+
r = m;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
while (l > 0 && (arr[l] > n || arr[l - 1] === arr[l])) l -= 1;
|
|
56
|
+
return Math.min(arr.length - 1, l);
|
|
57
|
+
};
|
|
58
|
+
const bisectRight = (arr) => (n) => {
|
|
59
|
+
let l = 0;
|
|
60
|
+
let r = arr.length;
|
|
61
|
+
while (l < r) {
|
|
62
|
+
const m = l + Math.floor((r - l) / 2);
|
|
63
|
+
if (arr[m] > n) {
|
|
64
|
+
r = m;
|
|
65
|
+
} else {
|
|
66
|
+
l = m + 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
while (r < arr.length && (arr[r - 1] < n || arr[r] === arr[r - 1])) r += 1;
|
|
70
|
+
return Math.max(0, r - 1);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const cells = (cell) => (container) => (data) => fill(data.length)((i, arr) => {
|
|
74
|
+
const prev = get(arr)(i - 1);
|
|
75
|
+
const height2 = typeof cell.height === "number" ? cell.height : cell.height(data[i], i, data);
|
|
76
|
+
let width = typeof cell.width === "number" ? cell.width : cell.width?.(data[i], i, data) ?? container.width;
|
|
77
|
+
if (!cell.gap) {
|
|
78
|
+
const rows = Math.max(1, Math.floor(container.width / width));
|
|
79
|
+
width = Math.floor(container.width / rows);
|
|
80
|
+
}
|
|
81
|
+
let x = (prev?.x ?? 0) + (prev?.width ?? 0);
|
|
82
|
+
let y = prev?.y ?? 0;
|
|
83
|
+
if (x + width > container.width) {
|
|
84
|
+
x = 0;
|
|
85
|
+
y += prev?.height ?? 0;
|
|
86
|
+
}
|
|
87
|
+
return { i, x, y, width, height: height2 };
|
|
88
|
+
});
|
|
89
|
+
const height = (cells2) => (get(cells2)(cells2.length - 1)?.y ?? 0) + (get(cells2)(cells2.length - 1)?.height ?? 0);
|
|
90
|
+
const view = (container) => (cells2) => {
|
|
91
|
+
const ly = cells2.map((cell) => cell.y);
|
|
92
|
+
const min = bisectLeft(ly)(Math.max(0, container.y - container.height));
|
|
93
|
+
const max = bisectRight(ly)(Math.min(height(cells2), container.y + container.height));
|
|
94
|
+
return [min, max];
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const equals = (a) => (b) => {
|
|
98
|
+
if (a === b) return true;
|
|
99
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
100
|
+
if (a.length !== b.length) return false;
|
|
101
|
+
return a.every((v, i) => equals(v)(b[i]));
|
|
102
|
+
}
|
|
103
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
104
|
+
if (a === null || b === null) return false;
|
|
105
|
+
if (Array.isArray(a) || Array.isArray(b)) return false;
|
|
106
|
+
if (Object.keys(a).length !== Object.keys(b).length) return false;
|
|
107
|
+
return Object.entries(a).every(([k, v]) => {
|
|
108
|
+
if (!(k in b)) return false;
|
|
109
|
+
return equals(b[k])(v);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
};
|
|
114
|
+
const clone = (a) => {
|
|
115
|
+
if (a == null || typeof a !== "object") return a;
|
|
116
|
+
if (Array.isArray(a)) {
|
|
117
|
+
const b = [];
|
|
118
|
+
a.forEach((x, i) => {
|
|
119
|
+
b[i] = clone(x);
|
|
120
|
+
});
|
|
121
|
+
return b;
|
|
122
|
+
}
|
|
123
|
+
if (typeof a === "object") {
|
|
124
|
+
const b = {};
|
|
125
|
+
Object.keys(a).forEach((k) => {
|
|
126
|
+
b[k] = clone(a[k]);
|
|
127
|
+
});
|
|
128
|
+
return b;
|
|
129
|
+
}
|
|
130
|
+
throw new Error("Failed to clone, invalid type");
|
|
131
|
+
};
|
|
5
132
|
|
|
6
|
-
const
|
|
133
|
+
const set = (element) => (attributes) => Object.entries(attributes).forEach(([k, v]) => {
|
|
7
134
|
if (typeof v === "string") element.setAttribute(k, v);
|
|
8
135
|
if (typeof v === "number") element.setAttribute(k, `${v}`);
|
|
9
136
|
if (v === true) element.toggleAttribute(k, v);
|
|
10
137
|
});
|
|
138
|
+
const style = (element) => (style2) => Object.entries(style2).forEach(([k, v]) => {
|
|
139
|
+
element.style.setProperty(k, v);
|
|
140
|
+
});
|
|
11
141
|
const create = (element) => (attributes) => (children) => {
|
|
12
|
-
maybe(
|
|
142
|
+
maybe(set(element))(attributes);
|
|
13
143
|
element.append(...children);
|
|
14
144
|
return element;
|
|
15
145
|
};
|
|
16
|
-
const html = (
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
146
|
+
const html = (env) => (tag) => (attributes) => (...children) => {
|
|
147
|
+
const root = create(env.document.createElement(tag))(attributes)(children);
|
|
148
|
+
maybe(style(root))(attributes?.style);
|
|
149
|
+
return root;
|
|
150
|
+
};
|
|
151
|
+
const svg$1 = (env) => (tag) => (attributes) => (...children) => create(env.document.createElementNS("http://www.w3.org/2000/svg", tag))(attributes)(children);
|
|
152
|
+
const mathml$1 = (env) => (tag) => (attributes) => (...children) => create(env.document.createElementNS("http://www.w3.org/1998/Math/MathML", tag))(attributes)(children);
|
|
153
|
+
const xml$1 = (env) => (tag) => (attributes) => (...children) => create(env.document.createElementNS("http://www.w3.org/1999/xhtml", tag))(attributes)(children);
|
|
20
154
|
const list$1 = (render) => (root) => {
|
|
21
|
-
|
|
155
|
+
let cache = [];
|
|
22
156
|
return (next) => {
|
|
23
|
-
const refs = /* @__PURE__ */ new WeakSet();
|
|
24
157
|
while (root.children.length > next.length) root.lastChild?.remove();
|
|
25
|
-
next.forEach((
|
|
158
|
+
next.forEach((x, i) => {
|
|
159
|
+
if (i < cache.length && equals(x)(cache[i])) return;
|
|
160
|
+
const element = render(x, i, next);
|
|
26
161
|
const child = root.children.item(i);
|
|
27
|
-
let element = cache.get(data);
|
|
28
|
-
if (element === child) return;
|
|
29
|
-
if (!element) {
|
|
30
|
-
element = render(data, i);
|
|
31
|
-
cache.set(data, element);
|
|
32
|
-
}
|
|
33
|
-
if (refs.has(element)) {
|
|
34
|
-
element = element.cloneNode(true);
|
|
35
|
-
} else {
|
|
36
|
-
refs.add(element);
|
|
37
|
-
}
|
|
38
162
|
if (child) {
|
|
39
163
|
root.replaceChild(element, child);
|
|
40
164
|
} else {
|
|
41
165
|
root.appendChild(element);
|
|
42
166
|
}
|
|
43
167
|
});
|
|
168
|
+
cache = clone(next);
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
const virtual$1 = (env) => (cell) => (render) => (root) => {
|
|
172
|
+
style(root)({
|
|
173
|
+
"position": "relative",
|
|
174
|
+
"max-height": "100%",
|
|
175
|
+
"overflow-y": "scroll"
|
|
176
|
+
});
|
|
177
|
+
let cache = [];
|
|
178
|
+
let state = [];
|
|
179
|
+
const update = debounce(env)((full) => {
|
|
180
|
+
if (full) cache = cells(cell)({ width: root.scrollWidth })(state);
|
|
181
|
+
const [min, max] = view({
|
|
182
|
+
height: root.getBoundingClientRect().height,
|
|
183
|
+
y: Math.floor(root.scrollTop)
|
|
184
|
+
})(cache);
|
|
185
|
+
const spacer = html(env)("div")({
|
|
186
|
+
"aria-hidden": "true",
|
|
187
|
+
"style": {
|
|
188
|
+
"width": "100%",
|
|
189
|
+
"height": `${height(cache)}px`,
|
|
190
|
+
"z-index": "-1"
|
|
191
|
+
}
|
|
192
|
+
})();
|
|
193
|
+
root.replaceChildren(...cache.slice(min, max + 1).map((cell2) => {
|
|
194
|
+
const child = render(cell2.i);
|
|
195
|
+
style(child)({
|
|
196
|
+
position: "absolute",
|
|
197
|
+
transform: `translate(${cell2.x}px, ${cell2.y}px)`,
|
|
198
|
+
width: `${cell2.width}px`,
|
|
199
|
+
height: `${cell2.height}px`
|
|
200
|
+
});
|
|
201
|
+
return child;
|
|
202
|
+
}), spacer);
|
|
203
|
+
});
|
|
204
|
+
root.addEventListener("scroll", () => update(false), { passive: true });
|
|
205
|
+
root.addEventListener("resize", () => update(true), { passive: true });
|
|
206
|
+
return {
|
|
207
|
+
update: (next) => {
|
|
208
|
+
state = next;
|
|
209
|
+
update(true);
|
|
210
|
+
},
|
|
211
|
+
scrollTo: (i) => {
|
|
212
|
+
const y = get(cache)(i)?.y;
|
|
213
|
+
if (typeof y !== "number") return;
|
|
214
|
+
root.scrollTop = y;
|
|
215
|
+
}
|
|
44
216
|
};
|
|
45
217
|
};
|
|
46
|
-
|
|
47
|
-
class Env {
|
|
48
|
-
_document;
|
|
49
|
-
get document() {
|
|
50
|
-
if (!this._document) throw new Error("Missing document");
|
|
51
|
-
return this._document;
|
|
52
|
-
}
|
|
53
|
-
set document(document2) {
|
|
54
|
-
this._document = document2;
|
|
55
|
-
}
|
|
56
|
-
constructor() {
|
|
57
|
-
this._document = typeof document === "undefined" ? null : document;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
218
|
|
|
61
219
|
const env = new Env();
|
|
62
|
-
var hyper =
|
|
63
|
-
const svg =
|
|
64
|
-
const mathml =
|
|
65
|
-
const xml =
|
|
220
|
+
var hyper = html(env);
|
|
221
|
+
const svg = svg$1(env);
|
|
222
|
+
const mathml = mathml$1(env);
|
|
223
|
+
const xml = xml$1(env);
|
|
66
224
|
const list = list$1;
|
|
225
|
+
const virtual = virtual$1(env);
|
|
67
226
|
|
|
68
|
-
export { hyper as default, env, list, mathml, svg, xml };
|
|
227
|
+
export { hyper as default, env, list, mathml, svg, virtual, xml };
|