add scroll and viewport resize observers

This commit is contained in:
Yanzhen Yu
2026-04-01 12:00:00 +08:00
parent 7078ce2f2a
commit ee4fc3ce7e
4 changed files with 121 additions and 29 deletions

View File

@@ -1,6 +1,6 @@
import { snapshot } from 'rrweb-snapshot'; import { snapshot } from 'rrweb-snapshot';
import initObservers from './observer'; import initObservers from './observer';
import { mirror } from '../utils'; import { mirror, on } from '../utils';
import { import {
EventType, EventType,
event, event,
@@ -9,14 +9,6 @@ import {
IncrementalSource, IncrementalSource,
} from '../types'; } from '../types';
function on(
type: string,
fn: EventListenerOrEventListenerObject,
target = document,
) {
target.addEventListener(type, fn, { capture: true, passive: true });
}
function wrapEvent(e: event): eventWithTime { function wrapEvent(e: event): eventWithTime {
return { return {
...e, ...e,
@@ -80,6 +72,26 @@ function record(options: recordOptions) {
}, },
}), }),
), ),
scrollCb: p =>
emit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Scroll,
...p,
},
}),
),
viewportResizeCb: d =>
emit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.ViewportResize,
...d,
},
}),
),
}); });
}); });
} catch (error) { } catch (error) {

View File

@@ -1,5 +1,5 @@
import { INode } from 'rrweb-snapshot'; import { INode } from 'rrweb-snapshot';
import { mirror, throttle } from '../utils'; import { mirror, throttle, on } from '../utils';
import { import {
mutationCallBack, mutationCallBack,
textMutation, textMutation,
@@ -9,9 +9,11 @@ import {
observerParam, observerParam,
mousemoveCallBack, mousemoveCallBack,
mousePosition, mousePosition,
handlerMap,
mouseInteractionCallBack, mouseInteractionCallBack,
MouseInteractions, MouseInteractions,
listenerHandler,
scrollCallback,
viewportResizeCallback,
} from '../types'; } from '../types';
function initMutationObserver(cb: mutationCallBack): MutationObserver { function initMutationObserver(cb: mutationCallBack): MutationObserver {
@@ -68,6 +70,7 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
id: mirror.getId(n as INode), id: mirror.getId(n as INode),
}); });
}); });
// TODO: init new nodes to be INode
addedNodes.forEach(n => { addedNodes.forEach(n => {
adds.push({ adds.push({
parentId: id, parentId: id,
@@ -104,7 +107,7 @@ function initMutationObserver(cb: mutationCallBack): MutationObserver {
return observer; return observer;
} }
function initMousemoveObserver(cb: mousemoveCallBack): () => void { function initMousemoveObserver(cb: mousemoveCallBack): listenerHandler {
let positions: mousePosition[] = []; let positions: mousePosition[] = [];
let timeBaseline: number | null; let timeBaseline: number | null;
const wrappedCb = throttle(() => { const wrappedCb = throttle(() => {
@@ -136,16 +139,13 @@ function initMousemoveObserver(cb: mousemoveCallBack): () => void {
trailing: false, trailing: false,
}, },
); );
document.addEventListener('mousemove', updatePosition); return on('mousemove', updatePosition);
return () => {
document.removeEventListener('mousemove', updatePosition);
};
} }
function initMouseInteractionObserver( function initMouseInteractionObserver(
cb: mouseInteractionCallBack, cb: mouseInteractionCallBack,
): () => void { ): listenerHandler {
const handlers: handlerMap = {}; const handlers: listenerHandler[] = [];
const getHandler = (eventKey: keyof typeof MouseInteractions) => { const getHandler = (eventKey: keyof typeof MouseInteractions) => {
return (event: MouseEvent) => { return (event: MouseEvent) => {
const id = mirror.getId(event.target as INode); const id = mirror.getId(event.target as INode);
@@ -163,25 +163,69 @@ function initMouseInteractionObserver(
.forEach((eventKey: keyof typeof MouseInteractions) => { .forEach((eventKey: keyof typeof MouseInteractions) => {
const eventName = eventKey.toLowerCase(); const eventName = eventKey.toLowerCase();
const handler = getHandler(eventKey); const handler = getHandler(eventKey);
handlers[eventName] = handler; handlers.push(on(eventName, handler));
document.addEventListener(eventName, handler);
}); });
return () => { return () => {
Object.keys(handlers).forEach(eventName => { handlers.forEach(h => h());
document.removeEventListener(eventName, handlers[eventName]);
});
}; };
} }
function initScrollObserver(cb: scrollCallback): listenerHandler {
const updatePosition = throttle<UIEvent>(evt => {
if (!evt.target) {
return;
}
const id = mirror.getId(evt.target as INode);
if (evt.target === document) {
cb({
id,
x: document.documentElement.scrollTop,
y: document.documentElement.scrollLeft,
});
} else {
cb({
id,
x: (evt.target as HTMLElement).scrollTop,
y: (evt.target as HTMLElement).scrollLeft,
});
}
}, 100);
return on('scroll', updatePosition);
}
function initViewportResizeObserver(
cb: viewportResizeCallback,
): listenerHandler {
const updateDimension = throttle(() => {
const height =
window.innerHeight ||
(document.documentElement && document.documentElement.clientHeight) ||
(document.body && document.body.clientHeight);
const width =
window.innerWidth ||
(document.documentElement && document.documentElement.clientWidth) ||
(document.body && document.body.clientWidth);
cb({
width: Number(width),
height: Number(height),
});
}, 200);
return on('resize', updateDimension, window);
}
export default function initObservers(o: observerParam) { export default function initObservers(o: observerParam) {
const mutationObserver = initMutationObserver(o.mutationCb); const mutationObserver = initMutationObserver(o.mutationCb);
const mousemoveHandler = initMousemoveObserver(o.mousemoveCb); const mousemoveHandler = initMousemoveObserver(o.mousemoveCb);
const mouseInteractionHandler = initMouseInteractionObserver( const mouseInteractionHandler = initMouseInteractionObserver(
o.mouseInteractionCb, o.mouseInteractionCb,
); );
const scrollHandler = initScrollObserver(o.scrollCb);
const viewportResizeHandler = initViewportResizeObserver(o.viewportResizeCb);
return { return {
mutationObserver, mutationObserver,
mousemoveHandler, mousemoveHandler,
mouseInteractionHandler, mouseInteractionHandler,
scrollHandler,
viewportResizeHandler,
}; };
} }

View File

@@ -35,6 +35,8 @@ export enum IncrementalSource {
Mutation, Mutation,
MouseMove, MouseMove,
MouseInteraction, MouseInteraction,
Scroll,
ViewportResize,
} }
export type mutationData = { export type mutationData = {
@@ -50,10 +52,20 @@ export type mouseInteractionData = {
source: IncrementalSource.MouseInteraction; source: IncrementalSource.MouseInteraction;
} & mouseInteractionParam; } & mouseInteractionParam;
export type scrollData = {
source: IncrementalSource.Scroll;
} & scrollPosition;
export type viewportResizeData = {
source: IncrementalSource.ViewportResize;
} & viewportResizeDimention;
export type incrementalData = export type incrementalData =
| mutationData | mutationData
| mousemoveData | mousemoveData
| mouseInteractionData; | mouseInteractionData
| scrollData
| viewportResizeData;
export type event = export type event =
| domContentLoadedEvent | domContentLoadedEvent
@@ -73,6 +85,8 @@ export type observerParam = {
mutationCb: mutationCallBack; mutationCb: mutationCallBack;
mousemoveCb: mousemoveCallBack; mousemoveCb: mousemoveCallBack;
mouseInteractionCb: mouseInteractionCallBack; mouseInteractionCb: mouseInteractionCallBack;
scrollCb: scrollCallback;
viewportResizeCb: viewportResizeCallback;
}; };
export type textMutation = { export type textMutation = {
@@ -116,10 +130,6 @@ export type mousePosition = {
timeOffset: number; timeOffset: number;
}; };
export type handlerMap = {
[key: string]: EventListener;
};
export enum MouseInteractions { export enum MouseInteractions {
MouseUp, MouseUp,
MouseDown, MouseDown,
@@ -142,6 +152,21 @@ type mouseInteractionParam = {
export type mouseInteractionCallBack = (d: mouseInteractionParam) => void; export type mouseInteractionCallBack = (d: mouseInteractionParam) => void;
export type scrollPosition = {
id: number;
x: number;
y: number;
};
export type scrollCallback = (p: scrollPosition) => void;
export type viewportResizeDimention = {
width: number;
height: number;
};
export type viewportResizeCallback = (d: viewportResizeDimention) => void;
export type Mirror = { export type Mirror = {
map: idNodeMap; map: idNodeMap;
getId: (n: INode) => number; getId: (n: INode) => number;
@@ -152,3 +177,5 @@ export type throttleOptions = {
leading?: boolean; leading?: boolean;
trailing?: boolean; trailing?: boolean;
}; };
export type listenerHandler = () => void;

View File

@@ -1,4 +1,13 @@
import { Mirror, throttleOptions } from './types'; import { Mirror, throttleOptions, listenerHandler } from './types';
export function on(
type: string,
fn: EventListenerOrEventListenerObject,
target: Document | Window = document,
): listenerHandler {
target.addEventListener(type, fn, { capture: true, passive: true });
return () => target.removeEventListener(type, fn);
}
export const mirror: Mirror = { export const mirror: Mirror = {
map: {}, map: {},