@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 CHANGED
@@ -41,25 +41,78 @@ document.body.appendChild(img);
41
41
 
42
42
  ### List
43
43
 
44
- List items can be cached using `list`, only updated if the data is changed; order does not matter. Data does not need to be unique, as duplicate nodes 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
45
 
46
- **Note**, `list` only supports `string` and `number` type.
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
- type Planet = { id: string; name: string };
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
- const planets = new Map<string, Planet>();
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
- const ul = h('ul')()(); // <ul></ul>
59
- const render = (id: string) => h('li')()(planets.get(id)?.name ?? '-');
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
- const update = list(render)(ul);
62
- update(planets); // <ul><li>Jupiter</li><li>Mars</li><li>Pluto</li></ul>
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 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 extends string | number>(render: (data: T, i: number) => Element) => (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,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 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);
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
- const cache = /* @__PURE__ */ new Map();
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((data, i) => {
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 = (tag) => html(env.document)(tag);
63
- const svg = (tag) => svg$1(env.document)(tag);
64
- const mathml = (tag) => mathml$1(env.document)(tag);
65
- 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);
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chronocide/hyper",
3
- "version": "0.5.0",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "types": "dist/hyper.d.ts",
6
6
  "exports": {