diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.spec.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.spec.ts index a46945552a3..9cdbac5f832 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.spec.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.spec.ts @@ -219,7 +219,7 @@ describe('IframeComponent', () => { }; }); - it('should remove and add listener on load', () => { + it('should remove and add listener on load using the same stable function references', () => { iframeEl.triggerEventHandler('load', { target: { contentDocument: { @@ -228,19 +228,26 @@ describe('IframeComponent', () => { } }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const keyDownRef = (comp as any).boundEmitKeyDown; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const customEventRef = (comp as any).boundEmitCustomEvent; + + // remove must receive the exact same reference as add so the browser + // can actually deregister the handler instead of silently failing expect( comp.iframeElement.nativeElement.contentWindow.removeEventListener - ).toHaveBeenCalledWith('keydown', expect.any(Function)); + ).toHaveBeenCalledWith('keydown', keyDownRef); expect( comp.iframeElement.nativeElement.contentWindow.document.removeEventListener - ).toHaveBeenCalledWith('ng-event', expect.any(Function)); + ).toHaveBeenCalledWith('ng-event', customEventRef); expect( comp.iframeElement.nativeElement.contentWindow.addEventListener - ).toHaveBeenCalledWith('keydown', expect.any(Function)); + ).toHaveBeenCalledWith('keydown', keyDownRef); expect( comp.iframeElement.nativeElement.contentWindow.document.addEventListener - ).toHaveBeenCalledWith('ng-event', expect.any(Function)); + ).toHaveBeenCalledWith('ng-event', customEventRef); }); it('should set the colors to the jsp on load', () => { diff --git a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.ts b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.ts index 61c68f3a7ac..06aeb5621c8 100644 --- a/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.ts +++ b/core-web/apps/dotcms-ui/src/app/view/components/_common/iframe/iframe-component/iframe.component.ts @@ -76,6 +76,13 @@ export class IframeComponent implements OnInit, OnDestroy { private destroy$: Subject = new Subject(); + // Stable bound references required so removeEventListener can match the + // exact function object that was passed to addEventListener. Using + // .bind(this) inline creates a new object each call, making removal a + // no-op and causing listeners to accumulate on every iframe load event. + private readonly boundEmitKeyDown = this.emitKeyDown.bind(this); + private readonly boundEmitCustomEvent = this.emitCustonEvent.bind(this); + ngOnInit(): void { this.iframeOverlayService.overlay .pipe(takeUntil(this.destroy$)) @@ -264,17 +271,11 @@ export class IframeComponent implements OnInit, OnDestroy { } private handleIframeEvents($event): void { - this.getIframeWindow().removeEventListener('keydown', this.emitKeyDown.bind(this)); - this.getIframeWindow().document.removeEventListener( - 'ng-event', - this.emitCustonEvent.bind(this) - ); - - this.getIframeWindow().addEventListener('keydown', this.emitKeyDown.bind(this)); - this.getIframeWindow().document.addEventListener( - 'ng-event', - this.emitCustonEvent.bind(this) - ); + this.getIframeWindow().removeEventListener('keydown', this.boundEmitKeyDown); + this.getIframeWindow().document.removeEventListener('ng-event', this.boundEmitCustomEvent); + + this.getIframeWindow().addEventListener('keydown', this.boundEmitKeyDown); + this.getIframeWindow().document.addEventListener('ng-event', this.boundEmitCustomEvent); this.charge.emit($event); const doc = this.getIframeDocument();