mount-observer 0.1.10 → 0.1.12

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/README.md CHANGED
@@ -11,8 +11,11 @@ The following features have been implemented and tested:
11
11
 
12
12
  ### Core Functionality
13
13
  - ✅ **matching**: CSS selector-based element matching
14
- - ✅ **withInstance**: Constructor-based element filtering (single or array)
14
+ - ✅ **whereInstanceOf**: Constructor-based element filtering (single or array)
15
15
  - ✅ **withMediaMatching**: Media query-based conditional mounting (string or MediaQueryList)
16
+ - ✅ **whereObservedRootSizeMatches**: Container query-based conditional mounting (observes root element size)
17
+ - ✅ **whereElementIntersectsWith**: Intersection observer-based conditional mounting (observes element visibility)
18
+ - ✅ **whereConnectionHas**: Network connection-based conditional mounting (observes connection speed/type)
16
19
  - ✅ **withScopePerimeter**: Donut hole scoping (exclude elements inside matching ancestors)
17
20
 
18
21
  ### Lifecycle & Events
@@ -32,19 +35,15 @@ The following features have been implemented and tested:
32
35
  - ✅ **Memory management**: WeakRef usage for DOM node references
33
36
 
34
37
  ### Not Yet Implemented
35
- - ❌ Intersection observer integration
36
- - ❌ Container query support
37
- - ❌ Shadow DOM traversal utilities
38
38
  - ❌ Reconnect event handling
39
- - ❌ Multiple import types (CSS, JSON, HTML)
40
39
 
41
40
  # The MountObserver API
42
41
 
43
42
  Author: Bruce B. Anderson (with valuable feedback from @doeixd)
44
43
 
45
- Issues / PRs / polyfill: [mount-observer](https://github.com/bahrus/mount-observer)
44
+ Issues / PRs / polyfill: [mount-observer](https://github.com/bahrus/mount-observer/tree/v2)
46
45
 
47
- Last Update: Aug 7, 2025
46
+ Last Update: Feb 23, 2026
48
47
 
49
48
  ## Benefits of this API
50
49
 
@@ -83,6 +82,7 @@ There is quite a bit of functionality this proposal would open up that is exceed
83
82
 
84
83
  4. Some CSS selectors, such as the [scope donut hole range](https://css-tricks.com/solved-by-css-donuts-scopes/#aa-donut-scoping-with-scope), aren't supported by oEl.querySelectorAll(...) or oEl.matches(...).
85
84
 
85
+
86
86
  ### Most significant use cases
87
87
 
88
88
  The amount of code necessary to accomplish these common tasks designed to improve the user experience is significant. Building it into the platform would potentially:
@@ -107,20 +107,19 @@ Before getting into the weeds, let's demonstrate the two most prominent use case
107
107
  <div log-to-console="clicked on a div">hello</div>
108
108
 
109
109
  <script type=module>
110
- import 'mount-observer/ElementMountExtension.js';
111
- document.body.mount([{
112
- withAttrs:{base: 'log-to-console'},
113
- spawn: function(el){
110
+ document.body.mount({
111
+ matching: '[log-to-console]',
112
+ do: (el) => {
114
113
  el.addEventListener('click', e => {
115
114
  console.log(e.target.getAttribute('log-to-console'));
116
115
  });
117
- },
118
- }])
116
+ }
117
+ })
119
118
  </script>
120
119
  </body>
121
120
  ```
122
121
 
123
- See [this extending package](https://github.com/bahrus/mount-observer-script-element) that provides for a more declarative approach.
122
+
124
123
 
125
124
  ### Use Case 2: Lazy Global Custom Element Definition
126
125
 
@@ -151,6 +150,8 @@ document.mount({
151
150
 
152
151
  This registers custom elements with the global customElements registry.
153
152
 
153
+ See [this extending package](https://github.com/bahrus/mount-observer-script-element) that provides for a more declarative approach.
154
+
154
155
  ### Scoped
155
156
 
156
157
  To register the class in the same custom element registry as the element which calls the "mount" method (element in this case), use "builtIns.defineScopedCustomElement":
@@ -171,13 +172,13 @@ The `builtIns.enhanceMountedElement` handler automatically enhances mounted elem
171
172
  // MyEnhancement.js
172
173
  class ButtonEnhancement {
173
174
  constructor(element, ctx, initVals) {
174
- this.element = element;
175
+ this.element = new WeakRef(element);
175
176
  this.ctx = ctx;
176
177
  this.clickCount = 0;
177
178
 
178
- element.addEventListener('click', () => {
179
+ element.addEventListener('click', ({target}) => {
179
180
  this.clickCount++;
180
- element.setAttribute('data-clicks', this.clickCount);
181
+ target.setAttribute('data-clicks', this.clickCount);
181
182
  });
182
183
  }
183
184
  }
@@ -187,9 +188,6 @@ export default {
187
188
  enhKey: 'buttonEnh'
188
189
  };
189
190
 
190
- // main.js
191
- import 'mount-observer/ElementMountExtension.js';
192
-
193
191
  document.mount({
194
192
  matching: '.enhance-me',
195
193
  import: './MyEnhancement.js',
@@ -419,9 +417,54 @@ reference: [2, 3] // Both actions1 and actions2 will have their 'do' called if
419
417
 
420
418
  [Implemented as [Requirement11](requirements/Done/Requirement11.md)]
421
419
 
422
- ### Referenced withInstance
420
+ ## Media / container queries / instanceOf / custom checks [TODO] out of date
421
+
422
+ Unlike traditional CSS @import, CSS Modules don't support specifying different imports based on media queries. That can be another condition we can attach (and why not throw in container queries, based on the rootNode?):
423
+
424
+ ```JavaScript
425
+ const observer = new MountObserver({
426
+ select: 'div > p + p ~ span[class$="name"]', // not supported by polyfill
427
+ withMediaMatching: '(max-width: 1250px)',
428
+ whereObservedRootSizeMatches: '(min-width: 700px)',
429
+ whereElementIntersectsWith:{
430
+ rootMargin: "0px",
431
+ threshold: 1.0,
432
+ },
433
+ whereInstanceOf: [HTMLMarqueeElement], //or 'HTMLMarqueeElement'
434
+ whereLangIn: ['en-GB'], // Cannot be implemented - see https://github.com/whatwg/html/issues/7039
435
+ whereConnectionHas:{
436
+ effectiveTypeIn: ["slow-2g"],
437
+ },
438
+ import: ['./my-element-small.css', {type: 'css'}],
439
+ do: ...
440
+ });
441
+ ```
442
+
443
+ [whereInstanceOf implemented as [Requirement5](requirements/Done/Requirement5.md)]
444
+ [whereObservedRootSizeMatches implemented]
445
+ [whereElementIntersectsWith implemented]
446
+ [whereConnectionHas implemented]
423
447
 
424
- Similar to the `do` function, the `withInstance` check can also be moved to imported modules for 100% JSON-serializable configuration:
448
+ [withMediaMatching implemented as [Requirement6](requirements/Done/Requirement6.md)]
449
+
450
+ ## InstanceOf checks in detail
451
+
452
+ Carving out the special "whereInstanceOf" check is provided based on the assumption that there's a performance benefit from doing so. If not, the developer could just add that check inside the "confirm" callback logic (discussed later). For built-in elements, we can alternatively provide the string name, as indicated in the comment above, which certainly makes it JSON serializable, thus making it easy as pie to include in the MOSE JSON payload. I don't think there would be any ambiguity in doing so, which means I believe that answers the mystery in my mind whether it could be part of the low-level checklist that could be done within the c++/rust code / thread.
453
+
454
+ The picture becomes murkier for custom elements. The best solution in that case seems to be to utilize customElements.getName(...) as a basis for the match, but at first glance, that could preclude being able to use base classes which a family of custom elements subclass, if that superclass isn't itself a custom element. I suppose the solution to this conundrum, when warranted, is simply to burden the developer with defining a custom element for the superclass, and thus assigning it a name, applicable within ShadowDOM scopes as needed, even though it isn't actually necessarily used for any live custom elements. This would require already having imported the base class, only benefitting from lazy loading the code needed for each sub class, which might not always be all that high as a percentage, compared to the base class.
455
+
456
+ However, where this support for "whereInstanceOf" would be *most* helpful is when it comes to [*custom enhancements*](https://github.com/WICG/webcomponents/issues/1000) that only wish to lazily layer some heavy lifting functionality on top of certain families of already loaded and upgraded custom elements (possibly in addition to some (specified) built in elements). Here, the lazy loading of the *entire custom **enhancement***, based on the presence in the DOM of a member of the family of custom elements, would, if my calculations are correct, result in providing a significant benefit.
457
+
458
+
459
+ <!--
460
+
461
+ [TODO] Maybe should also (optionally?) pass back which checks failed and which succeeded on dismount. Not sure I really see a use case for it, but leaving the thought here for now
462
+
463
+ -->
464
+
465
+ ### Referenced whereInstanceOf
466
+
467
+ Similar to the `do` function, the `whereInstanceOf` check can also be moved to imported modules for 100% JSON-serializable configuration:
425
468
 
426
469
  ```javascript
427
470
  // module mySettings.js
@@ -432,9 +475,9 @@ const doFunction = function({localName}, {modules, observer, MountConfig, rootNo
432
475
  observer.disconnectedSignal.abort();
433
476
  };
434
477
 
435
- const withInstance = [HTMLMarqueeElement, SVGElement];
478
+ const whereInstanceOf = [HTMLMarqueeElement, SVGElement];
436
479
 
437
- export { doFunction as do, withInstance };
480
+ export { doFunction as do, whereInstanceOf };
438
481
 
439
482
  // my local module
440
483
  const observer = new MountObserver({
@@ -450,15 +493,15 @@ observer.observe(document);
450
493
  ```
451
494
 
452
495
  **Behavior:**
453
- - **Combining checks**: If both inline `withInstance` and referenced `withInstance` exist, they are AND'd together (element must match both)
454
- - **Multiple references**: If multiple referenced modules export `withInstance`, the element must match ALL of them (AND logic)
455
- - **Validation**: Referenced `withInstance` is validated after imports load. Throws an error if not a Constructor or array of Constructors
456
- - **Optional export**: If a referenced module doesn't export `withInstance`, it's silently ignored
496
+ - **Combining checks**: If both inline `whereInstanceOf` and referenced `whereInstanceOf` exist, they are AND'd together (element must match both)
497
+ - **Multiple references**: If multiple referenced modules export `whereInstanceOf`, the element must match ALL of them (AND logic)
498
+ - **Validation**: Referenced `whereInstanceOf` is validated after imports load. Throws an error if not a Constructor or array of Constructors
499
+ - **Optional export**: If a referenced module doesn't export `whereInstanceOf`, it's silently ignored
457
500
  - **Timing**:
458
- - With lazy loading (default): Inline `withInstance` is checked first (before imports), then referenced checks happen after imports load
501
+ - With lazy loading (default): Inline `whereInstanceOf` is checked first (before imports), then referenced checks happen after imports load
459
502
  - With `loadingEagerness: 'eager'`: Both inline and referenced checks happen together after imports are loaded
460
503
 
461
- This optimization ensures that with lazy loading, elements that don't match the inline `withInstance` won't trigger unnecessary imports.
504
+ This optimization ensures that with lazy loading, elements that don't match the inline `whereInstanceOf` won't trigger unnecessary imports.
462
505
 
463
506
  [Implemented as [Requirement12](requirements/Done/Requirement12.md)]
464
507
 
@@ -563,7 +606,7 @@ export { doFunction as do };
563
606
  </script>
564
607
  ```
565
608
 
566
- To keep this proposal / polyfill of reasonable size, mount observer script elements has its own [repo / sub-proposal](https://github.com/bahrus/mount-observer-script-element). There's much more to it, but it is awaiting implementation of scoped custom element registry before finalizing the requirements and (re)-implementing.
609
+ To keep this proposal / polyfill of reasonable size, mount observer script elements has its own [repo / sub-proposal](https://github.com/bahrus/mount-observer-script-element). There's much more to it, including support for inheritance across containing scoped custom element registries.
567
610
 
568
611
  But I think it's important to think about this way of making the mount observer declarative, as it provides one significant reason why we place so much emphasis on making sure that the mount observer settings (MountConfig) is as JSON serializable as possible.
569
612
 
@@ -719,6 +762,83 @@ The handler registry is global and shared across all MountObserver instances, si
719
762
 
720
763
  [Implemented as [Requirement14](requirements/Done/Requirement14.md)]
721
764
 
765
+ ### Handler defaults with static properties
766
+
767
+ Registered handler classes can specify default MountConfig properties using static class properties. When you reference a handler by name, its static properties are automatically merged with your inline configuration, with inline config always taking precedence:
768
+
769
+ ```JavaScript
770
+ import {EvtRt} from 'mount-observer/EvtRt.js';
771
+
772
+ class MyHandler extends EvtRt {
773
+ static matching = 'div > p + p ~ span[class$="name"]';
774
+ static whereInstanceOf = HTMLSpanElement;
775
+
776
+ mount(mountedElement, MountConfig, context){
777
+ mountedElement.textContent = 'hello';
778
+ }
779
+ dismount(mountedElement, MountConfig){
780
+ mountedElement.textContent = 'bye';
781
+ }
782
+ }
783
+
784
+ // Register the handler
785
+ MountObserver.define('myHandler', MyHandler);
786
+
787
+ // Use with defaults - will use handler's matching and whereInstanceOf
788
+ const observer1 = new MountObserver({
789
+ do: 'myHandler'
790
+ });
791
+ observer1.observe(document);
792
+
793
+ // Override specific properties - inline config trumps handler defaults
794
+ const observer2 = new MountObserver({
795
+ matching: 'span.special', // This overrides the handler's matching
796
+ do: 'myHandler' // Still uses handler's whereInstanceOf
797
+ });
798
+ observer2.observe(document);
799
+ ```
800
+
801
+ **How it works:**
802
+ 1. When `do` is a string reference to a registered handler, the handler's static properties are extracted
803
+ 2. Static properties are merged with the inline config using object spread
804
+ 3. Inline config properties always override handler defaults (inline trumps)
805
+ 4. All MountConfig properties can be specified as static properties (matching, whereInstanceOf, withMediaMatching, etc.)
806
+
807
+ **Benefits:**
808
+ - **DRY principle**: Define common configuration once in the handler class
809
+ - **Flexibility**: Override any property when needed for specific use cases
810
+ - **Composability**: Handlers become self-contained with their own default behavior
811
+ - **JSON serialization**: Configurations remain JSON-serializable since only the handler name is referenced
812
+
813
+ **Example with multiple properties:**
814
+
815
+ ```JavaScript
816
+ class InputHandler extends EvtRt {
817
+ static matching = 'input[type="text"]';
818
+ static whereInstanceOf = HTMLInputElement;
819
+ static withMediaMatching = '(min-width: 768px)';
820
+
821
+ mount(mountedElement, MountConfig, context){
822
+ mountedElement.placeholder = 'Enter text...';
823
+ }
824
+ }
825
+
826
+ MountObserver.define('inputHandler', InputHandler);
827
+
828
+ // Uses all handler defaults
829
+ const observer = new MountObserver({
830
+ do: 'inputHandler'
831
+ });
832
+
833
+ // Partially override - keeps whereInstanceOf and withMediaMatching from handler
834
+ const observer2 = new MountObserver({
835
+ matching: 'input[type="email"]', // Override matching only
836
+ do: 'inputHandler'
837
+ });
838
+ ```
839
+
840
+ [Implemented as [SupportWhereCriteriaWithRegisteredActions](requirements/SupportWhereCriteriaWithRegisteredActions.md)]
841
+
722
842
  ### Built in handlers
723
843
 
724
844
  This proposal advocates having the platform provide some built in handlers, that extend EvtRt, that is included with this Polyfill.
@@ -1391,45 +1511,8 @@ const observer = new MountObserver({
1391
1511
  });
1392
1512
  ```
1393
1513
 
1394
- ## Media / container queries / instanceOf / custom checks [TODO] out of date
1395
-
1396
- Unlike traditional CSS @import, CSS Modules don't support specifying different imports based on media queries. That can be another condition we can attach (and why not throw in container queries, based on the rootNode?):
1397
-
1398
- ```JavaScript
1399
- const observer = new MountObserver({
1400
- select: 'div > p + p ~ span[class$="name"]', // not supported by polyfill
1401
- withMediaMatching: '(max-width: 1250px)',
1402
- whereSizeOfContainerMatches: '(min-width: 700px)',
1403
- whereContainerHas: '[itemprop=isActive][value="true"]',
1404
- withInstance: [HTMLMarqueeElement], //or ['HTMLMarqueeElement']
1405
- whereLangIn: ['en-GB'],
1406
- whereConnectionHas:{
1407
- effectiveTypeIn: ["slow-2g"],
1408
- },
1409
- import: ['./my-element-small.css', {type: 'css'}],
1410
- do: ...
1411
- });
1412
- ```
1413
-
1414
- [withInstance implemented as [Requirement5](requirements/Done/Requirement5.md)]
1415
-
1416
- [withMediaMatching implemented as [Requirement6](requirements/Done/Requirement6.md)]
1417
-
1418
- ## InstanceOf checks in detail
1419
-
1420
- Carving out the special "withInstance" check is provided based on the assumption that there's a performance benefit from doing so. If not, the developer could just add that check inside the "confirm" callback logic (discussed later). For built-in elements, we can alternatively provide the string name, as indicated in the comment above, which certainly makes it JSON serializable, thus making it easy as pie to include in the MOSE JSON payload. I don't think there would be any ambiguity in doing so, which means I believe that answers the mystery in my mind whether it could be part of the low-level checklist that could be done within the c++/rust code / thread.
1421
-
1422
- The picture becomes murkier for custom elements. The best solution in that case seems to be to utilize customElements.getName(...) as a basis for the match, but at first glance, that could preclude being able to use base classes which a family of custom elements subclass, if that superclass isn't itself a custom element. I suppose the solution to this conundrum, when warranted, is simply to burden the developer with defining a custom element for the superclass, and thus assigning it a name, applicable within ShadowDOM scopes as needed, even though it isn't actually necessarily used for any live custom elements. This would require already having imported the base class, only benefitting from lazy loading the code needed for each sub class, which might not always be all that high as a percentage, compared to the base class.
1423
-
1424
- However, where this support for "withInstance" would be *most* helpful is when it comes to [*custom enhancements*](https://github.com/WICG/webcomponents/issues/1000) that only wish to lazily layer some heavy lifting functionality on top of certain families of already loaded and upgraded custom elements (possibly in addition to some (specified) built in elements). Here, the lazy loading of the *entire custom **enhancement***, based on the presence in the DOM of a member of the family of custom elements, would, if my calculations are correct, result in providing a significant benefit.
1425
1514
 
1426
1515
 
1427
- <!--
1428
-
1429
- [TODO] Maybe should also (optionally?) pass back which checks failed and which succeeded on dismount. Not sure I really see a use case for it, but leaving the thought here for now
1430
-
1431
- -->
1432
-
1433
1516
  ## Subscribing
1434
1517
 
1435
1518
  Subscribing can be done via:
@@ -1512,7 +1595,7 @@ So the dismount event should provide a "checklist" of all the conditions, and th
1512
1595
  mediaMatches: true,
1513
1596
  containerMatches: true,
1514
1597
  satisfiesCustomConditiselect: true,
1515
- whereLangIn: ['en-GB'],
1598
+ // whereLangIn: ['en-GB'], // Not implemented - requires platform support
1516
1599
  whereConnectiselect:{
1517
1600
  effectiveTypeMatches: true
1518
1601
  },
@@ -1824,8 +1907,7 @@ Just as it is useful to be able lazy load external imports when needed, it would
1824
1907
  <template mount='{
1825
1908
  "select": ":not([defer-loading])",
1826
1909
  "loadingEagerness": "eager",
1827
- "withMediaMatching": "(min-width: 700px)",
1828
- "whereLangIn": ["en-GB"],
1910
+ "withMediaMatching": "(min-width: 700px)"
1829
1911
  }'>
1830
1912
  <div>I don't know why you say <slot name=slot2></slot> I say <slot name=slot1></slot></div>
1831
1913
  </template>
@@ -1833,8 +1915,7 @@ Just as it is useful to be able lazy load external imports when needed, it would
1833
1915
  <template mount='{
1834
1916
  "select": ":not([defer-loading])",
1835
1917
  "loadingEagerness": "lazy",
1836
- "withMediaMatching": "(max-width: 700px)",
1837
- "whereLangIn": ["fr"],
1918
+ "withMediaMatching": "(max-width: 700px)"
1838
1919
  }'>
1839
1920
  <div>Je ne sais pas pourquoi tu dis <slot name=slot2></slot> je dis <slot name=slot1></slot></div>
1840
1921
  </template>
@@ -0,0 +1,116 @@
1
+ import { DismountEvent } from './Events.js';
2
+ export function setupConnectionMonitor(init, rootNodeRef, mountedElements, modules, observer, processNode) {
3
+ const { whereConnectionHas } = init;
4
+ if (!whereConnectionHas) {
5
+ throw new Error('whereConnectionHas is required');
6
+ }
7
+ // Get connection object with vendor prefixes
8
+ const nav = navigator;
9
+ const connection = nav.connection || nav.mozConnection || nav.webkitConnection;
10
+ // If Network Information API is not supported, warn and pass the condition
11
+ if (!connection) {
12
+ console.warn('Network Information API is not supported in this browser. whereConnectionHas condition will be ignored.');
13
+ return {
14
+ conditionMatches: true,
15
+ cleanup: () => { }
16
+ };
17
+ }
18
+ // Check initial condition
19
+ let conditionMatches = evaluateConnectionCondition(connection, whereConnectionHas);
20
+ // Set up change listener
21
+ const changeHandler = () => {
22
+ const previousMatches = conditionMatches;
23
+ conditionMatches = evaluateConnectionCondition(connection, whereConnectionHas);
24
+ if (conditionMatches && !previousMatches) {
25
+ // Connection now matches - process elements
26
+ handleConditionMatch();
27
+ }
28
+ else if (!conditionMatches && previousMatches) {
29
+ // Connection no longer matches - dismount all elements
30
+ handleConditionUnmatch();
31
+ }
32
+ };
33
+ function handleConditionMatch() {
34
+ // Process all elements in the observed node
35
+ const rootNode = rootNodeRef.deref();
36
+ if (rootNode) {
37
+ processNode(rootNode);
38
+ }
39
+ }
40
+ function handleConditionUnmatch() {
41
+ // Dismount all currently mounted elements
42
+ const rootNode = rootNodeRef.deref();
43
+ if (!rootNode) {
44
+ return;
45
+ }
46
+ const context = {
47
+ modules,
48
+ observer: observer,
49
+ rootNode,
50
+ MountConfig: init
51
+ };
52
+ // Get all mounted elements from the WeakDual setWeak
53
+ const mountedElementsList = [];
54
+ for (const ref of mountedElements.setWeak) {
55
+ const element = ref.deref();
56
+ if (element) {
57
+ mountedElementsList.push(element);
58
+ }
59
+ }
60
+ // Dismount each element
61
+ for (const element of mountedElementsList) {
62
+ // Remove from both structures
63
+ mountedElements.weakSet.delete(element);
64
+ for (const ref of mountedElements.setWeak) {
65
+ if (ref.deref() === element) {
66
+ mountedElements.setWeak.delete(ref);
67
+ break;
68
+ }
69
+ }
70
+ // Dispatch dismount event with reason
71
+ observer.dispatchEvent(new DismountEvent(element, 'connection-failed', init));
72
+ }
73
+ }
74
+ // Listen for connection changes
75
+ connection.addEventListener('change', changeHandler);
76
+ return {
77
+ conditionMatches,
78
+ cleanup: () => {
79
+ connection.removeEventListener('change', changeHandler);
80
+ }
81
+ };
82
+ }
83
+ /**
84
+ * Evaluate if the current connection meets the specified conditions
85
+ */
86
+ function evaluateConnectionCondition(connection, condition) {
87
+ // Check effectiveType (e.g., 'slow-2g', '2g', '3g', '4g')
88
+ if (condition.effectiveTypeIn && condition.effectiveTypeIn.length > 0) {
89
+ const effectiveType = connection.effectiveType;
90
+ if (!effectiveType || !condition.effectiveTypeIn.includes(effectiveType)) {
91
+ return false;
92
+ }
93
+ }
94
+ // Check downlink (bandwidth in Mbps)
95
+ if (condition.downlinkMin !== undefined) {
96
+ const downlink = connection.downlink;
97
+ if (downlink === undefined || downlink < condition.downlinkMin) {
98
+ return false;
99
+ }
100
+ }
101
+ if (condition.downlinkMax !== undefined) {
102
+ const downlink = connection.downlink;
103
+ if (downlink === undefined || downlink > condition.downlinkMax) {
104
+ return false;
105
+ }
106
+ }
107
+ // Check RTT (round-trip time in ms)
108
+ if (condition.rttMax !== undefined) {
109
+ const rtt = connection.rtt;
110
+ if (rtt === undefined || rtt > condition.rttMax) {
111
+ return false;
112
+ }
113
+ }
114
+ // All conditions passed
115
+ return true;
116
+ }
@@ -0,0 +1,164 @@
1
+ // Network connection monitoring for MountObserver
2
+ import type { MountConfig, MountContext, WeakDual, ConnectionCondition } from './types/mount-observer/types.js';
3
+ import { DismountEvent } from './Events.js';
4
+
5
+ // Extend Navigator type to include connection properties
6
+ interface NetworkInformation extends EventTarget {
7
+ downlink?: number;
8
+ effectiveType?: string;
9
+ rtt?: number;
10
+ saveData?: boolean;
11
+ }
12
+
13
+ interface NavigatorWithConnection extends Navigator {
14
+ connection?: NetworkInformation;
15
+ mozConnection?: NetworkInformation;
16
+ webkitConnection?: NetworkInformation;
17
+ }
18
+
19
+ export function setupConnectionMonitor(
20
+ init: MountConfig,
21
+ rootNodeRef: WeakRef<Node>,
22
+ mountedElements: WeakDual<Element>,
23
+ modules: any[],
24
+ observer: EventTarget,
25
+ processNode: (node: Node) => void
26
+ ): {
27
+ conditionMatches: boolean;
28
+ cleanup: () => void;
29
+ } {
30
+ const { whereConnectionHas } = init;
31
+
32
+ if (!whereConnectionHas) {
33
+ throw new Error('whereConnectionHas is required');
34
+ }
35
+
36
+ // Get connection object with vendor prefixes
37
+ const nav = navigator as NavigatorWithConnection;
38
+ const connection = nav.connection || nav.mozConnection || nav.webkitConnection;
39
+
40
+ // If Network Information API is not supported, warn and pass the condition
41
+ if (!connection) {
42
+ console.warn('Network Information API is not supported in this browser. whereConnectionHas condition will be ignored.');
43
+ return {
44
+ conditionMatches: true,
45
+ cleanup: () => {}
46
+ };
47
+ }
48
+
49
+ // Check initial condition
50
+ let conditionMatches = evaluateConnectionCondition(connection, whereConnectionHas);
51
+
52
+ // Set up change listener
53
+ const changeHandler = () => {
54
+ const previousMatches = conditionMatches;
55
+ conditionMatches = evaluateConnectionCondition(connection, whereConnectionHas);
56
+
57
+ if (conditionMatches && !previousMatches) {
58
+ // Connection now matches - process elements
59
+ handleConditionMatch();
60
+ } else if (!conditionMatches && previousMatches) {
61
+ // Connection no longer matches - dismount all elements
62
+ handleConditionUnmatch();
63
+ }
64
+ };
65
+
66
+ function handleConditionMatch(): void {
67
+ // Process all elements in the observed node
68
+ const rootNode = rootNodeRef.deref();
69
+ if (rootNode) {
70
+ processNode(rootNode);
71
+ }
72
+ }
73
+
74
+ function handleConditionUnmatch(): void {
75
+ // Dismount all currently mounted elements
76
+ const rootNode = rootNodeRef.deref();
77
+ if (!rootNode) {
78
+ return;
79
+ }
80
+
81
+ const context: MountContext = {
82
+ modules,
83
+ observer: observer as any,
84
+ rootNode,
85
+ MountConfig: init
86
+ };
87
+
88
+ // Get all mounted elements from the WeakDual setWeak
89
+ const mountedElementsList: Element[] = [];
90
+ for (const ref of mountedElements.setWeak) {
91
+ const element = ref.deref();
92
+ if (element) {
93
+ mountedElementsList.push(element);
94
+ }
95
+ }
96
+
97
+ // Dismount each element
98
+ for (const element of mountedElementsList) {
99
+ // Remove from both structures
100
+ mountedElements.weakSet.delete(element);
101
+ for (const ref of mountedElements.setWeak) {
102
+ if (ref.deref() === element) {
103
+ mountedElements.setWeak.delete(ref);
104
+ break;
105
+ }
106
+ }
107
+
108
+ // Dispatch dismount event with reason
109
+ observer.dispatchEvent(new DismountEvent(element, 'connection-failed', init));
110
+ }
111
+ }
112
+
113
+ // Listen for connection changes
114
+ connection.addEventListener('change', changeHandler);
115
+
116
+ return {
117
+ conditionMatches,
118
+ cleanup: () => {
119
+ connection.removeEventListener('change', changeHandler);
120
+ }
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Evaluate if the current connection meets the specified conditions
126
+ */
127
+ function evaluateConnectionCondition(
128
+ connection: NetworkInformation,
129
+ condition: ConnectionCondition
130
+ ): boolean {
131
+ // Check effectiveType (e.g., 'slow-2g', '2g', '3g', '4g')
132
+ if (condition.effectiveTypeIn && condition.effectiveTypeIn.length > 0) {
133
+ const effectiveType = connection.effectiveType;
134
+ if (!effectiveType || !condition.effectiveTypeIn.includes(effectiveType)) {
135
+ return false;
136
+ }
137
+ }
138
+
139
+ // Check downlink (bandwidth in Mbps)
140
+ if (condition.downlinkMin !== undefined) {
141
+ const downlink = connection.downlink;
142
+ if (downlink === undefined || downlink < condition.downlinkMin) {
143
+ return false;
144
+ }
145
+ }
146
+
147
+ if (condition.downlinkMax !== undefined) {
148
+ const downlink = connection.downlink;
149
+ if (downlink === undefined || downlink > condition.downlinkMax) {
150
+ return false;
151
+ }
152
+ }
153
+
154
+ // Check RTT (round-trip time in ms)
155
+ if (condition.rttMax !== undefined) {
156
+ const rtt = connection.rtt;
157
+ if (rtt === undefined || rtt > condition.rttMax) {
158
+ return false;
159
+ }
160
+ }
161
+
162
+ // All conditions passed
163
+ return true;
164
+ }