Skip to content

Commit a0f37a5

Browse files
committed
improve webcomponent clone
1 parent 1b4d5ad commit a0f37a5

1 file changed

Lines changed: 161 additions & 154 deletions

File tree

src/core/clone.js

Lines changed: 161 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -16,191 +16,198 @@ import { cache } from '../core/cache.js'
1616
* @returns {Node|null} Cloned node with styles and shadow DOM content, or null for empty text nodes or filtered elements
1717
*/
1818

19-
function isSlotElement(node) {
20-
return node.nodeType === Node.ELEMENT_NODE && node.tagName === 'SLOT';
21-
}
22-
19+
2320
export function deepClone(node, compress, options = {}, originalRoot) {
24-
// 1. Validación inicial del nodo
25-
if (!node) {
26-
throw new Error('Invalid node: node is null or undefined');
27-
}
21+
if (!node) throw new Error('Invalid node');
2822

29-
try {
30-
// 2. Manejo de nodos de texto
31-
if (node.nodeType === Node.TEXT_NODE) {
32-
return node.cloneNode(true);
33-
}
23+
// Local set to avoid duplicates in slot processing
24+
const clonedAssignedNodes = new Set();
25+
let pendingSelectValue = null; // Track select value for later fix
3426

35-
// 3. Manejo de nodos que no son elementos
36-
if (node.nodeType !== Node.ELEMENT_NODE) {
37-
return node.cloneNode(true);
38-
}
27+
// 1. Text nodes
28+
if (node.nodeType === Node.TEXT_NODE) {
29+
return node.cloneNode(true);
30+
}
3931

40-
// 4. Manejo de elementos excluidos
41-
if (node.getAttribute("data-capture") === "exclude") {
42-
const spacer = document.createElement("div");
43-
const rect = node.getBoundingClientRect();
44-
spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
45-
return spacer;
46-
}
32+
// 2. Non-element nodes (comments, etc.)
33+
if (node.nodeType !== Node.ELEMENT_NODE) {
34+
return node.cloneNode(true);
35+
}
4736

48-
// 5. Manejo de selectores excluidos
49-
if (options.exclude && Array.isArray(options.exclude)) {
50-
for (const selector of options.exclude) {
51-
try {
52-
if (node.matches?.(selector)) {
53-
const spacer = document.createElement("div");
54-
const rect = node.getBoundingClientRect();
55-
spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
56-
return spacer;
57-
}
58-
} catch (err) {
59-
console.warn(`Invalid selector in exclude option: ${selector}`, err);
60-
}
61-
}
62-
}
37+
// 3. Exclude by attribute
38+
if (node.getAttribute("data-capture") === "exclude") {
39+
const spacer = document.createElement("div");
40+
const rect = node.getBoundingClientRect();
41+
spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
42+
return spacer;
43+
}
6344

64-
// 6. Manejo de filtros personalizados
65-
if (typeof options.filter === "function") {
45+
// 4. Exclude by selectors
46+
if (options.exclude && Array.isArray(options.exclude)) {
47+
for (const selector of options.exclude) {
6648
try {
67-
if (!options.filter(node, originalRoot || node)) {
49+
if (node.matches?.(selector)) {
6850
const spacer = document.createElement("div");
6951
const rect = node.getBoundingClientRect();
70-
spacer.style.cssText = `display: inline-block; width: ${rect.width}px; height: ${rect.height}px; visibility: hidden;`;
52+
spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
7153
return spacer;
7254
}
7355
} catch (err) {
74-
console.warn("Error in filter function:", err);
56+
console.warn(`Invalid selector in exclude option: ${selector}`, err);
7557
}
7658
}
59+
}
7760

78-
// 7. Manejo especial de iframes
79-
if (node.tagName === "IFRAME") {
80-
const fallback = document.createElement("div");
81-
fallback.textContent = "";
82-
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;`;
83-
return fallback;
61+
// 5. Custom filter function
62+
if (typeof options.filter === "function") {
63+
try {
64+
if (!options.filter(node, originalRoot || node)) {
65+
const spacer = document.createElement("div");
66+
const rect = node.getBoundingClientRect();
67+
spacer.style.cssText = `display:inline-block;width:${rect.width}px;height:${rect.height}px;visibility:hidden;`;
68+
return spacer;
69+
}
70+
} catch (err) {
71+
console.warn("Error in filter function:", err);
8472
}
73+
}
8574

86-
// 8. Manejo de placeholders
87-
if (node.getAttribute("data-capture") === "placeholder") {
88-
const clone2 = node.cloneNode(false);
89-
cache.preNodeMap.set(clone2, node);
90-
inlineAllStyles(node, clone2, compress);
91-
const placeholder = document.createElement("div");
92-
placeholder.textContent = node.getAttribute("data-placeholder-text") || "";
93-
placeholder.style.cssText = `color: #666;font-size: 12px;text-align: center;line-height: 1.4;padding: 0.5em;box-sizing: border-box;`;
94-
clone2.appendChild(placeholder);
95-
return clone2;
96-
}
75+
// 6. Special case: iframe → fallback pattern
76+
if (node.tagName === "IFRAME") {
77+
const fallback = document.createElement("div");
78+
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;`;
79+
return fallback;
80+
}
9781

98-
// 9. Manejo especial de canvas
99-
if (node.tagName === "CANVAS") {
100-
const dataURL = node.toDataURL();
101-
const img = document.createElement("img");
102-
img.src = dataURL;
103-
img.width = node.width;
104-
img.height = node.height;
82+
// 7. Placeholder nodes
83+
if (node.getAttribute("data-capture") === "placeholder") {
84+
const clone2 = node.cloneNode(false);
85+
cache.preNodeMap.set(clone2, node);
86+
inlineAllStyles(node, clone2, compress);
87+
const placeholder = document.createElement("div");
88+
placeholder.textContent = node.getAttribute("data-placeholder-text") || "";
89+
placeholder.style.cssText = `color:#666;font-size:12px;text-align:center;line-height:1.4;padding:0.5em;box-sizing:border-box;`;
90+
clone2.appendChild(placeholder);
91+
return clone2;
92+
}
10593

106-
cache.preNodeMap.set(img, node);
107-
inlineAllStyles(node, img, compress);
94+
// 8. Canvas → convert to image
95+
if (node.tagName === "CANVAS") {
96+
const dataURL = node.toDataURL();
97+
const img = document.createElement("img");
98+
img.src = dataURL;
99+
img.width = node.width;
100+
img.height = node.height;
101+
cache.preNodeMap.set(img, node);
102+
inlineAllStyles(node, img, compress);
103+
return img;
104+
}
108105

109-
return img;
110-
}
111-
// 10. Clonación del nodo principal
112-
let clone;
113-
try {
114-
clone = node.cloneNode(false);
115-
if (!clone) {
116-
throw new Error('Failed to clone node: cloneNode returned null');
117-
}
118-
cache.preNodeMap.set(clone, node);
119-
} catch (err) {
120-
console.error("[Snapdom] Failed to clone node:", node, err);
121-
throw err; // Propaga el error original
122-
}
106+
// 9. Base clone (without children)
107+
let clone;
108+
try {
109+
clone = node.cloneNode(false);
110+
cache.preNodeMap.set(clone, node);
111+
} catch (err) {
112+
console.error("[Snapdom] Failed to clone node:", node, err);
113+
throw err;
114+
}
123115

124-
// 11. Manejo de elementos especiales (input, textarea, select)
125-
if (node instanceof HTMLInputElement) {
126-
clone.value = node.value;
127-
clone.setAttribute("value", node.value);
128-
if (node.checked !== void 0) {
129-
clone.checked = node.checked;
130-
if (node.checked) clone.setAttribute("checked", "");
131-
}
132-
} else if (node instanceof HTMLTextAreaElement) {
133-
const rect = node.getBoundingClientRect();
134-
clone.textContent = node.value;
135-
clone.style.width = `${rect.width}px`;
136-
clone.style.height = `${rect.height}px`;
137-
} else if (node instanceof HTMLSelectElement) {
138-
clone.value = node.value;
139-
Array.from(clone.options).forEach((opt) => {
140-
if (opt.value === node.value) {
141-
opt.setAttribute("selected", "");
142-
} else {
143-
opt.removeAttribute("selected");
144-
}
145-
});
116+
// Special handling: textarea (keep size and value)
117+
if (node instanceof HTMLTextAreaElement) {
118+
clone.textContent = node.value;
119+
clone.value = node.value;
120+
const rect = node.getBoundingClientRect();
121+
clone.style.width = `${rect.width}px`;
122+
clone.style.height = `${rect.height}px`;
123+
return clone;
124+
}
125+
126+
// Special handling: input
127+
if (node instanceof HTMLInputElement) {
128+
clone.value = node.value;
129+
clone.setAttribute("value", node.value);
130+
if (node.checked !== void 0) {
131+
clone.checked = node.checked;
132+
if (node.checked) clone.setAttribute("checked", "");
146133
}
134+
return clone;
135+
}
147136

148-
// 12. Aplicación de estilos
149-
inlineAllStyles(node, clone, compress);
137+
// Special handling: select → postpone value adjustment
138+
if (node instanceof HTMLSelectElement) {
139+
pendingSelectValue = node.value;
140+
}
150141

151-
// 13. Clonación de hijos
152-
try {
153-
if (isSlotElement(node)) {
154-
const assigned = node.assignedNodes?.({ flatten: true }) || [];
155-
const nodesToClone = assigned.length > 0 ? assigned : Array.from(node.childNodes);
156-
const fragment = document.createDocumentFragment();
157-
158-
for (const child of nodesToClone) {
159-
try {
160-
const clonedChild = deepClone(child, compress, options, originalRoot || node);
161-
if (clonedChild) fragment.appendChild(clonedChild);
162-
} catch (err) {
163-
console.warn("[Snapdom] Failed to clone slot content:", child, err);
164-
}
165-
}
166-
return fragment;
167-
} else if (node instanceof HTMLTextAreaElement) {
168-
} else {
169-
const baseChildren = node.shadowRoot ? node.shadowRoot.childNodes : node.childNodes;
170-
171-
for (const child of baseChildren) {
172-
try {
173-
const clonedChild = deepClone(child, compress, options, originalRoot || node);
174-
if (clonedChild) clone.appendChild(clonedChild);
175-
} catch (err) {
176-
console.warn("[Snapdom] Failed to clone child node:", child, err);
142+
// 11. Inline styles
143+
inlineAllStyles(node, clone, compress);
144+
145+
// 12. ShadowRoot logic
146+
if (node.shadowRoot) {
147+
const hasSlot = Array.from(node.shadowRoot.querySelectorAll("slot")).length > 0;
148+
149+
if (hasSlot) {
150+
// ShadowRoot with slots: only store styles
151+
for (const child of node.shadowRoot.childNodes) {
152+
if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") {
153+
const cssText = child.textContent || "";
154+
if (cssText.trim() && compress) {
155+
if (!cache.preStyle) cache.preStyle = new WeakMap();
156+
cache.preStyle.set(child, cssText);
177157
}
178158
}
179-
180-
if (node.shadowRoot && node.childNodes.length > 0 && !node.shadowRoot.querySelector('slot')) {
181-
const lightDomContent = document.createDocumentFragment();
182-
for (const child of node.childNodes) {
183-
try {
184-
const clonedChild = deepClone(child, compress, options, originalRoot || node);
185-
if (clonedChild) lightDomContent.appendChild(clonedChild);
186-
} catch (err) {
187-
console.warn("[Snapdom] Failed to clone light DOM child:", child, err);
188-
}
159+
}
160+
} else {
161+
// ShadowRoot without slots: clone full content
162+
const shadowFrag = document.createDocumentFragment();
163+
for (const child of node.shadowRoot.childNodes) {
164+
if (child.nodeType === Node.ELEMENT_NODE && child.tagName === "STYLE") {
165+
const cssText = child.textContent || "";
166+
if (cssText.trim() && compress) {
167+
if (!cache.preStyle) cache.preStyle = new WeakMap();
168+
cache.preStyle.set(child, cssText);
189169
}
190-
clone.appendChild(lightDomContent);
170+
continue;
191171
}
172+
const clonedChild = deepClone(child, compress, options, originalRoot || node);
173+
if (clonedChild) shadowFrag.appendChild(clonedChild);
192174
}
193-
} catch (err) {
194-
console.error("[Snapdom] Error cloning children:", err);
195-
throw err;
175+
clone.appendChild(shadowFrag);
196176
}
177+
}
197178

198-
return clone;
179+
// 13. Slot outside ShadowRoot
180+
if (node.tagName === "SLOT") {
181+
const assigned = node.assignedNodes?.({ flatten: true }) || [];
182+
const nodesToClone = assigned.length > 0 ? assigned : Array.from(node.childNodes);
183+
const fragment = document.createDocumentFragment();
199184

200-
} catch (error) {
201-
console.error("[Snapdom] Error in deepClone:", error);
202-
throw error; // Propaga el error al llamador
185+
for (const child of nodesToClone) {
186+
const clonedChild = deepClone(child, compress, options, originalRoot || node);
187+
if (clonedChild) fragment.appendChild(clonedChild);
188+
}
189+
return fragment;
190+
}
191+
192+
// 14. Clone children (light DOM), skipping duplicates
193+
for (const child of node.childNodes) {
194+
if (clonedAssignedNodes.has(child)) continue;
195+
196+
const clonedChild = deepClone(child, compress, options, originalRoot || node);
197+
if (clonedChild) clone.appendChild(clonedChild);
198+
}
199+
200+
// Adjust select value after children are cloned
201+
if (pendingSelectValue !== null && clone instanceof HTMLSelectElement) {
202+
clone.value = pendingSelectValue;
203+
for (const opt of clone.options) {
204+
if (opt.value === pendingSelectValue) {
205+
opt.setAttribute("selected", "");
206+
} else {
207+
opt.removeAttribute("selected");
208+
}
209+
}
203210
}
204-
}
205211

206-
212+
return clone;
213+
}

0 commit comments

Comments
 (0)