@esportsplus/template 0.26.5 → 0.28.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.
Files changed (44) hide show
  1. package/.github/workflows/publish.yml +2 -2
  2. package/build/attributes.js +6 -6
  3. package/build/constants.d.ts +2 -3
  4. package/build/constants.js +3 -4
  5. package/build/event/index.js +7 -4
  6. package/build/event/ontick.js +3 -3
  7. package/build/html/index.d.ts +4 -3
  8. package/build/html/index.js +3 -7
  9. package/build/html/parser.js +60 -52
  10. package/build/index.d.ts +1 -0
  11. package/build/render.js +4 -5
  12. package/build/slot/array.d.ts +25 -3
  13. package/build/slot/array.js +123 -48
  14. package/build/slot/cleanup.js +2 -2
  15. package/build/slot/effect.d.ts +12 -3
  16. package/build/slot/effect.js +17 -13
  17. package/build/slot/index.js +2 -2
  18. package/build/slot/render.js +17 -9
  19. package/build/types.d.ts +3 -10
  20. package/build/types.js +1 -1
  21. package/build/utilities/marker.d.ts +2 -0
  22. package/build/utilities/marker.js +4 -0
  23. package/build/utilities/raf.d.ts +2 -0
  24. package/build/utilities/raf.js +1 -0
  25. package/package.json +7 -4
  26. package/src/attributes.ts +6 -6
  27. package/src/constants.ts +6 -8
  28. package/src/event/index.ts +9 -4
  29. package/src/event/ontick.ts +3 -3
  30. package/src/html/index.ts +5 -9
  31. package/src/html/parser.ts +21 -7
  32. package/src/index.ts +1 -0
  33. package/src/render.ts +5 -8
  34. package/src/slot/array.ts +172 -65
  35. package/src/slot/cleanup.ts +2 -2
  36. package/src/slot/effect.ts +20 -13
  37. package/src/slot/index.ts +2 -2
  38. package/src/slot/render.ts +22 -12
  39. package/src/types.ts +3 -11
  40. package/src/utilities/marker.ts +6 -0
  41. package/src/utilities/raf.ts +1 -0
  42. package/build/utilities/queue.d.ts +0 -2
  43. package/build/utilities/queue.js +0 -3
  44. package/src/utilities/queue.ts +0 -7
package/src/slot/array.ts CHANGED
@@ -1,29 +1,47 @@
1
- import { root, ReactiveArray } from '@esportsplus/reactivity';
2
- import { EMPTY_FRAGMENT } from '~/constants';
3
- import { RenderableReactive, SlotGroup } from '~/types';
1
+ import { read, root, set, signal, ReactiveArray } from '@esportsplus/reactivity';
2
+ import { ARRAY_SLOT, EMPTY_FRAGMENT } from '~/constants';
3
+ import { SlotGroup } from '~/types';
4
4
  import { append } from '~/utilities/fragment';
5
- import { cloneNode, firstChild, lastChild } from '~/utilities/node';
5
+ import { cloneNode, firstChild, lastChild, nextSibling } from '~/utilities/node';
6
6
  import { ondisconnect, remove } from './cleanup';
7
+ import marker from '~/utilities/marker';
8
+ import raf from '~/utilities/raf';
9
+ import html from '~/html';
10
+
11
+
12
+ type ArraySlotOp<T> =
13
+ | { items: T[]; op: 'concat' }
14
+ | { deleteCount: number; items: T[]; op: 'splice'; start: number }
15
+ | { items: T[]; op: 'push' }
16
+ | { items: T[]; op: 'unshift' }
17
+ | { op: 'clear' }
18
+ | { op: 'pop' }
19
+ | { op: 'reverse' }
20
+ | { op: 'shift' }
21
+ | { op: 'sort'; order: number[] };
7
22
 
8
23
 
9
24
  class ArraySlot<T> {
10
- array: ReactiveArray<T>;
11
- fragment: Node;
12
- marker: Element;
13
- nodes: SlotGroup[] = [];
14
- template: (...args: Parameters< RenderableReactive<T>['template'] >) => SlotGroup;
25
+ private queue: ArraySlotOp<T>[] = [];
26
+ private marker: Element;
27
+ private nodes: SlotGroup[] = [];
28
+ private scheduled: boolean = false;
29
+ private signal;
30
+ private template: (...args: Parameters<(value: T) => ReturnType<typeof html>>) => SlotGroup;
15
31
 
32
+ readonly fragment: Node;
16
33
 
17
- constructor(anchor: Element, array: ReactiveArray<T>, template: RenderableReactive<T>['template']) {
34
+
35
+ constructor(private array: ReactiveArray<T>, template: ((value: T) => ReturnType<typeof html>)) {
18
36
  let fragment = this.fragment = cloneNode.call(EMPTY_FRAGMENT);
19
37
 
20
- this.array = array;
21
- this.marker = anchor;
22
- this.template = function (data, i) {
38
+ this.marker = marker.cloneNode();
39
+ this.signal = signal(array.length);
40
+ this.template = function (data) {
23
41
  let dispose: VoidFunction,
24
42
  frag = root((d) => {
25
43
  dispose = d;
26
- return template(data, i);
44
+ return template(data);
27
45
  }),
28
46
  group = {
29
47
  head: firstChild.call(frag),
@@ -36,45 +54,46 @@ class ArraySlot<T> {
36
54
  return group;
37
55
  };
38
56
 
39
- array.on('clear', () => this.clear());
40
- array.on('reverse', () => {
41
- root(() => this.render());
57
+ append.call(fragment, this.marker);
58
+
59
+ if (array.length) {
60
+ root(() => {
61
+ this.nodes = array.map(this.template);
62
+ });
63
+ }
64
+
65
+ array.on('clear', () => {
66
+ this.queue.length = 0;
67
+ this.schedule({ op: 'clear' });
68
+ });
69
+ array.on('concat', ({ items }) => {
70
+ this.schedule({ items, op: 'concat' });
71
+ });
72
+ array.on('pop', () => {
73
+ this.schedule({ op: 'pop' });
42
74
  });
43
- array.on('pop', () => this.pop());
44
75
  array.on('push', ({ items }) => {
45
- root(() => this.push(items));
76
+ this.schedule({ items, op: 'push' });
77
+ });
78
+ array.on('reverse', () => {
79
+ this.schedule({ op: 'reverse' });
46
80
  });
47
- array.on('shift', () => this.shift());
48
- array.on('sort', () => {
49
- root(() => this.render());
81
+ array.on('shift', () => {
82
+ this.schedule({ op: 'shift' });
83
+ });
84
+ array.on('sort', ({ order }) => {
85
+ this.schedule({ op: 'sort', order });
50
86
  });
51
87
  array.on('splice', ({ deleteCount, items, start }) => {
52
- root(() => this.splice(start, deleteCount, ...items));
88
+ this.schedule({ deleteCount, items, op: 'splice', start });
53
89
  });
54
90
  array.on('unshift', ({ items }) => {
55
- root(() => this.unshift(items));
91
+ this.schedule({ items, op: 'unshift' });
56
92
  });
57
93
  }
58
94
 
59
95
 
60
- get length() {
61
- return this.nodes.length;
62
- }
63
-
64
- set length(n: number) {
65
- if (n >= this.nodes.length) {
66
- return;
67
- }
68
- else if (n === 0) {
69
- this.clear();
70
- }
71
- else {
72
- this.splice(n);
73
- }
74
- }
75
-
76
-
77
- anchor(index: number = this.nodes.length - 1) {
96
+ private anchor(index: number = this.nodes.length - 1) {
78
97
  let node = this.nodes[index];
79
98
 
80
99
  if (node) {
@@ -84,12 +103,11 @@ class ArraySlot<T> {
84
103
  return this.marker;
85
104
  }
86
105
 
87
- clear() {
106
+ private clear() {
88
107
  remove(...this.nodes.splice(0));
89
-
90
108
  }
91
109
 
92
- pop() {
110
+ private pop() {
93
111
  let group = this.nodes.pop();
94
112
 
95
113
  if (group) {
@@ -97,7 +115,7 @@ class ArraySlot<T> {
97
115
  }
98
116
  }
99
117
 
100
- push(items: T[]) {
118
+ private push(items: T[]) {
101
119
  let anchor = this.anchor();
102
120
 
103
121
  this.nodes.push( ...items.map(this.template) );
@@ -105,16 +123,63 @@ class ArraySlot<T> {
105
123
  anchor.after(this.fragment);
106
124
  }
107
125
 
108
- render() {
109
- if (this.nodes.length) {
110
- remove(...this.nodes.splice(0));
126
+ private schedule(op: ArraySlotOp<T>) {
127
+ this.queue.push(op);
128
+
129
+ if (this.scheduled) {
130
+ return;
111
131
  }
112
132
 
113
- this.nodes = this.array.map(this.template);
114
- this.marker.after(this.fragment);
133
+ this.scheduled = true;
134
+
135
+ raf(() => {
136
+ let queue = this.queue;
137
+
138
+ this.queue.length = 0;
139
+
140
+ root(() => {
141
+ for (let i = 0, n = queue.length; i < n; i++) {
142
+ let op = queue[i];
143
+
144
+ switch (op.op) {
145
+ case 'clear':
146
+ this.clear();
147
+ break;
148
+ case 'concat':
149
+ this.push(op.items);
150
+ break;
151
+ case 'pop':
152
+ this.pop();
153
+ break;
154
+ case 'push':
155
+ this.push(op.items);
156
+ break;
157
+ case 'reverse':
158
+ this.nodes.reverse();
159
+ this.sync();
160
+ break;
161
+ case 'shift':
162
+ this.shift();
163
+ break;
164
+ case 'sort':
165
+ this.sort(op.order);
166
+ break;
167
+ case 'splice':
168
+ this.splice(op.start, op.deleteCount, op.items);
169
+ break;
170
+ case 'unshift':
171
+ this.unshift(op.items);
172
+ break;
173
+ }
174
+ }
175
+ });
176
+
177
+ set(this.signal, this.nodes.length);
178
+ this.scheduled = false;
179
+ });
115
180
  }
116
181
 
117
- shift() {
182
+ private shift() {
118
183
  let group = this.nodes.shift();
119
184
 
120
185
  if (group) {
@@ -122,7 +187,29 @@ class ArraySlot<T> {
122
187
  }
123
188
  }
124
189
 
125
- splice(start: number, stop: number = this.nodes.length, ...items: T[]) {
190
+ private sort(order: number[]) {
191
+ let nodes = this.nodes,
192
+ n = nodes.length;
193
+
194
+ if (n !== order.length) {
195
+ remove(...nodes.splice(0));
196
+
197
+ this.nodes = this.array.map(this.template);
198
+ this.marker.after(this.fragment);
199
+ return;
200
+ }
201
+
202
+ let sorted = new Array(n) as SlotGroup[];
203
+
204
+ for (let i = 0; i < n; i++) {
205
+ sorted[i] = nodes[order[i]];
206
+ }
207
+
208
+ this.nodes = sorted;
209
+ this.sync();
210
+ }
211
+
212
+ private splice(start: number, stop: number = this.nodes.length, items: T[]) {
126
213
  if (!items.length) {
127
214
  remove(...this.nodes.splice(start, stop));
128
215
  return;
@@ -132,22 +219,42 @@ class ArraySlot<T> {
132
219
  this.anchor(start - 1).after(this.fragment);
133
220
  }
134
221
 
135
- unshift(items: T[]) {
136
- this.nodes.unshift(...items.map(this.template));
222
+ private sync() {
223
+ let nodes = this.nodes,
224
+ n = nodes.length;
225
+
226
+ if (!n) {
227
+ return;
228
+ }
229
+
230
+ for (let i = 0; i < n; i++) {
231
+ let group = nodes[i],
232
+ next: Node | null,
233
+ node: Node | null = group.head;
234
+
235
+ while (node) {
236
+ next = node === group.tail ? null : nextSibling.call(node);
237
+
238
+ append.call(this.fragment, node);
239
+ node = next;
240
+ }
241
+ }
242
+
137
243
  this.marker.after(this.fragment);
138
244
  }
139
- }
140
245
 
246
+ private unshift(items: T[]) {
247
+ this.nodes.unshift(...items.map(this.template));
248
+ this.marker.after(this.fragment);
249
+ }
141
250
 
142
- export default <T>(anchor: Element, renderable: RenderableReactive<T>) => {
143
- let array = renderable.array,
144
- slot = new ArraySlot(anchor, array, renderable.template);
145
251
 
146
- if (array.length) {
147
- root(() => {
148
- slot.nodes = array.map(slot.template);
149
- });
252
+ get length() {
253
+ return read(this.signal);
150
254
  }
255
+ }
256
+
257
+ Object.defineProperty(ArraySlot.prototype, ARRAY_SLOT, { value: true });
258
+
151
259
 
152
- return slot.fragment;
153
- };
260
+ export { ArraySlot };
@@ -16,9 +16,9 @@ const remove = (...groups: SlotGroup[]) => {
16
16
  tail = group.tail || head;
17
17
 
18
18
  while (tail) {
19
- if (CLEANUP in tail) {
20
- fns = tail[CLEANUP] as VoidFunction[];
19
+ fns = tail[CLEANUP] as VoidFunction[] | undefined;
21
20
 
21
+ if (fns !== undefined) {
22
22
  while (fn = fns.pop()) {
23
23
  fn();
24
24
  }
@@ -1,8 +1,8 @@
1
1
  import { effect } from '@esportsplus/reactivity';
2
2
  import { Element, Renderable, SlotGroup } from '~/types';
3
3
  import { firstChild, lastChild, nodeValue } from '~/utilities/node'
4
- import { raf } from '~/utilities/queue'
5
4
  import { remove } from './cleanup';
5
+ import raf from '~/utilities/raf'
6
6
  import text from '~/utilities/text';
7
7
  import render from './render';
8
8
 
@@ -24,22 +24,27 @@ class EffectSlot {
24
24
  anchor: Element;
25
25
  disposer: VoidFunction;
26
26
  group: SlotGroup | null = null;
27
+ scheduled = false;
27
28
  textnode: Node | null = null;
28
29
 
29
30
 
30
31
  constructor(anchor: Element, fn: (dispose?: VoidFunction) => Renderable<any>) {
31
- let dispose = fn.length ? () => this.dispose() : undefined;
32
+ let dispose = fn.length ? () => this.dispose() : undefined,
33
+ slot = this;
32
34
 
33
35
  this.anchor = anchor;
34
- this.disposer = effect(() => {
36
+ this.disposer = effect(function () {
35
37
  let value = read( fn(dispose) );
36
38
 
37
- if (!this.disposer) {
38
- this.update(value);
39
+ if (!slot.disposer) {
40
+ slot.update(value);
39
41
  }
40
- else {
41
- raf.add(() => {
42
- this.update(value);
42
+ else if (!slot.scheduled) {
43
+ slot.scheduled = true;
44
+
45
+ raf(() => {
46
+ slot.scheduled = false;
47
+ slot.update(this.value);
43
48
  });
44
49
  }
45
50
  });
@@ -72,15 +77,19 @@ class EffectSlot {
72
77
  }
73
78
 
74
79
  if (typeof value !== 'object') {
80
+ if (typeof value !== 'string') {
81
+ value = String(value);
82
+ }
83
+
75
84
  if (textnode) {
76
- nodeValue.call(textnode, String(value));
85
+ nodeValue.call(textnode, value);
77
86
 
78
87
  if (!textnode.isConnected) {
79
88
  anchor.after(textnode);
80
89
  }
81
90
  }
82
91
  else {
83
- anchor.after( this.textnode = text( String(value) ) );
92
+ anchor.after( this.textnode = text(value as string) );
84
93
  }
85
94
  }
86
95
  else {
@@ -104,6 +113,4 @@ class EffectSlot {
104
113
  }
105
114
 
106
115
 
107
- export default (anchor: Element, fn: (dispose?: VoidFunction) => Renderable<any>) => {
108
- new EffectSlot(anchor, fn);
109
- };
116
+ export { EffectSlot };
package/src/slot/index.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { Element } from '~/types';
2
- import effect from './effect';
2
+ import { EffectSlot } from './effect';
3
3
  import render from './render';
4
4
 
5
5
 
6
6
  export default (anchor: Element, value: unknown): void => {
7
7
  if (typeof value === 'function') {
8
- effect(anchor, value as Parameters<typeof effect>[1]);
8
+ new EffectSlot(anchor, value as ConstructorParameters<typeof EffectSlot>[1]);
9
9
  }
10
10
  else {
11
11
  anchor.after( render(anchor, value) );
@@ -1,10 +1,10 @@
1
1
  import { isArray } from '@esportsplus/utilities';
2
- import { EMPTY_FRAGMENT, RENDERABLE } from '~/constants';
3
- import { Element, RenderableReactive } from '~/types';
2
+ import { ARRAY_SLOT, EMPTY_FRAGMENT } from '~/constants';
3
+ import { Element } from '~/types';
4
4
  import { cloneNode, lastChild } from '~/utilities/node';
5
5
  import { append } from '~/utilities/fragment';
6
+ import { ArraySlot } from './array';
6
7
  import text from '~/utilities/text';
7
- import array from './array';
8
8
 
9
9
 
10
10
  export default function render(anchor: Element, value: unknown): Node {
@@ -16,19 +16,30 @@ export default function render(anchor: Element, value: unknown): Node {
16
16
  return text(value as any);
17
17
  }
18
18
 
19
- if (RENDERABLE in value) {
20
- return array(anchor, value as RenderableReactive<unknown>);
19
+ if ((value as any)[ARRAY_SLOT] === true) {
20
+ return (value as ArraySlot<unknown>).fragment;
21
21
  }
22
22
 
23
- if ('nodeType' in value) {
23
+ if ((value as any).nodeType !== undefined) {
24
24
  return value as Node;
25
25
  }
26
26
 
27
+ let n = (value as any).length;
28
+
29
+ if (typeof n === 'number') {
30
+ if (n === 0) {
31
+ return EMPTY_FRAGMENT;
32
+ }
33
+ else if (n === 1) {
34
+ return render(anchor, (value as any)[0]);
35
+ }
36
+ }
37
+
27
38
  if (isArray(value)) {
28
39
  let fragment = cloneNode.call(EMPTY_FRAGMENT);
29
40
 
30
- for (let i = 0, n = (value as unknown[]).length; i < n; i++) {
31
- append.call(fragment, render(anchor, (value as unknown[])[i]));
41
+ for (let i = 0; i < n; i++) {
42
+ append.call(fragment, render(anchor, value[i]));
32
43
  anchor = lastChild.call(fragment);
33
44
  }
34
45
 
@@ -36,11 +47,10 @@ export default function render(anchor: Element, value: unknown): Node {
36
47
  }
37
48
 
38
49
  if (value instanceof NodeList) {
39
- let fragment = cloneNode.call(EMPTY_FRAGMENT),
40
- nodes = Array.from(value as NodeList);
50
+ let fragment = cloneNode.call(EMPTY_FRAGMENT);
41
51
 
42
- for (let i = 0, n = nodes.length; i < n; i++) {
43
- append.call(fragment, nodes[i]);
52
+ for (let i = 0; i < n; i++) {
53
+ append.call(fragment, value[i]);
44
54
  }
45
55
 
46
56
  return fragment;
package/src/types.ts CHANGED
@@ -1,9 +1,7 @@
1
- import { ReactiveArray } from '@esportsplus/reactivity';
2
- import { RENDERABLE, RENDERABLE_HTML_REACTIVE_ARRAY } from './constants';
3
1
  import { firstChild } from './utilities/node';
2
+ import { ArraySlot } from './slot/array';
4
3
  import attributes from './attributes';
5
4
  import slot from './slot';
6
- import html from './html';
7
5
 
8
6
 
9
7
  type Attribute = Effect<Primitive | Primitive[]> | ((...args: any[]) => void) | Primitive;
@@ -29,13 +27,7 @@ type Element = HTMLElement & Attributes<any>;
29
27
  // - Importing from ^ causes 'cannot be named without a reference to...' error
30
28
  type Primitive = bigint | boolean | null | number | string | undefined;
31
29
 
32
- type Renderable<T> = DocumentFragment | Effect<T> | Node | NodeList | Primitive | RenderableReactive<T> | Renderable<T>[];
33
-
34
- type RenderableReactive<T> = Readonly<{
35
- [RENDERABLE]: typeof RENDERABLE_HTML_REACTIVE_ARRAY;
36
- array: ReactiveArray<T>;
37
- template: (value: T, i: number) => ReturnType<typeof html>;
38
- }>;
30
+ type Renderable<T> = DocumentFragment | ArraySlot<T> | Effect<T> | Node | NodeList | Primitive | Renderable<T>[];
39
31
 
40
32
  type SlotGroup = {
41
33
  head: Element;
@@ -57,7 +49,7 @@ type Template = {
57
49
  export type {
58
50
  Attribute, Attributes,
59
51
  Effect, Element,
60
- Renderable, RenderableReactive,
52
+ Renderable,
61
53
  SlotGroup,
62
54
  Template
63
55
  };
@@ -0,0 +1,6 @@
1
+ import { SLOT_HTML } from '~/constants';
2
+ import { fragment } from './fragment';
3
+ import { firstChild } from './node';
4
+
5
+
6
+ export default firstChild.call( fragment(SLOT_HTML) );
@@ -0,0 +1 @@
1
+ export default globalThis?.requestAnimationFrame;
@@ -1,2 +0,0 @@
1
- declare const raf: import("@esportsplus/tasks/build/factory").Scheduler;
2
- export { raf };
@@ -1,3 +0,0 @@
1
- import { raf as r } from '@esportsplus/tasks';
2
- const raf = r();
3
- export { raf };
@@ -1,7 +0,0 @@
1
- import { raf as r } from '@esportsplus/tasks';
2
-
3
-
4
- const raf = r();
5
-
6
-
7
- export { raf };