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.
- package/LICENSE +21 -0
- package/dist/Solarite-debug.js +4847 -3878
- package/dist/Solarite.js +4583 -3626
- package/dist/Solarite.min.js +2 -4
- package/package.json +19 -6
- package/readme.md +58 -11
- package/src/Globals.js +54 -72
- package/src/HtmlParser.js +90 -90
- package/src/MultiValueMap.js +57 -105
- package/src/NodeGroup.js +624 -470
- package/src/Path.js +224 -211
- package/src/PathToAttribValue.js +401 -261
- package/src/PathToAttribs.js +113 -80
- package/src/PathToComment.js +7 -7
- package/src/PathToComponent.js +183 -188
- package/src/PathToEvent.js +76 -64
- package/src/PathToKey.js +19 -0
- package/src/PathToNodes.js +1053 -566
- package/src/RootNodeGroup.js +120 -8
- package/src/Shell.js +570 -348
- package/src/Solarite.d.ts +134 -113
- package/src/Solarite.js +243 -286
- package/src/Template.js +195 -274
- package/src/Util.js +353 -351
- package/src/assert.js +10 -10
- package/src/assignAttributes.js +63 -0
- package/src/delve.js +55 -43
- package/src/h.js +220 -138
- package/src/jsx-dev-runtime.d.ts +1 -0
- package/src/jsx-dev-runtime.js +5 -0
- package/src/jsx-runtime.d.ts +21 -0
- package/src/jsx-runtime.js +84 -0
- package/src/jsx.js +194 -0
- package/src/toEl.js +77 -82
- package/dist/udomdiff-license.txt +0 -18
- package/src/getArg.js +0 -137
- package/src/hash.js +0 -89
- package/src/udomdiff.js +0 -176
- package/src/unused/FastLookupArray.js +0 -54
- package/src/unused/Hashes.js +0 -339
- package/src/unused/InUse.test.js +0 -92
- package/src/unused/InUseMap.js +0 -98
- package/src/unused/LinkedList.js +0 -117
- package/src/unused/LinkedList.test.js +0 -115
- package/src/unused/Misc.js +0 -13
- package/src/unused/Perf.js +0 -47
- package/src/unused/TrackedArray.js +0 -54
- package/src/unused/WeakArray.js +0 -33
- package/src/watch.js +0 -543
package/src/PathToAttribValue.js
CHANGED
|
@@ -1,262 +1,402 @@
|
|
|
1
|
-
import Path from "./Path.js";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
this.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
}
|