Skip to content
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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,22 @@ When supplied, the toCanvas function will return a blob matching the given image

Defaults to `image/png`

### includedPseudoElements

A list of pseudo elements which will be included when capture the image. `::before` and `::after` are included by default.

```javascript
htmlToImage.toPng(document.getElementById('my-node'), {
/* this will include the Chrome scrollbar pseudo elements */
includedPseudoElements: [
'::-webkit-scrollbar',
'::-webkit-scrollbar-thumb'
]
}).then(function (dataUrl) {
/* do something */
});
```

## Browsers

Only standard lib is currently used, but make sure your browser supports:
Expand Down
14 changes: 9 additions & 5 deletions src/clone-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,11 @@ function cloneCSSStyle<T extends HTMLElement>(nativeNode: T, clonedNode: T) {
) {
value = 'block'
}

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

targetStyle.setProperty(
name,
value,
Expand Down Expand Up @@ -170,10 +170,14 @@ function cloneSelectValue<T extends HTMLElement>(nativeNode: T, clonedNode: T) {
}
}

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)
clonePseudoElements(nativeNode, clonedNode, options.includedPseudoElements)
cloneInputValue(nativeNode, clonedNode)
cloneSelectValue(nativeNode, clonedNode)
}
Expand Down Expand Up @@ -240,6 +244,6 @@ export async function cloneNode<T extends HTMLElement>(
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))
}
13 changes: 7 additions & 6 deletions src/clone-pseudos.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { uuid, toArray } from './util'

type Pseudo = ':before' | ':after'
import { Pseudo } from './types'
import { uuid, toArray, deduplicate } from './util'

function formatCSSText(style: CSSStyleDeclaration) {
const content = style.getPropertyValue('content')
Expand All @@ -23,7 +22,7 @@ function getPseudoElementStyle(
pseudo: Pseudo,
style: CSSStyleDeclaration,
): Text {
const selector = `.${className}:${pseudo}`
const selector = `.${className}${pseudo}`
const cssText = style.cssText
? formatCSSText(style)
: formatCSSProperties(style)
Expand Down Expand Up @@ -57,7 +56,9 @@ function clonePseudoElement<T extends HTMLElement>(
export function clonePseudoElements<T extends HTMLElement>(
nativeNode: T,
clonedNode: T,
includedPseudoElements: Pseudo[] = [],
) {
clonePseudoElement(nativeNode, clonedNode, ':before')
clonePseudoElement(nativeNode, clonedNode, ':after')
deduplicate(['::before', '::after', ...includedPseudoElements]).forEach(
(pseudo: Pseudo) => clonePseudoElement(nativeNode, clonedNode, pseudo),
)
}
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export type Pseudo = `::${string}`

export interface Options {
/**
* Width in pixels to be applied to node before rendering.
Expand Down Expand Up @@ -91,4 +93,10 @@ export interface Options {
*
*/
fetchRequestInit?: RequestInit

/**
* A list of pseudo elements which will be included when capture the image.
* ::before and ::after are included by default.
*/
includedPseudoElements?: Pseudo[]
}
4 changes: 4 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,7 @@ export const isInstanceOfElement = <
isInstanceOfElement(nodePrototype, instance)
)
}

export const deduplicate = <T>(list: T[]): T[] => {
return list.sort().filter((item, i) => !i || item !== list[i - 1])
}
1 change: 1 addition & 0 deletions test/resources/included-pseudo-elements/image
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
data:image/png;base64,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
3 changes: 3 additions & 0 deletions test/resources/included-pseudo-elements/node.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div class="pseudo">
<div></div>
</div>
22 changes: 22 additions & 0 deletions test/resources/included-pseudo-elements/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.pseudo {
width: 100px;
height: 100px;
overflow: scroll;
}
.pseudo > div {
height: 200px;
}
.pseudo::-webkit-scrollbar {
height: 10px;
width: 10px;
background-color: red;
}
.pseudo::-webkit-scrollbar-thumb {
background: black;
}

#dom-node {
height: 200px;
width: 200px;
background-color: #fff;
}
19 changes: 19 additions & 0 deletions test/spec/options.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getSvgDocument,
compareToRefImage,
assertTextRendered,
renderAndCheck,
} from './helper'
import { toPng, toSvg } from '../../src'

Expand Down Expand Up @@ -173,4 +174,22 @@ describe('work with options', () => {
.then(done)
.catch(done)
})

it('should preserve content of pseudo elements in includedPseudoElements list', (done) => {
bootstrap(
'included-pseudo-elements/node.html',
'included-pseudo-elements/style.css',
'included-pseudo-elements/image',
)
.then((node) =>
renderAndCheck(node, {
includedPseudoElements: [
'::-webkit-scrollbar',
'::-webkit-scrollbar-thumb',
],
}),
)
.then(done)
.catch(done)
})
})