mount-observer 0.1.11 → 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,11 +35,7 @@ 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
 
@@ -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:
@@ -100,29 +100,26 @@ The extra flexibility this new primitive would provide could be quite useful to
100
100
 
101
101
  Before getting into the weeds, let's demonstrate the two most prominent use cases:
102
102
 
103
- ### Use Case 1: Custom Attribute Enhancement [TODO]: out of date
103
+ ### Use Case 1: Custom Attribute Enhancement
104
104
 
105
105
  ```html
106
106
  <body>
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
-
112
-
113
- document.body.mount([{
114
- withAttrs:{base: 'log-to-console'},
115
- spawn: function(el){
110
+ document.body.mount({
111
+ matching: '[log-to-console]',
112
+ do: (el) => {
116
113
  el.addEventListener('click', e => {
117
114
  console.log(e.target.getAttribute('log-to-console'));
118
115
  });
119
- },
120
- }])
116
+ }
117
+ })
121
118
  </script>
122
119
  </body>
123
120
  ```
124
121
 
125
- See [this extending package](https://github.com/bahrus/mount-observer-script-element) that provides for a more declarative approach.
122
+
126
123
 
127
124
  ### Use Case 2: Lazy Global Custom Element Definition
128
125
 
@@ -153,6 +150,8 @@ document.mount({
153
150
 
154
151
  This registers custom elements with the global customElements registry.
155
152
 
153
+ See [this extending package](https://github.com/bahrus/mount-observer-script-element) that provides for a more declarative approach.
154
+
156
155
  ### Scoped
157
156
 
158
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":
@@ -173,13 +172,13 @@ The `builtIns.enhanceMountedElement` handler automatically enhances mounted elem
173
172
  // MyEnhancement.js
174
173
  class ButtonEnhancement {
175
174
  constructor(element, ctx, initVals) {
176
- this.element = element;
175
+ this.element = new WeakRef(element);
177
176
  this.ctx = ctx;
178
177
  this.clickCount = 0;
179
178
 
180
- element.addEventListener('click', () => {
179
+ element.addEventListener('click', ({target}) => {
181
180
  this.clickCount++;
182
- element.setAttribute('data-clicks', this.clickCount);
181
+ target.setAttribute('data-clicks', this.clickCount);
183
182
  });
184
183
  }
185
184
  }
@@ -189,9 +188,6 @@ export default {
189
188
  enhKey: 'buttonEnh'
190
189
  };
191
190
 
192
- // main.js
193
- import 'mount-observer/ElementMountExtension.js';
194
-
195
191
  document.mount({
196
192
  matching: '.enhance-me',
197
193
  import: './MyEnhancement.js',
@@ -421,9 +417,54 @@ reference: [2, 3] // Both actions1 and actions2 will have their 'do' called if
421
417
 
422
418
  [Implemented as [Requirement11](requirements/Done/Requirement11.md)]
423
419
 
424
- ### 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
+ ```
425
442
 
426
- Similar to the `do` function, the `withInstance` check can also be moved to imported modules for 100% JSON-serializable configuration:
443
+ [whereInstanceOf implemented as [Requirement5](requirements/Done/Requirement5.md)]
444
+ [whereObservedRootSizeMatches implemented]
445
+ [whereElementIntersectsWith implemented]
446
+ [whereConnectionHas implemented]
447
+
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:
427
468
 
428
469
  ```javascript
429
470
  // module mySettings.js
@@ -434,9 +475,9 @@ const doFunction = function({localName}, {modules, observer, MountConfig, rootNo
434
475
  observer.disconnectedSignal.abort();
435
476
  };
436
477
 
437
- const withInstance = [HTMLMarqueeElement, SVGElement];
478
+ const whereInstanceOf = [HTMLMarqueeElement, SVGElement];
438
479
 
439
- export { doFunction as do, withInstance };
480
+ export { doFunction as do, whereInstanceOf };
440
481
 
441
482
  // my local module
442
483
  const observer = new MountObserver({
@@ -452,15 +493,15 @@ observer.observe(document);
452
493
  ```
453
494
 
454
495
  **Behavior:**
455
- - **Combining checks**: If both inline `withInstance` and referenced `withInstance` exist, they are AND'd together (element must match both)
456
- - **Multiple references**: If multiple referenced modules export `withInstance`, the element must match ALL of them (AND logic)
457
- - **Validation**: Referenced `withInstance` is validated after imports load. Throws an error if not a Constructor or array of Constructors
458
- - **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
459
500
  - **Timing**:
460
- - 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
461
502
  - With `loadingEagerness: 'eager'`: Both inline and referenced checks happen together after imports are loaded
462
503
 
463
- 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.
464
505
 
465
506
  [Implemented as [Requirement12](requirements/Done/Requirement12.md)]
466
507
 
@@ -565,7 +606,7 @@ export { doFunction as do };
565
606
  </script>
566
607
  ```
567
608
 
568
- 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.
569
610
 
570
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.
571
612
 
@@ -721,6 +762,83 @@ The handler registry is global and shared across all MountObserver instances, si
721
762
 
722
763
  [Implemented as [Requirement14](requirements/Done/Requirement14.md)]
723
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
+
724
842
  ### Built in handlers
725
843
 
726
844
  This proposal advocates having the platform provide some built in handlers, that extend EvtRt, that is included with this Polyfill.
@@ -1393,45 +1511,8 @@ const observer = new MountObserver({
1393
1511
  });
1394
1512
  ```
1395
1513
 
1396
- ## Media / container queries / instanceOf / custom checks [TODO] out of date
1397
-
1398
- 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?):
1399
-
1400
- ```JavaScript
1401
- const observer = new MountObserver({
1402
- select: 'div > p + p ~ span[class$="name"]', // not supported by polyfill
1403
- withMediaMatching: '(max-width: 1250px)',
1404
- whereSizeOfContainerMatches: '(min-width: 700px)',
1405
- whereContainerHas: '[itemprop=isActive][value="true"]',
1406
- withInstance: [HTMLMarqueeElement], //or ['HTMLMarqueeElement']
1407
- whereLangIn: ['en-GB'],
1408
- whereConnectionHas:{
1409
- effectiveTypeIn: ["slow-2g"],
1410
- },
1411
- import: ['./my-element-small.css', {type: 'css'}],
1412
- do: ...
1413
- });
1414
- ```
1415
-
1416
- [withInstance implemented as [Requirement5](requirements/Done/Requirement5.md)]
1417
-
1418
- [withMediaMatching implemented as [Requirement6](requirements/Done/Requirement6.md)]
1419
-
1420
- ## InstanceOf checks in detail
1421
-
1422
- 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.
1423
-
1424
- 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.
1425
-
1426
- 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.
1427
1514
 
1428
1515
 
1429
- <!--
1430
-
1431
- [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
1432
-
1433
- -->
1434
-
1435
1516
  ## Subscribing
1436
1517
 
1437
1518
  Subscribing can be done via:
@@ -1514,7 +1595,7 @@ So the dismount event should provide a "checklist" of all the conditions, and th
1514
1595
  mediaMatches: true,
1515
1596
  containerMatches: true,
1516
1597
  satisfiesCustomConditiselect: true,
1517
- whereLangIn: ['en-GB'],
1598
+ // whereLangIn: ['en-GB'], // Not implemented - requires platform support
1518
1599
  whereConnectiselect:{
1519
1600
  effectiveTypeMatches: true
1520
1601
  },
@@ -1826,8 +1907,7 @@ Just as it is useful to be able lazy load external imports when needed, it would
1826
1907
  <template mount='{
1827
1908
  "select": ":not([defer-loading])",
1828
1909
  "loadingEagerness": "eager",
1829
- "withMediaMatching": "(min-width: 700px)",
1830
- "whereLangIn": ["en-GB"],
1910
+ "withMediaMatching": "(min-width: 700px)"
1831
1911
  }'>
1832
1912
  <div>I don't know why you say <slot name=slot2></slot> I say <slot name=slot1></slot></div>
1833
1913
  </template>
@@ -1835,8 +1915,7 @@ Just as it is useful to be able lazy load external imports when needed, it would
1835
1915
  <template mount='{
1836
1916
  "select": ":not([defer-loading])",
1837
1917
  "loadingEagerness": "lazy",
1838
- "withMediaMatching": "(max-width: 700px)",
1839
- "whereLangIn": ["fr"],
1918
+ "withMediaMatching": "(max-width: 700px)"
1840
1919
  }'>
1841
1920
  <div>Je ne sais pas pourquoi tu dis <slot name=slot2></slot> je dis <slot name=slot1></slot></div>
1842
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
+ }