impl replay the mutations and mouse interactions

This commit is contained in:
Yanzhen Yu
2026-04-01 12:00:00 +08:00
parent f7e4a90751
commit f4ded6b6d1
3 changed files with 86 additions and 10 deletions

File diff suppressed because one or more lines are too long

View File

@@ -6,8 +6,10 @@ import {
IncrementalSource, IncrementalSource,
fullSnapshotEvent, fullSnapshotEvent,
eventWithTime, eventWithTime,
MouseInteractions,
} from '../types'; } from '../types';
import eventsStr from './events'; import eventsStr from './events';
import { mirror, getIdNodeMap } from '../utils';
const _events: eventWithTime[] = JSON.parse(eventsStr); const _events: eventWithTime[] = JSON.parse(eventsStr);
@@ -94,13 +96,57 @@ class Replayer {
.replace(/>/g, '>'), .replace(/>/g, '>'),
); );
this.iframe.contentDocument!.close(); this.iframe.contentDocument!.close();
mirror.map = getIdNodeMap(this.iframe.contentDocument!);
// avoid form submit to refresh the iframe
this.iframe.contentDocument!.querySelectorAll('form').forEach(form => {
form.addEventListener('submit', evt => evt.preventDefault());
});
} }
} }
private applyIncremental(d: incrementalData) { private applyIncremental(d: incrementalData) {
switch (d.source) { switch (d.source) {
case IncrementalSource.Mutation: case IncrementalSource.Mutation: {
d.texts.forEach(mutation => {
const target = (mirror.getNode(mutation.id) as Node) as Text;
target.textContent = mutation.value;
});
d.attributes.forEach(mutation => {
const target = (mirror.getNode(mutation.id) as Node) as Element;
for (const attributeName in mutation.attributes) {
if (typeof attributeName === 'string') {
const value = mutation.attributes[attributeName];
if (value) {
target.setAttribute(attributeName, value);
} else {
target.removeAttribute(attributeName);
}
}
}
});
// TODO: update id node map
d.removes.forEach(mutation => {
const target = (mirror.getNode(mutation.id) as Node) as Element;
const parent = (mirror.getNode(mutation.parentId) as Node) as Element;
parent.removeChild(target);
});
d.adds.forEach(mutation => {
const target = (mirror.getNode(mutation.id) as Node) as Element;
const parent = (mirror.getNode(mutation.parentId) as Node) as Element;
if (mutation.nextId) {
const next = (mirror.getNode(mutation.nextId) as Node) as Element;
parent.insertBefore(target, next);
} else if (mutation.previousId) {
const previous = (mirror.getNode(
mutation.previousId,
) as Node) as Element;
parent.insertBefore(target, previous.nextSibling);
} else {
parent.appendChild(target);
}
});
break; break;
}
case IncrementalSource.MouseMove: case IncrementalSource.MouseMove:
d.positions.forEach(p => { d.positions.forEach(p => {
later(() => { later(() => {
@@ -109,8 +155,19 @@ class Replayer {
}, p.timeOffset); }, p.timeOffset);
}); });
break; break;
case IncrementalSource.MouseInteraction: case IncrementalSource.MouseInteraction: {
const event = new Event(MouseInteractions[d.type].toLowerCase());
const target = (mirror.getNode(d.id) as Node) as HTMLElement;
target.dispatchEvent(event);
if (d.type === MouseInteractions.Blur) {
target.blur();
} else if (d.type === MouseInteractions.Click) {
target.click();
} else if (d.type === MouseInteractions.Focus) {
target.focus();
}
break; break;
}
case IncrementalSource.Scroll: case IncrementalSource.Scroll:
// TODO: maybe element // TODO: maybe element
this.iframe.contentWindow!.scrollTo({ this.iframe.contentWindow!.scrollTo({
@@ -124,13 +181,11 @@ class Replayer {
this.iframe.height = `${d.height}px`; this.iframe.height = `${d.height}px`;
break; break;
case IncrementalSource.Input: { case IncrementalSource.Input: {
const target: HTMLInputElement | null = this.iframe.contentDocument!.querySelector( const target: HTMLInputElement = (mirror.getNode(
`[data-rrid="${d.id}"]`, d.id,
); ) as Node) as HTMLInputElement;
if (target) { target.checked = d.isChecked;
target.checked = d.isChecked; target.value = d.text;
target.value = d.text;
}
break; break;
} }
default: default:

View File

@@ -1,3 +1,4 @@
import { idNodeMap, NodeType, serializeNodeWithId } from 'rrweb-snapshot';
import { import {
Mirror, Mirror,
throttleOptions, throttleOptions,
@@ -29,6 +30,26 @@ export const mirror: Mirror = {
}, },
}; };
// TODO: transform this into the snapshot repo
export function getIdNodeMap(doc: Document) {
const map: idNodeMap = {};
function walk(n: Node) {
const node = serializeNodeWithId(n, doc, map);
if (!node) {
return null;
}
if (node.type === NodeType.Document || node.type === NodeType.Element) {
for (const _n of Array.from(n.childNodes)) {
walk(_n);
}
}
}
walk(doc);
return map;
}
// copy from underscore and modified // copy from underscore and modified
export function throttle<T>( export function throttle<T>(
func: (arg: T) => void, func: (arg: T) => void,