Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to selectively include style properties when cloning element #436

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,11 @@ Defaults to `false`
A string indicating the image format. The default type is image/png; that type is also used if the given type isn't supported.
When supplied, the toCanvas function will return a blob matching the given image type and quality.

Defaults to `image/png`
Defaults to `image/png`

### includeStyleProperties

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.

## Browsers

Expand Down
2 changes: 1 addition & 1 deletion src/apply-style.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Options } from './types'
import type { Options } from './types'

export function applyStyle<T extends HTMLElement>(
node: T,
Expand Down
37 changes: 25 additions & 12 deletions src/clone-node.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { Options } from './types'
import { clonePseudoElements } from './clone-pseudos'
import { createImage, toArray, isInstanceOfElement } from './util'
import {
createImage,
toArray,
isInstanceOfElement,
getStyleProperties,
} from './util'

Check warning on line 8 in src/clone-node.ts

View check run for this annotation

Codecov / codecov/patch

src/clone-node.ts#L8

Added line #L8 was not covered by tests
import { getMimeType } from './mimes'
import { resourceToDataURL } from './dataurl'

Expand Down Expand Up @@ -29,12 +34,12 @@
return createImage(dataURL)
}

async function cloneIFrameElement(iframe: HTMLIFrameElement) {
async function cloneIFrameElement(iframe: HTMLIFrameElement, options: Options) {
try {
if (iframe?.contentDocument?.body) {
return (await cloneNode(
iframe.contentDocument.body,
{},
options,
true,
)) as HTMLBodyElement
}
Expand All @@ -58,7 +63,7 @@
}

if (isInstanceOfElement(node, HTMLIFrameElement)) {
return cloneIFrameElement(node)
return cloneIFrameElement(node, options)
}

return node.cloneNode(false) as T
Expand Down Expand Up @@ -107,7 +112,11 @@
return clonedNode
}

function cloneCSSStyle<T extends HTMLElement>(nativeNode: T, clonedNode: T) {
function cloneCSSStyle<T extends HTMLElement>(
nativeNode: T,
clonedNode: T,
options: Options,
) {

Check warning on line 119 in src/clone-node.ts

View check run for this annotation

Codecov / codecov/patch

src/clone-node.ts#L118-L119

Added lines #L118 - L119 were not covered by tests
const targetStyle = clonedNode.style
if (!targetStyle) {
return
Expand All @@ -118,7 +127,7 @@
targetStyle.cssText = sourceStyle.cssText
targetStyle.transformOrigin = sourceStyle.transformOrigin
} else {
toArray<string>(sourceStyle).forEach((name) => {
getStyleProperties(options).forEach((name) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of allocating a new array with all the property names for each node in order call .forEach on it, it would be even faster to just use a good-old for loop to iterate over all the properties. Since CSSStyleDeclaration is "array-like", it works well:

const propertyNames = options.includeStyleProperties ?? sourceStyle;
for (let i = 0; i < propertyNames; ++i) {
  const name = propertyNames[i];
  let value = sourceStyle.getPropertyValue(name);
  // ...
}

let value = sourceStyle.getPropertyValue(name)
if (name === 'font-size' && value.endsWith('px')) {
const reducedFont =
Expand All @@ -133,11 +142,11 @@
) {
value = 'block'
}

if (name === 'd' && clonedNode.getAttribute('d')) {
value = `path(${clonedNode.getAttribute('d')})`
}

targetStyle.setProperty(
name,
value,
Expand Down Expand Up @@ -170,10 +179,14 @@
}
}

function decorate<T extends HTMLElement>(nativeNode: T, clonedNode: T): T {
function decorate<T extends HTMLElement>(
nativeNode: T,
clonedNode: T,
options: Options,
): T {
if (isInstanceOfElement(clonedNode, Element)) {
cloneCSSStyle(nativeNode, clonedNode)
clonePseudoElements(nativeNode, clonedNode)
cloneCSSStyle(nativeNode, clonedNode, options)
clonePseudoElements(nativeNode, clonedNode, options)
cloneInputValue(nativeNode, clonedNode)
cloneSelectValue(nativeNode, clonedNode)
}
Expand Down Expand Up @@ -240,6 +253,6 @@
return Promise.resolve(node)
.then((clonedNode) => cloneSingleNode(clonedNode, options) as Promise<T>)
.then((clonedNode) => cloneChildren(node, clonedNode, options))
.then((clonedNode) => decorate(node, clonedNode))
.then((clonedNode) => decorate(node, clonedNode, options))
.then((clonedNode) => ensureSVGSymbols(clonedNode, options))
}
20 changes: 13 additions & 7 deletions src/clone-pseudos.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { uuid, toArray } from './util'
import type { Options } from './types'
import { uuid, getStyleProperties } from './util'

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

Expand All @@ -7,8 +8,8 @@
return `${style.cssText} content: '${content.replace(/'|"/g, '')}';`
}

function formatCSSProperties(style: CSSStyleDeclaration) {
return toArray<string>(style)
function formatCSSProperties(style: CSSStyleDeclaration, options: Options) {

Check warning on line 11 in src/clone-pseudos.ts

View check run for this annotation

Codecov / codecov/patch

src/clone-pseudos.ts#L11

Added line #L11 was not covered by tests
return getStyleProperties(options)
.map((name) => {
const value = style.getPropertyValue(name)
const priority = style.getPropertyPriority(name)
Expand All @@ -22,11 +23,12 @@
className: string,
pseudo: Pseudo,
style: CSSStyleDeclaration,
options: Options,

Check warning on line 26 in src/clone-pseudos.ts

View check run for this annotation

Codecov / codecov/patch

src/clone-pseudos.ts#L26

Added line #L26 was not covered by tests
): Text {
const selector = `.${className}:${pseudo}`
const cssText = style.cssText
? formatCSSText(style)
: formatCSSProperties(style)
: formatCSSProperties(style, options)

Check warning on line 31 in src/clone-pseudos.ts

View check run for this annotation

Codecov / codecov/patch

src/clone-pseudos.ts#L31

Added line #L31 was not covered by tests

return document.createTextNode(`${selector}{${cssText}}`)
}
Expand All @@ -35,6 +37,7 @@
nativeNode: T,
clonedNode: T,
pseudo: Pseudo,
options: Options,
) {
const style = window.getComputedStyle(nativeNode, pseudo)
const content = style.getPropertyValue('content')
Expand All @@ -50,14 +53,17 @@
}

const styleElement = document.createElement('style')
styleElement.appendChild(getPseudoElementStyle(className, pseudo, style))
styleElement.appendChild(
getPseudoElementStyle(className, pseudo, style, options),
)
clonedNode.appendChild(styleElement)
}

export function clonePseudoElements<T extends HTMLElement>(
nativeNode: T,
clonedNode: T,
options: Options,
) {
clonePseudoElement(nativeNode, clonedNode, ':before')
clonePseudoElement(nativeNode, clonedNode, ':after')
clonePseudoElement(nativeNode, clonedNode, ':before', options)
clonePseudoElement(nativeNode, clonedNode, ':after', options)
}
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ export interface Options {
* An object whose properties to be copied to node's style before rendering.
*/
style?: Partial<CSSStyleDeclaration>
/**
* An array of style properties to be copied to node's style before rendering.
* For performance-critical scenarios, users may want to specify only the
* required properties instead of all styles.
*/
includeStyleProperties?: string[]
/**
* A function taking DOM node as argument. Should return `true` if passed
* node should be included in the output. Excluding node means excluding
Expand Down
16 changes: 16 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@ export function toArray<T>(arrayLike: any): T[] {
return arr
}

let styleProps: string[] | null = null
export function getStyleProperties(options: Options = {}): string[] {
if (styleProps) {
return styleProps
}

if (options.includeStyleProperties) {
styleProps = options.includeStyleProperties
return styleProps
}

styleProps = toArray(window.getComputedStyle(document.documentElement))

return styleProps
}

function px(node: HTMLElement, styleProperty: string) {
const win = node.ownerDocument.defaultView || window
const val = win.getComputedStyle(node).getPropertyValue(styleProperty)
Expand Down
1 change: 1 addition & 0 deletions test/resources/style/image-include-style
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAAXNSR0IArs4c6QAAAXJJREFUeF7t3LENwDAMxEBp/6FlJFNcQU9AkHio897MTY8xsAVhWvwgBbF6FATrUZCCaAYwnm5IQTADGE4LKQhmAMNpIQXBDGA4LaQgmAEMp4UUBDOA4bSQgmAGMJwWUhDMAIbTQgqCGcBwWkhBMAMYTgspCGYAw2khBcEMYDgtpCCYAQynhRQEM4DhtJCCYAYwnBZSEMwAhtNCCoIZwHBaSEEwAxhOCykIZgDDaSEFwQxgOC2kIJgBDKeFFAQzgOG0kIJgBjCcnbl+lIOiFASK8aEUpCCYAQynhRQEM4DhtJCCYAYwnBZSEMwAhtNCCoIZwHBaSEEwAxhOCykIZgDDaSEFwQxgOC2kIJgBDKeFFAQzgOG0kIJgBjCcFlIQzACG00IKghnAcFpIQTADGE4LKQhmAMNpIQXBDGA4LaQgmAEMp4UUBDOA4bSQgmAGMJwWUhDMAIbTQgqCGcBwWkhBMAMYTgspCGYAw2khBcEMYDgPsePHnRi+YEEAAAAASUVORK5CYII=
12 changes: 12 additions & 0 deletions test/spec/options.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ describe('work with options', () => {
.catch(done)
})

it('should only clone specified style properties when includeStyleProperties is provided', (done) => {
bootstrap('style/node.html', 'style/style.css', 'style/image-include-style')
.then((node) => {
return toPng(node, {
includeStyleProperties: ['width', 'height'],
})
})
.then(check)
.then(done)
.catch(done)
})

it('should combine dimensions and style', (done) => {
bootstrap('scale/node.html', 'scale/style.css', 'scale/image')
.then((node) => {
Expand Down
Loading