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;