@@ -175,39 +175,70 @@ function hasAmbiguousGsapScriptChange(previous: string, restored: string): boole
175175 ) ;
176176}
177177
178- /**
179- * Every live element a restore changes, paired with its restored markup, or null when the restore is
180- * beyond an attribute sync. A file is the active document or a sub-composition the live preview
181- * inlines into each `[data-composition-file]` host; only the active document's GSAP script may change.
182- */
178+ type RestoreTarget = { live : Element ; restored : Element } ;
179+
180+ /** Whether a file, template content included, carries a GSAP script. */
181+ export function restoreHasGsapScript ( html : string ) : boolean {
182+ return findGsapScriptElements ( parseRestoreSource ( html ) ) . length > 0 ;
183+ }
184+
185+ // The active document is the whole preview; a sub-composition lives in each host that inlines it.
186+ function liveScopes ( doc : Document , path : string , isActive : boolean ) : ParentNode [ ] {
187+ if ( isActive ) return [ doc ] ;
188+ return Array . from ( doc . querySelectorAll ( `[data-composition-file="${ CSS . escape ( path ) } "]` ) ) ;
189+ }
190+
191+ function scopeTargets (
192+ scope : ParentNode ,
193+ keys : string [ ] ,
194+ restoredByKey : Map < string , Element > ,
195+ ) : RestoreTarget [ ] | null {
196+ const liveByKey = identityElementMap ( scope ) ;
197+ if ( ! liveByKey ) return null ;
198+ const targets : RestoreTarget [ ] = [ ] ;
199+ for ( const key of keys ) {
200+ const live = liveByKey . get ( key ) ;
201+ const restored = restoredByKey . get ( key ) ;
202+ // The preview rewrites a sub-composition's own root, so its attributes are not the file's.
203+ if ( ! live || ! restored || live . hasAttribute ( "data-hf-inner-root" ) ) return null ;
204+ targets . push ( { live, restored } ) ;
205+ }
206+ return targets ;
207+ }
208+
209+ function fileTargets (
210+ doc : Document ,
211+ path : string ,
212+ isActive : boolean ,
213+ { previous, restored } : UndoRestoreFile ,
214+ ) : RestoreTarget [ ] | null {
215+ // Only the active document's GSAP script can be re-run in place.
216+ if ( ! isActive && ( restoreHasGsapScript ( previous ) || restoreHasGsapScript ( restored ) ) ) return null ;
217+ const scopes = liveScopes ( doc , path , isActive ) ;
218+ const diff = diffSoftReloadableRestore ( previous , restored ) ;
219+ if ( ! scopes . length || ! diff || hasAmbiguousGsapScriptChange ( previous , restored ) ) return null ;
220+ const restoredByKey = identityElementMap ( parseRestoreSource ( restored ) ) ;
221+ if ( ! restoredByKey ) return null ;
222+ const targets : RestoreTarget [ ] = [ ] ;
223+ for ( const scope of scopes ) {
224+ const found = scopeTargets ( scope , diff . changedElementKeys , restoredByKey ) ;
225+ if ( ! found ) return null ;
226+ targets . push ( ...found ) ;
227+ }
228+ return targets ;
229+ }
230+
231+ /** Every live element a restore changes with its restored markup, or null when it needs a reload. */
183232function planRestoreTargets (
184233 doc : Document ,
185234 activeDocPath : string ,
186235 files : Record < string , UndoRestoreFile > ,
187- ) : Array < { live : Element ; restored : Element } > | null {
188- const targets : Array < { live : Element ; restored : Element } > = [ ] ;
189- for ( const [ path , { previous, restored } ] of Object . entries ( files ) ) {
190- const isActive = path === activeDocPath ;
191- const scopes : ParentNode [ ] = isActive
192- ? [ doc ]
193- : Array . from ( doc . querySelectorAll ( `[data-composition-file="${ CSS . escape ( path ) } "]` ) ) ;
194- const diff = diffSoftReloadableRestore ( previous , restored ) ;
195- if ( ! scopes . length || ! diff || hasAmbiguousGsapScriptChange ( previous , restored ) ) return null ;
196- if ( ! isActive && extractGsapScriptText ( previous ) !== extractGsapScriptText ( restored ) )
197- return null ;
198- const restoredByKey = identityElementMap ( parseRestoreSource ( restored ) ) ;
199- if ( ! restoredByKey ) return null ;
200- for ( const scope of scopes ) {
201- const liveByKey = identityElementMap ( scope ) ;
202- if ( ! liveByKey ) return null ;
203- for ( const key of diff . changedElementKeys ) {
204- const live = liveByKey . get ( key ) ;
205- const restoredEl = restoredByKey . get ( key ) ;
206- // The preview rewrites a sub-composition's own root, so its attributes are not the file's.
207- if ( ! live || ! restoredEl || live . hasAttribute ( "data-hf-inner-root" ) ) return null ;
208- targets . push ( { live, restored : restoredEl } ) ;
209- }
210- }
236+ ) : RestoreTarget [ ] | null {
237+ const targets : RestoreTarget [ ] = [ ] ;
238+ for ( const [ path , file ] of Object . entries ( files ) ) {
239+ const found = fileTargets ( doc , path , path === activeDocPath , file ) ;
240+ if ( ! found ) return null ;
241+ targets . push ( ...found ) ;
211242 }
212243 return targets ;
213244}
@@ -233,10 +264,7 @@ function planRestoreTargets(
233264 * lane move, timing shift, style tweak) → NO script execution — the
234265 * blink-free finalization only (seek + __hfForceTimelineRebind + manual
235266 * reapply, exactly the rebindPreviewTiming path), so timing-attribute
236- * reverts refresh their visibility windows. Re-running an unchanged
237- * script here used to be the biggest undo blink source: it tore down
238- * and rebuilt live timelines (and full-reloaded whenever the script
239- * couldn't be scoped) for restores that never touched it.
267+ * reverts refresh their visibility windows.
240268 *
241269 * Returns "soft" when applied in place, "full" when it escalated to reloadPreview
242270 * (ineligible restore, missing target, or a permanent soft-reload failure).
0 commit comments