fix: elements would lose some states like scroll position because of "virtual parent" optimization (#427)
* fix: elements would lose some state like scroll position because of "virtual parent" optimization * refactor: the bugfix code bug: elements would lose some state like scroll position because of "virtual parent" optimization * fix: an error occured at applyMutation(remove nodes part) error message: Uncaught (in promise) DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
|||||||
scrollData,
|
scrollData,
|
||||||
inputData,
|
inputData,
|
||||||
canvasMutationData,
|
canvasMutationData,
|
||||||
|
ElementState,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import {
|
import {
|
||||||
mirror,
|
mirror,
|
||||||
@@ -45,6 +46,7 @@ const SKIP_TIME_INTERVAL = 5 * 1000;
|
|||||||
const mitt = (mittProxy as any).default || mittProxy;
|
const mitt = (mittProxy as any).default || mittProxy;
|
||||||
|
|
||||||
const REPLAY_CONSOLE_PREFIX = '[replayer]';
|
const REPLAY_CONSOLE_PREFIX = '[replayer]';
|
||||||
|
const SCROLL_ATTRIBUTE_NAME = '__rrweb_scroll__';
|
||||||
|
|
||||||
const defaultMouseTailConfig = {
|
const defaultMouseTailConfig = {
|
||||||
duration: 500,
|
duration: 500,
|
||||||
@@ -78,6 +80,7 @@ export class Replayer {
|
|||||||
|
|
||||||
private treeIndex!: TreeIndex;
|
private treeIndex!: TreeIndex;
|
||||||
private fragmentParentMap!: Map<INode, INode>;
|
private fragmentParentMap!: Map<INode, INode>;
|
||||||
|
private elementStateMap!: Map<INode, ElementState>;
|
||||||
|
|
||||||
private imageMap: Map<eventWithTime, HTMLImageElement> = new Map();
|
private imageMap: Map<eventWithTime, HTMLImageElement> = new Map();
|
||||||
|
|
||||||
@@ -113,6 +116,7 @@ export class Replayer {
|
|||||||
|
|
||||||
this.treeIndex = new TreeIndex();
|
this.treeIndex = new TreeIndex();
|
||||||
this.fragmentParentMap = new Map<INode, INode>();
|
this.fragmentParentMap = new Map<INode, INode>();
|
||||||
|
this.elementStateMap = new Map<INode, ElementState>();
|
||||||
this.emitter.on(ReplayerEvents.Flush, () => {
|
this.emitter.on(ReplayerEvents.Flush, () => {
|
||||||
const { scrollMap, inputMap } = this.treeIndex.flush();
|
const { scrollMap, inputMap } = this.treeIndex.flush();
|
||||||
|
|
||||||
@@ -130,8 +134,11 @@ export class Replayer {
|
|||||||
((parent as unknown) as HTMLTextAreaElement).value = frag.textContent;
|
((parent as unknown) as HTMLTextAreaElement).value = frag.textContent;
|
||||||
}
|
}
|
||||||
parent.appendChild(frag);
|
parent.appendChild(frag);
|
||||||
|
// restore state of elements after they are mounted
|
||||||
|
this.restoreState(parent);
|
||||||
}
|
}
|
||||||
this.fragmentParentMap.clear();
|
this.fragmentParentMap.clear();
|
||||||
|
this.elementStateMap.clear();
|
||||||
|
|
||||||
for (const d of scrollMap.values()) {
|
for (const d of scrollMap.values()) {
|
||||||
this.applyScroll(d);
|
this.applyScroll(d);
|
||||||
@@ -913,6 +920,14 @@ export class Replayer {
|
|||||||
const realParent = this.fragmentParentMap.get(parent);
|
const realParent = this.fragmentParentMap.get(parent);
|
||||||
if (realParent && realParent.contains(target)) {
|
if (realParent && realParent.contains(target)) {
|
||||||
realParent.removeChild(target);
|
realParent.removeChild(target);
|
||||||
|
} else if (this.fragmentParentMap.has(target)) {
|
||||||
|
/**
|
||||||
|
* the target itself is a fragment document and it's not in the dom
|
||||||
|
* so we should remove the real target from its parent
|
||||||
|
*/
|
||||||
|
const realTarget = this.fragmentParentMap.get(target)!;
|
||||||
|
parent.removeChild(realTarget);
|
||||||
|
this.fragmentParentMap.delete(target);
|
||||||
} else {
|
} else {
|
||||||
parent.removeChild(target);
|
parent.removeChild(target);
|
||||||
}
|
}
|
||||||
@@ -964,6 +979,10 @@ export class Replayer {
|
|||||||
const virtualParent = (document.createDocumentFragment() as unknown) as INode;
|
const virtualParent = (document.createDocumentFragment() as unknown) as INode;
|
||||||
mirror.map[mutation.parentId] = virtualParent;
|
mirror.map[mutation.parentId] = virtualParent;
|
||||||
this.fragmentParentMap.set(virtualParent, parent);
|
this.fragmentParentMap.set(virtualParent, parent);
|
||||||
|
|
||||||
|
// store the state, like scroll position, of child nodes before they are unmounted from dom
|
||||||
|
this.storeState(parent);
|
||||||
|
|
||||||
while (parent.firstChild) {
|
while (parent.firstChild) {
|
||||||
virtualParent.appendChild(parent.firstChild);
|
virtualParent.appendChild(parent.firstChild);
|
||||||
}
|
}
|
||||||
@@ -1248,6 +1267,51 @@ export class Replayer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* store state of elements before unmounted from dom recursively
|
||||||
|
* the state should be restored in the handler of event ReplayerEvents.Flush
|
||||||
|
* e.g. browser would lose scroll position after the process that we add children of parent node to Fragment Document as virtual dom
|
||||||
|
*/
|
||||||
|
private storeState(parent: INode) {
|
||||||
|
if (parent) {
|
||||||
|
if (parent.nodeType === parent.ELEMENT_NODE) {
|
||||||
|
const parentElement = (parent as unknown) as HTMLElement;
|
||||||
|
if (parentElement.scrollLeft || parentElement.scrollTop) {
|
||||||
|
// store scroll position state
|
||||||
|
this.elementStateMap.set(parent, {
|
||||||
|
scroll: [parentElement.scrollLeft, parentElement.scrollTop],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const children = parentElement.children;
|
||||||
|
for (let i = 0; i < children.length; i++)
|
||||||
|
this.storeState((children[i] as unknown) as INode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* restore the state of elements recursively, which was stored before elements were unmounted from dom in virtual parent mode
|
||||||
|
* this function corresponds to function storeState
|
||||||
|
*/
|
||||||
|
private restoreState(parent: INode) {
|
||||||
|
if (parent.nodeType === parent.ELEMENT_NODE) {
|
||||||
|
const parentElement = (parent as unknown) as HTMLElement;
|
||||||
|
if (this.elementStateMap.has(parent)) {
|
||||||
|
const storedState = this.elementStateMap.get(parent)!;
|
||||||
|
// restore scroll position
|
||||||
|
if (storedState.scroll) {
|
||||||
|
parentElement.scrollLeft = storedState.scroll[0];
|
||||||
|
parentElement.scrollTop = storedState.scroll[1];
|
||||||
|
}
|
||||||
|
this.elementStateMap.delete(parent);
|
||||||
|
}
|
||||||
|
const children = parentElement.children;
|
||||||
|
for (let i = 0; i < children.length; i++) {
|
||||||
|
this.restoreState((children[i] as unknown) as INode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private warnNodeNotFound(d: incrementalData, id: number) {
|
private warnNodeNotFound(d: incrementalData, id: number) {
|
||||||
this.warn(`Node with id '${id}' not found in`, d);
|
this.warn(`Node with id '${id}' not found in`, d);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -466,3 +466,9 @@ export enum ReplayerEvents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type MaskInputFn = (text: string) => string;
|
export type MaskInputFn = (text: string) => string;
|
||||||
|
|
||||||
|
// store the state that would be changed during the process(unmount from dom and mount again)
|
||||||
|
export type ElementState = {
|
||||||
|
// [scrollLeft,scrollTop]
|
||||||
|
scroll?: [number, number];
|
||||||
|
};
|
||||||
|
|||||||
3
typings/replay/index.d.ts
vendored
3
typings/replay/index.d.ts
vendored
@@ -17,6 +17,7 @@ export declare class Replayer {
|
|||||||
private legacy_missingNodeRetryMap;
|
private legacy_missingNodeRetryMap;
|
||||||
private treeIndex;
|
private treeIndex;
|
||||||
private fragmentParentMap;
|
private fragmentParentMap;
|
||||||
|
private elementStateMap;
|
||||||
private imageMap;
|
private imageMap;
|
||||||
constructor(events: Array<eventWithTime | string>, config?: Partial<playerConfig>);
|
constructor(events: Array<eventWithTime | string>, config?: Partial<playerConfig>);
|
||||||
on(event: string, handler: Handler): this;
|
on(event: string, handler: Handler): this;
|
||||||
@@ -47,6 +48,8 @@ export declare class Replayer {
|
|||||||
private hoverElements;
|
private hoverElements;
|
||||||
private isUserInteraction;
|
private isUserInteraction;
|
||||||
private backToNormal;
|
private backToNormal;
|
||||||
|
private storeState;
|
||||||
|
private restoreState;
|
||||||
private warnNodeNotFound;
|
private warnNodeNotFound;
|
||||||
private warnCanvasMutationFailed;
|
private warnCanvasMutationFailed;
|
||||||
private debugNodeNotFound;
|
private debugNodeNotFound;
|
||||||
|
|||||||
3
typings/types.d.ts
vendored
3
typings/types.d.ts
vendored
@@ -354,4 +354,7 @@ export declare enum ReplayerEvents {
|
|||||||
StateChange = "state-change"
|
StateChange = "state-change"
|
||||||
}
|
}
|
||||||
export declare type MaskInputFn = (text: string) => string;
|
export declare type MaskInputFn = (text: string) => string;
|
||||||
|
export declare type ElementState = {
|
||||||
|
scroll?: [number, number];
|
||||||
|
};
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
Reference in New Issue
Block a user