solarite 0.2.4 → 0.3.1

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 (38) hide show
  1. package/dist/Solarite-debug.js +1457 -1402
  2. package/dist/Solarite.js +1425 -1272
  3. package/dist/Solarite.min.js +2 -2
  4. package/package.json +5 -6
  5. package/readme.md +2 -4
  6. package/src/{solarite/ExprPath.js → ExprPath.js} +421 -231
  7. package/src/Globals.js +79 -0
  8. package/src/HtmlParser.js +91 -0
  9. package/src/{util/MultiValueMap.js → MultiValueMap.js} +22 -26
  10. package/src/{solarite/NodeGroup.js → NodeGroup.js} +286 -226
  11. package/src/{solarite/Shell.js → Shell.js} +119 -92
  12. package/src/Solarite.d.ts +62 -0
  13. package/src/{solarite/Solarite.js → Solarite.js} +15 -13
  14. package/src/{solarite/Template.js → Template.js} +22 -19
  15. package/src/Util.js +330 -0
  16. package/src/{util/Errors.js → assert.js} +1 -0
  17. package/src/createSolarite.js +154 -0
  18. package/src/{util/delve.js → delve.js} +5 -4
  19. package/src/{solarite/getArg.js → getArg.js} +41 -15
  20. package/src/{solarite/r.js → h.js} +59 -29
  21. package/src/{solarite/hash.js → hash.js} +12 -9
  22. package/src/unused/FastLookupArray.js +54 -0
  23. package/src/unused/Hashes.js +339 -0
  24. package/src/unused/InUse.test.js +92 -0
  25. package/src/unused/InUseMap.js +98 -0
  26. package/src/unused/LinkedList.js +117 -0
  27. package/src/unused/LinkedList.test.js +115 -0
  28. package/src/unused/Misc.js +13 -0
  29. package/src/unused/Perf.js +47 -0
  30. package/src/unused/TrackedArray.js +54 -0
  31. package/src/watch.js +546 -0
  32. package/src/solarite/Globals.js +0 -54
  33. package/src/solarite/Util.js +0 -388
  34. package/src/solarite/createSolarite.js +0 -274
  35. package/src/solarite/watch3.js +0 -189
  36. package/src/util/Util.js +0 -113
  37. /package/src/{solarite/udomdiff.js → udomdiff.js} +0 -0
  38. /package/src/{util → unused}/WeakArray.js +0 -0
package/src/watch.js ADDED
@@ -0,0 +1,546 @@
1
+ /**
2
+ *
3
+ *
4
+ * TODO:
5
+ * 1. Have option to automatically render?
6
+ *
7
+ * Limitations:
8
+ * 1. If we use one path to get a property during render, but a different path to set it, it will not be marked for rendering.
9
+ *
10
+ */
11
+
12
+
13
+
14
+ // Example:
15
+ /*
16
+ class WatchExample extends Solarite {
17
+
18
+ constructor(items = []) {
19
+ super();
20
+
21
+ this.items = items;
22
+ watch(this, 'items');
23
+
24
+ this.name = 'George';
25
+ watch(this, 'name');
26
+
27
+ this.render();
28
+ }
29
+
30
+ render() {
31
+ r(this)`
32
+ <watch-example>
33
+ ${() => this.name + '!'}
34
+
35
+ ${this.items.map(item => r`
36
+ <div>${item.name}</div>
37
+ `)}
38
+ ${() => this.items.length}
39
+ </watch-example>`;
40
+ }
41
+ }
42
+ customElements.define('watch-example', WatchExample);
43
+
44
+ let a = new WatchExample();
45
+
46
+ // Items is a Proxy.
47
+ // Calling push() will trigger the map'd ExprPaths to add another at the end.
48
+ // And the .length expression to update.
49
+ // Because accessing .items returns a proxy.
50
+ a.items.push({name: 'Fred'});
51
+ */
52
+
53
+ import Globals from "./Globals.js";
54
+ import Util from "./Util.js";
55
+ import {assert} from "./assert.js";
56
+
57
+ let unusedArg = Symbol('unusedArg');
58
+
59
+
60
+
61
+ function removeProxy(obj) {
62
+ if (obj && obj.$removeProxy)
63
+ return obj.$removeProxy;
64
+ return obj;
65
+ }
66
+
67
+ /**
68
+ * Render the ExprPaths that were added to rootNg.exprsToRender.
69
+ * @param root {HTMLElement}
70
+ * @param trackModified {boolean}
71
+ * @returns {Node[]} Modified elements. */
72
+ export function renderWatched(root, trackModified=false) {
73
+ let rootNg = Globals.nodeGroups.get(root);
74
+ let modified;
75
+
76
+ if (trackModified)
77
+ modified = new Set();
78
+
79
+
80
+ // Mark NodeGroups of expressionpaths as freed.
81
+ // for (let [exprPath, ops] of rootNg.exprsToRender) {
82
+ // if (ops instanceof WholeArrayOp) {}
83
+ // else if (ops instanceof ValueOp) {}
84
+ // else {} // Array Slice Op
85
+ // }
86
+
87
+ for (let [exprPath, ops] of rootNg.exprsToRender) {
88
+
89
+ // Reapply the whole expression.
90
+ if (ops instanceof WholeArrayOp) {
91
+
92
+ // So it doesn't use the old value inside the map callback in the get handler above.
93
+ // TODO: Find a more sensible way to pass newValue.
94
+ ops.markNodeGroupsAvailable(exprPath);
95
+ exprPath.watchFunction.newValue = ops.array;
96
+ exprPath.apply([exprPath.watchFunction], false);
97
+
98
+ //exprPath.freeNodeGroups();
99
+
100
+ if (trackModified)
101
+ modified.add(...exprPath.getNodes());
102
+ }
103
+
104
+ // Update a single value in a map callback
105
+ // TODO: Why is this not an array of ops?
106
+ else if (ops instanceof ValueOp) {
107
+
108
+ // TODO: I need to only free node groups of watched expressions.
109
+ exprPath.watchFunction.newValue = ops.value;
110
+ exprPath.apply([exprPath.watchFunction], false); // False to not free nodeGroups.
111
+
112
+ //exprPath.freeNodeGroups();
113
+
114
+ if (trackModified)
115
+ modified.add(...exprPath.getNodes());
116
+ }
117
+
118
+ // Selectively update NodeGroups created by array.map()
119
+ else {
120
+
121
+ for (let i = 0; i < ops.length; i++) {
122
+ let op = ops[i];
123
+ let nextOp = ops[i + 1];
124
+
125
+ // If we have two Adjacent ArraySpliceOps that swap eachother's items,
126
+ // then be fast by directly swap their DOM nodes.
127
+ if (nextOp instanceof ArraySpliceOp && nextOp.deleteCount === 1 && nextOp.items.length === 1
128
+ && op instanceof ArraySpliceOp && op.deleteCount === 1 && op.items.length === 1
129
+ && nextOp.array[nextOp.index] === op.firstDeleted
130
+ && op.array[op.index] === nextOp.firstDeleted
131
+ ) {
132
+
133
+ let nga = exprPath.nodeGroups[op.index];
134
+ let ngb = exprPath.nodeGroups[nextOp.index];
135
+
136
+ // Swap the nodegroup nga and ngb node positions
137
+ let nextA = nga.endNode.nextSibling;
138
+ let nextB = ngb.endNode.nextSibling;
139
+ for (let node of nga.getNodes()) // TODO: Manually iterate instead of calling getNodes().
140
+ node.parentNode.insertBefore(node, nextB);
141
+ for (let node of ngb.getNodes())
142
+ node.parentNode.insertBefore(node, nextA);
143
+
144
+ /*
145
+ // replaceWidth version:
146
+ let nextB = ngb.endNode.nextSibling;
147
+
148
+ let ngaNodes = nga.getNodes();
149
+ let ngbNodes = ngb.getNodes();
150
+ let len = Math.min(ngaNodes.length, ngbNodes.length);
151
+
152
+ for (let i=0; i< len; i++)
153
+ ngaNodes[i].replaceWith(ngbNodes[i]);
154
+ // TODO: Insert additional nodes here.
155
+ for (let node of nga.getNodes())
156
+ nextB.parentNode.insertBefore(node, nextB);
157
+ */
158
+
159
+ exprPath.nodeGroups[op.index] = ngb;
160
+ exprPath.nodeGroups[nextOp.index] = nga;
161
+
162
+ if (trackModified) {
163
+ nga.getNodes().map(n => modified.add(n));
164
+ ngb.getNodes().map(n => modified.add(n));
165
+ }
166
+ i++;// skip next op
167
+ }
168
+
169
+ // ArraySpliceOp
170
+ else { // (op instanceof ArraySpliceOp) {
171
+
172
+ if (trackModified && op.deleteCount)
173
+ modified.add(
174
+ ...exprPath.nodeGroups.slice(op.index, op.index + op.deleteCount).map(ng => ng.getNodes()).flat()
175
+ );
176
+
177
+ op.markNodeGroupsAvailable(exprPath);
178
+ exprPath.applyArrayOp(op);
179
+
180
+ if (trackModified && op.items.length) {
181
+ exprPath.nodeGroups.slice(op.index, op.index + op.items.length)
182
+ .map(ng => ng.getNodes())
183
+ .flat()
184
+ .map(n => modified.add(n));
185
+ }
186
+ }
187
+ }
188
+ }
189
+ }
190
+ rootNg.exprsToRender = new Map(); // clear
191
+
192
+ if (trackModified)
193
+ return [...modified];
194
+ }
195
+
196
+
197
+
198
+
199
+
200
+
201
+
202
+
203
+
204
+
205
+
206
+
207
+
208
+ /**
209
+ * Passed as an argument when creating a new Proxy().
210
+ * Handles getting and setting properties on the proxied object. */
211
+ class ProxyHandler {
212
+
213
+ /** @type {Record<string, [Proxy, ProxyHandler]>} Proxies for child properties. */
214
+ proxies = {}
215
+
216
+ /** @type Set<ExprPath> ExprPaths that will need to be re-rendered when this variable is modified. */
217
+ exprPaths = new Set();
218
+
219
+ /**
220
+ * ExprPaths that will need to be re-rendered when one of this variable's primitive properties is modified,
221
+ * since primitives can't have their own ProxyHandler.
222
+ * @type {Record<prop:string, affected:Set<ExprPath>>} */
223
+ childExprPaths = {};
224
+
225
+
226
+ constructor(root, value) {
227
+
228
+ /** @type {Object} The top level object being proxied. */
229
+ this.root = root;
230
+
231
+ /** @type {*} the value found when starting at root and following the path? */
232
+ this.value = value;
233
+
234
+ /** @type {RootNodeGroup} Cached, to save time on lookups. */
235
+ this.rootNodeGroup = null;
236
+ }
237
+
238
+ /**
239
+ * Get a cached proxy of a sub-property.
240
+ * @param prop {string}
241
+ * @param val {*}
242
+ * @returns {[Proxy, ProxyHandler]} */
243
+ getProxyandHandler(prop, val) {
244
+ let result = this.proxies[prop];
245
+ if (!result) {
246
+ let handler = new ProxyHandler(this.root, this.value);
247
+ result = this.proxies[prop] = [new Proxy(val, handler), handler];
248
+ }
249
+ return result;
250
+ }
251
+
252
+ /**
253
+ * We override get() so we can mark which ExprPaths read from each variable in the hierarchy.
254
+ * Then later when we call set on a variable, we can see which ExprPaths use it, and can mark them to be re-rendered.
255
+ * @param obj
256
+ * @param prop {string}
257
+ * @param receiver
258
+ * @returns {*|Proxy|(function(*): function(): any)} */
259
+ get(obj, prop, receiver) {
260
+
261
+ if (prop === '$removeProxy')
262
+ return obj;
263
+
264
+ // if (prop === 'items')
265
+ // debugger;
266
+
267
+ const result = (obj === receiver)
268
+ ? this.value // top-level value.
269
+ : Reflect.get(obj, prop, receiver); // avoid infinite recursion.
270
+
271
+ // We override the map() function the first time render() is called.
272
+ // But it's not re-overridden when we call renderWatched()
273
+ if (Array.isArray(obj)) {
274
+
275
+ if (prop === 'map') {
276
+
277
+ const self = this;
278
+
279
+ // This outer function is so the ExprPath calls it as a function,
280
+ // instead of it being evaluated immediately when the Template is created.
281
+ // This allows ExprPath.apply() to set the Globals.currentExprPath before evaluating further.
282
+ return (callback) =>
283
+
284
+ // This is the new map function.
285
+ function mapFunction() {
286
+
287
+ // Save the ExprPaths that called the array used by .map()
288
+ const currExprPath = Globals.currentExprPath;
289
+ if (currExprPath)
290
+ self.exprPaths.add(currExprPath);
291
+
292
+ // Apply the map function.
293
+ const newObj = mapFunction.newValue || obj;
294
+ Globals.currentExprPath.mapCallback = callback;
295
+ // If new Proxy fails b/c newObj isn't an object, make sure the expression is a function.
296
+ // TODO: Find a way to warn about this automatically.
297
+ let p = new Proxy(newObj, self);
298
+ return Array.prototype.map.call(p, callback);
299
+ }
300
+ }
301
+
302
+ else if (prop === 'push' || prop==='pop' || prop === 'splice') {
303
+ const rootNg = Globals.nodeGroups.get(this.root);
304
+ return new WatchedArray(rootNg, obj, this.exprPaths)[prop];
305
+ }
306
+ }
307
+
308
+
309
+ // Save the ExprPath that's currently accessing this variable.
310
+ const currExprPath = Globals.currentExprPath;
311
+
312
+ // Accessing a sub-property
313
+ if (result && typeof result === 'object') {
314
+ let [proxiedResult, handler] = this.getProxyandHandler(prop, result); // Clone this handler and append prop to the path.
315
+
316
+ if (currExprPath && prop !== 'constructor')
317
+ handler.exprPaths.add(currExprPath);
318
+
319
+ return proxiedResult;
320
+ }
321
+ else {
322
+ if (currExprPath && prop !== 'constructor') {
323
+
324
+ // We can't have Proxies on primitive types,
325
+ // So we store the affected expressions in the parent Proxy.
326
+ if (!this.childExprPaths[prop])
327
+ this.childExprPaths[prop] = new Set([currExprPath]);
328
+ else
329
+ this.childExprPaths[prop].add(currExprPath);
330
+ }
331
+
332
+ return result;
333
+ }
334
+ }
335
+
336
+ // TODO: Will fail for attribute w/ a value having multiple ExprPaths.
337
+ // TODO: This won't update a component's expressions.
338
+ set(obj, prop, val, receiver) {
339
+
340
+ val = removeProxy(val);
341
+
342
+ // 1. Add to the list of ExprPaths to re-render.
343
+ if (!this.rootNodeGroup)
344
+ this.rootNodeGroup = Globals.nodeGroups.get(this.root);
345
+ const rootNg = this.rootNodeGroup
346
+
347
+ // New: // TODO: Should I instead be checking if the old value of val is a primitive?
348
+ let isPrimitive = !val || typeof val !== 'object';
349
+ let exprPaths = isPrimitive
350
+ ? this.childExprPaths[prop] || []
351
+ : this.getProxyandHandler(prop, val)[1].exprPaths;
352
+
353
+ const isArray = Array.isArray(obj);
354
+ for (let exprPath of exprPaths) {
355
+
356
+ if (isArray) {
357
+ if (Number.isInteger(+prop)) {
358
+ const exprsToRender = rootNg.exprsToRender.get(exprPath);
359
+
360
+ // If we're not re-rendering the whole thing.
361
+ if (!(exprsToRender instanceof WholeArrayOp))
362
+ // TODO: Inline this for performance
363
+ Util.mapArrayAdd(rootNg.exprsToRender, exprPath, new ArraySpliceOp(obj, prop, 1, [val]));
364
+ }
365
+
366
+ // Reapply the whole expression.
367
+ else
368
+ rootNg.exprsToRender.set(exprPath, new WholeArrayOp(val));
369
+ }
370
+ else
371
+ rootNg.exprsToRender.set(exprPath, new ValueOp(val));
372
+ }
373
+
374
+ // 2. Set the value.
375
+ if (obj === receiver)
376
+ this.value = val; // top-level value.
377
+ else // Set the value while avoiding infinite recursion.
378
+ Reflect.set(obj, prop, val, receiver);
379
+
380
+ // Value changed, so reset cached proxy.
381
+ if (val && typeof val === 'object')
382
+ delete this.proxies[prop];
383
+
384
+ return true;
385
+ }
386
+ }
387
+
388
+ /**
389
+ * This function markes a property of a web component to be watched for changes.
390
+ *
391
+ * Here is how watches work:
392
+ * 1. When we call watch() it creates properties on the root object that return Proxies to watch when values are set.
393
+ * 2. When they are set, we add their paths to the rootNodeGroup.exprsToRender that keeps track of what to re-render.
394
+ * 3. Then we call renderWatched() to re-render only those parts.
395
+ *
396
+ * In more detail:
397
+ * TODO
398
+ *
399
+ *
400
+ * @param root {HTMLElement} An instance of a Web Component that uses r() to render its content.
401
+ * @param field {string} The name of a top-level property of root.
402
+ * @param value {string|Symbol} The default value. */
403
+ export default function watch(root, field, value=unusedArg) {
404
+ // Store internal value used by get/set.
405
+ if (value !== unusedArg)
406
+ root[field] = value;
407
+ else
408
+ value = root[field];
409
+
410
+ let handler = new ProxyHandler(root, value);
411
+ Object.defineProperty(root, field, {
412
+ get: () => handler.get(root, field, root),
413
+ set: (val) => handler.set(root, field, val, root)
414
+ });
415
+ }
416
+ export {watch};
417
+
418
+
419
+ /**
420
+ * Wrap an array so that functions that modify the array are intercepted.
421
+ * We then add ArraySpliceOp's to the list of ops to run for each affected ExprPath.
422
+ * When renderWatched() is called it then applies those ops to the NodeGroups created by the map() function. */
423
+ class WatchedArray {
424
+
425
+ /**
426
+ * @param array {Array}
427
+ * @param rootNg {RootNodeGroup}
428
+ * @param exprPaths {ExprPath[]|Set<ExprPath>} Expression paths that use this array. */
429
+ constructor(rootNg, array, exprPaths) {
430
+ this.rootNg = rootNg;
431
+ //#IFDEV
432
+ assert(Array.isArray(array));
433
+ //#ENDIF
434
+ this.array = array;
435
+ this.exprPaths = exprPaths;
436
+ this.push = this.push.bind(this);
437
+ this.pop = this.pop.bind(this);
438
+ this.splice = this.splice.bind(this);
439
+ }
440
+
441
+ push(...args) {
442
+ return this.internalSplice('push', args, [this.array, this.array.length, 0, args]);
443
+ }
444
+
445
+ pop() {
446
+ if (this.array.length)
447
+ return this.internalSplice('pop', [], [this.array, this.array.length-1, 1]);
448
+ }
449
+
450
+ splice(...args) {
451
+ return this.internalSplice('splice', args, [this.array, ...args]);
452
+ }
453
+
454
+ internalSplice(func, args, spliceArgs) {
455
+ // Mark all expressions affected by the array function to be re-rendered
456
+ for (let exprPath of this.exprPaths) {
457
+ let exprsToRender = this.rootNg.exprsToRender.get(exprPath);
458
+ if (!(exprsToRender instanceof WholeArrayOp)) // If we're not already going to re-render the whole array.
459
+ Util.mapArrayAdd(this.rootNg.exprsToRender, exprPath, new ArraySpliceOp(...spliceArgs));
460
+ }
461
+
462
+ // Call original array function
463
+ return Array.prototype[func].call(this.array, ...args);
464
+ }
465
+ }
466
+
467
+
468
+
469
+ class WatchOp {}
470
+
471
+ export class ArraySpliceOp extends WatchOp {
472
+
473
+ /**
474
+ * Represents a splice operation (insertion, deletion, or replacement of elements)
475
+ * to be applied to an array during rendering.
476
+ *
477
+ * @param array {Array} The array affected by the splice operation.
478
+ * @param index {int} The starting index of the splice operation.
479
+ * @param deleteCount {int} The number of elements to delete from the array.
480
+ * @param items {Array} The elements to insert into the array at the starting index. */
481
+ constructor(array, index, deleteCount, items=[]) {
482
+ super();
483
+ //#IFDEV
484
+ assert(Array.isArray(array));
485
+ //#ENDIF
486
+ this.array = array;
487
+ this.index = index*1;
488
+ this.deleteCount = deleteCount;
489
+ this.items = items;
490
+
491
+ // Save the first item deleted so we can see if this should be turned into an ArraySwapOp later.
492
+ this.firstDeleted = deleteCount===1 ? array[index] : undefined;
493
+ }
494
+
495
+ markNodeGroupsAvailable(exprPath) {
496
+ if (this.deleteCount > 0) {
497
+ let count = this.index+this.deleteCount;
498
+ for (let i=this.index; i<count; i++) {
499
+ let oldNg = exprPath.nodeGroups[i];
500
+ exprPath.nodeGroupsAttachedAvailable.add(oldNg.exactKey, oldNg);
501
+ exprPath.nodeGroupsAttachedAvailable.add(oldNg.closeKey, oldNg);
502
+ }
503
+ }
504
+ }
505
+ }
506
+
507
+ class ValueOp extends WatchOp {
508
+ constructor(value) {
509
+ super();
510
+ this.value = value;
511
+ }
512
+
513
+ markNodeGroupsAvailable(exprPath) {}
514
+ }
515
+
516
+ // We detect such ops but we never need to instantiate this class.
517
+ // class ArraySwapOp extends WatchOp {
518
+ // constructor(array, index1, index2) {
519
+ // super();
520
+ // //#IFDEV
521
+ // assert(Array.isArray(array));
522
+ // //#ENDIF
523
+ // this.array = array;
524
+ // this.index1 = index1;
525
+ // this.index2 = index2;
526
+ // }
527
+ // }
528
+
529
+ class WholeArrayOp extends WatchOp {
530
+ constructor(array, value) {
531
+ super();
532
+ //#IFDEV
533
+ assert(Array.isArray(array));
534
+ //#ENDIF
535
+ this.array = array;
536
+ this.value = value;
537
+ }
538
+
539
+ markNodeGroupsAvailable(exprPath) {
540
+ for (let i=0; i<exprPath.nodeGroups.length; i++) {
541
+ let oldNg = exprPath.nodeGroups[i];
542
+ exprPath.nodeGroupsAttachedAvailable.add(oldNg.exactKey, oldNg);
543
+ exprPath.nodeGroupsAttachedAvailable.add(oldNg.closeKey, oldNg);
544
+ }
545
+ }
546
+ }
@@ -1,54 +0,0 @@
1
- var Globals = {
2
-
3
- /**
4
- * Used by NodeGroup.applyComponentExprs() */
5
- componentHash: new WeakMap(),
6
-
7
- /**
8
- * Store which instances of Solarite have already been added to the DOM.
9
- * @type {WeakSet<HTMLElement>} */
10
- connected: new WeakSet(),
11
-
12
- /**
13
- * Elements that have been rendered to by r() at least once.
14
- * This is used by the Solarite class to know when to call onFirstConnect()
15
- * @type {WeakSet<HTMLElement>} */
16
- rendered: new WeakSet(),
17
-
18
- /**
19
- * Used by watch3 to see which expressions are being accessed.
20
- * @type {[]}*/
21
- currentExprPath: null,
22
-
23
- /**
24
- * @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
25
- elementClasses: {},
26
-
27
- /**
28
- * Used by ExprPath.applyEventAttrib()
29
- * @type {WeakMap<Node, Object<eventName:string, [original:function, bound:function, args:*[]]>>} */
30
- nodeEvents: new WeakMap(),
31
-
32
- /**
33
- * Get the RootNodeGroup for an element.
34
- * @type {WeakMap<HTMLElement, RootNodeGroup>} */
35
- nodeGroups: new WeakMap(),
36
-
37
- /**
38
- * Used by r() path 9. */
39
- objToEl: new WeakMap(),
40
-
41
- pendingChildren: [],
42
-
43
- /**
44
- * Elements that are currently rendering via the r() function.
45
- * @type {WeakSet<HTMLElement>} */
46
- rendering: new WeakSet(),
47
-
48
- /**
49
- * Map from array of Html strings to a Shell created from them.
50
- * @type {WeakMap<string[], Shell>} */
51
- shells: new WeakMap()
52
- };
53
-
54
- export default Globals;