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.js
CHANGED
|
@@ -16,8 +16,17 @@ function reset() {
|
|
|
16
16
|
connected: new WeakSet(),
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* A hand-off in flight from PathToComponent.applyAll(), which parks the child nodes
|
|
20
|
+
* declared inside a component's tag here just before constructing it, to that
|
|
21
|
+
* component's RootNodeGroup.instantiate(), which puts them in its <slot>. Null when
|
|
22
|
+
* no hand-off is pending.
|
|
23
|
+
*
|
|
24
|
+
* It is addressed by Constructor rather than by tag name because a customized
|
|
25
|
+
* built-in has no usable tag at the moment it is consumed: a <tr is="my-row">
|
|
26
|
+
* reports a tagName of TR, and its 'is' attribute is not written until after the
|
|
27
|
+
* constructor -- which may already have rendered -- has returned.
|
|
28
|
+
*
|
|
29
|
+
* @type {?{Constructor:Function, nodes:Node[]}} */
|
|
21
30
|
currentSlotChildren: null,
|
|
22
31
|
|
|
23
32
|
div: document.createElement("div"),
|
|
@@ -57,6 +66,17 @@ function reset() {
|
|
|
57
66
|
}
|
|
58
67
|
reset();
|
|
59
68
|
|
|
69
|
+
// Warn when a second copy of Solarite loads into the same page. Each copy has its own classes and its own Globals,
|
|
70
|
+
// so a template or component made by one is not recognised by the other, and the failure that follows (a template
|
|
71
|
+
// rendered as "[object Object]", a slot that stays empty) gives no hint of the cause. The usual ways to get two are
|
|
72
|
+
// importing both Solarite.js and Solarite.min.js, or a JSX runtime file that doesn't match the build being imported.
|
|
73
|
+
// The marker is the same for every build, so the source, debug, and minified builds all detect one another.
|
|
74
|
+
let copy = Symbol.for('solarite');
|
|
75
|
+
if (globalThis[copy])
|
|
76
|
+
console.warn(`Solarite loaded twice: ${globalThis[copy]} and ${import.meta.url}. Templates and components from one won't work in the other.`);
|
|
77
|
+
else
|
|
78
|
+
globalThis[copy] = import.meta.url;
|
|
79
|
+
|
|
60
80
|
var Globals$1 = Globals;
|
|
61
81
|
|
|
62
82
|
/**
|
|
@@ -116,6 +136,12 @@ function isDelvePath(arr) {
|
|
|
116
136
|
// d means "don't create"
|
|
117
137
|
let d = {};
|
|
118
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Prefix that asks for a handler to bypass event delegation: `<button native:onclick=\${...}>`
|
|
141
|
+
* is bound with addEventListener at render time, taking its normal place in the browser's own
|
|
142
|
+
* dispatch order. Shared by Util.isEvent() and PathToEvent, which strips it. */
|
|
143
|
+
const nativeEventPrefix = 'native:';
|
|
144
|
+
|
|
119
145
|
let Util = {
|
|
120
146
|
|
|
121
147
|
/**
|
|
@@ -315,7 +341,14 @@ let Util = {
|
|
|
315
341
|
return node.value; // String
|
|
316
342
|
},
|
|
317
343
|
|
|
344
|
+
/**
|
|
345
|
+
* True for an attribute name that binds an event: `onclick`, or `native:onclick` for a
|
|
346
|
+
* handler that is registered with addEventListener when the template renders instead of
|
|
347
|
+
* being delegated. Only names an element really exposes as on* handlers count, so an
|
|
348
|
+
* attribute like `online` is never mistaken for one. */
|
|
318
349
|
isEvent(attribName) {
|
|
350
|
+
if (attribName.startsWith(nativeEventPrefix))
|
|
351
|
+
attribName = attribName.slice(nativeEventPrefix.length);
|
|
319
352
|
return attribName.startsWith('on') && attribName in Globals$1.div;
|
|
320
353
|
},
|
|
321
354
|
|
|
@@ -1072,17 +1105,20 @@ class PathToAttribValue extends Path {
|
|
|
1072
1105
|
|
|
1073
1106
|
// Delegated path: a bubbling event (when the root's options allow it, the default)
|
|
1074
1107
|
// stores its handler directly on the node as a per-event-type Symbol expando, with no
|
|
1075
|
-
// EventBinding object and no addEventListener call.
|
|
1076
|
-
//
|
|
1077
|
-
// the
|
|
1078
|
-
//
|
|
1108
|
+
// EventBinding object and no addEventListener call. When an event of that type
|
|
1109
|
+
// starts, jitDispatcher() attaches a real listener to each node on its path that
|
|
1110
|
+
// carries the expando, so the browser runs the handler at the node's own turn.
|
|
1111
|
+
// Re-renders just overwrite the property. this.delegatedKey is set by the PathToEvent
|
|
1112
|
+
// constructor only for delegatable event names, so this test also excludes
|
|
1113
|
+
// non-bubbling events and native:on* bindings.
|
|
1079
1114
|
if (capture === false && this.delegatedKey !== undefined) {
|
|
1080
1115
|
let opt = this.parentNg.rootNg.renderOptions?.eventDelegation ?? true;
|
|
1081
|
-
|
|
1082
|
-
|
|
1116
|
+
// true delegates everything, an array only the events it names, and any other
|
|
1117
|
+
// value (such as the retired 'document' string) counts as true.
|
|
1118
|
+
if (opt !== false && (!Array.isArray(opt) || opt.includes(eventName))) {
|
|
1083
1119
|
let dk = this.delegatedKey;
|
|
1084
1120
|
if (node[dk] === undefined) // First binding of this type on this node.
|
|
1085
|
-
ensureDelegatedDispatcher(root, eventName
|
|
1121
|
+
ensureDelegatedDispatcher(root, eventName);
|
|
1086
1122
|
// Array-form bindings (onclick=${[fn, arg]}, the hot per-row case) store the
|
|
1087
1123
|
// template's own [func, ...args] array; a plain function is stored bare.
|
|
1088
1124
|
// Either way, nothing is allocated.
|
|
@@ -1154,7 +1190,7 @@ function getEventBinding(node, key) {
|
|
|
1154
1190
|
return b instanceof EventBinding ? (b.key === key ? b : undefined) : b[key];
|
|
1155
1191
|
}
|
|
1156
1192
|
|
|
1157
|
-
// Bubbling events
|
|
1193
|
+
// Bubbling events the just-in-time dispatcher handles. Same set Solid.js delegates.
|
|
1158
1194
|
const delegatableEvents = new Set(['beforeinput', 'click', 'contextmenu', 'dblclick', 'focusin', 'focusout',
|
|
1159
1195
|
'input', 'keydown', 'keyup', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup',
|
|
1160
1196
|
'pointerdown', 'pointermove', 'pointerout', 'pointerover', 'pointerup', 'touchend', 'touchmove', 'touchstart']);
|
|
@@ -1178,79 +1214,142 @@ function delegatedKeyFor(eventName) {
|
|
|
1178
1214
|
// Exported so NodeGroup.applyStamp()'s compiled stamp program can write it directly.
|
|
1179
1215
|
const delegatedRootKey = Symbol('solariteDelegatedRoot');
|
|
1180
1216
|
|
|
1181
|
-
//
|
|
1217
|
+
// Set of event types that already have the dispatcher registered, kept on each root element
|
|
1218
|
+
// and on each document.
|
|
1182
1219
|
const delegatedTypesKey = Symbol('solariteDelegatedTypes');
|
|
1183
1220
|
|
|
1184
1221
|
/**
|
|
1185
|
-
* Register the
|
|
1186
|
-
* Shared by bindEvent()'s delegated branch and NodeGroup.applyStamp()'s stamp program.
|
|
1222
|
+
* Register the just-in-time dispatcher for eventName on root and on root's document, once
|
|
1223
|
+
* each. Shared by bindEvent()'s delegated branch and NodeGroup.applyStamp()'s stamp program.
|
|
1187
1224
|
*
|
|
1188
|
-
*
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
1191
|
-
*
|
|
1192
|
-
*
|
|
1225
|
+
* Both registrations are needed. The document's listener is what still reaches a bound node
|
|
1226
|
+
* after another component re-parents it outside its root (a toolbar a dock parks in its own
|
|
1227
|
+
* chrome). The root's listener is what reaches what the document cannot see: a component
|
|
1228
|
+
* that isn't in the document at all, nodes inside a closed shadow root, and a synthetic
|
|
1229
|
+
* event dispatched inside any shadow root without composed:true, which never leaves it.
|
|
1193
1230
|
* @param root {HTMLElement}
|
|
1194
|
-
* @param eventName {string}
|
|
1195
|
-
|
|
1196
|
-
function ensureDelegatedDispatcher(root, eventName, andDocument=false) {
|
|
1231
|
+
* @param eventName {string} */
|
|
1232
|
+
function ensureDelegatedDispatcher(root, eventName) {
|
|
1197
1233
|
let types = root[delegatedTypesKey];
|
|
1198
1234
|
if (types === undefined)
|
|
1199
1235
|
types = root[delegatedTypesKey] = new Set();
|
|
1200
1236
|
if (!types.has(eventName)) {
|
|
1201
1237
|
types.add(eventName);
|
|
1202
|
-
root.addEventListener(eventName,
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
let doc = root.ownerDocument ?? document;
|
|
1238
|
+
root.addEventListener(eventName, jitDispatcher, true);
|
|
1239
|
+
|
|
1240
|
+
let doc = root.ownerDocument;
|
|
1206
1241
|
let docTypes = doc[delegatedTypesKey];
|
|
1207
1242
|
if (docTypes === undefined)
|
|
1208
1243
|
docTypes = doc[delegatedTypesKey] = new Set();
|
|
1209
1244
|
if (!docTypes.has(eventName)) {
|
|
1210
1245
|
docTypes.add(eventName);
|
|
1211
|
-
doc.addEventListener(eventName,
|
|
1246
|
+
doc.addEventListener(eventName, jitDispatcher, true);
|
|
1212
1247
|
}
|
|
1213
1248
|
}
|
|
1214
1249
|
}
|
|
1215
1250
|
|
|
1216
|
-
//
|
|
1217
|
-
//
|
|
1251
|
+
// Set on an event by the first dispatcher to walk it, holding the length of the path it saw,
|
|
1252
|
+
// so the dispatchers on nested roots further down don't repeat the walk. A root inside a
|
|
1253
|
+
// closed shadow root sees a longer path than the document did, because composedPath() hides
|
|
1254
|
+
// a closed tree from listeners outside it, and that mismatch is what makes it walk again.
|
|
1218
1255
|
const delegatedDoneKey = Symbol('solariteDelegated');
|
|
1219
1256
|
|
|
1220
1257
|
/**
|
|
1221
|
-
*
|
|
1222
|
-
*
|
|
1223
|
-
*
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1258
|
+
* One shared bubble-phase listener per event type, attached to a node only for the duration
|
|
1259
|
+
* of one event. The browser invokes it at the node's own turn in propagation, and it reads
|
|
1260
|
+
* the node's handler THEN rather than when it was attached, so a handler that an earlier
|
|
1261
|
+
* listener in the same dispatch replaced or removed is honored.
|
|
1262
|
+
* @type {Object<string, {handleEvent: function(Event)}>} */
|
|
1263
|
+
const trampolines = {};
|
|
1264
|
+
|
|
1265
|
+
/**
|
|
1266
|
+
* @param type {string}
|
|
1267
|
+
* @return {{handleEvent: function(Event)}} */
|
|
1268
|
+
function trampolineFor(type) {
|
|
1269
|
+
let tramp = trampolines[type];
|
|
1270
|
+
if (tramp === undefined) {
|
|
1271
|
+
let dk = delegatedKeys[type];
|
|
1272
|
+
tramp = trampolines[type] = {
|
|
1273
|
+
// Quoted so the minifier's property mangling doesn't rename it, since the browser looks it up by name.
|
|
1274
|
+
'handleEvent'(ev) {
|
|
1275
|
+
let node = ev.currentTarget;
|
|
1276
|
+
let a = node[dk];
|
|
1277
|
+
if (a === undefined) // Unbound by an earlier handler in this same dispatch.
|
|
1278
|
+
return;
|
|
1279
|
+
let root = node[delegatedRootKey];
|
|
1280
|
+
if (typeof a === 'function')
|
|
1281
|
+
a.call(root, ev, node);
|
|
1282
|
+
else
|
|
1283
|
+
switch (a.length) {
|
|
1284
|
+
case 1: a[0].call(root, ev, node); break;
|
|
1285
|
+
case 2: a[0].call(root, a[1], ev, node); break;
|
|
1286
|
+
case 3: a[0].call(root, a[1], a[2], ev, node); break;
|
|
1287
|
+
default: a[0].call(root, ...a.slice(1), ev, node);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
return tramp;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// Nodes still carrying a trampoline, per event type, and the one timer that clears them.
|
|
1296
|
+
const pending = {};
|
|
1297
|
+
let sweepTimer = 0;
|
|
1298
|
+
|
|
1299
|
+
/**
|
|
1300
|
+
* Remove every trampoline attached since the last sweep. Runs as a task, which is always
|
|
1301
|
+
* after every dispatch in progress has finished. A microtask would not be: for a real click
|
|
1302
|
+
* the browser runs a microtask checkpoint between listeners, so a microtask sweep would strip
|
|
1303
|
+
* the trampolines before the event reached the first of them. The sweep is housekeeping
|
|
1304
|
+
* only; a trampoline left in place is harmless, because jitDispatcher() re-attaches it and
|
|
1305
|
+
* the trampoline reads its handler fresh. */
|
|
1306
|
+
function sweep() {
|
|
1307
|
+
sweepTimer = 0;
|
|
1308
|
+
for (let type in pending) {
|
|
1309
|
+
let nodes = pending[type];
|
|
1310
|
+
if (nodes.length !== 0) {
|
|
1311
|
+
pending[type] = [];
|
|
1312
|
+
let tramp = trampolines[type];
|
|
1313
|
+
for (let i=0; i<nodes.length; i++)
|
|
1314
|
+
nodes[i].removeEventListener(type, tramp);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* The capture-phase listener registered per delegated event type on every root and on the
|
|
1321
|
+
* document. It runs before the event reaches anything, walks the event's path, and attaches
|
|
1322
|
+
* the type's trampoline to each node holding a delegated handler. The browser then finishes
|
|
1323
|
+
* the dispatch natively, so those handlers interleave correctly with listeners anyone else
|
|
1324
|
+
* registered, stopPropagation() works in both directions, currentTarget is right, and the
|
|
1325
|
+
* event needn't bubble.
|
|
1326
|
+
*
|
|
1327
|
+
* Each attach removes the trampoline first. One left from an earlier event in this same task
|
|
1328
|
+
* would otherwise keep its old place in the node's listener list, ahead of listeners added
|
|
1329
|
+
* since; removing and re-adding puts it last, so the rule holds without exception: a
|
|
1330
|
+
* delegated handler runs after every listener its element had when the event started. */
|
|
1331
|
+
function jitDispatcher(ev) {
|
|
1332
|
+
let path = ev.composedPath();
|
|
1333
|
+
if (ev[delegatedDoneKey] === path.length)
|
|
1230
1334
|
return;
|
|
1231
|
-
ev[delegatedDoneKey] =
|
|
1232
|
-
|
|
1233
|
-
let
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
case 3: a[0].call(root, a[1], a[2], ev, current); break;
|
|
1246
|
-
default: a[0].call(root, ...a.slice(1), ev, current);
|
|
1247
|
-
}
|
|
1248
|
-
if (ev.cancelBubble)
|
|
1249
|
-
break;
|
|
1335
|
+
ev[delegatedDoneKey] = path.length;
|
|
1336
|
+
|
|
1337
|
+
let type = ev.type;
|
|
1338
|
+
let dk = delegatedKeys[type];
|
|
1339
|
+
let tramp = trampolineFor(type);
|
|
1340
|
+
let list = pending[type];
|
|
1341
|
+
if (list === undefined)
|
|
1342
|
+
list = pending[type] = [];
|
|
1343
|
+
for (let i=0; i<path.length; i++) {
|
|
1344
|
+
let node = path[i];
|
|
1345
|
+
if (node[dk] !== undefined) {
|
|
1346
|
+
node.removeEventListener(type, tramp);
|
|
1347
|
+
node.addEventListener(type, tramp);
|
|
1348
|
+
list.push(node);
|
|
1250
1349
|
}
|
|
1251
|
-
current = current.parentNode;
|
|
1252
1350
|
}
|
|
1253
|
-
|
|
1351
|
+
if (list.length !== 0 && sweepTimer === 0)
|
|
1352
|
+
sweepTimer = setTimeout(sweep);
|
|
1254
1353
|
}
|
|
1255
1354
|
|
|
1256
1355
|
class EventBinding {
|
|
@@ -1287,11 +1386,24 @@ class PathToEvent extends PathToAttribValue {
|
|
|
1287
1386
|
* Undefined for non-delegatable (non-bubbling) events; bindEvent() then binds directly. */
|
|
1288
1387
|
delegatedKey;
|
|
1289
1388
|
|
|
1389
|
+
/** @type {boolean} True for `native:onclick`: the handler is registered with addEventListener
|
|
1390
|
+
* when the template renders, so it runs at its element's own turn in the browser's dispatch
|
|
1391
|
+
* order instead of being delegated to the component root. */
|
|
1392
|
+
native;
|
|
1393
|
+
|
|
1290
1394
|
constructor(nodeBefore, nodeMarker, attribName=null, attrValue=null) {
|
|
1291
1395
|
super(null, nodeMarker, attribName, attrValue);
|
|
1292
1396
|
this.skipIfSame = true;
|
|
1293
|
-
|
|
1294
|
-
this.
|
|
1397
|
+
let name = attribName;
|
|
1398
|
+
this.native = name !== null && name.startsWith(nativeEventPrefix);
|
|
1399
|
+
if (this.native)
|
|
1400
|
+
name = name.slice(nativeEventPrefix.length);
|
|
1401
|
+
this.eventName = name ? name.slice(2) : null;
|
|
1402
|
+
|
|
1403
|
+
// A native binding leaves delegatedKey undefined. That is the single switch both
|
|
1404
|
+
// bindEvent() and the compiled stamp program test to choose the direct
|
|
1405
|
+
// addEventListener path, so nothing else has to know about the prefix.
|
|
1406
|
+
this.delegatedKey = (this.eventName !== null && !this.native) ? delegatedKeyFor(this.eventName) : undefined;
|
|
1295
1407
|
}
|
|
1296
1408
|
|
|
1297
1409
|
/**
|
|
@@ -3335,106 +3447,120 @@ class PathToComponent extends Path {
|
|
|
3335
3447
|
}
|
|
3336
3448
|
}
|
|
3337
3449
|
|
|
3338
|
-
//
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3450
|
+
// Constructing a component runs arbitrary user code -- field initializers, the
|
|
3451
|
+
// constructor body, render() -- and that code can build more components, re-entering
|
|
3452
|
+
// this method and overwriting the hand-off parked below. Saving the caller's value
|
|
3453
|
+
// here and restoring it in the finally makes the JS call stack the stack this hand-off
|
|
3454
|
+
// needs, and unlike an explicit stack it cannot leak if construction throws.
|
|
3455
|
+
let prevSlotChildren = Globals$1.currentSlotChildren;
|
|
3456
|
+
try {
|
|
3457
|
+
// 2. Instantiate component on first time.
|
|
3458
|
+
let isAttrib = el.getAttribute('_is');
|
|
3459
|
+
if (el.tagName.endsWith('-SOLARITE-PLACEHOLDER') || isAttrib) {
|
|
3460
|
+
|
|
3461
|
+
|
|
3462
|
+
// 2a. Instantiate component
|
|
3463
|
+
let tagName = (isAttrib || el.tagName.slice(0, -21)).toLowerCase(); // Remove -SOLARITE-PLACEHOLDER
|
|
3464
|
+
let Constructor = customElements.get(tagName);
|
|
3465
|
+
|
|
3466
|
+
// Not defined yet (e.g. the module is being lazily imported): keep the placeholder
|
|
3467
|
+
// and instantiate when the definition lands, like a native custom-element upgrade.
|
|
3468
|
+
// deferredExprs always holds the LATEST exprs so re-renders while undefined win.
|
|
3469
|
+
if (!Constructor) {
|
|
3470
|
+
this.deferredExprs = exprs;
|
|
3471
|
+
if (!this.whenDefinedPending) {
|
|
3472
|
+
this.whenDefinedPending = true;
|
|
3473
|
+
console.warn(`Solarite: <${tagName}> is not defined yet; waiting for customElements.define().`);
|
|
3474
|
+
customElements.whenDefined(tagName).then(() => {
|
|
3475
|
+
this.whenDefinedPending = false;
|
|
3476
|
+
let deferred = this.deferredExprs;
|
|
3477
|
+
this.deferredExprs = null;
|
|
3478
|
+
// Skip if a newer render already instantiated or replaced the placeholder.
|
|
3479
|
+
if (deferred && this.nodeMarker === el && el.tagName.endsWith('-SOLARITE-PLACEHOLDER'))
|
|
3480
|
+
this.applyAll(deferred);
|
|
3481
|
+
});
|
|
3482
|
+
}
|
|
3483
|
+
return;
|
|
3363
3484
|
}
|
|
3364
|
-
Globals$1.currentSlotChildren = null;
|
|
3365
|
-
return;
|
|
3366
|
-
}
|
|
3367
3485
|
|
|
3368
|
-
|
|
3369
|
-
|
|
3486
|
+
// Hand the children declared inside the component's tag to the RootNodeGroup that
|
|
3487
|
+
// its render() is about to create. There is no other channel: the children have
|
|
3488
|
+
// to be parked before new Constructor(), because a Solarite constructor may call
|
|
3489
|
+
// this.render() itself, and the element that would otherwise carry them does not
|
|
3490
|
+
// exist yet.
|
|
3491
|
+
Globals$1.currentSlotChildren = {Constructor, nodes: [...el.childNodes]};
|
|
3492
|
+
let newEl = new Constructor(attribs);
|
|
3493
|
+
|
|
3494
|
+
// 2b. Copy attributes over.
|
|
3495
|
+
if (isAttrib) {
|
|
3496
|
+
newEl.setAttribute('is', isAttrib);
|
|
3497
|
+
// el.removeAttribute('_is');
|
|
3498
|
+
}
|
|
3499
|
+
for (let attrib of el.attributes)
|
|
3500
|
+
if (attrib.name !== '_is')
|
|
3501
|
+
newEl.setAttribute(attrib.name, attrib.value);
|
|
3370
3502
|
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
// falsy test this used to spell out could never have decided anything.
|
|
3386
|
-
if (valType === 'boolean') {
|
|
3387
|
-
if (val)
|
|
3388
|
-
newEl.setAttribute(name, '');
|
|
3503
|
+
// Set dynamic attributes if they are primitive types.
|
|
3504
|
+
for (let name in attribs) {
|
|
3505
|
+
let val = attribs[name];
|
|
3506
|
+
let valType = typeof val;
|
|
3507
|
+
// Only true and false can reach here, so the undefined/null halves of the
|
|
3508
|
+
// falsy test this used to spell out could never have decided anything.
|
|
3509
|
+
if (valType === 'boolean') {
|
|
3510
|
+
if (val)
|
|
3511
|
+
newEl.setAttribute(name, '');
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
// If type is a non-boolean primitive, set the attribute value.
|
|
3515
|
+
else if (valType==='string' || valType === 'number' || valType==='bigint')
|
|
3516
|
+
newEl.setAttribute(name, val);
|
|
3389
3517
|
}
|
|
3390
3518
|
|
|
3391
|
-
// If type is a non-boolean primitive, set the attribute value.
|
|
3392
|
-
else if (valType==='string' || valType === 'number' || valType==='bigint')
|
|
3393
|
-
newEl.setAttribute(name, val);
|
|
3394
|
-
}
|
|
3395
3519
|
|
|
3520
|
+
// 2c. If an id pointed at the placeholder, update it to point to the new element.
|
|
3521
|
+
let id = newEl.getAttribute('data-id') || newEl.getAttribute('id');
|
|
3522
|
+
if (id)
|
|
3523
|
+
delve(this.parentNg.getRootEl(), id.split(/\./g), newEl);
|
|
3396
3524
|
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3525
|
+
// 2d. Update paths to use replaced element.
|
|
3526
|
+
let ng = this.parentNg;
|
|
3527
|
+
this.nodeMarker = newEl;
|
|
3528
|
+
for (let path of ng.paths) {
|
|
3529
|
+
if (path.nodeMarker === el)
|
|
3530
|
+
path.nodeMarker = newEl;
|
|
3531
|
+
if (path.nodeBefore === el)
|
|
3532
|
+
path.nodeBefore = newEl;
|
|
3533
|
+
}
|
|
3534
|
+
if (ng.startNode === el)
|
|
3535
|
+
ng.startNode = newEl;
|
|
3536
|
+
if (ng.endNode === el)
|
|
3537
|
+
ng.endNode = newEl;
|
|
3538
|
+
|
|
3539
|
+
// 2f. Call render() if it wasn't called by the constructor.
|
|
3540
|
+
// This must happen before we add it to the DOM which can trigger connectedCallback() -> renderFirstTime()
|
|
3541
|
+
// Because that path renders it without the attribute expressions.
|
|
3542
|
+
if (typeof newEl.render === 'function' && !Globals$1.rendered.has(newEl))
|
|
3543
|
+
newEl.render(attribs, true);
|
|
3544
|
+
|
|
3545
|
+
// 2g. Update attribute paths to use the new element and re-apply them.
|
|
3546
|
+
for (let i=0, attribPath; attribPath = this.attribPaths[i]; i++) {
|
|
3547
|
+
attribPath.parentNg = this.parentNg;
|
|
3548
|
+
attribPath.nodeMarker = newEl;
|
|
3549
|
+
attribPath.applyAll(exprs[i]);
|
|
3550
|
+
}
|
|
3401
3551
|
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
this.nodeMarker = newEl;
|
|
3405
|
-
for (let path of ng.paths) {
|
|
3406
|
-
if (path.nodeMarker === el)
|
|
3407
|
-
path.nodeMarker = newEl;
|
|
3408
|
-
if (path.nodeBefore === el)
|
|
3409
|
-
path.nodeBefore = newEl;
|
|
3552
|
+
// 2e. Swap it to the DOM.
|
|
3553
|
+
el.replaceWith(newEl);
|
|
3410
3554
|
}
|
|
3411
|
-
if (ng.startNode === el)
|
|
3412
|
-
ng.startNode = newEl;
|
|
3413
|
-
if (ng.endNode === el)
|
|
3414
|
-
ng.endNode = newEl;
|
|
3415
|
-
|
|
3416
|
-
// 2f. Call render() if it wasn't called by the constructor.
|
|
3417
|
-
// This must happen before we add it to the DOM which can trigger connectedCallback() -> renderFirstTime()
|
|
3418
|
-
// Because that path renders it without the attribute expressions.
|
|
3419
|
-
if (typeof newEl.render === 'function' && !Globals$1.rendered.has(newEl))
|
|
3420
|
-
newEl.render(attribs, true);
|
|
3421
|
-
|
|
3422
|
-
// 2g. Update attribute paths to use the new element and re-apply them.
|
|
3423
|
-
for (let i=0, attribPath; attribPath = this.attribPaths[i]; i++) {
|
|
3424
|
-
attribPath.parentNg = this.parentNg;
|
|
3425
|
-
attribPath.nodeMarker = newEl;
|
|
3426
|
-
attribPath.applyAll(exprs[i]);
|
|
3427
|
-
}
|
|
3428
|
-
|
|
3429
|
-
// 2e. Swap it to the DOM.
|
|
3430
|
-
el.replaceWith(newEl);
|
|
3431
|
-
}
|
|
3432
3555
|
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3556
|
+
// 2f. Render
|
|
3557
|
+
else if (typeof el.render === 'function')
|
|
3558
|
+
el.render(attribs, changed);
|
|
3436
3559
|
|
|
3437
|
-
|
|
3560
|
+
}
|
|
3561
|
+
finally {
|
|
3562
|
+
Globals$1.currentSlotChildren = prevSlotChildren;
|
|
3563
|
+
}
|
|
3438
3564
|
}
|
|
3439
3565
|
|
|
3440
3566
|
/**
|
|
@@ -3611,6 +3737,9 @@ class Shell {
|
|
|
3611
3737
|
// Smaller fragments make cloning, path resolution, and insertion faster.
|
|
3612
3738
|
stripTableWhitespace(this.docFrag);
|
|
3613
3739
|
|
|
3740
|
+
// 1c. Neutralize `is` so the browser can't upgrade a placeholder out from under us.
|
|
3741
|
+
renameIsAttribs(this.docFrag);
|
|
3742
|
+
|
|
3614
3743
|
// 2. Find placeholders
|
|
3615
3744
|
let node;
|
|
3616
3745
|
let toRemove = [];
|
|
@@ -3624,7 +3753,7 @@ class Shell {
|
|
|
3624
3753
|
|
|
3625
3754
|
// Replace attributes
|
|
3626
3755
|
if (node.nodeType === 1) {
|
|
3627
|
-
const hasIs = node.hasAttribute('
|
|
3756
|
+
const hasIs = node.hasAttribute('_is'); // Renamed from `is` in step 1c.
|
|
3628
3757
|
const isComponent = (hasIs || node.tagName.includes('-'));
|
|
3629
3758
|
const componentAttribPaths = [];
|
|
3630
3759
|
|
|
@@ -3725,10 +3854,6 @@ class Shell {
|
|
|
3725
3854
|
path.attribPaths = componentAttribPaths;
|
|
3726
3855
|
this.paths.splice(this.paths.length - componentAttribPaths.length, 0, path); // Insert before its componentAttribPaths
|
|
3727
3856
|
|
|
3728
|
-
if (hasIs) {
|
|
3729
|
-
node.setAttribute('_is', node.getAttribute('is'));
|
|
3730
|
-
node.removeAttribute('is');
|
|
3731
|
-
}
|
|
3732
3857
|
}
|
|
3733
3858
|
}
|
|
3734
3859
|
|
|
@@ -3745,7 +3870,7 @@ class Shell {
|
|
|
3745
3870
|
// Components and slots are excluded because they move their children
|
|
3746
3871
|
// during instantiation, which would orphan the expression's region.
|
|
3747
3872
|
if (parent.nodeType === 1 && !node.previousSibling && !node.nextSibling
|
|
3748
|
-
&& !parent.tagName.includes('-') && parent.tagName !== 'SLOT' && !parent.hasAttribute('
|
|
3873
|
+
&& !parent.tagName.includes('-') && parent.tagName !== 'SLOT' && !parent.hasAttribute('_is')) {
|
|
3749
3874
|
let path = new PathToNodes(null, parent);
|
|
3750
3875
|
path.wholeParent = true;
|
|
3751
3876
|
this.paths.push(path);
|
|
@@ -4151,6 +4276,38 @@ function stripTableWhitespace(el) {
|
|
|
4151
4276
|
}
|
|
4152
4277
|
}
|
|
4153
4278
|
|
|
4279
|
+
/**
|
|
4280
|
+
* Rename every `is` attribute to `_is`, rebuilding the element to do it.
|
|
4281
|
+
*
|
|
4282
|
+
* A component written as a dashed tag is neutralized in the shell by renaming the TAG
|
|
4283
|
+
* (`<my-tag>` becomes `<my-tag-SOLARITE-PLACEHOLDER>`), so the browser never recognizes the
|
|
4284
|
+
* placeholder and never upgrades it. A customized built-in cannot be neutralized that way,
|
|
4285
|
+
* because its tag has to stay real: a `<tr is="my-row">` that is not a `<tr>` is thrown out
|
|
4286
|
+
* by the parser's table rules. So its ATTRIBUTE is renamed instead.
|
|
4287
|
+
*
|
|
4288
|
+
* Renaming the attribute in place is not enough. `is` is also recorded in an internal slot on
|
|
4289
|
+
* the element, which removeAttribute() cannot clear and cloneNode() copies, so a placeholder
|
|
4290
|
+
* that was parsed with `is` stays a customized built-in as far as the browser is concerned.
|
|
4291
|
+
* Every clone of it is upgraded the moment it enters a document with a browsing context —
|
|
4292
|
+
* running the component's constructor on the placeholder, before PathToComponent has
|
|
4293
|
+
* instantiated the real element or evaluated the attribute expressions meant for it. A
|
|
4294
|
+
* constructor that renders then renders the placeholder, whose children are the ones the user
|
|
4295
|
+
* declared, and those get handed to the real instance as if they were slot content.
|
|
4296
|
+
*
|
|
4297
|
+
* Building a fresh element and moving everything across is the only way to drop that slot.
|
|
4298
|
+
* It happens once per unique template, because Shells are cached, and never per render.
|
|
4299
|
+
*
|
|
4300
|
+
* @param docFrag {DocumentFragment} */
|
|
4301
|
+
function renameIsAttribs(docFrag) {
|
|
4302
|
+
for (let el of docFrag.querySelectorAll('[is]')) {
|
|
4303
|
+
let clean = el.ownerDocument.createElement(el.tagName);
|
|
4304
|
+
for (let attrib of el.attributes)
|
|
4305
|
+
clean.setAttribute(attrib.name === 'is' ? '_is' : attrib.name, attrib.value);
|
|
4306
|
+
clean.append(...el.childNodes);
|
|
4307
|
+
el.replaceWith(clean);
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4154
4311
|
// One-entry memo for Shell.get().
|
|
4155
4312
|
let lastHtmlStrings = null, lastSvgMode = false, lastShell = null;
|
|
4156
4313
|
|
|
@@ -4472,17 +4629,17 @@ class NodeGroup {
|
|
|
4472
4629
|
let stampers = shell.stampPaths;
|
|
4473
4630
|
let rootNg = this.rootNg;
|
|
4474
4631
|
let root = rootNg.rootEl;
|
|
4632
|
+
// Any value other than false or an array of event names means delegate everything.
|
|
4475
4633
|
let opt = rootNg.renderOptions?.eventDelegation;
|
|
4476
|
-
let
|
|
4477
|
-
let delegateAll = opt === undefined || opt === true || delegateDoc;
|
|
4634
|
+
let delegateAll = opt !== false && !Array.isArray(opt);
|
|
4478
4635
|
|
|
4479
4636
|
// Register this shell's delegated dispatchers once for a whole run of rows. They live on
|
|
4480
|
-
// the root, not on the bound nodes, so asking per node — as the general
|
|
4481
|
-
// to — would be a call and a set lookup for every handler in the list.
|
|
4637
|
+
// the root and the document, not on the bound nodes, so asking per node — as the general
|
|
4638
|
+
// binding path has to — would be a call and a set lookup for every handler in the list.
|
|
4482
4639
|
let names = shell.stampEventNames;
|
|
4483
4640
|
if (names !== null && delegateAll && rootNg[lastStampedShellKey] !== shell) {
|
|
4484
4641
|
for (let k=0; k<names.length; k++)
|
|
4485
|
-
ensureDelegatedDispatcher(root, names[k]
|
|
4642
|
+
ensureDelegatedDispatcher(root, names[k]);
|
|
4486
4643
|
rootNg[lastStampedShellKey] = shell;
|
|
4487
4644
|
}
|
|
4488
4645
|
|
|
@@ -4812,13 +4969,30 @@ class RootNodeGroup extends NodeGroup {
|
|
|
4812
4969
|
if (el) {
|
|
4813
4970
|
this.rootEl = el;
|
|
4814
4971
|
|
|
4815
|
-
// Save slot
|
|
4816
|
-
// 1.
|
|
4817
|
-
//
|
|
4972
|
+
// Save the children that belong in this component's <slot>, from one of two places:
|
|
4973
|
+
// 1. A hand-off parked by PathToComponent.applyAll() just before it constructed
|
|
4974
|
+
// us, when this component was declared inside another template. It carries
|
|
4975
|
+
// the Constructor it was meant for, so an unrelated component built in the
|
|
4976
|
+
// meantime -- a field initializer creating a menu, say -- leaves it alone.
|
|
4977
|
+
// 2. el.childNodes, when render() is called manually for the first time.
|
|
4978
|
+
// An addressed hand-off wins even when its node list is empty: a component
|
|
4979
|
+
// declared as <my-tag></my-tag> is asking for an empty slot, not for whatever
|
|
4980
|
+
// its own constructor happened to put in the element.
|
|
4981
|
+
//
|
|
4982
|
+
// The hand-off is deliberately NOT cleared on read. A component that builds
|
|
4983
|
+
// another instance of its OWN class while constructing cannot be told apart
|
|
4984
|
+
// from itself by any address, so both match; the inner one takes the nodes and
|
|
4985
|
+
// this outer one takes them straight back, which is the only thing that makes
|
|
4986
|
+
// that case work.
|
|
4987
|
+
let handOff = Globals$1.currentSlotChildren;
|
|
4988
|
+
let mySlotNodes = handOff?.Constructor === el.constructor
|
|
4989
|
+
? handOff.nodes
|
|
4990
|
+
: (el.childNodes.length ? [...el.childNodes] : null);
|
|
4991
|
+
|
|
4818
4992
|
let slotChildren;
|
|
4819
|
-
if (
|
|
4993
|
+
if (mySlotNodes) {
|
|
4820
4994
|
slotChildren = Globals$1.doc.createDocumentFragment();
|
|
4821
|
-
slotChildren.append(...
|
|
4995
|
+
slotChildren.append(...mySlotNodes);
|
|
4822
4996
|
}
|
|
4823
4997
|
|
|
4824
4998
|
// If el should replace the root node of the fragment.
|