Skip to content

Commit bad0d9c

Browse files
authoredFeb 13, 2025··
Add option to selectively include style properties when cloning element (#436)
* feat: add `includeStyleProperties` option * the full list of computedStyle properties can be cached * users can now manually specify which style properties are included * docs: update docs
1 parent f0d0341 commit bad0d9c

File tree

8 files changed

+77
-19
lines changed

8 files changed

+77
-19
lines changed
 

‎README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,11 @@ Defaults to `false`
294294
A string indicating the image format. The default type is image/png; that type is also used if the given type isn't supported.
295295
When supplied, the toCanvas function will return a blob matching the given image type and quality.
296296

297-
Defaults to `image/png`
297+
Defaults to `image/png`
298+
299+
### includeStyleProperties
300+
301+
An array of style property names. Can be used to manually specify which style properties are included when cloning nodes. This can be useful for performance-critical scenarios.
298302

299303
## Browsers
300304

‎src/apply-style.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Options } from './types'
1+
import type { Options } from './types'
22

33
export function applyStyle<T extends HTMLElement>(
44
node: T,

‎src/clone-node.ts

+23-10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import type { Options } from './types'
22
import { clonePseudoElements } from './clone-pseudos'
3-
import { createImage, toArray, isInstanceOfElement } from './util'
3+
import {
4+
createImage,
5+
toArray,
6+
isInstanceOfElement,
7+
getStyleProperties,
8+
} from './util'
49
import { getMimeType } from './mimes'
510
import { resourceToDataURL } from './dataurl'
611

@@ -29,12 +34,12 @@ async function cloneVideoElement(video: HTMLVideoElement, options: Options) {
2934
return createImage(dataURL)
3035
}
3136

32-
async function cloneIFrameElement(iframe: HTMLIFrameElement) {
37+
async function cloneIFrameElement(iframe: HTMLIFrameElement, options: Options) {
3338
try {
3439
if (iframe?.contentDocument?.body) {
3540
return (await cloneNode(
3641
iframe.contentDocument.body,
37-
{},
42+
options,
3843
true,
3944
)) as HTMLBodyElement
4045
}
@@ -58,7 +63,7 @@ async function cloneSingleNode<T extends HTMLElement>(
5863
}
5964

6065
if (isInstanceOfElement(node, HTMLIFrameElement)) {
61-
return cloneIFrameElement(node)
66+
return cloneIFrameElement(node, options)
6267
}
6368

6469
return node.cloneNode(isSVGElement(node)) as T
@@ -114,7 +119,11 @@ async function cloneChildren<T extends HTMLElement>(
114119
return clonedNode
115120
}
116121

117-
function cloneCSSStyle<T extends HTMLElement>(nativeNode: T, clonedNode: T) {
122+
function cloneCSSStyle<T extends HTMLElement>(
123+
nativeNode: T,
124+
clonedNode: T,
125+
options: Options,
126+
) {
118127
const targetStyle = clonedNode.style
119128
if (!targetStyle) {
120129
return
@@ -125,7 +134,7 @@ function cloneCSSStyle<T extends HTMLElement>(nativeNode: T, clonedNode: T) {
125134
targetStyle.cssText = sourceStyle.cssText
126135
targetStyle.transformOrigin = sourceStyle.transformOrigin
127136
} else {
128-
toArray<string>(sourceStyle).forEach((name) => {
137+
getStyleProperties(options).forEach((name) => {
129138
let value = sourceStyle.getPropertyValue(name)
130139
if (name === 'font-size' && value.endsWith('px')) {
131140
const reducedFont =
@@ -177,10 +186,14 @@ function cloneSelectValue<T extends HTMLElement>(nativeNode: T, clonedNode: T) {
177186
}
178187
}
179188

180-
function decorate<T extends HTMLElement>(nativeNode: T, clonedNode: T): T {
189+
function decorate<T extends HTMLElement>(
190+
nativeNode: T,
191+
clonedNode: T,
192+
options: Options,
193+
): T {
181194
if (isInstanceOfElement(clonedNode, Element)) {
182-
cloneCSSStyle(nativeNode, clonedNode)
183-
clonePseudoElements(nativeNode, clonedNode)
195+
cloneCSSStyle(nativeNode, clonedNode, options)
196+
clonePseudoElements(nativeNode, clonedNode, options)
184197
cloneInputValue(nativeNode, clonedNode)
185198
cloneSelectValue(nativeNode, clonedNode)
186199
}
@@ -247,6 +260,6 @@ export async function cloneNode<T extends HTMLElement>(
247260
return Promise.resolve(node)
248261
.then((clonedNode) => cloneSingleNode(clonedNode, options) as Promise<T>)
249262
.then((clonedNode) => cloneChildren(node, clonedNode, options))
250-
.then((clonedNode) => decorate(node, clonedNode))
263+
.then((clonedNode) => decorate(node, clonedNode, options))
251264
.then((clonedNode) => ensureSVGSymbols(clonedNode, options))
252265
}

‎src/clone-pseudos.ts

+13-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { uuid, toArray } from './util'
1+
import type { Options } from './types'
2+
import { uuid, getStyleProperties } from './util'
23

34
type Pseudo = ':before' | ':after'
45

@@ -7,8 +8,8 @@ function formatCSSText(style: CSSStyleDeclaration) {
78
return `${style.cssText} content: '${content.replace(/'|"/g, '')}';`
89
}
910

10-
function formatCSSProperties(style: CSSStyleDeclaration) {
11-
return toArray<string>(style)
11+
function formatCSSProperties(style: CSSStyleDeclaration, options: Options) {
12+
return getStyleProperties(options)
1213
.map((name) => {
1314
const value = style.getPropertyValue(name)
1415
const priority = style.getPropertyPriority(name)
@@ -22,11 +23,12 @@ function getPseudoElementStyle(
2223
className: string,
2324
pseudo: Pseudo,
2425
style: CSSStyleDeclaration,
26+
options: Options,
2527
): Text {
2628
const selector = `.${className}:${pseudo}`
2729
const cssText = style.cssText
2830
? formatCSSText(style)
29-
: formatCSSProperties(style)
31+
: formatCSSProperties(style, options)
3032

3133
return document.createTextNode(`${selector}{${cssText}}`)
3234
}
@@ -35,6 +37,7 @@ function clonePseudoElement<T extends HTMLElement>(
3537
nativeNode: T,
3638
clonedNode: T,
3739
pseudo: Pseudo,
40+
options: Options,
3841
) {
3942
const style = window.getComputedStyle(nativeNode, pseudo)
4043
const content = style.getPropertyValue('content')
@@ -50,14 +53,17 @@ function clonePseudoElement<T extends HTMLElement>(
5053
}
5154

5255
const styleElement = document.createElement('style')
53-
styleElement.appendChild(getPseudoElementStyle(className, pseudo, style))
56+
styleElement.appendChild(
57+
getPseudoElementStyle(className, pseudo, style, options),
58+
)
5459
clonedNode.appendChild(styleElement)
5560
}
5661

5762
export function clonePseudoElements<T extends HTMLElement>(
5863
nativeNode: T,
5964
clonedNode: T,
65+
options: Options,
6066
) {
61-
clonePseudoElement(nativeNode, clonedNode, ':before')
62-
clonePseudoElement(nativeNode, clonedNode, ':after')
67+
clonePseudoElement(nativeNode, clonedNode, ':before', options)
68+
clonePseudoElement(nativeNode, clonedNode, ':after', options)
6369
}

‎src/types.ts

+6
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ export interface Options {
2323
* An object whose properties to be copied to node's style before rendering.
2424
*/
2525
style?: Partial<CSSStyleDeclaration>
26+
/**
27+
* An array of style properties to be copied to node's style before rendering.
28+
* For performance-critical scenarios, users may want to specify only the
29+
* required properties instead of all styles.
30+
*/
31+
includeStyleProperties?: string[]
2632
/**
2733
* A function taking DOM node as argument. Should return `true` if passed
2834
* node should be included in the output. Excluding node means excluding

‎src/util.ts

+16
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,22 @@ export function toArray<T>(arrayLike: any): T[] {
6565
return arr
6666
}
6767

68+
let styleProps: string[] | null = null
69+
export function getStyleProperties(options: Options = {}): string[] {
70+
if (styleProps) {
71+
return styleProps
72+
}
73+
74+
if (options.includeStyleProperties) {
75+
styleProps = options.includeStyleProperties
76+
return styleProps
77+
}
78+
79+
styleProps = toArray(window.getComputedStyle(document.documentElement))
80+
81+
return styleProps
82+
}
83+
6884
function px(node: HTMLElement, styleProperty: string) {
6985
const win = node.ownerDocument.defaultView || window
7086
const val = win.getComputedStyle(node).getPropertyValue(styleProperty)
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAAXNSR0IArs4c6QAAAXJJREFUeF7t3LENwDAMxEBp/6FlJFNcQU9AkHio897MTY8xsAVhWvwgBbF6FATrUZCCaAYwnm5IQTADGE4LKQhmAMNpIQXBDGA4LaQgmAEMp4UUBDOA4bSQgmAGMJwWUhDMAIbTQgqCGcBwWkhBMAMYTgspCGYAw2khBcEMYDgtpCCYAQynhRQEM4DhtJCCYAYwnBZSEMwAhtNCCoIZwHBaSEEwAxhOCykIZgDDaSEFwQxgOC2kIJgBDKeFFAQzgOG0kIJgBjCcnbl+lIOiFASK8aEUpCCYAQynhRQEM4DhtJCCYAYwnBZSEMwAhtNCCoIZwHBaSEEwAxhOCykIZgDDaSEFwQxgOC2kIJgBDKeFFAQzgOG0kIJgBjCcFlIQzACG00IKghnAcFpIQTADGE4LKQhmAMNpIQXBDGA4LaQgmAEMp4UUBDOA4bSQgmAGMJwWUhDMAIbTQgqCGcBwWkhBMAMYTgspCGYAw2khBcEMYDgPsePHnRi+YEEAAAAASUVORK5CYII=

‎test/spec/options.spec.ts

+12
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,18 @@ describe('work with options', () => {
6767
.catch(done)
6868
})
6969

70+
it('should only clone specified style properties when includeStyleProperties is provided', (done) => {
71+
bootstrap('style/node.html', 'style/style.css', 'style/image-include-style')
72+
.then((node) => {
73+
return toPng(node, {
74+
includeStyleProperties: ['width', 'height'],
75+
})
76+
})
77+
.then(check)
78+
.then(done)
79+
.catch(done)
80+
})
81+
7082
it('should combine dimensions and style', (done) => {
7183
bootstrap('scale/node.html', 'scale/style.css', 'scale/image')
7284
.then((node) => {

0 commit comments

Comments
 (0)
Please sign in to comment.