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/DefineCustomElementHandler.js +1 -0
- package/DefineCustomElementHandler.ts +1 -0
- package/EnhanceMountedElementHandler.js +3 -2
- package/EnhanceMountedElementHandler.ts +3 -2
- package/EvtRt.js +8 -3
- package/EvtRt.ts +12 -3
- package/MountObserver.js +119 -19
- package/MountObserver.ts +200 -56
- package/README.md +153 -74
- 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 +1 -1
- 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,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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
478
|
+
const whereInstanceOf = [HTMLMarqueeElement, SVGElement];
|
|
438
479
|
|
|
439
|
-
export { doFunction as do,
|
|
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 `
|
|
456
|
-
- **Multiple references**: If multiple referenced modules export `
|
|
457
|
-
- **Validation**: Referenced `
|
|
458
|
-
- **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
|
|
459
500
|
- **Timing**:
|
|
460
|
-
- With lazy loading (default): Inline `
|
|
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 `
|
|
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,
|
|
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
|
+
}
|