solarite 0.7.1 → 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 +2206 -955
- package/dist/Solarite.js +2154 -909
- package/dist/Solarite.min.js +2 -2
- package/dist/jsx-dev-runtime.js +3 -0
- package/dist/jsx-dev-runtime.min.js +3 -0
- package/dist/jsx-runtime.js +86 -0
- package/dist/jsx-runtime.min.js +2 -0
- package/package.json +8 -7
- package/readme.md +9 -9
- package/src/Globals.js +22 -2
- package/src/MappedList.js +34 -0
- package/src/MultiValueMap.js +4 -4
- package/src/NodeGroup.js +205 -119
- package/src/Path.js +52 -53
- package/src/PathToAttribValue.js +259 -112
- package/src/PathToAttribs.js +10 -18
- package/src/PathToComponent.js +121 -83
- package/src/PathToEvent.js +32 -12
- package/src/PathToKey.js +0 -6
- package/src/PathToNodes.js +695 -155
- package/src/RootNodeGroup.js +45 -35
- package/src/Selector.js +207 -0
- package/src/Shell.js +313 -123
- package/src/Solarite.d.ts +91 -13
- package/src/Solarite.js +38 -25
- package/src/Template.js +20 -7
- package/src/Util.js +88 -60
- package/src/assert.js +1 -1
- package/src/assignAttributes.js +20 -19
- package/src/h.js +64 -37
- package/src/jsx.js +4 -7
- package/src/toEl.js +1 -1
- package/src/HtmlParser.js +0 -91
- package/src/PathToComment.js +0 -8
package/src/PathToAttribValue.js
CHANGED
|
@@ -2,32 +2,30 @@ import Path from "./Path.js";
|
|
|
2
2
|
import Util from "./Util.js";
|
|
3
3
|
import delve, {isDelvePath} from "./delve.js";
|
|
4
4
|
import assert from "./assert.js";
|
|
5
|
+
import {SelectorRef} from "./Selector.js";
|
|
5
6
|
|
|
6
7
|
export default class PathToAttribValue extends Path {
|
|
7
8
|
|
|
8
9
|
/** @type {?string} Used only if type=AttribType.Value. */
|
|
9
|
-
|
|
10
|
+
attribName;
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* @type {?string[]} Used only if type=AttribType.Value. If null, use one expr to set the whole attribute value. */
|
|
13
14
|
attrValue;
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
isComponent;
|
|
16
|
+
// isComponentAttrib and isHtmlProperty are declared on the Path base class.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
constructor(nodeBefore, nodeMarker, attrName=null, attrValue=null) {
|
|
18
|
+
constructor(nodeBefore, nodeMarker, attribName=null, attrValue=null) {
|
|
21
19
|
super(null, nodeMarker);
|
|
22
|
-
this.
|
|
20
|
+
this.attribName = attribName;
|
|
23
21
|
this.attrValue = attrValue;
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
/**
|
|
27
25
|
* Set the value of an attribute. This can be for any attribute, not just attributes named "value".
|
|
28
26
|
* @param exprs {Expr[]} */
|
|
29
|
-
|
|
30
|
-
//#
|
|
27
|
+
applyAll(exprs) {
|
|
28
|
+
//#IFDEBUG
|
|
31
29
|
assert(Array.isArray(exprs));
|
|
32
30
|
//#ENDIF
|
|
33
31
|
|
|
@@ -40,14 +38,14 @@ export default class PathToAttribValue extends Path {
|
|
|
40
38
|
// Only update attributes if the value has changed.
|
|
41
39
|
// This is needed for setting input.value, .checked, option.selected, etc.
|
|
42
40
|
let oldVal = isProp
|
|
43
|
-
? node[this.
|
|
44
|
-
: node.getAttribute(this.
|
|
41
|
+
? node[this.attribName]
|
|
42
|
+
: node.getAttribute(this.attribName);
|
|
45
43
|
if (oldVal !== joinedValue) {
|
|
46
44
|
if (isProp)
|
|
47
|
-
node[this.
|
|
48
|
-
else if (this.
|
|
45
|
+
node[this.attribName] = joinedValue;
|
|
46
|
+
else if (this.attribName === 'value' && node.hasAttribute('contenteditable'))
|
|
49
47
|
node.innerHTML = joinedValue;
|
|
50
|
-
node.setAttribute(this.
|
|
48
|
+
node.setAttribute(this.attribName, joinedValue);
|
|
51
49
|
}
|
|
52
50
|
}
|
|
53
51
|
else
|
|
@@ -60,7 +58,7 @@ export default class PathToAttribValue extends Path {
|
|
|
60
58
|
applySingle(expr) {
|
|
61
59
|
// One expression surrounded by strings, e.g. class="a ${b} c". Join through apply().
|
|
62
60
|
if (this.attrValue)
|
|
63
|
-
return this.
|
|
61
|
+
return this.applyAll([expr]);
|
|
64
62
|
|
|
65
63
|
let node = this.nodeMarker;
|
|
66
64
|
|
|
@@ -81,12 +79,12 @@ export default class PathToAttribValue extends Path {
|
|
|
81
79
|
let [obj, path] = [expr[0], expr.slice(1)];
|
|
82
80
|
|
|
83
81
|
if (!obj)
|
|
84
|
-
throw new Error(`Solarite cannot bind
|
|
82
|
+
throw new Error(`Solarite cannot bind ${this.attribName} to ${obj}.`);
|
|
85
83
|
|
|
86
84
|
let value = delve(obj, path);
|
|
87
85
|
|
|
88
86
|
// Special case to allow setting select-multiple value from an array
|
|
89
|
-
if (this.
|
|
87
|
+
if (this.attribName === 'value' && node.type === 'select-multiple' && Array.isArray(value)) {
|
|
90
88
|
// Set the .selected property on the options having a value within value.
|
|
91
89
|
let strValues = value.map(v => v + '');
|
|
92
90
|
for (let option of node.options)
|
|
@@ -102,7 +100,7 @@ export default class PathToAttribValue extends Path {
|
|
|
102
100
|
const strValue = Util.isFalsy(value) ? '' : value;
|
|
103
101
|
|
|
104
102
|
// Special case for contenteditable
|
|
105
|
-
if (this.
|
|
103
|
+
if (this.attribName === 'value' && node.hasAttribute('contenteditable')) {
|
|
106
104
|
const existingValue = node.innerHTML;
|
|
107
105
|
if (strValue !== existingValue)
|
|
108
106
|
node.innerHTML = strValue;
|
|
@@ -111,28 +109,39 @@ export default class PathToAttribValue extends Path {
|
|
|
111
109
|
|
|
112
110
|
// If we don't have this condition, when we call render(), the browser will scroll to the currently
|
|
113
111
|
// selected item in a <select> and mess up manually scrolling to a different value.
|
|
114
|
-
if (strValue !== node[this.
|
|
115
|
-
node[this.
|
|
112
|
+
if (strValue !== node[this.attribName])
|
|
113
|
+
node[this.attribName] = strValue;
|
|
116
114
|
}
|
|
117
115
|
}
|
|
118
116
|
|
|
119
117
|
// TODO: We need to remove any old listeners, like in bindEventAttribute.
|
|
120
118
|
// Does bindEvent() now handle that?
|
|
121
119
|
let func = () => {
|
|
122
|
-
let value = (this.
|
|
120
|
+
let value = (this.attribName === 'value' || node.type === 'radio')
|
|
123
121
|
? Util.getInputValue(node)
|
|
124
|
-
: node[this.
|
|
122
|
+
: node[this.attribName];
|
|
125
123
|
delve(obj, path, value);
|
|
126
124
|
}
|
|
127
125
|
|
|
128
126
|
// We use capture so we update the values before other events added by the user.
|
|
129
127
|
// TODO: Bind to scroll events also?
|
|
130
128
|
// What about resize events and width/height?
|
|
131
|
-
this.bindEvent(node, this.parentNg.
|
|
129
|
+
this.bindEvent(node, this.parentNg.getRootEl(), this.attribName, 'input', func, null, true);
|
|
132
130
|
}
|
|
133
131
|
|
|
134
132
|
// Regular attribute
|
|
135
133
|
else {
|
|
134
|
+
// A selection binding (h.selector().when()) writes its own value and tells the
|
|
135
|
+
// selector which list this row belongs to, so a later change of selection reaches
|
|
136
|
+
// the attribute directly instead of going back through render(). The typeof test
|
|
137
|
+
// keeps ordinary string attributes — nearly all of them — from paying for the
|
|
138
|
+
// prototype check.
|
|
139
|
+
if (typeof expr === 'object' && expr instanceof SelectorRef) {
|
|
140
|
+
if (!this.isComponentAttrib)
|
|
141
|
+
expr.bind(node, this.attribName, this.parentNg);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
136
145
|
// Cache this on Path.isHtmlProperty when Shell creates the props.
|
|
137
146
|
// Have Path.clone() copy .isHtmlProperty?
|
|
138
147
|
let isProp = this.isHtmlProperty;
|
|
@@ -145,43 +154,53 @@ export default class PathToAttribValue extends Path {
|
|
|
145
154
|
else
|
|
146
155
|
expr = Util.makePrimitive(expr);
|
|
147
156
|
|
|
148
|
-
// Values
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
157
|
+
// Values that remove an attribute. The empty string is included so that an attribute
|
|
158
|
+
// disappears whenever its expression is empty, instead of only when it happened to be
|
|
159
|
+
// absent already. makePrimitive() above turns null into '', so plain null lands here
|
|
160
|
+
// too; the explicit null test still matters for a function expression returning null,
|
|
161
|
+
// which skips makePrimitive.
|
|
162
|
+
// An html property is exempt: on those, '' is a real value meaning "empty", as when
|
|
163
|
+
// clearing an <input>, so it belongs on the assignment path below.
|
|
164
|
+
if (expr === undefined || expr === false || expr === null || (expr === '' && !isProp)) {
|
|
165
|
+
if (isProp) {
|
|
166
|
+
// Clear the property with a value of its own type. Assigning false to a string
|
|
167
|
+
// property such as input.value would put the text "false" in the field.
|
|
168
|
+
let old = node[this.attribName];
|
|
169
|
+
node[this.attribName] = typeof old === 'boolean' ? false : '';
|
|
170
|
+
}
|
|
171
|
+
node.removeAttribute(this.attribName);
|
|
153
172
|
}
|
|
154
173
|
else if (expr === true) {
|
|
155
174
|
if (isProp)
|
|
156
|
-
node[this.
|
|
157
|
-
node.setAttribute(this.
|
|
175
|
+
node[this.attribName] = true;
|
|
176
|
+
node.setAttribute(this.attribName, '');
|
|
158
177
|
}
|
|
159
178
|
|
|
160
179
|
// A non-toggled attribute
|
|
161
180
|
else {
|
|
162
181
|
// Only update attributes if the value has changed.
|
|
163
182
|
// This is needed for setting input.value, .checked, option.selected, etc.
|
|
164
|
-
//
|
|
183
|
+
// Non-property attributes never reach here with '', since that removes above.
|
|
165
184
|
let oldVal = isProp
|
|
166
|
-
? node[this.
|
|
167
|
-
: node.getAttribute(this.
|
|
185
|
+
? node[this.attribName]
|
|
186
|
+
: node.getAttribute(this.attribName) ?? '';
|
|
168
187
|
if (oldVal !== expr) {
|
|
169
188
|
|
|
170
189
|
// <textarea value=${expr}></textarea>
|
|
171
190
|
// Without this branch we have no way to set the value of a textarea,
|
|
172
191
|
// since we also prohibit expressions that are a child of textarea.
|
|
173
192
|
if (isProp)
|
|
174
|
-
node[this.
|
|
193
|
+
node[this.attribName] = expr;
|
|
175
194
|
|
|
176
195
|
// Allow one-way binding to contenteditable value attribute.
|
|
177
196
|
// Contenteditables normally don't have a value attribute and have their content set via innerHTML.
|
|
178
197
|
// Solarite doesn't allow contenteditables to have expressions as their children.
|
|
179
|
-
else if (this.
|
|
198
|
+
else if (this.attribName === 'value' && node.hasAttribute('contenteditable')) {
|
|
180
199
|
node.innerHTML = expr;
|
|
181
200
|
}
|
|
182
201
|
|
|
183
202
|
// TODO: Putting an 'else' here would be more performant
|
|
184
|
-
node.setAttribute(this.
|
|
203
|
+
node.setAttribute(this.attribName, expr);
|
|
185
204
|
}
|
|
186
205
|
}
|
|
187
206
|
}
|
|
@@ -195,14 +214,14 @@ export default class PathToAttribValue extends Path {
|
|
|
195
214
|
* @return {string} The joined values of the expressions, or the first expression if there are no strings. */
|
|
196
215
|
getValue(exprs) {
|
|
197
216
|
|
|
198
|
-
//#
|
|
217
|
+
//#IFDEBUG
|
|
199
218
|
assert(Array.isArray(exprs));
|
|
200
219
|
//#ENDIF
|
|
201
220
|
//if (!Array.isArray(exprs))
|
|
202
221
|
// return exprs;
|
|
203
222
|
|
|
204
223
|
if (!this.attrValue) {// If it's not multiple paths inside a single attribute, return first (and only) expression.
|
|
205
|
-
//#
|
|
224
|
+
//#IFDEBUG
|
|
206
225
|
assert(exprs.length === 1);
|
|
207
226
|
//#ENDIF
|
|
208
227
|
return exprs[0];
|
|
@@ -213,6 +232,18 @@ export default class PathToAttribValue extends Path {
|
|
|
213
232
|
for (let i = 0; i < values.length; i++) {
|
|
214
233
|
result.push(values[i]);
|
|
215
234
|
if (i < values.length - 1) {
|
|
235
|
+
// A selection binding has to own the whole attribute, because its whole point is
|
|
236
|
+
// writing that attribute without re-rendering, which it can't do if the rest of
|
|
237
|
+
// the value comes from expressions it doesn't know about. Whether a selector sits
|
|
238
|
+
// inside a multi-part attribute is fixed by the shape of the template and never by
|
|
239
|
+
// the data, so this can only be an authoring mistake, and it always surfaces on the
|
|
240
|
+
// template's very first render -- exactly like the placement check in
|
|
241
|
+
// SelectorRef.bind(). That makes it safe to strip from the built file, where the
|
|
242
|
+
// throw is the only thing lost: makePrimitive() then turns the ref into '' and the
|
|
243
|
+
// attribute is written from its constant parts alone. Stripping it also keeps a
|
|
244
|
+
// per-expression instanceof out of the multi-part attribute loop.
|
|
245
|
+
if (typeof exprs[i] === 'object' && exprs[i] instanceof SelectorRef)
|
|
246
|
+
throw new Error(`Solarite: a selector must own the whole ${this.attribName} attribute.`);
|
|
216
247
|
let val = Util.makePrimitive(exprs[i]);
|
|
217
248
|
if (!Util.isFalsy(val))
|
|
218
249
|
result.push(val);
|
|
@@ -233,17 +264,44 @@ export default class PathToAttribValue extends Path {
|
|
|
233
264
|
/**
|
|
234
265
|
* @param funcAndArgs {?Array} The [func, ...args] array from the template, or null if func stands alone. */
|
|
235
266
|
bindEvent(node, root, key, eventName, func, funcAndArgs, capture=false) {
|
|
267
|
+
//#IFDEBUG
|
|
268
|
+
// Both callers already guarantee a function, so this only catches a future third caller.
|
|
269
|
+
// PathToEvent.applySingle() rejects every shape a template can produce and names the
|
|
270
|
+
// offending value, and the two-way binding path above passes a closure it just made
|
|
271
|
+
// here, so nothing a page author writes can reach this line. That makes it dev-only:
|
|
272
|
+
// stripping it from the built file costs no diagnostic that the surviving throw in
|
|
273
|
+
// PathToEvent doesn't already give, with a better message.
|
|
236
274
|
if (typeof func !== 'function')
|
|
237
|
-
throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.
|
|
275
|
+
throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attribName}=\${${func}}> because it's not a function.`);
|
|
276
|
+
//#ENDIF
|
|
238
277
|
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
|
|
278
|
+
// Delegated path: a bubbling event (when the root's options allow it, the default)
|
|
279
|
+
// stores its handler directly on the node as a per-event-type Symbol expando, with no
|
|
280
|
+
// EventBinding object and no addEventListener call. When an event of that type
|
|
281
|
+
// starts, jitDispatcher() attaches a real listener to each node on its path that
|
|
282
|
+
// carries the expando, so the browser runs the handler at the node's own turn.
|
|
283
|
+
// Re-renders just overwrite the property. this.delegatedKey is set by the PathToEvent
|
|
284
|
+
// constructor only for delegatable event names, so this test also excludes
|
|
285
|
+
// non-bubbling events and native:on* bindings.
|
|
286
|
+
if (capture === false && this.delegatedKey !== undefined) {
|
|
287
|
+
let opt = this.parentNg.rootNg.renderOptions?.eventDelegation ?? true;
|
|
288
|
+
// true delegates everything, an array only the events it names, and any other
|
|
289
|
+
// value (such as the retired 'document' string) counts as true.
|
|
290
|
+
if (opt !== false && (!Array.isArray(opt) || opt.includes(eventName))) {
|
|
291
|
+
let dk = this.delegatedKey;
|
|
292
|
+
if (node[dk] === undefined) // First binding of this type on this node.
|
|
293
|
+
ensureDelegatedDispatcher(root, eventName);
|
|
294
|
+
// Array-form bindings (onclick=${[fn, arg]}, the hot per-row case) store the
|
|
295
|
+
// template's own [func, ...args] array; a plain function is stored bare.
|
|
296
|
+
// Either way, nothing is allocated.
|
|
297
|
+
node[dk] = funcAndArgs || func;
|
|
298
|
+
node[delegatedRootKey] = root;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
243
302
|
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
// a plain function allocates a one-element array, which is rare (buttons, two-way).
|
|
303
|
+
// Direct path: capture bindings, non-bubbling events, and eventDelegation:false.
|
|
304
|
+
// Store the callable as a single [func, ...args] array.
|
|
247
305
|
let args = funcAndArgs || [func];
|
|
248
306
|
|
|
249
307
|
// One stable EventBinding object per node+key is registered with addEventListener
|
|
@@ -253,7 +311,7 @@ export default class PathToAttribValue extends Path {
|
|
|
253
311
|
let nodeEvents = node[eventBindingsKey];
|
|
254
312
|
if (nodeEvents === undefined) {
|
|
255
313
|
let b = node[eventBindingsKey] = new EventBinding(root, node, key, args);
|
|
256
|
-
|
|
314
|
+
node.addEventListener(eventName, b, capture);
|
|
257
315
|
return;
|
|
258
316
|
}
|
|
259
317
|
|
|
@@ -271,7 +329,7 @@ export default class PathToAttribValue extends Path {
|
|
|
271
329
|
let map = node[eventBindingsKey] = {};
|
|
272
330
|
map[nodeEvents.key] = nodeEvents;
|
|
273
331
|
binding = map[key] = new EventBinding(root, node, key, args);
|
|
274
|
-
|
|
332
|
+
node.addEventListener(eventName, binding, capture);
|
|
275
333
|
return;
|
|
276
334
|
}
|
|
277
335
|
}
|
|
@@ -279,11 +337,11 @@ export default class PathToAttribValue extends Path {
|
|
|
279
337
|
binding = nodeEvents[key];
|
|
280
338
|
if (!binding) {
|
|
281
339
|
binding = nodeEvents[key] = new EventBinding(root, node, key, args);
|
|
282
|
-
|
|
340
|
+
node.addEventListener(eventName, binding, capture);
|
|
283
341
|
return;
|
|
284
342
|
}
|
|
285
343
|
}
|
|
286
|
-
binding.
|
|
344
|
+
binding.rootEl = root;
|
|
287
345
|
binding.args = args;
|
|
288
346
|
}
|
|
289
347
|
}
|
|
@@ -304,82 +362,171 @@ export function getEventBinding(node, key) {
|
|
|
304
362
|
return b instanceof EventBinding ? (b.key === key ? b : undefined) : b[key];
|
|
305
363
|
}
|
|
306
364
|
|
|
365
|
+
// Bubbling events the just-in-time dispatcher handles. Same set Solid.js delegates.
|
|
366
|
+
const delegatableEvents = new Set(['beforeinput', 'click', 'contextmenu', 'dblclick', 'focusin', 'focusout',
|
|
367
|
+
'input', 'keydown', 'keyup', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup',
|
|
368
|
+
'pointerdown', 'pointermove', 'pointerout', 'pointerover', 'pointerup', 'touchend', 'touchmove', 'touchstart']);
|
|
369
|
+
|
|
370
|
+
// One Symbol per delegated event type; nodes store their delegated handler under it.
|
|
371
|
+
// Symbols (vs string expandos like Solid's $$click) can't collide with user properties.
|
|
372
|
+
const delegatedKeys = {};
|
|
373
|
+
|
|
307
374
|
/**
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
function
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
delegate = opt === true || opt.includes(eventName);
|
|
322
|
-
}
|
|
375
|
+
* Get the per-event-type Symbol key, or undefined for non-delegatable events.
|
|
376
|
+
* Called once per PathToEvent construction, never per bind.
|
|
377
|
+
* @param eventName {string}
|
|
378
|
+
* @return {symbol|undefined} */
|
|
379
|
+
export function delegatedKeyFor(eventName) {
|
|
380
|
+
if (!delegatableEvents.has(eventName))
|
|
381
|
+
return undefined;
|
|
382
|
+
return delegatedKeys[eventName] ??= Symbol('sol$' + eventName);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// The component root a node's delegated handlers run with as `this`.
|
|
386
|
+
// Exported so NodeGroup.applyStamp()'s compiled stamp program can write it directly.
|
|
387
|
+
export const delegatedRootKey = Symbol('solariteDelegatedRoot');
|
|
323
388
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
389
|
+
// Set of event types that already have the dispatcher registered, kept on each root element
|
|
390
|
+
// and on each document.
|
|
391
|
+
const delegatedTypesKey = Symbol('solariteDelegatedTypes');
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Register the just-in-time dispatcher for eventName on root and on root's document, once
|
|
395
|
+
* each. Shared by bindEvent()'s delegated branch and NodeGroup.applyStamp()'s stamp program.
|
|
396
|
+
*
|
|
397
|
+
* Both registrations are needed. The document's listener is what still reaches a bound node
|
|
398
|
+
* after another component re-parents it outside its root (a toolbar a dock parks in its own
|
|
399
|
+
* chrome). The root's listener is what reaches what the document cannot see: a component
|
|
400
|
+
* that isn't in the document at all, nodes inside a closed shadow root, and a synthetic
|
|
401
|
+
* event dispatched inside any shadow root without composed:true, which never leaves it.
|
|
402
|
+
* @param root {HTMLElement}
|
|
403
|
+
* @param eventName {string} */
|
|
404
|
+
export function ensureDelegatedDispatcher(root, eventName) {
|
|
405
|
+
let types = root[delegatedTypesKey];
|
|
406
|
+
if (types === undefined)
|
|
407
|
+
types = root[delegatedTypesKey] = new Set();
|
|
408
|
+
if (!types.has(eventName)) {
|
|
409
|
+
types.add(eventName);
|
|
410
|
+
root.addEventListener(eventName, jitDispatcher, true);
|
|
411
|
+
|
|
412
|
+
let doc = root.ownerDocument;
|
|
413
|
+
let docTypes = doc[delegatedTypesKey];
|
|
414
|
+
if (docTypes === undefined)
|
|
415
|
+
docTypes = doc[delegatedTypesKey] = new Set();
|
|
416
|
+
if (!docTypes.has(eventName)) {
|
|
417
|
+
docTypes.add(eventName);
|
|
418
|
+
doc.addEventListener(eventName, jitDispatcher, true);
|
|
332
419
|
}
|
|
333
420
|
}
|
|
334
|
-
else
|
|
335
|
-
node.addEventListener(eventName, binding, capture);
|
|
336
421
|
}
|
|
337
422
|
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
423
|
+
// Set on an event by the first dispatcher to walk it, holding the length of the path it saw,
|
|
424
|
+
// so the dispatchers on nested roots further down don't repeat the walk. A root inside a
|
|
425
|
+
// closed shadow root sees a longer path than the document did, because composedPath() hides
|
|
426
|
+
// a closed tree from listeners outside it, and that mismatch is what makes it walk again.
|
|
427
|
+
const delegatedDoneKey = Symbol('solariteDelegated');
|
|
342
428
|
|
|
343
|
-
|
|
344
|
-
|
|
429
|
+
/**
|
|
430
|
+
* One shared bubble-phase listener per event type, attached to a node only for the duration
|
|
431
|
+
* of one event. The browser invokes it at the node's own turn in propagation, and it reads
|
|
432
|
+
* the node's handler THEN rather than when it was attached, so a handler that an earlier
|
|
433
|
+
* listener in the same dispatch replaced or removed is honored.
|
|
434
|
+
* @type {Object<string, {handleEvent: function(Event)}>} */
|
|
435
|
+
const trampolines = {};
|
|
345
436
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
437
|
+
/**
|
|
438
|
+
* @param type {string}
|
|
439
|
+
* @return {{handleEvent: function(Event)}} */
|
|
440
|
+
function trampolineFor(type) {
|
|
441
|
+
let tramp = trampolines[type];
|
|
442
|
+
if (tramp === undefined) {
|
|
443
|
+
let dk = delegatedKeys[type];
|
|
444
|
+
tramp = trampolines[type] = {
|
|
445
|
+
// Quoted so the minifier's property mangling doesn't rename it, since the browser looks it up by name.
|
|
446
|
+
'handleEvent'(ev) {
|
|
447
|
+
let node = ev.currentTarget;
|
|
448
|
+
let a = node[dk];
|
|
449
|
+
if (a === undefined) // Unbound by an earlier handler in this same dispatch.
|
|
450
|
+
return;
|
|
451
|
+
let root = node[delegatedRootKey];
|
|
452
|
+
if (typeof a === 'function')
|
|
453
|
+
a.call(root, ev, node);
|
|
454
|
+
else
|
|
455
|
+
switch (a.length) {
|
|
456
|
+
case 1: a[0].call(root, ev, node); break;
|
|
457
|
+
case 2: a[0].call(root, a[1], ev, node); break;
|
|
458
|
+
case 3: a[0].call(root, a[1], a[2], ev, node); break;
|
|
459
|
+
default: a[0].call(root, ...a.slice(1), ev, node);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
return tramp;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Nodes still carrying a trampoline, per event type, and the one timer that clears them.
|
|
468
|
+
const pending = {};
|
|
469
|
+
let sweepTimer = 0;
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Remove every trampoline attached since the last sweep. Runs as a task, which is always
|
|
473
|
+
* after every dispatch in progress has finished. A microtask would not be: for a real click
|
|
474
|
+
* the browser runs a microtask checkpoint between listeners, so a microtask sweep would strip
|
|
475
|
+
* the trampolines before the event reached the first of them. The sweep is housekeeping
|
|
476
|
+
* only; a trampoline left in place is harmless, because jitDispatcher() re-attaches it and
|
|
477
|
+
* the trampoline reads its handler fresh. */
|
|
478
|
+
function sweep() {
|
|
479
|
+
sweepTimer = 0;
|
|
480
|
+
for (let type in pending) {
|
|
481
|
+
let nodes = pending[type];
|
|
482
|
+
if (nodes.length !== 0) {
|
|
483
|
+
pending[type] = [];
|
|
484
|
+
let tramp = trampolines[type];
|
|
485
|
+
for (let i=0; i<nodes.length; i++)
|
|
486
|
+
nodes[i].removeEventListener(type, tramp);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
349
490
|
|
|
350
491
|
/**
|
|
351
|
-
* The
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
|
|
359
|
-
|
|
492
|
+
* The capture-phase listener registered per delegated event type on every root and on the
|
|
493
|
+
* document. It runs before the event reaches anything, walks the event's path, and attaches
|
|
494
|
+
* the type's trampoline to each node holding a delegated handler. The browser then finishes
|
|
495
|
+
* the dispatch natively, so those handlers interleave correctly with listeners anyone else
|
|
496
|
+
* registered, stopPropagation() works in both directions, currentTarget is right, and the
|
|
497
|
+
* event needn't bubble.
|
|
498
|
+
*
|
|
499
|
+
* Each attach removes the trampoline first. One left from an earlier event in this same task
|
|
500
|
+
* would otherwise keep its old place in the node's listener list, ahead of listeners added
|
|
501
|
+
* since; removing and re-adding puts it last, so the rule holds without exception: a
|
|
502
|
+
* delegated handler runs after every listener its element had when the event started. */
|
|
503
|
+
function jitDispatcher(ev) {
|
|
504
|
+
let path = ev.composedPath();
|
|
505
|
+
if (ev[delegatedDoneKey] === path.length)
|
|
360
506
|
return;
|
|
361
|
-
ev[delegatedDoneKey] =
|
|
507
|
+
ev[delegatedDoneKey] = path.length;
|
|
508
|
+
|
|
362
509
|
let type = ev.type;
|
|
363
|
-
let
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
510
|
+
let dk = delegatedKeys[type];
|
|
511
|
+
let tramp = trampolineFor(type);
|
|
512
|
+
let list = pending[type];
|
|
513
|
+
if (list === undefined)
|
|
514
|
+
list = pending[type] = [];
|
|
515
|
+
for (let i=0; i<path.length; i++) {
|
|
516
|
+
let node = path[i];
|
|
517
|
+
if (node[dk] !== undefined) {
|
|
518
|
+
node.removeEventListener(type, tramp);
|
|
519
|
+
node.addEventListener(type, tramp);
|
|
520
|
+
list.push(node);
|
|
374
521
|
}
|
|
375
|
-
current = current.parentNode;
|
|
376
522
|
}
|
|
377
|
-
|
|
523
|
+
if (list.length !== 0 && sweepTimer === 0)
|
|
524
|
+
sweepTimer = setTimeout(sweep);
|
|
378
525
|
}
|
|
379
526
|
|
|
380
527
|
class EventBinding {
|
|
381
528
|
constructor(root, node, key, args) {
|
|
382
|
-
this.
|
|
529
|
+
this.rootEl = root;
|
|
383
530
|
this.node = node;
|
|
384
531
|
this.key = key;
|
|
385
532
|
|
|
@@ -393,10 +540,10 @@ class EventBinding {
|
|
|
393
540
|
'handleEvent'(event) {
|
|
394
541
|
let a = this.args;
|
|
395
542
|
switch (a.length) {
|
|
396
|
-
case 1: return a[0].call(this.
|
|
397
|
-
case 2: return a[0].call(this.
|
|
398
|
-
case 3: return a[0].call(this.
|
|
543
|
+
case 1: return a[0].call(this.rootEl, event, this.node);
|
|
544
|
+
case 2: return a[0].call(this.rootEl, a[1], event, this.node);
|
|
545
|
+
case 3: return a[0].call(this.rootEl, a[1], a[2], event, this.node);
|
|
399
546
|
}
|
|
400
|
-
return a[0].call(this.
|
|
547
|
+
return a[0].call(this.rootEl, ...a.slice(1), event, this.node);
|
|
401
548
|
}
|
|
402
549
|
}
|
package/src/PathToAttribs.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import Path from "./Path.js";
|
|
2
2
|
import Util from "./Util.js";
|
|
3
|
-
import assert from "./assert.js";
|
|
4
3
|
import PathToAttribValue from "./PathToAttribValue.js";
|
|
5
4
|
import PathToEvent from "./PathToEvent.js";
|
|
6
5
|
import {JsxAttr, styleToCss} from "./jsx.js";
|
|
@@ -11,24 +10,21 @@ export default class PathToAttribs extends Path {
|
|
|
11
10
|
* @type {Set<string>} Used for type=AttribType.Multiple to remember the attributes that were added. */
|
|
12
11
|
attrNames;
|
|
13
12
|
|
|
14
|
-
/** @type {
|
|
15
|
-
|
|
13
|
+
/** @type {PathToEvent|PathToAttribValue|undefined} Cached sub-path for the JSX
|
|
14
|
+
* whole-attribute fast path; see applyJsxAttr(). Declared so the first assignment
|
|
15
|
+
* doesn't transition the hidden class. */
|
|
16
|
+
jsxSub;
|
|
17
|
+
|
|
18
|
+
/** @type {?string} The attribute name jsxSub was built for. */
|
|
19
|
+
jsxSubName;
|
|
16
20
|
|
|
17
21
|
constructor(nodeBefore, nodeMarker) {
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
// nodeBefore is discarded: an attribute path has no nodes of its own. The marker goes
|
|
23
|
+
// straight through the base constructor rather than being stored a second time after it.
|
|
24
|
+
super(null, nodeMarker);
|
|
20
25
|
this.attrNames = new Set();
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
/**
|
|
24
|
-
* @param exprs {Expr[][]} Only the first is used. */
|
|
25
|
-
apply(exprs) {
|
|
26
|
-
//#IFDEV
|
|
27
|
-
assert(Array.isArray(exprs));
|
|
28
|
-
//#ENDIF
|
|
29
|
-
this.applySingle(exprs[0]);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
28
|
/**
|
|
33
29
|
* @param expr {Expr} */
|
|
34
30
|
applySingle(expr) {
|
|
@@ -107,8 +103,4 @@ export default class PathToAttribs extends Path {
|
|
|
107
103
|
value = styleToCss(value);
|
|
108
104
|
sub.applySingle(value);
|
|
109
105
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
getExpressionCount() { return 1 }
|
|
113
|
-
getValue(exprs) { return exprs[0]; }
|
|
114
106
|
}
|