mount-observer 0.0.1 → 0.0.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 CHANGED
@@ -12,7 +12,7 @@ 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
+ this.#isComplex = match !== undefined && (match.includes(' ') || match.includes(':'));
16
16
  if (whereElementIntersectsWith || whereMediaMatches)
17
17
  throw 'NI'; //not implemented
18
18
  this.#mountInit = init;
@@ -21,12 +21,31 @@ export class MountObserver extends EventTarget {
21
21
  this.#disconnected = new WeakSet();
22
22
  //this.#unmounted = new WeakSet();
23
23
  }
24
+ #calculatedSelector;
25
+ get #selector() {
26
+ if (this.#calculatedSelector !== undefined)
27
+ return this.#calculatedSelector;
28
+ const { match, attribMatches } = this.#mountInit;
29
+ const base = match || '*';
30
+ if (attribMatches === undefined)
31
+ return base;
32
+ const matches = [];
33
+ attribMatches.forEach(x => {
34
+ const { names } = x;
35
+ names.forEach(y => {
36
+ matches.push(`${base}[${y}]`);
37
+ });
38
+ });
39
+ this.#calculatedSelector = matches.join(',');
40
+ return this.#calculatedSelector;
41
+ }
24
42
  async observe(within) {
25
43
  const nodeToMonitor = this.#isComplex ? (within instanceof ShadowRoot ? within : within.getRootNode()) : within;
26
44
  if (!mutationObserverLookup.has(nodeToMonitor)) {
27
45
  mutationObserverLookup.set(nodeToMonitor, new RootMutObs(nodeToMonitor));
28
46
  }
29
47
  const rootMutObs = mutationObserverLookup.get(within);
48
+ const { attribMatches } = this.#mountInit;
30
49
  rootMutObs.addEventListener('mutation-event', (e) => {
31
50
  //TODO: disconnected
32
51
  if (this.#isComplex) {
@@ -39,11 +58,38 @@ export class MountObserver extends EventTarget {
39
58
  const doDisconnect = this.#mountInit.do?.onDisconnect;
40
59
  for (const mutationRecord of mutationRecords) {
41
60
  const { addedNodes, type, removedNodes } = mutationRecord;
42
- //console.log({target, mutationRecord});
61
+ console.log(mutationRecord);
43
62
  const addedElements = Array.from(addedNodes).filter(x => x instanceof Element);
44
63
  addedElements.forEach(x => elsToInspect.push(x));
45
64
  if (type === 'attributes') {
46
- const { target } = mutationRecord;
65
+ const { target, attributeName, oldValue } = mutationRecord;
66
+ if (target instanceof Element && attributeName !== null && attribMatches !== undefined && this.#mounted.has(target)) {
67
+ let idx = 0;
68
+ for (const attrMatch of attribMatches) {
69
+ const { names } = attrMatch;
70
+ if (names.includes(attributeName)) {
71
+ const newValue = target.getAttribute(attributeName);
72
+ // let parsedNewValue = undefined;
73
+ // switch(type){
74
+ // case 'boolean':
75
+ // parsedNewValue = newValue === 'true' ? true : newValue === 'false' ? false : null;
76
+ // break;
77
+ // case 'date':
78
+ // parsedNewValue = newValue === null ? null : new Date(newValue);
79
+ // break;
80
+ // case ''
81
+ // }
82
+ const attrChangeInfo = {
83
+ name: attributeName,
84
+ oldValue,
85
+ newValue,
86
+ idx
87
+ };
88
+ this.dispatchEvent(new AttrChangeEvent(target, attrChangeInfo));
89
+ }
90
+ idx++;
91
+ }
92
+ }
47
93
  elsToInspect.push(target);
48
94
  }
49
95
  const deletedElements = Array.from(removedNodes).filter(x => x instanceof Element);
@@ -73,7 +119,7 @@ export class MountObserver extends EventTarget {
73
119
  //first unmount non matching
74
120
  const alreadyMounted = this.#filterAndDismount();
75
121
  const onMount = this.#mountInit.do?.onMount;
76
- const imp = this.#mountInit.import;
122
+ const { import: imp, attribMatches } = this.#mountInit;
77
123
  for (const match of matching) {
78
124
  if (alreadyMounted.has(match))
79
125
  continue;
@@ -96,6 +142,28 @@ export class MountObserver extends EventTarget {
96
142
  if (onMount !== undefined)
97
143
  onMount(match, this, 'PostImport');
98
144
  this.dispatchEvent(new MountEvent(match));
145
+ if (attribMatches !== undefined) {
146
+ let idx = 0;
147
+ for (const attribMatch of attribMatches) {
148
+ let newValue = null;
149
+ const { names } = attribMatch;
150
+ let nonNullName = names[0];
151
+ for (const name of names) {
152
+ const attrVal = match.getAttribute(name);
153
+ if (attrVal !== null)
154
+ nonNullName = name;
155
+ newValue = newValue || attrVal;
156
+ }
157
+ const attribInfo = {
158
+ oldValue: null,
159
+ newValue,
160
+ idx,
161
+ name: nonNullName
162
+ };
163
+ this.dispatchEvent(new AttrChangeEvent(match, attribInfo));
164
+ idx++;
165
+ }
166
+ }
99
167
  this.#mountedList?.push(new WeakRef(match));
100
168
  //if(this.#unmounted.has(match)) this.#unmounted.delete(match);
101
169
  }
@@ -113,7 +181,8 @@ export class MountObserver extends EventTarget {
113
181
  const returnSet = new Set();
114
182
  if (this.#mountedList !== undefined) {
115
183
  const previouslyMounted = this.#mountedList.map(x => x.deref());
116
- const { match, whereSatisfies, whereInstanceOf } = this.#mountInit;
184
+ const { whereSatisfies, whereInstanceOf } = this.#mountInit;
185
+ const match = this.#selector;
117
186
  const elsToUnMount = previouslyMounted.filter(x => {
118
187
  if (x === undefined)
119
188
  return false;
@@ -132,7 +201,8 @@ export class MountObserver extends EventTarget {
132
201
  return returnSet;
133
202
  }
134
203
  async #filterAndMount(els, checkMatch) {
135
- const { match, whereSatisfies, whereInstanceOf } = this.#mountInit;
204
+ const { whereSatisfies, whereInstanceOf } = this.#mountInit;
205
+ const match = this.#selector;
136
206
  const elsToMount = els.filter(x => {
137
207
  if (checkMatch) {
138
208
  if (!x.matches(match))
@@ -151,8 +221,7 @@ export class MountObserver extends EventTarget {
151
221
  this.#mount(elsToMount);
152
222
  }
153
223
  async #inspectWithin(within) {
154
- const { match } = this.#mountInit;
155
- const els = Array.from(within.querySelectorAll(match));
224
+ const els = Array.from(within.querySelectorAll(this.#selector));
156
225
  this.#filterAndMount(els, false);
157
226
  }
158
227
  unobserve() {
@@ -188,3 +257,13 @@ export class DisconnectEvent extends Event {
188
257
  this.disconnectedElement = disconnectedElement;
189
258
  }
190
259
  }
260
+ export class AttrChangeEvent extends Event {
261
+ mountedElement;
262
+ attrChangeInfo;
263
+ static eventName = 'attr-change';
264
+ constructor(mountedElement, attrChangeInfo) {
265
+ super(AttrChangeEvent.eventName);
266
+ this.mountedElement = mountedElement;
267
+ this.attrChangeInfo = attrChangeInfo;
268
+ }
269
+ }
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,7 @@ 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
+ this.#isComplex = match !== undefined && (match.includes(' ') || match.includes(':'));
24
24
  if(whereElementIntersectsWith || whereMediaMatches) throw 'NI'; //not implemented
25
25
  this.#mountInit = init;
26
26
  this.#abortController = new AbortController();
@@ -29,12 +29,30 @@ export class MountObserver extends EventTarget implements MountContext{
29
29
  //this.#unmounted = new WeakSet();
30
30
  }
31
31
 
32
+ #calculatedSelector: string | undefined;
33
+ get #selector(){
34
+ if(this.#calculatedSelector !== undefined) return this.#calculatedSelector;
35
+ const {match, attribMatches} = this.#mountInit;
36
+ const base = match || '*';
37
+ if(attribMatches === undefined) return base;
38
+ const matches: Array<string> = [];
39
+ attribMatches.forEach(x => {
40
+ const {names} = x;
41
+ names.forEach(y => {
42
+ matches.push(`${base}[${y}]`)
43
+ });
44
+ });
45
+ this.#calculatedSelector = matches.join(',');
46
+ return this.#calculatedSelector;
47
+ }
48
+
32
49
  async observe(within: Node){
33
50
  const nodeToMonitor = this.#isComplex ? (within instanceof ShadowRoot ? within : within.getRootNode()) : within;
34
51
  if(!mutationObserverLookup.has(nodeToMonitor)){
35
52
  mutationObserverLookup.set(nodeToMonitor, new RootMutObs(nodeToMonitor));
36
53
  }
37
54
  const rootMutObs = mutationObserverLookup.get(within)!;
55
+ const {attribMatches} = this.#mountInit;
38
56
  (rootMutObs as any as AddMutationEventListener).addEventListener('mutation-event', (e: MutationEvent) => {
39
57
  //TODO: disconnected
40
58
  if(this.#isComplex){
@@ -47,11 +65,39 @@ export class MountObserver extends EventTarget implements MountContext{
47
65
  const doDisconnect = this.#mountInit.do?.onDisconnect;
48
66
  for(const mutationRecord of mutationRecords){
49
67
  const {addedNodes, type, removedNodes} = mutationRecord;
50
- //console.log({target, mutationRecord});
68
+ console.log(mutationRecord);
51
69
  const addedElements = Array.from(addedNodes).filter(x => x instanceof Element) as Array<Element>;
52
70
  addedElements.forEach(x => elsToInspect.push(x));
53
71
  if(type === 'attributes'){
54
- const {target} = mutationRecord;
72
+ const {target, attributeName, oldValue} = mutationRecord;
73
+ if(target instanceof Element && attributeName !== null && attribMatches !== undefined && this.#mounted.has(target)){
74
+ let idx = 0;
75
+ for(const attrMatch of attribMatches){
76
+ const {names} = attrMatch;
77
+ if(names.includes(attributeName)){
78
+ const newValue = target.getAttribute(attributeName);
79
+ // let parsedNewValue = undefined;
80
+ // switch(type){
81
+ // case 'boolean':
82
+ // parsedNewValue = newValue === 'true' ? true : newValue === 'false' ? false : null;
83
+ // break;
84
+ // case 'date':
85
+ // parsedNewValue = newValue === null ? null : new Date(newValue);
86
+ // break;
87
+ // case ''
88
+
89
+ // }
90
+ const attrChangeInfo: AttrChangeInfo = {
91
+ name: attributeName,
92
+ oldValue,
93
+ newValue,
94
+ idx
95
+ };
96
+ this.dispatchEvent(new AttrChangeEvent(target, attrChangeInfo));
97
+ }
98
+ idx++;
99
+ }
100
+ }
55
101
  elsToInspect.push(target as Element);
56
102
  }
57
103
  const deletedElements = Array.from(removedNodes).filter(x => x instanceof Element) as Array<Element>;
@@ -83,7 +129,7 @@ export class MountObserver extends EventTarget implements MountContext{
83
129
  //first unmount non matching
84
130
  const alreadyMounted = this.#filterAndDismount();
85
131
  const onMount = this.#mountInit.do?.onMount;
86
- const imp = this.#mountInit.import;
132
+ const {import: imp, attribMatches} = this.#mountInit;
87
133
  for(const match of matching){
88
134
  if(alreadyMounted.has(match)) continue;
89
135
  this.#mounted.add(match);
@@ -104,6 +150,27 @@ export class MountObserver extends EventTarget implements MountContext{
104
150
  }
105
151
  if(onMount !== undefined) onMount(match, this, 'PostImport');
106
152
  this.dispatchEvent(new MountEvent(match));
153
+ if(attribMatches !== undefined){
154
+ let idx = 0;
155
+ for(const attribMatch of attribMatches){
156
+ let newValue = null;
157
+ const {names} = attribMatch;
158
+ let nonNullName = names[0];
159
+ for(const name of names){
160
+ const attrVal = match.getAttribute(name);
161
+ if(attrVal !== null) nonNullName = name;
162
+ newValue = newValue || attrVal;
163
+ }
164
+ const attribInfo: AttrChangeInfo = {
165
+ oldValue: null,
166
+ newValue,
167
+ idx,
168
+ name: nonNullName
169
+ };
170
+ this.dispatchEvent(new AttrChangeEvent(match, attribInfo));
171
+ idx++;
172
+ }
173
+ }
107
174
  this.#mountedList?.push(new WeakRef(match));
108
175
  //if(this.#unmounted.has(match)) this.#unmounted.delete(match);
109
176
  }
@@ -123,7 +190,8 @@ export class MountObserver extends EventTarget implements MountContext{
123
190
  const returnSet = new Set<Element>();
124
191
  if(this.#mountedList !== undefined){
125
192
  const previouslyMounted = this.#mountedList.map(x => x.deref());
126
- const {match, whereSatisfies, whereInstanceOf} = this.#mountInit;
193
+ const {whereSatisfies, whereInstanceOf} = this.#mountInit;
194
+ const match = this.#selector;
127
195
  const elsToUnMount = previouslyMounted.filter(x => {
128
196
  if(x === undefined) return false;
129
197
  if(!x.matches(match)) return true;
@@ -140,7 +208,8 @@ export class MountObserver extends EventTarget implements MountContext{
140
208
  }
141
209
 
142
210
  async #filterAndMount(els: Array<Element>, checkMatch: boolean){
143
- const {match, whereSatisfies, whereInstanceOf} = this.#mountInit;
211
+ const {whereSatisfies, whereInstanceOf} = this.#mountInit;
212
+ const match = this.#selector;
144
213
  const elsToMount = els.filter(x => {
145
214
  if(checkMatch){
146
215
  if(!x.matches(match)) return false;
@@ -157,8 +226,7 @@ export class MountObserver extends EventTarget implements MountContext{
157
226
  }
158
227
 
159
228
  async #inspectWithin(within: Node){
160
- const {match} = this.#mountInit;
161
- const els = Array.from((within as Element).querySelectorAll(match));
229
+ const els = Array.from((within as Element).querySelectorAll(this.#selector));
162
230
  this.#filterAndMount(els, false);
163
231
  }
164
232
 
@@ -200,3 +268,10 @@ export class DisconnectEvent extends Event implements IDisconnectEvent{
200
268
  }
201
269
  }
202
270
 
271
+ export class AttrChangeEvent extends Event implements IAttrChangeEvent{
272
+ static eventName: attrChangeEventName = 'attr-change';
273
+ constructor(public mountedElement: Element, public attrChangeInfo: AttrChangeInfo){
274
+ super(AttrChangeEvent.eventName);
275
+ }
276
+ }
277
+
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,10 @@ 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
+
184
186
  ## Preemptive downloading
185
187
 
186
188
  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.2",
4
4
  "description": "",
5
5
  "main": "MountObserver.js",
6
6
  "module": "MountObserver.js",
@@ -0,0 +1,39 @@
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
+ do:{
23
+ onDisconnect: (el, ctx) => {
24
+ console.log({el, ctx});
25
+
26
+ }
27
+ }
28
+ });
29
+ mo.addEventListener('attr-change', e => {
30
+ console.log(e);
31
+ target.setAttribute('mark', 'good');
32
+ });
33
+ mo.observe(div);
34
+ setTimeout(() => {
35
+ span.setAttribute('test-1', 'hello')
36
+ }, 1000);
37
+ </script>
38
+ </body>
39
+ </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
+