@@ -725,41 +725,47 @@ export function addElementToHtml(
725725 } ;
726726}
727727
728- function selectorTargetsId ( selector : string , id : string ) : boolean {
729- return (
730- selector === `#${ id } ` ||
731- selector === `[data-hf-id="${ id } "]` ||
732- selector === `[data-hf-id='${ id } ']`
733- ) ;
728+ function elementSelectors ( element : Element ) : string [ ] {
729+ const selectors : string [ ] = [ ] ;
730+ const id = element . getAttribute ( "id" ) ;
731+ const hfId = element . getAttribute ( "data-hf-id" ) ;
732+ if ( id ) selectors . push ( `#${ id } ` ) ;
733+ if ( hfId ) selectors . push ( `[data-hf-id="${ hfId } "]` , `[data-hf-id='${ hfId } ']` ) ;
734+ return selectors ;
734735}
735736
736- function stripGsapForId ( script : string , elementId : string ) : string {
737- // Re-parse after every removal. Animation ids are count-based (positional), so
738- // removing one tween renumbers the survivors — ids captured from a single
739- // up-front parse go stale and silently no-op, orphaning later tweens on the
740- // now-deleted element. Always remove the FIRST still-matching animation in a
741- // freshly-parsed script until none remain.
742- let current = script ;
743- for ( ; ; ) {
744- const parsed = parseGsapScriptAcornForWrite ( current ) ;
745- if ( ! parsed ) return current ;
746- const match = parsed . located . find ( ( l ) =>
747- selectorTargetsId ( l . animation . targetSelector , elementId ) ,
748- ) ;
749- if ( ! match ) return current ;
750- const updated = removeAnimationFromScript ( current , match . id ) ;
751- // Guard against a non-removing match (would otherwise loop forever).
752- if ( updated === current ) return current ;
753- current = updated ;
754- }
755- }
737+ /** Remove a source subtree and its unambiguous, directly targeted GSAP tweens. */
738+ export function removeElementWithGsapCascade ( doc : Document , element : Element ) : void {
739+ const removedSelectors = new Set ( elementSelectors ( element ) ) ;
740+ walkCompositionDescendants ( element , ( child ) => {
741+ for ( const selector of elementSelectors ( child ) ) removedSelectors . add ( selector ) ;
742+ } ) ;
743+ element . remove ( ) ;
744+
745+ // Bare selectors can target repeated sub-composition instances. Keep a tween
746+ // if any surviving element still uses its selector rather than erasing the
747+ // surviving instance's animation along with the deleted subtree.
748+ walkCompositionDescendants ( doc , ( survivor ) => {
749+ for ( const selector of elementSelectors ( survivor ) ) removedSelectors . delete ( selector ) ;
750+ } ) ;
751+ if ( removedSelectors . size === 0 ) return ;
756752
757- function cascadeRemoveGsapById ( doc : Document , elementId : string ) : void {
758753 for ( const script of findScriptElementsDeep ( doc ) ) {
759- const text = script . textContent ?? "" ;
760- if ( ! text . includes ( "gsap" ) && ! text . includes ( "ScrollTrigger" ) ) continue ;
761- const updated = stripGsapForId ( text , elementId ) ;
762- if ( updated !== text ) script . textContent = updated ;
754+ let current = script . textContent ?? "" ;
755+ if ( ! current . includes ( "gsap" ) && ! current . includes ( "ScrollTrigger" ) ) continue ;
756+ // Writer ids are positional: reparse after each removal so later tweens
757+ // cannot be skipped after an earlier deletion renumbers them.
758+ for ( ; ; ) {
759+ const parsed = parseGsapScriptAcornForWrite ( current ) ;
760+ const match = parsed ?. located . find ( ( located ) =>
761+ removedSelectors . has ( located . animation . targetSelector ) ,
762+ ) ;
763+ if ( ! match ) break ;
764+ const updated = removeAnimationFromScript ( current , match . id ) ;
765+ if ( updated === current ) break ;
766+ current = updated ;
767+ }
768+ if ( current !== script . textContent ) script . textContent = current ;
763769 }
764770}
765771
@@ -771,8 +777,8 @@ export function removeElementFromHtml(html: string, elementId: string): string {
771777 "removeElementFromHtml: input HTML is empty or could not be parsed" ,
772778 ) ;
773779 }
774- doc . getElementById ( elementId ) ?. remove ( ) ;
775- cascadeRemoveGsapById ( doc , elementId ) ;
780+ const element = doc . getElementById ( elementId ) ;
781+ if ( element ) removeElementWithGsapCascade ( doc , element ) ;
776782 return "<!DOCTYPE html>\n" + doc . documentElement . outerHTML ;
777783}
778784
0 commit comments