5
5
*/
6
6
7
7
// Based on https://github.com/jouni/j-elements/blob/main/test/old-components/Stylable.js
8
+ const mediaRulesCache = new WeakMap ( ) ;
8
9
9
10
/**
10
11
* Check if the media query is a non-standard "tag scoped selector".
@@ -21,47 +22,85 @@ function isTagScopedMedia(media) {
21
22
}
22
23
23
24
/**
24
- * Check if the media query string matches the given tag name.
25
- *
26
- * @param {string } media
27
- * @param {string } tagName
28
- * @return {boolean }
29
- */
30
- function matchesTagScopedMedia ( media , tagName ) {
31
- return media === tagName ;
32
- }
33
-
34
- /**
35
- * Recursively processes a style sheet for matching "tag scoped" rules.
25
+ * Recursively processes a style sheet for media rules that match
26
+ * the specified predicate.
36
27
*
37
28
* @param {CSSStyleSheet } styleSheet
38
- * @param {string } tagName
29
+ * @param {(rule: CSSRule) => boolean } predicate
30
+ * @return {Array<CSSMediaRule | CSSImportRule> }
39
31
*/
40
- function extractStyleSheetTagScopedCSSRules ( styleSheet , tagName ) {
41
- const matchingRules = [ ] ;
32
+ function extractMediaRulesFromStyleSheet ( styleSheet , predicate ) {
33
+ const result = [ ] ;
42
34
43
35
for ( const rule of styleSheet . cssRules ) {
44
36
const ruleType = rule . constructor . name ;
45
37
46
38
if ( ruleType === 'CSSImportRule' ) {
47
- if ( ! isTagScopedMedia ( rule . media . mediaText ) ) {
48
- matchingRules . push ( ...extractStyleSheetTagScopedCSSRules ( rule . styleSheet , tagName ) ) ;
49
- continue ;
50
- }
51
-
52
- if ( matchesTagScopedMedia ( rule . media . mediaText , tagName ) ) {
53
- matchingRules . push ( ...rule . styleSheet . cssRules ) ;
39
+ if ( predicate ( rule ) ) {
40
+ result . push ( rule ) ;
41
+ } else {
42
+ result . push ( ...extractMediaRulesFromStyleSheet ( rule . styleSheet , predicate ) ) ;
54
43
}
55
44
}
56
45
57
46
if ( ruleType === 'CSSMediaRule' ) {
58
- if ( matchesTagScopedMedia ( rule . media . mediaText , tagName ) ) {
59
- matchingRules . push ( ... rule . cssRules ) ;
47
+ if ( predicate ( rule ) ) {
48
+ result . push ( rule ) ;
60
49
}
61
50
}
62
51
}
63
52
64
- return matchingRules ;
53
+ return result ;
54
+ }
55
+
56
+ /**
57
+ * Deduplicates media rules by their CSS text, keeping the last occurrence.
58
+ *
59
+ * @param {Array<CSSMediaRule | CSSImportRule> } rules
60
+ * @return {Array<CSSMediaRule | CSSImportRule> }
61
+ */
62
+ function deduplicateMediaRules ( rules ) {
63
+ const seen = new Set ( ) ;
64
+ return rules . reduceRight ( ( deduped , rule ) => {
65
+ const key = rule . styleSheet ?. cssText ?? rule . cssText ;
66
+ if ( ! seen . has ( key ) ) {
67
+ seen . add ( key ) ;
68
+ deduped . unshift ( rule ) ;
69
+ }
70
+ return deduped ;
71
+ } , [ ] ) ;
72
+ }
73
+
74
+ /**
75
+ * Extracts all CSS rules from a style sheet that are contained in media queries
76
+ * with a "tag scoped selector" matching the specified tag name.
77
+ *
78
+ * This function caches the results for each style sheet to avoid
79
+ * reprocessing the same style sheet multiple times.
80
+ *
81
+ * @param {CSSStyleSheet } styleSheet
82
+ * @param {string } tagName
83
+ * @return {CSSRule[] }
84
+ */
85
+ function extractTagScopedCSSRulesFromStyleSheet ( styleSheet , tagName ) {
86
+ let mediaRules = mediaRulesCache . get ( styleSheet ) ;
87
+ if ( ! mediaRules ) {
88
+ // Collect all media rules that look like "tag scoped selectors", e.g. "@media vaadin-text-field { ... }"
89
+ mediaRules = extractMediaRulesFromStyleSheet ( styleSheet , ( rule ) => isTagScopedMedia ( rule . media . mediaText ) ) ;
90
+
91
+ // Remove duplicate media rules which may result from multiple imports of the same stylesheet
92
+ mediaRules = deduplicateMediaRules ( mediaRules ) ;
93
+
94
+ // Group rules by tag name specified in the media query
95
+ mediaRules = Map . groupBy ( mediaRules , ( rule ) => rule . media . mediaText ) ;
96
+
97
+ // Save the processed media rules in the cache
98
+ mediaRulesCache . set ( styleSheet , mediaRules ) ;
99
+ }
100
+
101
+ return ( mediaRules . get ( tagName ) ?? [ ] ) . flatMap ( ( mediaRule ) =>
102
+ Array . from ( mediaRule . styleSheet ?. cssRules ?? mediaRule . cssRules ) ,
103
+ ) ;
65
104
}
66
105
67
106
/**
@@ -81,10 +120,10 @@ function extractStyleSheetTagScopedCSSRules(styleSheet, tagName) {
81
120
* @return {CSSRule[] }
82
121
*/
83
122
export function extractTagScopedCSSRules ( root , tagName ) {
84
- const styleSheets = new Set ( [ ... root . styleSheets ] ) ;
85
- const adoptedStyleSheets = new Set ( [ ... root . adoptedStyleSheets ] ) ;
123
+ const styleSheets = new Set ( root . styleSheets ) ;
124
+ const adoptedStyleSheets = new Set ( root . adoptedStyleSheets ) ;
86
125
87
126
return [ ...styleSheets . union ( adoptedStyleSheets ) ] . flatMap ( ( styleSheet ) => {
88
- return extractStyleSheetTagScopedCSSRules ( styleSheet , tagName ) ;
127
+ return extractTagScopedCSSRulesFromStyleSheet ( styleSheet , tagName ) ;
89
128
} ) ;
90
129
}
0 commit comments