From 5fc7d2b3c504bd4cb937d03f46b5f1c0b56c8944 Mon Sep 17 00:00:00 2001 From: MSZGs Date: Wed, 22 Dec 2021 23:01:49 +0100 Subject: [PATCH 1/2] feat: shadow DOM support --- src/dom.ts | 27 +++++++++++++++++++++++++-- src/element.ts | 4 +++- src/index.ts | 1 + src/inline.ts | 1 + src/text.ts | 23 +++++++++++++++++++++-- src/traversal.ts | 7 +++++++ 6 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/dom.ts b/src/dom.ts index 2e9c6630..83452a8e 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -35,13 +35,36 @@ 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 => 'labels' in element +export const hasOpenShadowRoot = ( + element: Element +): element is Omit & { shadowRoot: NonNullable } => + element.shadowRoot !== null && element.shadowRoot.mode === 'open' -export function* traverseDOM(node: Node, shouldEnter: (node: Node) => boolean = () => true): Iterable { +export const getChildNodes = (node: Node, useShadowRoot = true): NodeListOf | Node[] => { + if (useShadowRoot && isElement(node)) { + if (isHTMLSlotElement(node)) { + return node.assignedNodes() + } + + if (hasOpenShadowRoot(node)) { + return node.shadowRoot.childNodes + } + } + return node.childNodes +} + +export function* traverseDOM( + node: Node, + shouldEnter: (node: Node) => boolean = () => true, + useShadowRoot = false +): Iterable { yield node if (shouldEnter(node)) { - for (const childNode of node.childNodes) { + for (const childNode of getChildNodes(node, useShadowRoot)) { yield* traverseDOM(childNode) } } diff --git a/src/element.ts b/src/element.ts index 22bfd41a..40377170 100644 --- a/src/element.ts +++ b/src/element.ts @@ -19,6 +19,7 @@ import { isHTMLInputElement, isHTMLElement, isSVGSVGElement, + getChildNodes, } from './dom.js' import { convertLinearGradient } from './gradients.js' import { @@ -238,7 +239,8 @@ export function handleElement(element: Element, context: Readonly { // SVGs embedded through are never interactive. keepLinks: false, captureArea: svgRoot.viewBox.baseVal, + useShadowRoot: false, }, }) diff --git a/src/text.ts b/src/text.ts index fcaa561e..0e5b6444 100644 --- a/src/text.ts +++ b/src/text.ts @@ -1,5 +1,5 @@ import { isVisible } from './css.js' -import { svgNamespace } from './dom.js' +import { isElement, isHTMLElement, svgNamespace } from './dom.js' import { TraversalContext } from './traversal.js' import { doRectanglesIntersect, assert } from './util.js' @@ -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 = getTextParent(textNode) + if (parentElement === null) { + throw new Error('No parent found!') + } + const styles = window.getComputedStyle(parentElement) if (!isVisible(styles)) { return @@ -136,3 +140,18 @@ export function copyTextStyles(styles: CSSStyleDeclaration, svgElement: SVGEleme // tspan uses fill, CSS uses color svgElement.setAttribute('fill', styles.color) } + +function getTextParent(text: Text): HTMLElement | null { + if (text.parentElement !== null) { + return text.parentElement + } + + const parentNode = text.parentNode + if (parentNode instanceof ShadowRoot) { + if (isElement(parentNode.host) && isHTMLElement(parentNode.host)) { + return parentNode.host + } + } + + return null +} diff --git a/src/traversal.ts b/src/traversal.ts index b690fbd1..ad81d7d9 100644 --- a/src/traversal.ts +++ b/src/traversal.ts @@ -16,6 +16,13 @@ export interface DomToSvgOptions { * @default true */ keepLinks?: boolean + + /** + * Whether to include shadow root elements + * + * @default true + */ + useShadowRoot?: boolean } export interface TraversalContext { From 6f03c453b485526c3fd6758bbf8e55a04c2efad2 Mon Sep 17 00:00:00 2001 From: MSZGs Date: Thu, 23 Dec 2021 18:42:18 +0100 Subject: [PATCH 2/2] fix: wrong parent styles in shadow root --- src/dom.ts | 19 +++++++++++++++++-- src/element.ts | 8 +++++--- src/text.ts | 19 ++----------------- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/dom.ts b/src/dom.ts index 83452a8e..c2563539 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -44,7 +44,7 @@ export const hasOpenShadowRoot = ( ): element is Omit & { shadowRoot: NonNullable } => element.shadowRoot !== null && element.shadowRoot.mode === 'open' -export const getChildNodes = (node: Node, useShadowRoot = true): NodeListOf | Node[] => { +export const getRelativeChildNodes = (node: Node, useShadowRoot = true): NodeListOf | Node[] => { if (useShadowRoot && isElement(node)) { if (isHTMLSlotElement(node)) { return node.assignedNodes() @@ -57,6 +57,21 @@ export const getChildNodes = (node: Node, useShadowRoot = true): NodeListOf { + 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, @@ -64,7 +79,7 @@ export function* traverseDOM( ): Iterable { yield node if (shouldEnter(node)) { - for (const childNode of getChildNodes(node, useShadowRoot)) { + for (const childNode of getRelativeChildNodes(node, useShadowRoot)) { yield* traverseDOM(childNode) } } diff --git a/src/element.ts b/src/element.ts index 40377170..6eb62da7 100644 --- a/src/element.ts +++ b/src/element.ts @@ -19,7 +19,8 @@ import { isHTMLInputElement, isHTMLElement, isSVGSVGElement, - getChildNodes, + getRelativeChildNodes, + getRelativeParent, } from './dom.js' import { convertLinearGradient } from './gradients.js' import { @@ -48,7 +49,8 @@ export function handleElement(element: Element, context: Readonly