@chronocide/hyper 0.6.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 CHANGED
@@ -41,25 +41,78 @@ document.body.appendChild(img);
41
41
 
42
42
  ### List
43
43
 
44
- Keeping track of children within a list can be tedious, especially if using immutable data. `list` caches elements based on keys and compares each data entry, only updating the child i the data has changed. Keys do not need to be unique, duplicate elements are cloned.
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
+
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.
45
55
 
46
56
  ```ts
47
57
  import h, { list } from '@chronocide/hyper';
48
58
 
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
+ ```
91
+
92
+ ### Virtual
93
+
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.
97
+
98
+ ```ts
99
+ import h, { virtual } from '@chronocide/hyper';
100
+
49
101
  type Planet = { id: string; name: string };
50
102
 
51
103
  const ul = h('ul')()(); // <ul></ul>
52
104
  const render = (planet: Planet) => h('li')({ id: planet.id })(planet.name);
53
- const key = (planet: Planet) => planet.id;
54
- const update = list<Planet>(render)(key)(ul);
105
+ const { update, scrollTo } = virtual(render)(ul);
55
106
 
56
107
  const planets: Planet[] = [
57
108
  { id: 'jupiter', name: 'Jupiter' },
58
109
  { id: 'mars', name: 'Mars' },
59
- { id: 'pluto', name: 'Pluto' }
110
+ { id: 'pluto', name: 'Pluto' },
111
+ // ...
60
112
  ];
61
113
 
62
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 Attributes = Record<string, unknown>;
2
- type Child = Node | string;
3
- type HTMLVoidElementTagName = 'area' | 'base' | 'br' | 'col' | 'embed' | 'hr' | 'img' | 'input' | 'link' | 'meta' | 'source' | 'track' | 'wbr';
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 Attributes>(attributes?: P | undefined) => (...children: T extends HTMLVoidElementTagName ? never[] : Child[]) => HTMLElementTagNameMap[T];
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 | undefined) => (...children: Child[]) => SVGElementTagNameMap[T];
16
- declare const mathml: <T extends keyof MathMLElementEventMap>(tag: T) => <P extends Attributes>(attributes?: P | undefined) => (...children: Child[]) => MathMLElement;
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>(render: (x: T, i: number, arr: T[]) => Element) => (key: (x: T) => string) => (root: Element) => (next: T[]) => void;
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,69 +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 setAttributes = (element) => (attributes) => Object.entries(attributes).forEach(([k, v]) => {
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(setAttributes(element))(attributes);
142
+ maybe(set(element))(attributes);
13
143
  element.append(...children);
14
144
  return element;
15
145
  };
16
- const html = (document) => (tag) => (attributes) => (...children) => create(document.createElement(tag))(attributes)(children);
17
- const svg$1 = (document) => (tag) => (attributes) => (...children) => create(document.createElementNS("http://www.w3.org/2000/svg", tag))(attributes)(children);
18
- const mathml$1 = (document) => (tag) => (attributes) => (...children) => create(document.createElementNS("http://www.w3.org/1998/Math/MathML", tag))(attributes)(children);
19
- const xml$1 = (document) => (tag) => (attributes) => (...children) => create(document.createElementNS("http://www.w3.org/1999/xhtml", tag))(attributes)(children);
20
- const list$1 = (render) => (key) => (root) => {
21
- const cache = /* @__PURE__ */ new Map();
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);
154
+ const list$1 = (render) => (root) => {
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
158
  next.forEach((x, i) => {
26
- const k = key(x);
159
+ if (i < cache.length && equals(x)(cache[i])) return;
160
+ const element = render(x, i, next);
27
161
  const child = root.children.item(i);
28
- let element = cache.get(k);
29
- if (element === child) return;
30
- if (!element) {
31
- element = render(x, i, next);
32
- cache.set(k, element);
33
- }
34
- if (refs.has(element)) {
35
- element = element.cloneNode(true);
36
- } else {
37
- refs.add(element);
38
- }
39
162
  if (child) {
40
163
  root.replaceChild(element, child);
41
164
  } else {
42
165
  root.appendChild(element);
43
166
  }
44
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
+ }
45
216
  };
46
217
  };
47
-
48
- class Env {
49
- _document;
50
- get document() {
51
- if (!this._document) throw new Error("Missing document");
52
- return this._document;
53
- }
54
- set document(document2) {
55
- this._document = document2;
56
- }
57
- constructor() {
58
- this._document = typeof document === "undefined" ? null : document;
59
- }
60
- }
61
218
 
62
219
  const env = new Env();
63
- var hyper = (tag) => html(env.document)(tag);
64
- const svg = (tag) => svg$1(env.document)(tag);
65
- const mathml = (tag) => mathml$1(env.document)(tag);
66
- const xml = (tag) => xml$1(env.document)(tag);
220
+ var hyper = html(env);
221
+ const svg = svg$1(env);
222
+ const mathml = mathml$1(env);
223
+ const xml = xml$1(env);
67
224
  const list = list$1;
225
+ const virtual = virtual$1(env);
68
226
 
69
- export { hyper as default, env, list, mathml, svg, xml };
227
+ export { hyper as default, env, list, mathml, svg, virtual, xml };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chronocide/hyper",
3
- "version": "0.6.0",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "types": "dist/hyper.d.ts",
6
6
  "exports": {