mount-observer 0.0.1 → 0.0.3

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 CHANGED
@@ -12,7 +12,12 @@ export class MountObserver extends EventTarget {
12
12
  constructor(init) {
13
13
  super();
14
14
  const { match, whereElementIntersectsWith, whereMediaMatches } = init;
15
- this.#isComplex = match.includes(' ') || match.includes(':');
15
+ let isComplex = false;
16
+ if (match !== undefined) {
17
+ const reducedMatch = match.replaceAll(':not(', '');
18
+ isComplex = reducedMatch.includes(' ') || reducedMatch.includes(':');
19
+ }
20
+ this.#isComplex = isComplex;
16
21
  if (whereElementIntersectsWith || whereMediaMatches)
17
22
  throw 'NI'; //not implemented
18
23
  this.#mountInit = init;
@@ -21,12 +26,31 @@ export class MountObserver extends EventTarget {
21
26
  this.#disconnected = new WeakSet();
22
27
  //this.#unmounted = new WeakSet();
23
28
  }
29
+ #calculatedSelector;
30
+ get #selector() {
31
+ if (this.#calculatedSelector !== undefined)
32
+ return this.#calculatedSelector;
33
+ const { match, attribMatches } = this.#mountInit;
34
+ const base = match || '*';
35
+ if (attribMatches === undefined)
36
+ return base;
37
+ const matches = [];
38
+ attribMatches.forEach(x => {
39
+ const { names } = x;
40
+ names.forEach(y => {
41
+ matches.push(`${base}[${y}]`);
42
+ });
43
+ });
44
+ this.#calculatedSelector = matches.join(',');
45
+ return this.#calculatedSelector;
46
+ }
24
47
  async observe(within) {
25
48
  const nodeToMonitor = this.#isComplex ? (within instanceof ShadowRoot ? within : within.getRootNode()) : within;
26
49
  if (!mutationObserverLookup.has(nodeToMonitor)) {
27
50
  mutationObserverLookup.set(nodeToMonitor, new RootMutObs(nodeToMonitor));
28
51
  }
29
52
  const rootMutObs = mutationObserverLookup.get(within);
53
+ const { attribMatches } = this.#mountInit;
30
54
  rootMutObs.addEventListener('mutation-event', (e) => {
31
55
  //TODO: disconnected
32
56
  if (this.#isComplex) {
@@ -39,11 +63,38 @@ export class MountObserver extends EventTarget {
39
63
  const doDisconnect = this.#mountInit.do?.onDisconnect;
40
64
  for (const mutationRecord of mutationRecords) {
41
65
  const { addedNodes, type, removedNodes } = mutationRecord;
42
- //console.log({target, mutationRecord});
66
+ //console.log(mutationRecord);
43
67
  const addedElements = Array.from(addedNodes).filter(x => x instanceof Element);
44
68
  addedElements.forEach(x => elsToInspect.push(x));
45
69
  if (type === 'attributes') {
46
- const { target } = mutationRecord;
70
+ const { target, attributeName, oldValue } = mutationRecord;
71
+ if (target instanceof Element && attributeName !== null && attribMatches !== undefined && this.#mounted.has(target)) {
72
+ let idx = 0;
73
+ for (const attrMatch of attribMatches) {
74
+ const { names } = attrMatch;
75
+ if (names.includes(attributeName)) {
76
+ const newValue = target.getAttribute(attributeName);
77
+ // let parsedNewValue = undefined;
78
+ // switch(type){
79
+ // case 'boolean':
80
+ // parsedNewValue = newValue === 'true' ? true : newValue === 'false' ? false : null;
81
+ // break;
82
+ // case 'date':
83
+ // parsedNewValue = newValue === null ? null : new Date(newValue);
84
+ // break;
85
+ // case ''
86
+ // }
87
+ const attrChangeInfo = {
88
+ name: attributeName,
89
+ oldValue,
90
+ newValue,
91
+ idx
92
+ };
93
+ this.dispatchEvent(new AttrChangeEvent(target, attrChangeInfo));
94
+ }
95
+ idx++;
96
+ }
97
+ }
47
98
  elsToInspect.push(target);
48
99
  }
49
100
  const deletedElements = Array.from(removedNodes).filter(x => x instanceof Element);
@@ -73,7 +124,7 @@ export class MountObserver extends EventTarget {
73
124
  //first unmount non matching
74
125
  const alreadyMounted = this.#filterAndDismount();
75
126
  const onMount = this.#mountInit.do?.onMount;
76
- const imp = this.#mountInit.import;
127
+ const { import: imp, attribMatches } = this.#mountInit;
77
128
  for (const match of matching) {
78
129
  if (alreadyMounted.has(match))
79
130
  continue;
@@ -96,6 +147,28 @@ export class MountObserver extends EventTarget {
96
147
  if (onMount !== undefined)
97
148
  onMount(match, this, 'PostImport');
98
149
  this.dispatchEvent(new MountEvent(match));
150
+ if (attribMatches !== undefined) {
151
+ let idx = 0;
152
+ for (const attribMatch of attribMatches) {
153
+ let newValue = null;
154
+ const { names } = attribMatch;
155
+ let nonNullName = names[0];
156
+ for (const name of names) {
157
+ const attrVal = match.getAttribute(name);
158
+ if (attrVal !== null)
159
+ nonNullName = name;
160
+ newValue = newValue || attrVal;
161
+ }
162
+ const attribInfo = {
163
+ oldValue: null,
164
+ newValue,
165
+ idx,
166
+ name: nonNullName
167
+ };
168
+ this.dispatchEvent(new AttrChangeEvent(match, attribInfo));
169
+ idx++;
170
+ }
171
+ }
99
172
  this.#mountedList?.push(new WeakRef(match));
100
173
  //if(this.#unmounted.has(match)) this.#unmounted.delete(match);
101
174
  }
@@ -113,7 +186,8 @@ export class MountObserver extends EventTarget {
113
186
  const returnSet = new Set();
114
187
  if (this.#mountedList !== undefined) {
115
188
  const previouslyMounted = this.#mountedList.map(x => x.deref());
116
- const { match, whereSatisfies, whereInstanceOf } = this.#mountInit;
189
+ const { whereSatisfies, whereInstanceOf } = this.#mountInit;
190
+ const match = this.#selector;
117
191
  const elsToUnMount = previouslyMounted.filter(x => {
118
192
  if (x === undefined)
119
193
  return false;
@@ -132,7 +206,8 @@ export class MountObserver extends EventTarget {
132
206
  return returnSet;
133
207
  }
134
208
  async #filterAndMount(els, checkMatch) {
135
- const { match, whereSatisfies, whereInstanceOf } = this.#mountInit;
209
+ const { whereSatisfies, whereInstanceOf } = this.#mountInit;
210
+ const match = this.#selector;
136
211
  const elsToMount = els.filter(x => {
137
212
  if (checkMatch) {
138
213
  if (!x.matches(match))
@@ -151,8 +226,7 @@ export class MountObserver extends EventTarget {
151
226
  this.#mount(elsToMount);
152
227
  }
153
228
  async #inspectWithin(within) {
154
- const { match } = this.#mountInit;
155
- const els = Array.from(within.querySelectorAll(match));
229
+ const els = Array.from(within.querySelectorAll(this.#selector));
156
230
  this.#filterAndMount(els, false);
157
231
  }
158
232
  unobserve() {
@@ -188,3 +262,13 @@ export class DisconnectEvent extends Event {
188
262
  this.disconnectedElement = disconnectedElement;
189
263
  }
190
264
  }
265
+ export class AttrChangeEvent extends Event {
266
+ mountedElement;
267
+ attrChangeInfo;
268
+ static eventName = 'attr-change';
269
+ constructor(mountedElement, attrChangeInfo) {
270
+ super(AttrChangeEvent.eventName);
271
+ this.mountedElement = mountedElement;
272
+ this.attrChangeInfo = attrChangeInfo;
273
+ }
274
+ }
package/MountObserver.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import {MountInit, MountContext, AddMutationEventListener,
2
2
  MutationEvent, dismountEventName, mountEventName, IMountEvent, IDismountEvent,
3
- disconnectedEventName, IDisconnectEvent
3
+ disconnectedEventName, IDisconnectEvent, IAttrChangeEvent, attrChangeEventName, AttrChangeInfo
4
4
  } from './types';
5
5
  import {RootMutObs} from './RootMutObs.js';
6
6
 
@@ -20,7 +20,12 @@ export class MountObserver extends EventTarget implements MountContext{
20
20
  constructor(init: MountInit){
21
21
  super();
22
22
  const {match, whereElementIntersectsWith, whereMediaMatches} = init;
23
- this.#isComplex = match.includes(' ') || match.includes(':')
23
+ let isComplex = false;
24
+ if(match !== undefined){
25
+ const reducedMatch = match.replaceAll(':not(', '');
26
+ isComplex = reducedMatch.includes(' ') || reducedMatch.includes(':');
27
+ }
28
+ this.#isComplex = isComplex;
24
29
  if(whereElementIntersectsWith || whereMediaMatches) throw 'NI'; //not implemented
25
30
  this.#mountInit = init;
26
31
  this.#abortController = new AbortController();
@@ -29,12 +34,30 @@ export class MountObserver extends EventTarget implements MountContext{
29
34
  //this.#unmounted = new WeakSet();
30
35
  }
31
36
 
37
+ #calculatedSelector: string | undefined;
38
+ get #selector(){
39
+ if(this.#calculatedSelector !== undefined) return this.#calculatedSelector;
40
+ const {match, attribMatches} = this.#mountInit;
41
+ const base = match || '*';
42
+ if(attribMatches === undefined) return base;
43
+ const matches: Array<string> = [];
44
+ attribMatches.forEach(x => {
45
+ const {names} = x;
46
+ names.forEach(y => {
47
+ matches.push(`${base}[${y}]`)
48
+ });
49
+ });
50
+ this.#calculatedSelector = matches.join(',');
51
+ return this.#calculatedSelector;
52
+ }
53
+
32
54
  async observe(within: Node){
33
55
  const nodeToMonitor = this.#isComplex ? (within instanceof ShadowRoot ? within : within.getRootNode()) : within;
34
56
  if(!mutationObserverLookup.has(nodeToMonitor)){
35
57
  mutationObserverLookup.set(nodeToMonitor, new RootMutObs(nodeToMonitor));
36
58
  }
37
59
  const rootMutObs = mutationObserverLookup.get(within)!;
60
+ const {attribMatches} = this.#mountInit;
38
61
  (rootMutObs as any as AddMutationEventListener).addEventListener('mutation-event', (e: MutationEvent) => {
39
62
  //TODO: disconnected
40
63
  if(this.#isComplex){
@@ -47,11 +70,39 @@ export class MountObserver extends EventTarget implements MountContext{
47
70
  const doDisconnect = this.#mountInit.do?.onDisconnect;
48
71
  for(const mutationRecord of mutationRecords){
49
72
  const {addedNodes, type, removedNodes} = mutationRecord;
50
- //console.log({target, mutationRecord});
73
+ //console.log(mutationRecord);
51
74
  const addedElements = Array.from(addedNodes).filter(x => x instanceof Element) as Array<Element>;
52
75
  addedElements.forEach(x => elsToInspect.push(x));
53
76
  if(type === 'attributes'){
54
- const {target} = mutationRecord;
77
+ const {target, attributeName, oldValue} = mutationRecord;
78
+ if(target instanceof Element && attributeName !== null && attribMatches !== undefined && this.#mounted.has(target)){
79
+ let idx = 0;
80
+ for(const attrMatch of attribMatches){
81
+ const {names} = attrMatch;
82
+ if(names.includes(attributeName)){
83
+ const newValue = target.getAttribute(attributeName);
84
+ // let parsedNewValue = undefined;
85
+ // switch(type){
86
+ // case 'boolean':
87
+ // parsedNewValue = newValue === 'true' ? true : newValue === 'false' ? false : null;
88
+ // break;
89
+ // case 'date':
90
+ // parsedNewValue = newValue === null ? null : new Date(newValue);
91
+ // break;
92
+ // case ''
93
+
94
+ // }
95
+ const attrChangeInfo: AttrChangeInfo = {
96
+ name: attributeName,
97
+ oldValue,
98
+ newValue,
99
+ idx
100
+ };
101
+ this.dispatchEvent(new AttrChangeEvent(target, attrChangeInfo));
102
+ }
103
+ idx++;
104
+ }
105
+ }
55
106
  elsToInspect.push(target as Element);
56
107
  }
57
108
  const deletedElements = Array.from(removedNodes).filter(x => x instanceof Element) as Array<Element>;
@@ -83,7 +134,7 @@ export class MountObserver extends EventTarget implements MountContext{
83
134
  //first unmount non matching
84
135
  const alreadyMounted = this.#filterAndDismount();
85
136
  const onMount = this.#mountInit.do?.onMount;
86
- const imp = this.#mountInit.import;
137
+ const {import: imp, attribMatches} = this.#mountInit;
87
138
  for(const match of matching){
88
139
  if(alreadyMounted.has(match)) continue;
89
140
  this.#mounted.add(match);
@@ -104,6 +155,27 @@ export class MountObserver extends EventTarget implements MountContext{
104
155
  }
105
156
  if(onMount !== undefined) onMount(match, this, 'PostImport');
106
157
  this.dispatchEvent(new MountEvent(match));
158
+ if(attribMatches !== undefined){
159
+ let idx = 0;
160
+ for(const attribMatch of attribMatches){
161
+ let newValue = null;
162
+ const {names} = attribMatch;
163
+ let nonNullName = names[0];
164
+ for(const name of names){
165
+ const attrVal = match.getAttribute(name);
166
+ if(attrVal !== null) nonNullName = name;
167
+ newValue = newValue || attrVal;
168
+ }
169
+ const attribInfo: AttrChangeInfo = {
170
+ oldValue: null,
171
+ newValue,
172
+ idx,
173
+ name: nonNullName
174
+ };
175
+ this.dispatchEvent(new AttrChangeEvent(match, attribInfo));
176
+ idx++;
177
+ }
178
+ }
107
179
  this.#mountedList?.push(new WeakRef(match));
108
180
  //if(this.#unmounted.has(match)) this.#unmounted.delete(match);
109
181
  }
@@ -123,7 +195,8 @@ export class MountObserver extends EventTarget implements MountContext{
123
195
  const returnSet = new Set<Element>();
124
196
  if(this.#mountedList !== undefined){
125
197
  const previouslyMounted = this.#mountedList.map(x => x.deref());
126
- const {match, whereSatisfies, whereInstanceOf} = this.#mountInit;
198
+ const {whereSatisfies, whereInstanceOf} = this.#mountInit;
199
+ const match = this.#selector;
127
200
  const elsToUnMount = previouslyMounted.filter(x => {
128
201
  if(x === undefined) return false;
129
202
  if(!x.matches(match)) return true;
@@ -140,7 +213,8 @@ export class MountObserver extends EventTarget implements MountContext{
140
213
  }
141
214
 
142
215
  async #filterAndMount(els: Array<Element>, checkMatch: boolean){
143
- const {match, whereSatisfies, whereInstanceOf} = this.#mountInit;
216
+ const {whereSatisfies, whereInstanceOf} = this.#mountInit;
217
+ const match = this.#selector;
144
218
  const elsToMount = els.filter(x => {
145
219
  if(checkMatch){
146
220
  if(!x.matches(match)) return false;
@@ -157,8 +231,7 @@ export class MountObserver extends EventTarget implements MountContext{
157
231
  }
158
232
 
159
233
  async #inspectWithin(within: Node){
160
- const {match} = this.#mountInit;
161
- const els = Array.from((within as Element).querySelectorAll(match));
234
+ const els = Array.from((within as Element).querySelectorAll(this.#selector));
162
235
  this.#filterAndMount(els, false);
163
236
  }
164
237
 
@@ -200,3 +273,10 @@ export class DisconnectEvent extends Event implements IDisconnectEvent{
200
273
  }
201
274
  }
202
275
 
276
+ export class AttrChangeEvent extends Event implements IAttrChangeEvent{
277
+ static eventName: attrChangeEventName = 'attr-change';
278
+ constructor(public mountedElement: Element, public attrChangeInfo: AttrChangeInfo){
279
+ super(AttrChangeEvent.eventName);
280
+ }
281
+ }
282
+
package/README.md CHANGED
@@ -135,8 +135,6 @@ const observer = new MountObserver({
135
135
  })
136
136
  ```
137
137
 
138
-
139
-
140
138
  ## Subscribing
141
139
 
142
140
  Subscribing can be done via:
@@ -181,6 +179,38 @@ If an element that is in "mounted" state according to a MountObserver instance i
181
179
  4) If the new place it was added remains within the original rootNode and remains either dismounted or mounted, the MountObserver instance dispatches event "reconfirmed".
182
180
  5) If the element no longer satisfies the criteria of the MountObserver instance, the MountObserver instance will dispatch event "dismount". The same is done in reverse for moved elements that started out in a "dismounted" state.
183
181
 
182
+ ## Special support for attributes
183
+
184
+ Extra support is provided for monitoring attributes.
185
+
186
+ Example:
187
+
188
+ ```html
189
+ <div id=div>
190
+ <span id=span></span>
191
+ </div>
192
+ <script type=module>
193
+ import {MountObserver} from '../MountObserver.js';
194
+ const mo = new MountObserver({
195
+ match: '#span',
196
+ attribMatches:[
197
+ {
198
+ names: ['test-1']
199
+ }
200
+ ]
201
+ });
202
+ mo.addEventListener('attr-change', e => {
203
+ console.log(e);
204
+ });
205
+ mo.observe(div);
206
+ setTimeout(() => {
207
+ span.setAttribute('test-1', 'hello')
208
+ }, 1000);
209
+ </script>
210
+ ```
211
+
212
+
213
+
184
214
  ## Preemptive downloading
185
215
 
186
216
  There are two significant steps to imports, each of which imposes a cost:
package/RootMutObs.js CHANGED
@@ -8,6 +8,7 @@ export class RootMutObs extends EventTarget {
8
8
  subtree: true,
9
9
  childList: true,
10
10
  attributes: true,
11
+ attributeOldValue: true,
11
12
  });
12
13
  }
13
14
  #mutationObserver;
package/RootMutObs.ts CHANGED
@@ -10,6 +10,7 @@ export class RootMutObs extends EventTarget{
10
10
  subtree: true,
11
11
  childList: true,
12
12
  attributes: true,
13
+ attributeOldValue: true,
13
14
  });
14
15
  }
15
16
  #mutationObserver: MutationObserver;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mount-observer",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "",
5
5
  "main": "MountObserver.js",
6
6
  "module": "MountObserver.js",
@@ -0,0 +1,33 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Document</title>
7
+ </head>
8
+ <body>
9
+ <div id=div>
10
+ <span id=span></span>
11
+ </div>
12
+ <div id=target></div>
13
+ <script type=module>
14
+ import {MountObserver} from '../MountObserver.js';
15
+ const mo = new MountObserver({
16
+ match: '#span',
17
+ attribMatches:[
18
+ {
19
+ names: ['test-1']
20
+ }
21
+ ]
22
+ });
23
+ mo.addEventListener('attr-change', e => {
24
+ console.log(e);
25
+ target.setAttribute('mark', 'good');
26
+ });
27
+ mo.observe(div);
28
+ setTimeout(() => {
29
+ span.setAttribute('test-1', 'hello')
30
+ }, 1000);
31
+ </script>
32
+ </body>
33
+ </html>
@@ -0,0 +1,8 @@
1
+ import { test, expect } from '@playwright/test';
2
+ test('test1', async ({ page }) => {
3
+ await page.goto('./tests/test6.html');
4
+ // wait for 1 second
5
+ await page.waitForTimeout(1000);
6
+ const editor = page.locator('#target');
7
+ await expect(editor).toHaveAttribute('mark', 'good');
8
+ });
package/types.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  export interface MountInit{
2
- readonly match: CSSMatch,
3
- readonly whereElementIntersectsWith: IntersectionObserverInit,
4
- readonly whereMediaMatches: MediaQuery,
2
+ readonly match?: CSSMatch,
3
+ readonly attribMatches?: Array<AttribMatch>,
4
+ readonly whereElementIntersectsWith?: IntersectionObserverInit,
5
+ readonly whereMediaMatches?: MediaQuery,
5
6
  readonly whereInstanceOf?: Array<typeof Node>, //[TODO] What's the best way to type this?,
6
- readonly whereSatisfies: PipelineProcessor<boolean>,
7
+ readonly whereSatisfies?: PipelineProcessor<boolean>,
7
8
  readonly import?: ImportString | [ImportString, ImportAssertions] | PipelineProcessor,
8
9
  readonly do?: {
9
10
  readonly onMount: PipelineProcessor,
@@ -18,6 +19,14 @@ type CSSMatch = string;
18
19
  type ImportString = string;
19
20
  type MediaQuery = string;
20
21
 
22
+ export interface AttribMatch{
23
+ names: string[],
24
+ //for boolean, support true/false/mixed
25
+ // type?: 'number' | 'string' | 'date' | 'json-object' | 'boolean',
26
+ // valConverter?: (s: string) => any,
27
+ // validator?: (v: any) => boolean;
28
+ }
29
+
21
30
  export interface MountContext {
22
31
  // readonly mountInit: MountInit,
23
32
  // readonly mountedRefs: WeakRef<Element>[],
@@ -42,10 +51,18 @@ export interface AddMutationEventListener {
42
51
  }
43
52
  //#endregion
44
53
 
54
+ interface AttrChangeInfo{
55
+ name: string,
56
+ oldValue: string | null,
57
+ newValue: string | null,
58
+ idx: number,
59
+ //parsedNewValue?: any,
60
+ }
61
+
45
62
  //#region mount event
46
63
  export type mountEventName = 'mount';
47
64
  export interface IMountEvent{
48
- mountedElement: Element
65
+ mountedElement: Element,
49
66
  }
50
67
  export type mountEventHandler = (e: IMountEvent) => void;
51
68
 
@@ -80,3 +97,14 @@ export interface AddDisconnectEventListener {
80
97
  }
81
98
  //endregion
82
99
 
100
+ //#region attribute change event
101
+ export type attrChangeEventName = 'attr-change';
102
+ export interface IAttrChangeEvent extends IMountEvent {
103
+ attrChangeInfo: AttrChangeInfo,
104
+ }
105
+ export type attrChangeEventHander = (e: IAttrChangeEvent) => void;
106
+ export interface AddAttrChangeEventistener{
107
+ addEventListener(eventName: attrChangeEventName, handler: attrChangeEventHander, options?: AddEventListenerOptions): void;
108
+ }
109
+ //#endregion
110
+