From 74696faf47c07b48b9c9587db0b999da1c08a8be Mon Sep 17 00:00:00 2001 From: Niklas von Hertzen Date: Sat, 1 Jan 2022 01:57:17 +0800 Subject: [PATCH] fix: adopted stylesheets (#2785) --- src/dom/document-cloner.ts | 59 +++++++++++++++---- src/dom/node-parser.ts | 2 + .../autonomous-custom-element.js | 4 ++ .../reftests/webcomponents/webcomponents.html | 7 ++- 4 files changed, 57 insertions(+), 15 deletions(-) diff --git a/src/dom/document-cloner.ts b/src/dom/document-cloner.ts index 796e557..d2fd930 100644 --- a/src/dom/document-cloner.ts +++ b/src/dom/document-cloner.ts @@ -2,12 +2,14 @@ import {Bounds} from '../css/layout/bounds'; import { isBodyElement, isCanvasElement, + isCustomElement, isElementNode, isHTMLElementNode, isIFrameElement, isImageElement, isScriptElement, isSelectElement, + isSlotElement, isStyleElement, isSVGElementNode, isTextareaElement, @@ -63,7 +65,7 @@ export class DocumentCloner { throw new Error('Cloned element does not have an owner document'); } - this.documentElement = this.cloneNode(element.ownerDocument.documentElement) as HTMLElement; + this.documentElement = this.cloneNode(element.ownerDocument.documentElement, false) as HTMLElement; } toIFrame(ownerDocument: Document, windowSize: Bounds): Promise { @@ -160,6 +162,17 @@ export class DocumentCloner { } } + if (isCustomElement(clone)) { + return this.createCustomElementClone(clone); + } + + return clone; + } + + createCustomElementClone(node: HTMLElement): HTMLElement { + const clone = document.createElement('html2canvascustomelement'); + copyCSSStyles(node.style, clone); + return clone; } @@ -231,7 +244,20 @@ export class DocumentCloner { return clonedCanvas; } - cloneNode(node: Node): Node { + appendChildNode(clone: HTMLElement | SVGElement, child: Node, copyStyles: boolean): void { + if ( + !isElementNode(child) || + (!isScriptElement(child) && + !child.hasAttribute(IGNORE_ATTRIBUTE) && + (typeof this.options.ignoreElements !== 'function' || !this.options.ignoreElements(child))) + ) { + if (!this.options.copyStyles || !isElementNode(child) || !isStyleElement(child)) { + clone.appendChild(this.cloneNode(child, copyStyles)); + } + } + } + + cloneNode(node: Node, copyStyles: boolean): Node { if (isTextNode(node)) { return document.createTextNode(node.data); } @@ -260,16 +286,22 @@ export class DocumentCloner { const counters = this.counters.parse(new CSSParsedCounterDeclaration(this.context, style)); const before = this.resolvePseudoContent(node, clone, styleBefore, PseudoElementType.BEFORE); - for (let child = node.firstChild; child; child = child.nextSibling) { - if ( - !isElementNode(child) || - (!isScriptElement(child) && - !child.hasAttribute(IGNORE_ATTRIBUTE) && - (typeof this.options.ignoreElements !== 'function' || !this.options.ignoreElements(child))) - ) { - if (!this.options.copyStyles || !isElementNode(child) || !isStyleElement(child)) { - clone.appendChild(this.cloneNode(child)); + if (isCustomElement(node)) { + copyStyles = true; + } + + for ( + let child = node.shadowRoot ? node.shadowRoot.firstChild : node.firstChild; + child; + child = child.nextSibling + ) { + if (isElementNode(child) && isSlotElement(child) && typeof child.assignedNodes === 'function') { + const assignedNodes = child.assignedNodes() as ChildNode[]; + if (assignedNodes.length) { + assignedNodes.forEach((assignedNode) => this.appendChildNode(clone, assignedNode, copyStyles)); } + } else { + this.appendChildNode(clone, child, copyStyles); } } @@ -284,7 +316,10 @@ export class DocumentCloner { this.counters.pop(counters); - if (style && (this.options.copyStyles || isSVGElementNode(node)) && !isIFrameElement(node)) { + if ( + (style && (this.options.copyStyles || isSVGElementNode(node)) && !isIFrameElement(node)) || + copyStyles + ) { copyCSSStyles(style, clone); } diff --git a/src/dom/node-parser.ts b/src/dom/node-parser.ts index 260974e..0bae313 100644 --- a/src/dom/node-parser.ts +++ b/src/dom/node-parser.ts @@ -131,3 +131,5 @@ export const isScriptElement = (node: Element): node is HTMLScriptElement => nod export const isTextareaElement = (node: Element): node is HTMLTextAreaElement => node.tagName === 'TEXTAREA'; export const isSelectElement = (node: Element): node is HTMLSelectElement => node.tagName === 'SELECT'; export const isSlotElement = (node: Element): node is HTMLSlotElement => node.tagName === 'SLOT'; +// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name +export const isCustomElement = (node: Element): node is HTMLElement => node.tagName.indexOf('-') > 0; diff --git a/tests/reftests/webcomponents/autonomous-custom-element.js b/tests/reftests/webcomponents/autonomous-custom-element.js index 659cb56..80608f9 100644 --- a/tests/reftests/webcomponents/autonomous-custom-element.js +++ b/tests/reftests/webcomponents/autonomous-custom-element.js @@ -40,6 +40,10 @@ class AutonomousCustomElement extends HTMLElement { wrapper.appendChild(img); wrapper.appendChild(info); } + + connectedCallback() { + this.shadowRoot.adoptedStyleSheets = [sheet]; + } } customElements.define('autonomous-custom-element', AutonomousCustomElement); diff --git a/tests/reftests/webcomponents/webcomponents.html b/tests/reftests/webcomponents/webcomponents.html index cfa59f1..c4245f5 100644 --- a/tests/reftests/webcomponents/webcomponents.html +++ b/tests/reftests/webcomponents/webcomponents.html @@ -3,10 +3,11 @@ Web components tests + -