@shortfuse/materialdesignweb 0.7.6 → 0.8.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/README.md +57 -68
- package/components/Badge.js +2 -2
- package/components/BottomAppBar.js +3 -5
- package/components/Box.js +33 -3
- package/components/Button.js +48 -21
- package/components/Button.md +9 -9
- package/components/Card.js +9 -16
- package/components/Checkbox.js +45 -36
- package/components/CheckboxIcon.js +2 -2
- package/components/Chip.js +1 -1
- package/components/Dialog.js +228 -359
- package/components/DialogActions.js +2 -2
- package/components/Divider.js +3 -3
- package/components/ExtendedFab.js +4 -8
- package/components/Fab.js +1 -2
- package/components/FilterChip.js +4 -4
- package/components/Headline.js +1 -1
- package/components/Icon.js +8 -8
- package/components/IconButton.js +9 -14
- package/components/Input.js +273 -1
- package/components/Layout.js +485 -16
- package/components/List.js +6 -4
- package/components/ListItem.js +12 -12
- package/components/ListOption.js +21 -5
- package/components/Listbox.js +239 -0
- package/components/Menu.js +77 -526
- package/components/MenuItem.js +12 -14
- package/components/Nav.js +0 -2
- package/components/NavBar.js +8 -79
- package/components/NavDrawer.js +12 -11
- package/components/NavDrawerItem.js +2 -1
- package/components/NavItem.js +18 -8
- package/components/NavRail.js +15 -7
- package/components/NavRailItem.js +3 -1
- package/components/Popup.js +20 -0
- package/components/Progress.js +24 -23
- package/components/Radio.js +42 -35
- package/components/RadioIcon.js +3 -3
- package/components/Ripple.js +2 -3
- package/components/Search.js +85 -0
- package/components/SegmentedButton.js +1 -10
- package/components/SegmentedButtonGroup.js +16 -10
- package/components/Select.js +4 -4
- package/components/Shape.js +1 -1
- package/components/Slider.js +43 -50
- package/components/Snackbar.js +4 -5
- package/components/Surface.js +3 -3
- package/components/Switch.js +55 -21
- package/components/SwitchIcon.js +10 -8
- package/components/Tab.js +11 -9
- package/components/TabContent.js +4 -3
- package/components/TabList.js +2 -2
- package/components/TabPanel.js +11 -8
- package/components/TextArea.js +38 -35
- package/components/Tooltip.js +2 -2
- package/components/TopAppBar.js +65 -147
- package/core/Composition.js +985 -628
- package/core/CompositionAdapter.js +315 -0
- package/core/CustomElement.js +153 -90
- package/core/DomAdapter.js +586 -0
- package/core/ICustomElement.d.ts +2 -2
- package/core/css.js +8 -7
- package/core/customTypes.js +53 -31
- package/{utils → core}/jsonMergePatch.js +36 -14
- package/core/observe.js +111 -57
- package/core/optimizations.js +23 -0
- package/core/template.js +17 -11
- package/core/test.js +126 -0
- package/core/typings.d.ts +11 -5
- package/core/uid.js +13 -0
- package/dist/index.min.js +83 -152
- package/dist/index.min.js.map +4 -4
- package/dist/meta.json +1 -1
- package/mixins/AriaReflectorMixin.js +1 -2
- package/mixins/AriaToolbarMixin.js +2 -3
- package/mixins/ControlMixin.js +25 -17
- package/mixins/DensityMixin.js +0 -1
- package/mixins/FlexableMixin.js +1 -2
- package/mixins/FormAssociatedMixin.js +13 -10
- package/mixins/InputMixin.js +2 -9
- package/mixins/KeyboardNavMixin.js +14 -1
- package/mixins/PopupMixin.js +757 -0
- package/mixins/RTLObserverMixin.js +0 -1
- package/mixins/ResizeObserverMixin.js +0 -1
- package/mixins/RippleMixin.js +3 -4
- package/mixins/ScrollListenerMixin.js +41 -32
- package/mixins/SemiStickyMixin.js +151 -0
- package/mixins/ShapeMixin.js +29 -24
- package/mixins/StateMixin.js +11 -6
- package/mixins/SurfaceMixin.js +3 -57
- package/mixins/TextFieldMixin.js +57 -65
- package/mixins/ThemableMixin.js +78 -156
- package/mixins/TooltipTriggerMixin.js +7 -13
- package/mixins/TouchTargetMixin.js +4 -3
- package/package.json +9 -5
- package/theming/index.js +1 -1
- package/theming/themableMixinLoader.js +12 -0
- package/utils/{hct → material-color}/blend.js +8 -10
- package/utils/{hct → material-color/hct}/Cam16.js +196 -69
- package/utils/{hct → material-color/hct}/Hct.js +61 -19
- package/utils/{hct → material-color/hct}/ViewingConditions.js +3 -3
- package/utils/{hct → material-color/hct}/hctSolver.js +9 -16
- package/utils/{hct → material-color}/helper.js +11 -18
- package/utils/{hct → material-color/palettes}/CorePalette.js +79 -19
- package/utils/{hct → material-color/palettes}/TonalPalette.js +12 -4
- package/utils/material-color/scheme/Scheme.js +376 -0
- package/utils/{hct/colorUtils.js → material-color/utils/color.js} +61 -1
- package/utils/popup.js +46 -25
- package/components/ListSelect.js +0 -220
- package/components/Option.js +0 -91
- package/components/Pane.js +0 -281
- package/core/identify.js +0 -40
- package/utils/hct/Scheme.js +0 -587
- /package/utils/{hct/mathUtils.js → material-color/utils/math.js} +0 -0
package/core/Composition.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/* eslint-disable sort-class-members/sort-class-members */
|
|
2
|
+
|
|
3
|
+
import CompositionAdapter from './CompositionAdapter.js';
|
|
2
4
|
import { generateCSSStyleSheets, generateHTMLStyleElements } from './css.js';
|
|
3
|
-
import { identifierFromElement } from './identify.js';
|
|
4
5
|
import { observeFunction } from './observe.js';
|
|
6
|
+
import { createEmptyComment, createEmptyTextNode } from './optimizations.js';
|
|
5
7
|
import { generateFragment, inlineFunctions } from './template.js';
|
|
8
|
+
import { generateUID } from './uid.js';
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* @template T
|
|
9
|
-
* @typedef {Composition<?>|HTMLStyleElement|CSSStyleSheet|DocumentFragment|
|
|
12
|
+
* @typedef {Composition<?>|HTMLStyleElement|CSSStyleSheet|DocumentFragment|string} CompositionPart
|
|
10
13
|
*/
|
|
11
14
|
|
|
12
15
|
/**
|
|
@@ -18,30 +21,228 @@ import { generateFragment, inlineFunctions } from './template.js';
|
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* @template T
|
|
21
|
-
* @typedef {Object}
|
|
22
|
-
* @prop {
|
|
23
|
-
* @prop {
|
|
24
|
+
* @typedef {Object} RenderOptions
|
|
25
|
+
* @prop {T} [store] what
|
|
26
|
+
* @prop {DocumentFragment|ShadowRoot|HTMLElement|Element} [target] where
|
|
27
|
+
* @prop {any} [context] `this` on callbacks/events
|
|
28
|
+
* @prop {any} [injections]
|
|
24
29
|
*/
|
|
25
30
|
|
|
26
31
|
/**
|
|
27
32
|
* @template {any} T
|
|
28
33
|
* @typedef {Object} NodeBindEntry
|
|
29
|
-
* @prop {string}
|
|
30
|
-
* @prop {number}
|
|
31
|
-
* @prop {string}
|
|
34
|
+
* @prop {string} [key]
|
|
35
|
+
* @prop {number} [index]
|
|
36
|
+
* @prop {string} tag
|
|
37
|
+
* @prop {string|number} subnode Index of childNode or attrName
|
|
38
|
+
* @prop {string[]} props
|
|
39
|
+
* @prop {string[][]} deepProps
|
|
32
40
|
* @prop {boolean} [negate]
|
|
33
41
|
* @prop {boolean} [doubleNegate]
|
|
34
|
-
* @prop {Function} [
|
|
35
|
-
* @prop {
|
|
42
|
+
* @prop {Function} [expression]
|
|
43
|
+
* @prop {(options: RenderOptions<?>, element: Element, changes:any, data:any) => any} [render] custom render function
|
|
44
|
+
* @prop {import('./typings.js').CompositionEventListener<T>[]} [listeners]
|
|
45
|
+
* @prop {Composition<any>} [composition] // Sub composition templating (eg: array)
|
|
36
46
|
* @prop {T} defaultValue
|
|
37
47
|
*/
|
|
38
48
|
|
|
49
|
+
/** @typedef {any[]} RenderState */
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @typedef RenderGraphSearch
|
|
53
|
+
* @prop {(state:InitializationState, changes:any, data:any) => any} invocation
|
|
54
|
+
* @prop {number} cacheIndex
|
|
55
|
+
* @prop {number} ranFlagIndex
|
|
56
|
+
* @prop {number} dirtyIndex
|
|
57
|
+
* @prop {string | Function | string[]} query
|
|
58
|
+
* @prop {Function} [expression]
|
|
59
|
+
* @prop {string} prop
|
|
60
|
+
* @prop {string[]} deepProp
|
|
61
|
+
* @prop {string[]} propsUsed
|
|
62
|
+
* @prop {string[][]} deepPropsUsed
|
|
63
|
+
* @prop {any} defaultValue
|
|
64
|
+
* @prop {RenderGraphSearch} [subSearch]
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @typedef RenderGraphAction
|
|
69
|
+
* @prop {(state:InitializationState, value:any, changes: any, data:any) => any} invocation
|
|
70
|
+
* @prop {number} [commentIndex]
|
|
71
|
+
* @prop {number} [nodeIndex]
|
|
72
|
+
* @prop {number} [cacheIndex]
|
|
73
|
+
* @prop {string} [attrName]
|
|
74
|
+
* @prop {any} [defaultValue]
|
|
75
|
+
* @prop {RenderGraphSearch} search
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @type {RenderGraphAction['invocation']}
|
|
80
|
+
* @this {RenderGraphAction}
|
|
81
|
+
*/
|
|
82
|
+
function writeDOMAttribute(state, value) {
|
|
83
|
+
const { nodeIndex, attrName } = this;
|
|
84
|
+
/** @type {Element} */
|
|
85
|
+
const element = state.nodes[nodeIndex];
|
|
86
|
+
switch (value) {
|
|
87
|
+
case undefined:
|
|
88
|
+
case null:
|
|
89
|
+
case false:
|
|
90
|
+
element.removeAttribute(attrName);
|
|
91
|
+
return false;
|
|
92
|
+
case true:
|
|
93
|
+
element.setAttribute(attrName, '');
|
|
94
|
+
return '';
|
|
95
|
+
default:
|
|
96
|
+
element.setAttribute(attrName, value);
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @type {RenderGraphAction['invocation']}
|
|
103
|
+
* @this {RenderGraphAction}
|
|
104
|
+
*/
|
|
105
|
+
function writeDOMText(state, value) {
|
|
106
|
+
// @ts-ignore Skip cast
|
|
107
|
+
state.nodes[this.nodeIndex].data = value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @type {RenderGraphAction['invocation']}
|
|
112
|
+
* @this {RenderGraphAction}
|
|
113
|
+
*/
|
|
114
|
+
function writeDOMElementAttachedState(state, value) {
|
|
115
|
+
const { commentIndex, nodeIndex } = this;
|
|
116
|
+
let comment = state.comments[commentIndex];
|
|
117
|
+
if (!comment) {
|
|
118
|
+
comment = createEmptyComment();
|
|
119
|
+
state.comments[commentIndex] = comment;
|
|
120
|
+
}
|
|
121
|
+
const element = state.nodes[nodeIndex];
|
|
122
|
+
const show = value != null && value !== false;
|
|
123
|
+
if (show) {
|
|
124
|
+
comment.replaceWith(element);
|
|
125
|
+
} else {
|
|
126
|
+
element.replaceWith(comment);
|
|
127
|
+
}
|
|
128
|
+
return show;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @type {RenderGraphAction['invocation']}
|
|
133
|
+
* @this {RenderGraphAction}
|
|
134
|
+
*/
|
|
135
|
+
function writeDOMHideElementOnInit(state) {
|
|
136
|
+
const { commentIndex, nodeIndex } = this;
|
|
137
|
+
|
|
138
|
+
const comment = createEmptyComment();
|
|
139
|
+
state.comments[commentIndex] = comment;
|
|
140
|
+
|
|
141
|
+
state.nodes[nodeIndex].replaceWith(comment);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @param {RenderGraphSearch} search
|
|
146
|
+
* @param {Parameters<RenderGraphSearch['invocation']>} args
|
|
147
|
+
*/
|
|
148
|
+
function executeSearch(search, ...args) {
|
|
149
|
+
const [state] = args;
|
|
150
|
+
const cachedValue = state.caches[search.cacheIndex];
|
|
151
|
+
if (state.ranFlags[search.ranFlagIndex]) {
|
|
152
|
+
// Return last result
|
|
153
|
+
return {
|
|
154
|
+
value: cachedValue,
|
|
155
|
+
dirty: state.dirtyFlags[search.dirtyIndex],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
state.ranFlags[search.ranFlagIndex] = true;
|
|
159
|
+
let result;
|
|
160
|
+
if (search.subSearch) {
|
|
161
|
+
const subResult = executeSearch(search.subSearch, ...args);
|
|
162
|
+
// Use last cached value (if any)
|
|
163
|
+
if (!subResult.dirty && cachedValue !== undefined) {
|
|
164
|
+
state.dirtyFlags[search.dirtyIndex] = false;
|
|
165
|
+
return { value: cachedValue, dirty: false };
|
|
166
|
+
}
|
|
167
|
+
// Pass from subquery
|
|
168
|
+
result = search.invocation(subResult.value);
|
|
169
|
+
} else {
|
|
170
|
+
result = search.invocation(...args);
|
|
171
|
+
}
|
|
172
|
+
if ((result === undefined) || (cachedValue === result)) {
|
|
173
|
+
// Returnf rom cache
|
|
174
|
+
return { value: result, dirty: false };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Overwrite cache and flag as dirty
|
|
178
|
+
state.caches[search.cacheIndex] = result;
|
|
179
|
+
state.dirtyFlags[search.dirtyIndex] = true;
|
|
180
|
+
return { value: result, dirty: true };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @type {RenderGraphSearch['invocation']}
|
|
185
|
+
* @this {RenderGraphSearch}
|
|
186
|
+
*/
|
|
187
|
+
function searchWithExpression(state, changes, data) {
|
|
188
|
+
return this.expression.call(
|
|
189
|
+
state.options.context,
|
|
190
|
+
state.options.store ?? data,
|
|
191
|
+
state.options.injections,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @type {RenderGraphSearch['invocation']}
|
|
197
|
+
* @this {RenderGraphSearch}
|
|
198
|
+
*/
|
|
199
|
+
function searchWithProp(state, changes, data) {
|
|
200
|
+
return changes[this.prop];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @type {RenderGraphSearch['invocation']}
|
|
205
|
+
* @this {RenderGraphSearch}
|
|
206
|
+
*/
|
|
207
|
+
function searchWithDeepProp(state, changes, data) {
|
|
208
|
+
let scope = changes;
|
|
209
|
+
for (const prop of this.deepProp) {
|
|
210
|
+
if (scope === null) return null;
|
|
211
|
+
if (prop in scope === false) return undefined;
|
|
212
|
+
scope = scope[prop];
|
|
213
|
+
}
|
|
214
|
+
return scope;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @typedef InterpolateOptions
|
|
219
|
+
* @prop {Object} [defaults] Default values to use for interpolation
|
|
220
|
+
* @prop {{iterable:string} & Record<string,any>} [injections] Context-specific injected properties. (Experimental)
|
|
221
|
+
*/
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @typedef InitializationState
|
|
225
|
+
* @prop {Element} lastElement
|
|
226
|
+
* @prop {boolean} isShadowRoot
|
|
227
|
+
* @prop {ChildNode} lastChildNode
|
|
228
|
+
* @prop {(Element|Text)[]} nodes
|
|
229
|
+
* @prop {any[]} caches
|
|
230
|
+
* @prop {Comment[]} comments
|
|
231
|
+
* @prop {boolean[]} ranFlags
|
|
232
|
+
* @prop {boolean[]} dirtyFlags
|
|
233
|
+
* @prop {Element[]} refs
|
|
234
|
+
* @prop {number} lastChildNodeIndex
|
|
235
|
+
* @prop {DocumentFragment} instanceFragment
|
|
236
|
+
* @prop {RenderOptions<?>} options
|
|
237
|
+
*/
|
|
238
|
+
|
|
39
239
|
/** Splits: `{template}text{template}` as `['', 'template', 'text', 'template', '']` */
|
|
40
240
|
const STRING_INTERPOLATION_REGEX = /{([^}]*)}/g;
|
|
41
241
|
|
|
42
242
|
/**
|
|
43
243
|
* Returns event listener bound to shadow root host.
|
|
44
244
|
* Use this function to avoid generating extra closures
|
|
245
|
+
*
|
|
45
246
|
* @this {HTMLElement}
|
|
46
247
|
* @param {Function} fn
|
|
47
248
|
*/
|
|
@@ -54,33 +255,23 @@ function buildShadowRootChildListener(fn) {
|
|
|
54
255
|
}
|
|
55
256
|
|
|
56
257
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* @param {
|
|
60
|
-
* @param {
|
|
61
|
-
* @
|
|
62
|
-
* @return {Object}
|
|
258
|
+
* @example
|
|
259
|
+
* propFromObject('foo', {foo:'bar'}) == ['foo', 'bar'];
|
|
260
|
+
* @param {string} prop
|
|
261
|
+
* @param {any} source
|
|
262
|
+
* @return {any}
|
|
63
263
|
*/
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const scopedKey = scope ? `${scope}.${key}` : key;
|
|
68
|
-
target[scopedKey] = value;
|
|
69
|
-
if (value != null && typeof value === 'object') {
|
|
70
|
-
flattenObject(value, syntax, target, scopedKey);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
if (Array.isArray(object)) {
|
|
74
|
-
const scopedKey = scope ? `${scope}.length` : 'length';
|
|
75
|
-
target[scopedKey] = object.length;
|
|
264
|
+
function propFromObject(prop, source) {
|
|
265
|
+
if (source) {
|
|
266
|
+
return source[prop];
|
|
76
267
|
}
|
|
77
|
-
return
|
|
268
|
+
return undefined;
|
|
78
269
|
}
|
|
79
270
|
|
|
80
271
|
/**
|
|
81
272
|
* @example
|
|
82
|
-
*
|
|
83
|
-
* 'address
|
|
273
|
+
* deepPropFromObject(
|
|
274
|
+
* ['address', 'home, 'houseNumber'],
|
|
84
275
|
* {
|
|
85
276
|
* address: {
|
|
86
277
|
* home: {
|
|
@@ -88,22 +279,25 @@ function flattenObject(object, syntax = 'dot', target = {}, scope = '') {
|
|
|
88
279
|
* },
|
|
89
280
|
* }
|
|
90
281
|
* }
|
|
91
|
-
* )
|
|
92
|
-
* @param {string}
|
|
282
|
+
* ) == [houseNumber, 35]
|
|
283
|
+
* @param {string[]} nameArray
|
|
93
284
|
* @param {any} source
|
|
94
|
-
* @return {
|
|
285
|
+
* @return {any}
|
|
95
286
|
*/
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
let
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
287
|
+
function deepPropFromObject(nameArray, source) {
|
|
288
|
+
if (!source) return undefined;
|
|
289
|
+
let scope = source;
|
|
290
|
+
let prop;
|
|
291
|
+
for (prop of nameArray) {
|
|
292
|
+
if (typeof scope === 'object') {
|
|
293
|
+
if (scope === null) return null;
|
|
294
|
+
if (!(prop in scope)) return undefined;
|
|
295
|
+
scope = scope[prop];
|
|
296
|
+
} else {
|
|
297
|
+
return scope[prop];
|
|
298
|
+
}
|
|
104
299
|
}
|
|
105
|
-
|
|
106
|
-
return [child, value];
|
|
300
|
+
return scope;
|
|
107
301
|
}
|
|
108
302
|
|
|
109
303
|
/**
|
|
@@ -125,26 +319,84 @@ function valueFromPropName(prop, source) {
|
|
|
125
319
|
|
|
126
320
|
/** @template T */
|
|
127
321
|
export default class Composition {
|
|
322
|
+
#interpolationState = {
|
|
323
|
+
nodeIndex: -1,
|
|
324
|
+
ranFlagIndex: 0,
|
|
325
|
+
cacheIndex: 0,
|
|
326
|
+
dirtyIndex: 0,
|
|
327
|
+
commentIndex: 0,
|
|
328
|
+
/** @type {this['nodesToBind'][0]} */
|
|
329
|
+
nodeEntry: null,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// eslint-disable-next-line symbol-description
|
|
333
|
+
static shadowRootTag = Symbol();
|
|
334
|
+
|
|
335
|
+
/** @type {{tag:string, textNodes: number[]}[]} */
|
|
336
|
+
nodesToBind = [];
|
|
337
|
+
|
|
338
|
+
/** @type {string[]} */
|
|
339
|
+
props = [];
|
|
340
|
+
|
|
341
|
+
/** @type {RenderGraphSearch[]} */
|
|
342
|
+
searches = [];
|
|
343
|
+
|
|
344
|
+
/** @type {any[]} */
|
|
345
|
+
initCache = [];
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Index of searches by query (dotted notation for deep props)
|
|
349
|
+
*
|
|
350
|
+
* @type {Map<Function|string, RenderGraphSearch>}
|
|
351
|
+
*/
|
|
352
|
+
searchByQuery = new Map();
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Index of searches by query (dotted notation for deep props)
|
|
356
|
+
*
|
|
357
|
+
* @type {Map<string, RenderGraphAction[]>}
|
|
358
|
+
*/
|
|
359
|
+
actionsByPropsUsed = new Map();
|
|
360
|
+
|
|
361
|
+
/** @type {RenderGraphAction[]} */
|
|
362
|
+
actions = [];
|
|
363
|
+
|
|
364
|
+
/** @type {RenderGraphAction[]} */
|
|
365
|
+
postInitActions = [];
|
|
366
|
+
|
|
367
|
+
/** @type {Set<string>} */
|
|
368
|
+
tagsWithBindings = new Set();
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Array of element tags
|
|
372
|
+
*
|
|
373
|
+
* @type {string[]}
|
|
374
|
+
*/
|
|
375
|
+
tags = [];
|
|
376
|
+
|
|
128
377
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
378
|
+
* Array of property bindings sorted by tag/subnode
|
|
379
|
+
*
|
|
380
|
+
* @type {Set<string>}
|
|
131
381
|
*/
|
|
132
|
-
|
|
382
|
+
watchedProps = new Set();
|
|
133
383
|
|
|
134
384
|
/**
|
|
135
385
|
* Data of arrays used in templates
|
|
136
|
-
* Usage of a [
|
|
386
|
+
* Usage of a [mdw-for] will create an ArrayLike expectation based on key
|
|
137
387
|
* Only store metadata, not actual data. Currently only needs length.
|
|
138
388
|
* TBD if more is needed later
|
|
139
389
|
* Referenced by property key (string)
|
|
140
|
-
*
|
|
390
|
+
*
|
|
391
|
+
* @type {CompositionAdapter}
|
|
141
392
|
*/
|
|
142
|
-
|
|
393
|
+
adapter;
|
|
143
394
|
|
|
144
395
|
/**
|
|
145
396
|
* Collection of events to bind.
|
|
146
397
|
* Indexed by ID
|
|
147
|
-
*
|
|
398
|
+
*
|
|
399
|
+
* @type {Map<string|symbol, import('./typings.js').CompositionEventListener<any>[]>}
|
|
148
400
|
*/
|
|
149
401
|
events = new Map();
|
|
150
402
|
|
|
@@ -152,18 +404,11 @@ export default class Composition {
|
|
|
152
404
|
* Snapshot of composition at initial state.
|
|
153
405
|
* This fragment can be cloned for first rendering, instead of calling
|
|
154
406
|
* of using `render()` to construct the initial DOM tree.
|
|
407
|
+
*
|
|
155
408
|
* @type {DocumentFragment}
|
|
156
409
|
*/
|
|
157
410
|
cloneable;
|
|
158
411
|
|
|
159
|
-
/**
|
|
160
|
-
* Result of interpolation of the composition template.
|
|
161
|
-
* Includes all DOM elements, which is used to reference for adding and
|
|
162
|
-
* removing DOM elements during render.
|
|
163
|
-
* @type {DocumentFragment}
|
|
164
|
-
*/
|
|
165
|
-
interpolation;
|
|
166
|
-
|
|
167
412
|
/** @type {(HTMLStyleElement|CSSStyleSheet)[]} */
|
|
168
413
|
styles = [];
|
|
169
414
|
|
|
@@ -173,25 +418,19 @@ export default class Composition {
|
|
|
173
418
|
/** @type {DocumentFragment} */
|
|
174
419
|
stylesFragment;
|
|
175
420
|
|
|
176
|
-
/** @type {((this:T, changes:T) => any)[]} */
|
|
177
|
-
watchers = [];
|
|
178
|
-
|
|
179
421
|
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* @type {
|
|
422
|
+
* List of IDs used by template elements
|
|
423
|
+
* May be needed to be removed when adding to non-DocumentFragment
|
|
424
|
+
*
|
|
425
|
+
* @type {string[]}
|
|
184
426
|
*/
|
|
185
|
-
|
|
427
|
+
allIds = [];
|
|
186
428
|
|
|
187
429
|
/**
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* `cloneable` due to default state. Used to reconstruct conditional elements
|
|
191
|
-
* with conditional children in default state as well (unlike `interpolation`).
|
|
192
|
-
* @type {Map<string, {element: Element, id: string, parentId: string, commentCache: WeakMap<Element|DocumentFragment,Comment>}>}
|
|
430
|
+
* Collection of IDs used for referencing elements
|
|
431
|
+
* Not meant for live DOM. Removed before attaching to document
|
|
193
432
|
*/
|
|
194
|
-
|
|
433
|
+
temporaryIds = new Set();
|
|
195
434
|
|
|
196
435
|
/** Flag set when template and styles have been interpolated */
|
|
197
436
|
interpolated = false;
|
|
@@ -212,9 +451,6 @@ export default class Composition {
|
|
|
212
451
|
yield part;
|
|
213
452
|
}
|
|
214
453
|
yield this.template;
|
|
215
|
-
for (const part of this.watchers) {
|
|
216
|
-
yield part;
|
|
217
|
-
}
|
|
218
454
|
}
|
|
219
455
|
|
|
220
456
|
/**
|
|
@@ -224,8 +460,6 @@ export default class Composition {
|
|
|
224
460
|
for (const part of parts) {
|
|
225
461
|
if (typeof part === 'string') {
|
|
226
462
|
this.append(generateFragment(part.trim()));
|
|
227
|
-
} else if (typeof part === 'function') {
|
|
228
|
-
this.watchers.push(part);
|
|
229
463
|
} else if (part instanceof Composition) {
|
|
230
464
|
this.append(...part);
|
|
231
465
|
} else if (part instanceof DocumentFragment) {
|
|
@@ -240,257 +474,249 @@ export default class Composition {
|
|
|
240
474
|
|
|
241
475
|
/** @param {import('./typings.js').CompositionEventListener<T>} listener */
|
|
242
476
|
addCompositionEventListener(listener) {
|
|
243
|
-
const key = listener.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
this.events.set(key,
|
|
477
|
+
const key = listener.tag ?? '';
|
|
478
|
+
if (this.events.has(key)) {
|
|
479
|
+
this.events.get(key).push(listener);
|
|
480
|
+
} else {
|
|
481
|
+
this.events.set(key, [listener]);
|
|
248
482
|
}
|
|
249
|
-
set.add(listener);
|
|
250
483
|
return this;
|
|
251
484
|
}
|
|
252
485
|
|
|
253
486
|
/**
|
|
487
|
+
* @param {string|symbol} tag
|
|
488
|
+
* @param {EventTarget} target
|
|
489
|
+
* @param {any} [context]
|
|
490
|
+
* @return {void}
|
|
491
|
+
*/
|
|
492
|
+
#bindCompositionEventListeners(tag, target, context) {
|
|
493
|
+
if (!this.events.has(tag)) return;
|
|
494
|
+
for (const event of this.events.get(tag)) {
|
|
495
|
+
let listener;
|
|
496
|
+
if (event.handleEvent) {
|
|
497
|
+
listener = event.handleEvent;
|
|
498
|
+
} else if (event.deepProp.length) {
|
|
499
|
+
listener = deepPropFromObject(event.deepProp, this.interpolateOptions.defaults);
|
|
500
|
+
} else {
|
|
501
|
+
listener = propFromObject(event.prop, this.interpolateOptions.defaults);
|
|
502
|
+
}
|
|
503
|
+
target.addEventListener(event.type, context ? listener.bind(context) : listener, event);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* TODO: Add types and clean up closure leak
|
|
254
509
|
* Updates component nodes based on data.
|
|
255
510
|
* Expects data in JSON Merge Patch format
|
|
511
|
+
*
|
|
256
512
|
* @see https://www.rfc-editor.org/rfc/rfc7386
|
|
257
|
-
* @
|
|
258
|
-
* @param {Partial
|
|
259
|
-
* @param {
|
|
260
|
-
* @param {
|
|
261
|
-
* @return {
|
|
513
|
+
* @template {Object} T
|
|
514
|
+
* @param {Partial<T>} changes what specifically
|
|
515
|
+
* @param {T} [data]
|
|
516
|
+
* @param {RenderOptions<T>} [options]
|
|
517
|
+
* @return {Function & {target:Element}} anchor
|
|
262
518
|
*/
|
|
263
|
-
render(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
/** @type {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
519
|
+
render(changes, data, options = {}) {
|
|
520
|
+
// console.log('render', changes, options);
|
|
521
|
+
if (!this.interpolated) {
|
|
522
|
+
this.interpolate({ defaults: data ?? changes, injections: options?.injections });
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const instanceFragment = /** @type {DocumentFragment} */ (this.cloneable.cloneNode(true));
|
|
526
|
+
|
|
527
|
+
const target = options.target ?? instanceFragment.firstElementChild;
|
|
528
|
+
|
|
529
|
+
const isShadowRoot = target instanceof ShadowRoot;
|
|
530
|
+
|
|
531
|
+
/** @type {InitializationState} */
|
|
532
|
+
const initState = {
|
|
533
|
+
instanceFragment,
|
|
534
|
+
lastChildNode: null,
|
|
535
|
+
lastChildNodeIndex: 0,
|
|
536
|
+
lastElement: null,
|
|
537
|
+
isShadowRoot,
|
|
538
|
+
ranFlags: [],
|
|
539
|
+
comments: [],
|
|
540
|
+
nodes: [],
|
|
541
|
+
caches: this.initCache.slice(),
|
|
542
|
+
dirtyFlags: [],
|
|
543
|
+
refs: [],
|
|
544
|
+
options,
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
const nodes = initState.nodes;
|
|
548
|
+
for (const { tag, textNodes } of this.nodesToBind) {
|
|
549
|
+
const element = instanceFragment.getElementById(tag);
|
|
550
|
+
initState.refs.push(element);
|
|
551
|
+
nodes.push(element);
|
|
552
|
+
this.#bindCompositionEventListeners(tag, element, options.context);
|
|
553
|
+
|
|
554
|
+
if (!textNodes.length) continue;
|
|
555
|
+
|
|
556
|
+
let textNode = element.firstChild;
|
|
557
|
+
let currentIndex = 0;
|
|
558
|
+
for (const index of textNodes) {
|
|
559
|
+
while (index !== currentIndex) {
|
|
560
|
+
textNode = textNode.nextSibling;
|
|
561
|
+
currentIndex++;
|
|
295
562
|
}
|
|
563
|
+
nodes.push(textNode);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
this.#bindCompositionEventListeners('', options.context);
|
|
567
|
+
this.#bindCompositionEventListeners(Composition.shadowRootTag, options.context.shadowRoot, options.context);
|
|
296
568
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if (prop in flattened) continue;
|
|
314
|
-
let lastIndexOfDot = prop.lastIndexOf('.');
|
|
315
|
-
if (lastIndexOfDot === -1) {
|
|
316
|
-
// console.debug('injected shallow', prop);
|
|
317
|
-
args[prop] = store[prop];
|
|
318
|
-
} else {
|
|
319
|
-
// Relying on props being sorted...
|
|
320
|
-
console.debug('need deep', prop);
|
|
321
|
-
let entry;
|
|
322
|
-
let propSearchKey = prop;
|
|
323
|
-
let lastPropSearchKey = prop;
|
|
324
|
-
while (!entry) {
|
|
325
|
-
entry = entryFromPropName(propSearchKey, args);
|
|
326
|
-
if (entry) {
|
|
327
|
-
const propName = lastPropSearchKey.slice(propSearchKey.length + 1);
|
|
328
|
-
entry[1][propName] = valueFromPropName(lastPropSearchKey, store);
|
|
329
|
-
break;
|
|
330
|
-
}
|
|
331
|
-
if (lastIndexOfDot === -1) break;
|
|
332
|
-
lastPropSearchKey = prop;
|
|
333
|
-
propSearchKey = prop.slice(0, lastIndexOfDot);
|
|
334
|
-
lastIndexOfDot = propSearchKey.lastIndexOf(',');
|
|
335
|
-
}
|
|
336
|
-
if (!entry) {
|
|
337
|
-
console.warn('what do?');
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
value = fn.call(context, args);
|
|
342
|
-
fnResults.set(fn, value);
|
|
569
|
+
for (const action of this.postInitActions) {
|
|
570
|
+
action.invocation(initState);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const draw = (changes, data) => {
|
|
574
|
+
let ranSearch = false;
|
|
575
|
+
for (const prop of this.props) {
|
|
576
|
+
if (!this.actionsByPropsUsed.has(prop)) continue;
|
|
577
|
+
if (!(prop in changes)) continue;
|
|
578
|
+
const actions = this.actionsByPropsUsed.get(prop);
|
|
579
|
+
for (const action of actions) {
|
|
580
|
+
ranSearch = true;
|
|
581
|
+
const result = executeSearch(action.search, initState, changes, data);
|
|
582
|
+
if (result.dirty) {
|
|
583
|
+
// console.log('dirty, updating from batch', initState.nodes[action.nodeIndex], 'with', result.value);
|
|
584
|
+
action.invocation(initState, result.value, changes, data);
|
|
343
585
|
}
|
|
344
|
-
} else {
|
|
345
|
-
value = rawValue;
|
|
346
586
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
587
|
+
}
|
|
588
|
+
if (!ranSearch) return;
|
|
589
|
+
initState.ranFlags.fill(false);
|
|
590
|
+
initState.dirtyFlags.fill(false);
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
if (isShadowRoot) {
|
|
594
|
+
options.context ??= target.host;
|
|
595
|
+
if ('adoptedStyleSheets' in target) {
|
|
596
|
+
if (this.adoptedStyleSheets.length) {
|
|
597
|
+
target.adoptedStyleSheets = [
|
|
598
|
+
...target.adoptedStyleSheets,
|
|
599
|
+
...this.adoptedStyleSheets,
|
|
600
|
+
];
|
|
353
601
|
}
|
|
602
|
+
} else if (this.stylesFragment.hasChildNodes()) {
|
|
603
|
+
instanceFragment.prepend(this.stylesFragment.cloneNode(true));
|
|
604
|
+
}
|
|
605
|
+
} else {
|
|
606
|
+
options.context ??= target;
|
|
607
|
+
}
|
|
354
608
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
: Number.parseInt(node.slice('#text'.length), 10);
|
|
360
|
-
let nodesFound = 0;
|
|
361
|
-
for (const childNode of ref.childNodes) {
|
|
362
|
-
if (childNode.nodeType !== Node.TEXT_NODE) continue;
|
|
363
|
-
if (index !== nodesFound++) continue;
|
|
364
|
-
childNode.nodeValue = value ?? '';
|
|
365
|
-
break;
|
|
366
|
-
}
|
|
367
|
-
if (index > nodesFound) {
|
|
368
|
-
console.warn('Node not found, adding?');
|
|
369
|
-
ref.append(value);
|
|
370
|
-
}
|
|
371
|
-
} else if (node === '_if') {
|
|
372
|
-
const attached = root.contains(ref);
|
|
373
|
-
const orphaned = ref.parentElement == null && ref.parentNode !== root;
|
|
374
|
-
const shouldShow = value !== null && value !== false;
|
|
375
|
-
if (orphaned && ref.parentNode) {
|
|
376
|
-
console.warn('Orphaned with parent node?', id, { attached, orphaned, shouldShow }, ref.parentNode);
|
|
377
|
-
}
|
|
378
|
-
if (attached !== !orphaned) {
|
|
379
|
-
console.warn('Conditional state', id, { attached, orphaned, shouldShow });
|
|
380
|
-
console.warn('Not attached and not orphaned. Should do nothing?', ref, ref.parentElement);
|
|
381
|
-
}
|
|
382
|
-
if (shouldShow) {
|
|
383
|
-
if (orphaned) {
|
|
384
|
-
const metadata = this.conditionalElementMetadata.get(id);
|
|
385
|
-
if (!metadata) {
|
|
386
|
-
console.error(id);
|
|
387
|
-
throw new Error('Could not find conditional element metadata');
|
|
388
|
-
}
|
|
609
|
+
if (changes !== this.interpolateOptions.defaults) {
|
|
610
|
+
// Not default, overwrite nodes
|
|
611
|
+
draw(changes, data);
|
|
612
|
+
}
|
|
389
613
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
break;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
metadata.commentCache.set(this, comment);
|
|
410
|
-
}
|
|
411
|
-
if (comment) {
|
|
412
|
-
console.debug('Composition: Add', id, 'back', ref.outerHTML);
|
|
413
|
-
comment.replaceWith(ref);
|
|
414
|
-
} else {
|
|
415
|
-
console.warn('Could not add', id, 'back to parent');
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
} else if (!orphaned) {
|
|
419
|
-
const metadata = this.conditionalElementMetadata.get(id);
|
|
420
|
-
if (!metadata) {
|
|
421
|
-
console.error(id);
|
|
422
|
-
throw new Error(`Could not find conditional element metadata for ${id}`);
|
|
423
|
-
}
|
|
424
|
-
let comment = metadata.commentCache.get(root);
|
|
425
|
-
if (!comment) {
|
|
426
|
-
comment = new Comment(`{#${id}}`);
|
|
427
|
-
metadata.commentCache.set(this, comment);
|
|
428
|
-
}
|
|
429
|
-
console.debug('Composition: Remove', id, ref.outerHTML);
|
|
430
|
-
ref.replaceWith(comment);
|
|
431
|
-
}
|
|
432
|
-
} else if (value === false || value == null) {
|
|
433
|
-
ref.removeAttribute(node);
|
|
434
|
-
} else {
|
|
435
|
-
ref.setAttribute(node, value === true ? '' : value);
|
|
614
|
+
if (isShadowRoot) {
|
|
615
|
+
target.append(instanceFragment);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
draw.target = target;
|
|
619
|
+
draw.byProp = (prop, value, data) => {
|
|
620
|
+
if (!this.actionsByPropsUsed.has(prop)) return;
|
|
621
|
+
let ranSearch = false;
|
|
622
|
+
|
|
623
|
+
// Update search
|
|
624
|
+
if (this.searchByQuery.has(prop)) {
|
|
625
|
+
ranSearch = true;
|
|
626
|
+
const search = this.searchByQuery.get(prop);
|
|
627
|
+
const cachedValue = initState.caches[search.cacheIndex];
|
|
628
|
+
if (cachedValue === value) {
|
|
629
|
+
return;
|
|
436
630
|
}
|
|
631
|
+
initState.ranFlags[search.ranFlagIndex] = true;
|
|
632
|
+
initState.caches[search.cacheIndex] = value;
|
|
633
|
+
initState.dirtyFlags[search.dirtyIndex] = true;
|
|
634
|
+
}
|
|
437
635
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
636
|
+
let changes;
|
|
637
|
+
const actions = this.actionsByPropsUsed.get(prop);
|
|
638
|
+
for (const action of actions) {
|
|
639
|
+
if (action.search.query === prop) {
|
|
640
|
+
action.invocation(initState, value);
|
|
641
|
+
} else {
|
|
642
|
+
changes ??= { [prop]: value };
|
|
643
|
+
data ??= changes;
|
|
644
|
+
ranSearch = true;
|
|
645
|
+
const result = executeSearch(action.search, initState, changes, data);
|
|
646
|
+
if (result.dirty) {
|
|
647
|
+
// console.debug('dirty, updating by prop', prop, initState.nodes[action.nodeIndex], 'with', result.value);
|
|
648
|
+
action.invocation(initState, result.value, changes, data);
|
|
649
|
+
}
|
|
443
650
|
}
|
|
444
|
-
set.add(node);
|
|
445
651
|
}
|
|
446
|
-
|
|
652
|
+
|
|
653
|
+
if (!ranSearch) return;
|
|
654
|
+
initState.ranFlags.fill(false);
|
|
655
|
+
initState.dirtyFlags.fill(false);
|
|
656
|
+
};
|
|
657
|
+
draw.state = initState;
|
|
658
|
+
return draw;
|
|
447
659
|
}
|
|
448
660
|
|
|
449
661
|
/**
|
|
450
662
|
* @param {Attr|Text} node
|
|
451
663
|
* @param {Element} element
|
|
452
|
-
* @param {
|
|
664
|
+
* @param {InterpolateOptions} [options]
|
|
453
665
|
* @param {string} [parsedValue]
|
|
454
|
-
* @return {
|
|
666
|
+
* @return {true|undefined} remove node
|
|
455
667
|
*/
|
|
456
|
-
#interpolateNode(node, element,
|
|
668
|
+
#interpolateNode(node, element, options, parsedValue) {
|
|
457
669
|
const { nodeValue, nodeName, nodeType } = node;
|
|
458
670
|
|
|
671
|
+
/** @type {Attr} */
|
|
672
|
+
let attr;
|
|
673
|
+
/** @type {Text} */
|
|
674
|
+
let text;
|
|
675
|
+
if (nodeType === Node.ATTRIBUTE_NODE) {
|
|
676
|
+
attr = /** @type {Attr} */ (node);
|
|
677
|
+
} else {
|
|
678
|
+
text = /** @type {Text} */ (node);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Get template strings(s) in node if not passed
|
|
459
682
|
if (parsedValue == null) {
|
|
460
|
-
if (!nodeValue) return
|
|
683
|
+
if (!nodeValue) return;
|
|
461
684
|
const trimmed = nodeValue.trim();
|
|
462
|
-
if (!trimmed) return
|
|
463
|
-
if (
|
|
464
|
-
if (trimmed[0] !== '{') return
|
|
685
|
+
if (!trimmed) return;
|
|
686
|
+
if (attr) {
|
|
687
|
+
if (trimmed[0] !== '{') return;
|
|
465
688
|
const { length } = trimmed;
|
|
466
|
-
if (trimmed[length - 1] !== '}') return
|
|
689
|
+
if (trimmed[length - 1] !== '}') return;
|
|
467
690
|
parsedValue = trimmed.slice(1, -1);
|
|
691
|
+
// TODO: Support segmented attribute values
|
|
468
692
|
} else {
|
|
469
693
|
// Split text node into segments
|
|
470
694
|
// TODO: Benchmark indexOf pre-check vs regex
|
|
471
695
|
|
|
472
696
|
const segments = trimmed.split(STRING_INTERPOLATION_REGEX);
|
|
473
|
-
if (segments.length < 3) return
|
|
697
|
+
if (segments.length < 3) return;
|
|
474
698
|
if (segments.length === 3 && !segments[0] && !segments[2]) {
|
|
475
699
|
parsedValue = segments[1];
|
|
476
700
|
} else {
|
|
477
|
-
segments.
|
|
701
|
+
for (const [index, segment] of segments.entries()) {
|
|
478
702
|
// is even = is template string
|
|
479
703
|
if (index % 2) {
|
|
480
|
-
const newNode =
|
|
481
|
-
|
|
482
|
-
this.#interpolateNode(newNode, element,
|
|
483
|
-
} else {
|
|
484
|
-
|
|
485
|
-
node.before(segment);
|
|
704
|
+
const newNode = createEmptyTextNode();
|
|
705
|
+
text.before(newNode);
|
|
706
|
+
this.#interpolateNode(newNode, element, options, segment);
|
|
707
|
+
} else if (segment) {
|
|
708
|
+
text.before(segment);
|
|
486
709
|
}
|
|
487
|
-
}
|
|
488
|
-
//
|
|
710
|
+
}
|
|
711
|
+
// eslint-disable-next-line consistent-return
|
|
489
712
|
return true;
|
|
490
713
|
}
|
|
491
714
|
}
|
|
492
715
|
}
|
|
493
716
|
|
|
717
|
+
// Check mutations
|
|
718
|
+
|
|
719
|
+
const query = parsedValue;
|
|
494
720
|
const negate = parsedValue[0] === '!';
|
|
495
721
|
let doubleNegate = false;
|
|
496
722
|
if (negate) {
|
|
@@ -504,171 +730,508 @@ export default class Composition {
|
|
|
504
730
|
let isEvent;
|
|
505
731
|
let textNodeIndex;
|
|
506
732
|
|
|
507
|
-
if (
|
|
733
|
+
if (text) {
|
|
508
734
|
// eslint-disable-next-line unicorn/consistent-destructuring
|
|
509
|
-
if (element !==
|
|
735
|
+
if (element !== text.parentElement) {
|
|
510
736
|
console.warn('mismatch?');
|
|
511
|
-
element =
|
|
737
|
+
element = text.parentElement;
|
|
512
738
|
}
|
|
513
739
|
textNodeIndex = 0;
|
|
514
|
-
|
|
740
|
+
/** @type {ChildNode} */
|
|
741
|
+
let prev = text;
|
|
515
742
|
while ((prev = prev.previousSibling)) {
|
|
516
|
-
|
|
517
|
-
textNodeIndex++;
|
|
518
|
-
}
|
|
743
|
+
textNodeIndex++;
|
|
519
744
|
}
|
|
520
745
|
} else {
|
|
521
746
|
// @ts-ignore Skip cast
|
|
522
747
|
// eslint-disable-next-line unicorn/consistent-destructuring
|
|
523
|
-
if (element !==
|
|
748
|
+
if (element !== attr.ownerElement) {
|
|
524
749
|
console.warn('mismatch?');
|
|
525
|
-
element =
|
|
750
|
+
element = attr.ownerElement;
|
|
526
751
|
}
|
|
527
752
|
if (nodeName.startsWith('on')) {
|
|
528
753
|
// Do not interpolate inline event listeners
|
|
529
|
-
|
|
530
|
-
|
|
754
|
+
const hyphenIndex = nodeName.indexOf('-');
|
|
755
|
+
if (hyphenIndex === -1) return;
|
|
756
|
+
isEvent = hyphenIndex === 2;
|
|
531
757
|
}
|
|
532
758
|
}
|
|
533
759
|
|
|
534
|
-
const id = identifierFromElement(element, true);
|
|
535
|
-
|
|
536
760
|
if (isEvent) {
|
|
761
|
+
element.removeAttribute(nodeName);
|
|
762
|
+
const tag = this.#tagElement(element);
|
|
537
763
|
const eventType = nodeName.slice(3);
|
|
538
764
|
const [, flags, type] = eventType.match(/^([*1~]+)?(.*)$/);
|
|
539
|
-
|
|
765
|
+
|
|
766
|
+
let handleEvent;
|
|
767
|
+
/** @type {string} */
|
|
768
|
+
let prop;
|
|
769
|
+
/** @type {string[]} */
|
|
770
|
+
let deepProp = [];
|
|
771
|
+
if (parsedValue.startsWith('#')) {
|
|
772
|
+
handleEvent = inlineFunctions.get(parsedValue).fn;
|
|
773
|
+
} else {
|
|
774
|
+
const parsedProps = parsedValue.split('.');
|
|
775
|
+
if (parsedProps.length === 1) {
|
|
776
|
+
prop = parsedValue;
|
|
777
|
+
deepProp = [];
|
|
778
|
+
} else {
|
|
779
|
+
prop = parsedProps[0];
|
|
780
|
+
deepProp = parsedProps;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
this.addCompositionEventListener({
|
|
785
|
+
tag,
|
|
786
|
+
type,
|
|
787
|
+
handleEvent,
|
|
788
|
+
prop,
|
|
789
|
+
deepProp,
|
|
540
790
|
once: flags?.includes('1'),
|
|
541
791
|
passive: flags?.includes('~'),
|
|
542
792
|
capture: flags?.includes('*'),
|
|
543
|
-
};
|
|
793
|
+
});
|
|
544
794
|
|
|
545
|
-
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
546
797
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
798
|
+
/** @type {RenderGraphSearch} */
|
|
799
|
+
let search;
|
|
800
|
+
|
|
801
|
+
if (this.searchByQuery.has(query)) {
|
|
802
|
+
search = this.searchByQuery.get(query);
|
|
803
|
+
} else {
|
|
804
|
+
// Has subquery?
|
|
805
|
+
const subquery = parsedValue;
|
|
806
|
+
const isSubquery = subquery !== query;
|
|
807
|
+
/** @type {RenderGraphSearch} */
|
|
808
|
+
let subSearch;
|
|
809
|
+
if (isSubquery && this.searchByQuery.has(subquery)) {
|
|
810
|
+
subSearch = this.searchByQuery.get(subquery);
|
|
554
811
|
} else {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
812
|
+
// Construct subsearch, even is not subquery.
|
|
813
|
+
/** @type {Function} */
|
|
814
|
+
let expression;
|
|
815
|
+
/** @type {string[]} */
|
|
816
|
+
let propsUsed;
|
|
817
|
+
/** @type {string[][]} */
|
|
818
|
+
let deepPropsUsed;
|
|
819
|
+
let defaultValue;
|
|
820
|
+
let prop;
|
|
821
|
+
let deepProp;
|
|
822
|
+
let invocation;
|
|
823
|
+
|
|
824
|
+
let inlineFunctionOptions;
|
|
825
|
+
// Is Inline Function?
|
|
826
|
+
if (parsedValue.startsWith('#')) {
|
|
827
|
+
inlineFunctionOptions = inlineFunctions.get(parsedValue);
|
|
828
|
+
if (!inlineFunctionOptions) {
|
|
829
|
+
console.warn(`Invalid interpolation value: ${parsedValue}`);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
expression = inlineFunctionOptions.fn;
|
|
833
|
+
invocation = searchWithExpression;
|
|
834
|
+
if (inlineFunctionOptions.props) {
|
|
835
|
+
// console.log('This function has already been called. Reuse props', inlineFunctionOptions, this);
|
|
836
|
+
propsUsed = inlineFunctionOptions.props;
|
|
837
|
+
deepPropsUsed = inlineFunctionOptions.deepProps;
|
|
838
|
+
defaultValue = inlineFunctionOptions.defaultValue ?? null;
|
|
839
|
+
} else {
|
|
840
|
+
defaultValue = inlineFunctionOptions.fn;
|
|
841
|
+
}
|
|
842
|
+
} else {
|
|
843
|
+
defaultValue = null;
|
|
844
|
+
if (options?.defaults) {
|
|
845
|
+
defaultValue = deepPropFromObject(parsedValue.split('.'), options.defaults) ?? null;
|
|
846
|
+
}
|
|
847
|
+
if (defaultValue == null && options?.injections) {
|
|
848
|
+
defaultValue = valueFromPropName(parsedValue, options.injections);
|
|
849
|
+
console.log('default value from injection', parsedValue, { defaultValue });
|
|
850
|
+
}
|
|
851
|
+
}
|
|
559
852
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
853
|
+
if (!propsUsed) {
|
|
854
|
+
if (typeof defaultValue === 'function') {
|
|
855
|
+
// Value must be reinterpolated and function observed
|
|
856
|
+
const observeResult = observeFunction.call(this, defaultValue, options?.defaults, options?.injections);
|
|
857
|
+
const uniqueProps = new Set([
|
|
858
|
+
...observeResult.props.this,
|
|
859
|
+
...observeResult.props.args[0],
|
|
860
|
+
...observeResult.props.args[1],
|
|
861
|
+
]);
|
|
862
|
+
const uniqueDeepProps = new Set([
|
|
863
|
+
...observeResult.deepPropStrings.this,
|
|
864
|
+
...observeResult.deepPropStrings.args[0],
|
|
865
|
+
]);
|
|
866
|
+
expression = defaultValue;
|
|
867
|
+
defaultValue = observeResult.defaultValue;
|
|
868
|
+
propsUsed = [...uniqueProps];
|
|
869
|
+
deepPropsUsed = [...uniqueDeepProps].map((deepPropString) => deepPropString.split('.'));
|
|
870
|
+
invocation = searchWithExpression;
|
|
871
|
+
// console.log(this.static.name, fn.name || parsedValue, combinedSet);
|
|
872
|
+
} else {
|
|
873
|
+
// property binding
|
|
874
|
+
const parsedProps = parsedValue.split('.');
|
|
875
|
+
if (parsedProps.length === 1) {
|
|
876
|
+
prop = parsedValue;
|
|
877
|
+
propsUsed = [prop];
|
|
878
|
+
invocation = searchWithProp;
|
|
879
|
+
} else {
|
|
880
|
+
propsUsed = [parsedProps[0]];
|
|
881
|
+
deepProp = parsedProps;
|
|
882
|
+
deepPropsUsed = [parsedProps];
|
|
883
|
+
invocation = searchWithDeepProp;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// TODO: Rewrite property as deep with array index?
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (inlineFunctionOptions) {
|
|
891
|
+
inlineFunctionOptions.defaultValue = defaultValue;
|
|
892
|
+
inlineFunctionOptions.props = propsUsed;
|
|
893
|
+
inlineFunctionOptions.deepProps = deepPropsUsed;
|
|
894
|
+
}
|
|
895
|
+
subSearch = {
|
|
896
|
+
cacheIndex: this.#interpolationState.cacheIndex++,
|
|
897
|
+
dirtyIndex: this.#interpolationState.dirtyIndex++,
|
|
898
|
+
ranFlagIndex: this.#interpolationState.ranFlagIndex++,
|
|
899
|
+
query: subquery,
|
|
900
|
+
defaultValue,
|
|
901
|
+
subSearch: null,
|
|
902
|
+
prop,
|
|
903
|
+
propsUsed,
|
|
904
|
+
deepProp,
|
|
905
|
+
deepPropsUsed,
|
|
906
|
+
invocation,
|
|
907
|
+
expression,
|
|
908
|
+
};
|
|
909
|
+
this.addSearch(subSearch);
|
|
574
910
|
}
|
|
575
|
-
if (
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
911
|
+
if (isSubquery) {
|
|
912
|
+
search = {
|
|
913
|
+
cacheIndex: this.#interpolationState.cacheIndex++,
|
|
914
|
+
dirtyIndex: this.#interpolationState.dirtyIndex++,
|
|
915
|
+
ranFlagIndex: this.#interpolationState.ranFlagIndex++,
|
|
916
|
+
query,
|
|
917
|
+
subSearch,
|
|
918
|
+
negate,
|
|
919
|
+
doubleNegate,
|
|
920
|
+
prop: subSearch.prop,
|
|
921
|
+
deepProp: subSearch.deepProp,
|
|
922
|
+
propsUsed: subSearch.propsUsed,
|
|
923
|
+
deepPropsUsed: subSearch.deepPropsUsed,
|
|
924
|
+
defaultValue: doubleNegate ? !!subSearch.defaultValue
|
|
925
|
+
: (negate ? !subSearch.defaultValue : subSearch.defaultValue),
|
|
926
|
+
invocation(value) {
|
|
927
|
+
if (this.doubleNegate) return !!value;
|
|
928
|
+
if (this.negate) return !value;
|
|
929
|
+
console.warn('Unknown query mutation', this.query);
|
|
930
|
+
return value;
|
|
931
|
+
},
|
|
932
|
+
};
|
|
933
|
+
this.addSearch(search);
|
|
579
934
|
} else {
|
|
580
|
-
|
|
935
|
+
// Store as search instead
|
|
936
|
+
search = subSearch;
|
|
581
937
|
}
|
|
582
|
-
} else {
|
|
583
|
-
defaultValue = valueFromPropName(parsedValue, defaults);
|
|
584
938
|
}
|
|
585
939
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
940
|
+
// Tag
|
|
941
|
+
let tag;
|
|
942
|
+
let subnode = null;
|
|
943
|
+
let defaultValue = search.defaultValue;
|
|
944
|
+
if (text) {
|
|
945
|
+
text.data = defaultValue;
|
|
946
|
+
subnode = textNodeIndex;
|
|
947
|
+
} else if (nodeName === 'mdw-if') {
|
|
948
|
+
tag = this.#tagElement(element);
|
|
949
|
+
element.removeAttribute(nodeName);
|
|
950
|
+
defaultValue = defaultValue != null && defaultValue !== false;
|
|
951
|
+
} else {
|
|
952
|
+
subnode = nodeName;
|
|
953
|
+
if (nodeName === 'id' || defaultValue == null || defaultValue === false) {
|
|
954
|
+
element.removeAttribute(nodeName);
|
|
594
955
|
} else {
|
|
595
|
-
|
|
956
|
+
element.setAttribute(nodeName, defaultValue === true ? '' : defaultValue);
|
|
596
957
|
}
|
|
597
958
|
}
|
|
598
959
|
|
|
599
|
-
|
|
600
|
-
console.warn(': Invalid binding:', parsedValue);
|
|
601
|
-
defaultValue = null;
|
|
602
|
-
}
|
|
960
|
+
tag ??= this.#tagElement(element);
|
|
603
961
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
962
|
+
// Node entry
|
|
963
|
+
let nodeEntry = this.#interpolationState.nodeEntry;
|
|
964
|
+
if (!nodeEntry || nodeEntry.tag !== tag) {
|
|
965
|
+
nodeEntry = {
|
|
966
|
+
tag,
|
|
967
|
+
textNodes: [],
|
|
968
|
+
};
|
|
969
|
+
this.#interpolationState.nodeEntry = nodeEntry;
|
|
970
|
+
this.nodesToBind.push(nodeEntry);
|
|
971
|
+
this.#interpolationState.nodeIndex++;
|
|
608
972
|
}
|
|
609
973
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
inlineFunctionOptions.props = props;
|
|
613
|
-
}
|
|
974
|
+
/** @type {RenderGraphAction} */
|
|
975
|
+
let action;
|
|
614
976
|
|
|
615
|
-
//
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
977
|
+
// Node Action
|
|
978
|
+
if (text) {
|
|
979
|
+
nodeEntry.textNodes.push(textNodeIndex);
|
|
980
|
+
|
|
981
|
+
this.#interpolationState.nodeIndex++;
|
|
982
|
+
action = {
|
|
983
|
+
nodeIndex: this.#interpolationState.nodeIndex,
|
|
984
|
+
invocation: writeDOMText,
|
|
985
|
+
defaultValue,
|
|
986
|
+
search,
|
|
987
|
+
};
|
|
988
|
+
} else if (subnode) {
|
|
989
|
+
action = {
|
|
990
|
+
nodeIndex: this.#interpolationState.nodeIndex,
|
|
991
|
+
attrName: subnode,
|
|
992
|
+
defaultValue,
|
|
993
|
+
invocation: writeDOMAttribute,
|
|
994
|
+
search,
|
|
995
|
+
};
|
|
996
|
+
} else {
|
|
997
|
+
action = {
|
|
998
|
+
nodeIndex: this.#interpolationState.nodeIndex,
|
|
999
|
+
commentIndex: this.#interpolationState.commentIndex++,
|
|
1000
|
+
defaultValue,
|
|
1001
|
+
invocation: writeDOMElementAttachedState,
|
|
1002
|
+
search,
|
|
1003
|
+
};
|
|
1004
|
+
if (!defaultValue) {
|
|
1005
|
+
this.postInitActions.push({
|
|
1006
|
+
...action,
|
|
1007
|
+
invocation: writeDOMHideElementOnInit,
|
|
1008
|
+
});
|
|
623
1009
|
}
|
|
624
|
-
set.add(entry);
|
|
625
1010
|
}
|
|
626
1011
|
|
|
627
|
-
|
|
1012
|
+
this.addAction(action);
|
|
1013
|
+
this.tagsWithBindings.add(tag);
|
|
1014
|
+
}
|
|
628
1015
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
1016
|
+
/**
|
|
1017
|
+
* @param {Element} element
|
|
1018
|
+
* @return {string}
|
|
1019
|
+
*/
|
|
1020
|
+
#tagElement(element) {
|
|
1021
|
+
let id = element.id;
|
|
1022
|
+
if (id) {
|
|
1023
|
+
if (!this.allIds.includes(id)) {
|
|
1024
|
+
this.allIds.push(id);
|
|
636
1025
|
}
|
|
637
|
-
} else if (defaultValue == null || defaultValue === false) {
|
|
638
|
-
element.removeAttribute(nodeName);
|
|
639
1026
|
} else {
|
|
640
|
-
|
|
1027
|
+
id = generateUID();
|
|
1028
|
+
this.temporaryIds.add(id);
|
|
1029
|
+
this.allIds.push(id);
|
|
1030
|
+
element.id = id;
|
|
1031
|
+
}
|
|
1032
|
+
return id;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* TODO: Subtemplating lacks optimization, though functional.
|
|
1037
|
+
* - Would benefit from custom type handler for arrays
|
|
1038
|
+
* to avoid multi-iteration change-detection.
|
|
1039
|
+
* - Could benefit from debounced/throttled render
|
|
1040
|
+
* - Consider remap of {item.prop} as {array[index].prop}
|
|
1041
|
+
*
|
|
1042
|
+
* @param {Element} element
|
|
1043
|
+
* @param {InterpolateOptions} options
|
|
1044
|
+
* @return {?Composition<?>}
|
|
1045
|
+
*/
|
|
1046
|
+
#interpolateIterable(element, options) {
|
|
1047
|
+
// TODO: Microbenchmark element.attributes
|
|
1048
|
+
const forAttr = element.getAttribute('mdw-for');
|
|
1049
|
+
const trimmed = forAttr?.trim();
|
|
1050
|
+
if (!trimmed) {
|
|
1051
|
+
console.warn('Malformed mdw-for found at', element);
|
|
1052
|
+
return null;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
if (trimmed[0] !== '{') {
|
|
1056
|
+
console.warn('Malformed mdw-for found at', element);
|
|
1057
|
+
return null;
|
|
1058
|
+
}
|
|
1059
|
+
const { length } = trimmed;
|
|
1060
|
+
if (trimmed[length - 1] !== '}') {
|
|
1061
|
+
console.warn('Malformed mdw-for found at', element);
|
|
1062
|
+
return null;
|
|
641
1063
|
}
|
|
642
|
-
|
|
1064
|
+
const parsedValue = trimmed.slice(1, -1);
|
|
1065
|
+
const [valueName, iterableName] = parsedValue.split(/\s+of\s+/);
|
|
1066
|
+
element.removeAttribute('mdw-for');
|
|
1067
|
+
// Create a new composition targetting element as root
|
|
1068
|
+
|
|
1069
|
+
const elementAnchor = document.createElement('template');
|
|
1070
|
+
element.replaceWith(elementAnchor);
|
|
1071
|
+
const tag = this.#tagElement(elementAnchor);
|
|
1072
|
+
// console.log('tagging placeholder element with', elementAnchor, tag);
|
|
1073
|
+
|
|
1074
|
+
let nodeEntry = this.#interpolationState.nodeEntry;
|
|
1075
|
+
if (!nodeEntry || nodeEntry.tag !== tag) {
|
|
1076
|
+
nodeEntry = {
|
|
1077
|
+
tag,
|
|
1078
|
+
textNodes: [],
|
|
1079
|
+
};
|
|
1080
|
+
this.#interpolationState.nodeEntry = nodeEntry;
|
|
1081
|
+
this.nodesToBind.push(nodeEntry);
|
|
1082
|
+
this.#interpolationState.nodeIndex++;
|
|
1083
|
+
console.log('adding node entry', tag, this.#interpolationState.nodeIndex);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const newComposition = new Composition();
|
|
1087
|
+
newComposition.template.append(element);
|
|
1088
|
+
// Move uninterpolated element to new composition template.
|
|
1089
|
+
const injections = {
|
|
1090
|
+
...options.injections,
|
|
1091
|
+
[valueName]: null,
|
|
1092
|
+
index: null,
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
const propsUsed = [iterableName];
|
|
1096
|
+
/** @type {RenderGraphSearch} */
|
|
1097
|
+
const search = {
|
|
1098
|
+
cacheIndex: this.#interpolationState.cacheIndex++,
|
|
1099
|
+
dirtyIndex: this.#interpolationState.dirtyIndex++,
|
|
1100
|
+
ranFlagIndex: this.#interpolationState.ranFlagIndex++,
|
|
1101
|
+
propsUsed,
|
|
1102
|
+
deepPropsUsed: [[iterableName]],
|
|
1103
|
+
defaultValue: {},
|
|
1104
|
+
invocation(state, changes, data) {
|
|
1105
|
+
// Return unique to always specify dirty
|
|
1106
|
+
return {};
|
|
1107
|
+
},
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
/** @type {RenderGraphAction} */
|
|
1111
|
+
const action = {
|
|
1112
|
+
defaultValue: null,
|
|
1113
|
+
nodeIndex: this.#interpolationState.nodeIndex,
|
|
1114
|
+
search,
|
|
1115
|
+
commentIndex: this.#interpolationState.commentIndex++,
|
|
1116
|
+
injections,
|
|
1117
|
+
invocation(state, value, changes, data) {
|
|
1118
|
+
if (!newComposition.adapter) {
|
|
1119
|
+
// console.log({ state.options });
|
|
1120
|
+
const instanceAnchorElement = state.nodes[this.nodeIndex];
|
|
1121
|
+
const anchorNode = createEmptyComment();
|
|
1122
|
+
// Avoid leak
|
|
1123
|
+
state.nodes[this.commentIndex] = anchorNode;
|
|
1124
|
+
instanceAnchorElement.replaceWith(anchorNode);
|
|
1125
|
+
newComposition.adapter = new CompositionAdapter({
|
|
1126
|
+
anchorNode,
|
|
1127
|
+
composition: newComposition,
|
|
1128
|
+
renderOptions: {
|
|
1129
|
+
target: null,
|
|
1130
|
+
context: state.options.context,
|
|
1131
|
+
store: state.options.store,
|
|
1132
|
+
injections: this.injections,
|
|
1133
|
+
},
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
const { adapter } = newComposition;
|
|
1137
|
+
const iterable = (data ?? state.options.store)[iterableName];
|
|
1138
|
+
// Remove oversized
|
|
1139
|
+
if (!iterable || iterable.length === 0) {
|
|
1140
|
+
adapter.removeEntries();
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
const changeList = changes[iterableName];
|
|
1144
|
+
const innerChanges = { ...changes };
|
|
1145
|
+
const needTargetAll = newComposition.props.some((prop) => prop !== iterableName && prop in changes);
|
|
1146
|
+
|
|
1147
|
+
adapter.startBatch();
|
|
1148
|
+
if (!needTargetAll && !Array.isArray(changeList)) {
|
|
1149
|
+
const iterator = Array.isArray(changeList) ? changeList.entries() : Object.entries(changeList);
|
|
1150
|
+
// console.log('changeList render', iterator);
|
|
1151
|
+
for (const [key, change] of iterator) {
|
|
1152
|
+
if (key === 'length') continue;
|
|
1153
|
+
if (change === null) {
|
|
1154
|
+
// console.warn('null?', 'remove?', key);
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
const index = (+key);
|
|
1158
|
+
const resource = iterable[index];
|
|
1159
|
+
innerChanges[valueName] = change;
|
|
1160
|
+
this.injections[valueName] = resource;
|
|
1161
|
+
this.injections.index = index;
|
|
1162
|
+
|
|
1163
|
+
adapter.renderData(index, innerChanges, data, resource, change);
|
|
1164
|
+
}
|
|
1165
|
+
} else {
|
|
1166
|
+
if (!changeList) {
|
|
1167
|
+
delete innerChanges[valueName];
|
|
1168
|
+
}
|
|
1169
|
+
// console.log('full array render', iterable);
|
|
1170
|
+
for (const [index, resource] of iterable.entries()) {
|
|
1171
|
+
let change;
|
|
1172
|
+
if (changeList) {
|
|
1173
|
+
// console.warn('full array render has changeList?', changeList);
|
|
1174
|
+
if (!needTargetAll && !(index in changeList)) {
|
|
1175
|
+
console.warn('huh?');
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
change = changeList[index];
|
|
1179
|
+
if (change === null) {
|
|
1180
|
+
// console.warn('remove?');
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
innerChanges[valueName] = change;
|
|
1184
|
+
}
|
|
1185
|
+
this.injections[valueName] = resource;
|
|
1186
|
+
this.injections.index = index;
|
|
1187
|
+
|
|
1188
|
+
adapter.renderData(index, innerChanges, data, resource, change);
|
|
1189
|
+
// adapter.renderIndex(index, innerChanges, data, resource);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
adapter.stopBatch();
|
|
1193
|
+
|
|
1194
|
+
adapter.removeEntries(iterable.length);
|
|
1195
|
+
},
|
|
1196
|
+
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1199
|
+
newComposition.interpolate({
|
|
1200
|
+
defaults: options.defaults,
|
|
1201
|
+
injections,
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
propsUsed.push(...newComposition.props);
|
|
1205
|
+
this.addSearch(search);
|
|
1206
|
+
this.addAction(action);
|
|
1207
|
+
this.tagsWithBindings.add(tag);
|
|
1208
|
+
// console.log('adding', iterable, 'bind to', this);
|
|
1209
|
+
// this.addBinding(iterable, entry);
|
|
1210
|
+
return newComposition;
|
|
643
1211
|
}
|
|
644
1212
|
|
|
645
1213
|
/**
|
|
646
|
-
* @param {
|
|
1214
|
+
* @param {InterpolateOptions} [options]
|
|
647
1215
|
*/
|
|
648
|
-
interpolate(
|
|
1216
|
+
interpolate(options) {
|
|
1217
|
+
this.interpolateOptions = options;
|
|
649
1218
|
// console.log('Template', [...this.template.children].map((child) => child.outerHTML).join('\n'));
|
|
650
1219
|
|
|
651
1220
|
// Copy template before working on it
|
|
652
1221
|
// Store into `cloneable` to split later into `interpolation`
|
|
653
1222
|
this.cloneable = /** @type {DocumentFragment} */ (this.template.cloneNode(true));
|
|
654
1223
|
|
|
655
|
-
/**
|
|
656
|
-
* Track elements to be removed before using for cloning
|
|
657
|
-
* @type {Element[]}
|
|
658
|
-
*/
|
|
659
|
-
const removalList = [];
|
|
660
|
-
|
|
661
1224
|
const TREE_WALKER_FILTER = 5; /* NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT */
|
|
662
1225
|
|
|
663
1226
|
const treeWalker = document.createTreeWalker(this.cloneable, TREE_WALKER_FILTER);
|
|
1227
|
+
/** Note: `node` and treeWalker.currentNode may deviate */
|
|
664
1228
|
let node = treeWalker.nextNode();
|
|
665
1229
|
while (node) {
|
|
666
1230
|
/** @type {Element} */
|
|
667
1231
|
let element = null;
|
|
668
|
-
let removeElement = false;
|
|
669
1232
|
switch (node.nodeType) {
|
|
670
1233
|
case Node.ELEMENT_NODE:
|
|
671
|
-
element = node;
|
|
1234
|
+
element = /** @type {Element} */ (node);
|
|
672
1235
|
if (element instanceof HTMLTemplateElement) {
|
|
673
1236
|
node = treeWalker.nextSibling();
|
|
674
1237
|
continue;
|
|
@@ -689,27 +1252,26 @@ export default class Composition {
|
|
|
689
1252
|
node = treeWalker.nextSibling();
|
|
690
1253
|
continue;
|
|
691
1254
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
this
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
1255
|
+
|
|
1256
|
+
if (element.hasAttribute('mdw-for')) {
|
|
1257
|
+
node = treeWalker.nextSibling();
|
|
1258
|
+
this.#interpolateIterable(element, options);
|
|
1259
|
+
} else {
|
|
1260
|
+
const idAttr = element.attributes.id;
|
|
1261
|
+
if (idAttr) {
|
|
1262
|
+
this.#interpolateNode(idAttr, element, options);
|
|
1263
|
+
this.#tagElement(element);
|
|
1264
|
+
}
|
|
1265
|
+
for (const attr of [...element.attributes].reverse()) {
|
|
1266
|
+
if (attr.nodeName === 'id') continue; // Already handled
|
|
1267
|
+
this.#interpolateNode(attr, element, options);
|
|
705
1268
|
}
|
|
706
|
-
removeElement ||= this.#interpolateNode(attr, element, defaults);
|
|
707
1269
|
}
|
|
708
1270
|
|
|
709
1271
|
break;
|
|
710
1272
|
case Node.TEXT_NODE:
|
|
711
1273
|
element = node.parentNode;
|
|
712
|
-
if (this.#interpolateNode(/** @type {Text} */ (node), element,
|
|
1274
|
+
if (this.#interpolateNode(/** @type {Text} */ (node), element, options)) {
|
|
713
1275
|
const nextNode = treeWalker.nextNode();
|
|
714
1276
|
node.remove();
|
|
715
1277
|
node = nextNode;
|
|
@@ -720,29 +1282,9 @@ export default class Composition {
|
|
|
720
1282
|
default:
|
|
721
1283
|
throw new Error(`Unexpected node type: ${node.nodeType}`);
|
|
722
1284
|
}
|
|
723
|
-
if (removeElement) {
|
|
724
|
-
removalList.push(element);
|
|
725
|
-
}
|
|
726
1285
|
node = treeWalker.nextNode();
|
|
727
1286
|
}
|
|
728
1287
|
|
|
729
|
-
// Split into `interpolation` before removing elements
|
|
730
|
-
/** @type {DocumentFragment} */
|
|
731
|
-
this.interpolation = /** @type {DocumentFragment} */ (this.cloneable.cloneNode(true));
|
|
732
|
-
|
|
733
|
-
// console.debug('Interpolated', [...this.interpolation.children].map((child) => child.outerHTML).join('\n'));
|
|
734
|
-
|
|
735
|
-
// Remove elements from `cloneable` and place comment placeholders
|
|
736
|
-
// Remove in reverse so conditionals within conditionals are properly isolated
|
|
737
|
-
for (const element of [...removalList].reverse()) {
|
|
738
|
-
const { id } = element;
|
|
739
|
-
element.replaceWith(new Comment(`{#${id}}`));
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
for (const watcher of this.watchers) {
|
|
743
|
-
this.bindWatcher(watcher, defaults);
|
|
744
|
-
}
|
|
745
|
-
|
|
746
1288
|
if ('adoptedStyleSheets' in document) {
|
|
747
1289
|
this.adoptedStyleSheets = [
|
|
748
1290
|
...generateCSSStyleSheets(this.styles),
|
|
@@ -754,235 +1296,50 @@ export default class Composition {
|
|
|
754
1296
|
);
|
|
755
1297
|
}
|
|
756
1298
|
|
|
757
|
-
this.
|
|
1299
|
+
this.props = [...this.actionsByPropsUsed.keys()];
|
|
758
1300
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
* @see https://www.rfc-editor.org/rfc/rfc7386
|
|
766
|
-
* @param {DocumentFragment|ShadowRoot} root where
|
|
767
|
-
* @param {Partial<?>} data what
|
|
768
|
-
* @return {void}
|
|
769
|
-
*/
|
|
770
|
-
initialRender(root, data) {
|
|
771
|
-
if (!this.interpolated) this.interpolate(data);
|
|
772
|
-
|
|
773
|
-
if ('adoptedStyleSheets' in root) {
|
|
774
|
-
root.adoptedStyleSheets = [
|
|
775
|
-
...root.adoptedStyleSheets,
|
|
776
|
-
...this.adoptedStyleSheets,
|
|
777
|
-
];
|
|
778
|
-
} else if (root instanceof ShadowRoot) {
|
|
779
|
-
root.append(this.stylesFragment.cloneNode(true));
|
|
780
|
-
} else {
|
|
781
|
-
// TODO: Support document styles
|
|
782
|
-
// console.warn('Cannot apply styles to singular element');
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
root.append(this.cloneable.cloneNode(true));
|
|
786
|
-
|
|
787
|
-
// console.log('Initial render', [...root.children].map((child) => child.outerHTML).join('\n'));
|
|
788
|
-
|
|
789
|
-
/** @type {EventTarget} */
|
|
790
|
-
const rootEventTarget = root instanceof ShadowRoot ? root.host : root;
|
|
791
|
-
// Bind events in reverse order to support stopImmediatePropagation
|
|
792
|
-
for (const [id, events] of [...this.events].reverse()) {
|
|
793
|
-
// Prepare all event listeners first
|
|
794
|
-
for (const entry of [...events].reverse()) {
|
|
795
|
-
let listener = entry.listener;
|
|
796
|
-
if (!listener) {
|
|
797
|
-
if (root instanceof ShadowRoot) {
|
|
798
|
-
listener = entry.handleEvent ?? valueFromPropName(entry.prop, data);
|
|
799
|
-
if (id) {
|
|
800
|
-
// Wrap to retarget this
|
|
801
|
-
listener = buildShadowRootChildListener(listener);
|
|
802
|
-
}
|
|
803
|
-
// Cache and reuse
|
|
804
|
-
entry.listener = listener;
|
|
805
|
-
// console.log('caching listener', entry);
|
|
806
|
-
} else {
|
|
807
|
-
throw new TypeError('Anonymous event listeners cannot be used in templates');
|
|
808
|
-
// console.warn('creating new listener', entry);
|
|
809
|
-
// listener = entry.handleEvent ?? ((event) => {
|
|
810
|
-
// valueFromPropName(entry.prop, data)(event);
|
|
811
|
-
// });
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
const eventTarget = id ? root.getElementById(id) : rootEventTarget;
|
|
815
|
-
if (!eventTarget) {
|
|
816
|
-
// Element is not available yet. Bind on reference
|
|
817
|
-
console.debug('Composition: Skip bind events for', id);
|
|
818
|
-
continue;
|
|
819
|
-
}
|
|
820
|
-
eventTarget.addEventListener(entry.type, listener, entry);
|
|
1301
|
+
for (const id of this.allIds) {
|
|
1302
|
+
if (!this.tagsWithBindings.has(id)) {
|
|
1303
|
+
this.nodesToBind.push({
|
|
1304
|
+
tag: id,
|
|
1305
|
+
textNodes: [],
|
|
1306
|
+
});
|
|
821
1307
|
}
|
|
822
1308
|
}
|
|
823
1309
|
|
|
824
|
-
this.
|
|
825
|
-
}
|
|
1310
|
+
this.tags = this.nodesToBind.map((n) => n.tag);
|
|
826
1311
|
|
|
827
|
-
|
|
828
|
-
* @param {string} id
|
|
829
|
-
* @param {Element} element
|
|
830
|
-
*/
|
|
831
|
-
attachEventListeners(id, element) {
|
|
832
|
-
const events = this.events.get(id);
|
|
833
|
-
if (events) {
|
|
834
|
-
console.debug('attaching events for', id);
|
|
835
|
-
} else {
|
|
836
|
-
// console.log('no events for', id);
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
for (const entry of [...this.events.get(id)].reverse()) {
|
|
840
|
-
const { listener } = entry;
|
|
841
|
-
if (!listener) {
|
|
842
|
-
throw new Error('Template must be interpolated before attaching events');
|
|
843
|
-
}
|
|
844
|
-
element.addEventListener(entry.type, listener, entry);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
1312
|
+
this.interpolated = true;
|
|
847
1313
|
|
|
848
|
-
|
|
849
|
-
* @param {Element|DocumentFragment} root
|
|
850
|
-
* @return {Map<string,Element>}
|
|
851
|
-
*/
|
|
852
|
-
getReferences(root) {
|
|
853
|
-
let references = this.referenceCache.get(root);
|
|
854
|
-
if (!references) {
|
|
855
|
-
references = new Map();
|
|
856
|
-
this.referenceCache.set(root, references);
|
|
857
|
-
}
|
|
858
|
-
return references;
|
|
1314
|
+
// console.log('Cloneable', [...this.cloneable.children].map((child) => child.outerHTML).join('\n'));
|
|
859
1315
|
}
|
|
860
1316
|
|
|
861
1317
|
/**
|
|
862
|
-
* @param {
|
|
863
|
-
* @
|
|
864
|
-
* @return {Element}
|
|
1318
|
+
* @param {RenderGraphSearch} search
|
|
1319
|
+
* @return {RenderGraphSearch}
|
|
865
1320
|
*/
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
return element;
|
|
872
|
-
}
|
|
873
|
-
if (element === null) return null; // Cached null response
|
|
874
|
-
|
|
875
|
-
// Undefined
|
|
876
|
-
|
|
877
|
-
// console.log('Search in DOM', id);
|
|
878
|
-
element = root.getElementById(id);
|
|
879
|
-
|
|
880
|
-
if (element) {
|
|
881
|
-
// console.log('Found in DOM', id);
|
|
882
|
-
references.set(id, element);
|
|
883
|
-
return element;
|
|
1321
|
+
addSearch(search) {
|
|
1322
|
+
this.searches.push(search);
|
|
1323
|
+
if (search.query) {
|
|
1324
|
+
this.searchByQuery.set(search.query, search);
|
|
1325
|
+
this.initCache[search.cacheIndex] = search.defaultValue;
|
|
884
1326
|
}
|
|
885
|
-
|
|
886
|
-
// Element not in DOM means child of conditional element
|
|
887
|
-
/** @type {Element} */
|
|
888
|
-
let anchorElement;
|
|
889
|
-
|
|
890
|
-
// Check if element is conditional
|
|
891
|
-
let cloneTarget = this.conditionalElementMetadata.get(id)?.element;
|
|
892
|
-
|
|
893
|
-
if (!cloneTarget) {
|
|
894
|
-
// Check if element even exists in interpolation
|
|
895
|
-
// Check interpolation (full-tree) first
|
|
896
|
-
const interpolatedElement = this.interpolation.getElementById(id);
|
|
897
|
-
if (!interpolatedElement) {
|
|
898
|
-
console.warn('Not in full-tree', id);
|
|
899
|
-
// Cache not in full composition
|
|
900
|
-
references.set(id, null);
|
|
901
|
-
return null;
|
|
902
|
-
}
|
|
903
|
-
// Iterate backgrounds until closest conditional element
|
|
904
|
-
// const anchorElementId = this.template.getElementById(id).closest('[_if]').id;
|
|
905
|
-
// anchorElement = this.references.get(anchorElementId) this.interpolation.getElementById(anchorElementId).cloneNode(true);
|
|
906
|
-
let parentElement = interpolatedElement;
|
|
907
|
-
while ((parentElement = parentElement.parentElement) != null) {
|
|
908
|
-
const parentId = parentElement.id;
|
|
909
|
-
if (!parentId) {
|
|
910
|
-
console.warn('Parent does not have ID!');
|
|
911
|
-
cloneTarget = parentElement;
|
|
912
|
-
continue;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
// Parent already referenced
|
|
916
|
-
const referencedParent = references.get(parentId);
|
|
917
|
-
if (referencedParent) {
|
|
918
|
-
// Element may have been removed without ever tree-walking
|
|
919
|
-
console.debug('Parent already referenced', parentId, '>', id);
|
|
920
|
-
anchorElement = referencedParent;
|
|
921
|
-
break;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
const liveElement = root.getElementById(parentId);
|
|
925
|
-
if (liveElement) {
|
|
926
|
-
console.warn('Parent in DOM and not referenced', parentId, '>', id);
|
|
927
|
-
// Parent already in DOM. Cache reference
|
|
928
|
-
references.set(parentId, liveElement);
|
|
929
|
-
anchorElement = referencedParent;
|
|
930
|
-
break;
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
const conditionalParent = this.conditionalElementMetadata.get(parentId)?.element;
|
|
934
|
-
if (conditionalParent) {
|
|
935
|
-
console.debug('Found parent conditional element', parentId, '>', id);
|
|
936
|
-
cloneTarget = conditionalParent;
|
|
937
|
-
break;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
cloneTarget = parentElement;
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
anchorElement ??= /** @type {Element} */ (cloneTarget.cloneNode(true));
|
|
945
|
-
|
|
946
|
-
// Iterate downwards and cache all references
|
|
947
|
-
let node = anchorElement;
|
|
948
|
-
const iterator = document.createTreeWalker(anchorElement, NodeFilter.SHOW_ELEMENT);
|
|
949
|
-
do {
|
|
950
|
-
const nodeIdentifier = node.id;
|
|
951
|
-
if (!element && nodeIdentifier === id) {
|
|
952
|
-
element = node;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
if (nodeIdentifier) {
|
|
956
|
-
// console.debug('Caching element', nodeIdentifier);
|
|
957
|
-
references.set(nodeIdentifier, node);
|
|
958
|
-
if (cloneTarget) {
|
|
959
|
-
// Attach events regardless of DOM state.
|
|
960
|
-
// EventTargets should still fire even if not part of live document
|
|
961
|
-
this.attachEventListeners(id, element);
|
|
962
|
-
}
|
|
963
|
-
} else {
|
|
964
|
-
console.warn('Could not cache node', node);
|
|
965
|
-
}
|
|
966
|
-
} while ((node = iterator.nextNode()));
|
|
967
|
-
return element;
|
|
1327
|
+
return search;
|
|
968
1328
|
}
|
|
969
1329
|
|
|
970
1330
|
/**
|
|
971
|
-
* @param {
|
|
972
|
-
* @
|
|
973
|
-
* @return {boolean} reusable
|
|
1331
|
+
* @param {RenderGraphAction} action
|
|
1332
|
+
* @return {RenderGraphAction}
|
|
974
1333
|
*/
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
set
|
|
982
|
-
this.bindings.set(prop, set);
|
|
1334
|
+
addAction(action) {
|
|
1335
|
+
this.actions.push(action);
|
|
1336
|
+
for (const prop of action.search.propsUsed) {
|
|
1337
|
+
if (this.actionsByPropsUsed.has(prop)) {
|
|
1338
|
+
this.actionsByPropsUsed.get(prop).push(action);
|
|
1339
|
+
} else {
|
|
1340
|
+
this.actionsByPropsUsed.set(prop, [action]);
|
|
983
1341
|
}
|
|
984
|
-
set.add(entry);
|
|
985
1342
|
}
|
|
986
|
-
return
|
|
1343
|
+
return action;
|
|
987
1344
|
}
|
|
988
1345
|
}
|