@@ -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+
2320export 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