mount-observer 0.1.1 → 0.1.2
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 +76 -77
- package/MountObserver.ts +89 -89
- package/README.md +344 -24
- package/attrChanges.js +70 -0
- package/attrChanges.ts +90 -0
- package/emitEvents.js +103 -0
- package/emitEvents.ts +126 -0
- package/index.ts +4 -0
- package/mediaQuery.js +14 -11
- package/mediaQuery.ts +16 -13
- package/package.json +13 -1
- package/types.d.ts +17 -0
- package/whereOutside.js +19 -0
- package/whereOutside.ts +25 -0
package/emitEvents.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emits events from a mounted element based on the mountedElemEmits configuration.
|
|
3
|
+
* This module is dynamically loaded only when mountedElemEmits is configured.
|
|
4
|
+
*/
|
|
5
|
+
export async function emitMountedElementEvents(element, mountInit, processedEventsForElement) {
|
|
6
|
+
const configs = Array.isArray(mountInit.mountedElemEmits)
|
|
7
|
+
? mountInit.mountedElemEmits
|
|
8
|
+
: [mountInit.mountedElemEmits];
|
|
9
|
+
for (const config of configs) {
|
|
10
|
+
await emitSingleEvent(element, mountInit, config, processedEventsForElement);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async function emitSingleEvent(element, mountInit, config, processedEventsForElement) {
|
|
14
|
+
// Check if this event should only fire once per element
|
|
15
|
+
if (config.oncePerMountedElement) {
|
|
16
|
+
const eventId = getEventId(config);
|
|
17
|
+
let processedEvents = processedEventsForElement.get(element);
|
|
18
|
+
if (!processedEvents) {
|
|
19
|
+
processedEvents = new Set();
|
|
20
|
+
processedEventsForElement.set(element, processedEvents);
|
|
21
|
+
}
|
|
22
|
+
if (processedEvents.has(eventId)) {
|
|
23
|
+
return; // Already emitted for this element
|
|
24
|
+
}
|
|
25
|
+
processedEvents.add(eventId);
|
|
26
|
+
}
|
|
27
|
+
// Resolve event constructor
|
|
28
|
+
const EventCtor = resolveEventConstructor(config.event);
|
|
29
|
+
// Process args with magic string substitution
|
|
30
|
+
const processedArgs = config.args !== undefined
|
|
31
|
+
? processMagicStrings(config.args, element, mountInit)
|
|
32
|
+
: undefined;
|
|
33
|
+
// Construct the event
|
|
34
|
+
let event;
|
|
35
|
+
if (processedArgs === undefined) {
|
|
36
|
+
event = new EventCtor();
|
|
37
|
+
}
|
|
38
|
+
else if (Array.isArray(processedArgs)) {
|
|
39
|
+
// For array args, ensure bubbles is set if second arg is an options object
|
|
40
|
+
if (processedArgs.length === 2 && typeof processedArgs[0] === 'string' && typeof processedArgs[1] === 'object' && processedArgs[1] !== null) {
|
|
41
|
+
// Merge bubbles: true into the options object if not already set
|
|
42
|
+
const options = { bubbles: true, ...processedArgs[1] };
|
|
43
|
+
event = new EventCtor(processedArgs[0], options);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
event = new EventCtor(...processedArgs);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// Single arg - if it's a string (event name), add bubbles: true by default
|
|
51
|
+
if (typeof processedArgs === 'string') {
|
|
52
|
+
event = new EventCtor(processedArgs, { bubbles: true });
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
event = new EventCtor(processedArgs);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Apply eventProps if specified
|
|
59
|
+
if (config.eventProps) {
|
|
60
|
+
const { assignGingerly } = await import('assign-gingerly/index.js');
|
|
61
|
+
const processedProps = processMagicStrings(config.eventProps, element, mountInit);
|
|
62
|
+
assignGingerly(event, processedProps);
|
|
63
|
+
}
|
|
64
|
+
// Dispatch the event from the mounted element
|
|
65
|
+
element.dispatchEvent(event);
|
|
66
|
+
}
|
|
67
|
+
function resolveEventConstructor(event) {
|
|
68
|
+
if (typeof event === 'string') {
|
|
69
|
+
const EventCtor = globalThis[event];
|
|
70
|
+
if (!EventCtor || typeof EventCtor !== 'function') {
|
|
71
|
+
throw new Error(`Event constructor "${event}" not found in globalThis`);
|
|
72
|
+
}
|
|
73
|
+
return EventCtor;
|
|
74
|
+
}
|
|
75
|
+
return event;
|
|
76
|
+
}
|
|
77
|
+
function getEventId(config) {
|
|
78
|
+
const eventName = typeof config.event === 'string' ? config.event : config.event.name;
|
|
79
|
+
const argsStr = JSON.stringify(config.args || '');
|
|
80
|
+
return `${eventName}:${argsStr}`;
|
|
81
|
+
}
|
|
82
|
+
function processMagicStrings(value, element, mountInit) {
|
|
83
|
+
if (typeof value === 'string') {
|
|
84
|
+
if (value === '{{mountedElement}}') {
|
|
85
|
+
return element;
|
|
86
|
+
}
|
|
87
|
+
if (value === '{{mountInit}}') {
|
|
88
|
+
return mountInit;
|
|
89
|
+
}
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(value)) {
|
|
93
|
+
return value.map(item => processMagicStrings(item, element, mountInit));
|
|
94
|
+
}
|
|
95
|
+
if (value && typeof value === 'object') {
|
|
96
|
+
const processed = {};
|
|
97
|
+
for (const [key, val] of Object.entries(value)) {
|
|
98
|
+
processed[key] = processMagicStrings(val, element, mountInit);
|
|
99
|
+
}
|
|
100
|
+
return processed;
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
103
|
+
}
|
package/emitEvents.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { EventConfig, EventConstructor, MountInit } from './types.d.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Emits events from a mounted element based on the mountedElemEmits configuration.
|
|
5
|
+
* This module is dynamically loaded only when mountedElemEmits is configured.
|
|
6
|
+
*/
|
|
7
|
+
export async function emitMountedElementEvents(
|
|
8
|
+
element: Element,
|
|
9
|
+
mountInit: MountInit,
|
|
10
|
+
processedEventsForElement: WeakMap<Element, Set<string>>
|
|
11
|
+
): Promise<void> {
|
|
12
|
+
const configs = Array.isArray(mountInit.mountedElemEmits)
|
|
13
|
+
? mountInit.mountedElemEmits
|
|
14
|
+
: [mountInit.mountedElemEmits!];
|
|
15
|
+
|
|
16
|
+
for (const config of configs) {
|
|
17
|
+
await emitSingleEvent(element, mountInit, config, processedEventsForElement);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function emitSingleEvent(
|
|
22
|
+
element: Element,
|
|
23
|
+
mountInit: MountInit,
|
|
24
|
+
config: EventConfig,
|
|
25
|
+
processedEventsForElement: WeakMap<Element, Set<string>>
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
// Check if this event should only fire once per element
|
|
28
|
+
if (config.oncePerMountedElement) {
|
|
29
|
+
const eventId = getEventId(config);
|
|
30
|
+
let processedEvents = processedEventsForElement.get(element);
|
|
31
|
+
|
|
32
|
+
if (!processedEvents) {
|
|
33
|
+
processedEvents = new Set<string>();
|
|
34
|
+
processedEventsForElement.set(element, processedEvents);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (processedEvents.has(eventId)) {
|
|
38
|
+
return; // Already emitted for this element
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
processedEvents.add(eventId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Resolve event constructor
|
|
45
|
+
const EventCtor = resolveEventConstructor(config.event);
|
|
46
|
+
|
|
47
|
+
// Process args with magic string substitution
|
|
48
|
+
const processedArgs = config.args !== undefined
|
|
49
|
+
? processMagicStrings(config.args, element, mountInit)
|
|
50
|
+
: undefined;
|
|
51
|
+
|
|
52
|
+
// Construct the event
|
|
53
|
+
let event: Event;
|
|
54
|
+
if (processedArgs === undefined) {
|
|
55
|
+
event = new EventCtor();
|
|
56
|
+
} else if (Array.isArray(processedArgs)) {
|
|
57
|
+
// For array args, ensure bubbles is set if second arg is an options object
|
|
58
|
+
if (processedArgs.length === 2 && typeof processedArgs[0] === 'string' && typeof processedArgs[1] === 'object' && processedArgs[1] !== null) {
|
|
59
|
+
// Merge bubbles: true into the options object if not already set
|
|
60
|
+
const options = { bubbles: true, ...processedArgs[1] };
|
|
61
|
+
event = new EventCtor(processedArgs[0], options);
|
|
62
|
+
} else {
|
|
63
|
+
event = new EventCtor(...processedArgs);
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
// Single arg - if it's a string (event name), add bubbles: true by default
|
|
67
|
+
if (typeof processedArgs === 'string') {
|
|
68
|
+
event = new EventCtor(processedArgs, { bubbles: true });
|
|
69
|
+
} else {
|
|
70
|
+
event = new EventCtor(processedArgs);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Apply eventProps if specified
|
|
75
|
+
if (config.eventProps) {
|
|
76
|
+
const { assignGingerly } = await import('assign-gingerly/index.js');
|
|
77
|
+
const processedProps = processMagicStrings(config.eventProps, element, mountInit);
|
|
78
|
+
assignGingerly(event, processedProps);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Dispatch the event from the mounted element
|
|
82
|
+
element.dispatchEvent(event);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveEventConstructor(event: string | EventConstructor): EventConstructor {
|
|
86
|
+
if (typeof event === 'string') {
|
|
87
|
+
const EventCtor = (globalThis as any)[event];
|
|
88
|
+
if (!EventCtor || typeof EventCtor !== 'function') {
|
|
89
|
+
throw new Error(`Event constructor "${event}" not found in globalThis`);
|
|
90
|
+
}
|
|
91
|
+
return EventCtor;
|
|
92
|
+
}
|
|
93
|
+
return event;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getEventId(config: EventConfig): string {
|
|
97
|
+
const eventName = typeof config.event === 'string' ? config.event : config.event.name;
|
|
98
|
+
const argsStr = JSON.stringify(config.args || '');
|
|
99
|
+
return `${eventName}:${argsStr}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function processMagicStrings(value: any, element: Element, mountInit: MountInit): any {
|
|
103
|
+
if (typeof value === 'string') {
|
|
104
|
+
if (value === '{{mountedElement}}') {
|
|
105
|
+
return element;
|
|
106
|
+
}
|
|
107
|
+
if (value === '{{mountInit}}') {
|
|
108
|
+
return mountInit;
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (Array.isArray(value)) {
|
|
114
|
+
return value.map(item => processMagicStrings(item, element, mountInit));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (value && typeof value === 'object') {
|
|
118
|
+
const processed: any = {};
|
|
119
|
+
for (const [key, val] of Object.entries(value)) {
|
|
120
|
+
processed[key] = processMagicStrings(val, element, mountInit);
|
|
121
|
+
}
|
|
122
|
+
return processed;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return value;
|
|
126
|
+
}
|
package/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// Main entry point for MountObserver v2
|
|
2
2
|
export { MountObserver } from './MountObserver.js';
|
|
3
|
+
export { whereOutside } from './whereOutside.js';
|
|
4
|
+
export { emitMountedElementEvents } from './emitEvents.js';
|
|
5
|
+
export { checkAttrChanges } from './attrChanges.js';
|
|
3
6
|
export type {
|
|
4
7
|
MountInit,
|
|
5
8
|
MountObserverOptions,
|
|
@@ -20,3 +23,4 @@ export {
|
|
|
20
23
|
mediamatchEventName,
|
|
21
24
|
mediaunmatchEventName
|
|
22
25
|
} from './Events.js';
|
|
26
|
+
|
package/mediaQuery.js
CHANGED
|
@@ -52,21 +52,24 @@ export function setupMediaQuery(init, rootNodeRef, mountedElements, modules, obs
|
|
|
52
52
|
rootNode
|
|
53
53
|
}
|
|
54
54
|
};
|
|
55
|
-
// Get all mounted elements
|
|
55
|
+
// Get all mounted elements from the WeakDual setWeak
|
|
56
56
|
const mountedElementsList = [];
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
mountedElementsList.push(element);
|
|
62
|
-
}
|
|
57
|
+
for (const ref of mountedElements.setWeak) {
|
|
58
|
+
const element = ref.deref();
|
|
59
|
+
if (element) {
|
|
60
|
+
mountedElementsList.push(element);
|
|
63
61
|
}
|
|
64
|
-
|
|
65
|
-
};
|
|
66
|
-
collectMountedElements(rootNode);
|
|
62
|
+
}
|
|
67
63
|
// Dismount each element
|
|
68
64
|
for (const element of mountedElementsList) {
|
|
69
|
-
|
|
65
|
+
// Remove from both structures
|
|
66
|
+
mountedElements.weakSet.delete(element);
|
|
67
|
+
for (const ref of mountedElements.setWeak) {
|
|
68
|
+
if (ref.deref() === element) {
|
|
69
|
+
mountedElements.setWeak.delete(ref);
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
70
73
|
// Call dismount callback
|
|
71
74
|
if (init.do && typeof init.do !== 'function' && init.do.dismount) {
|
|
72
75
|
init.do.dismount(element, context);
|
package/mediaQuery.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Media query handling for MountObserver
|
|
2
|
-
import type { MountInit, MountContext } from './types.js';
|
|
2
|
+
import type { MountInit, MountContext, WeakDual } from './types.js';
|
|
3
3
|
import { MediaMatchEvent, MediaUnmatchEvent, DismountEvent } from './Events.js';
|
|
4
4
|
|
|
5
5
|
export function setupMediaQuery(
|
|
6
6
|
init: MountInit,
|
|
7
7
|
rootNodeRef: WeakRef<Node>,
|
|
8
|
-
mountedElements:
|
|
8
|
+
mountedElements: WeakDual<Element>,
|
|
9
9
|
modules: any[],
|
|
10
10
|
observer: EventTarget,
|
|
11
11
|
processNode: (node: Node) => void
|
|
@@ -74,22 +74,25 @@ export function setupMediaQuery(
|
|
|
74
74
|
}
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
-
// Get all mounted elements
|
|
77
|
+
// Get all mounted elements from the WeakDual setWeak
|
|
78
78
|
const mountedElementsList: Element[] = [];
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
mountedElementsList.push(element);
|
|
84
|
-
}
|
|
79
|
+
for (const ref of mountedElements.setWeak) {
|
|
80
|
+
const element = ref.deref();
|
|
81
|
+
if (element) {
|
|
82
|
+
mountedElementsList.push(element);
|
|
85
83
|
}
|
|
86
|
-
|
|
87
|
-
};
|
|
88
|
-
collectMountedElements(rootNode);
|
|
84
|
+
}
|
|
89
85
|
|
|
90
86
|
// Dismount each element
|
|
91
87
|
for (const element of mountedElementsList) {
|
|
92
|
-
|
|
88
|
+
// Remove from both structures
|
|
89
|
+
mountedElements.weakSet.delete(element);
|
|
90
|
+
for (const ref of mountedElements.setWeak) {
|
|
91
|
+
if (ref.deref() === element) {
|
|
92
|
+
mountedElements.setWeak.delete(ref);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
93
96
|
|
|
94
97
|
// Call dismount callback
|
|
95
98
|
if (init.do && typeof init.do !== 'function' && init.do.dismount) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mount-observer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Observe and act on css matches.",
|
|
5
5
|
"main": "MountObserver.js",
|
|
6
6
|
"module": "MountObserver.js",
|
|
@@ -19,6 +19,18 @@
|
|
|
19
19
|
"./MountObserver.js": {
|
|
20
20
|
"default": "./MountObserver.js",
|
|
21
21
|
"types": "./MountObserver.ts"
|
|
22
|
+
},
|
|
23
|
+
"./whereOutside.js": {
|
|
24
|
+
"default": "./whereOutside.js",
|
|
25
|
+
"types": "./whereOutside.ts"
|
|
26
|
+
},
|
|
27
|
+
"./emitEvents.js": {
|
|
28
|
+
"default": "./emitEvents.js",
|
|
29
|
+
"types": "./emitEvents.ts"
|
|
30
|
+
},
|
|
31
|
+
"./attrChanges.js": {
|
|
32
|
+
"default": "./attrChanges.js",
|
|
33
|
+
"types": "./attrChanges.ts"
|
|
22
34
|
}
|
|
23
35
|
},
|
|
24
36
|
"files": [
|
package/types.d.ts
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
export type Constructor = new (...args: any[]) => any;
|
|
4
4
|
|
|
5
|
+
export type EventConstructor = {new(...args: any[]): Event};
|
|
6
|
+
|
|
7
|
+
export interface EventConfig {
|
|
8
|
+
event: string | EventConstructor;
|
|
9
|
+
args?: any | any[];
|
|
10
|
+
eventProps?: Record<string, any>;
|
|
11
|
+
oncePerMountedElement?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
export type DismountReason =
|
|
6
15
|
| 'media-query-failed'
|
|
7
16
|
| 'where-element-matches-failed';
|
|
@@ -11,12 +20,14 @@ export interface MountInit {
|
|
|
11
20
|
whereAttr?: WhereAttr;
|
|
12
21
|
whereInstanceOf?: Constructor | Constructor[];
|
|
13
22
|
whereMediaMatches?: string | MediaQueryList;
|
|
23
|
+
whereOutside?: string;
|
|
14
24
|
import?: string | ImportSpec | Array<string | ImportSpec>;
|
|
15
25
|
do?: DoCallback | DoCallbacks;
|
|
16
26
|
loadingEagerness?: 'eager' | 'lazy';
|
|
17
27
|
assignGingerly?: Record<string, any>;
|
|
18
28
|
map?: MapConfig;
|
|
19
29
|
getPlayByPlay?: boolean;
|
|
30
|
+
mountedElemEmits?: EventConfig | EventConfig[];
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
export interface MapConfig {
|
|
@@ -71,10 +82,16 @@ export interface MountObserverOptions {
|
|
|
71
82
|
disconnectedSignal?: AbortSignal;
|
|
72
83
|
}
|
|
73
84
|
|
|
85
|
+
export interface WeakDual<T extends Object>{
|
|
86
|
+
weakSet: WeakSet<T>,
|
|
87
|
+
setWeak: Set<WeakRef<T>>
|
|
88
|
+
}
|
|
89
|
+
|
|
74
90
|
export interface IMountObserver extends EventTarget {
|
|
75
91
|
observe(rootNode: Node): Promise<void>;
|
|
76
92
|
disconnect(): void;
|
|
77
93
|
disconnectedSignal: AbortSignal;
|
|
94
|
+
assignGingerly(config: Record<string, any> | undefined): Promise<void>;
|
|
78
95
|
}
|
|
79
96
|
|
|
80
97
|
export interface IMountEvent extends Event {
|
package/whereOutside.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if an element is outside (not inside) any ancestor matching a selector.
|
|
3
|
+
* This implements "donut hole" scoping.
|
|
4
|
+
*
|
|
5
|
+
* @param rootNode - The root node to stop traversal at (not checked against selector)
|
|
6
|
+
* @param matchCandidate - The element to check
|
|
7
|
+
* @param outside - CSS selector for excluding ancestors
|
|
8
|
+
* @returns true if element is outside all matching ancestors, false otherwise
|
|
9
|
+
*/
|
|
10
|
+
export function whereOutside(rootNode, matchCandidate, outside) {
|
|
11
|
+
let current = matchCandidate.parentElement;
|
|
12
|
+
while (current && current !== rootNode) {
|
|
13
|
+
if (current.matches(outside)) {
|
|
14
|
+
return false; // Found an excluding ancestor
|
|
15
|
+
}
|
|
16
|
+
current = current.parentElement;
|
|
17
|
+
}
|
|
18
|
+
return true; // No excluding ancestors found
|
|
19
|
+
}
|
package/whereOutside.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if an element is outside (not inside) any ancestor matching a selector.
|
|
3
|
+
* This implements "donut hole" scoping.
|
|
4
|
+
*
|
|
5
|
+
* @param rootNode - The root node to stop traversal at (not checked against selector)
|
|
6
|
+
* @param matchCandidate - The element to check
|
|
7
|
+
* @param outside - CSS selector for excluding ancestors
|
|
8
|
+
* @returns true if element is outside all matching ancestors, false otherwise
|
|
9
|
+
*/
|
|
10
|
+
export function whereOutside(
|
|
11
|
+
rootNode: Node,
|
|
12
|
+
matchCandidate: Element,
|
|
13
|
+
outside: string
|
|
14
|
+
): boolean {
|
|
15
|
+
let current = matchCandidate.parentElement;
|
|
16
|
+
|
|
17
|
+
while (current && current !== rootNode) {
|
|
18
|
+
if (current.matches(outside)) {
|
|
19
|
+
return false; // Found an excluding ancestor
|
|
20
|
+
}
|
|
21
|
+
current = current.parentElement;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return true; // No excluding ancestors found
|
|
25
|
+
}
|