solarite 0.5.2 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/dist/Solarite-debug.js +4847 -3878
  3. package/dist/Solarite.js +4583 -3626
  4. package/dist/Solarite.min.js +2 -4
  5. package/package.json +19 -6
  6. package/readme.md +58 -11
  7. package/src/Globals.js +54 -72
  8. package/src/HtmlParser.js +90 -90
  9. package/src/MultiValueMap.js +57 -105
  10. package/src/NodeGroup.js +624 -470
  11. package/src/Path.js +224 -211
  12. package/src/PathToAttribValue.js +401 -261
  13. package/src/PathToAttribs.js +113 -80
  14. package/src/PathToComment.js +7 -7
  15. package/src/PathToComponent.js +183 -188
  16. package/src/PathToEvent.js +76 -64
  17. package/src/PathToKey.js +19 -0
  18. package/src/PathToNodes.js +1053 -566
  19. package/src/RootNodeGroup.js +120 -8
  20. package/src/Shell.js +570 -348
  21. package/src/Solarite.d.ts +134 -113
  22. package/src/Solarite.js +243 -286
  23. package/src/Template.js +195 -274
  24. package/src/Util.js +353 -351
  25. package/src/assert.js +10 -10
  26. package/src/assignAttributes.js +63 -0
  27. package/src/delve.js +55 -43
  28. package/src/h.js +220 -138
  29. package/src/jsx-dev-runtime.d.ts +1 -0
  30. package/src/jsx-dev-runtime.js +5 -0
  31. package/src/jsx-runtime.d.ts +21 -0
  32. package/src/jsx-runtime.js +84 -0
  33. package/src/jsx.js +194 -0
  34. package/src/toEl.js +77 -82
  35. package/dist/udomdiff-license.txt +0 -18
  36. package/src/getArg.js +0 -137
  37. package/src/hash.js +0 -89
  38. package/src/udomdiff.js +0 -176
  39. package/src/unused/FastLookupArray.js +0 -54
  40. package/src/unused/Hashes.js +0 -339
  41. package/src/unused/InUse.test.js +0 -92
  42. package/src/unused/InUseMap.js +0 -98
  43. package/src/unused/LinkedList.js +0 -117
  44. package/src/unused/LinkedList.test.js +0 -115
  45. package/src/unused/Misc.js +0 -13
  46. package/src/unused/Perf.js +0 -47
  47. package/src/unused/TrackedArray.js +0 -54
  48. package/src/unused/WeakArray.js +0 -33
  49. package/src/watch.js +0 -543
@@ -1,262 +1,402 @@
1
- import Path from "./Path.js";
2
- import Globals from "./Globals.js";
3
- import Util from "./Util.js";
4
- import delve from "./delve.js";
5
- import assert from "./assert.js";
6
-
7
- export default class PathToAttribValue extends Path {
8
-
9
- /** @type {?string} Used only if type=AttribType.Value. */
10
- attrName;
11
-
12
- /**
13
- * @type {?string[]} Used only if type=AttribType.Value. If null, use one expr to set the whole attribute value. */
14
- attrValue;
15
-
16
- /** @type {boolean} Provides value for attribute on a component. */
17
- isComponent;
18
-
19
- isHtmlProperty;
20
-
21
- constructor(nodeBefore, nodeMarker, attrName=null, attrValue=null) {
22
- super(null, nodeMarker);
23
- this.attrName = attrName;
24
- this.attrValue = attrValue;
25
- }
26
-
27
- /**
28
- * Set the value of an attribute. This can be for any attribute, not just attributes named "value".
29
- * @param exprs {Expr[]} */
30
- apply(exprs) {
31
- //#IFDEV
32
- assert(Array.isArray(exprs));
33
- //#ENDIF
34
-
35
- let node = this.nodeMarker;
36
- let expr = exprs[0];
37
-
38
- let multiple = this.attrValue;
39
-
40
- // Two-way binding between attributes
41
- // Passing a path to the value attribute.
42
- // Copies the attribute to the property when the input event fires.
43
- // value=${[this, 'value]'}
44
- // checked=${[this, 'isAgree']}
45
- // This same logic is in NodeGroup.instantiateComponent() for components.
46
- if (!multiple && Util.isPath(expr)) {
47
-
48
- // Don't bind events to component placeholders.
49
- // PathToComponent will do the binding later when it instantiates the component.
50
- if (this.isComponentAttrib && node.tagName.endsWith('-SOLARITE-PLACEHOLDER'))
51
- return;
52
-
53
- /** @type {[Object, string[]]} */
54
- let [obj, path] = [expr[0], expr.slice(1)];
55
-
56
- if (!obj)
57
- throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${[${expr.map(item => item ? `'${item}'` : item+'').join(', ')}]}>.`);
58
-
59
- let value = delve(obj, path);
60
-
61
- // Special case to allow setting select-multiple value from an array
62
- if (this.attrName === 'value' && node.type === 'select-multiple' && Array.isArray(value)) {
63
- // Set the .selected property on the options having a value within value.
64
- let strValues = value.map(v => v + '');
65
- for (let option of node.options)
66
- option.selected = strValues.includes(option.value)
67
- }
68
- else {
69
- // TODO: should we remove isFalsy, since these are always props?
70
- const strValue = Util.isFalsy(value) ? '' : value;
71
-
72
- // Special case for contenteditable
73
- if (this.attrName === 'value' && node.hasAttribute('contenteditable')) {
74
- const existingValue = node.innerHTML;
75
- if (strValue !== existingValue)
76
- node.innerHTML = strValue;
77
- }
78
- else {
79
-
80
- // If we don't have this condition, when we call render(), the browser will scroll to the currently
81
- // selected item in a <select> and mess up manually scrolling to a different value.
82
- if (strValue !== node[this.attrName])
83
- node[this.attrName] = strValue;
84
- }
85
- }
86
-
87
- // TODO: We need to remove any old listeners, like in bindEventAttribute.
88
- // Does bindEvent() now handle that?
89
- let func = () => {
90
- let value = (this.attrName === 'value')
91
- ? Util.getInputValue(node)
92
- : node[this.attrName];
93
- delve(obj, path, value);
94
- }
95
-
96
- // We use capture so we update the values before other events added by the user.
97
- // TODO: Bind to scroll events also?
98
- // What about resize events and width/height?
99
- this.bindEvent(node, this.parentNg.getRootNode(), this.attrName, 'input', func, [], true);
100
- }
101
-
102
- // Regular attribute
103
- else {
104
- // Cache this on Path.isHtmlProperty when Shell creates the props.
105
- // Have Path.clone() copy .isHtmlProperty?
106
- let isProp = this.isHtmlProperty;
107
-
108
- // Values to toggle an attribute
109
- if (!multiple) {
110
- Globals.currentPath = this; // Used by watch()
111
- if (typeof expr === 'function') {
112
- if (this.isComponentAttrib)
113
- return;
114
-
115
- this.watchFunction = expr; // The function that gets the expression, used for renderWatched()
116
- expr = expr();
117
- }
118
- else
119
- expr = Util.makePrimitive(expr);
120
- Globals.currentPath = null;
121
- }
122
-
123
-
124
- if (!multiple && (expr === undefined || expr === false || expr === null)) { // Util.isFalsy() inlined.
125
- if (isProp)
126
- node[this.attrName] = false;
127
- node.removeAttribute(this.attrName);
128
- }
129
- else if (!multiple && expr === true) {
130
- if (isProp)
131
- node[this.attrName] = true;
132
- node.setAttribute(this.attrName, '');
133
- }
134
-
135
- // A non-toggled attribute
136
- else {
137
-
138
- // If it's a series of expressions among strings, join them together.
139
- let joinedValue = multiple // avoid function call if there are no strings
140
- ? this.getValue(exprs)
141
- : expr // If the attribute is one expression with no strings
142
-
143
- // Only update attributes if the value has changed.
144
- // This is needed for setting input.value, .checked, option.selected, etc.
145
- let oldVal = isProp
146
- ? node[this.attrName]
147
- : node.getAttribute(this.attrName);
148
- if (oldVal !== joinedValue) {
149
-
150
- // <textarea value=${expr}></textarea>
151
- // Without this branch we have no way to set the value of a textarea,
152
- // since we also prohibit expressions that are a child of textarea.
153
- if (isProp)
154
- node[this.attrName] = joinedValue;
155
-
156
- // Allow one-way binding to contenteditable value attribute.
157
- // Contenteditables normally don't have a value attribute and have their content set via innerHTML.
158
- // Solarite doesn't allow contenteditables to have expressions as their children.
159
- else if (this.attrName === 'value' && node.hasAttribute('contenteditable')) {
160
- node.innerHTML = joinedValue;
161
- }
162
-
163
- // TODO: Putting an 'else' here would be more performant
164
- node.setAttribute(this.attrName, joinedValue);
165
- }
166
- }
167
- }
168
- }
169
-
170
-
171
- getExpressionCount() { return this.attrValue ? this.attrValue.length-1 : 1 }
172
-
173
- /**
174
- * @param exprs {Expr|Expr[]} // TODO: Why is this sometimes not an array?
175
- * @return {string} The joined values of the expressions, or the first expression if there are no strings. */
176
- getValue(exprs) {
177
-
178
- //#IFDEV
179
- assert(Array.isArray(exprs));
180
- //#ENDIF
181
- //if (!Array.isArray(exprs))
182
- // return exprs;
183
-
184
- if (!this.attrValue) {// If it's not multiple paths inside a single attribute, return first (and only) expression.
185
- //#IFDEV
186
- assert(exprs.length === 1);
187
- //#ENDIF
188
- return exprs[0];
189
- }
190
-
191
- let result = [];
192
- let values = this.attrValue;
193
- for (let i = 0; i < values.length; i++) {
194
- result.push(values[i]);
195
- if (i < values.length - 1) {
196
- Globals.currentPath = this; // Used by watch()
197
- let val = Util.makePrimitive(exprs[i]);
198
- Globals.currentPath = null;
199
- if (!Util.isFalsy(val))
200
- result.push(val);
201
- }
202
- }
203
- return result.join('')
204
- }
205
-
206
- /**
207
- * Call function when eventName is triggerd on node.
208
- * @param node {HTMLElement}
209
- * @param root {HTMLElement}
210
- * @param key {string}
211
- * @param eventName {string}
212
- * @param func {function}
213
- * @param args {array}
214
- * @param capture {boolean} */
215
- bindEvent(node, root, key, eventName, func, args, capture=false) {
216
- let nodeEvents = Globals.nodeEvents.get(node);
217
- if (!nodeEvents) {
218
- nodeEvents = {[key]: new Array(3)};
219
- Globals.nodeEvents.set(node, nodeEvents);
220
- }
221
- let nodeEvent = nodeEvents[key];
222
- if (!nodeEvent)
223
- nodeEvents[key] = nodeEvent = new Array(3);
224
-
225
- if (typeof func !== 'function')
226
- throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${${func}}> because it's not a function.`);
227
-
228
- // If function has changed, remove and rebind the event.
229
- if (nodeEvent[0] !== func) {
230
-
231
- // TODO: We should be removing event listeners when calling getNodeGroup(),
232
- // when we get the node from the list of nodeGroupsAttached/nodeGroupsDetached,
233
- // instead of only when we rebind an event.
234
- let [existing, existingBound, _] = nodeEvent;
235
- if (existing)
236
- node.removeEventListener(eventName, existingBound, capture);
237
-
238
- let originalFunc = func;
239
-
240
- // BoundFunc sets the "this" variable to be the current Solarite component.
241
- let boundFunc = (event) => {
242
- let args = nodeEvent[2];
243
- return originalFunc.call(root, ...args, event, node);
244
- }
245
-
246
- // Save both the original and bound functions.
247
- // Original so we can compare it against a newly assigned function.
248
- // Bound so we can use it with removeEventListner().
249
- nodeEvent[0] = originalFunc;
250
- nodeEvent[1] = boundFunc;
251
-
252
- node.addEventListener(eventName, boundFunc, capture);
253
-
254
- // TODO: classic event attribs?
255
- //el[attr.name] = e => // e.g. el.onclick = ... // put "event", "el", and "this" in scope for the event code.
256
- // (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el)
257
- }
258
-
259
- // Otherwise just update the args to the function.
260
- nodeEvents[key][2] = args;
261
- }
1
+ import Path from "./Path.js";
2
+ import Util from "./Util.js";
3
+ import delve, {isDelvePath} from "./delve.js";
4
+ import assert from "./assert.js";
5
+
6
+ export default class PathToAttribValue extends Path {
7
+
8
+ /** @type {?string} Used only if type=AttribType.Value. */
9
+ attrName;
10
+
11
+ /**
12
+ * @type {?string[]} Used only if type=AttribType.Value. If null, use one expr to set the whole attribute value. */
13
+ attrValue;
14
+
15
+ /** @type {boolean} Provides value for attribute on a component. */
16
+ isComponent;
17
+
18
+ isHtmlProperty;
19
+
20
+ constructor(nodeBefore, nodeMarker, attrName=null, attrValue=null) {
21
+ super(null, nodeMarker);
22
+ this.attrName = attrName;
23
+ this.attrValue = attrValue;
24
+ }
25
+
26
+ /**
27
+ * Set the value of an attribute. This can be for any attribute, not just attributes named "value".
28
+ * @param exprs {Expr[]} */
29
+ apply(exprs) {
30
+ //#IFDEV
31
+ assert(Array.isArray(exprs));
32
+ //#ENDIF
33
+
34
+ // Multiple expressions in one attribute value, e.g. class="a ${b} c ${d}"
35
+ if (this.attrValue) {
36
+ let node = this.nodeMarker;
37
+ let joinedValue = this.getValue(exprs);
38
+ let isProp = this.isHtmlProperty;
39
+
40
+ // Only update attributes if the value has changed.
41
+ // This is needed for setting input.value, .checked, option.selected, etc.
42
+ let oldVal = isProp
43
+ ? node[this.attrName]
44
+ : node.getAttribute(this.attrName);
45
+ if (oldVal !== joinedValue) {
46
+ if (isProp)
47
+ node[this.attrName] = joinedValue;
48
+ else if (this.attrName === 'value' && node.hasAttribute('contenteditable'))
49
+ node.innerHTML = joinedValue;
50
+ node.setAttribute(this.attrName, joinedValue);
51
+ }
52
+ }
53
+ else
54
+ this.applySingle(exprs[0]);
55
+ }
56
+
57
+ /**
58
+ * Set the attribute from a single expression that makes up its whole value.
59
+ * @param expr {Expr} */
60
+ applySingle(expr) {
61
+ // One expression surrounded by strings, e.g. class="a ${b} c". Join through apply().
62
+ if (this.attrValue)
63
+ return this.apply([expr]);
64
+
65
+ let node = this.nodeMarker;
66
+
67
+ // Two-way binding between attributes
68
+ // Passing a path to the value attribute.
69
+ // Copies the attribute to the property when the input event fires.
70
+ // value=${[this, 'value]'}
71
+ // checked=${[this, 'isAgree']}
72
+ // This same logic is in NodeGroup.instantiateComponent() for components.
73
+ if (isDelvePath(expr)) {
74
+
75
+ // Don't bind events to component placeholders.
76
+ // PathToComponent will do the binding later when it instantiates the component.
77
+ if (this.isComponentAttrib && node.tagName.endsWith('-SOLARITE-PLACEHOLDER'))
78
+ return;
79
+
80
+ /** @type {[Object, string[]]} */
81
+ let [obj, path] = [expr[0], expr.slice(1)];
82
+
83
+ if (!obj)
84
+ throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${[${expr.map(item => item ? `'${item}'` : item+'').join(', ')}]}>.`);
85
+
86
+ let value = delve(obj, path);
87
+
88
+ // Special case to allow setting select-multiple value from an array
89
+ if (this.attrName === 'value' && node.type === 'select-multiple' && Array.isArray(value)) {
90
+ // Set the .selected property on the options having a value within value.
91
+ let strValues = value.map(v => v + '');
92
+ for (let option of node.options)
93
+ option.selected = strValues.includes(option.value)
94
+ }
95
+
96
+ // Radio group: this radio is checked when its value matches the bound model value.
97
+ else if (node.type === 'radio')
98
+ node.checked = node.value === (value + '');
99
+
100
+ else {
101
+ // TODO: should we remove isFalsy, since these are always props?
102
+ const strValue = Util.isFalsy(value) ? '' : value;
103
+
104
+ // Special case for contenteditable
105
+ if (this.attrName === 'value' && node.hasAttribute('contenteditable')) {
106
+ const existingValue = node.innerHTML;
107
+ if (strValue !== existingValue)
108
+ node.innerHTML = strValue;
109
+ }
110
+ else {
111
+
112
+ // If we don't have this condition, when we call render(), the browser will scroll to the currently
113
+ // selected item in a <select> and mess up manually scrolling to a different value.
114
+ if (strValue !== node[this.attrName])
115
+ node[this.attrName] = strValue;
116
+ }
117
+ }
118
+
119
+ // TODO: We need to remove any old listeners, like in bindEventAttribute.
120
+ // Does bindEvent() now handle that?
121
+ let func = () => {
122
+ let value = (this.attrName === 'value' || node.type === 'radio')
123
+ ? Util.getInputValue(node)
124
+ : node[this.attrName];
125
+ delve(obj, path, value);
126
+ }
127
+
128
+ // We use capture so we update the values before other events added by the user.
129
+ // TODO: Bind to scroll events also?
130
+ // What about resize events and width/height?
131
+ this.bindEvent(node, this.parentNg.getRootNode(), this.attrName, 'input', func, null, true);
132
+ }
133
+
134
+ // Regular attribute
135
+ else {
136
+ // Cache this on Path.isHtmlProperty when Shell creates the props.
137
+ // Have Path.clone() copy .isHtmlProperty?
138
+ let isProp = this.isHtmlProperty;
139
+
140
+ if (typeof expr === 'function') {
141
+ if (this.isComponentAttrib)
142
+ return;
143
+ expr = expr();
144
+ }
145
+ else
146
+ expr = Util.makePrimitive(expr);
147
+
148
+ // Values to toggle an attribute
149
+ if (expr === undefined || expr === false || expr === null) { // Util.isFalsy() inlined.
150
+ if (isProp)
151
+ node[this.attrName] = false;
152
+ node.removeAttribute(this.attrName);
153
+ }
154
+ else if (expr === true) {
155
+ if (isProp)
156
+ node[this.attrName] = true;
157
+ node.setAttribute(this.attrName, '');
158
+ }
159
+
160
+ // A non-toggled attribute
161
+ else {
162
+ // Only update attributes if the value has changed.
163
+ // This is needed for setting input.value, .checked, option.selected, etc.
164
+ // A missing attribute counts as '', so empty values don't write empty attributes.
165
+ let oldVal = isProp
166
+ ? node[this.attrName]
167
+ : node.getAttribute(this.attrName) ?? '';
168
+ if (oldVal !== expr) {
169
+
170
+ // <textarea value=${expr}></textarea>
171
+ // Without this branch we have no way to set the value of a textarea,
172
+ // since we also prohibit expressions that are a child of textarea.
173
+ if (isProp)
174
+ node[this.attrName] = expr;
175
+
176
+ // Allow one-way binding to contenteditable value attribute.
177
+ // Contenteditables normally don't have a value attribute and have their content set via innerHTML.
178
+ // Solarite doesn't allow contenteditables to have expressions as their children.
179
+ else if (this.attrName === 'value' && node.hasAttribute('contenteditable')) {
180
+ node.innerHTML = expr;
181
+ }
182
+
183
+ // TODO: Putting an 'else' here would be more performant
184
+ node.setAttribute(this.attrName, expr);
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+
191
+ getExpressionCount() { return this.attrValue ? this.attrValue.length-1 : 1 }
192
+
193
+ /**
194
+ * @param exprs {Expr|Expr[]} // TODO: Why is this sometimes not an array?
195
+ * @return {string} The joined values of the expressions, or the first expression if there are no strings. */
196
+ getValue(exprs) {
197
+
198
+ //#IFDEV
199
+ assert(Array.isArray(exprs));
200
+ //#ENDIF
201
+ //if (!Array.isArray(exprs))
202
+ // return exprs;
203
+
204
+ if (!this.attrValue) {// If it's not multiple paths inside a single attribute, return first (and only) expression.
205
+ //#IFDEV
206
+ assert(exprs.length === 1);
207
+ //#ENDIF
208
+ return exprs[0];
209
+ }
210
+
211
+ let result = [];
212
+ let values = this.attrValue;
213
+ for (let i = 0; i < values.length; i++) {
214
+ result.push(values[i]);
215
+ if (i < values.length - 1) {
216
+ let val = Util.makePrimitive(exprs[i]);
217
+ if (!Util.isFalsy(val))
218
+ result.push(val);
219
+ }
220
+ }
221
+ return result.join('')
222
+ }
223
+
224
+ /**
225
+ * Call function when eventName is triggerd on node.
226
+ * @param node {HTMLElement}
227
+ * @param root {HTMLElement}
228
+ * @param key {string}
229
+ * @param eventName {string}
230
+ * @param func {function}
231
+ * @param args {array}
232
+ * @param capture {boolean} */
233
+ /**
234
+ * @param funcAndArgs {?Array} The [func, ...args] array from the template, or null if func stands alone. */
235
+ bindEvent(node, root, key, eventName, func, funcAndArgs, capture=false) {
236
+ if (typeof func !== 'function')
237
+ throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${${func}}> because it's not a function.`);
238
+
239
+ // Whether to delegate is decided in registerBinding(), which only runs for a NEW binding.
240
+ // Re-renders rebind existing rows (just updating binding.args below), so they skip the
241
+ // options lookup + delegatableEvents check entirely.
242
+ let options = this.parentNg.rootNg.options;
243
+
244
+ // Store the callable as a single [func, ...args] array. Array-form bindings
245
+ // (onclick=${[fn, arg]}, the hot per-row case) pass it through with no allocation;
246
+ // a plain function allocates a one-element array, which is rare (buttons, two-way).
247
+ let args = funcAndArgs || [func];
248
+
249
+ // One stable EventBinding object per node+key is registered with addEventListener
250
+ // and dispatches to the current args. This way, assigning a new function
251
+ // (e.g. a fresh arrow function on each render) never needs add/removeEventListener.
252
+ // Most nodes have one binding, stored directly; a second key upgrades to a map.
253
+ let nodeEvents = node[eventBindingsKey];
254
+ if (nodeEvents === undefined) {
255
+ let b = node[eventBindingsKey] = new EventBinding(root, node, key, args);
256
+ registerBinding(b, node, eventName, capture, options, root);
257
+ return;
258
+ }
259
+
260
+ let binding;
261
+
262
+ // The node already has a single EventBinding stored directly at node[eventBindingsKey].
263
+ // If it's for this same key (e.g. 'click' rebound on re-render), just update it below.
264
+ // Otherwise this is the node's second event key, so upgrade the slot to a
265
+ // {key: EventBinding} map holding both. Nodes with one handler (the common case)
266
+ // never pay for that map object.
267
+ if (nodeEvents instanceof EventBinding) {
268
+ if (nodeEvents.key === key)
269
+ binding = nodeEvents;
270
+ else {
271
+ let map = node[eventBindingsKey] = {};
272
+ map[nodeEvents.key] = nodeEvents;
273
+ binding = map[key] = new EventBinding(root, node, key, args);
274
+ registerBinding(binding, node, eventName, capture, options, root);
275
+ return;
276
+ }
277
+ }
278
+ else {
279
+ binding = nodeEvents[key];
280
+ if (!binding) {
281
+ binding = nodeEvents[key] = new EventBinding(root, node, key, args);
282
+ registerBinding(binding, node, eventName, capture, options, root);
283
+ return;
284
+ }
285
+ }
286
+ binding.root = root;
287
+ binding.args = args;
288
+ }
289
+ }
290
+
291
+ const eventBindingsKey = Symbol('solariteEvents');
292
+
293
+ /**
294
+ * Get the EventBinding registered for a node+key, or undefined.
295
+ * Lets a component invoke its own two-way binding (e.g. flush a bound value before
296
+ * dispatching a change event) without exposing the private storage Symbol.
297
+ * @param node {Node}
298
+ * @param key {string}
299
+ * @return {EventBinding|undefined} */
300
+ export function getEventBinding(node, key) {
301
+ let b = node[eventBindingsKey];
302
+ if (b === undefined)
303
+ return undefined;
304
+ return b instanceof EventBinding ? (b.key === key ? b : undefined) : b[key];
305
+ }
306
+
307
+ /**
308
+ * Attach a new EventBinding either directly or through the root component's delegated
309
+ * dispatcher. The dispatcher lives on the root element (not the document) so a component
310
+ * still receives delegated events while detached from the document, and events stay scoped
311
+ * to the component that rendered them. */
312
+ function registerBinding(binding, node, eventName, capture, options, root) {
313
+ // Bubbling events are delegated by default: they skip addEventListener entirely, and one
314
+ // root-level dispatcher per event type finds bindings by walking up from the event target.
315
+ // eventDelegation:false opts out; an array delegates only the named events. Capture
316
+ // bindings and non-bubbling events always stay direct.
317
+ let delegate = false;
318
+ if (capture === false) {
319
+ let opt = options?.eventDelegation ?? true;
320
+ if (opt !== false && delegatableEvents.has(eventName))
321
+ delegate = opt === true || opt.includes(eventName);
322
+ }
323
+
324
+ if (delegate) {
325
+ binding.delegated = true;
326
+ let types = root[delegatedTypesKey];
327
+ if (types === undefined)
328
+ types = root[delegatedTypesKey] = new Set();
329
+ if (!types.has(eventName)) {
330
+ types.add(eventName);
331
+ root.addEventListener(eventName, delegatedDispatcher);
332
+ }
333
+ }
334
+ else
335
+ node.addEventListener(eventName, binding, capture);
336
+ }
337
+
338
+ // Bubbling events that one root-level listener can dispatch. Same set Solid.js delegates.
339
+ const delegatableEvents = new Set(['beforeinput', 'click', 'contextmenu', 'dblclick', 'focusin', 'focusout',
340
+ 'input', 'keydown', 'keyup', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup',
341
+ 'pointerdown', 'pointermove', 'pointerout', 'pointerover', 'pointerup', 'touchend', 'touchmove', 'touchstart']);
342
+
343
+ // Per-root-element Set of event types that already have a delegated dispatcher registered.
344
+ const delegatedTypesKey = Symbol('solariteDelegatedTypes');
345
+
346
+ // Marks an event the innermost root dispatcher has already walked, so an outer root's
347
+ // listener (when components are nested) skips it instead of dispatching the bindings again.
348
+ const delegatedDoneKey = Symbol('solariteDelegated');
349
+
350
+ /**
351
+ * The per-root listener for each delegated event type. The first (innermost) root the
352
+ * bubbling event reaches walks from the event target upward, invoking delegated
353
+ * EventBindings stored on the nodes along the way; outer roots then see the done-marker and
354
+ * skip. Each binding carries its own root, so handlers in an outer component still run with
355
+ * the correct `this`. event.currentTarget is patched to the node whose binding is running,
356
+ * and restored after. stopPropagation() inside a handler ends the walk, mirroring native
357
+ * bubbling. */
358
+ function delegatedDispatcher(ev) {
359
+ if (ev[delegatedDoneKey])
360
+ return;
361
+ ev[delegatedDoneKey] = true;
362
+ let type = ev.type;
363
+ let current = ev.target;
364
+ Object.defineProperty(ev, 'currentTarget', {configurable: true, get() { return current }});
365
+ while (current) {
366
+ let b = current[eventBindingsKey];
367
+ if (b !== undefined) {
368
+ let binding = b instanceof EventBinding ? b : b[type];
369
+ if (binding !== undefined && binding.delegated === true && binding.key === type) {
370
+ binding.handleEvent(ev);
371
+ if (ev.cancelBubble)
372
+ break;
373
+ }
374
+ }
375
+ current = current.parentNode;
376
+ }
377
+ delete ev.currentTarget; // Restore the native getter from the prototype.
378
+ }
379
+
380
+ class EventBinding {
381
+ constructor(root, node, key, args) {
382
+ this.root = root;
383
+ this.node = node;
384
+ this.key = key;
385
+
386
+ /** @type {Array} [func, ...args]; always at least [func]. */
387
+ this.args = args;
388
+ }
389
+
390
+ // Called by the browser via the addEventListener(name, object) form.
391
+ // Sets the "this" variable to be the current Solarite component.
392
+ // Quoted so the minifier's property mangling doesn't rename it, since the browser looks it up by name.
393
+ 'handleEvent'(event) {
394
+ let a = this.args;
395
+ switch (a.length) {
396
+ case 1: return a[0].call(this.root, event, this.node);
397
+ case 2: return a[0].call(this.root, a[1], event, this.node);
398
+ case 3: return a[0].call(this.root, a[1], a[2], event, this.node);
399
+ }
400
+ return a[0].call(this.root, ...a.slice(1), event, this.node);
401
+ }
262
402
  }