Skip to content

Commit

Permalink
fix: elements would lose some states like scroll position because of …
Browse files Browse the repository at this point in the history
…"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
  • Loading branch information
YunFeng0817 authored Nov 27, 2020
1 parent 658999c commit 96e4bfd
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 0 deletions.
64 changes: 64 additions & 0 deletions src/replay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
scrollData,
inputData,
canvasMutationData,
ElementState,
} from '../types';
import {
mirror,
Expand All @@ -45,6 +46,7 @@ const SKIP_TIME_INTERVAL = 5 * 1000;
const mitt = (mittProxy as any).default || mittProxy;

const REPLAY_CONSOLE_PREFIX = '[replayer]';
const SCROLL_ATTRIBUTE_NAME = '__rrweb_scroll__';

const defaultMouseTailConfig = {
duration: 500,
Expand Down Expand Up @@ -78,6 +80,7 @@ export class Replayer {

private treeIndex!: TreeIndex;
private fragmentParentMap!: Map<INode, INode>;
private elementStateMap!: Map<INode, ElementState>;

private imageMap: Map<eventWithTime, HTMLImageElement> = new Map();

Expand Down Expand Up @@ -113,6 +116,7 @@ export class Replayer {

this.treeIndex = new TreeIndex();
this.fragmentParentMap = new Map<INode, INode>();
this.elementStateMap = new Map<INode, ElementState>();
this.emitter.on(ReplayerEvents.Flush, () => {
const { scrollMap, inputMap } = this.treeIndex.flush();

Expand All @@ -130,8 +134,11 @@ export class Replayer {
((parent as unknown) as HTMLTextAreaElement).value = frag.textContent;
}
parent.appendChild(frag);
// restore state of elements after they are mounted
this.restoreState(parent);
}
this.fragmentParentMap.clear();
this.elementStateMap.clear();

for (const d of scrollMap.values()) {
this.applyScroll(d);
Expand Down Expand Up @@ -913,6 +920,14 @@ export class Replayer {
const realParent = this.fragmentParentMap.get(parent);
if (realParent && realParent.contains(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 {
parent.removeChild(target);
}
Expand Down Expand Up @@ -964,6 +979,10 @@ export class Replayer {
const virtualParent = (document.createDocumentFragment() as unknown) as INode;
mirror.map[mutation.parentId] = virtualParent;
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) {
virtualParent.appendChild(parent.firstChild);
}
Expand Down Expand Up @@ -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) {
this.warn(`Node with id '${id}' not found in`, d);
}
Expand Down
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,3 +466,9 @@ export enum ReplayerEvents {
}

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 changes: 3 additions & 0 deletions typings/replay/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export declare class Replayer {
private legacy_missingNodeRetryMap;
private treeIndex;
private fragmentParentMap;
private elementStateMap;
private imageMap;
constructor(events: Array<eventWithTime | string>, config?: Partial<playerConfig>);
on(event: string, handler: Handler): this;
Expand Down Expand Up @@ -47,6 +48,8 @@ export declare class Replayer {
private hoverElements;
private isUserInteraction;
private backToNormal;
private storeState;
private restoreState;
private warnNodeNotFound;
private warnCanvasMutationFailed;
private debugNodeNotFound;
Expand Down
3 changes: 3 additions & 0 deletions typings/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,4 +354,7 @@ export declare enum ReplayerEvents {
StateChange = "state-change"
}
export declare type MaskInputFn = (text: string) => string;
export declare type ElementState = {
scroll?: [number, number];
};
export {};

0 comments on commit 96e4bfd

Please sign in to comment.