Skip to content

Commit 1506ad8

Browse files
feat: add clone stylesheet utility functions (#69)
1 parent 23832c8 commit 1506ad8

File tree

5 files changed

+194
-1
lines changed

5 files changed

+194
-1
lines changed

alessiofrittoli-env.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/// <reference types="@alessiofrittoli/fetcher" />
12
/// <reference types="@alessiofrittoli/type-utils" />
23

34
// NOTE: This file should not be edited

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@alessiofrittoli/web-utils",
3-
"version": "1.20.0",
3+
"version": "2.0.0-alpha.1",
44
"description": "Common TypeScript web utilities",
55
"author": {
66
"name": "Alessio Frittoli",
@@ -121,6 +121,7 @@
121121
},
122122
"dependencies": {
123123
"@alessiofrittoli/date-utils": "^4.1.0",
124+
"@alessiofrittoli/fetcher": "^1.1.0",
124125
"@alessiofrittoli/math-utils": "^1.18.0",
125126
"@alessiofrittoli/type-utils": "^1.9.0",
126127
"@alessiofrittoli/url-utils": "^3.7.1"

pnpm-lock.yaml

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
onlyBuiltDependencies:
2+
- '@alessiofrittoli/fetcher'
23
- '@alessiofrittoli/type-utils'
34
- esbuild
45
- unrs-resolver

src/dom.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { fetch } from '@alessiofrittoli/fetcher/fetch'
2+
import { Url, type UrlInput } from '@alessiofrittoli/url-utils'
3+
14
/**
25
* Prevent Element Overflow.
36
*
@@ -26,4 +29,155 @@ export const restoreScroll = ( target: HTMLElement = document.documentElement )
2629
target.style.removeProperty( 'padding-right' )
2730
target.style.removeProperty( '--scrollbar-size' )
2831

32+
}
33+
34+
/**
35+
* Represents a style input.
36+
*
37+
*
38+
* @property {UrlInput} - A URL string or input pointing to an external stylesheet
39+
* @property {HTMLStyleElement} - An HTML style element containing CSS rules
40+
* @property {CSSStyleSheet} - A CSS stylesheet object
41+
* @property {StyleSheetList} - A collection of CSS stylesheets
42+
*/
43+
export type Style = UrlInput | HTMLStyleElement | CSSStyleSheet | StyleSheetList
44+
45+
46+
/**
47+
* Represents a single style object or an array of style objects.
48+
*
49+
* @typeParam Style The style object type. See {@link Style}.
50+
*/
51+
export type Styles = Style | Style[]
52+
53+
54+
/**
55+
* Clones a StyleSheetList or array of CSSStyleSheets into an array of `HTMLStyleElement` objects.
56+
*
57+
* This function extracts CSS rules from each stylesheet and creates corresponding `<style>`
58+
* elements containing the serialized CSS text. If an error occurs while processing a stylesheet,
59+
* the error is logged and that stylesheet is skipped.
60+
*
61+
* @param styles The source `StyleSheetList` or array of `CSSStyleSheet` objects to clone.
62+
*
63+
* @returns An array of `HTMLStyleElement` objects, each containing the CSS rules from the source stylesheets.
64+
* Failed stylesheets are filtered out and not included in the result.
65+
*
66+
* @example
67+
*
68+
* ```ts
69+
* const styles = cloneStyleSheetList( document.styleSheets )
70+
* styles.forEach( style => shadowRoot.appendChild( style ) )
71+
* ```
72+
*/
73+
export const cloneStyleSheetList = ( styles: StyleSheetList | CSSStyleSheet[] ) => (
74+
[ ...styles ].map( ( { cssRules } ) => {
75+
try {
76+
77+
const style = document.createElement( 'style' )
78+
79+
for ( let i = 0; i < cssRules.length; i++ ) {
80+
const rule = cssRules[ i ]
81+
82+
if ( ! rule ) continue
83+
84+
style.appendChild(
85+
document.createTextNode( rule.cssText )
86+
)
87+
}
88+
89+
return style
90+
91+
} catch ( error ) {
92+
93+
console.error( 'Error while cloning styles.', error )
94+
95+
}
96+
} ).filter( Boolean ) as HTMLStyleElement[]
97+
)
98+
99+
100+
/**
101+
* Clones style sheets from various sources into new `HTMLStyleElement` instances.
102+
*
103+
* @param styles A single style source or array of style sources. Can be:
104+
* - `StyleSheetList`: A list of stylesheets
105+
* - `CSSStyleSheet`: A single stylesheet object
106+
* - `HTMLStyleElement`: A style DOM element
107+
* - `UrlInput`: A URL string or object pointing to a stylesheet
108+
*
109+
* @returns A promise that resolves to an array of cloned `HTMLStyleElement` nodes.
110+
* Each element is a new style element containing the CSS rules from the source.
111+
*/
112+
export const cloneStyleSheets = async ( styles: Styles ): Promise<HTMLStyleElement[]> => {
113+
114+
if ( ! Array.isArray( styles ) ) {
115+
return cloneStyleSheets( [ styles ] )
116+
}
117+
118+
const styleNodes: HTMLStyleElement[] = []
119+
const styleSheetList: StyleSheetList[] = []
120+
const styleSheets: CSSStyleSheet[] = []
121+
const styleElements: HTMLStyleElement[] = []
122+
const styleUrls: UrlInput[] = []
123+
124+
styles.forEach( style => {
125+
if ( style instanceof StyleSheetList ) {
126+
styleSheetList.push( style )
127+
return
128+
}
129+
if ( style instanceof CSSStyleSheet ) {
130+
styleSheets.push( style )
131+
return
132+
}
133+
if ( style instanceof HTMLStyleElement ) {
134+
styleElements.push( style )
135+
return
136+
}
137+
138+
styleUrls.push( style )
139+
} )
140+
141+
if ( styleSheetList.length > 0 ) {
142+
styleNodes.push( ...styleSheetList.flatMap(
143+
styleSheetList => cloneStyleSheetList( styleSheetList )
144+
) )
145+
}
146+
147+
if ( styleSheets.length > 0 ) {
148+
styleNodes.push( ...cloneStyleSheetList( styleSheets ) )
149+
}
150+
151+
if ( styleElements.length > 0 ) {
152+
styleNodes.push( ...styleElements.map( style => {
153+
const target = document.createElement( 'style' )
154+
target.appendChild(
155+
document.createTextNode( style.innerText )
156+
)
157+
return target
158+
} ) )
159+
}
160+
161+
await Promise.allSettled(
162+
styleUrls.map( async urlInput => {
163+
164+
const { data, error } = await fetch<string>( Url.format( urlInput ) )
165+
166+
if ( error ) throw error
167+
168+
const style = document.createElement( 'style' )
169+
170+
style.appendChild(
171+
document.createTextNode( data )
172+
)
173+
174+
styleNodes.push( style )
175+
176+
return style
177+
178+
} )
179+
)
180+
181+
return styleNodes
182+
29183
}

0 commit comments

Comments
 (0)