// https://github.com/zumerlab/snapdom
//
// MIT License
//
// Copyright (c) 2025 ZumerLab
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
/**
* Deep cloning utilities for DOM elements, including styles and shadow DOM.
* @module clone
*/
/**
* Freeze the responsive selection of an
that has srcset/sizes.
* Copies a concrete URL into `src` and removes `srcset`/`sizes` so the clone
* doesn't need layout to resolve a candidate.
* Works with because currentSrc reflects the chosen source.
* @param {HTMLImageElement} original - Image in the live DOM.
* @param {HTMLImageElement} cloned - Just-created cloned
.
*/
function freezeImgSrcset(original, cloned) {
try {
const chosen = original.currentSrc || original.src || '';
if (!chosen) return;
cloned.setAttribute('src', chosen);
cloned.removeAttribute('srcset');
cloned.removeAttribute('sizes');
// Hint deterministic decode/load for capture
cloned.loading = 'eager';
cloned.decoding = 'sync';
} catch {
// no-op
}
}
/**
* Creates a deep clone of a DOM node, including styles, shadow DOM, and special handling for excluded/placeholder/canvas nodes.
*
* @param {Node} node - Node to clone
* @returns {Node|null} Cloned node with styles and shadow DOM content, or null for empty text nodes or filtered elements
*/
export function deepCloneBasic(node) {
if (!node) throw new Error('Invalid node');
// Local set to avoid duplicates in slot processing
const clonedAssignedNodes = new Set();
let pendingSelectValue = null; // Track select value for later fix
// 1. Text nodes
if (node.nodeType === Node.TEXT_NODE) {
return node.cloneNode(true);
}
// 2. Non-element nodes (comments, etc.)
if (node.nodeType !== Node.ELEMENT_NODE) {
return node.cloneNode(true);
}
// 6. Special case: iframe → fallback pattern
if (node.tagName === "IFRAME") {
const fallback = document.createElement("div");
fallback.style.cssText = `width:${node.offsetWidth}px;height:${node.offsetHeight}px;background-image:repeating-linear-gradient(45deg,#ddd,#ddd 5px,#f9f9f9 5px,#f9f9f9 10px);display:flex;align-items:center;justify-content:center;font-size:12px;color:#555;border:1px solid #aaa;`;
return fallback;
}
// 8. Canvas → convert to image
if (node.tagName === "CANVAS") {
const dataURL = node.toDataURL();
const img = document.createElement("img");
img.src = dataURL;
img.width = node.width;
img.height = node.height;
return img;
}
// 9. Base clone (without children)
let clone;
try {
clone = node.cloneNode(false);
if (node.tagName === 'IMG') {
freezeImgSrcset(node, clone);
}
} catch (err) {
console.error("[Snapdom] Failed to clone node:", node, err);
throw err;
}
// Special handling: textarea (keep size and value)
if (node instanceof HTMLTextAreaElement) {
clone.textContent = node.value;
clone.value = node.value;
const rect = node.getBoundingClientRect();
clone.style.boxSizing = 'border-box';
clone.style.width = `${rect.width}px`;
clone.style.height = `${rect.height}px`;
return clone;
}
// Special handling: input
if (node instanceof HTMLInputElement) {
if (node.hasAttribute("value")) {
clone.value = node.value;
clone.setAttribute("value", node.value);
}
if (node.checked !== void 0) {
clone.checked = node.checked;
if (node.checked) clone.setAttribute("checked", "");
if (node.indeterminate) clone.indeterminate = node.indeterminate;
}
// return clone;
}
// Special handling: select → postpone value adjustment
if (node instanceof HTMLSelectElement) {
pendingSelectValue = node.value;
}
// 12. ShadowRoot logic
if (node.shadowRoot) {
const hasSlot = Array.from(node.shadowRoot.querySelectorAll("slot")).length > 0;
if (hasSlot) {
} else {
// ShadowRoot without slots: clone full content
const shadowFrag = document.createDocumentFragment();
for (const child of node.shadowRoot.childNodes) {
if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") {
continue;
}
const clonedChild = deepCloneBasic(child);
if (clonedChild) shadowFrag.appendChild(clonedChild);
}
clone.appendChild(shadowFrag);
}
}
// 13. Slot outside ShadowRoot
if (node.tagName === "SLOT") {
const assigned = node.assignedNodes?.({ flatten: true }) || [];
const nodesToClone = assigned.length > 0 ? assigned : Array.from(node.childNodes);
const fragment = document.createDocumentFragment();
for (const child of nodesToClone) {
const clonedChild = deepCloneBasic(child);
if (clonedChild) fragment.appendChild(clonedChild);
}
return fragment;
}
// 14. Clone children (light DOM), skipping duplicates
for (const child of node.childNodes) {
if (clonedAssignedNodes.has(child)) continue;
const clonedChild = deepCloneBasic(child);
if (clonedChild) clone.appendChild(clonedChild);
}
// Adjust select value after children are cloned
if (pendingSelectValue !== null && clone instanceof HTMLSelectElement) {
clone.value = pendingSelectValue;
for (const opt of clone.options) {
if (opt.value === pendingSelectValue) {
opt.setAttribute("selected", "");
} else {
opt.removeAttribute("selected");
}
}
}
// Fix scrolling (taken from prepareClone).
const scrollX = node.scrollLeft;
const scrollY = node.scrollTop;
const hasScroll = scrollX || scrollY;
if (hasScroll && clone instanceof HTMLElement) {
clone.style.overflow = "hidden";
clone.style.scrollbarWidth = "none";
clone.style.msOverflowStyle = "none";
const inner = document.createElement("div");
inner.style.transform = `translate(${-scrollX}px, ${-scrollY}px)`;
inner.style.willChange = "transform";
inner.style.display = "inline-block";
inner.style.width = "100%";
while (clone.firstChild) {
inner.appendChild(clone.firstChild);
}
clone.appendChild(inner);
}
return clone;
}