From 424044ede3a96a9d80026787caa7c7998838e9b5 Mon Sep 17 00:00:00 2001 From: Lucky Feng Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] fix: issue #548 (#550) 1. Do not use virtual parent optimization if the mutation targets have iframe elements as children. This will cause some performance regression but will be easy to add and ship. 2. If an iframe element has already been a child of a virtual parent, add the virtual parent back to the dom. --- src/replay/index.ts | 72 ++++++++++++++++++++++++++++----------- typings/replay/index.d.ts | 1 + 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/src/replay/index.ts b/src/replay/index.ts index 61c7559c..07bd69a4 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -166,23 +166,9 @@ export class Replayer { this.emitter.on(ReplayerEvents.Flush, () => { const { scrollMap, inputMap } = this.treeIndex.flush(); - for (const [frag, parent] of this.fragmentParentMap.entries()) { - mirror.map[parent.__sn.id] = parent; - /** - * If we have already set value attribute on textarea, - * then we could not apply text content as default value any more. - */ - if ( - parent.__sn.type === NodeType.Element && - parent.__sn.tagName === 'textarea' && - frag.textContent - ) { - ((parent as unknown) as HTMLTextAreaElement).value = frag.textContent; - } - parent.appendChild(frag); - // restore state of elements after they are mounted - this.restoreState(parent); - } + this.fragmentParentMap.forEach((parent, frag) => + this.restoreRealParent(frag, parent), + ); this.fragmentParentMap.clear(); this.elementStateMap.clear(); @@ -627,6 +613,20 @@ export class Replayer { iframeEl: HTMLIFrameElement, ) { const collected: AppendedIframe[] = []; + // If iframeEl is detached from dom, iframeEl.contentDocument is null. + if (!iframeEl.contentDocument) { + let parent = iframeEl.parentNode; + while (parent) { + // The parent of iframeEl is virtual parent and we need to mount it on the dom. + if (this.fragmentParentMap.has((parent as unknown) as INode)) { + const frag = (parent as unknown) as INode; + const realParent = this.fragmentParentMap.get(frag)!; + this.restoreRealParent(frag, realParent); + break; + } + parent = parent.parentNode; + } + } buildNodeWithSN(mutation.node, { doc: iframeEl.contentDocument!, map: mirror.map, @@ -1139,8 +1139,19 @@ export class Replayer { parentInDocument = this.iframe.contentDocument.body.contains(parent); } - // if parent element is an iframe, iframe document can't be appended to virtual parent - if (useVirtualParent && parentInDocument && !isIframeINode(parent)) { + const hasIframeChild = + ((parent as unknown) as HTMLElement).getElementsByTagName?.('iframe') + .length > 0; + /** + * Why !isIframeINode(parent)? If parent element is an iframe, iframe document can't be appended to virtual parent. + * Why !hasIframeChild? If we move iframe elements from dom to fragment document, we will lose the contentDocument of iframe. So we need to disable the virtual dom optimization if a parent node contains iframe elements. + */ + if ( + useVirtualParent && + parentInDocument && + !isIframeINode(parent) && + !hasIframeChild + ) { const virtualParent = (document.createDocumentFragment() as unknown) as INode; mirror.map[mutation.parentId] = virtualParent; this.fragmentParentMap.set(virtualParent, parent); @@ -1529,6 +1540,29 @@ export class Replayer { }); } + /** + * Replace the virtual parent with the real parent. + * @param frag fragment document, the virtual parent + * @param parent real parent element + */ + private restoreRealParent(frag: INode, parent: INode) { + mirror.map[parent.__sn.id] = parent; + /** + * If we have already set value attribute on textarea, + * then we could not apply text content as default value any more. + */ + if ( + parent.__sn.type === NodeType.Element && + parent.__sn.tagName === 'textarea' && + frag.textContent + ) { + ((parent as unknown) as HTMLTextAreaElement).value = frag.textContent; + } + parent.appendChild(frag); + // restore state of elements after they are mounted + this.restoreState(parent); + } + /** * store state of elements before unmounted from dom recursively * the state should be restored in the handler of event ReplayerEvents.Flush diff --git a/typings/replay/index.d.ts b/typings/replay/index.d.ts index f1df3a2f..441552ed 100644 --- a/typings/replay/index.d.ts +++ b/typings/replay/index.d.ts @@ -55,6 +55,7 @@ export declare class Replayer { private hoverElements; private isUserInteraction; private backToNormal; + private restoreRealParent; private storeState; private restoreState; private warnNodeNotFound;