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

feat: add shadow DOM support #171

Open
wants to merge 2 commits into
base: main
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
42 changes: 40 additions & 2 deletions src/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,51 @@ export const isHTMLImageElement = (element: Element): element is HTMLImageElemen
element.tagName === 'IMG' && isHTMLElement(element)
export const isHTMLInputElement = (element: Element): element is HTMLInputElement =>
element.tagName === 'INPUT' && isHTMLElement(element)
export const isHTMLSlotElement = (element: Element): element is HTMLSlotElement =>
element.tagName === 'SLOT' && isHTMLElement(element)
export const hasLabels = (element: HTMLElement): element is HTMLElement & Pick<HTMLInputElement, 'labels'> =>
'labels' in element
export const hasOpenShadowRoot = (
element: Element
): element is Omit<Element, 'shadowRoot'> & { shadowRoot: NonNullable<Element['shadowRoot']> } =>
element.shadowRoot !== null && element.shadowRoot.mode === 'open'

export function* traverseDOM(node: Node, shouldEnter: (node: Node) => boolean = () => true): Iterable<Node> {
export const getRelativeChildNodes = (node: Node, useShadowRoot = true): NodeListOf<ChildNode> | Node[] => {
if (useShadowRoot && isElement(node)) {
if (isHTMLSlotElement(node)) {
return node.assignedNodes()
}

if (hasOpenShadowRoot(node)) {
return node.shadowRoot.childNodes
}
}
return node.childNodes
}

export const getRelativeParent = (node: Node): HTMLElement | null => {
if (node.parentElement !== null) {
return node.parentElement
}

const parentNode = node.parentNode
if (parentNode instanceof ShadowRoot) {
if (isElement(parentNode.host) && isHTMLElement(parentNode.host)) {
return parentNode.host
}
}

return null
}

export function* traverseDOM(
node: Node,
shouldEnter: (node: Node) => boolean = () => true,
useShadowRoot = false
): Iterable<Node> {
yield node
if (shouldEnter(node)) {
for (const childNode of node.childNodes) {
for (const childNode of getRelativeChildNodes(node, useShadowRoot)) {
yield* traverseDOM(childNode)
}
}
Expand Down
8 changes: 6 additions & 2 deletions src/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
isHTMLInputElement,
isHTMLElement,
isSVGSVGElement,
getRelativeChildNodes,
getRelativeParent,
} from './dom.js'
import { convertLinearGradient } from './gradients.js'
import {
Expand Down Expand Up @@ -47,7 +49,8 @@ export function handleElement(element: Element, context: Readonly<TraversalConte
const rectanglesIntersect = doRectanglesIntersect(bounds, context.options.captureArea)

const styles = window.getComputedStyle(element)
const parentStyles = element.parentElement && window.getComputedStyle(element.parentElement)
const parent = getRelativeParent(element)
const parentStyles = parent && window.getComputedStyle(parent)

const svgContainer =
isHTMLAnchorElement(element) && context.options.keepLinks
Expand Down Expand Up @@ -238,7 +241,8 @@ export function handleElement(element: Element, context: Readonly<TraversalConte
} else {
// Walk children even if rectangles don't intersect,
// because children can overflow the parent's bounds as long as overflow: visible (default).
for (const child of element.childNodes) {
const childNodes = getRelativeChildNodes(element, context.options.useShadowRoot)
for (const child of childNodes) {
walkNode(child, childContext)
}
if (ownStackingLayers) {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export function elementToSVG(element: Element, options?: DomToSvgOptions): XMLDo
options: {
captureArea: options?.captureArea ?? element.getBoundingClientRect(),
keepLinks: options?.keepLinks !== false,
useShadowRoot: options?.useShadowRoot !== false,
},
})

Expand Down
1 change: 1 addition & 0 deletions src/inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export async function inlineResources(element: Element): Promise<void> {
// SVGs embedded through <img> are never interactive.
keepLinks: false,
captureArea: svgRoot.viewBox.baseVal,
useShadowRoot: false,
},
})

Expand Down
8 changes: 6 additions & 2 deletions src/text.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isVisible } from './css.js'
import { svgNamespace } from './dom.js'
import { getRelativeParent, svgNamespace } from './dom.js'
import { TraversalContext } from './traversal.js'
import { doRectanglesIntersect, assert } from './util.js'

Expand All @@ -8,7 +8,11 @@ export function handleTextNode(textNode: Text, context: TraversalContext): void
throw new Error("Element's ownerDocument has no defaultView")
}
const window = textNode.ownerDocument.defaultView
const parentElement = textNode.parentElement!
const parentElement = getRelativeParent(textNode)
if (parentElement === null) {
throw new Error('No parent found!')
}

const styles = window.getComputedStyle(parentElement)
if (!isVisible(styles)) {
return
Expand Down
7 changes: 7 additions & 0 deletions src/traversal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ export interface DomToSvgOptions {
* @default true
*/
keepLinks?: boolean

/**
* Whether to include shadow root elements
*
* @default true
*/
useShadowRoot?: boolean
}

export interface TraversalContext {
Expand Down