@excom/dom-observer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.rush/temp/chunked-rush-logs/dom-observer.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/dom-observer.build_docs.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/dom-observer.build_package-metas.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/operation/build_docs/all.log +1 -0
- package/.rush/temp/operation/build_docs/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_docs/state.json +3 -0
- package/.rush/temp/operation/build_package-metas/all.log +1 -0
- package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_package-metas/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +3 -0
- package/config/rig.json +5 -0
- package/dom-observer.ts +186 -0
- package/index.ts +17 -0
- package/package.json +45 -0
- package/rush-logs/dom-observer.apply-exports.cache.log +1 -0
- package/rush-logs/dom-observer.apply-exports.log +1 -0
- package/rush-logs/dom-observer.build_docs.cache.log +1 -0
- package/rush-logs/dom-observer.build_docs.log +1 -0
- package/rush-logs/dom-observer.build_package-metas.cache.log +1 -0
- package/rush-logs/dom-observer.build_package-metas.log +1 -0
- package/support/custom-elements.json +134 -0
- package/support/demos/simple.html +18 -0
- package/support/dist-docs/dom-observer.md +117 -0
- package/support/docs/INTERNAL.md +1 -0
- package/support/docs/README.md +59 -0
- package/support/package-meta.json +90 -0
- package/support/tests/__snapshots__/simple.view.test.ts.snap +23 -0
- package/support/tests/dom-observer.test.ts +326 -0
- package/support/tests/simple.view.test.ts +40 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
|
package/config/rig.json
ADDED
package/dom-observer.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { LoopGuard, selectOne } from "@excom/kit-utils";
|
|
2
|
+
import { Neutron, TEvent } from "@excom/neutron";
|
|
3
|
+
|
|
4
|
+
export type DomObserverChangeDetail = {
|
|
5
|
+
target: Element;
|
|
6
|
+
mutations: MutationRecord[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type DomObserverChangeEvent = TEvent & {
|
|
10
|
+
type: "dom-observer-change";
|
|
11
|
+
detail: DomObserverChangeDetail;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolves a target element via a CSS selector and fires a
|
|
16
|
+
* `dom-observer-change` event whenever that element mutates. Renders
|
|
17
|
+
* nothing of its own — pair it with `<event-handler>` (or Quark) to
|
|
18
|
+
* react to the changes.
|
|
19
|
+
*
|
|
20
|
+
* If `target-ref` does not match anything when the element connects,
|
|
21
|
+
* a document-level `MutationObserver` waits for a matching element to
|
|
22
|
+
* appear and then switches over to observing it.
|
|
23
|
+
*
|
|
24
|
+
* When the target is an `HTMLTemplateElement`, the `<template>` itself
|
|
25
|
+
* is observed (attribute/child mutations on the element), AND so is the
|
|
26
|
+
* template's `.content` `DocumentFragment` — otherwise mutations to the
|
|
27
|
+
* authored template children would be invisible (they don't live as
|
|
28
|
+
* descendants of the `<template>` element in the DOM tree).
|
|
29
|
+
*
|
|
30
|
+
* @fires dom-observer-change - Fires whenever the resolved target mutates,
|
|
31
|
+
* and once immediately (with `mutations: []`) as soon as the target is
|
|
32
|
+
* resolved so listeners can seed from current state. `mutations` is
|
|
33
|
+
* the `MutationRecord[]` from the underlying `MutationObserver`
|
|
34
|
+
* callback (empty on that first fire).
|
|
35
|
+
* @type DomObserverChangeEvent
|
|
36
|
+
*
|
|
37
|
+
* @example Log every mutation on a specific element
|
|
38
|
+
* <event-handler listen-for="dom-observer-change" target-ref="window">
|
|
39
|
+
* <dom-observer target-ref="#watched"></dom-observer>
|
|
40
|
+
* </event-handler>
|
|
41
|
+
*/
|
|
42
|
+
export const DomObserver = Neutron({
|
|
43
|
+
tag: "dom-observer",
|
|
44
|
+
events: {
|
|
45
|
+
change: { prefixWithTag: true },
|
|
46
|
+
},
|
|
47
|
+
props: {
|
|
48
|
+
/**
|
|
49
|
+
* @option
|
|
50
|
+
* CSS selector used to resolve the element to observe. Resolved
|
|
51
|
+
* against `document`. If no element matches at connect time, the
|
|
52
|
+
* element waits for one to appear.
|
|
53
|
+
*/
|
|
54
|
+
targetRef: String,
|
|
55
|
+
/**
|
|
56
|
+
* @state
|
|
57
|
+
* The currently observed target element (if any).
|
|
58
|
+
*/
|
|
59
|
+
targetElement: HTMLElement,
|
|
60
|
+
/**
|
|
61
|
+
* @state
|
|
62
|
+
* Document-level observer used to wait for a target matching
|
|
63
|
+
* `target-ref` to appear. Disconnected as soon as the target is
|
|
64
|
+
* found.
|
|
65
|
+
*/
|
|
66
|
+
targetFindingObserver: MutationObserver,
|
|
67
|
+
/**
|
|
68
|
+
* @state
|
|
69
|
+
* Observer attached to the resolved `targetElement` (and to its
|
|
70
|
+
* `.content` fragment when the target is a `<template>`).
|
|
71
|
+
*/
|
|
72
|
+
targetChangeObserver: MutationObserver,
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
.defineMethods({
|
|
76
|
+
findTargetElement: (element) => {
|
|
77
|
+
// @ts-ignore TODO defineMethods
|
|
78
|
+
const { targetRef, setTargetElement } = element;
|
|
79
|
+
if (!targetRef) return;
|
|
80
|
+
const targetElement = selectOne(targetRef, {
|
|
81
|
+
scope: element,
|
|
82
|
+
});
|
|
83
|
+
if (targetElement) {
|
|
84
|
+
return { targetElement };
|
|
85
|
+
}
|
|
86
|
+
/* Wait for a match. `MutationRecord.target` on a childList mutation
|
|
87
|
+
is the *parent*, not the inserted child, so scan the added nodes
|
|
88
|
+
(and their descendants). */
|
|
89
|
+
const targetFindingObserver = new MutationObserver((mutations) => {
|
|
90
|
+
for (const mutation of mutations) {
|
|
91
|
+
for (const node of mutation.addedNodes) {
|
|
92
|
+
if (!(node instanceof Element)) continue;
|
|
93
|
+
const match = node.matches(targetRef)
|
|
94
|
+
? node
|
|
95
|
+
: node.querySelector(targetRef);
|
|
96
|
+
if (match) {
|
|
97
|
+
setTargetElement(match);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
targetFindingObserver.observe(document.body, {
|
|
104
|
+
childList: true,
|
|
105
|
+
subtree: true,
|
|
106
|
+
});
|
|
107
|
+
return { targetFindingObserver };
|
|
108
|
+
},
|
|
109
|
+
setTargetElement: ({ targetFindingObserver }, targetElement) => {
|
|
110
|
+
targetFindingObserver?.disconnect();
|
|
111
|
+
return {
|
|
112
|
+
targetElement,
|
|
113
|
+
targetFindingObserver: null,
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
notifyChange: ({ targetElement }, mutations: MutationRecord[] = []) => ({
|
|
117
|
+
emit: ["change", { detail: { target: targetElement, mutations } }],
|
|
118
|
+
}),
|
|
119
|
+
})
|
|
120
|
+
.onPropChanged(
|
|
121
|
+
"targetRef",
|
|
122
|
+
({ targetRef, targetFindingObserver }, previous) => {
|
|
123
|
+
const shouldDisconnect = previous.targetRef && targetFindingObserver;
|
|
124
|
+
if (shouldDisconnect) {
|
|
125
|
+
targetFindingObserver.disconnect();
|
|
126
|
+
}
|
|
127
|
+
return [
|
|
128
|
+
shouldDisconnect && { targetFindingObserver: null },
|
|
129
|
+
targetRef && { findTargetElement: [] },
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
.onPropChanged(
|
|
134
|
+
"targetElement",
|
|
135
|
+
({ targetElement, targetChangeObserver, notifyChange }, previous) => {
|
|
136
|
+
if (previous.targetElement && targetChangeObserver) {
|
|
137
|
+
targetChangeObserver.disconnect();
|
|
138
|
+
if (!targetElement) return { targetChangeObserver: null };
|
|
139
|
+
}
|
|
140
|
+
if (targetElement) {
|
|
141
|
+
/* The change event continues the chain that made the mutation
|
|
142
|
+
(a Quark rule writing an attribute, an effect inserting
|
|
143
|
+
children): inherit the loop-guard depth on those addresses. */
|
|
144
|
+
const observer = new MutationObserver((mutations) =>
|
|
145
|
+
LoopGuard.run(
|
|
146
|
+
Math.max(
|
|
147
|
+
0,
|
|
148
|
+
...mutations.map((m) =>
|
|
149
|
+
LoopGuard.depthOf(m.target, m.attributeName ?? "content")
|
|
150
|
+
)
|
|
151
|
+
),
|
|
152
|
+
() => notifyChange(mutations)
|
|
153
|
+
)
|
|
154
|
+
);
|
|
155
|
+
const init: MutationObserverInit = {
|
|
156
|
+
attributes: true,
|
|
157
|
+
childList: true,
|
|
158
|
+
characterData: true,
|
|
159
|
+
subtree: true,
|
|
160
|
+
};
|
|
161
|
+
observer.observe(targetElement, init);
|
|
162
|
+
/* A `<template>`'s authored content lives on `.content`, not as
|
|
163
|
+
DOM descendants. Observe both so either mutation is one stream. */
|
|
164
|
+
if (targetElement instanceof HTMLTemplateElement) {
|
|
165
|
+
observer.observe(targetElement.content, init);
|
|
166
|
+
}
|
|
167
|
+
return [{ targetChangeObserver: observer }, { notifyChange: [] }];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
.onDisconnected(
|
|
172
|
+
({ isMoving, targetChangeObserver, targetFindingObserver }) => {
|
|
173
|
+
if (isMoving) return;
|
|
174
|
+
targetChangeObserver?.disconnect();
|
|
175
|
+
targetFindingObserver?.disconnect();
|
|
176
|
+
return {
|
|
177
|
+
targetElement: null,
|
|
178
|
+
targetChangeObserver: null,
|
|
179
|
+
targetFindingObserver: null,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
.onConnected(({ isMoving, wasMounted, findTargetElement }) => {
|
|
184
|
+
if (isMoving || !wasMounted) return;
|
|
185
|
+
findTargetElement();
|
|
186
|
+
});
|
package/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DomObserver } from "./dom-observer";
|
|
2
|
+
|
|
3
|
+
DomObserver.define();
|
|
4
|
+
|
|
5
|
+
export { DomObserver };
|
|
6
|
+
|
|
7
|
+
type T_HTMLDomObserverElement = typeof DomObserver.CustomElement;
|
|
8
|
+
declare global {
|
|
9
|
+
interface HTMLDomObserverElement extends T_HTMLDomObserverElement {}
|
|
10
|
+
interface Window {
|
|
11
|
+
HTMLDomObserverElement: HTMLDomObserverElement;
|
|
12
|
+
}
|
|
13
|
+
interface HTMLElementTagNameMap {
|
|
14
|
+
"dom-observer": HTMLDomObserverElement;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export type { HTMLDomObserverElement };
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@excom/dom-observer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "<dom-observer> custom element that resolves a target via a CSS selector and fires events on its mutations.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=24.13.0"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@excom/neutron": "^0.1.0",
|
|
12
|
+
"@excom/kit-utils": "^0.1.0"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@excom/heft-rig": "^0.1.0"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"url": "excom-dev/nucleus",
|
|
20
|
+
"directory": "packages/dom-observer"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/excom-dev/nucleus/tree/main/packages/dom-observer/support/docs/README.md",
|
|
23
|
+
"bugs": "https://github.com/excom-dev/nucleus/issues",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"dom-observer",
|
|
26
|
+
"neutron",
|
|
27
|
+
"custom-elements",
|
|
28
|
+
"mutation-observer"
|
|
29
|
+
],
|
|
30
|
+
"excom": {
|
|
31
|
+
"documented": true,
|
|
32
|
+
"packageType": "kit-element"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "node node_modules/@excom/heft-rig/scripts/vite-build.mjs",
|
|
36
|
+
"build:watch": "node node_modules/@excom/heft-rig/scripts/vite-build-watch.mjs",
|
|
37
|
+
"format": "node node_modules/@excom/heft-rig/scripts/format.mjs",
|
|
38
|
+
"test": "node node_modules/@excom/heft-rig/scripts/vitest.mjs",
|
|
39
|
+
"coverage": "node node_modules/@excom/heft-rig/scripts/coverage.mjs",
|
|
40
|
+
"dev": "node node_modules/@excom/heft-rig/scripts/vite-dev.mjs",
|
|
41
|
+
"preview": "node node_modules/@excom/heft-rig/scripts/vite-preview.mjs",
|
|
42
|
+
"build:package-metas": "node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs",
|
|
43
|
+
"build:docs": "node node_modules/@excom/heft-rig/scripts/build-docs.mjs"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Caching has been disabled for this project's "apply-exports" command.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This project does not define the caching behavior of the "build:docs" command, so caching has been disabled.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This project does not define the caching behavior of the "build:package-metas" command, so caching has been disabled.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0.0",
|
|
3
|
+
"modules": [
|
|
4
|
+
{
|
|
5
|
+
"kind": "javascript-module",
|
|
6
|
+
"path": "dom-observer.ts",
|
|
7
|
+
"declarations": [
|
|
8
|
+
{
|
|
9
|
+
"kind": "class",
|
|
10
|
+
"name": "DomObserver",
|
|
11
|
+
"customElement": true,
|
|
12
|
+
"tagName": "dom-observer",
|
|
13
|
+
"description": "Resolves a target element via a CSS selector and fires a `dom-observer-change` event whenever that element mutates. Renders nothing of its own — pair it with `<event-handler>` (or Quark) to react to the changes. If `target-ref` does not match anything when the element connects, a document-level `MutationObserver` waits for a matching element to appear and then switches over to observing it. When the target is an `HTMLTemplateElement`, the `<template>` itself is observed (attribute/child mutations on the element), AND so is the template's `.content` `DocumentFragment` — otherwise mutations to the authored template children would be invisible (they don't live as descendants of the `<template>` element in the DOM tree).",
|
|
14
|
+
"attributes": [
|
|
15
|
+
{
|
|
16
|
+
"name": "target-ref",
|
|
17
|
+
"type": {
|
|
18
|
+
"text": "string"
|
|
19
|
+
},
|
|
20
|
+
"description": "CSS selector used to resolve the element to observe. Resolved against `document`. If no element matches at connect time, the element waits for one to appear.",
|
|
21
|
+
"fieldName": "targetRef"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "target-element",
|
|
25
|
+
"type": {
|
|
26
|
+
"text": "HTMLElement"
|
|
27
|
+
},
|
|
28
|
+
"description": "The currently observed target element (if any).",
|
|
29
|
+
"fieldName": "targetElement"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "target-finding-observer",
|
|
33
|
+
"type": {
|
|
34
|
+
"text": "MutationObserver"
|
|
35
|
+
},
|
|
36
|
+
"description": "Document-level observer used to wait for a target matching `target-ref` to appear. Disconnected as soon as the target is found.",
|
|
37
|
+
"fieldName": "targetFindingObserver"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "target-change-observer",
|
|
41
|
+
"type": {
|
|
42
|
+
"text": "MutationObserver"
|
|
43
|
+
},
|
|
44
|
+
"description": "Observer attached to the resolved `targetElement` (and to its `.content` fragment when the target is a `<template>`).",
|
|
45
|
+
"fieldName": "targetChangeObserver"
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"members": [
|
|
49
|
+
{
|
|
50
|
+
"kind": "field",
|
|
51
|
+
"name": "targetRef",
|
|
52
|
+
"type": {
|
|
53
|
+
"text": "string"
|
|
54
|
+
},
|
|
55
|
+
"privacy": "public",
|
|
56
|
+
"readonly": false,
|
|
57
|
+
"description": "CSS selector used to resolve the element to observe. Resolved against `document`. If no element matches at connect time, the element waits for one to appear.",
|
|
58
|
+
"_neutron": {
|
|
59
|
+
"surface": "option"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"kind": "field",
|
|
64
|
+
"name": "targetElement",
|
|
65
|
+
"type": {
|
|
66
|
+
"text": "HTMLElement"
|
|
67
|
+
},
|
|
68
|
+
"privacy": "public",
|
|
69
|
+
"readonly": true,
|
|
70
|
+
"description": "The currently observed target element (if any).",
|
|
71
|
+
"_neutron": {
|
|
72
|
+
"surface": "state"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"kind": "field",
|
|
77
|
+
"name": "targetFindingObserver",
|
|
78
|
+
"type": {
|
|
79
|
+
"text": "MutationObserver"
|
|
80
|
+
},
|
|
81
|
+
"privacy": "public",
|
|
82
|
+
"readonly": true,
|
|
83
|
+
"description": "Document-level observer used to wait for a target matching `target-ref` to appear. Disconnected as soon as the target is found.",
|
|
84
|
+
"_neutron": {
|
|
85
|
+
"surface": "state"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"kind": "field",
|
|
90
|
+
"name": "targetChangeObserver",
|
|
91
|
+
"type": {
|
|
92
|
+
"text": "MutationObserver"
|
|
93
|
+
},
|
|
94
|
+
"privacy": "public",
|
|
95
|
+
"readonly": true,
|
|
96
|
+
"description": "Observer attached to the resolved `targetElement` (and to its `.content` fragment when the target is a `<template>`).",
|
|
97
|
+
"_neutron": {
|
|
98
|
+
"surface": "state"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
"events": [
|
|
103
|
+
{
|
|
104
|
+
"name": "dom-observer-change",
|
|
105
|
+
"description": "Fires whenever the resolved target mutates, and once immediately (with `mutations: []`) as soon as the target is resolved so listeners can seed from current state. `mutations` is the `MutationRecord[]` from the underlying `MutationObserver` callback (empty on that first fire).",
|
|
106
|
+
"type": {
|
|
107
|
+
"text": "DomObserverChangeEvent",
|
|
108
|
+
"expanded": "CustomEvent & { type: \"dom-observer-change\"; detail: { target: Element; mutations: MutationRecord[]; }; bubbles: true; cancelable: true; composed: true }"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
"exports": [
|
|
115
|
+
{
|
|
116
|
+
"kind": "js",
|
|
117
|
+
"name": "DomObserver",
|
|
118
|
+
"declaration": {
|
|
119
|
+
"name": "DomObserver",
|
|
120
|
+
"module": "dom-observer.ts"
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"kind": "custom-element-definition",
|
|
125
|
+
"name": "dom-observer",
|
|
126
|
+
"declaration": {
|
|
127
|
+
"name": "DomObserver",
|
|
128
|
+
"module": "dom-observer.ts"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<div>
|
|
2
|
+
<section>
|
|
3
|
+
<dom-observer target-ref=":scope + details"></dom-observer>
|
|
4
|
+
<details id="do-watched">
|
|
5
|
+
<summary>Toggle me — mutates the "open" attribute</summary>
|
|
6
|
+
<p>Toggling this
|
|
7
|
+
fired <code>dom-observer-change</code>.</p>
|
|
8
|
+
</details>
|
|
9
|
+
<output class="tag-pre" style="white-space: pre;"></output>
|
|
10
|
+
</section>
|
|
11
|
+
<quark-sheet>
|
|
12
|
+
@use "/demo-utils" as *;
|
|
13
|
+
|
|
14
|
+
section {
|
|
15
|
+
@on dom-observer-change (handle: appendOutputFromDetail);
|
|
16
|
+
}
|
|
17
|
+
</quark-sheet>
|
|
18
|
+
</div>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# dom-observer
|
|
2
|
+
|
|
3
|
+
Fire an event whenever a configured element mutates.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
```html
|
|
7
|
+
<div>
|
|
8
|
+
<section>
|
|
9
|
+
<dom-observer target-ref=":scope + details"></dom-observer>
|
|
10
|
+
<details id="do-watched">
|
|
11
|
+
<summary>Toggle me — mutates the "open" attribute</summary>
|
|
12
|
+
<p>Toggling this
|
|
13
|
+
fired <code>dom-observer-change</code>.</p>
|
|
14
|
+
</details>
|
|
15
|
+
<output class="tag-pre" style="white-space: pre;"></output>
|
|
16
|
+
</section>
|
|
17
|
+
<quark-sheet>
|
|
18
|
+
@use "/demo-utils" as *;
|
|
19
|
+
|
|
20
|
+
section {
|
|
21
|
+
@on dom-observer-change (handle: appendOutputFromDetail);
|
|
22
|
+
}
|
|
23
|
+
</quark-sheet>
|
|
24
|
+
</div>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
- **Mutation events** Fires `dom-observer-change` on target changes
|
|
31
|
+
- **Selector-based** `target-ref` resolves any element, anywhere
|
|
32
|
+
- **Waits for its target** No matching element yet? It watches for one
|
|
33
|
+
- **Fires once immediately** An empty-`mutations` fire on resolve lets
|
|
34
|
+
listeners seed from current state
|
|
35
|
+
- **`<template>`-aware** Also observes a template's `.content` fragment
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
`@excom/dom-observer` v0.1.0
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pnpm add @excom/dom-observer
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install @excom/dom-observer
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
yarn add @excom/dom-observer
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Import
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import "@excom/dom-observer";
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
Point `target-ref` at any selector, then react to `dom-observer-change`
|
|
65
|
+
with `<event-handler>` (or Quark).
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<dom-observer target-ref="#watched"></dom-observer>
|
|
69
|
+
<event-handler listen-for="dom-observer-change" target-ref="#log">
|
|
70
|
+
<!-- runs on every #watched mutation, and once on attach -->
|
|
71
|
+
</event-handler>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### API Reference
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
#### Attributes
|
|
78
|
+
|
|
79
|
+
| Name | Surface | Type | Default | Values | Description |
|
|
80
|
+
| --- | --- | --- | --- | --- | --- |
|
|
81
|
+
| `target-ref` | option | `string` | | | CSS selector used to resolve the element to observe. Resolved against `document`. If no element matches at connect time, the element waits for one to appear. |
|
|
82
|
+
| `target-element` | state | `HTMLElement` | | | The currently observed target element (if any). |
|
|
83
|
+
| `target-finding-observer` | state | `MutationObserver` | | | Document-level observer used to wait for a target matching `target-ref` to appear. Disconnected as soon as the target is found. |
|
|
84
|
+
| `target-change-observer` | state | `MutationObserver` | | | Observer attached to the resolved `targetElement` (and to its `.content` fragment when the target is a `<template>`). |
|
|
85
|
+
|
|
86
|
+
#### Fires
|
|
87
|
+
|
|
88
|
+
| Name | Type | Description |
|
|
89
|
+
| --- | --- | --- |
|
|
90
|
+
| `dom-observer-change` | `DomObserverChangeEvent` (`CustomEvent & { type: "dom-observer-change"; detail: { target: Element; mutations: MutationRecord[]; }; bubbles: true; cancelable: true; composed: true }`) | Fires whenever the resolved target mutates, and once immediately (with `mutations: []`) as soon as the target is resolved so listeners can seed from current state. `mutations` is the `MutationRecord[]` from the underlying `MutationObserver` callback (empty on that first fire). |
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
### Examples
|
|
95
|
+
|
|
96
|
+
#### Waiting for the target to exist
|
|
97
|
+
|
|
98
|
+
If nothing matches `target-ref` at connect time, `<dom-observer>` watches
|
|
99
|
+
the document for a match and switches over automatically — no glue code:
|
|
100
|
+
|
|
101
|
+
```html
|
|
102
|
+
<dom-observer target-ref="article#late"></dom-observer>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### Observing a `<template>`
|
|
106
|
+
|
|
107
|
+
A `<template>`'s authored content lives on its `.content`
|
|
108
|
+
`DocumentFragment`, not as DOM descendants of the `<template>` itself.
|
|
109
|
+
`<dom-observer>` observes both, so mutations to either surface through the
|
|
110
|
+
same event stream:
|
|
111
|
+
|
|
112
|
+
```html
|
|
113
|
+
<template id="rows">
|
|
114
|
+
<li>seed</li>
|
|
115
|
+
</template>
|
|
116
|
+
<dom-observer target-ref="#rows"></dom-observer>
|
|
117
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- Consider moving `findTargetElement` and related logic to its own Base element or common util. Could be useful for other elements that use `x-ref`s to watch for the desired element if it's not in the document yet.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# dom-observer
|
|
2
|
+
|
|
3
|
+
Fire an event whenever a configured element mutates.
|
|
4
|
+
|
|
5
|
+
<include-content data-demo="simple"></include-content>
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Mutation events** Fires `dom-observer-change` on target changes
|
|
10
|
+
- **Selector-based** `target-ref` resolves any element, anywhere
|
|
11
|
+
- **Waits for its target** No matching element yet? It watches for one
|
|
12
|
+
- **Fires once immediately** An empty-`mutations` fire on resolve lets
|
|
13
|
+
listeners seed from current state
|
|
14
|
+
- **`<template>`-aware** Also observes a template's `.content` fragment
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
<include-content is-active template-ref="/views/install-section/install-section.html"></include-content>
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Point `target-ref` at any selector, then react to `dom-observer-change`
|
|
23
|
+
with `<event-handler>` (or Quark).
|
|
24
|
+
|
|
25
|
+
```html
|
|
26
|
+
<dom-observer target-ref="#watched"></dom-observer>
|
|
27
|
+
<event-handler listen-for="dom-observer-change" target-ref="#log">
|
|
28
|
+
<!-- runs on every #watched mutation, and once on attach -->
|
|
29
|
+
</event-handler>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### API Reference
|
|
33
|
+
|
|
34
|
+
<include-content is-active template-ref="/views/api-reference/api-reference.html"></include-content>
|
|
35
|
+
|
|
36
|
+
### Examples
|
|
37
|
+
|
|
38
|
+
#### Waiting for the target to exist
|
|
39
|
+
|
|
40
|
+
If nothing matches `target-ref` at connect time, `<dom-observer>` watches
|
|
41
|
+
the document for a match and switches over automatically — no glue code:
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<dom-observer target-ref="article#late"></dom-observer>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
#### Observing a `<template>`
|
|
48
|
+
|
|
49
|
+
A `<template>`'s authored content lives on its `.content`
|
|
50
|
+
`DocumentFragment`, not as DOM descendants of the `<template>` itself.
|
|
51
|
+
`<dom-observer>` observes both, so mutations to either surface through the
|
|
52
|
+
same event stream:
|
|
53
|
+
|
|
54
|
+
```html
|
|
55
|
+
<template id="rows">
|
|
56
|
+
<li>seed</li>
|
|
57
|
+
</template>
|
|
58
|
+
<dom-observer target-ref="#rows"></dom-observer>
|
|
59
|
+
```
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"shortName": "dom-observer",
|
|
3
|
+
"package": {
|
|
4
|
+
"name": "@excom/dom-observer",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"description": "<dom-observer> custom element that resolves a target via a CSS selector and fires events on its mutations.",
|
|
7
|
+
"peerDependencies": {},
|
|
8
|
+
"excom": {
|
|
9
|
+
"documented": true,
|
|
10
|
+
"packageType": "kit-element"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"demos": {
|
|
14
|
+
"simple": "<div>\n <section>\n <dom-observer target-ref=\":scope + details\"></dom-observer>\n <details id=\"do-watched\">\n <summary>Toggle me — mutates the \"open\" attribute</summary>\n <p>Toggling this\n fired <code>dom-observer-change</code>.</p>\n </details>\n <output class=\"tag-pre\" style=\"white-space: pre;\"></output>\n </section>\n <quark-sheet>\n @use \"/demo-utils\" as *;\n\n section {\n @on dom-observer-change (handle: appendOutputFromDetail);\n }\n </quark-sheet>\n</div>"
|
|
15
|
+
},
|
|
16
|
+
"readme": "<h1 id=\"md-dom-observer\">dom-observer</h1>\n<p>Fire an event whenever a configured element mutates.</p>\n<p><include-content data-demo=\"simple\"></include-content></p>\n<h2 id=\"md-features\">Features</h2>\n<ul>\n<li><strong>Mutation events</strong> Fires <code>dom-observer-change</code> on target changes</li>\n<li><strong>Selector-based</strong> <code>target-ref</code> resolves any element, anywhere</li>\n<li><strong>Waits for its target</strong> No matching element yet? It watches for one</li>\n<li><strong>Fires once immediately</strong> An empty-<code>mutations</code> fire on resolve lets\nlisteners seed from current state</li>\n<li><strong><code><template></code>-aware</strong> Also observes a template's <code>.content</code> fragment</li>\n</ul>\n<h2 id=\"md-installation\">Installation</h2>\n<p><include-content is-active template-ref=\"/views/install-section/install-section.html\"></include-content></p>\n<h2 id=\"md-usage\">Usage</h2>\n<p>Point <code>target-ref</code> at any selector, then react to <code>dom-observer-change</code>\nwith <code><event-handler></code> (or Quark).</p>\n<include-content data-language=\"html\"><template><dom-observer target-ref=\"#watched\"></dom-observer>\n<event-handler listen-for=\"dom-observer-change\" target-ref=\"#log\">\n <!-- runs on every #watched mutation, and once on attach -->\n</event-handler></template></include-content>\n<h3 id=\"md-api-reference\">API Reference</h3>\n<p><include-content is-active template-ref=\"/views/api-reference/api-reference.html\"></include-content></p>\n<h3 id=\"md-examples\">Examples</h3>\n<h4 id=\"md-waiting-for-the-target-to-exist\">Waiting for the target to exist</h4>\n<p>If nothing matches <code>target-ref</code> at connect time, <code><dom-observer></code> watches\nthe document for a match and switches over automatically — no glue code:</p>\n<include-content data-language=\"html\"><template><dom-observer target-ref=\"article#late\"></dom-observer></template></include-content>\n<h4 id=\"md-observing-a-lt-template-gt\">Observing a <code><template></code></h4>\n<p>A <code><template></code>'s authored content lives on its <code>.content</code>\n<code>DocumentFragment</code>, not as DOM descendants of the <code><template></code> itself.\n<code><dom-observer></code> observes both, so mutations to either surface through the\nsame event stream:</p>\n<include-content data-language=\"html\"><template><template id=\"rows\">\n <li>seed</li>\n</template>\n<dom-observer target-ref=\"#rows\"></dom-observer></template></include-content>\n",
|
|
17
|
+
"docs": {
|
|
18
|
+
"readme": "<h1 id=\"md-dom-observer\">dom-observer</h1>\n<p>Fire an event whenever a configured element mutates.</p>\n<p><include-content data-demo=\"simple\"></include-content></p>\n<h2 id=\"md-features\">Features</h2>\n<ul>\n<li><strong>Mutation events</strong> Fires <code>dom-observer-change</code> on target changes</li>\n<li><strong>Selector-based</strong> <code>target-ref</code> resolves any element, anywhere</li>\n<li><strong>Waits for its target</strong> No matching element yet? It watches for one</li>\n<li><strong>Fires once immediately</strong> An empty-<code>mutations</code> fire on resolve lets\nlisteners seed from current state</li>\n<li><strong><code><template></code>-aware</strong> Also observes a template's <code>.content</code> fragment</li>\n</ul>\n<h2 id=\"md-installation\">Installation</h2>\n<p><include-content is-active template-ref=\"/views/install-section/install-section.html\"></include-content></p>\n<h2 id=\"md-usage\">Usage</h2>\n<p>Point <code>target-ref</code> at any selector, then react to <code>dom-observer-change</code>\nwith <code><event-handler></code> (or Quark).</p>\n<include-content data-language=\"html\"><template><dom-observer target-ref=\"#watched\"></dom-observer>\n<event-handler listen-for=\"dom-observer-change\" target-ref=\"#log\">\n <!-- runs on every #watched mutation, and once on attach -->\n</event-handler></template></include-content>\n<h3 id=\"md-api-reference\">API Reference</h3>\n<p><include-content is-active template-ref=\"/views/api-reference/api-reference.html\"></include-content></p>\n<h3 id=\"md-examples\">Examples</h3>\n<h4 id=\"md-waiting-for-the-target-to-exist\">Waiting for the target to exist</h4>\n<p>If nothing matches <code>target-ref</code> at connect time, <code><dom-observer></code> watches\nthe document for a match and switches over automatically — no glue code:</p>\n<include-content data-language=\"html\"><template><dom-observer target-ref=\"article#late\"></dom-observer></template></include-content>\n<h4 id=\"md-observing-a-lt-template-gt\">Observing a <code><template></code></h4>\n<p>A <code><template></code>'s authored content lives on its <code>.content</code>\n<code>DocumentFragment</code>, not as DOM descendants of the <code><template></code> itself.\n<code><dom-observer></code> observes both, so mutations to either surface through the\nsame event stream:</p>\n<include-content data-language=\"html\"><template><template id=\"rows\">\n <li>seed</li>\n</template>\n<dom-observer target-ref=\"#rows\"></dom-observer></template></include-content>\n"
|
|
19
|
+
},
|
|
20
|
+
"installation": {
|
|
21
|
+
"name": "@excom/dom-observer",
|
|
22
|
+
"shortName": "dom-observer",
|
|
23
|
+
"version": "0.1.0",
|
|
24
|
+
"description": "<dom-observer> custom element that resolves a target via a CSS selector and fires events on its mutations.",
|
|
25
|
+
"packageType": "kit-element",
|
|
26
|
+
"cdn": "<script src=\"https://unpkg.com/@excom/kit-utils/dist/index.umd.min.js\"></script>\n<script src=\"https://unpkg.com/@excom/neutron/dist/index.umd.min.js\"></script>\n<script src=\"https://unpkg.com/@excom/dom-observer@0.1.0/dist/index.umd.min.js\"></script>",
|
|
27
|
+
"install": {
|
|
28
|
+
"npm": "npm install @excom/dom-observer"
|
|
29
|
+
},
|
|
30
|
+
"imports": {
|
|
31
|
+
"js": "import \"@excom/dom-observer\";",
|
|
32
|
+
"html": "<!-- import path to `node_modules` will depend on your build setup -->\n<script type=\"module\" src=\"/node_modules/@excom/dom-observer\"></script>\n<link rel=\"stylesheet\" href=\"/node_modules/@excom/dom-observer\">"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": []
|
|
35
|
+
},
|
|
36
|
+
"elementApis": [
|
|
37
|
+
{
|
|
38
|
+
"tag": "dom-observer",
|
|
39
|
+
"kind": "class",
|
|
40
|
+
"attributes": [
|
|
41
|
+
{
|
|
42
|
+
"name": "target-ref",
|
|
43
|
+
"type": "string",
|
|
44
|
+
"description": "CSS selector used to resolve the element to observe. Resolved against <code>document</code>. If no element matches at connect time, the element waits for one to appear.",
|
|
45
|
+
"fieldName": "targetRef",
|
|
46
|
+
"surface": "option"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "target-change-observer",
|
|
50
|
+
"type": "MutationObserver",
|
|
51
|
+
"description": "Observer attached to the resolved <code>targetElement</code> (and to its <code>.content</code> fragment when the target is a <code><template></code>).",
|
|
52
|
+
"fieldName": "targetChangeObserver",
|
|
53
|
+
"surface": "state"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "target-element",
|
|
57
|
+
"type": "HTMLElement",
|
|
58
|
+
"description": "The currently observed target element (if any).",
|
|
59
|
+
"fieldName": "targetElement",
|
|
60
|
+
"surface": "state"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "target-finding-observer",
|
|
64
|
+
"type": "MutationObserver",
|
|
65
|
+
"description": "Document-level observer used to wait for a target matching <code>target-ref</code> to appear. Disconnected as soon as the target is found.",
|
|
66
|
+
"fieldName": "targetFindingObserver",
|
|
67
|
+
"surface": "state"
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
"events": [
|
|
71
|
+
{
|
|
72
|
+
"name": "dom-observer-change",
|
|
73
|
+
"description": "Fires whenever the resolved target mutates, and once immediately (with <code>mutations: []</code>) as soon as the target is resolved so listeners can seed from current state. <code>mutations</code> is the <code>MutationRecord[]</code> from the underlying <code>MutationObserver</code> callback (empty on that first fire).",
|
|
74
|
+
"type": "DomObserverChangeEvent",
|
|
75
|
+
"typeExpanded": "CustomEvent & { type: \"dom-observer-change\"; detail: { target: Element; mutations: MutationRecord[]; }; bubbles: true; cancelable: true; composed: true }"
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
"slots": [],
|
|
79
|
+
"cssProperties": [],
|
|
80
|
+
"cssClasses": [],
|
|
81
|
+
"cssAliases": [],
|
|
82
|
+
"listens": [],
|
|
83
|
+
"commands": [],
|
|
84
|
+
"defaultActions": [],
|
|
85
|
+
"expectedChildren": [],
|
|
86
|
+
"provisions": []
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"exportedFiles": {}
|
|
90
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`simple view > logs attribute mutations on the details element > complexity 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"attributeRuns": 0,
|
|
6
|
+
"closest": 0,
|
|
7
|
+
"getVar": 0,
|
|
8
|
+
"importNode": 0,
|
|
9
|
+
"listenerRuns": 0,
|
|
10
|
+
"matches": 0,
|
|
11
|
+
"parentElement": 0,
|
|
12
|
+
"quarkRuns": 0,
|
|
13
|
+
"queryScopeCost": 0,
|
|
14
|
+
"querySelectorAll": 0,
|
|
15
|
+
"removeAttribute": 0,
|
|
16
|
+
"ruleRuns": 0,
|
|
17
|
+
"schedulePaint": 0,
|
|
18
|
+
"setAttribute": 1,
|
|
19
|
+
"setVar": 0,
|
|
20
|
+
"textContent": 1,
|
|
21
|
+
"variableRuns": 0,
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterEach,
|
|
3
|
+
beforeEach,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
fixture,
|
|
7
|
+
it,
|
|
8
|
+
vi,
|
|
9
|
+
wait,
|
|
10
|
+
} from "@excom/heft-rig/profiles/default/config/test-utils";
|
|
11
|
+
import "../../index";
|
|
12
|
+
|
|
13
|
+
describe("dom-observer", () => {
|
|
14
|
+
/*
|
|
15
|
+
* Listen at document before any fixture, so the sync
|
|
16
|
+
* `dom-observer-change` on connect is captured.
|
|
17
|
+
*/
|
|
18
|
+
let events: CustomEvent[];
|
|
19
|
+
const listener = (e: Event) => events.push(e as CustomEvent);
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
events = [];
|
|
23
|
+
document.addEventListener("dom-observer-change", listener);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
document.removeEventListener("dom-observer-change", listener);
|
|
28
|
+
document.body.innerHTML = "";
|
|
29
|
+
vi.restoreAllMocks();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("emits dom-observer-change once the target is resolved", async () => {
|
|
33
|
+
const target = document.createElement("div");
|
|
34
|
+
target.id = "watched";
|
|
35
|
+
document.body.appendChild(target);
|
|
36
|
+
|
|
37
|
+
const el = fixture<HTMLDomObserverElement>(
|
|
38
|
+
`<dom-observer target-ref="#watched"></dom-observer>`,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
await wait(0);
|
|
42
|
+
expect(events.length).toBe(1);
|
|
43
|
+
expect(events[0].detail.mutations).toEqual([]);
|
|
44
|
+
expect(el.targetElement).toBe(target);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("emits dom-observer-change on attribute mutations of the target", async () => {
|
|
48
|
+
const target = document.createElement("div");
|
|
49
|
+
target.id = "watched";
|
|
50
|
+
document.body.appendChild(target);
|
|
51
|
+
|
|
52
|
+
fixture<HTMLDomObserverElement>(
|
|
53
|
+
`<dom-observer target-ref="#watched"></dom-observer>`,
|
|
54
|
+
);
|
|
55
|
+
await wait(0);
|
|
56
|
+
events.length = 0;
|
|
57
|
+
|
|
58
|
+
target.setAttribute("data-state", "active");
|
|
59
|
+
await wait(0);
|
|
60
|
+
|
|
61
|
+
expect(events.length).toBeGreaterThanOrEqual(1);
|
|
62
|
+
const last = events[events.length - 1];
|
|
63
|
+
expect(last.detail.mutations[0].type).toBe("attributes");
|
|
64
|
+
expect(last.detail.mutations[0].attributeName).toBe("data-state");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("emits dom-observer-change on childList mutations of the target", async () => {
|
|
68
|
+
const target = document.createElement("ul");
|
|
69
|
+
target.id = "watched";
|
|
70
|
+
document.body.appendChild(target);
|
|
71
|
+
|
|
72
|
+
fixture<HTMLDomObserverElement>(
|
|
73
|
+
`<dom-observer target-ref="#watched"></dom-observer>`,
|
|
74
|
+
);
|
|
75
|
+
await wait(0);
|
|
76
|
+
events.length = 0;
|
|
77
|
+
|
|
78
|
+
target.appendChild(document.createElement("li"));
|
|
79
|
+
await wait(0);
|
|
80
|
+
|
|
81
|
+
const types = events.flatMap((e) =>
|
|
82
|
+
(e.detail.mutations as MutationRecord[]).map((m) => m.type),
|
|
83
|
+
);
|
|
84
|
+
expect(types).toContain("childList");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("waits for the target to appear before observing", async () => {
|
|
88
|
+
const el = fixture<HTMLDomObserverElement>(
|
|
89
|
+
`<dom-observer target-ref="#late"></dom-observer>`,
|
|
90
|
+
);
|
|
91
|
+
await wait(0);
|
|
92
|
+
expect(events.length).toBe(0);
|
|
93
|
+
expect(el.targetElement).toBeFalsy();
|
|
94
|
+
|
|
95
|
+
const target = document.createElement("section");
|
|
96
|
+
target.id = "late";
|
|
97
|
+
document.body.appendChild(target);
|
|
98
|
+
await wait(0);
|
|
99
|
+
|
|
100
|
+
expect(el.targetElement).toBe(target);
|
|
101
|
+
expect(events.length).toBe(1);
|
|
102
|
+
expect(events[0].detail.mutations).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("observes a <template>'s .content fragment", async () => {
|
|
106
|
+
const tpl = document.createElement("template");
|
|
107
|
+
tpl.id = "rows";
|
|
108
|
+
tpl.innerHTML = "<li>seed</li>";
|
|
109
|
+
document.body.appendChild(tpl);
|
|
110
|
+
|
|
111
|
+
fixture<HTMLDomObserverElement>(
|
|
112
|
+
`<dom-observer target-ref="#rows"></dom-observer>`,
|
|
113
|
+
);
|
|
114
|
+
await wait(0);
|
|
115
|
+
events.length = 0;
|
|
116
|
+
|
|
117
|
+
// Mutating `template.content` must surface; `.content` is a
|
|
118
|
+
// DocumentFragment outside the `<template>` descendant tree.
|
|
119
|
+
const li = document.createElement("li");
|
|
120
|
+
li.textContent = "added";
|
|
121
|
+
tpl.content.appendChild(li);
|
|
122
|
+
await wait(0);
|
|
123
|
+
|
|
124
|
+
const types = events.flatMap((e) =>
|
|
125
|
+
(e.detail.mutations as MutationRecord[]).map((m) => m.type),
|
|
126
|
+
);
|
|
127
|
+
expect(types).toContain("childList");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("emits dom-observer-change on characterData mutations of the target", async () => {
|
|
131
|
+
const target = document.createElement("p");
|
|
132
|
+
target.id = "watched";
|
|
133
|
+
target.textContent = "before";
|
|
134
|
+
document.body.appendChild(target);
|
|
135
|
+
|
|
136
|
+
fixture<HTMLDomObserverElement>(
|
|
137
|
+
`<dom-observer target-ref="#watched"></dom-observer>`,
|
|
138
|
+
);
|
|
139
|
+
await wait(0);
|
|
140
|
+
events.length = 0;
|
|
141
|
+
|
|
142
|
+
(target.firstChild as Text).data = "after";
|
|
143
|
+
await wait(0);
|
|
144
|
+
|
|
145
|
+
const types = events.flatMap((e) =>
|
|
146
|
+
(e.detail.mutations as MutationRecord[]).map((m) => m.type),
|
|
147
|
+
);
|
|
148
|
+
expect(types).toContain("characterData");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("finds a late target nested inside an inserted subtree", async () => {
|
|
152
|
+
const el = fixture<HTMLDomObserverElement>(
|
|
153
|
+
`<dom-observer target-ref="#late-nested"></dom-observer>`,
|
|
154
|
+
);
|
|
155
|
+
await wait(0);
|
|
156
|
+
expect(el.targetElement).toBeFalsy();
|
|
157
|
+
|
|
158
|
+
const wrapper = document.createElement("div");
|
|
159
|
+
wrapper.innerHTML = `<section><p id="late-nested"></p></section>`;
|
|
160
|
+
document.body.appendChild(wrapper);
|
|
161
|
+
await wait(0);
|
|
162
|
+
|
|
163
|
+
expect(el.targetElement).toBe(wrapper.querySelector("#late-nested"));
|
|
164
|
+
expect(events.length).toBe(1);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("re-targets while still waiting for the first target", async () => {
|
|
168
|
+
const target = document.createElement("div");
|
|
169
|
+
target.id = "watched";
|
|
170
|
+
document.body.appendChild(target);
|
|
171
|
+
|
|
172
|
+
const el = fixture<HTMLDomObserverElement>(
|
|
173
|
+
`<dom-observer target-ref="#never-appears"></dom-observer>`,
|
|
174
|
+
);
|
|
175
|
+
await wait(0);
|
|
176
|
+
expect(el.targetFindingObserver).toBeTruthy();
|
|
177
|
+
expect(events.length).toBe(0);
|
|
178
|
+
|
|
179
|
+
el.targetRef = "#watched";
|
|
180
|
+
await wait(0);
|
|
181
|
+
expect(el.targetElement).toBe(target);
|
|
182
|
+
expect(el.targetFindingObserver).toBeNull();
|
|
183
|
+
expect(events.length).toBe(1);
|
|
184
|
+
|
|
185
|
+
// the abandoned finder no longer switches targets
|
|
186
|
+
const late = document.createElement("div");
|
|
187
|
+
late.id = "never-appears";
|
|
188
|
+
document.body.appendChild(late);
|
|
189
|
+
await wait(0);
|
|
190
|
+
expect(el.targetElement).toBe(target);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("re-observes when target-ref moves to another resolved element", async () => {
|
|
194
|
+
const first = document.createElement("div");
|
|
195
|
+
first.id = "first";
|
|
196
|
+
const second = document.createElement("div");
|
|
197
|
+
second.id = "second";
|
|
198
|
+
document.body.append(first, second);
|
|
199
|
+
|
|
200
|
+
const el = fixture<HTMLDomObserverElement>(
|
|
201
|
+
`<dom-observer target-ref="#first"></dom-observer>`,
|
|
202
|
+
);
|
|
203
|
+
await wait(0);
|
|
204
|
+
expect(el.targetElement).toBe(first);
|
|
205
|
+
events.length = 0;
|
|
206
|
+
|
|
207
|
+
el.targetRef = "#second";
|
|
208
|
+
await wait(0);
|
|
209
|
+
expect(el.targetElement).toBe(second);
|
|
210
|
+
// seeds listeners from the new target
|
|
211
|
+
expect(events.length).toBe(1);
|
|
212
|
+
expect(events[0].detail.target).toBe(second);
|
|
213
|
+
events.length = 0;
|
|
214
|
+
|
|
215
|
+
first.setAttribute("data-state", "ignored");
|
|
216
|
+
await wait(0);
|
|
217
|
+
expect(events.length).toBe(0);
|
|
218
|
+
|
|
219
|
+
second.setAttribute("data-state", "seen");
|
|
220
|
+
await wait(0);
|
|
221
|
+
expect(events.length).toBe(1);
|
|
222
|
+
expect(events[0].detail.target).toBe(second);
|
|
223
|
+
expect(events[0].detail.mutations[0].attributeName).toBe("data-state");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("stops observing when the target is cleared", async () => {
|
|
227
|
+
const target = document.createElement("div");
|
|
228
|
+
target.id = "watched";
|
|
229
|
+
document.body.appendChild(target);
|
|
230
|
+
|
|
231
|
+
const el = fixture<HTMLDomObserverElement>(
|
|
232
|
+
`<dom-observer target-ref="#watched"></dom-observer>`,
|
|
233
|
+
);
|
|
234
|
+
await wait(0);
|
|
235
|
+
expect(el.targetChangeObserver).toBeTruthy();
|
|
236
|
+
events.length = 0;
|
|
237
|
+
|
|
238
|
+
el.targetElement = null;
|
|
239
|
+
await wait(0);
|
|
240
|
+
expect(el.targetChangeObserver).toBeNull();
|
|
241
|
+
|
|
242
|
+
target.setAttribute("data-state", "cleared");
|
|
243
|
+
await wait(0);
|
|
244
|
+
expect(events.length).toBe(0);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("keeps observing across a synchronous move", async () => {
|
|
248
|
+
const target = document.createElement("div");
|
|
249
|
+
target.id = "watched";
|
|
250
|
+
document.body.appendChild(target);
|
|
251
|
+
|
|
252
|
+
const el = fixture<HTMLDomObserverElement>(
|
|
253
|
+
`<dom-observer target-ref="#watched"></dom-observer>`,
|
|
254
|
+
);
|
|
255
|
+
await wait(0);
|
|
256
|
+
events.length = 0;
|
|
257
|
+
|
|
258
|
+
const other = document.createElement("div");
|
|
259
|
+
document.body.appendChild(other);
|
|
260
|
+
other.appendChild(el);
|
|
261
|
+
await wait(0);
|
|
262
|
+
expect(el.targetElement).toBe(target);
|
|
263
|
+
expect(events.length).toBe(0);
|
|
264
|
+
|
|
265
|
+
target.setAttribute("data-state", "moved");
|
|
266
|
+
await wait(0);
|
|
267
|
+
expect(events.length).toBe(1);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("re-resolves the target after a real disconnect and reconnect", async () => {
|
|
271
|
+
const target = document.createElement("div");
|
|
272
|
+
target.id = "watched";
|
|
273
|
+
document.body.appendChild(target);
|
|
274
|
+
|
|
275
|
+
const el = fixture<HTMLDomObserverElement>(
|
|
276
|
+
`<dom-observer target-ref="#watched"></dom-observer>`,
|
|
277
|
+
);
|
|
278
|
+
await wait(0);
|
|
279
|
+
el.remove();
|
|
280
|
+
await wait(0);
|
|
281
|
+
expect(el.targetElement).toBeNull();
|
|
282
|
+
events.length = 0;
|
|
283
|
+
|
|
284
|
+
document.body.appendChild(el);
|
|
285
|
+
await wait(0);
|
|
286
|
+
expect(el.targetElement).toBe(target);
|
|
287
|
+
expect(events.length).toBe(1);
|
|
288
|
+
expect(events[0].detail.mutations).toEqual([]);
|
|
289
|
+
|
|
290
|
+
target.setAttribute("data-state", "reconnected");
|
|
291
|
+
await wait(0);
|
|
292
|
+
expect(events.length).toBe(2);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("reconnects without a target-ref as a no-op", async () => {
|
|
296
|
+
const el = fixture<HTMLDomObserverElement>(`<dom-observer></dom-observer>`);
|
|
297
|
+
await wait(0);
|
|
298
|
+
el.remove();
|
|
299
|
+
await wait(0);
|
|
300
|
+
expect(() => document.body.appendChild(el)).not.toThrow();
|
|
301
|
+
await wait(0);
|
|
302
|
+
expect(el.targetElement).toBeFalsy();
|
|
303
|
+
expect(el.targetFindingObserver).toBeFalsy();
|
|
304
|
+
expect(events.length).toBe(0);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("disconnects observers when disconnected", async () => {
|
|
308
|
+
const target = document.createElement("div");
|
|
309
|
+
target.id = "watched";
|
|
310
|
+
document.body.appendChild(target);
|
|
311
|
+
|
|
312
|
+
const el = fixture<HTMLDomObserverElement>(
|
|
313
|
+
`<dom-observer target-ref="#watched"></dom-observer>`,
|
|
314
|
+
);
|
|
315
|
+
await wait(0);
|
|
316
|
+
events.length = 0;
|
|
317
|
+
|
|
318
|
+
el.remove();
|
|
319
|
+
await wait(0);
|
|
320
|
+
|
|
321
|
+
target.setAttribute("data-state", "after-disconnect");
|
|
322
|
+
await wait(0);
|
|
323
|
+
expect(events.length).toBe(0);
|
|
324
|
+
expect(el.targetElement).toBeNull();
|
|
325
|
+
});
|
|
326
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import "@excom/quark-sheet";
|
|
2
|
+
import "../../index";
|
|
3
|
+
import {
|
|
4
|
+
afterEach,
|
|
5
|
+
beforeEach,
|
|
6
|
+
describe,
|
|
7
|
+
expect,
|
|
8
|
+
it,
|
|
9
|
+
} from "@excom/heft-rig/profiles/default/config/test-utils";
|
|
10
|
+
import {
|
|
11
|
+
expectComplexity,
|
|
12
|
+
flush,
|
|
13
|
+
installDemoModules,
|
|
14
|
+
measureComplexity,
|
|
15
|
+
mountView,
|
|
16
|
+
readDemo,
|
|
17
|
+
restoreDemoModules,
|
|
18
|
+
} from "@excom/quark/support/tests/view-helpers";
|
|
19
|
+
|
|
20
|
+
describe("simple view", () => {
|
|
21
|
+
beforeEach(() => installDemoModules());
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
document.body.innerHTML = "";
|
|
24
|
+
restoreDemoModules();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("logs attribute mutations on the details element", async () => {
|
|
28
|
+
const { root, quark } = await mountView(readDemo(import.meta.url, "simple"));
|
|
29
|
+
const details = root.querySelector("details")!;
|
|
30
|
+
const meter = measureComplexity(quark!);
|
|
31
|
+
details.setAttribute("open", "");
|
|
32
|
+
await flush();
|
|
33
|
+
const budget = meter.take();
|
|
34
|
+
meter.stop();
|
|
35
|
+
expect(root.querySelector("output")?.textContent).toMatch(
|
|
36
|
+
/HTMLDetailsElement|MutationRecord/,
|
|
37
|
+
);
|
|
38
|
+
expectComplexity(budget);
|
|
39
|
+
});
|
|
40
|
+
});
|