solarite 0.8.0 → 0.9.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/dist/Solarite-debug.js +340 -166
- package/dist/Solarite.js +339 -165
- package/dist/Solarite.min.js +2 -2
- package/dist/jsx-dev-runtime.min.js +3 -0
- package/dist/jsx-runtime.min.js +2 -0
- package/package.json +2 -2
- package/readme.md +6 -6
- package/src/Globals.js +22 -2
- package/src/NodeGroup.js +5 -5
- package/src/PathToAttribValue.js +122 -56
- package/src/PathToComponent.js +104 -90
- package/src/PathToEvent.js +16 -2
- package/src/RootNodeGroup.js +22 -5
- package/src/Shell.js +37 -6
- package/src/Solarite.d.ts +14 -12
- package/src/Util.js +13 -0
package/dist/Solarite-debug.js
CHANGED
|
@@ -21,8 +21,17 @@ function reset() {
|
|
|
21
21
|
connected: new WeakSet(),
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* A hand-off in flight from PathToComponent.applyAll(), which parks the child nodes
|
|
25
|
+
* declared inside a component's tag here just before constructing it, to that
|
|
26
|
+
* component's RootNodeGroup.instantiate(), which puts them in its <slot>. Null when
|
|
27
|
+
* no hand-off is pending.
|
|
28
|
+
*
|
|
29
|
+
* It is addressed by Constructor rather than by tag name because a customized
|
|
30
|
+
* built-in has no usable tag at the moment it is consumed: a <tr is="my-row">
|
|
31
|
+
* reports a tagName of TR, and its 'is' attribute is not written until after the
|
|
32
|
+
* constructor -- which may already have rendered -- has returned.
|
|
33
|
+
*
|
|
34
|
+
* @type {?{Constructor:Function, nodes:Node[]}} */
|
|
26
35
|
currentSlotChildren: null,
|
|
27
36
|
|
|
28
37
|
div: document.createElement("div"),
|
|
@@ -62,6 +71,17 @@ function reset() {
|
|
|
62
71
|
}
|
|
63
72
|
reset();
|
|
64
73
|
|
|
74
|
+
// Warn when a second copy of Solarite loads into the same page. Each copy has its own classes and its own Globals,
|
|
75
|
+
// so a template or component made by one is not recognised by the other, and the failure that follows (a template
|
|
76
|
+
// rendered as "[object Object]", a slot that stays empty) gives no hint of the cause. The usual ways to get two are
|
|
77
|
+
// importing both Solarite.js and Solarite.min.js, or a JSX runtime file that doesn't match the build being imported.
|
|
78
|
+
// The marker is the same for every build, so the source, debug, and minified builds all detect one another.
|
|
79
|
+
let copy = Symbol.for('solarite');
|
|
80
|
+
if (globalThis[copy])
|
|
81
|
+
console.warn(`Solarite loaded twice: ${globalThis[copy]} and ${import.meta.url}. Templates and components from one won't work in the other.`);
|
|
82
|
+
else
|
|
83
|
+
globalThis[copy] = import.meta.url;
|
|
84
|
+
|
|
65
85
|
var Globals$1 = Globals;
|
|
66
86
|
|
|
67
87
|
/**
|
|
@@ -121,6 +141,12 @@ function isDelvePath(arr) {
|
|
|
121
141
|
// d means "don't create"
|
|
122
142
|
let d = {};
|
|
123
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Prefix that asks for a handler to bypass event delegation: `<button native:onclick=\${...}>`
|
|
146
|
+
* is bound with addEventListener at render time, taking its normal place in the browser's own
|
|
147
|
+
* dispatch order. Shared by Util.isEvent() and PathToEvent, which strips it. */
|
|
148
|
+
const nativeEventPrefix = 'native:';
|
|
149
|
+
|
|
124
150
|
let Util = {
|
|
125
151
|
|
|
126
152
|
/**
|
|
@@ -320,7 +346,14 @@ let Util = {
|
|
|
320
346
|
return node.value; // String
|
|
321
347
|
},
|
|
322
348
|
|
|
349
|
+
/**
|
|
350
|
+
* True for an attribute name that binds an event: `onclick`, or `native:onclick` for a
|
|
351
|
+
* handler that is registered with addEventListener when the template renders instead of
|
|
352
|
+
* being delegated. Only names an element really exposes as on* handlers count, so an
|
|
353
|
+
* attribute like `online` is never mistaken for one. */
|
|
323
354
|
isEvent(attribName) {
|
|
355
|
+
if (attribName.startsWith(nativeEventPrefix))
|
|
356
|
+
attribName = attribName.slice(nativeEventPrefix.length);
|
|
324
357
|
return attribName.startsWith('on') && attribName in Globals$1.div;
|
|
325
358
|
},
|
|
326
359
|
|
|
@@ -1190,17 +1223,20 @@ class PathToAttribValue extends Path {
|
|
|
1190
1223
|
|
|
1191
1224
|
// Delegated path: a bubbling event (when the root's options allow it, the default)
|
|
1192
1225
|
// stores its handler directly on the node as a per-event-type Symbol expando, with no
|
|
1193
|
-
// EventBinding object and no addEventListener call.
|
|
1194
|
-
//
|
|
1195
|
-
// the
|
|
1196
|
-
//
|
|
1226
|
+
// EventBinding object and no addEventListener call. When an event of that type
|
|
1227
|
+
// starts, jitDispatcher() attaches a real listener to each node on its path that
|
|
1228
|
+
// carries the expando, so the browser runs the handler at the node's own turn.
|
|
1229
|
+
// Re-renders just overwrite the property. this.delegatedKey is set by the PathToEvent
|
|
1230
|
+
// constructor only for delegatable event names, so this test also excludes
|
|
1231
|
+
// non-bubbling events and native:on* bindings.
|
|
1197
1232
|
if (capture === false && this.delegatedKey !== undefined) {
|
|
1198
1233
|
let opt = this.parentNg.rootNg.renderOptions?.eventDelegation ?? true;
|
|
1199
|
-
|
|
1200
|
-
|
|
1234
|
+
// true delegates everything, an array only the events it names, and any other
|
|
1235
|
+
// value (such as the retired 'document' string) counts as true.
|
|
1236
|
+
if (opt !== false && (!Array.isArray(opt) || opt.includes(eventName))) {
|
|
1201
1237
|
let dk = this.delegatedKey;
|
|
1202
1238
|
if (node[dk] === undefined) // First binding of this type on this node.
|
|
1203
|
-
ensureDelegatedDispatcher(root, eventName
|
|
1239
|
+
ensureDelegatedDispatcher(root, eventName);
|
|
1204
1240
|
// Array-form bindings (onclick=${[fn, arg]}, the hot per-row case) store the
|
|
1205
1241
|
// template's own [func, ...args] array; a plain function is stored bare.
|
|
1206
1242
|
// Either way, nothing is allocated.
|
|
@@ -1272,7 +1308,7 @@ function getEventBinding(node, key) {
|
|
|
1272
1308
|
return b instanceof EventBinding ? (b.key === key ? b : undefined) : b[key];
|
|
1273
1309
|
}
|
|
1274
1310
|
|
|
1275
|
-
// Bubbling events
|
|
1311
|
+
// Bubbling events the just-in-time dispatcher handles. Same set Solid.js delegates.
|
|
1276
1312
|
const delegatableEvents = new Set(['beforeinput', 'click', 'contextmenu', 'dblclick', 'focusin', 'focusout',
|
|
1277
1313
|
'input', 'keydown', 'keyup', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup',
|
|
1278
1314
|
'pointerdown', 'pointermove', 'pointerout', 'pointerover', 'pointerup', 'touchend', 'touchmove', 'touchstart']);
|
|
@@ -1296,79 +1332,142 @@ function delegatedKeyFor(eventName) {
|
|
|
1296
1332
|
// Exported so NodeGroup.applyStamp()'s compiled stamp program can write it directly.
|
|
1297
1333
|
const delegatedRootKey = Symbol('solariteDelegatedRoot');
|
|
1298
1334
|
|
|
1299
|
-
//
|
|
1335
|
+
// Set of event types that already have the dispatcher registered, kept on each root element
|
|
1336
|
+
// and on each document.
|
|
1300
1337
|
const delegatedTypesKey = Symbol('solariteDelegatedTypes');
|
|
1301
1338
|
|
|
1302
1339
|
/**
|
|
1303
|
-
* Register the
|
|
1304
|
-
* Shared by bindEvent()'s delegated branch and NodeGroup.applyStamp()'s stamp program.
|
|
1340
|
+
* Register the just-in-time dispatcher for eventName on root and on root's document, once
|
|
1341
|
+
* each. Shared by bindEvent()'s delegated branch and NodeGroup.applyStamp()'s stamp program.
|
|
1305
1342
|
*
|
|
1306
|
-
*
|
|
1307
|
-
*
|
|
1308
|
-
*
|
|
1309
|
-
*
|
|
1310
|
-
*
|
|
1343
|
+
* Both registrations are needed. The document's listener is what still reaches a bound node
|
|
1344
|
+
* after another component re-parents it outside its root (a toolbar a dock parks in its own
|
|
1345
|
+
* chrome). The root's listener is what reaches what the document cannot see: a component
|
|
1346
|
+
* that isn't in the document at all, nodes inside a closed shadow root, and a synthetic
|
|
1347
|
+
* event dispatched inside any shadow root without composed:true, which never leaves it.
|
|
1311
1348
|
* @param root {HTMLElement}
|
|
1312
|
-
* @param eventName {string}
|
|
1313
|
-
|
|
1314
|
-
function ensureDelegatedDispatcher(root, eventName, andDocument=false) {
|
|
1349
|
+
* @param eventName {string} */
|
|
1350
|
+
function ensureDelegatedDispatcher(root, eventName) {
|
|
1315
1351
|
let types = root[delegatedTypesKey];
|
|
1316
1352
|
if (types === undefined)
|
|
1317
1353
|
types = root[delegatedTypesKey] = new Set();
|
|
1318
1354
|
if (!types.has(eventName)) {
|
|
1319
1355
|
types.add(eventName);
|
|
1320
|
-
root.addEventListener(eventName,
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
let doc = root.ownerDocument ?? document;
|
|
1356
|
+
root.addEventListener(eventName, jitDispatcher, true);
|
|
1357
|
+
|
|
1358
|
+
let doc = root.ownerDocument;
|
|
1324
1359
|
let docTypes = doc[delegatedTypesKey];
|
|
1325
1360
|
if (docTypes === undefined)
|
|
1326
1361
|
docTypes = doc[delegatedTypesKey] = new Set();
|
|
1327
1362
|
if (!docTypes.has(eventName)) {
|
|
1328
1363
|
docTypes.add(eventName);
|
|
1329
|
-
doc.addEventListener(eventName,
|
|
1364
|
+
doc.addEventListener(eventName, jitDispatcher, true);
|
|
1330
1365
|
}
|
|
1331
1366
|
}
|
|
1332
1367
|
}
|
|
1333
1368
|
|
|
1334
|
-
//
|
|
1335
|
-
//
|
|
1369
|
+
// Set on an event by the first dispatcher to walk it, holding the length of the path it saw,
|
|
1370
|
+
// so the dispatchers on nested roots further down don't repeat the walk. A root inside a
|
|
1371
|
+
// closed shadow root sees a longer path than the document did, because composedPath() hides
|
|
1372
|
+
// a closed tree from listeners outside it, and that mismatch is what makes it walk again.
|
|
1336
1373
|
const delegatedDoneKey = Symbol('solariteDelegated');
|
|
1337
1374
|
|
|
1338
1375
|
/**
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1341
|
-
*
|
|
1342
|
-
*
|
|
1343
|
-
*
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
let
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
a
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1376
|
+
* One shared bubble-phase listener per event type, attached to a node only for the duration
|
|
1377
|
+
* of one event. The browser invokes it at the node's own turn in propagation, and it reads
|
|
1378
|
+
* the node's handler THEN rather than when it was attached, so a handler that an earlier
|
|
1379
|
+
* listener in the same dispatch replaced or removed is honored.
|
|
1380
|
+
* @type {Object<string, {handleEvent: function(Event)}>} */
|
|
1381
|
+
const trampolines = {};
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* @param type {string}
|
|
1385
|
+
* @return {{handleEvent: function(Event)}} */
|
|
1386
|
+
function trampolineFor(type) {
|
|
1387
|
+
let tramp = trampolines[type];
|
|
1388
|
+
if (tramp === undefined) {
|
|
1389
|
+
let dk = delegatedKeys[type];
|
|
1390
|
+
tramp = trampolines[type] = {
|
|
1391
|
+
// Quoted so the minifier's property mangling doesn't rename it, since the browser looks it up by name.
|
|
1392
|
+
'handleEvent'(ev) {
|
|
1393
|
+
let node = ev.currentTarget;
|
|
1394
|
+
let a = node[dk];
|
|
1395
|
+
if (a === undefined) // Unbound by an earlier handler in this same dispatch.
|
|
1396
|
+
return;
|
|
1397
|
+
let root = node[delegatedRootKey];
|
|
1398
|
+
if (typeof a === 'function')
|
|
1399
|
+
a.call(root, ev, node);
|
|
1400
|
+
else
|
|
1401
|
+
switch (a.length) {
|
|
1402
|
+
case 1: a[0].call(root, ev, node); break;
|
|
1403
|
+
case 2: a[0].call(root, a[1], ev, node); break;
|
|
1404
|
+
case 3: a[0].call(root, a[1], a[2], ev, node); break;
|
|
1405
|
+
default: a[0].call(root, ...a.slice(1), ev, node);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
return tramp;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// Nodes still carrying a trampoline, per event type, and the one timer that clears them.
|
|
1414
|
+
const pending = {};
|
|
1415
|
+
let sweepTimer = 0;
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* Remove every trampoline attached since the last sweep. Runs as a task, which is always
|
|
1419
|
+
* after every dispatch in progress has finished. A microtask would not be: for a real click
|
|
1420
|
+
* the browser runs a microtask checkpoint between listeners, so a microtask sweep would strip
|
|
1421
|
+
* the trampolines before the event reached the first of them. The sweep is housekeeping
|
|
1422
|
+
* only; a trampoline left in place is harmless, because jitDispatcher() re-attaches it and
|
|
1423
|
+
* the trampoline reads its handler fresh. */
|
|
1424
|
+
function sweep() {
|
|
1425
|
+
sweepTimer = 0;
|
|
1426
|
+
for (let type in pending) {
|
|
1427
|
+
let nodes = pending[type];
|
|
1428
|
+
if (nodes.length !== 0) {
|
|
1429
|
+
pending[type] = [];
|
|
1430
|
+
let tramp = trampolines[type];
|
|
1431
|
+
for (let i=0; i<nodes.length; i++)
|
|
1432
|
+
nodes[i].removeEventListener(type, tramp);
|
|
1368
1433
|
}
|
|
1369
|
-
current = current.parentNode;
|
|
1370
1434
|
}
|
|
1371
|
-
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* The capture-phase listener registered per delegated event type on every root and on the
|
|
1439
|
+
* document. It runs before the event reaches anything, walks the event's path, and attaches
|
|
1440
|
+
* the type's trampoline to each node holding a delegated handler. The browser then finishes
|
|
1441
|
+
* the dispatch natively, so those handlers interleave correctly with listeners anyone else
|
|
1442
|
+
* registered, stopPropagation() works in both directions, currentTarget is right, and the
|
|
1443
|
+
* event needn't bubble.
|
|
1444
|
+
*
|
|
1445
|
+
* Each attach removes the trampoline first. One left from an earlier event in this same task
|
|
1446
|
+
* would otherwise keep its old place in the node's listener list, ahead of listeners added
|
|
1447
|
+
* since; removing and re-adding puts it last, so the rule holds without exception: a
|
|
1448
|
+
* delegated handler runs after every listener its element had when the event started. */
|
|
1449
|
+
function jitDispatcher(ev) {
|
|
1450
|
+
let path = ev.composedPath();
|
|
1451
|
+
if (ev[delegatedDoneKey] === path.length)
|
|
1452
|
+
return;
|
|
1453
|
+
ev[delegatedDoneKey] = path.length;
|
|
1454
|
+
|
|
1455
|
+
let type = ev.type;
|
|
1456
|
+
let dk = delegatedKeys[type];
|
|
1457
|
+
let tramp = trampolineFor(type);
|
|
1458
|
+
let list = pending[type];
|
|
1459
|
+
if (list === undefined)
|
|
1460
|
+
list = pending[type] = [];
|
|
1461
|
+
for (let i=0; i<path.length; i++) {
|
|
1462
|
+
let node = path[i];
|
|
1463
|
+
if (node[dk] !== undefined) {
|
|
1464
|
+
node.removeEventListener(type, tramp);
|
|
1465
|
+
node.addEventListener(type, tramp);
|
|
1466
|
+
list.push(node);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
if (list.length !== 0 && sweepTimer === 0)
|
|
1470
|
+
sweepTimer = setTimeout(sweep);
|
|
1372
1471
|
}
|
|
1373
1472
|
|
|
1374
1473
|
class EventBinding {
|
|
@@ -1405,11 +1504,24 @@ class PathToEvent extends PathToAttribValue {
|
|
|
1405
1504
|
* Undefined for non-delegatable (non-bubbling) events; bindEvent() then binds directly. */
|
|
1406
1505
|
delegatedKey;
|
|
1407
1506
|
|
|
1507
|
+
/** @type {boolean} True for `native:onclick`: the handler is registered with addEventListener
|
|
1508
|
+
* when the template renders, so it runs at its element's own turn in the browser's dispatch
|
|
1509
|
+
* order instead of being delegated to the component root. */
|
|
1510
|
+
native;
|
|
1511
|
+
|
|
1408
1512
|
constructor(nodeBefore, nodeMarker, attribName=null, attrValue=null) {
|
|
1409
1513
|
super(null, nodeMarker, attribName, attrValue);
|
|
1410
1514
|
this.skipIfSame = true;
|
|
1411
|
-
|
|
1412
|
-
this.
|
|
1515
|
+
let name = attribName;
|
|
1516
|
+
this.native = name !== null && name.startsWith(nativeEventPrefix);
|
|
1517
|
+
if (this.native)
|
|
1518
|
+
name = name.slice(nativeEventPrefix.length);
|
|
1519
|
+
this.eventName = name ? name.slice(2) : null;
|
|
1520
|
+
|
|
1521
|
+
// A native binding leaves delegatedKey undefined. That is the single switch both
|
|
1522
|
+
// bindEvent() and the compiled stamp program test to choose the direct
|
|
1523
|
+
// addEventListener path, so nothing else has to know about the prefix.
|
|
1524
|
+
this.delegatedKey = (this.eventName !== null && !this.native) ? delegatedKeyFor(this.eventName) : undefined;
|
|
1413
1525
|
}
|
|
1414
1526
|
|
|
1415
1527
|
/**
|
|
@@ -3504,106 +3616,120 @@ class PathToComponent extends Path {
|
|
|
3504
3616
|
}
|
|
3505
3617
|
}
|
|
3506
3618
|
|
|
3507
|
-
//
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3619
|
+
// Constructing a component runs arbitrary user code -- field initializers, the
|
|
3620
|
+
// constructor body, render() -- and that code can build more components, re-entering
|
|
3621
|
+
// this method and overwriting the hand-off parked below. Saving the caller's value
|
|
3622
|
+
// here and restoring it in the finally makes the JS call stack the stack this hand-off
|
|
3623
|
+
// needs, and unlike an explicit stack it cannot leak if construction throws.
|
|
3624
|
+
let prevSlotChildren = Globals$1.currentSlotChildren;
|
|
3625
|
+
try {
|
|
3626
|
+
// 2. Instantiate component on first time.
|
|
3627
|
+
let isAttrib = el.getAttribute('_is');
|
|
3628
|
+
if (el.tagName.endsWith('-SOLARITE-PLACEHOLDER') || isAttrib) {
|
|
3629
|
+
|
|
3630
|
+
|
|
3631
|
+
// 2a. Instantiate component
|
|
3632
|
+
let tagName = (isAttrib || el.tagName.slice(0, -21)).toLowerCase(); // Remove -SOLARITE-PLACEHOLDER
|
|
3633
|
+
let Constructor = customElements.get(tagName);
|
|
3634
|
+
|
|
3635
|
+
// Not defined yet (e.g. the module is being lazily imported): keep the placeholder
|
|
3636
|
+
// and instantiate when the definition lands, like a native custom-element upgrade.
|
|
3637
|
+
// deferredExprs always holds the LATEST exprs so re-renders while undefined win.
|
|
3638
|
+
if (!Constructor) {
|
|
3639
|
+
this.deferredExprs = exprs;
|
|
3640
|
+
if (!this.whenDefinedPending) {
|
|
3641
|
+
this.whenDefinedPending = true;
|
|
3642
|
+
console.warn(`Solarite: <${tagName}> is not defined yet; waiting for customElements.define().`);
|
|
3643
|
+
customElements.whenDefined(tagName).then(() => {
|
|
3644
|
+
this.whenDefinedPending = false;
|
|
3645
|
+
let deferred = this.deferredExprs;
|
|
3646
|
+
this.deferredExprs = null;
|
|
3647
|
+
// Skip if a newer render already instantiated or replaced the placeholder.
|
|
3648
|
+
if (deferred && this.nodeMarker === el && el.tagName.endsWith('-SOLARITE-PLACEHOLDER'))
|
|
3649
|
+
this.applyAll(deferred);
|
|
3650
|
+
});
|
|
3651
|
+
}
|
|
3652
|
+
return;
|
|
3532
3653
|
}
|
|
3533
|
-
Globals$1.currentSlotChildren = null;
|
|
3534
|
-
return;
|
|
3535
|
-
}
|
|
3536
3654
|
|
|
3537
|
-
|
|
3538
|
-
|
|
3655
|
+
// Hand the children declared inside the component's tag to the RootNodeGroup that
|
|
3656
|
+
// its render() is about to create. There is no other channel: the children have
|
|
3657
|
+
// to be parked before new Constructor(), because a Solarite constructor may call
|
|
3658
|
+
// this.render() itself, and the element that would otherwise carry them does not
|
|
3659
|
+
// exist yet.
|
|
3660
|
+
Globals$1.currentSlotChildren = {Constructor, nodes: [...el.childNodes]};
|
|
3661
|
+
let newEl = new Constructor(attribs);
|
|
3662
|
+
|
|
3663
|
+
// 2b. Copy attributes over.
|
|
3664
|
+
if (isAttrib) {
|
|
3665
|
+
newEl.setAttribute('is', isAttrib);
|
|
3666
|
+
// el.removeAttribute('_is');
|
|
3667
|
+
}
|
|
3668
|
+
for (let attrib of el.attributes)
|
|
3669
|
+
if (attrib.name !== '_is')
|
|
3670
|
+
newEl.setAttribute(attrib.name, attrib.value);
|
|
3539
3671
|
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
// falsy test this used to spell out could never have decided anything.
|
|
3555
|
-
if (valType === 'boolean') {
|
|
3556
|
-
if (val)
|
|
3557
|
-
newEl.setAttribute(name, '');
|
|
3672
|
+
// Set dynamic attributes if they are primitive types.
|
|
3673
|
+
for (let name in attribs) {
|
|
3674
|
+
let val = attribs[name];
|
|
3675
|
+
let valType = typeof val;
|
|
3676
|
+
// Only true and false can reach here, so the undefined/null halves of the
|
|
3677
|
+
// falsy test this used to spell out could never have decided anything.
|
|
3678
|
+
if (valType === 'boolean') {
|
|
3679
|
+
if (val)
|
|
3680
|
+
newEl.setAttribute(name, '');
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
// If type is a non-boolean primitive, set the attribute value.
|
|
3684
|
+
else if (valType==='string' || valType === 'number' || valType==='bigint')
|
|
3685
|
+
newEl.setAttribute(name, val);
|
|
3558
3686
|
}
|
|
3559
3687
|
|
|
3560
|
-
// If type is a non-boolean primitive, set the attribute value.
|
|
3561
|
-
else if (valType==='string' || valType === 'number' || valType==='bigint')
|
|
3562
|
-
newEl.setAttribute(name, val);
|
|
3563
|
-
}
|
|
3564
3688
|
|
|
3689
|
+
// 2c. If an id pointed at the placeholder, update it to point to the new element.
|
|
3690
|
+
let id = newEl.getAttribute('data-id') || newEl.getAttribute('id');
|
|
3691
|
+
if (id)
|
|
3692
|
+
delve(this.parentNg.getRootEl(), id.split(/\./g), newEl);
|
|
3565
3693
|
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3694
|
+
// 2d. Update paths to use replaced element.
|
|
3695
|
+
let ng = this.parentNg;
|
|
3696
|
+
this.nodeMarker = newEl;
|
|
3697
|
+
for (let path of ng.paths) {
|
|
3698
|
+
if (path.nodeMarker === el)
|
|
3699
|
+
path.nodeMarker = newEl;
|
|
3700
|
+
if (path.nodeBefore === el)
|
|
3701
|
+
path.nodeBefore = newEl;
|
|
3702
|
+
}
|
|
3703
|
+
if (ng.startNode === el)
|
|
3704
|
+
ng.startNode = newEl;
|
|
3705
|
+
if (ng.endNode === el)
|
|
3706
|
+
ng.endNode = newEl;
|
|
3707
|
+
|
|
3708
|
+
// 2f. Call render() if it wasn't called by the constructor.
|
|
3709
|
+
// This must happen before we add it to the DOM which can trigger connectedCallback() -> renderFirstTime()
|
|
3710
|
+
// Because that path renders it without the attribute expressions.
|
|
3711
|
+
if (typeof newEl.render === 'function' && !Globals$1.rendered.has(newEl))
|
|
3712
|
+
newEl.render(attribs, true);
|
|
3713
|
+
|
|
3714
|
+
// 2g. Update attribute paths to use the new element and re-apply them.
|
|
3715
|
+
for (let i=0, attribPath; attribPath = this.attribPaths[i]; i++) {
|
|
3716
|
+
attribPath.parentNg = this.parentNg;
|
|
3717
|
+
attribPath.nodeMarker = newEl;
|
|
3718
|
+
attribPath.applyAll(exprs[i]);
|
|
3719
|
+
}
|
|
3570
3720
|
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
this.nodeMarker = newEl;
|
|
3574
|
-
for (let path of ng.paths) {
|
|
3575
|
-
if (path.nodeMarker === el)
|
|
3576
|
-
path.nodeMarker = newEl;
|
|
3577
|
-
if (path.nodeBefore === el)
|
|
3578
|
-
path.nodeBefore = newEl;
|
|
3579
|
-
}
|
|
3580
|
-
if (ng.startNode === el)
|
|
3581
|
-
ng.startNode = newEl;
|
|
3582
|
-
if (ng.endNode === el)
|
|
3583
|
-
ng.endNode = newEl;
|
|
3584
|
-
|
|
3585
|
-
// 2f. Call render() if it wasn't called by the constructor.
|
|
3586
|
-
// This must happen before we add it to the DOM which can trigger connectedCallback() -> renderFirstTime()
|
|
3587
|
-
// Because that path renders it without the attribute expressions.
|
|
3588
|
-
if (typeof newEl.render === 'function' && !Globals$1.rendered.has(newEl))
|
|
3589
|
-
newEl.render(attribs, true);
|
|
3590
|
-
|
|
3591
|
-
// 2g. Update attribute paths to use the new element and re-apply them.
|
|
3592
|
-
for (let i=0, attribPath; attribPath = this.attribPaths[i]; i++) {
|
|
3593
|
-
attribPath.parentNg = this.parentNg;
|
|
3594
|
-
attribPath.nodeMarker = newEl;
|
|
3595
|
-
attribPath.applyAll(exprs[i]);
|
|
3721
|
+
// 2e. Swap it to the DOM.
|
|
3722
|
+
el.replaceWith(newEl);
|
|
3596
3723
|
}
|
|
3597
3724
|
|
|
3598
|
-
//
|
|
3599
|
-
el.
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
// 2f. Render
|
|
3603
|
-
else if (typeof el.render === 'function')
|
|
3604
|
-
el.render(attribs, changed);
|
|
3725
|
+
// 2f. Render
|
|
3726
|
+
else if (typeof el.render === 'function')
|
|
3727
|
+
el.render(attribs, changed);
|
|
3605
3728
|
|
|
3606
|
-
|
|
3729
|
+
}
|
|
3730
|
+
finally {
|
|
3731
|
+
Globals$1.currentSlotChildren = prevSlotChildren;
|
|
3732
|
+
}
|
|
3607
3733
|
}
|
|
3608
3734
|
|
|
3609
3735
|
/**
|
|
@@ -3792,6 +3918,9 @@ class Shell {
|
|
|
3792
3918
|
// Smaller fragments make cloning, path resolution, and insertion faster.
|
|
3793
3919
|
stripTableWhitespace(this.docFrag);
|
|
3794
3920
|
|
|
3921
|
+
// 1c. Neutralize `is` so the browser can't upgrade a placeholder out from under us.
|
|
3922
|
+
renameIsAttribs(this.docFrag);
|
|
3923
|
+
|
|
3795
3924
|
// 2. Find placeholders
|
|
3796
3925
|
let node;
|
|
3797
3926
|
let toRemove = [];
|
|
@@ -3805,7 +3934,7 @@ class Shell {
|
|
|
3805
3934
|
|
|
3806
3935
|
// Replace attributes
|
|
3807
3936
|
if (node.nodeType === 1) {
|
|
3808
|
-
const hasIs = node.hasAttribute('
|
|
3937
|
+
const hasIs = node.hasAttribute('_is'); // Renamed from `is` in step 1c.
|
|
3809
3938
|
const isComponent = (hasIs || node.tagName.includes('-'));
|
|
3810
3939
|
const componentAttribPaths = [];
|
|
3811
3940
|
|
|
@@ -3909,10 +4038,6 @@ class Shell {
|
|
|
3909
4038
|
path.attribPaths = componentAttribPaths;
|
|
3910
4039
|
this.paths.splice(this.paths.length - componentAttribPaths.length, 0, path); // Insert before its componentAttribPaths
|
|
3911
4040
|
|
|
3912
|
-
if (hasIs) {
|
|
3913
|
-
node.setAttribute('_is', node.getAttribute('is'));
|
|
3914
|
-
node.removeAttribute('is');
|
|
3915
|
-
}
|
|
3916
4041
|
}
|
|
3917
4042
|
}
|
|
3918
4043
|
|
|
@@ -3929,7 +4054,7 @@ class Shell {
|
|
|
3929
4054
|
// Components and slots are excluded because they move their children
|
|
3930
4055
|
// during instantiation, which would orphan the expression's region.
|
|
3931
4056
|
if (parent.nodeType === 1 && !node.previousSibling && !node.nextSibling
|
|
3932
|
-
&& !parent.tagName.includes('-') && parent.tagName !== 'SLOT' && !parent.hasAttribute('
|
|
4057
|
+
&& !parent.tagName.includes('-') && parent.tagName !== 'SLOT' && !parent.hasAttribute('_is')) {
|
|
3933
4058
|
let path = new PathToNodes(null, parent);
|
|
3934
4059
|
path.wholeParent = true;
|
|
3935
4060
|
this.paths.push(path);
|
|
@@ -4343,6 +4468,38 @@ function stripTableWhitespace(el) {
|
|
|
4343
4468
|
}
|
|
4344
4469
|
}
|
|
4345
4470
|
|
|
4471
|
+
/**
|
|
4472
|
+
* Rename every `is` attribute to `_is`, rebuilding the element to do it.
|
|
4473
|
+
*
|
|
4474
|
+
* A component written as a dashed tag is neutralized in the shell by renaming the TAG
|
|
4475
|
+
* (`<my-tag>` becomes `<my-tag-SOLARITE-PLACEHOLDER>`), so the browser never recognizes the
|
|
4476
|
+
* placeholder and never upgrades it. A customized built-in cannot be neutralized that way,
|
|
4477
|
+
* because its tag has to stay real: a `<tr is="my-row">` that is not a `<tr>` is thrown out
|
|
4478
|
+
* by the parser's table rules. So its ATTRIBUTE is renamed instead.
|
|
4479
|
+
*
|
|
4480
|
+
* Renaming the attribute in place is not enough. `is` is also recorded in an internal slot on
|
|
4481
|
+
* the element, which removeAttribute() cannot clear and cloneNode() copies, so a placeholder
|
|
4482
|
+
* that was parsed with `is` stays a customized built-in as far as the browser is concerned.
|
|
4483
|
+
* Every clone of it is upgraded the moment it enters a document with a browsing context —
|
|
4484
|
+
* running the component's constructor on the placeholder, before PathToComponent has
|
|
4485
|
+
* instantiated the real element or evaluated the attribute expressions meant for it. A
|
|
4486
|
+
* constructor that renders then renders the placeholder, whose children are the ones the user
|
|
4487
|
+
* declared, and those get handed to the real instance as if they were slot content.
|
|
4488
|
+
*
|
|
4489
|
+
* Building a fresh element and moving everything across is the only way to drop that slot.
|
|
4490
|
+
* It happens once per unique template, because Shells are cached, and never per render.
|
|
4491
|
+
*
|
|
4492
|
+
* @param docFrag {DocumentFragment} */
|
|
4493
|
+
function renameIsAttribs(docFrag) {
|
|
4494
|
+
for (let el of docFrag.querySelectorAll('[is]')) {
|
|
4495
|
+
let clean = el.ownerDocument.createElement(el.tagName);
|
|
4496
|
+
for (let attrib of el.attributes)
|
|
4497
|
+
clean.setAttribute(attrib.name === 'is' ? '_is' : attrib.name, attrib.value);
|
|
4498
|
+
clean.append(...el.childNodes);
|
|
4499
|
+
el.replaceWith(clean);
|
|
4500
|
+
}
|
|
4501
|
+
}
|
|
4502
|
+
|
|
4346
4503
|
// One-entry memo for Shell.get().
|
|
4347
4504
|
let lastHtmlStrings = null, lastSvgMode = false, lastShell = null;
|
|
4348
4505
|
|
|
@@ -4672,17 +4829,17 @@ class NodeGroup {
|
|
|
4672
4829
|
let stampers = shell.stampPaths;
|
|
4673
4830
|
let rootNg = this.rootNg;
|
|
4674
4831
|
let root = rootNg.rootEl;
|
|
4832
|
+
// Any value other than false or an array of event names means delegate everything.
|
|
4675
4833
|
let opt = rootNg.renderOptions?.eventDelegation;
|
|
4676
|
-
let
|
|
4677
|
-
let delegateAll = opt === undefined || opt === true || delegateDoc;
|
|
4834
|
+
let delegateAll = opt !== false && !Array.isArray(opt);
|
|
4678
4835
|
|
|
4679
4836
|
// Register this shell's delegated dispatchers once for a whole run of rows. They live on
|
|
4680
|
-
// the root, not on the bound nodes, so asking per node — as the general
|
|
4681
|
-
// to — would be a call and a set lookup for every handler in the list.
|
|
4837
|
+
// the root and the document, not on the bound nodes, so asking per node — as the general
|
|
4838
|
+
// binding path has to — would be a call and a set lookup for every handler in the list.
|
|
4682
4839
|
let names = shell.stampEventNames;
|
|
4683
4840
|
if (names !== null && delegateAll && rootNg[lastStampedShellKey] !== shell) {
|
|
4684
4841
|
for (let k=0; k<names.length; k++)
|
|
4685
|
-
ensureDelegatedDispatcher(root, names[k]
|
|
4842
|
+
ensureDelegatedDispatcher(root, names[k]);
|
|
4686
4843
|
rootNg[lastStampedShellKey] = shell;
|
|
4687
4844
|
}
|
|
4688
4845
|
|
|
@@ -5075,13 +5232,30 @@ class RootNodeGroup extends NodeGroup {
|
|
|
5075
5232
|
if (el) {
|
|
5076
5233
|
this.rootEl = el;
|
|
5077
5234
|
|
|
5078
|
-
// Save slot
|
|
5079
|
-
// 1.
|
|
5080
|
-
//
|
|
5235
|
+
// Save the children that belong in this component's <slot>, from one of two places:
|
|
5236
|
+
// 1. A hand-off parked by PathToComponent.applyAll() just before it constructed
|
|
5237
|
+
// us, when this component was declared inside another template. It carries
|
|
5238
|
+
// the Constructor it was meant for, so an unrelated component built in the
|
|
5239
|
+
// meantime -- a field initializer creating a menu, say -- leaves it alone.
|
|
5240
|
+
// 2. el.childNodes, when render() is called manually for the first time.
|
|
5241
|
+
// An addressed hand-off wins even when its node list is empty: a component
|
|
5242
|
+
// declared as <my-tag></my-tag> is asking for an empty slot, not for whatever
|
|
5243
|
+
// its own constructor happened to put in the element.
|
|
5244
|
+
//
|
|
5245
|
+
// The hand-off is deliberately NOT cleared on read. A component that builds
|
|
5246
|
+
// another instance of its OWN class while constructing cannot be told apart
|
|
5247
|
+
// from itself by any address, so both match; the inner one takes the nodes and
|
|
5248
|
+
// this outer one takes them straight back, which is the only thing that makes
|
|
5249
|
+
// that case work.
|
|
5250
|
+
let handOff = Globals$1.currentSlotChildren;
|
|
5251
|
+
let mySlotNodes = handOff?.Constructor === el.constructor
|
|
5252
|
+
? handOff.nodes
|
|
5253
|
+
: (el.childNodes.length ? [...el.childNodes] : null);
|
|
5254
|
+
|
|
5081
5255
|
let slotChildren;
|
|
5082
|
-
if (
|
|
5256
|
+
if (mySlotNodes) {
|
|
5083
5257
|
slotChildren = Globals$1.doc.createDocumentFragment();
|
|
5084
|
-
slotChildren.append(...
|
|
5258
|
+
slotChildren.append(...mySlotNodes);
|
|
5085
5259
|
}
|
|
5086
5260
|
|
|
5087
5261
|
// If el should replace the root node of the fragment.
|