import type { Options } from './types' import { getMimeType } from './mimes' import { resourceToDataURL } from './dataurl' import { clonePseudoElements } from './clone-pseudos' import { createImage, toArray } from './util' async function cloneCanvasElement(canvas: HTMLCanvasElement) { const dataURL = canvas.toDataURL() if (dataURL === 'data:,') { return canvas.cloneNode(false) as HTMLCanvasElement } return createImage(dataURL) } async function cloneVideoElement(video: HTMLVideoElement, options: Options) { const poster = video.poster const contentType = getMimeType(poster) const dataURL = await resourceToDataURL(poster, contentType, options) return createImage(dataURL) } async function cloneSingleNode( node: T, options: Options, ): Promise { if (node instanceof HTMLCanvasElement) { return cloneCanvasElement(node) } if (node instanceof HTMLVideoElement && node.poster) { return cloneVideoElement(node, options) } return node.cloneNode(false) as T } const isSlotElement = (node: HTMLElement): node is HTMLSlotElement => node.tagName != null && node.tagName.toUpperCase() === 'SLOT' async function cloneChildren( nativeNode: T, clonedNode: T, options: Options, ): Promise { const children = isSlotElement(nativeNode) && nativeNode.assignedNodes ? toArray(nativeNode.assignedNodes()) : toArray((nativeNode.shadowRoot ?? nativeNode).childNodes) if (children.length === 0 || nativeNode instanceof HTMLVideoElement) { return clonedNode } await children.reduce( (deferred, child) => deferred .then(() => cloneNode(child, options)) .then((clonedChild: HTMLElement | null) => { if (clonedChild) { clonedNode.appendChild(clonedChild) } }), Promise.resolve(), ) return clonedNode } function cloneCSSStyle(nativeNode: T, clonedNode: T) { const targetStyle = clonedNode.style if (!targetStyle) { return } const sourceStyle = window.getComputedStyle(nativeNode) if (sourceStyle.cssText) { targetStyle.cssText = sourceStyle.cssText targetStyle.transformOrigin = sourceStyle.transformOrigin } else { toArray(sourceStyle).forEach((name) => { let value = sourceStyle.getPropertyValue(name) if (name === 'font-size' && value.endsWith('px')) { const reducedFont = Math.floor(parseFloat(value.substring(0, value.length - 2))) - 0.1 value = `${reducedFont}px` } targetStyle.setProperty( name, value, sourceStyle.getPropertyPriority(name), ) }) } } function cloneInputValue(nativeNode: T, clonedNode: T) { if (nativeNode instanceof HTMLTextAreaElement) { clonedNode.innerHTML = nativeNode.value } if (nativeNode instanceof HTMLInputElement) { clonedNode.setAttribute('value', nativeNode.value) } } function cloneSelectValue(nativeNode: T, clonedNode: T) { if (nativeNode instanceof HTMLSelectElement) { const clonedSelect = clonedNode as any as HTMLSelectElement const selectedOption = Array.from(clonedSelect.children).find( (child) => nativeNode.value === child.getAttribute('value'), ) if (selectedOption) { selectedOption.setAttribute('selected', '') } } } function decorate(nativeNode: T, clonedNode: T): T { if (clonedNode instanceof Element) { cloneCSSStyle(nativeNode, clonedNode) clonePseudoElements(nativeNode, clonedNode) cloneInputValue(nativeNode, clonedNode) cloneSelectValue(nativeNode, clonedNode) } return clonedNode } export async function cloneNode( node: T, options: Options, isRoot?: boolean, ): Promise { if (!isRoot && options.filter && !options.filter(node)) { return null } return Promise.resolve(node) .then((clonedNode) => cloneSingleNode(clonedNode, options) as Promise) .then((clonedNode) => cloneChildren(node, clonedNode, options)) .then((clonedNode) => decorate(node, clonedNode)) }