mount-observer 0.0.6 → 0.0.8
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/MountObserver.js +68 -65
- package/README.md +239 -57
- package/doWhereAttr.js +43 -0
- package/getWhereAttrSelector.js +35 -0
- package/package.json +1 -1
- package/types.d.ts +20 -7
package/MountObserver.js
CHANGED
|
@@ -12,10 +12,10 @@ export class MountObserver extends EventTarget {
|
|
|
12
12
|
#isComplex;
|
|
13
13
|
constructor(init) {
|
|
14
14
|
super();
|
|
15
|
-
const {
|
|
15
|
+
const { on, whereElementIntersectsWith, whereMediaMatches } = init;
|
|
16
16
|
let isComplex = false;
|
|
17
|
-
if (
|
|
18
|
-
const reducedMatch =
|
|
17
|
+
if (on !== undefined) {
|
|
18
|
+
const reducedMatch = on.replaceAll(':not(', '');
|
|
19
19
|
isComplex = reducedMatch.includes(' ') || reducedMatch.includes(':');
|
|
20
20
|
}
|
|
21
21
|
this.#isComplex = isComplex;
|
|
@@ -28,21 +28,20 @@ export class MountObserver extends EventTarget {
|
|
|
28
28
|
//this.#unmounted = new WeakSet();
|
|
29
29
|
}
|
|
30
30
|
#calculatedSelector;
|
|
31
|
-
|
|
31
|
+
#fullListOfAttrs;
|
|
32
|
+
//get #attrVals
|
|
33
|
+
async #selector() {
|
|
32
34
|
if (this.#calculatedSelector !== undefined)
|
|
33
35
|
return this.#calculatedSelector;
|
|
34
|
-
const {
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
37
|
-
return
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
this.#calculatedSelector = matches.join(',');
|
|
36
|
+
const { on, whereAttr } = this.#mountInit;
|
|
37
|
+
const withoutAttrs = on || '*';
|
|
38
|
+
if (whereAttr === undefined)
|
|
39
|
+
return withoutAttrs;
|
|
40
|
+
const { getWhereAttrSelector } = await import('./getWhereAttrSelector.js');
|
|
41
|
+
const info = getWhereAttrSelector(whereAttr, withoutAttrs);
|
|
42
|
+
const { fullListOfAttrs, calculatedSelector } = info;
|
|
43
|
+
this.#fullListOfAttrs = fullListOfAttrs;
|
|
44
|
+
this.#calculatedSelector = calculatedSelector;
|
|
46
45
|
return this.#calculatedSelector;
|
|
47
46
|
}
|
|
48
47
|
unobserve(within) {
|
|
@@ -86,8 +85,9 @@ export class MountObserver extends EventTarget {
|
|
|
86
85
|
}
|
|
87
86
|
}
|
|
88
87
|
const rootMutObs = mutationObserverLookup.get(within);
|
|
89
|
-
const {
|
|
90
|
-
|
|
88
|
+
//const {whereAttr} = this.#mountInit;
|
|
89
|
+
const fullListOfAttrs = this.#fullListOfAttrs;
|
|
90
|
+
rootMutObs.addEventListener('mutation-event', async (e) => {
|
|
91
91
|
//TODO: disconnected
|
|
92
92
|
if (this.#isComplex) {
|
|
93
93
|
this.#inspectWithin(within, false);
|
|
@@ -96,7 +96,7 @@ export class MountObserver extends EventTarget {
|
|
|
96
96
|
const { mutationRecords } = e;
|
|
97
97
|
const elsToInspect = [];
|
|
98
98
|
//const elsToDisconnect: Array<Element> = [];
|
|
99
|
-
const doDisconnect = this.#mountInit.do?.
|
|
99
|
+
const doDisconnect = this.#mountInit.do?.disconnect;
|
|
100
100
|
for (const mutationRecord of mutationRecords) {
|
|
101
101
|
const { addedNodes, type, removedNodes } = mutationRecord;
|
|
102
102
|
//console.log(mutationRecord);
|
|
@@ -104,22 +104,11 @@ export class MountObserver extends EventTarget {
|
|
|
104
104
|
addedElements.forEach(x => elsToInspect.push(x));
|
|
105
105
|
if (type === 'attributes') {
|
|
106
106
|
const { target, attributeName, oldValue } = mutationRecord;
|
|
107
|
-
if (target instanceof Element && attributeName !== null &&
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (names.includes(attributeName)) {
|
|
107
|
+
if (target instanceof Element && attributeName !== null && this.#mounted.has(target)) {
|
|
108
|
+
if (fullListOfAttrs !== undefined) {
|
|
109
|
+
const idx = fullListOfAttrs.indexOf(attributeName);
|
|
110
|
+
if (idx > -1) {
|
|
112
111
|
const newValue = target.getAttribute(attributeName);
|
|
113
|
-
// let parsedNewValue = undefined;
|
|
114
|
-
// switch(type){
|
|
115
|
-
// case 'boolean':
|
|
116
|
-
// parsedNewValue = newValue === 'true' ? true : newValue === 'false' ? false : null;
|
|
117
|
-
// break;
|
|
118
|
-
// case 'date':
|
|
119
|
-
// parsedNewValue = newValue === null ? null : new Date(newValue);
|
|
120
|
-
// break;
|
|
121
|
-
// case ''
|
|
122
|
-
// }
|
|
123
112
|
const attrChangeInfo = {
|
|
124
113
|
name: attributeName,
|
|
125
114
|
oldValue,
|
|
@@ -128,7 +117,13 @@ export class MountObserver extends EventTarget {
|
|
|
128
117
|
};
|
|
129
118
|
this.dispatchEvent(new AttrChangeEvent(target, attrChangeInfo));
|
|
130
119
|
}
|
|
131
|
-
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const { whereAttr } = this.#mountInit;
|
|
123
|
+
if (whereAttr !== undefined) {
|
|
124
|
+
const { doWhereAttr } = await import('./doWhereAttr.js');
|
|
125
|
+
doWhereAttr(whereAttr, attributeName, target, oldValue, this);
|
|
126
|
+
}
|
|
132
127
|
}
|
|
133
128
|
}
|
|
134
129
|
elsToInspect.push(target);
|
|
@@ -140,7 +135,7 @@ export class MountObserver extends EventTarget {
|
|
|
140
135
|
// this.#mountedList = this.#mountedList?.filter(x => x.deref() !== deletedElement);
|
|
141
136
|
this.#disconnected.add(deletedElement);
|
|
142
137
|
if (doDisconnect !== undefined) {
|
|
143
|
-
doDisconnect(deletedElement, this);
|
|
138
|
+
doDisconnect(deletedElement, this, {});
|
|
144
139
|
}
|
|
145
140
|
this.dispatchEvent(new DisconnectEvent(deletedElement));
|
|
146
141
|
}
|
|
@@ -160,9 +155,10 @@ export class MountObserver extends EventTarget {
|
|
|
160
155
|
}
|
|
161
156
|
async #mount(matching, initializing) {
|
|
162
157
|
//first unmount non matching
|
|
163
|
-
const alreadyMounted = this.#filterAndDismount();
|
|
164
|
-
const
|
|
165
|
-
const { import: imp
|
|
158
|
+
const alreadyMounted = await this.#filterAndDismount();
|
|
159
|
+
const mount = this.#mountInit.do?.mount;
|
|
160
|
+
const { import: imp } = this.#mountInit;
|
|
161
|
+
const fullListOfAttrs = this.#fullListOfAttrs;
|
|
166
162
|
for (const match of matching) {
|
|
167
163
|
if (alreadyMounted.has(match))
|
|
168
164
|
continue;
|
|
@@ -186,41 +182,47 @@ export class MountObserver extends EventTarget {
|
|
|
186
182
|
break;
|
|
187
183
|
}
|
|
188
184
|
}
|
|
189
|
-
if (
|
|
190
|
-
|
|
185
|
+
if (mount !== undefined) {
|
|
186
|
+
mount(match, this, {
|
|
191
187
|
stage: 'PostImport',
|
|
192
188
|
initializing
|
|
193
189
|
});
|
|
194
190
|
}
|
|
195
191
|
this.dispatchEvent(new MountEvent(match, initializing));
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
for (const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
for (const name of names) {
|
|
203
|
-
const attrVal = match.getAttribute(name);
|
|
204
|
-
if (attrVal !== null)
|
|
205
|
-
nonNullName = name;
|
|
206
|
-
newValue = newValue || attrVal;
|
|
192
|
+
if (fullListOfAttrs !== undefined) {
|
|
193
|
+
const { whereAttr } = this.#mountInit;
|
|
194
|
+
for (const name of fullListOfAttrs) {
|
|
195
|
+
if (whereAttr !== undefined) {
|
|
196
|
+
const { doWhereAttr } = await import('./doWhereAttr.js');
|
|
197
|
+
doWhereAttr(whereAttr, name, match, null, this);
|
|
207
198
|
}
|
|
208
|
-
const attribInfo = {
|
|
209
|
-
oldValue: null,
|
|
210
|
-
newValue,
|
|
211
|
-
idx,
|
|
212
|
-
name: nonNullName
|
|
213
|
-
};
|
|
214
|
-
this.dispatchEvent(new AttrChangeEvent(match, attribInfo));
|
|
215
|
-
idx++;
|
|
216
199
|
}
|
|
200
|
+
// let idx = 0;
|
|
201
|
+
// for(const attribMatch of attribMatches){
|
|
202
|
+
// let newValue = null;
|
|
203
|
+
// const {names} = attribMatch;
|
|
204
|
+
// let nonNullName = names[0];
|
|
205
|
+
// for(const name of names){
|
|
206
|
+
// const attrVal = match.getAttribute(name);
|
|
207
|
+
// if(attrVal !== null) nonNullName = name;
|
|
208
|
+
// newValue = newValue || attrVal;
|
|
209
|
+
// }
|
|
210
|
+
// const attribInfo: AttrChangeInfo = {
|
|
211
|
+
// oldValue: null,
|
|
212
|
+
// newValue,
|
|
213
|
+
// idx,
|
|
214
|
+
// name: nonNullName
|
|
215
|
+
// };
|
|
216
|
+
// this.dispatchEvent(new AttrChangeEvent(match, attribInfo));
|
|
217
|
+
// idx++;
|
|
218
|
+
// }
|
|
217
219
|
}
|
|
218
220
|
this.#mountedList?.push(new WeakRef(match));
|
|
219
221
|
//if(this.#unmounted.has(match)) this.#unmounted.delete(match);
|
|
220
222
|
}
|
|
221
223
|
}
|
|
222
224
|
async #dismount(unmatching) {
|
|
223
|
-
const onDismount = this.#mountInit.do?.
|
|
225
|
+
const onDismount = this.#mountInit.do?.dismount;
|
|
224
226
|
for (const unmatch of unmatching) {
|
|
225
227
|
if (onDismount !== undefined) {
|
|
226
228
|
onDismount(unmatch, this, {});
|
|
@@ -228,12 +230,12 @@ export class MountObserver extends EventTarget {
|
|
|
228
230
|
this.dispatchEvent(new DismountEvent(unmatch));
|
|
229
231
|
}
|
|
230
232
|
}
|
|
231
|
-
#filterAndDismount() {
|
|
233
|
+
async #filterAndDismount() {
|
|
232
234
|
const returnSet = new Set();
|
|
233
235
|
if (this.#mountedList !== undefined) {
|
|
234
236
|
const previouslyMounted = this.#mountedList.map(x => x.deref());
|
|
235
237
|
const { whereSatisfies, whereInstanceOf } = this.#mountInit;
|
|
236
|
-
const match = this.#selector;
|
|
238
|
+
const match = await this.#selector();
|
|
237
239
|
const elsToUnMount = previouslyMounted.filter(x => {
|
|
238
240
|
if (x === undefined)
|
|
239
241
|
return false;
|
|
@@ -253,7 +255,7 @@ export class MountObserver extends EventTarget {
|
|
|
253
255
|
}
|
|
254
256
|
async #filterAndMount(els, checkMatch, initializing) {
|
|
255
257
|
const { whereSatisfies, whereInstanceOf } = this.#mountInit;
|
|
256
|
-
const match = this.#selector;
|
|
258
|
+
const match = await this.#selector();
|
|
257
259
|
const elsToMount = els.filter(x => {
|
|
258
260
|
if (checkMatch) {
|
|
259
261
|
if (!x.matches(match))
|
|
@@ -272,7 +274,7 @@ export class MountObserver extends EventTarget {
|
|
|
272
274
|
this.#mount(elsToMount, initializing);
|
|
273
275
|
}
|
|
274
276
|
async #inspectWithin(within, initializing) {
|
|
275
|
-
const els = Array.from(within.querySelectorAll(this.#selector));
|
|
277
|
+
const els = Array.from(within.querySelectorAll(await this.#selector()));
|
|
276
278
|
this.#filterAndMount(els, false, initializing);
|
|
277
279
|
}
|
|
278
280
|
}
|
|
@@ -318,3 +320,4 @@ export class AttrChangeEvent extends Event {
|
|
|
318
320
|
this.attrChangeInfo = attrChangeInfo;
|
|
319
321
|
}
|
|
320
322
|
}
|
|
323
|
+
//const hasRootInDefault = ['data', 'enh', 'data-enh']
|
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
[](https://bundlephobia.com/result?p=mount-observer)
|
|
4
4
|
<img src="http://img.badgesize.io/https://cdn.jsdelivr.net/npm/mount-observer?compression=gzip">
|
|
5
5
|
|
|
6
|
+
Note that much of what is described below has not yet been polyfilled.
|
|
6
7
|
|
|
7
8
|
# The MountObserver api.
|
|
8
9
|
|
|
@@ -10,13 +11,13 @@ Author: Bruce B. Anderson
|
|
|
10
11
|
|
|
11
12
|
Issues / pr's / polyfill: [mount-observer](https://github.com/bahrus/mount-observer)
|
|
12
13
|
|
|
13
|
-
Last Update:
|
|
14
|
+
Last Update: 2024-2-14
|
|
14
15
|
|
|
15
16
|
## Benefits of this API
|
|
16
17
|
|
|
17
|
-
What follows is a far more ambitious alternative to the [lazy custom element proposal](https://github.com/w3c/webcomponents/issues/782). The goals of the MountObserver api are more encompassing, and less focused on registering custom elements. In fact, this proposal addresses numerous use cases in one api. It is basically mapping common filtering conditions in the DOM, to
|
|
18
|
+
What follows is a far more ambitious alternative to the [lazy custom element proposal](https://github.com/w3c/webcomponents/issues/782). The goals of the MountObserver api are more encompassing, and less focused on registering custom elements. In fact, this proposal addresses numerous use cases in one api. It is basically mapping common filtering conditions in the DOM, to mounting a "campaign" of some sort, like importing a resource, and/or progressively enhancing an element, and/or "binding from a distance".
|
|
18
19
|
|
|
19
|
-
"Binding from a distance" refers to empowering the developer to essentially manage their own "stylesheets" -- but rather than for purposes of styling, using these rules to attach behaviors, set property values, etc.
|
|
20
|
+
["Binding from a distance"](https://github.com/WICG/webcomponents/issues/1035#issuecomment-1806393525) refers to empowering the developer to essentially manage their own "stylesheets" -- but rather than for purposes of styling, using these rules to attach behaviors, set property values, etc, to the HTML as it streams in. Libraries that take this approach include [Corset](https://corset.dev/) and [trans-render](https://github.com/bahrus/trans-render). The concept has been promoted by a [number](https://bkardell.com/blog/CSSLike.html) [of](https://www.w3.org/TR/NOTE-AS) prominent voices in the community.
|
|
20
21
|
|
|
21
22
|
The underlying theme is this api is meant to make it easy for the developer to do the right thing, by encouraging lazy loading and smaller footprints. It rolls up most all the other observer api's into one.
|
|
22
23
|
|
|
@@ -24,7 +25,7 @@ The underlying theme is this api is meant to make it easy for the developer to d
|
|
|
24
25
|
|
|
25
26
|
There is quite a bit of functionality this proposal would open up, that is exceedingly difficult to polyfill reliably:
|
|
26
27
|
|
|
27
|
-
1. It is unclear how to use mutation observers to observe changes to [custom state](https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet). The closest thing might be a solution like [this](https://davidwalsh.name/detect-node-insertion), but that falls short for elements that aren't visible, or during template instantiation.
|
|
28
|
+
1. It is unclear how to use mutation observers to observe changes to [custom state](https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet). The closest thing might be a solution like [this](https://davidwalsh.name/detect-node-insertion), but that falls short for elements that aren't visible, or during template instantiation, and requires carefully constructed "negating" queries if needing to know when the css selector is no longer matching.
|
|
28
29
|
|
|
29
30
|
2. For simple css matches, like "my-element", or "[name='hello']" it is enough to use a mutation observer, and only observe the elements within the specified DOM region (more on that below). But as CSS has evolved, it is quite easy to think of numerous css selectors that would require us to expand our mutation observer to need to scan the entire Shadow DOM realm, or the entire DOM tree outside any Shadow DOM, for any and all mutations (including attribute changes), and re-evaluate every single element within the specified DOM region for new matches or old matches that no longer match. Things like child selectors, :has, and so on. All this is done, miraculously, by the browser in a performant way. Reproducing this in userland using JavaScript alone, matching the same performance seems impossible.
|
|
30
31
|
|
|
@@ -36,11 +37,10 @@ The amount of code necessary to accomplish these common tasks designed to improv
|
|
|
36
37
|
|
|
37
38
|
1. Give the developer a strong signal to do the right thing, by
|
|
38
39
|
1. Making lazy loading of resource dependencies easy, to the benefit of users with expensive networks.
|
|
39
|
-
2. Supporting "binding from a distance" that can
|
|
40
|
-
3. Supporting "progressive enhancement.
|
|
41
|
-
2.
|
|
42
|
-
3.
|
|
43
|
-
4. To do the job right, polyfills really need to reexamine **all** the elements within the observed node for matches **anytime any element within the Shadow Root so much as sneezes (has attribute modified, changes custom state, etc)**, due to modern selectors such as the :has selector. Surely, the platform has found ways to do this more efficiently?
|
|
40
|
+
2. Supporting "binding from a distance" that can set property values of elements in bulk as the HTML streams in. For example, say a web page is streaming in HTML with thousands of input elements (say a long tax form). We want to have some indication in the head tag of the HTML (for example) to make all the input elements read only as they stream through the page. With css, we could do similar things, for example set the background to red of all input elements. Why can't we do something similar with setting properties like readOnly, disabled, etc? With this api, giving developers the "keys" to css filtering, so they can "mount a campaign" to apply common settings on them all feels like something that almost every web developer has mentally screamed to themselves "why can't I do that?", doesn't it?
|
|
41
|
+
3. Supporting "progressive enhancement" more effectively.
|
|
42
|
+
2. Potentially by allowing the platform to do more work in the low-level (c/c++/rust?) code, without as much context switching into the JavaScript memory space, which may reduce cpu cycles as well. This is done by passing into the API substantial number of conditions, which can all be evaluated at a lower level, before the api needs to surface up to the developer "found one!".
|
|
43
|
+
3. As discussed earlier, to do the job right, polyfills really need to reexamine **all** the elements within the observed node for matches **anytime any element within the Shadow Root so much as sneezes (has attribute modified, changes custom state, etc)**, due to modern selectors such as the :has selector. Surely, the platform has found ways to do this more efficiently?
|
|
44
44
|
|
|
45
45
|
The extra flexibility this new primitive would provide could be quite useful to things other than lazy loading of custom elements, such as implementing [custom enhancements](https://github.com/WICG/webcomponents/issues/1000) as well as [binding from a distance](https://github.com/WICG/webcomponents/issues/1035#issuecomment-1806393525) in userland.
|
|
46
46
|
|
|
@@ -50,12 +50,12 @@ To specify the equivalent of what the alternative proposal linked to above would
|
|
|
50
50
|
|
|
51
51
|
```JavaScript
|
|
52
52
|
const observer = new MountObserver({
|
|
53
|
-
|
|
53
|
+
on:'my-element',
|
|
54
54
|
import: './my-element.js',
|
|
55
55
|
do: {
|
|
56
|
-
|
|
56
|
+
mount: ({localName}, {module}) => {
|
|
57
57
|
if(!customElements.get(localName)) {
|
|
58
|
-
customElements.define(localName, module.
|
|
58
|
+
customElements.define(localName, module.MyElement);
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
}
|
|
@@ -67,36 +67,43 @@ If no import is specified, it would go straight to do.* (if any such callbacks a
|
|
|
67
67
|
|
|
68
68
|
This only searches for elements matching 'my-element' outside any shadow DOM.
|
|
69
69
|
|
|
70
|
-
But the observe method can accept a shadowRoot, or a node inside a shadowRoot as well.
|
|
70
|
+
But the observe method can accept a node within the document, or a shadowRoot, or a node inside a shadowRoot as well.
|
|
71
71
|
|
|
72
72
|
The import can also be a function:
|
|
73
73
|
|
|
74
74
|
```JavaScript
|
|
75
75
|
const observer = new MountObserver({
|
|
76
|
-
|
|
76
|
+
on: 'my-element',
|
|
77
77
|
import: async (matchingElement, {module}) => await import('./my-element.js');
|
|
78
78
|
});
|
|
79
79
|
observer.observe(myRootNode);
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
which would work better with current bundlers, I suspect. Also, we can do interesting things like merge multiple imports into one "module". But should this API built into the platform, such functions wouldn't be necessary, as bundlers could start to recognize strings that are passed to the MountObserver's constructor.
|
|
82
|
+
which would work better with current bundlers, I suspect. Also, we can do interesting things like merge multiple imports into one "module". But should this API be built into the platform, such functions wouldn't be necessary, as bundlers could start to recognize strings that are passed to the MountObserver's constructor.
|
|
83
83
|
|
|
84
|
-
This proposal would also include support for CSS, JSON, HTML module imports.
|
|
85
|
-
|
|
86
|
-
"match" is a css query, and could include multiple matches using the comma separator, i.e. no limitation on CSS expressions.
|
|
84
|
+
This proposal would also include support for CSS, JSON, HTML module imports.
|
|
87
85
|
|
|
88
86
|
The "observer" constant above is a class instance that inherits from EventTarget, which means it can be subscribed to by outside interests.
|
|
89
87
|
|
|
90
|
-
|
|
88
|
+
## Binding from a distance
|
|
89
|
+
|
|
90
|
+
It is important to note that "on" is a css query with no restrictions. So something like:
|
|
91
91
|
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
```JavaScript
|
|
93
|
+
const observer = new MountObserver({
|
|
94
|
+
on:'div > p + p ~ span[class$="name"]',
|
|
95
|
+
do:{
|
|
96
|
+
mount: (matchingElement) => {
|
|
97
|
+
//attach some behavior or set some property value or add an event listener, etc.
|
|
98
|
+
matchingElement.textContent = 'hello';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
})
|
|
97
102
|
```
|
|
98
103
|
|
|
99
|
-
|
|
104
|
+
... would work.
|
|
105
|
+
|
|
106
|
+
This would allow developers to create "stylesheet" like capabilities.
|
|
100
107
|
|
|
101
108
|
|
|
102
109
|
## Extra lazy loading
|
|
@@ -107,7 +114,7 @@ However, we could make the loading even more lazy by specifying intersection opt
|
|
|
107
114
|
|
|
108
115
|
```JavaScript
|
|
109
116
|
const observer = new MountObserver({
|
|
110
|
-
|
|
117
|
+
on: 'my-element',
|
|
111
118
|
whereElementIntersectsWith:{
|
|
112
119
|
rootMargin: "0px",
|
|
113
120
|
threshold: 1.0,
|
|
@@ -122,27 +129,31 @@ Unlike traditional CSS @import, CSS Modules don't support specifying different i
|
|
|
122
129
|
|
|
123
130
|
```JavaScript
|
|
124
131
|
const observer = new MountObserver({
|
|
125
|
-
|
|
132
|
+
on: 'div > p + p ~ span[class$="name"]',
|
|
126
133
|
whereMediaMatches: '(max-width: 1250px)',
|
|
127
134
|
whereSizeOfContainerMatches: '(min-width: 700px)',
|
|
128
135
|
whereInstanceOf: [HTMLMarqueeElement],
|
|
129
136
|
whereSatisfies: async (matchingElement, context) => true,
|
|
137
|
+
whereLangIn: ['en-GB'],
|
|
138
|
+
whereConnection:{
|
|
139
|
+
effectiveTypeIn: ["slow-2g"],
|
|
140
|
+
},
|
|
130
141
|
import: ['./my-element-small.css', {type: 'css'}],
|
|
131
142
|
do: {
|
|
132
|
-
|
|
143
|
+
mount: ({localName}, {module}) => {
|
|
133
144
|
...
|
|
134
145
|
},
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
146
|
+
dismount: ...,
|
|
147
|
+
disconnect: ...,
|
|
148
|
+
reconnect: ...,
|
|
149
|
+
reconfirm: ...,
|
|
150
|
+
exit: ...,
|
|
151
|
+
forget: ...,
|
|
141
152
|
}
|
|
142
153
|
})
|
|
143
154
|
```
|
|
144
155
|
|
|
145
|
-
Callbacks like we see above are useful for tight coupling, and probably are unmatched in terms of performance. However, since these rules may be of interest to multiple parties, it is
|
|
156
|
+
Callbacks like we see above are useful for tight coupling, and probably are unmatched in terms of performance. However, since these rules may be of interest to multiple parties, it is useful to also provide the ability for multiple parties to subscribe to these css rules. This can be done via:
|
|
146
157
|
|
|
147
158
|
## Subscribing
|
|
148
159
|
|
|
@@ -155,21 +166,22 @@ observer.addEventListener('mount', e => {
|
|
|
155
166
|
module: e.module
|
|
156
167
|
});
|
|
157
168
|
});
|
|
158
|
-
|
|
159
169
|
observer.addEventListener('dismount', e => {
|
|
160
170
|
...
|
|
161
171
|
});
|
|
162
|
-
|
|
163
|
-
observer.addEventListener('reconnect', e => {
|
|
172
|
+
observer.addEventListener('disconnect', e => {
|
|
164
173
|
...
|
|
165
174
|
});
|
|
166
|
-
observer.addEventListener('
|
|
175
|
+
observer.addEventListener('reconnect', e => {
|
|
167
176
|
...
|
|
168
177
|
});
|
|
169
178
|
observer.addEventListener('reconfirm', e => {
|
|
170
179
|
...
|
|
171
180
|
});
|
|
172
|
-
observer.addEventListener('
|
|
181
|
+
observer.addEventListener('exit', e => {
|
|
182
|
+
...
|
|
183
|
+
});
|
|
184
|
+
observer.addEventListener('forget', e => {
|
|
173
185
|
...
|
|
174
186
|
});
|
|
175
187
|
```
|
|
@@ -184,49 +196,220 @@ If an element that is in "mounted" state according to a MountObserver instance i
|
|
|
184
196
|
|
|
185
197
|
1) "disconnect" event is dispatched from the MountObserver instance the moment the mounted element is disconnected from the DOM fragment.
|
|
186
198
|
2) If/when the element is added somewhere else in the DOM tree, the mountObserver instance will dispatch event "reconnect", regardless of where. [Note: can't polyfill this very easily]
|
|
187
|
-
3) If the mounted element is added outside the rootNode being observed, the mountObserver instance will dispatch event "
|
|
188
|
-
4)
|
|
189
|
-
5) If the
|
|
199
|
+
3) If the mounted element is added outside the rootNode being observed, the mountObserver instance will dispatch event "exit", and the MountObserver instance will relinquish any further responsibility for this element.
|
|
200
|
+
4) Ideally event "forget" would be dispatched just before the platform garbage collects an element the MountObserver instance is still monitoring, after all hard references are relinquished (or is that self-contradictory?).
|
|
201
|
+
5) If the new place it was added remains within the original rootNode and remains mounted, the MountObserver instance dispatches event "reconfirmed".
|
|
202
|
+
6) If the element no longer satisfies the criteria of the MountObserver instance, the MountObserver instance will dispatch event "dismount".
|
|
190
203
|
|
|
191
|
-
##
|
|
204
|
+
## A tribute to attributes
|
|
192
205
|
|
|
193
|
-
Extra support is provided for monitoring attributes.
|
|
206
|
+
Extra support is provided for monitoring attributes. There are two primary reasons for needing to provide special support for attributes with this API:
|
|
207
|
+
|
|
208
|
+
Being that for both custom elements, as well as (hopefully) [custom enhancements](https://github.com/WICG/webcomponents/issues/1000) we need to carefully work with sets of "owned" [observed](https://github.com/WICG/webcomponents/issues/1045) attributes, and in some cases we may need to manage combinations of prefixes and suffixes for better name-spacing management, creating the most effective css query becomes challenging.
|
|
209
|
+
|
|
210
|
+
We want to be alerted by the discovery of elements adorned by these attributes, but then continue to be alerted to changes of their values, and we can't enumerate which values we are interested in, so we must subscribe to all values as they change.
|
|
211
|
+
|
|
212
|
+
### Scenario 1 -- Custom Element integration with ObserveObservedAttributes API [WIP]
|
|
194
213
|
|
|
195
214
|
Example:
|
|
196
215
|
|
|
197
216
|
```html
|
|
198
217
|
<div id=div>
|
|
199
|
-
<
|
|
218
|
+
<my-custom-element my-first-observed-attribute="hello"></my-custom-element>
|
|
200
219
|
</div>
|
|
201
220
|
<script type=module>
|
|
202
221
|
import {MountObserver} from '../MountObserver.js';
|
|
203
222
|
const mo = new MountObserver({
|
|
204
|
-
|
|
205
|
-
|
|
223
|
+
on: '*',
|
|
224
|
+
whereInstanceOf: [MyCustomElement]
|
|
225
|
+
});
|
|
226
|
+
mo.addEventListener('parsed-attrs-changed', e => {
|
|
227
|
+
const {matchingElement, modifiedObjectFieldValues, preModifiedFieldValues} = e;
|
|
228
|
+
console.log({matchingElement, modifiedObjectFieldValues, preModifiedFieldValues});
|
|
229
|
+
});
|
|
230
|
+
mo.observe(div);
|
|
231
|
+
setTimeout(() => {
|
|
232
|
+
const myCustomElement = document.querySelector('my-custom-element');
|
|
233
|
+
myCustomElement.setAttribute('my-first-observed-attribute', 'good-bye');
|
|
234
|
+
}, 1000);
|
|
235
|
+
</script>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
### Scenario 2 -- Custom Enhancements in userland
|
|
240
|
+
|
|
241
|
+
Based on [the proposal as it currently stands](https://github.com/WICG/webcomponents/issues/1000), in this case the class prototype would *not* have the attributes defined as a static property of the class, so that the constructor arguments in the previous scenario wouldn't be sufficient. So instead, what would seem to provide the most help for providing for custom enhancements in userland, and for any other kind of progressive enhancement based on attributes going forward.
|
|
242
|
+
|
|
243
|
+
Suppose we have a progressive enhancement that we want to apply based on the presence of 1 or more attributes.
|
|
244
|
+
|
|
245
|
+
To make this discussion concrete, let's suppose the "canonical" names of those attributes are:
|
|
246
|
+
|
|
247
|
+
```html
|
|
248
|
+
<div id=div>
|
|
249
|
+
<section
|
|
250
|
+
my-enhancement=greetings
|
|
251
|
+
my-enhancement-first-aspect=hello
|
|
252
|
+
my-enhancement-second-aspect=goodbye
|
|
253
|
+
my-enhancement-first-aspect-wow-this-is-deep
|
|
254
|
+
my-enhancement-first-aspect-have-you-considered-using-json-for-this=just-saying
|
|
255
|
+
></section>
|
|
256
|
+
</div>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Now suppose we are worried about namespace clashes, plus we want to serve environments where HTML5 compliance is a must.
|
|
260
|
+
|
|
261
|
+
So we also want to recognize additional attributes that should map to these canonical attributes:
|
|
262
|
+
|
|
263
|
+
We want to also support:
|
|
264
|
+
|
|
265
|
+
```html
|
|
266
|
+
<div id=div>
|
|
267
|
+
<section class=hello
|
|
268
|
+
data-my-enhancement=greetings
|
|
269
|
+
data-my-enhancement-first-aspect=hello
|
|
270
|
+
data-my-enhancement-second-aspect=goodbye
|
|
271
|
+
data-my-enhancement-first-aspect-wow-this-is-deep
|
|
272
|
+
data-my-enhancement-first-aspect-have-you-considered-using-json-for-this=just-saying
|
|
273
|
+
></section>
|
|
274
|
+
</div>
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Based on the current unspoken rules, no one will raise an eyebrow with these attributes, because the platform has indicated it will generally avoid dashes in attributes (with an exception or two that will only happen in a blue moon, like aria-*).
|
|
278
|
+
|
|
279
|
+
But now when we consider applying this enhancement to custom elements, we have a new risk. What's to prevent the custom element from having an attribute named my-enhancement?
|
|
280
|
+
|
|
281
|
+
So let's say we want to insist that on custom elements, we must have the data- prefix?
|
|
282
|
+
|
|
283
|
+
And we want to support an alternative, more semantic sounding prefix to data, say enh-*, endorsed by [this proposal](https://github.com/WICG/webcomponents/issues/1000).
|
|
284
|
+
|
|
285
|
+
Here's what the api provides:
|
|
286
|
+
|
|
287
|
+
## Option 1 -- The carpal syndrome syntax
|
|
288
|
+
|
|
289
|
+
```JavaScript
|
|
290
|
+
import {MountObserver} from '../MountObserver.js';
|
|
291
|
+
const mo = new MountObserver({
|
|
292
|
+
on: '*',
|
|
293
|
+
whereAttr:{
|
|
294
|
+
isIn: [
|
|
295
|
+
'data-my-enhancement',
|
|
296
|
+
'data-my-enhancement-first-aspect',
|
|
297
|
+
'data-my-enhancement-second-aspect',
|
|
298
|
+
'enh-my-enhancement',
|
|
299
|
+
'enh-my-enhancement-first-aspect',
|
|
300
|
+
'enh-my-enhancement-second-aspect',
|
|
301
|
+
//...some ten more combinations not listed
|
|
206
302
|
{
|
|
207
|
-
|
|
208
|
-
|
|
303
|
+
name: 'my-enhancement',
|
|
304
|
+
builtIn: true
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: 'my-enhancement-first-attr',
|
|
308
|
+
builtIn: true
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: 'my-enhancement-second-aspect',
|
|
312
|
+
builtIn: true
|
|
313
|
+
},
|
|
314
|
+
...
|
|
209
315
|
]
|
|
316
|
+
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## Option 2 -- The DRY Way
|
|
322
|
+
|
|
323
|
+
```JavaScript
|
|
324
|
+
import {MountObserver} from '../MountObserver.js';
|
|
325
|
+
const mo = new MountObserver({
|
|
326
|
+
on: '*',
|
|
327
|
+
whereAttr:{
|
|
328
|
+
hasRootIn: ['data', 'enh', 'data-enh'],
|
|
329
|
+
hasBase: 'my-enhancement',
|
|
330
|
+
hasBranchIn: ['first-aspect', 'second-aspect', ''],
|
|
331
|
+
hasLeafIn: {
|
|
332
|
+
'first-aspect': ['wow-this-is-deep', 'have-you-considered-using-json-for-this'],
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
MountObserver provides a breakdown of the matching attribute when encountered:
|
|
339
|
+
|
|
340
|
+
```html
|
|
341
|
+
<div id=div>
|
|
342
|
+
<section class=hello my-enhancement-first-attr-wow-this-is-deep="hello"></section>
|
|
343
|
+
</div>
|
|
344
|
+
<script type=module>
|
|
345
|
+
import {MountObserver} from '../MountObserver.js';
|
|
346
|
+
const mo = new MountObserver({
|
|
347
|
+
on: '*',
|
|
348
|
+
whereAttr:{
|
|
349
|
+
hasRootIn: ['data', 'enh', 'data-enh'],
|
|
350
|
+
hasBase: 'my-enhancement',
|
|
351
|
+
hasBranchIn: ['first-attr', 'second-attr', ''],
|
|
352
|
+
hasLeafIn: {
|
|
353
|
+
'first-attr': ['wow-this-is-deep', 'have-you-considered-using-json-for-this'],
|
|
354
|
+
}
|
|
355
|
+
}
|
|
210
356
|
});
|
|
211
|
-
mo.addEventListener('attr-change', e => {
|
|
357
|
+
mo.addEventListener('observed-attr-change', e => {
|
|
212
358
|
console.log(e);
|
|
213
359
|
// {
|
|
360
|
+
// matchingElement,
|
|
214
361
|
// attrChangeInfo:{
|
|
215
|
-
// name: '
|
|
362
|
+
// name: 'data-my-enhancement-first-aspect-wow-this-is-deep'
|
|
363
|
+
// root: 'data',
|
|
364
|
+
// base: 'my-enhancement',
|
|
365
|
+
// branch: 'first-attr',
|
|
366
|
+
// leaf: 'wow-this-is-deep',
|
|
216
367
|
// oldValue: null,
|
|
217
|
-
// newValue: '
|
|
368
|
+
// newValue: 'good-bye'
|
|
218
369
|
// idx: 0,
|
|
219
370
|
// }
|
|
220
371
|
// }
|
|
221
372
|
});
|
|
222
373
|
mo.observe(div);
|
|
223
374
|
setTimeout(() => {
|
|
224
|
-
|
|
375
|
+
const myCustomElement = document.querySelector('my-custom-element');
|
|
376
|
+
myCustomElement.setAttribute('data-my-enhancement-first-aspect-wow-this-is-deep', 'good-bye');
|
|
225
377
|
}, 1000);
|
|
226
378
|
</script>
|
|
227
379
|
```
|
|
228
380
|
|
|
381
|
+
Some libraries prefer to use the colon (:) rather than a dash to separate these levels of settings:
|
|
382
|
+
|
|
383
|
+
Possibly some libraries may prefer to mix it up a bit:
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
```html
|
|
387
|
+
<div id=div>
|
|
388
|
+
<section class=hello
|
|
389
|
+
data-my-enhancement=greetings
|
|
390
|
+
data-my-enhancement:first-aspect=hello
|
|
391
|
+
data-my-enhancement:second-aspect=goodbye
|
|
392
|
+
data-my-enhancement:first-aspect--wow-this-is-deep
|
|
393
|
+
data-my-enhancement:first-aspect--have-you-considered-using-json-for-this=just-saying
|
|
394
|
+
></section>
|
|
395
|
+
</div>
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
To support, specify the delimiter thusly:
|
|
229
399
|
|
|
400
|
+
```JavaScript
|
|
401
|
+
const mo = new MountObserver({
|
|
402
|
+
on: '*',
|
|
403
|
+
whereAttr:{
|
|
404
|
+
hasRootIn: ['data', 'enh', 'data-enh'],
|
|
405
|
+
hasBase: ['-', 'my-enhancement'],
|
|
406
|
+
hasBranchIn: [':', ['first-attr', 'second-attr', '']],
|
|
407
|
+
hasLeafIn: {
|
|
408
|
+
'first-attr': ['--', ['wow-this-is-deep', 'have-you-considered-using-json-for-this']],
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
```
|
|
230
413
|
|
|
231
414
|
## Preemptive downloading
|
|
232
415
|
|
|
@@ -243,15 +426,14 @@ So for this we add option:
|
|
|
243
426
|
|
|
244
427
|
```JavaScript
|
|
245
428
|
const observer = new MountObserver({
|
|
246
|
-
|
|
429
|
+
on: 'my-element',
|
|
247
430
|
loading: 'eager',
|
|
248
431
|
import: './my-element.js',
|
|
249
432
|
do:{
|
|
250
|
-
|
|
433
|
+
mount: (matchingElement, {module}) => customElements.define(module.MyElement)
|
|
251
434
|
}
|
|
252
435
|
})
|
|
253
436
|
```
|
|
254
437
|
|
|
255
438
|
So what this does is only check for the presence of an element with tag name "my-element", and it starts downloading the resource, even before the element has "mounted" based on other criteria.
|
|
256
439
|
|
|
257
|
-
|
package/doWhereAttr.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { AttrChangeEvent } from "./MountObserver.js";
|
|
2
|
+
export function doWhereAttr(whereAttr, attributeName, target, oldValue, mo) {
|
|
3
|
+
const { hasRootIn, hasBase, hasBranchIn } = whereAttr;
|
|
4
|
+
const name = attributeName;
|
|
5
|
+
let restOfName = name;
|
|
6
|
+
let root;
|
|
7
|
+
let branch;
|
|
8
|
+
let idx = 0;
|
|
9
|
+
const hasBaseIsString = typeof hasBase === 'string';
|
|
10
|
+
const baseSelector = hasBaseIsString ? hasBase : hasBase[1];
|
|
11
|
+
const rootToBaseDelimiter = hasBaseIsString ? '-' : hasBase[0];
|
|
12
|
+
if (hasRootIn !== undefined) {
|
|
13
|
+
for (const rootTest in hasRootIn) {
|
|
14
|
+
if (restOfName.startsWith(rootTest)) {
|
|
15
|
+
root = rootTest;
|
|
16
|
+
restOfName = restOfName.substring(root.length + rootToBaseDelimiter.length);
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (!restOfName.startsWith(baseSelector))
|
|
22
|
+
return;
|
|
23
|
+
restOfName = restOfName.substring(hasBase.length);
|
|
24
|
+
if (hasBranchIn) {
|
|
25
|
+
for (const branchTest in hasBranchIn) {
|
|
26
|
+
if (restOfName.startsWith(branchTest)) {
|
|
27
|
+
branch = branchTest;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const newValue = target.getAttribute(attributeName);
|
|
33
|
+
const attrChangeInfo = {
|
|
34
|
+
name,
|
|
35
|
+
root,
|
|
36
|
+
base: baseSelector,
|
|
37
|
+
branch,
|
|
38
|
+
oldValue,
|
|
39
|
+
newValue,
|
|
40
|
+
idx
|
|
41
|
+
};
|
|
42
|
+
mo.dispatchEvent(new AttrChangeEvent(target, attrChangeInfo));
|
|
43
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function getWhereAttrSelector(whereAttr, withoutAttrs) {
|
|
2
|
+
const { hasBase, hasBranchIn, hasRootIn } = whereAttr;
|
|
3
|
+
const fullListOfAttrs = [];
|
|
4
|
+
//TODO: share this block with doWhereAttr?
|
|
5
|
+
const hasBaseIsString = typeof hasBase === 'string';
|
|
6
|
+
const baseSelector = hasBaseIsString ? hasBase : hasBase[1];
|
|
7
|
+
const rootToBaseDelimiter = hasBaseIsString ? '-' : hasBase[0];
|
|
8
|
+
//end TODO
|
|
9
|
+
let prefixLessMatches = [baseSelector];
|
|
10
|
+
if (hasBranchIn !== undefined) {
|
|
11
|
+
let baseToBranchDelimiter = '-';
|
|
12
|
+
let branches;
|
|
13
|
+
if (hasBranchIn.length === 2 && Array.isArray(hasBranchIn[1])) {
|
|
14
|
+
baseToBranchDelimiter = hasBranchIn[0];
|
|
15
|
+
branches = hasBranchIn[1];
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
branches = hasBranchIn;
|
|
19
|
+
}
|
|
20
|
+
prefixLessMatches = branches.map(x => `${baseSelector}${baseToBranchDelimiter}x`);
|
|
21
|
+
}
|
|
22
|
+
const stems = hasRootIn || [''];
|
|
23
|
+
for (const stem of stems) {
|
|
24
|
+
const prefix = typeof stem === 'string' ? stem : stem.path;
|
|
25
|
+
for (const prefixLessMatch of prefixLessMatches) {
|
|
26
|
+
fullListOfAttrs.push(prefix.length === 0 ? prefixLessMatch : `${prefix}${rootToBaseDelimiter}${prefixLessMatch}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const listOfSelectors = fullListOfAttrs.map(s => `${withoutAttrs}[${s}]`);
|
|
30
|
+
const calculatedSelector = listOfSelectors.join(',');
|
|
31
|
+
return {
|
|
32
|
+
fullListOfAttrs,
|
|
33
|
+
calculatedSelector
|
|
34
|
+
};
|
|
35
|
+
}
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
export interface MountInit{
|
|
2
|
-
readonly
|
|
3
|
-
readonly attribMatches?: Array<AttribMatch>,
|
|
2
|
+
readonly on?: CSSMatch,
|
|
3
|
+
//readonly attribMatches?: Array<AttribMatch>,
|
|
4
|
+
readonly whereAttr?: WhereAttr,
|
|
4
5
|
readonly whereElementIntersectsWith?: IntersectionObserverInit,
|
|
5
6
|
readonly whereMediaMatches?: MediaQuery,
|
|
6
7
|
readonly whereInstanceOf?: Array<typeof Node>, //[TODO] What's the best way to type this?,
|
|
7
8
|
readonly whereSatisfies?: PipelineProcessor<boolean>,
|
|
8
9
|
readonly import?: ImportString | [ImportString, ImportAssertions] | PipelineProcessor,
|
|
9
10
|
readonly do?: {
|
|
10
|
-
readonly
|
|
11
|
-
readonly
|
|
12
|
-
readonly
|
|
13
|
-
readonly
|
|
14
|
-
readonly
|
|
11
|
+
readonly mount?: PipelineProcessor,
|
|
12
|
+
readonly dismount?: PipelineProcessor,
|
|
13
|
+
readonly disconnect?: PipelineProcessor,
|
|
14
|
+
readonly reconfirm?: PipelineProcessor,
|
|
15
|
+
readonly exit?: PipelineProcessor,
|
|
15
16
|
}
|
|
16
17
|
// /**
|
|
17
18
|
// * Purpose -- there are scenarios where we may only want to affect changes that occur after the initial
|
|
@@ -19,6 +20,14 @@ export interface MountInit{
|
|
|
19
20
|
// */
|
|
20
21
|
// readonly ignoreInitialMatches?: boolean,
|
|
21
22
|
}
|
|
23
|
+
export interface WhereAttr{
|
|
24
|
+
hasBase: string | [string, string],
|
|
25
|
+
hasBranchIn?: Array<string> | [string, Array<string>],
|
|
26
|
+
hasRootIn?: Array<string | {
|
|
27
|
+
path: string,
|
|
28
|
+
context: 'BuiltIn' | 'CustomElement' | 'Both'
|
|
29
|
+
}>,
|
|
30
|
+
}
|
|
22
31
|
type CSSMatch = string;
|
|
23
32
|
type ImportString = string;
|
|
24
33
|
type MediaQuery = string;
|
|
@@ -62,6 +71,10 @@ export interface AddMutationEventListener {
|
|
|
62
71
|
|
|
63
72
|
interface AttrChangeInfo{
|
|
64
73
|
name: string,
|
|
74
|
+
root?: string,
|
|
75
|
+
base?: string,
|
|
76
|
+
branch?: string,
|
|
77
|
+
leaf?: string, //TODO
|
|
65
78
|
oldValue: string | null,
|
|
66
79
|
newValue: string | null,
|
|
67
80
|
idx: number,
|