Spaces:
Build error
Build error
| import type { Options } from './types' | |
| import { clonePseudoElements } from './clone-pseudos' | |
| import { createImage, toArray, isInstanceOfElement } from './util' | |
| import { getMimeType } from './mimes' | |
| import { resourceToDataURL } from './dataurl' | |
| 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) { | |
| if (video.currentSrc) { | |
| const canvas = document.createElement('canvas') | |
| const ctx = canvas.getContext('2d') | |
| canvas.width = video.clientWidth | |
| canvas.height = video.clientHeight | |
| ctx?.drawImage(video, 0, 0, canvas.width, canvas.height) | |
| const dataURL = canvas.toDataURL() | |
| return createImage(dataURL) | |
| } | |
| const poster = video.poster | |
| const contentType = getMimeType(poster) | |
| const dataURL = await resourceToDataURL(poster, contentType, options) | |
| return createImage(dataURL) | |
| } | |
| async function cloneIFrameElement(iframe: HTMLIFrameElement) { | |
| try { | |
| if (iframe?.contentDocument?.body) { | |
| return (await cloneNode( | |
| iframe.contentDocument.body, | |
| {}, | |
| true, | |
| )) as HTMLBodyElement | |
| } | |
| } catch { | |
| // Failed to clone iframe | |
| } | |
| return iframe.cloneNode(false) as HTMLIFrameElement | |
| } | |
| async function cloneSingleNode<T extends HTMLElement>( | |
| node: T, | |
| options: Options, | |
| ): Promise<HTMLElement> { | |
| if (isInstanceOfElement(node, HTMLCanvasElement)) { | |
| return cloneCanvasElement(node) | |
| } | |
| if (isInstanceOfElement(node, HTMLVideoElement)) { | |
| return cloneVideoElement(node, options) | |
| } | |
| if (isInstanceOfElement(node, HTMLIFrameElement)) { | |
| return cloneIFrameElement(node) | |
| } | |
| return node.cloneNode(false) as T | |
| } | |
| const isSlotElement = (node: HTMLElement): node is HTMLSlotElement => | |
| node.tagName != null && node.tagName.toUpperCase() === 'SLOT' | |
| async function cloneChildren<T extends HTMLElement>( | |
| nativeNode: T, | |
| clonedNode: T, | |
| options: Options, | |
| ): Promise<T> { | |
| let children: T[] = [] | |
| if (isSlotElement(nativeNode) && nativeNode.assignedNodes) { | |
| children = toArray<T>(nativeNode.assignedNodes()) | |
| } else if ( | |
| isInstanceOfElement(nativeNode, HTMLIFrameElement) && | |
| nativeNode.contentDocument?.body | |
| ) { | |
| children = toArray<T>(nativeNode.contentDocument.body.childNodes) | |
| } else { | |
| children = toArray<T>((nativeNode.shadowRoot ?? nativeNode).childNodes) | |
| } | |
| if ( | |
| children.length === 0 || | |
| isInstanceOfElement(nativeNode, 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<T extends HTMLElement>(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<string>(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` | |
| } | |
| if ( | |
| isInstanceOfElement(nativeNode, HTMLIFrameElement) && | |
| name === 'display' && | |
| value === 'inline' | |
| ) { | |
| value = 'block' | |
| } | |
| if (name === 'd' && clonedNode.getAttribute('d')) { | |
| value = `path(${clonedNode.getAttribute('d')})` | |
| } | |
| targetStyle.setProperty( | |
| name, | |
| value, | |
| sourceStyle.getPropertyPriority(name), | |
| ) | |
| }) | |
| } | |
| } | |
| function cloneInputValue<T extends HTMLElement>(nativeNode: T, clonedNode: T) { | |
| if (isInstanceOfElement(nativeNode, HTMLTextAreaElement)) { | |
| clonedNode.innerHTML = nativeNode.value | |
| } | |
| if (isInstanceOfElement(nativeNode, HTMLInputElement)) { | |
| clonedNode.setAttribute('value', nativeNode.value) | |
| } | |
| } | |
| function cloneSelectValue<T extends HTMLElement>(nativeNode: T, clonedNode: T) { | |
| if (isInstanceOfElement(nativeNode, 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<T extends HTMLElement>(nativeNode: T, clonedNode: T): T { | |
| if (isInstanceOfElement(clonedNode, Element)) { | |
| cloneCSSStyle(nativeNode, clonedNode) | |
| clonePseudoElements(nativeNode, clonedNode) | |
| cloneInputValue(nativeNode, clonedNode) | |
| cloneSelectValue(nativeNode, clonedNode) | |
| } | |
| return clonedNode | |
| } | |
| async function ensureSVGSymbols<T extends HTMLElement>( | |
| clone: T, | |
| options: Options, | |
| ) { | |
| const uses = clone.querySelectorAll ? clone.querySelectorAll('use') : [] | |
| if (uses.length === 0) { | |
| return clone | |
| } | |
| const processedDefs: { [key: string]: HTMLElement } = {} | |
| for (let i = 0; i < uses.length; i++) { | |
| const use = uses[i] | |
| const id = use.getAttribute('xlink:href') | |
| if (id) { | |
| const exist = clone.querySelector(id) | |
| const definition = document.querySelector(id) as HTMLElement | |
| if (!exist && definition && !processedDefs[id]) { | |
| // eslint-disable-next-line no-await-in-loop | |
| processedDefs[id] = (await cloneNode(definition, options, true))! | |
| } | |
| } | |
| } | |
| const nodes = Object.values(processedDefs) | |
| if (nodes.length) { | |
| const ns = 'http://www.w3.org/1999/xhtml' | |
| const svg = document.createElementNS(ns, 'svg') | |
| svg.setAttribute('xmlns', ns) | |
| svg.style.position = 'absolute' | |
| svg.style.width = '0' | |
| svg.style.height = '0' | |
| svg.style.overflow = 'hidden' | |
| svg.style.display = 'none' | |
| const defs = document.createElementNS(ns, 'defs') | |
| svg.appendChild(defs) | |
| for (let i = 0; i < nodes.length; i++) { | |
| defs.appendChild(nodes[i]) | |
| } | |
| clone.appendChild(svg) | |
| } | |
| return clone | |
| } | |
| export async function cloneNode<T extends HTMLElement>( | |
| node: T, | |
| options: Options, | |
| isRoot?: boolean, | |
| ): Promise<T | null> { | |
| if (!isRoot && options.filter && !options.filter(node)) { | |
| return null | |
| } | |
| 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) => ensureSVGSymbols(clonedNode, options)) | |
| } | |