mount-observer 0.0.4 → 0.0.6
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 +64 -19
- package/README.md +18 -6
- package/RootMutObs.js +3 -0
- package/package.json +1 -1
- package/types.d.ts +19 -10
package/MountObserver.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RootMutObs } from './RootMutObs.js';
|
|
2
2
|
const mutationObserverLookup = new WeakMap();
|
|
3
|
+
const refCount = new WeakMap();
|
|
3
4
|
export class MountObserver extends EventTarget {
|
|
4
5
|
#mountInit;
|
|
5
6
|
#rootMutObs;
|
|
@@ -44,17 +45,52 @@ export class MountObserver extends EventTarget {
|
|
|
44
45
|
this.#calculatedSelector = matches.join(',');
|
|
45
46
|
return this.#calculatedSelector;
|
|
46
47
|
}
|
|
48
|
+
unobserve(within) {
|
|
49
|
+
const nodeToMonitor = this.#isComplex ? (within instanceof ShadowRoot ? within : within.getRootNode()) : within;
|
|
50
|
+
const currentCount = refCount.get(nodeToMonitor);
|
|
51
|
+
if (currentCount !== undefined) {
|
|
52
|
+
if (currentCount <= 1) {
|
|
53
|
+
const observer = mutationObserverLookup.get(nodeToMonitor);
|
|
54
|
+
if (observer === undefined) {
|
|
55
|
+
console.warn(refCountErr);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
observer.disconnect();
|
|
59
|
+
mutationObserverLookup.delete(nodeToMonitor);
|
|
60
|
+
refCount.delete(nodeToMonitor);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
refCount.set(nodeToMonitor, currentCount + 1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
if (mutationObserverLookup.has(nodeToMonitor)) {
|
|
69
|
+
console.warn(refCountErr);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
47
73
|
async observe(within) {
|
|
48
74
|
const nodeToMonitor = this.#isComplex ? (within instanceof ShadowRoot ? within : within.getRootNode()) : within;
|
|
49
75
|
if (!mutationObserverLookup.has(nodeToMonitor)) {
|
|
50
76
|
mutationObserverLookup.set(nodeToMonitor, new RootMutObs(nodeToMonitor));
|
|
77
|
+
refCount.set(nodeToMonitor, 1);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const currentCount = refCount.get(nodeToMonitor);
|
|
81
|
+
if (currentCount === undefined) {
|
|
82
|
+
console.warn(refCountErr);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
refCount.set(nodeToMonitor, currentCount + 1);
|
|
86
|
+
}
|
|
51
87
|
}
|
|
52
88
|
const rootMutObs = mutationObserverLookup.get(within);
|
|
53
89
|
const { attribMatches } = this.#mountInit;
|
|
54
90
|
rootMutObs.addEventListener('mutation-event', (e) => {
|
|
55
91
|
//TODO: disconnected
|
|
56
92
|
if (this.#isComplex) {
|
|
57
|
-
this.#inspectWithin(within);
|
|
93
|
+
this.#inspectWithin(within, false);
|
|
58
94
|
return;
|
|
59
95
|
}
|
|
60
96
|
const { mutationRecords } = e;
|
|
@@ -109,9 +145,11 @@ export class MountObserver extends EventTarget {
|
|
|
109
145
|
this.dispatchEvent(new DisconnectEvent(deletedElement));
|
|
110
146
|
}
|
|
111
147
|
}
|
|
112
|
-
this.#filterAndMount(elsToInspect, true);
|
|
148
|
+
this.#filterAndMount(elsToInspect, true, false);
|
|
113
149
|
}, { signal: this.#abortController.signal });
|
|
114
|
-
|
|
150
|
+
//if(ignoreInitialMatches !== true){
|
|
151
|
+
await this.#inspectWithin(within, true);
|
|
152
|
+
//}
|
|
115
153
|
}
|
|
116
154
|
#confirmInstanceOf(el, whereInstanceOf) {
|
|
117
155
|
for (const test of whereInstanceOf) {
|
|
@@ -120,7 +158,7 @@ export class MountObserver extends EventTarget {
|
|
|
120
158
|
}
|
|
121
159
|
return false;
|
|
122
160
|
}
|
|
123
|
-
async #mount(matching) {
|
|
161
|
+
async #mount(matching, initializing) {
|
|
124
162
|
//first unmount non matching
|
|
125
163
|
const alreadyMounted = this.#filterAndDismount();
|
|
126
164
|
const onMount = this.#mountInit.do?.onMount;
|
|
@@ -141,13 +179,20 @@ export class MountObserver extends EventTarget {
|
|
|
141
179
|
}
|
|
142
180
|
break;
|
|
143
181
|
case 'function':
|
|
144
|
-
this.module = await imp(match, this,
|
|
182
|
+
this.module = await imp(match, this, {
|
|
183
|
+
stage: 'Import',
|
|
184
|
+
initializing
|
|
185
|
+
});
|
|
145
186
|
break;
|
|
146
187
|
}
|
|
147
188
|
}
|
|
148
|
-
if (onMount !== undefined)
|
|
149
|
-
onMount(match, this,
|
|
150
|
-
|
|
189
|
+
if (onMount !== undefined) {
|
|
190
|
+
onMount(match, this, {
|
|
191
|
+
stage: 'PostImport',
|
|
192
|
+
initializing
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
this.dispatchEvent(new MountEvent(match, initializing));
|
|
151
196
|
if (attribMatches !== undefined) {
|
|
152
197
|
let idx = 0;
|
|
153
198
|
for (const attribMatch of attribMatches) {
|
|
@@ -178,7 +223,7 @@ export class MountObserver extends EventTarget {
|
|
|
178
223
|
const onDismount = this.#mountInit.do?.onDismount;
|
|
179
224
|
for (const unmatch of unmatching) {
|
|
180
225
|
if (onDismount !== undefined) {
|
|
181
|
-
onDismount(unmatch, this);
|
|
226
|
+
onDismount(unmatch, this, {});
|
|
182
227
|
}
|
|
183
228
|
this.dispatchEvent(new DismountEvent(unmatch));
|
|
184
229
|
}
|
|
@@ -195,7 +240,7 @@ export class MountObserver extends EventTarget {
|
|
|
195
240
|
if (!x.matches(match))
|
|
196
241
|
return true;
|
|
197
242
|
if (whereSatisfies !== undefined) {
|
|
198
|
-
if (!whereSatisfies(x, this, 'Inspecting'))
|
|
243
|
+
if (!whereSatisfies(x, this, { stage: 'Inspecting', initializing: false }))
|
|
199
244
|
return true;
|
|
200
245
|
}
|
|
201
246
|
returnSet.add(x);
|
|
@@ -206,7 +251,7 @@ export class MountObserver extends EventTarget {
|
|
|
206
251
|
this.#mountedList = Array.from(returnSet).map(x => new WeakRef(x));
|
|
207
252
|
return returnSet;
|
|
208
253
|
}
|
|
209
|
-
async #filterAndMount(els, checkMatch) {
|
|
254
|
+
async #filterAndMount(els, checkMatch, initializing) {
|
|
210
255
|
const { whereSatisfies, whereInstanceOf } = this.#mountInit;
|
|
211
256
|
const match = this.#selector;
|
|
212
257
|
const elsToMount = els.filter(x => {
|
|
@@ -215,7 +260,7 @@ export class MountObserver extends EventTarget {
|
|
|
215
260
|
return false;
|
|
216
261
|
}
|
|
217
262
|
if (whereSatisfies !== undefined) {
|
|
218
|
-
if (!whereSatisfies(x, this, 'Inspecting'))
|
|
263
|
+
if (!whereSatisfies(x, this, { stage: 'Inspecting', initializing }))
|
|
219
264
|
return false;
|
|
220
265
|
}
|
|
221
266
|
if (whereInstanceOf !== undefined) {
|
|
@@ -224,16 +269,14 @@ export class MountObserver extends EventTarget {
|
|
|
224
269
|
}
|
|
225
270
|
return true;
|
|
226
271
|
});
|
|
227
|
-
this.#mount(elsToMount);
|
|
272
|
+
this.#mount(elsToMount, initializing);
|
|
228
273
|
}
|
|
229
|
-
async #inspectWithin(within) {
|
|
274
|
+
async #inspectWithin(within, initializing) {
|
|
230
275
|
const els = Array.from(within.querySelectorAll(this.#selector));
|
|
231
|
-
this.#filterAndMount(els, false);
|
|
232
|
-
}
|
|
233
|
-
unobserve() {
|
|
234
|
-
throw 'NI';
|
|
276
|
+
this.#filterAndMount(els, false, initializing);
|
|
235
277
|
}
|
|
236
278
|
}
|
|
279
|
+
const refCountErr = 'mount-observer ref count mismatch';
|
|
237
280
|
// https://github.com/webcomponents-cg/community-protocols/issues/12#issuecomment-872415080
|
|
238
281
|
/**
|
|
239
282
|
* The `mutation-event` event represents something that happened.
|
|
@@ -241,10 +284,12 @@ export class MountObserver extends EventTarget {
|
|
|
241
284
|
*/
|
|
242
285
|
export class MountEvent extends Event {
|
|
243
286
|
mountedElement;
|
|
287
|
+
initializing;
|
|
244
288
|
static eventName = 'mount';
|
|
245
|
-
constructor(mountedElement) {
|
|
289
|
+
constructor(mountedElement, initializing) {
|
|
246
290
|
super(MountEvent.eventName);
|
|
247
291
|
this.mountedElement = mountedElement;
|
|
292
|
+
this.initializing = initializing;
|
|
248
293
|
}
|
|
249
294
|
}
|
|
250
295
|
export class DismountEvent extends Event {
|
package/README.md
CHANGED
|
@@ -3,13 +3,14 @@
|
|
|
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
|
+
|
|
6
7
|
# The MountObserver api.
|
|
7
8
|
|
|
8
9
|
Author: Bruce B. Anderson
|
|
9
10
|
|
|
10
11
|
Issues / pr's / polyfill: [mount-observer](https://github.com/bahrus/mount-observer)
|
|
11
12
|
|
|
12
|
-
Last Update: 2023-11-
|
|
13
|
+
Last Update: 2023-11-23
|
|
13
14
|
|
|
14
15
|
## Benefits of this API
|
|
15
16
|
|
|
@@ -133,13 +134,16 @@ const observer = new MountObserver({
|
|
|
133
134
|
},
|
|
134
135
|
onDismount: ...,
|
|
135
136
|
onDisconnect: ...,
|
|
137
|
+
onOutsideRootNode: ...,
|
|
136
138
|
onReconnect: ...,
|
|
137
|
-
onReconfirm:
|
|
139
|
+
onReconfirm: ...,
|
|
138
140
|
onOutOfScope: ...,
|
|
139
141
|
}
|
|
140
142
|
})
|
|
141
143
|
```
|
|
142
144
|
|
|
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 usesful to also provide the ability for multiple parties to subscribe to these css rules. This can be done via:
|
|
146
|
+
|
|
143
147
|
## Subscribing
|
|
144
148
|
|
|
145
149
|
Subscribing can be done via:
|
|
@@ -178,11 +182,11 @@ The moment a MountObserver instance's "observe" method is called (passing in a r
|
|
|
178
182
|
|
|
179
183
|
If an element that is in "mounted" state according to a MountObserver instance is moved from one parent DOM element to another:
|
|
180
184
|
|
|
181
|
-
1) "disconnect" event is dispatched from the MountObserver instance the moment the element is disconnected from the DOM fragment.
|
|
185
|
+
1) "disconnect" event is dispatched from the MountObserver instance the moment the mounted element is disconnected from the DOM fragment.
|
|
182
186
|
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]
|
|
183
|
-
3) If the element is added outside the rootNode being observed, the mountObserver instance will dispatch event "outside-root-node", and the MountObserver instance will relinquish any further responsibility for this element. Ideally this would also be dispatched when the platform garbage collects the element as well after all hard references are relinquished.
|
|
184
|
-
4) If the new place it was added remains within the original rootNode and remains
|
|
185
|
-
5) If the element no longer satisfies the criteria of the MountObserver instance, the MountObserver instance will dispatch event "dismount".
|
|
187
|
+
3) If the mounted element is added outside the rootNode being observed, the mountObserver instance will dispatch event "outside-root-node", and the MountObserver instance will relinquish any further responsibility for this element. Ideally this would also be dispatched when the platform garbage collects the element as well after all hard references are relinquished.
|
|
188
|
+
4) If the new place it was added remains within the original rootNode and remains mounted, the MountObserver instance dispatches event "reconfirmed".
|
|
189
|
+
5) If the element no longer satisfies the criteria of the MountObserver instance, the MountObserver instance will dispatch event "dismount".
|
|
186
190
|
|
|
187
191
|
## Special support for observable attributes
|
|
188
192
|
|
|
@@ -206,6 +210,14 @@ Example:
|
|
|
206
210
|
});
|
|
207
211
|
mo.addEventListener('attr-change', e => {
|
|
208
212
|
console.log(e);
|
|
213
|
+
// {
|
|
214
|
+
// attrChangeInfo:{
|
|
215
|
+
// name: 'test-1',
|
|
216
|
+
// oldValue: null,
|
|
217
|
+
// newValue: 'hello'
|
|
218
|
+
// idx: 0,
|
|
219
|
+
// }
|
|
220
|
+
// }
|
|
209
221
|
});
|
|
210
222
|
mo.observe(div);
|
|
211
223
|
setTimeout(() => {
|
package/RootMutObs.js
CHANGED
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -7,13 +7,17 @@ export interface MountInit{
|
|
|
7
7
|
readonly whereSatisfies?: PipelineProcessor<boolean>,
|
|
8
8
|
readonly import?: ImportString | [ImportString, ImportAssertions] | PipelineProcessor,
|
|
9
9
|
readonly do?: {
|
|
10
|
-
readonly onMount
|
|
11
|
-
readonly onDismount
|
|
12
|
-
readonly onDisconnect
|
|
13
|
-
readonly onReconfirmed
|
|
14
|
-
readonly onOutsideRootNode
|
|
10
|
+
readonly onMount?: PipelineProcessor,
|
|
11
|
+
readonly onDismount?: PipelineProcessor,
|
|
12
|
+
readonly onDisconnect?: PipelineProcessor,
|
|
13
|
+
readonly onReconfirmed?: PipelineProcessor,
|
|
14
|
+
readonly onOutsideRootNode?: PipelineProcessor,
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
// /**
|
|
17
|
+
// * Purpose -- there are scenarios where we may only want to affect changes that occur after the initial
|
|
18
|
+
// * server rendering, so we only want to mount elements that appear
|
|
19
|
+
// */
|
|
20
|
+
// readonly ignoreInitialMatches?: boolean,
|
|
17
21
|
}
|
|
18
22
|
type CSSMatch = string;
|
|
19
23
|
type ImportString = string;
|
|
@@ -27,17 +31,22 @@ export interface AttribMatch{
|
|
|
27
31
|
// validator?: (v: any) => boolean;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
export interface
|
|
34
|
+
export interface IMountObserver {
|
|
31
35
|
// readonly mountInit: MountInit,
|
|
32
36
|
// readonly mountedRefs: WeakRef<Element>[],
|
|
33
37
|
// readonly dismountedRefs: WeakRef<Element>[],
|
|
34
38
|
observe(within: Node): void;
|
|
35
|
-
unobserve(): void;
|
|
39
|
+
unobserve(within: Node): void;
|
|
36
40
|
module?: any;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
export interface MountContext{
|
|
44
|
+
stage?: PipelineStage,
|
|
45
|
+
initializing?: boolean,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type PipelineStage = 'Inspecting' | 'PreImport' | 'PostImport' | 'Import'
|
|
49
|
+
export type PipelineProcessor<ReturnType = void> = (matchingElement: Element, observer: IMountObserver, ctx: MountContext) => Promise<ReturnType>;
|
|
41
50
|
|
|
42
51
|
//#region mutation event
|
|
43
52
|
export type mutationEventName = 'mutation-event';
|