solarite 0.1.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 (63) hide show
  1. package/build/build.bat +3 -0
  2. package/build/build.js +139 -0
  3. package/build/lib/rollup.min.js +11 -0
  4. package/build/lib/source-map.min.js +1 -0
  5. package/build/lib/terser.min.js +1 -0
  6. package/dist/Solarite-debug.js +4143 -0
  7. package/dist/Solarite.js +3740 -0
  8. package/dist/Solarite.min.js +4 -0
  9. package/docs/index.md +423 -0
  10. package/docs/js/Playground.js +184 -0
  11. package/docs/js/codemirror/codemirror6.js +32036 -0
  12. package/docs/js/codemirror/themeSolarIce.js +312 -0
  13. package/docs/js/documentation.js +32 -0
  14. package/docs/js/ui/CodeEditor.js +840 -0
  15. package/docs/js/ui/DarkToggle.js +52 -0
  16. package/docs/js/ui/FlexResizer.js +142 -0
  17. package/docs/js/util/Draggable2.js +151 -0
  18. package/docs/js/util/Errors.js +9 -0
  19. package/docs/js/util/Html.js +147 -0
  20. package/docs/js/util/Icons.js +623 -0
  21. package/docs/js/util/Input.js +253 -0
  22. package/docs/js/util/Util.js +88 -0
  23. package/docs/js/util/delve.js +43 -0
  24. package/docs/media/FiraCode400.woff2 +0 -0
  25. package/docs/media/cabin-latin-700.woff2 +0 -0
  26. package/docs/media/documentation.css +93 -0
  27. package/docs/media/eternium.css +1123 -0
  28. package/docs/media/solarite-machine.webp +0 -0
  29. package/index.html +325 -0
  30. package/package.json +33 -0
  31. package/readme.md +3 -0
  32. package/src/solarite/ExprPath.js +554 -0
  33. package/src/solarite/MultiValueMap.js +65 -0
  34. package/src/solarite/NodeGroup.js +706 -0
  35. package/src/solarite/NodeGroupManager.js +582 -0
  36. package/src/solarite/Shell.js +307 -0
  37. package/src/solarite/Solarite.js +19 -0
  38. package/src/solarite/Template.js +85 -0
  39. package/src/solarite/Util.js +264 -0
  40. package/src/solarite/createSolarite.js +267 -0
  41. package/src/solarite/getArg.js +99 -0
  42. package/src/solarite/hash.js +101 -0
  43. package/src/solarite/r.js +143 -0
  44. package/src/solarite/udomdiff.js +233 -0
  45. package/src/solarite/watch.js +302 -0
  46. package/src/solarite/watch2.js +439 -0
  47. package/src/unused/FastLookupArray.js +54 -0
  48. package/src/unused/Hashes.js +339 -0
  49. package/src/unused/InUse.test.js +92 -0
  50. package/src/unused/InUseMap.js +98 -0
  51. package/src/unused/LinkedList.js +117 -0
  52. package/src/unused/LinkedList.test.js +115 -0
  53. package/src/unused/Perf.js +47 -0
  54. package/src/unused/Template.js +108 -0
  55. package/src/util/Errors.js +9 -0
  56. package/src/util/Util.js +88 -0
  57. package/src/util/delve.js +43 -0
  58. package/tests/Benchmark.test.js +319 -0
  59. package/tests/NodeGroup.test.js +115 -0
  60. package/tests/Shell.test.js +75 -0
  61. package/tests/Solarite.test.js +2896 -0
  62. package/tests/Testimony.js +602 -0
  63. package/tests/index.html +75 -0
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "solarite",
3
+ "version": "0.1.0",
4
+ "description": "Solarite is a small, fast, compilation-free JavaScript web component library that closely follows modern web standards.",
5
+ "main": "dist/Solarite.js",
6
+ "directories": {
7
+ "doc": "docs",
8
+ "test": "tests"
9
+ },
10
+ "scripts": {
11
+ "test": "echo \"Error: no test specified\" && exit 1"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/Vorticode/solarite.git"
16
+ },
17
+ "keywords": [
18
+ "JavaScript",
19
+ "UI",
20
+ "framework",
21
+ "library",
22
+ "reactive",
23
+ "vanilla",
24
+ "minimal",
25
+ "web component"
26
+ ],
27
+ "author": "Eric P",
28
+ "license": "MIT",
29
+ "bugs": {
30
+ "url": "https://github.com/Vorticode/solarite/issues"
31
+ },
32
+ "homepage": "https://vorticode.github.io/solarite/"
33
+ }
package/readme.md ADDED
@@ -0,0 +1,3 @@
1
+ # Solarite
2
+
3
+ Solarite is a small (10KB min+gzip), fast, compilation-free JavaScript web component library that follows modern web standards and integrates well with vanilla JavaScript.
@@ -0,0 +1,554 @@
1
+ import {assert} from "../util/Errors.js";
2
+ import delve from "../util/delve.js";
3
+ import {getObjectId} from "./hash.js";
4
+ import NodeGroup from "./NodeGroup.js";
5
+ import {div, findArrayDiff, setIndent} from "./Util.js";
6
+
7
+ /**
8
+ * Path to where an expression should be evaluated within a Shell.
9
+ * Path is only valid until the expressions before it are evaluated.
10
+ * TODO: Make this based on parent and node instead of path? */
11
+ export default class ExprPath {
12
+
13
+ /**
14
+ * @type {PathType} */
15
+ type;
16
+
17
+ // Used for attributes:
18
+
19
+ /** @type {?string} Used only if type=AttribType.Value. */
20
+ attrName;
21
+
22
+ /**
23
+ * @type {?string[]} Used only if type=AttribType.Value. If null, use one expr to set the whole attribute value. */
24
+ attrValue;
25
+
26
+ /**
27
+ * @type {Set<string>} Used for type=AttribType.Multiple to remember the attributes that were added. */
28
+ attrNames;
29
+
30
+
31
+
32
+ /**
33
+ * @type {Node} Node that occurs before this ExprPath's first Node.
34
+ * This is necessary because udomdiff() can steal nodes from another ExprPath.
35
+ * If we had a pointer to our own startNode then that node could be moved somewhere else w/o us knowing it.
36
+ * Used only for type='content'
37
+ * Will be null if ExprPath has no Nodes. */
38
+ nodeBefore;
39
+
40
+ /**
41
+ * If type is AttribType.Multiple or AttribType.Value, points to the node having the attribute.
42
+ * If type is 'content', points to a node that never changes that this NodeGroup should always insert its nodes before.
43
+ * An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
44
+ * @type {Node|HTMLElement} */
45
+ nodeMarker;
46
+
47
+ /** @deprecated */
48
+ get parentNode() {
49
+ return this.nodeMarker.parentNode;
50
+ }
51
+
52
+ // These are set after an expression is assigned:
53
+
54
+
55
+ /** @type {NodeGroup} */
56
+ parentNg;
57
+
58
+ /** @type {NodeGroup[]} */
59
+ nodeGroups = [];
60
+
61
+
62
+
63
+
64
+ // Caches to make things faster
65
+
66
+ /**
67
+ * @private
68
+ * @type {Node[]} Cached result of getNodes() */
69
+ nodesCache;
70
+
71
+ // What are these?
72
+ nodeBeforeIndex;
73
+ nodeMarkerPath;
74
+
75
+ // TODO: Keep this cached?
76
+ expr;
77
+
78
+ // for debugging
79
+ //#IFDEV
80
+ parentIndex;
81
+ //#ENDIF
82
+
83
+ /**
84
+ * @param nodeBefore {Node}
85
+ * @param nodeMarker {?Node}
86
+ * @param type {string}
87
+ * @param attrName {?string}
88
+ * @param attrValue {string[]} */
89
+ constructor(nodeBefore, nodeMarker, type=PathType.Content, attrName=null, attrValue=null) {
90
+
91
+ //#IFDEV
92
+ /*
93
+ Object.defineProperty(this, 'debug', {
94
+ get() {
95
+ return [
96
+ `parentNode: ${this.nodeBefore.parentNode?.tagName?.toLowerCase()}`,
97
+ 'nodes:',
98
+ ...setIndent(this.getNodes().map(item => {
99
+ if (item instanceof Node)
100
+ return item.outerHTML || item.textContent
101
+ else if (item instanceof NodeGroup)
102
+ return item.debug
103
+ }), 1).flat()
104
+ ]
105
+ }
106
+ })
107
+
108
+ Object.defineProperty(this, 'debugNodes', {
109
+ get: () =>
110
+ this.getNodes()
111
+ })
112
+ */
113
+ //#ENDIF
114
+
115
+ // If path is a node.
116
+ this.nodeBefore = nodeBefore;
117
+ this.nodeMarker = nodeMarker;
118
+ this.type = type;
119
+ this.attrName = attrName;
120
+ this.attrValue = attrValue;
121
+ if (type === PathType.Multiple)
122
+ this.attrNames = new Set();
123
+ }
124
+
125
+ applyMultipleAttribs(node, expr) {
126
+ /*#IFDEV*/assert(this.type === PathType.Multiple);/*#ENDIF*/
127
+
128
+ if (Array.isArray(expr))
129
+ expr = expr.flat().join(' '); // flat and join so we can accept arrays of arrays of strings.
130
+
131
+ // Add new attributes
132
+ let oldNames = this.attrNames;
133
+ this.attrNames = new Set();
134
+ if (expr) {
135
+ let attrs = (expr +'') // Split string into multiple attributes.
136
+ .split(/([\w-]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s]+))/g)
137
+ .map(text => text.trim())
138
+ .filter(text => text.length);
139
+
140
+ for (let attr of attrs) {
141
+ let [name, value] = attr.split(/\s*=\s*/); // split on first equals.
142
+ value = (value || '').replace(/^(['"])(.*)\1$/, '$2'); // trim value quotes if they match.
143
+ node.setAttribute(name, value);
144
+ this.attrNames.add(name)
145
+ }
146
+ }
147
+
148
+ // Remove old attributes.
149
+ for (let oldName of oldNames)
150
+ if (!this.attrNames.has(oldName))
151
+ node.removeAttribute(oldName);
152
+ }
153
+
154
+ /**
155
+ * Handle attributes for event binding, such as:
156
+ * onclick=${(e, el) => this.doSomething(el, 'meow')}
157
+ * onclick=${[this.doSomething, 'meow']}
158
+ * onclick=${[this, 'doSomething', 'meow']}
159
+ *
160
+ * @param node
161
+ * @param expr
162
+ * @param root */
163
+ applyEventAttrib(node, expr, root) {
164
+ /*#IFDEV*/assert(this.type === PathType.Value || this.type === PathType.Component);/*#ENDIF*/
165
+
166
+ let eventName = this.attrName.slice(2);
167
+ let func;
168
+
169
+ // Convert array to function.
170
+ // TODO: This doesn't work for [this, 'doSomething', 'meow']
171
+ let args = [];
172
+ if (Array.isArray(expr)) {
173
+ for (let i=0; i<expr.length; i++)
174
+ if (typeof expr[i] === 'function') {
175
+ func = expr[i];
176
+ args = expr.slice(i+1);
177
+ break;
178
+ }
179
+
180
+ // oninput=${[this, 'value']}
181
+ if (!func) {
182
+ func = setValue
183
+ args = [expr[0], expr.slice(1), node]
184
+ node.value = delve(expr[0], expr.slice(1));
185
+ }
186
+ }
187
+ else
188
+ func = expr;
189
+
190
+ let eventKey = getObjectId(node) + eventName;
191
+ let [existing, existingBound] = nodeEvents[eventKey] || [];
192
+ nodeEventArgs[eventKey] = args; // TODO: Put this in nodeEvents[]
193
+
194
+
195
+ if (existing !== func) {
196
+ if (existing)
197
+ node.removeEventListener(eventName, existingBound);
198
+
199
+ let originalFunc = func;
200
+
201
+ // BoundFunc sets the "this" variable to be the current Solarite component.
202
+ let boundFunc = event => originalFunc.call(root, ...args, event, node);
203
+
204
+ // Save both the original and bound functions.
205
+ // Original so we can compare it against a newly assigned function.
206
+ // Bound so we can use it with removeEventListner().
207
+ nodeEvents[eventKey] = [originalFunc, boundFunc];
208
+
209
+ node.addEventListener(eventName, boundFunc);
210
+
211
+ // TODO: classic event attribs:
212
+ //el[attr.name] = e => // e.g. el.onclick = ...
213
+ // (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el) // put "event", "el", and "this" in scope for the event code.
214
+ }
215
+ }
216
+
217
+ applyValueAttrib(node, exprs, exprIndex) {
218
+ let expr = exprs[exprIndex];
219
+
220
+ // Array for form element data binding.
221
+ // TODO: This never worked, and was moved to applyEventAttrib.
222
+ // let isArrayValue = Array.isArray(expr);
223
+ // if (isArrayValue && expr.length >= 2 && !expr.slice(1).find(v => !['string', 'number'].includes(typeof v))) {
224
+ // node.value = delve(expr[0], expr.slice(1));
225
+ // node.addEventListener('input', e => {
226
+ // delve(expr[0], expr.slice(1), node.value) // TODO: support other properties like checked
227
+ // });
228
+ // }
229
+
230
+ // Values to toggle an attribute
231
+ if (!this.attrValue && (expr === false || expr === null || expr === undefined))
232
+ node.removeAttribute(this.attrName);
233
+
234
+ else if (!this.attrValue && expr === true)
235
+ node.setAttribute(this.attrName, '');
236
+
237
+ // Regular attribute
238
+ else {
239
+ let value = [];
240
+
241
+ // We go backward because NodeGroup.applyExprs() calls this function, and it goes backward through the exprs.
242
+ if (this.attrValue) {
243
+ for (let i=this.attrValue.length-1; i>=0; i--) {
244
+ value.unshift(this.attrValue[i]);
245
+ if (i > 0) {
246
+ let val = exprs[exprIndex];
247
+ if (val !== false && val !== null && val !== undefined)
248
+ value.unshift(val);
249
+ exprIndex--;
250
+ }
251
+ }
252
+
253
+ exprIndex ++;
254
+ }
255
+ else
256
+ value.unshift(expr);
257
+
258
+ let joinedValue = value.join('')
259
+ node.setAttribute(this.attrName, joinedValue);
260
+
261
+ // This is needed for setting input.value, .checked, option.selected, etc.
262
+ // But in some cases setting the attribute is enough. such as div.setAttribute('title') updates div.title.
263
+ // TODO: How to tell which is which?
264
+ if (this.attrName in node)
265
+ node[this.attrName] = joinedValue;
266
+
267
+ }
268
+
269
+ return exprIndex;
270
+ }
271
+
272
+
273
+ /**
274
+ *
275
+ * @param newRoot {HTMLElement}
276
+ * @return {ExprPath} */
277
+ clone(newRoot) {
278
+ /*#IFDEV*/this.verify();/*#ENDIF*/
279
+
280
+ // Resolve node paths.
281
+ let nodeMarker, nodeBefore;
282
+ let root = newRoot;
283
+ let path = this.nodeMarkerPath;
284
+ for (let i=path.length-1; i>0; i--)
285
+ root = root.childNodes[path[i]];
286
+ let childNodes = root.childNodes;
287
+ nodeMarker = childNodes[path[0]]
288
+ if (this.nodeBefore)
289
+ nodeBefore = childNodes[this.nodeBeforeIndex];
290
+
291
+ let result = new ExprPath(nodeBefore, nodeMarker, this.type, this.attrName, this.attrValue);
292
+
293
+ //#IFDEV
294
+ result.verify();
295
+ result.parentIndex = this.parentIndex; // used for debugging?
296
+ //#ENDIF
297
+
298
+ return result;
299
+ }
300
+
301
+ /**
302
+ * Clear the nodeCache of this ExprPath, as well as all parent and child ExprPaths that
303
+ * share the same DOM parent node.
304
+ *
305
+ * TODO: Is recursive clearing ever necessary?
306
+ */
307
+ clearNodesCache() {
308
+ let path = this;
309
+
310
+ // Clear cache parent ExprPaths that have the same parentNode
311
+ let parentNode = this.parentNode;
312
+ while (path && path.parentNode === parentNode) {
313
+ path.nodesCache = null;
314
+ path = path.parentNg?.parentPath
315
+
316
+ // If stuck in an infinite loop here, the problem is likely due to Template hash colisions.
317
+ // Which cause one path to be the descendant of itself, creating a cycle.
318
+ }
319
+
320
+ function clearChildNodeCache(path) {
321
+
322
+ // Clear cache of child ExprPaths that have the same parentNode
323
+ for (let ng of path.nodeGroups) {
324
+ if (ng) // Can be null from applyOneExpr()'s push(null) call.
325
+ for (let path2 of ng.paths) {
326
+ if (path2.type === PathType.Content && path2.parentNode === parentNode) {
327
+ path2.nodesCache = null;
328
+ clearChildNodeCache(path2);
329
+ }
330
+ }
331
+ }
332
+ }
333
+
334
+ clearChildNodeCache(this);
335
+ }
336
+
337
+
338
+ /**
339
+ * Attempt to remove all of this ExprPath's nodes from the DOM, if it can be done using a special fast method.
340
+ * @returns {boolean} Returns false if Nodes werne't removed, and they should instead be removed manually. */
341
+ fastClear() {
342
+ let parent = this.nodeBefore.parentNode;
343
+ if (this.nodeBefore === parent.firstChild && this.nodeMarker === parent.lastChild) {
344
+
345
+ // If parent is the only child of the grandparent, replace the whole parent.
346
+ // And if it has no siblings, it's not created by a NodeGroup/path.
347
+ let grandparent = parent.parentNode
348
+ if (grandparent && parent === grandparent.firstChild && parent === grandparent.lastChild && !parent.hasAttribute('id')) {
349
+ let replacement = document.createElement(parent.tagName)
350
+ replacement.append(this.nodeBefore, this.nodeMarker)
351
+ for (let attrib of parent.attributes)
352
+ replacement.setAttribute(attrib.name, attrib.value)
353
+ parent.replaceWith(replacement)
354
+ }
355
+ else {
356
+ parent.innerHTML = ''; // Faster than calling .removeChild() a thousand times.
357
+ parent.append(this.nodeBefore, this.nodeMarker)
358
+ }
359
+ return true;
360
+ }
361
+ return false;
362
+ }
363
+
364
+ /**
365
+ * @return {(Node|HTMLElement)[]} */
366
+ getNodes() {
367
+
368
+ // Why doesn't this work?
369
+ // let result2 = [];
370
+ // for (let ng of this.nodeGroups)
371
+ // result2.push(...ng.getNodes())
372
+ // return result2;
373
+
374
+
375
+ let result
376
+
377
+ // This shaves about 5ms off the partialUpdate benchmark.
378
+ /*result = this.nodesCache;
379
+ if (result) {
380
+
381
+ //#IFDEV
382
+ this.checkNodesCache();
383
+ //#ENDIF
384
+
385
+ return result
386
+ }*/
387
+
388
+ result = [];
389
+ let current = this.nodeBefore.nextSibling;
390
+ let nodeMarker = this.nodeMarker;
391
+ while (current && current !== nodeMarker) {
392
+ result.push(current)
393
+ current = current.nextSibling
394
+ }
395
+
396
+ this.nodesCache = result;
397
+ return result;
398
+ }
399
+
400
+ getParentNode() { // Same as this.parentNode
401
+ return this.nodeMarker.parentNode
402
+ }
403
+
404
+ removeNodeGroup(ng) {
405
+ let idx = this.nodeGroups.indexOf(ng);
406
+ /*#IFDEV*/assert(idx !== -1);/*#ENDIF*/
407
+ this.nodeGroups.splice(idx);
408
+ ng.parentPath = null;
409
+ this.clearNodesCache();
410
+ }
411
+
412
+ //#IFDEV
413
+
414
+ get debug() {
415
+ return [
416
+ `parentNode: ${this.nodeBefore.parentNode?.tagName?.toLowerCase()}`,
417
+ 'nodes:',
418
+ ...setIndent(this.getNodes().map(item => {
419
+ if (item instanceof Node)
420
+ return item.outerHTML || item.textContent
421
+ else if (item instanceof NodeGroup)
422
+ return item.debug
423
+ }), 1).flat()
424
+ ]
425
+ }
426
+
427
+ get debugNodes() {
428
+ // Clear nodesCache so that getNodes() manually gets the nodes.
429
+ let nc = this.nodesCache;
430
+ this.nodesCache = null;
431
+ let result = this.getNodes()
432
+ this.nodesCache = nc;
433
+ return result;
434
+ }
435
+
436
+ verify() {
437
+ if (!window.verify)
438
+ return;
439
+
440
+ assert(this.type!==PathType.Content || this.nodeBefore)
441
+ assert(this.type!==PathType.Content || this.nodeBefore.parentNode)
442
+
443
+ // Need either nodeMarker or parentNode
444
+ assert(this.nodeMarker)
445
+
446
+ // nodeMarker must be attached.
447
+ assert(!this.nodeMarker || this.nodeMarker.parentNode)
448
+
449
+ // nodeBefore and nodeMarker must have same parent.
450
+ assert(this.type!==PathType.Content || this.nodeBefore.parentNode === this.nodeMarker.parentNode)
451
+
452
+ assert(this.nodeBefore !== this.nodeMarker)
453
+ assert(this.type!==PathType.Content|| !this.nodeBefore.parentNode || this.nodeBefore.compareDocumentPosition(this.nodeMarker) === Node.DOCUMENT_POSITION_FOLLOWING)
454
+
455
+ // Detect cyclic parent and grandparent references.
456
+ assert(this.parentNg?.parentPath !== this)
457
+ assert(this.parentNg?.parentPath?.parentNg?.parentPath !== this)
458
+ assert(this.parentNg?.parentPath?.parentNg?.parentPath?.parentNg?.parentPath !== this)
459
+
460
+ for (let ng of this.nodeGroups)
461
+ ng.verify();
462
+
463
+ // Make sure the nodesCache matches the nodes.
464
+ this.checkNodesCache();
465
+ }
466
+
467
+ checkNodesCache() {
468
+ return;
469
+
470
+ // Make sure cache is accurate.
471
+ // If this is invalid, then perhaps another component append()'d one of our nodes to itself.
472
+ // Or perhaps one of our nodes is used in an expression more than once.
473
+ // TODO: Find a way to check for and warn when this happens.
474
+ // MutationObserver is too slow since it's asynchronous.
475
+ // My own MutationWatcher has to modify DOM prototypes, which is rather invasive.
476
+ if (this.nodesCache) {
477
+ let nodes = [];
478
+ let current = this.nodeBefore.nextSibling;
479
+ let nodeMarker = this.nodeMarker;
480
+ while (current && current !== nodeMarker) {
481
+ nodes.push(current)
482
+ current = current.nextSibling
483
+ }
484
+ assert(findArrayDiff(this.nodesCache, nodes) === false);
485
+ }
486
+ }
487
+ //#ENDIF
488
+ }
489
+
490
+
491
+
492
+ /**
493
+ *
494
+ * @param root
495
+ * @param path {string[]}
496
+ * @param node {HTMLElement}
497
+ */
498
+ function setValue(root, path, node) {
499
+ let val = node.value;
500
+ if (node.type === 'number')
501
+ val = parseFloat(val);
502
+
503
+ delve(root, path, val);
504
+
505
+ //this.render();
506
+ }
507
+
508
+ /** @enum {string} */
509
+ export const PathType = {
510
+ /** Child of a node */
511
+ Content: 'content',
512
+
513
+ /** One or more whole attributes */
514
+ Multiple: 'attrName',
515
+
516
+ /** Value of an attribute. */
517
+ Value: 'attrValue',
518
+
519
+ /** Value of an attribute being passed to a component. */
520
+ Component: 'component',
521
+
522
+ /** Expressions inside Html comments. */
523
+ Comment: 'comment',
524
+ }
525
+
526
+
527
+ /** @return {int[]} Returns indices in reverse order, because doing it that way is faster. */
528
+ export function getNodePath(node) {
529
+ let result = [];
530
+ while(true) {
531
+ let parent = node.parentNode
532
+ if (!parent)
533
+ break;
534
+ result.push(Array.prototype.indexOf.call(node.parentNode.childNodes, node))
535
+ node = parent;
536
+ }
537
+ return result;
538
+ }
539
+
540
+ /**
541
+ * Note that the path is backward, with the outermost element at the end.
542
+ * @param root {HTMLElement|Document|DocumentFragment|ParentNode}
543
+ * @param path {int[]}
544
+ * @returns {Node|HTMLElement} */
545
+ export function resolveNodePath(root, path) {
546
+ for (let i=path.length-1; i>=0; i--)
547
+ root = root.childNodes[path[i]];
548
+ return root;
549
+ }
550
+
551
+
552
+ // TODO: Memory from this is never freed. Use a WeakMap<Node, Object<eventName:string, function[]>>
553
+ let nodeEvents = {};
554
+ let nodeEventArgs = {};
@@ -0,0 +1,65 @@
1
+ export default class MultiValueMap {
2
+
3
+ /** @type {Object<string, Set>} */
4
+ data = {};
5
+
6
+ // Set a new value for a key
7
+ add(key, value) {
8
+ let data = this.data;
9
+ let set = data[key]
10
+ if (!set) {
11
+ set = new Set();
12
+ data[key] = set;
13
+ }
14
+ set.add(value);
15
+ }
16
+
17
+ // Get all values for a key
18
+ getAll(key) {
19
+ return this.data[key] || [];
20
+ }
21
+
22
+ // Remove one value from a key, and return it
23
+ delete(key, val=undefined) {
24
+ // if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
25
+ // debugger;
26
+
27
+ let data = this.data;
28
+ // The partialUpdate benchmark shows having this check first makes the function slightly faster.
29
+ // if (!data.hasOwnProperty(key))
30
+ // return undefined;
31
+
32
+ // Delete a specific value.
33
+ let result;
34
+ let set = data[key];
35
+ if (!set) // slower than pre-check.
36
+ return undefined;
37
+
38
+ if (val !== undefined) {
39
+ set.delete(val);
40
+ result = val;
41
+ }
42
+
43
+ // Delete any value.
44
+ else {
45
+ result = set.values().next().value;
46
+ // [result] = set; // Does the same as above. is about the same speed?
47
+ set.delete(result);
48
+ }
49
+
50
+ // TODO: Will this make it slower?
51
+ if (set.size === 0)
52
+ delete data[key];
53
+
54
+ return result;
55
+ }
56
+
57
+ hasValue(val) {
58
+ let data = this.data;
59
+ let names = [];
60
+ for (let name in data)
61
+ if (data[name].has(val)) // TODO: iterate twice to pre-size array?
62
+ names.push(name)
63
+ return names;
64
+ }
65
+ }