mount-observer 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Events.js +41 -7
- package/Events.ts +31 -13
- package/MountObserver.js +97 -41
- package/MountObserver.ts +126 -41
- package/SharedMutationObserver.js +70 -0
- package/SharedMutationObserver.ts +96 -0
- package/index.ts +5 -2
- package/mediaQuery.js +86 -0
- package/mediaQuery.ts +113 -0
- package/package.json +7 -4
- package/types.d.ts +18 -0
- package/constants.js +0 -6
- package/constants.ts +0 -7
package/Events.js
CHANGED
|
@@ -1,44 +1,78 @@
|
|
|
1
|
-
// Event
|
|
2
|
-
|
|
1
|
+
// Event name constants
|
|
2
|
+
export const loadEventName = 'load';
|
|
3
|
+
export const mountEventName = 'mount';
|
|
4
|
+
export const dismountEventName = 'dismount';
|
|
5
|
+
export const disconnectEventName = 'disconnect';
|
|
6
|
+
export const attrchangeEventName = 'attrchange';
|
|
7
|
+
export const mediamatchEventName = 'mediamatch';
|
|
8
|
+
export const mediaunmatchEventName = 'mediaunmatch';
|
|
3
9
|
export class MountEvent extends Event {
|
|
4
10
|
matchingElement;
|
|
5
11
|
modules;
|
|
12
|
+
mountInit;
|
|
6
13
|
static eventName = mountEventName;
|
|
7
|
-
constructor(matchingElement, modules) {
|
|
14
|
+
constructor(matchingElement, modules, mountInit) {
|
|
8
15
|
super(MountEvent.eventName);
|
|
9
16
|
this.matchingElement = matchingElement;
|
|
10
17
|
this.modules = modules;
|
|
18
|
+
this.mountInit = mountInit;
|
|
11
19
|
}
|
|
12
20
|
}
|
|
13
21
|
export class DismountEvent extends Event {
|
|
14
22
|
matchingElement;
|
|
23
|
+
reason;
|
|
24
|
+
mountInit;
|
|
15
25
|
static eventName = dismountEventName;
|
|
16
|
-
constructor(matchingElement) {
|
|
26
|
+
constructor(matchingElement, reason, mountInit) {
|
|
17
27
|
super(DismountEvent.eventName);
|
|
18
28
|
this.matchingElement = matchingElement;
|
|
29
|
+
this.reason = reason;
|
|
30
|
+
this.mountInit = mountInit;
|
|
19
31
|
}
|
|
20
32
|
}
|
|
21
33
|
export class DisconnectEvent extends Event {
|
|
22
34
|
matchingElement;
|
|
35
|
+
mountInit;
|
|
23
36
|
static eventName = disconnectEventName;
|
|
24
|
-
constructor(matchingElement) {
|
|
37
|
+
constructor(matchingElement, mountInit) {
|
|
25
38
|
super(DisconnectEvent.eventName);
|
|
26
39
|
this.matchingElement = matchingElement;
|
|
40
|
+
this.mountInit = mountInit;
|
|
27
41
|
}
|
|
28
42
|
}
|
|
29
43
|
export class LoadEvent extends Event {
|
|
30
44
|
modules;
|
|
45
|
+
mountInit;
|
|
31
46
|
static eventName = loadEventName;
|
|
32
|
-
constructor(modules) {
|
|
47
|
+
constructor(modules, mountInit) {
|
|
33
48
|
super(LoadEvent.eventName);
|
|
34
49
|
this.modules = modules;
|
|
50
|
+
this.mountInit = mountInit;
|
|
35
51
|
}
|
|
36
52
|
}
|
|
37
53
|
export class AttrChangeEvent extends Event {
|
|
38
54
|
changes;
|
|
55
|
+
mountInit;
|
|
39
56
|
static eventName = attrchangeEventName;
|
|
40
|
-
constructor(changes) {
|
|
57
|
+
constructor(changes, mountInit) {
|
|
41
58
|
super(AttrChangeEvent.eventName);
|
|
42
59
|
this.changes = changes;
|
|
60
|
+
this.mountInit = mountInit;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export class MediaMatchEvent extends Event {
|
|
64
|
+
mountInit;
|
|
65
|
+
static eventName = mediamatchEventName;
|
|
66
|
+
constructor(mountInit) {
|
|
67
|
+
super(MediaMatchEvent.eventName);
|
|
68
|
+
this.mountInit = mountInit;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export class MediaUnmatchEvent extends Event {
|
|
72
|
+
mountInit;
|
|
73
|
+
static eventName = mediaunmatchEventName;
|
|
74
|
+
constructor(mountInit) {
|
|
75
|
+
super(MediaUnmatchEvent.eventName);
|
|
76
|
+
this.mountInit = mountInit;
|
|
43
77
|
}
|
|
44
78
|
}
|
package/Events.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
// Event classes for MountObserver
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
import type { IMountEvent, IDismountEvent, IAttrChangeEvent, AttrChange, MountInit, DismountReason } from './types.js';
|
|
3
|
+
|
|
4
|
+
// Event name constants
|
|
5
|
+
export const loadEventName = 'load';
|
|
6
|
+
export const mountEventName = 'mount';
|
|
7
|
+
export const dismountEventName = 'dismount';
|
|
8
|
+
export const disconnectEventName = 'disconnect';
|
|
9
|
+
export const attrchangeEventName = 'attrchange';
|
|
10
|
+
export const mediamatchEventName = 'mediamatch';
|
|
11
|
+
export const mediaunmatchEventName = 'mediaunmatch';
|
|
10
12
|
|
|
11
13
|
export class MountEvent extends Event implements IMountEvent {
|
|
12
14
|
static eventName: typeof mountEventName = mountEventName;
|
|
13
15
|
|
|
14
|
-
constructor(public matchingElement: Element, public modules: any[]) {
|
|
16
|
+
constructor(public matchingElement: Element, public modules: any[], public mountInit: MountInit) {
|
|
15
17
|
super(MountEvent.eventName);
|
|
16
18
|
}
|
|
17
19
|
}
|
|
@@ -19,7 +21,7 @@ export class MountEvent extends Event implements IMountEvent {
|
|
|
19
21
|
export class DismountEvent extends Event implements IDismountEvent {
|
|
20
22
|
static eventName: typeof dismountEventName = dismountEventName;
|
|
21
23
|
|
|
22
|
-
constructor(public matchingElement: Element) {
|
|
24
|
+
constructor(public matchingElement: Element, public reason: DismountReason, public mountInit: MountInit) {
|
|
23
25
|
super(DismountEvent.eventName);
|
|
24
26
|
}
|
|
25
27
|
}
|
|
@@ -27,7 +29,7 @@ export class DismountEvent extends Event implements IDismountEvent {
|
|
|
27
29
|
export class DisconnectEvent extends Event {
|
|
28
30
|
static eventName: typeof disconnectEventName = disconnectEventName;
|
|
29
31
|
|
|
30
|
-
constructor(public matchingElement: Element) {
|
|
32
|
+
constructor(public matchingElement: Element, public mountInit: MountInit) {
|
|
31
33
|
super(DisconnectEvent.eventName);
|
|
32
34
|
}
|
|
33
35
|
}
|
|
@@ -35,7 +37,7 @@ export class DisconnectEvent extends Event {
|
|
|
35
37
|
export class LoadEvent extends Event {
|
|
36
38
|
static eventName: typeof loadEventName = loadEventName;
|
|
37
39
|
|
|
38
|
-
constructor(public modules: any[]) {
|
|
40
|
+
constructor(public modules: any[], public mountInit: MountInit) {
|
|
39
41
|
super(LoadEvent.eventName);
|
|
40
42
|
}
|
|
41
43
|
}
|
|
@@ -43,7 +45,23 @@ export class LoadEvent extends Event {
|
|
|
43
45
|
export class AttrChangeEvent extends Event implements IAttrChangeEvent {
|
|
44
46
|
static eventName: typeof attrchangeEventName = attrchangeEventName;
|
|
45
47
|
|
|
46
|
-
constructor(public changes: AttrChange[]) {
|
|
48
|
+
constructor(public changes: AttrChange[], public mountInit: MountInit) {
|
|
47
49
|
super(AttrChangeEvent.eventName);
|
|
48
50
|
}
|
|
49
51
|
}
|
|
52
|
+
|
|
53
|
+
export class MediaMatchEvent extends Event {
|
|
54
|
+
static eventName: typeof mediamatchEventName = mediamatchEventName;
|
|
55
|
+
|
|
56
|
+
constructor(public mountInit: MountInit) {
|
|
57
|
+
super(MediaMatchEvent.eventName);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class MediaUnmatchEvent extends Event {
|
|
62
|
+
static eventName: typeof mediaunmatchEventName = mediaunmatchEventName;
|
|
63
|
+
|
|
64
|
+
constructor(public mountInit: MountInit) {
|
|
65
|
+
super(MediaUnmatchEvent.eventName);
|
|
66
|
+
}
|
|
67
|
+
}
|
package/MountObserver.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MountEvent, DismountEvent, DisconnectEvent, LoadEvent, AttrChangeEvent } from './Events.js';
|
|
2
|
+
import { registerSharedObserver, unregisterSharedObserver } from './SharedMutationObserver.js';
|
|
2
3
|
export class MountObserver extends EventTarget {
|
|
3
4
|
#init;
|
|
4
5
|
#options;
|
|
@@ -6,12 +7,15 @@ export class MountObserver extends EventTarget {
|
|
|
6
7
|
#modules = [];
|
|
7
8
|
#mountedElements = new WeakSet();
|
|
8
9
|
#processedElements = new WeakSet();
|
|
9
|
-
#
|
|
10
|
+
#mutationCallback;
|
|
10
11
|
#rootNode;
|
|
11
12
|
#importsLoaded = false;
|
|
12
13
|
#elementAttrStates = new WeakMap();
|
|
14
|
+
#elementOnceAttrs = new WeakMap();
|
|
13
15
|
#matchesWhereAttrFn = null;
|
|
14
16
|
#buildAttrCoordinateMapFn = null;
|
|
17
|
+
#mediaQueryCleanup;
|
|
18
|
+
#mediaMatches = true;
|
|
15
19
|
constructor(init, options = {}) {
|
|
16
20
|
super();
|
|
17
21
|
this.#init = init;
|
|
@@ -41,6 +45,15 @@ export class MountObserver extends EventTarget {
|
|
|
41
45
|
this.#buildAttrCoordinateMapFn = buildAttrCoordinateMap;
|
|
42
46
|
}
|
|
43
47
|
}
|
|
48
|
+
async #setupMediaQuery() {
|
|
49
|
+
if (!this.#rootNode) {
|
|
50
|
+
throw new Error('Cannot setup media query before observe() is called');
|
|
51
|
+
}
|
|
52
|
+
const { setupMediaQuery } = await import('./mediaQuery.js');
|
|
53
|
+
const result = setupMediaQuery(this.#init, this.#rootNode, this.#mountedElements, this.#modules, this, (node) => this.#processNode(node));
|
|
54
|
+
this.#mediaMatches = result.mediaMatches;
|
|
55
|
+
this.#mediaQueryCleanup = result.cleanup;
|
|
56
|
+
}
|
|
44
57
|
get disconnectedSignal() {
|
|
45
58
|
return this.#abortController.signal;
|
|
46
59
|
}
|
|
@@ -49,14 +62,24 @@ export class MountObserver extends EventTarget {
|
|
|
49
62
|
throw new Error('Already observing');
|
|
50
63
|
}
|
|
51
64
|
this.#rootNode = new WeakRef(rootNode);
|
|
65
|
+
// Set up media query if specified (needs rootNode to be set first)
|
|
66
|
+
if (this.#init.whereMediaMatches) {
|
|
67
|
+
await this.#setupMediaQuery();
|
|
68
|
+
}
|
|
52
69
|
// Wait for whereAttr utilities to load if needed
|
|
53
70
|
if (this.#init.whereAttr && !this.#matchesWhereAttrFn) {
|
|
54
71
|
await this.#preloadWhereAttrUtilities();
|
|
55
72
|
}
|
|
56
|
-
// Process existing elements
|
|
57
|
-
this.#
|
|
58
|
-
|
|
59
|
-
|
|
73
|
+
// Process existing elements only if media matches
|
|
74
|
+
if (this.#mediaMatches) {
|
|
75
|
+
this.#processNode(rootNode);
|
|
76
|
+
}
|
|
77
|
+
// Create mutation callback
|
|
78
|
+
this.#mutationCallback = (mutations) => {
|
|
79
|
+
// Skip processing if media doesn't match
|
|
80
|
+
if (!this.#mediaMatches) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
60
83
|
const attrChanges = [];
|
|
61
84
|
for (const mutation of mutations) {
|
|
62
85
|
if (mutation.type === 'childList') {
|
|
@@ -82,9 +105,9 @@ export class MountObserver extends EventTarget {
|
|
|
82
105
|
}
|
|
83
106
|
// Batch and dispatch attribute changes
|
|
84
107
|
if (attrChanges.length > 0) {
|
|
85
|
-
this.dispatchEvent(new AttrChangeEvent(attrChanges));
|
|
108
|
+
this.dispatchEvent(new AttrChangeEvent(attrChanges, this.#init));
|
|
86
109
|
}
|
|
87
|
-
}
|
|
110
|
+
};
|
|
88
111
|
const observerConfig = {
|
|
89
112
|
childList: true,
|
|
90
113
|
subtree: true
|
|
@@ -94,12 +117,20 @@ export class MountObserver extends EventTarget {
|
|
|
94
117
|
observerConfig.attributes = true;
|
|
95
118
|
observerConfig.attributeOldValue = true;
|
|
96
119
|
}
|
|
97
|
-
|
|
120
|
+
// Register with shared mutation observer
|
|
121
|
+
registerSharedObserver(rootNode, this.#mutationCallback, observerConfig);
|
|
98
122
|
}
|
|
99
123
|
disconnect() {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
124
|
+
const rootNode = this.#rootNode?.deref();
|
|
125
|
+
// Unregister from shared mutation observer
|
|
126
|
+
if (rootNode && this.#mutationCallback) {
|
|
127
|
+
unregisterSharedObserver(rootNode, this.#mutationCallback);
|
|
128
|
+
this.#mutationCallback = undefined;
|
|
129
|
+
}
|
|
130
|
+
// Remove media query listener
|
|
131
|
+
if (this.#mediaQueryCleanup) {
|
|
132
|
+
this.#mediaQueryCleanup();
|
|
133
|
+
this.#mediaQueryCleanup = undefined;
|
|
103
134
|
}
|
|
104
135
|
this.#abortController.abort();
|
|
105
136
|
this.#rootNode = undefined;
|
|
@@ -112,7 +143,7 @@ export class MountObserver extends EventTarget {
|
|
|
112
143
|
const { loadImports } = await import('./loadImports.js');
|
|
113
144
|
this.#modules = await loadImports(this.#init.import);
|
|
114
145
|
this.#importsLoaded = true;
|
|
115
|
-
this.dispatchEvent(new LoadEvent(this.#modules));
|
|
146
|
+
this.dispatchEvent(new LoadEvent(this.#modules, this.#init));
|
|
116
147
|
}
|
|
117
148
|
#processNode(node) {
|
|
118
149
|
// If it's an element node, check if it matches
|
|
@@ -125,38 +156,45 @@ export class MountObserver extends EventTarget {
|
|
|
125
156
|
// Process children
|
|
126
157
|
if (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.DOCUMENT_NODE) {
|
|
127
158
|
const root = node;
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// Get all elements matching the CSS selector first
|
|
132
|
-
root.querySelectorAll(this.#init.whereElementMatches).forEach(child => {
|
|
133
|
-
if (this.#matchesSelector(child)) {
|
|
134
|
-
this.#handleMatch(child);
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
// Optimize: use querySelectorAll directly when no whereAttr
|
|
140
|
-
root.querySelectorAll(this.#init.whereElementMatches).forEach(child => {
|
|
159
|
+
// Get all elements matching the CSS selector first
|
|
160
|
+
root.querySelectorAll(this.#init.whereElementMatches).forEach(child => {
|
|
161
|
+
if (this.#matchesSelector(child)) {
|
|
141
162
|
this.#handleMatch(child);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
144
165
|
}
|
|
145
166
|
}
|
|
146
167
|
#matchesSelector(element) {
|
|
168
|
+
//TODO: reduce redundncy with this.#init?
|
|
147
169
|
// Check whereElementMatches condition
|
|
148
170
|
const matchesElement = element.matches(this.#init.whereElementMatches);
|
|
149
|
-
|
|
150
|
-
if (!this.#init.whereAttr) {
|
|
151
|
-
return matchesElement;
|
|
152
|
-
}
|
|
153
|
-
// Use cached function (should be loaded by now from constructor)
|
|
154
|
-
if (!this.#matchesWhereAttrFn) {
|
|
155
|
-
console.warn('whereAttr utilities not loaded yet');
|
|
171
|
+
if (!matchesElement) {
|
|
156
172
|
return false;
|
|
157
173
|
}
|
|
158
|
-
//
|
|
159
|
-
|
|
174
|
+
// Check whereAttr condition if specified
|
|
175
|
+
if (this.#init.whereAttr) {
|
|
176
|
+
// Use cached function (should be loaded by now from constructor)
|
|
177
|
+
if (!this.#matchesWhereAttrFn) {
|
|
178
|
+
console.warn('whereAttr utilities not loaded yet');
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
if (!this.#matchesWhereAttrFn(element, this.#init.whereAttr)) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Check whereInstanceOf condition if specified
|
|
186
|
+
if (this.#init.whereInstanceOf) {
|
|
187
|
+
const constructors = Array.isArray(this.#init.whereInstanceOf)
|
|
188
|
+
? this.#init.whereInstanceOf
|
|
189
|
+
: [this.#init.whereInstanceOf];
|
|
190
|
+
// Element must be an instance of at least one constructor (OR logic for array)
|
|
191
|
+
const matchesInstanceOf = constructors.some(constructor => element instanceof constructor);
|
|
192
|
+
if (!matchesInstanceOf) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// All conditions passed
|
|
197
|
+
return true;
|
|
160
198
|
}
|
|
161
199
|
async #handleMatch(element) {
|
|
162
200
|
if (this.#processedElements.has(element)) {
|
|
@@ -195,12 +233,12 @@ export class MountObserver extends EventTarget {
|
|
|
195
233
|
}
|
|
196
234
|
}
|
|
197
235
|
// Dispatch mount event
|
|
198
|
-
this.dispatchEvent(new MountEvent(element, this.#modules));
|
|
236
|
+
this.dispatchEvent(new MountEvent(element, this.#modules, this.#init));
|
|
199
237
|
// Check for initial attribute changes if whereAttr is configured
|
|
200
238
|
if (this.#init.whereAttr) {
|
|
201
239
|
const changes = this.#checkAttrChanges(element);
|
|
202
240
|
if (changes.length > 0) {
|
|
203
|
-
this.dispatchEvent(new AttrChangeEvent(changes));
|
|
241
|
+
this.dispatchEvent(new AttrChangeEvent(changes, this.#init));
|
|
204
242
|
}
|
|
205
243
|
}
|
|
206
244
|
}
|
|
@@ -226,12 +264,30 @@ export class MountObserver extends EventTarget {
|
|
|
226
264
|
if (currentValue !== null) {
|
|
227
265
|
currentAttrs.add(attrName);
|
|
228
266
|
}
|
|
267
|
+
// Check if this attribute has "once: true" in its map entry
|
|
268
|
+
const mapEntry = this.#init.map?.[coordinate] || null;
|
|
269
|
+
const isOnce = mapEntry?.once === true;
|
|
270
|
+
// If "once" is true, check if we've already seen this attribute
|
|
271
|
+
if (isOnce) {
|
|
272
|
+
let onceAttrs = this.#elementOnceAttrs.get(element);
|
|
273
|
+
if (!onceAttrs) {
|
|
274
|
+
onceAttrs = new Set();
|
|
275
|
+
this.#elementOnceAttrs.set(element, onceAttrs);
|
|
276
|
+
}
|
|
277
|
+
// If we've already seen this attribute, skip it
|
|
278
|
+
if (onceAttrs.has(attrName)) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
// Mark this attribute as seen if it currently has a value
|
|
282
|
+
if (currentValue !== null) {
|
|
283
|
+
onceAttrs.add(attrName);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
229
286
|
// Include if: currently has value OR previously had value but now removed
|
|
230
287
|
if (currentValue !== null || (previousValue !== undefined && currentValue === null)) {
|
|
231
288
|
// Check if value changed
|
|
232
289
|
if (currentValue !== previousValue) {
|
|
233
290
|
const attrNode = currentValue !== null ? element.getAttributeNode(attrName) : null;
|
|
234
|
-
const mapEntry = this.#init.map?.[coordinate] || null;
|
|
235
291
|
changes.push({
|
|
236
292
|
value: currentValue,
|
|
237
293
|
attrNode,
|
|
@@ -274,7 +330,7 @@ export class MountObserver extends EventTarget {
|
|
|
274
330
|
this.#init.do.dismount(element, context);
|
|
275
331
|
}
|
|
276
332
|
// Dispatch dismount event
|
|
277
|
-
this.dispatchEvent(new DismountEvent(element));
|
|
333
|
+
this.dispatchEvent(new DismountEvent(element, 'where-element-matches-failed', this.#init));
|
|
278
334
|
// Check if element is being moved within the same root
|
|
279
335
|
// If it's truly disconnected, dispatch disconnect event
|
|
280
336
|
setTimeout(() => {
|
|
@@ -282,7 +338,7 @@ export class MountObserver extends EventTarget {
|
|
|
282
338
|
if (this.#init.do && typeof this.#init.do !== 'function' && this.#init.do.disconnect) {
|
|
283
339
|
this.#init.do.disconnect(element, context);
|
|
284
340
|
}
|
|
285
|
-
this.dispatchEvent(new DisconnectEvent(element));
|
|
341
|
+
this.dispatchEvent(new DisconnectEvent(element, this.#init));
|
|
286
342
|
}
|
|
287
343
|
}, 0);
|
|
288
344
|
}
|
package/MountObserver.ts
CHANGED
|
@@ -10,8 +10,15 @@ import {
|
|
|
10
10
|
DismountEvent,
|
|
11
11
|
DisconnectEvent,
|
|
12
12
|
LoadEvent,
|
|
13
|
-
AttrChangeEvent
|
|
13
|
+
AttrChangeEvent,
|
|
14
|
+
MediaMatchEvent,
|
|
15
|
+
MediaUnmatchEvent
|
|
14
16
|
} from './Events.js';
|
|
17
|
+
import {
|
|
18
|
+
registerSharedObserver,
|
|
19
|
+
unregisterSharedObserver,
|
|
20
|
+
type MutationCallback
|
|
21
|
+
} from './SharedMutationObserver.js';
|
|
15
22
|
|
|
16
23
|
export class MountObserver extends EventTarget implements IMountObserver {
|
|
17
24
|
#init: MountInit;
|
|
@@ -20,12 +27,15 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
20
27
|
#modules: any[] = [];
|
|
21
28
|
#mountedElements = new WeakSet<Element>();
|
|
22
29
|
#processedElements = new WeakSet<Element>();
|
|
23
|
-
#
|
|
30
|
+
#mutationCallback: MutationCallback | undefined;
|
|
24
31
|
#rootNode: WeakRef<Node> | undefined;
|
|
25
32
|
#importsLoaded = false;
|
|
26
33
|
#elementAttrStates = new WeakMap<Element, Map<string, string | null>>();
|
|
34
|
+
#elementOnceAttrs = new WeakMap<Element, Set<string>>();
|
|
27
35
|
#matchesWhereAttrFn: ((element: Element, whereAttr: any) => boolean) | null = null;
|
|
28
36
|
#buildAttrCoordinateMapFn: ((whereAttr: any, isCustomElement: boolean) => any) | null = null;
|
|
37
|
+
#mediaQueryCleanup?: () => void;
|
|
38
|
+
#mediaMatches: boolean = true;
|
|
29
39
|
|
|
30
40
|
constructor(init: MountInit, options: MountObserverOptions = {}) {
|
|
31
41
|
super();
|
|
@@ -60,6 +70,25 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
60
70
|
this.#buildAttrCoordinateMapFn = buildAttrCoordinateMap;
|
|
61
71
|
}
|
|
62
72
|
}
|
|
73
|
+
|
|
74
|
+
async #setupMediaQuery(): Promise<void> {
|
|
75
|
+
if (!this.#rootNode) {
|
|
76
|
+
throw new Error('Cannot setup media query before observe() is called');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { setupMediaQuery } = await import('./mediaQuery.js');
|
|
80
|
+
const result = setupMediaQuery(
|
|
81
|
+
this.#init,
|
|
82
|
+
this.#rootNode,
|
|
83
|
+
this.#mountedElements,
|
|
84
|
+
this.#modules,
|
|
85
|
+
this,
|
|
86
|
+
(node) => this.#processNode(node)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
this.#mediaMatches = result.mediaMatches;
|
|
90
|
+
this.#mediaQueryCleanup = result.cleanup;
|
|
91
|
+
}
|
|
63
92
|
|
|
64
93
|
get disconnectedSignal(): AbortSignal {
|
|
65
94
|
return this.#abortController.signal;
|
|
@@ -72,16 +101,28 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
72
101
|
|
|
73
102
|
this.#rootNode = new WeakRef(rootNode);
|
|
74
103
|
|
|
104
|
+
// Set up media query if specified (needs rootNode to be set first)
|
|
105
|
+
if (this.#init.whereMediaMatches) {
|
|
106
|
+
await this.#setupMediaQuery();
|
|
107
|
+
}
|
|
108
|
+
|
|
75
109
|
// Wait for whereAttr utilities to load if needed
|
|
76
110
|
if (this.#init.whereAttr && !this.#matchesWhereAttrFn) {
|
|
77
111
|
await this.#preloadWhereAttrUtilities();
|
|
78
112
|
}
|
|
79
113
|
|
|
80
|
-
// Process existing elements
|
|
81
|
-
this.#
|
|
114
|
+
// Process existing elements only if media matches
|
|
115
|
+
if (this.#mediaMatches) {
|
|
116
|
+
this.#processNode(rootNode);
|
|
117
|
+
}
|
|
82
118
|
|
|
83
|
-
//
|
|
84
|
-
this.#
|
|
119
|
+
// Create mutation callback
|
|
120
|
+
this.#mutationCallback = (mutations) => {
|
|
121
|
+
// Skip processing if media doesn't match
|
|
122
|
+
if (!this.#mediaMatches) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
85
126
|
const attrChanges: AttrChange[] = [];
|
|
86
127
|
|
|
87
128
|
for (const mutation of mutations) {
|
|
@@ -108,9 +149,9 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
108
149
|
|
|
109
150
|
// Batch and dispatch attribute changes
|
|
110
151
|
if (attrChanges.length > 0) {
|
|
111
|
-
this.dispatchEvent(new AttrChangeEvent(attrChanges));
|
|
152
|
+
this.dispatchEvent(new AttrChangeEvent(attrChanges, this.#init));
|
|
112
153
|
}
|
|
113
|
-
}
|
|
154
|
+
};
|
|
114
155
|
|
|
115
156
|
const observerConfig: MutationObserverInit = {
|
|
116
157
|
childList: true,
|
|
@@ -123,14 +164,25 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
123
164
|
observerConfig.attributeOldValue = true;
|
|
124
165
|
}
|
|
125
166
|
|
|
126
|
-
|
|
167
|
+
// Register with shared mutation observer
|
|
168
|
+
registerSharedObserver(rootNode, this.#mutationCallback, observerConfig);
|
|
127
169
|
}
|
|
128
170
|
|
|
129
171
|
disconnect(): void {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
172
|
+
const rootNode = this.#rootNode?.deref();
|
|
173
|
+
|
|
174
|
+
// Unregister from shared mutation observer
|
|
175
|
+
if (rootNode && this.#mutationCallback) {
|
|
176
|
+
unregisterSharedObserver(rootNode, this.#mutationCallback);
|
|
177
|
+
this.#mutationCallback = undefined;
|
|
133
178
|
}
|
|
179
|
+
|
|
180
|
+
// Remove media query listener
|
|
181
|
+
if (this.#mediaQueryCleanup) {
|
|
182
|
+
this.#mediaQueryCleanup();
|
|
183
|
+
this.#mediaQueryCleanup = undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
134
186
|
this.#abortController.abort();
|
|
135
187
|
this.#rootNode = undefined;
|
|
136
188
|
}
|
|
@@ -145,7 +197,7 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
145
197
|
this.#modules = await loadImports(this.#init.import);
|
|
146
198
|
this.#importsLoaded = true;
|
|
147
199
|
|
|
148
|
-
this.dispatchEvent(new LoadEvent(this.#modules));
|
|
200
|
+
this.dispatchEvent(new LoadEvent(this.#modules, this.#init));
|
|
149
201
|
}
|
|
150
202
|
|
|
151
203
|
#processNode(node: Node): void {
|
|
@@ -162,41 +214,52 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
162
214
|
if (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.DOCUMENT_NODE) {
|
|
163
215
|
const root = node as Element | Document;
|
|
164
216
|
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// Get all elements matching the CSS selector first
|
|
169
|
-
root.querySelectorAll(this.#init.whereElementMatches).forEach(child => {
|
|
170
|
-
if (this.#matchesSelector(child)) {
|
|
171
|
-
this.#handleMatch(child);
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
} else {
|
|
175
|
-
// Optimize: use querySelectorAll directly when no whereAttr
|
|
176
|
-
root.querySelectorAll(this.#init.whereElementMatches).forEach(child => {
|
|
217
|
+
// Get all elements matching the CSS selector first
|
|
218
|
+
root.querySelectorAll(this.#init.whereElementMatches).forEach(child => {
|
|
219
|
+
if (this.#matchesSelector(child)) {
|
|
177
220
|
this.#handleMatch(child);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
180
223
|
}
|
|
181
224
|
}
|
|
182
225
|
|
|
183
226
|
#matchesSelector(element: Element): boolean {
|
|
227
|
+
//TODO: reduce redundncy with this.#init?
|
|
184
228
|
// Check whereElementMatches condition
|
|
185
229
|
const matchesElement = element.matches(this.#init.whereElementMatches);
|
|
230
|
+
if (!matchesElement) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
186
233
|
|
|
187
|
-
//
|
|
188
|
-
if (
|
|
189
|
-
|
|
234
|
+
// Check whereAttr condition if specified
|
|
235
|
+
if (this.#init.whereAttr) {
|
|
236
|
+
// Use cached function (should be loaded by now from constructor)
|
|
237
|
+
if (!this.#matchesWhereAttrFn) {
|
|
238
|
+
console.warn('whereAttr utilities not loaded yet');
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!this.#matchesWhereAttrFn(element, this.#init.whereAttr)) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
190
245
|
}
|
|
191
246
|
|
|
192
|
-
//
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
247
|
+
// Check whereInstanceOf condition if specified
|
|
248
|
+
if (this.#init.whereInstanceOf) {
|
|
249
|
+
const constructors = Array.isArray(this.#init.whereInstanceOf)
|
|
250
|
+
? this.#init.whereInstanceOf
|
|
251
|
+
: [this.#init.whereInstanceOf];
|
|
252
|
+
|
|
253
|
+
// Element must be an instance of at least one constructor (OR logic for array)
|
|
254
|
+
const matchesInstanceOf = constructors.some(constructor => element instanceof constructor);
|
|
255
|
+
|
|
256
|
+
if (!matchesInstanceOf) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
196
259
|
}
|
|
197
260
|
|
|
198
|
-
//
|
|
199
|
-
return
|
|
261
|
+
// All conditions passed
|
|
262
|
+
return true;
|
|
200
263
|
}
|
|
201
264
|
|
|
202
265
|
async #handleMatch(element: Element): Promise<void> {
|
|
@@ -242,13 +305,13 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
242
305
|
}
|
|
243
306
|
|
|
244
307
|
// Dispatch mount event
|
|
245
|
-
this.dispatchEvent(new MountEvent(element, this.#modules));
|
|
308
|
+
this.dispatchEvent(new MountEvent(element, this.#modules, this.#init));
|
|
246
309
|
|
|
247
310
|
// Check for initial attribute changes if whereAttr is configured
|
|
248
311
|
if (this.#init.whereAttr) {
|
|
249
312
|
const changes = this.#checkAttrChanges(element);
|
|
250
313
|
if (changes.length > 0) {
|
|
251
|
-
this.dispatchEvent(new AttrChangeEvent(changes));
|
|
314
|
+
this.dispatchEvent(new AttrChangeEvent(changes, this.#init));
|
|
252
315
|
}
|
|
253
316
|
}
|
|
254
317
|
}
|
|
@@ -281,12 +344,34 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
281
344
|
currentAttrs.add(attrName);
|
|
282
345
|
}
|
|
283
346
|
|
|
347
|
+
// Check if this attribute has "once: true" in its map entry
|
|
348
|
+
const mapEntry = this.#init.map?.[coordinate] || null;
|
|
349
|
+
const isOnce = mapEntry?.once === true;
|
|
350
|
+
|
|
351
|
+
// If "once" is true, check if we've already seen this attribute
|
|
352
|
+
if (isOnce) {
|
|
353
|
+
let onceAttrs = this.#elementOnceAttrs.get(element);
|
|
354
|
+
if (!onceAttrs) {
|
|
355
|
+
onceAttrs = new Set<string>();
|
|
356
|
+
this.#elementOnceAttrs.set(element, onceAttrs);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// If we've already seen this attribute, skip it
|
|
360
|
+
if (onceAttrs.has(attrName)) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Mark this attribute as seen if it currently has a value
|
|
365
|
+
if (currentValue !== null) {
|
|
366
|
+
onceAttrs.add(attrName);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
284
370
|
// Include if: currently has value OR previously had value but now removed
|
|
285
371
|
if (currentValue !== null || (previousValue !== undefined && currentValue === null)) {
|
|
286
372
|
// Check if value changed
|
|
287
373
|
if (currentValue !== previousValue) {
|
|
288
374
|
const attrNode = currentValue !== null ? element.getAttributeNode(attrName) : null;
|
|
289
|
-
const mapEntry = this.#init.map?.[coordinate] || null;
|
|
290
375
|
|
|
291
376
|
changes.push({
|
|
292
377
|
value: currentValue,
|
|
@@ -337,7 +422,7 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
337
422
|
}
|
|
338
423
|
|
|
339
424
|
// Dispatch dismount event
|
|
340
|
-
this.dispatchEvent(new DismountEvent(element));
|
|
425
|
+
this.dispatchEvent(new DismountEvent(element, 'where-element-matches-failed', this.#init));
|
|
341
426
|
|
|
342
427
|
// Check if element is being moved within the same root
|
|
343
428
|
// If it's truly disconnected, dispatch disconnect event
|
|
@@ -347,7 +432,7 @@ export class MountObserver extends EventTarget implements IMountObserver {
|
|
|
347
432
|
this.#init.do.disconnect(element, context);
|
|
348
433
|
}
|
|
349
434
|
|
|
350
|
-
this.dispatchEvent(new DisconnectEvent(element));
|
|
435
|
+
this.dispatchEvent(new DisconnectEvent(element, this.#init));
|
|
351
436
|
}
|
|
352
437
|
}, 0);
|
|
353
438
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages shared MutationObserver instances for multiple MountObserver instances
|
|
3
|
+
* observing the same root node. This reduces overhead when multiple observers
|
|
4
|
+
* are watching the same DOM fragment.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Global registry of shared observers, keyed by root node
|
|
8
|
+
*/
|
|
9
|
+
const sharedObservers = new WeakMap();
|
|
10
|
+
/**
|
|
11
|
+
* Registers a callback with the shared MutationObserver for the given root node.
|
|
12
|
+
* Creates a new shared observer if one doesn't exist for this root.
|
|
13
|
+
*/
|
|
14
|
+
export function registerSharedObserver(rootNode, callback, config) {
|
|
15
|
+
let sharedData = sharedObservers.get(rootNode);
|
|
16
|
+
if (!sharedData) {
|
|
17
|
+
// Create new shared observer for this root node
|
|
18
|
+
const observer = new MutationObserver((mutations) => {
|
|
19
|
+
// Distribute mutations to all registered callbacks
|
|
20
|
+
const callbacks = sharedData.callbacks;
|
|
21
|
+
for (const cb of callbacks) {
|
|
22
|
+
cb(mutations);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
observer.observe(rootNode, config);
|
|
26
|
+
sharedData = {
|
|
27
|
+
observer,
|
|
28
|
+
callbacks: new Set(),
|
|
29
|
+
config
|
|
30
|
+
};
|
|
31
|
+
sharedObservers.set(rootNode, sharedData);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
// Verify config matches (for safety)
|
|
35
|
+
// In practice, all MountObservers should use the same config
|
|
36
|
+
if (!configsMatch(sharedData.config, config)) {
|
|
37
|
+
console.warn('MutationObserver config mismatch detected. Using existing config.');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Register the callback
|
|
41
|
+
sharedData.callbacks.add(callback);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Unregisters a callback from the shared MutationObserver.
|
|
45
|
+
* If this was the last callback, disconnects and removes the shared observer.
|
|
46
|
+
*/
|
|
47
|
+
export function unregisterSharedObserver(rootNode, callback) {
|
|
48
|
+
const sharedData = sharedObservers.get(rootNode);
|
|
49
|
+
if (!sharedData) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Remove the callback
|
|
53
|
+
sharedData.callbacks.delete(callback);
|
|
54
|
+
// If no more callbacks, disconnect and cleanup
|
|
55
|
+
if (sharedData.callbacks.size === 0) {
|
|
56
|
+
sharedData.observer.disconnect();
|
|
57
|
+
sharedObservers.delete(rootNode);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Checks if two MutationObserverInit configs are equivalent
|
|
62
|
+
*/
|
|
63
|
+
function configsMatch(a, b) {
|
|
64
|
+
return a.childList === b.childList &&
|
|
65
|
+
a.subtree === b.subtree &&
|
|
66
|
+
a.attributes === b.attributes &&
|
|
67
|
+
a.attributeOldValue === b.attributeOldValue &&
|
|
68
|
+
a.characterData === b.characterData &&
|
|
69
|
+
a.characterDataOldValue === b.characterDataOldValue;
|
|
70
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages shared MutationObserver instances for multiple MountObserver instances
|
|
3
|
+
* observing the same root node. This reduces overhead when multiple observers
|
|
4
|
+
* are watching the same DOM fragment.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type MutationCallback = (mutations: MutationRecord[]) => void;
|
|
8
|
+
|
|
9
|
+
interface SharedObserverData {
|
|
10
|
+
observer: MutationObserver;
|
|
11
|
+
callbacks: Set<MutationCallback>;
|
|
12
|
+
config: MutationObserverInit;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Global registry of shared observers, keyed by root node
|
|
17
|
+
*/
|
|
18
|
+
const sharedObservers = new WeakMap<Node, SharedObserverData>();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Registers a callback with the shared MutationObserver for the given root node.
|
|
22
|
+
* Creates a new shared observer if one doesn't exist for this root.
|
|
23
|
+
*/
|
|
24
|
+
export function registerSharedObserver(
|
|
25
|
+
rootNode: Node,
|
|
26
|
+
callback: MutationCallback,
|
|
27
|
+
config: MutationObserverInit
|
|
28
|
+
): void {
|
|
29
|
+
let sharedData = sharedObservers.get(rootNode);
|
|
30
|
+
|
|
31
|
+
if (!sharedData) {
|
|
32
|
+
// Create new shared observer for this root node
|
|
33
|
+
const observer = new MutationObserver((mutations) => {
|
|
34
|
+
// Distribute mutations to all registered callbacks
|
|
35
|
+
const callbacks = sharedData!.callbacks;
|
|
36
|
+
for (const cb of callbacks) {
|
|
37
|
+
cb(mutations);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
observer.observe(rootNode, config);
|
|
42
|
+
|
|
43
|
+
sharedData = {
|
|
44
|
+
observer,
|
|
45
|
+
callbacks: new Set(),
|
|
46
|
+
config
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
sharedObservers.set(rootNode, sharedData);
|
|
50
|
+
} else {
|
|
51
|
+
// Verify config matches (for safety)
|
|
52
|
+
// In practice, all MountObservers should use the same config
|
|
53
|
+
if (!configsMatch(sharedData.config, config)) {
|
|
54
|
+
console.warn('MutationObserver config mismatch detected. Using existing config.');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Register the callback
|
|
59
|
+
sharedData.callbacks.add(callback);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Unregisters a callback from the shared MutationObserver.
|
|
64
|
+
* If this was the last callback, disconnects and removes the shared observer.
|
|
65
|
+
*/
|
|
66
|
+
export function unregisterSharedObserver(
|
|
67
|
+
rootNode: Node,
|
|
68
|
+
callback: MutationCallback
|
|
69
|
+
): void {
|
|
70
|
+
const sharedData = sharedObservers.get(rootNode);
|
|
71
|
+
|
|
72
|
+
if (!sharedData) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Remove the callback
|
|
77
|
+
sharedData.callbacks.delete(callback);
|
|
78
|
+
|
|
79
|
+
// If no more callbacks, disconnect and cleanup
|
|
80
|
+
if (sharedData.callbacks.size === 0) {
|
|
81
|
+
sharedData.observer.disconnect();
|
|
82
|
+
sharedObservers.delete(rootNode);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Checks if two MutationObserverInit configs are equivalent
|
|
88
|
+
*/
|
|
89
|
+
function configsMatch(a: MutationObserverInit, b: MutationObserverInit): boolean {
|
|
90
|
+
return a.childList === b.childList &&
|
|
91
|
+
a.subtree === b.subtree &&
|
|
92
|
+
a.attributes === b.attributes &&
|
|
93
|
+
a.attributeOldValue === b.attributeOldValue &&
|
|
94
|
+
a.characterData === b.characterData &&
|
|
95
|
+
a.characterDataOldValue === b.characterDataOldValue;
|
|
96
|
+
}
|
package/index.ts
CHANGED
package/mediaQuery.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { MediaMatchEvent, MediaUnmatchEvent, DismountEvent } from './Events.js';
|
|
2
|
+
export function setupMediaQuery(init, rootNodeRef, mountedElements, modules, observer, processNode) {
|
|
3
|
+
const { whereMediaMatches } = init;
|
|
4
|
+
// Create or use MediaQueryList
|
|
5
|
+
let mediaQueryList;
|
|
6
|
+
if (typeof whereMediaMatches === 'string') {
|
|
7
|
+
mediaQueryList = window.matchMedia(whereMediaMatches);
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
mediaQueryList = whereMediaMatches;
|
|
11
|
+
}
|
|
12
|
+
// Track current state
|
|
13
|
+
let mediaMatches = mediaQueryList.matches;
|
|
14
|
+
// Set up change listener
|
|
15
|
+
const mediaChangeHandler = (e) => {
|
|
16
|
+
const previousMatches = mediaMatches;
|
|
17
|
+
mediaMatches = e.matches;
|
|
18
|
+
if (e.matches && !previousMatches) {
|
|
19
|
+
// Media query now matches - wake up and process elements
|
|
20
|
+
handleMediaMatch();
|
|
21
|
+
}
|
|
22
|
+
else if (!e.matches && previousMatches) {
|
|
23
|
+
// Media query no longer matches - dismount all elements
|
|
24
|
+
handleMediaUnmatch();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
function handleMediaMatch() {
|
|
28
|
+
// Dispatch mediamatch event if requested
|
|
29
|
+
if (init.getPlayByPlay) {
|
|
30
|
+
observer.dispatchEvent(new MediaMatchEvent(init));
|
|
31
|
+
}
|
|
32
|
+
// Process all elements in the observed node
|
|
33
|
+
const rootNode = rootNodeRef.deref();
|
|
34
|
+
if (rootNode) {
|
|
35
|
+
processNode(rootNode);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function handleMediaUnmatch() {
|
|
39
|
+
// Dispatch mediaunmatch event if requested
|
|
40
|
+
if (init.getPlayByPlay) {
|
|
41
|
+
observer.dispatchEvent(new MediaUnmatchEvent(init));
|
|
42
|
+
}
|
|
43
|
+
// Dismount all currently mounted elements
|
|
44
|
+
const rootNode = rootNodeRef.deref();
|
|
45
|
+
if (!rootNode) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const context = {
|
|
49
|
+
modules,
|
|
50
|
+
observer: observer,
|
|
51
|
+
observeInfo: {
|
|
52
|
+
rootNode
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
// Get all mounted elements (we need to iterate through the DOM to find them)
|
|
56
|
+
const mountedElementsList = [];
|
|
57
|
+
const collectMountedElements = (node) => {
|
|
58
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
59
|
+
const element = node;
|
|
60
|
+
if (mountedElements.has(element)) {
|
|
61
|
+
mountedElementsList.push(element);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
node.childNodes.forEach(child => collectMountedElements(child));
|
|
65
|
+
};
|
|
66
|
+
collectMountedElements(rootNode);
|
|
67
|
+
// Dismount each element
|
|
68
|
+
for (const element of mountedElementsList) {
|
|
69
|
+
mountedElements.delete(element);
|
|
70
|
+
// Call dismount callback
|
|
71
|
+
if (init.do && typeof init.do !== 'function' && init.do.dismount) {
|
|
72
|
+
init.do.dismount(element, context);
|
|
73
|
+
}
|
|
74
|
+
// Dispatch dismount event with reason
|
|
75
|
+
observer.dispatchEvent(new DismountEvent(element, 'media-query-failed', init));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
mediaQueryList.addEventListener('change', mediaChangeHandler);
|
|
79
|
+
return {
|
|
80
|
+
mediaQueryList,
|
|
81
|
+
mediaMatches,
|
|
82
|
+
cleanup: () => {
|
|
83
|
+
mediaQueryList.removeEventListener('change', mediaChangeHandler);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
package/mediaQuery.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Media query handling for MountObserver
|
|
2
|
+
import type { MountInit, MountContext } from './types.js';
|
|
3
|
+
import { MediaMatchEvent, MediaUnmatchEvent, DismountEvent } from './Events.js';
|
|
4
|
+
|
|
5
|
+
export function setupMediaQuery(
|
|
6
|
+
init: MountInit,
|
|
7
|
+
rootNodeRef: WeakRef<Node>,
|
|
8
|
+
mountedElements: WeakSet<Element>,
|
|
9
|
+
modules: any[],
|
|
10
|
+
observer: EventTarget,
|
|
11
|
+
processNode: (node: Node) => void
|
|
12
|
+
): {
|
|
13
|
+
mediaQueryList: MediaQueryList;
|
|
14
|
+
mediaMatches: boolean;
|
|
15
|
+
cleanup: () => void;
|
|
16
|
+
} {
|
|
17
|
+
const { whereMediaMatches } = init;
|
|
18
|
+
|
|
19
|
+
// Create or use MediaQueryList
|
|
20
|
+
let mediaQueryList: MediaQueryList;
|
|
21
|
+
if (typeof whereMediaMatches === 'string') {
|
|
22
|
+
mediaQueryList = window.matchMedia(whereMediaMatches);
|
|
23
|
+
} else {
|
|
24
|
+
mediaQueryList = whereMediaMatches!;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Track current state
|
|
28
|
+
let mediaMatches = mediaQueryList.matches;
|
|
29
|
+
|
|
30
|
+
// Set up change listener
|
|
31
|
+
const mediaChangeHandler = (e: MediaQueryListEvent) => {
|
|
32
|
+
const previousMatches = mediaMatches;
|
|
33
|
+
mediaMatches = e.matches;
|
|
34
|
+
|
|
35
|
+
if (e.matches && !previousMatches) {
|
|
36
|
+
// Media query now matches - wake up and process elements
|
|
37
|
+
handleMediaMatch();
|
|
38
|
+
} else if (!e.matches && previousMatches) {
|
|
39
|
+
// Media query no longer matches - dismount all elements
|
|
40
|
+
handleMediaUnmatch();
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function handleMediaMatch(): void {
|
|
45
|
+
// Dispatch mediamatch event if requested
|
|
46
|
+
if (init.getPlayByPlay) {
|
|
47
|
+
observer.dispatchEvent(new MediaMatchEvent(init));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Process all elements in the observed node
|
|
51
|
+
const rootNode = rootNodeRef.deref();
|
|
52
|
+
if (rootNode) {
|
|
53
|
+
processNode(rootNode);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function handleMediaUnmatch(): void {
|
|
58
|
+
// Dispatch mediaunmatch event if requested
|
|
59
|
+
if (init.getPlayByPlay) {
|
|
60
|
+
observer.dispatchEvent(new MediaUnmatchEvent(init));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Dismount all currently mounted elements
|
|
64
|
+
const rootNode = rootNodeRef.deref();
|
|
65
|
+
if (!rootNode) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const context: MountContext = {
|
|
70
|
+
modules,
|
|
71
|
+
observer: observer as any,
|
|
72
|
+
observeInfo: {
|
|
73
|
+
rootNode
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Get all mounted elements (we need to iterate through the DOM to find them)
|
|
78
|
+
const mountedElementsList: Element[] = [];
|
|
79
|
+
const collectMountedElements = (node: Node) => {
|
|
80
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
81
|
+
const element = node as Element;
|
|
82
|
+
if (mountedElements.has(element)) {
|
|
83
|
+
mountedElementsList.push(element);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
node.childNodes.forEach(child => collectMountedElements(child));
|
|
87
|
+
};
|
|
88
|
+
collectMountedElements(rootNode);
|
|
89
|
+
|
|
90
|
+
// Dismount each element
|
|
91
|
+
for (const element of mountedElementsList) {
|
|
92
|
+
mountedElements.delete(element);
|
|
93
|
+
|
|
94
|
+
// Call dismount callback
|
|
95
|
+
if (init.do && typeof init.do !== 'function' && init.do.dismount) {
|
|
96
|
+
init.do.dismount(element, context);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Dispatch dismount event with reason
|
|
100
|
+
observer.dispatchEvent(new DismountEvent(element, 'media-query-failed', init));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
mediaQueryList.addEventListener('change', mediaChangeHandler);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
mediaQueryList,
|
|
108
|
+
mediaMatches,
|
|
109
|
+
cleanup: () => {
|
|
110
|
+
mediaQueryList.removeEventListener('change', mediaChangeHandler);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mount-observer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Observe and act on css matches.",
|
|
5
5
|
"main": "MountObserver.js",
|
|
6
6
|
"module": "MountObserver.js",
|
|
@@ -13,22 +13,25 @@
|
|
|
13
13
|
},
|
|
14
14
|
"exports": {
|
|
15
15
|
".": {
|
|
16
|
+
"default": "./index.js",
|
|
17
|
+
"types": "./index.ts"
|
|
18
|
+
},
|
|
19
|
+
"./MountObserver.js": {
|
|
16
20
|
"default": "./MountObserver.js",
|
|
17
21
|
"types": "./MountObserver.ts"
|
|
18
22
|
}
|
|
19
|
-
|
|
20
23
|
},
|
|
21
24
|
"files": [
|
|
22
25
|
"*.js",
|
|
23
26
|
"*.ts"
|
|
24
27
|
],
|
|
25
|
-
"types": "./
|
|
28
|
+
"types": "./types.d.ts",
|
|
26
29
|
"scripts": {
|
|
27
30
|
"serve": "node ./node_modules/spa-ssi/serve.js",
|
|
28
31
|
"test": "playwright test",
|
|
29
32
|
"safari": "npx playwright wk http://localhost:8000",
|
|
30
33
|
"update": "ncu -u && npm install"
|
|
31
34
|
},
|
|
32
|
-
"author": "",
|
|
35
|
+
"author": "Bruce B. Anderson <anderson.bruce.b@gmail.com>",
|
|
33
36
|
"license": "MIT"
|
|
34
37
|
}
|
package/types.d.ts
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
// Core types for MountObserver v2 - Polyfill Supported Scenario I
|
|
2
2
|
|
|
3
|
+
export type Constructor = new (...args: any[]) => any;
|
|
4
|
+
|
|
5
|
+
export type DismountReason =
|
|
6
|
+
| 'media-query-failed'
|
|
7
|
+
| 'where-element-matches-failed';
|
|
8
|
+
|
|
3
9
|
export interface MountInit {
|
|
4
10
|
whereElementMatches: string;
|
|
5
11
|
whereAttr?: WhereAttr;
|
|
12
|
+
whereInstanceOf?: Constructor | Constructor[];
|
|
13
|
+
whereMediaMatches?: string | MediaQueryList;
|
|
6
14
|
import?: string | ImportSpec | Array<string | ImportSpec>;
|
|
7
15
|
do?: DoCallback | DoCallbacks;
|
|
8
16
|
loadingEagerness?: 'eager' | 'lazy';
|
|
9
17
|
assignGingerly?: Record<string, any>;
|
|
10
18
|
map?: MapConfig;
|
|
19
|
+
getPlayByPlay?: boolean;
|
|
11
20
|
}
|
|
12
21
|
|
|
13
22
|
export interface MapConfig {
|
|
@@ -17,6 +26,11 @@ export interface MapConfig {
|
|
|
17
26
|
export interface MapEntry {
|
|
18
27
|
instanceOf?: string;
|
|
19
28
|
mapsTo?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Only notify the presence of this attribute
|
|
31
|
+
* the first time it is seen
|
|
32
|
+
*/
|
|
33
|
+
once?: boolean;
|
|
20
34
|
[key: string]: any;
|
|
21
35
|
}
|
|
22
36
|
|
|
@@ -66,14 +80,18 @@ export interface IMountObserver extends EventTarget {
|
|
|
66
80
|
export interface IMountEvent extends Event {
|
|
67
81
|
matchingElement: Element;
|
|
68
82
|
modules: any[];
|
|
83
|
+
mountInit: MountInit;
|
|
69
84
|
}
|
|
70
85
|
|
|
71
86
|
export interface IDismountEvent extends Event {
|
|
72
87
|
matchingElement: Element;
|
|
88
|
+
reason: DismountReason;
|
|
89
|
+
mountInit: MountInit;
|
|
73
90
|
}
|
|
74
91
|
|
|
75
92
|
export interface IAttrChangeEvent extends Event {
|
|
76
93
|
changes: AttrChange[];
|
|
94
|
+
mountInit: MountInit;
|
|
77
95
|
}
|
|
78
96
|
|
|
79
97
|
export interface AttrChange {
|
package/constants.js
DELETED
package/constants.ts
DELETED