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/DefineCustomElementHandler.js +1 -0
- package/DefineCustomElementHandler.ts +1 -0
- package/EnhanceMountedElementHandler.js +26 -12
- package/EnhanceMountedElementHandler.ts +25 -13
- package/EvtRt.js +8 -3
- package/EvtRt.ts +12 -3
- package/MountObserver.js +119 -19
- package/MountObserver.ts +200 -56
- package/README.md +154 -73
- package/connectionMonitor.js +116 -0
- package/connectionMonitor.ts +164 -0
- package/elementIntersection.js +67 -0
- package/elementIntersection.ts +96 -0
- package/observedRootHas.js +87 -0
- package/package.json +2 -2
- package/rootSizeObserver.js +124 -0
- package/rootSizeObserver.ts +157 -0
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
|
-
- ✅ **
|
|
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:
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
478
|
+
const whereInstanceOf = [HTMLMarqueeElement, SVGElement];
|
|
436
479
|
|
|
437
|
-
export { doFunction as do,
|
|
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 `
|
|
454
|
-
- **Multiple references**: If multiple referenced modules export `
|
|
455
|
-
- **Validation**: Referenced `
|
|
456
|
-
- **Optional export**: If a referenced module doesn't export `
|
|
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 `
|
|
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 `
|
|
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,
|
|
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
|
+
}
|