From 1c4357780780c571acf1d13a58dadd4e7dbf3cf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=B3=BD=E5=BA=B7?= Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] Add nested scroll support (#31) --- src/rebuild.ts | 29 +++++++++++++++++++++++++++-- src/snapshot.ts | 7 +++++++ src/types.ts | 2 +- typings/types.d.ts | 2 +- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/rebuild.ts b/src/rebuild.ts index c9be6d23..bb26aefa 100644 --- a/src/rebuild.ts +++ b/src/rebuild.ts @@ -102,7 +102,7 @@ function buildNode( continue; } let value = n.attributes[name]; - value = typeof value === 'boolean' ? '' : value; + value = typeof value === 'boolean' || typeof value === 'number' ? '' : value; // attribute names start with rr_ are internal attributes added by rrweb if (!name.startsWith('rr_')) { const isTextarea = tagName === 'textarea' && name === 'value'; @@ -220,6 +220,29 @@ export function buildNodeWithSN( return node as INode; } +function sideEffects(idNodeMap: idNodeMap) { + for(let id in idNodeMap) { + const node = idNodeMap[id]; + const n = node.__sn; + if (n.type !== NodeType.Element) { + continue; + } + const el = node as Node as HTMLElement; + for(const name in n.attributes) { + if (!(n.attributes.hasOwnProperty(name) && name.startsWith('rr_'))) { + continue; + } + const value = n.attributes[name]; + if (name === 'rr_scrollLeft') { + el.scrollLeft = value as number; + } + if (name === 'rr_scrollTop') { + el.scrollTop = value as number; + } + } + } +} + function rebuild( n: serializedNodeWithId, doc: Document, @@ -229,7 +252,9 @@ function rebuild( HACK_CSS: boolean = true, ): [Node | null, idNodeMap] { const idNodeMap: idNodeMap = {}; - return [buildNodeWithSN(n, doc, idNodeMap, false, HACK_CSS), idNodeMap]; + const node = buildNodeWithSN(n, doc, idNodeMap, false, HACK_CSS); + sideEffects(idNodeMap); + return [node, idNodeMap]; } export default rebuild; diff --git a/src/snapshot.ts b/src/snapshot.ts index 4519a2be..685e0fd5 100644 --- a/src/snapshot.ts +++ b/src/snapshot.ts @@ -269,6 +269,13 @@ function serializeNode( ? 'paused' : 'played'; } + // scroll + if ((n as HTMLElement).scrollLeft) { + attributes.rr_scrollLeft = (n as HTMLElement).scrollLeft; + } + if ((n as HTMLElement).scrollTop) { + attributes.rr_scrollTop = (n as HTMLElement).scrollTop; + } if (needBlock) { const { width, height } = (n as HTMLElement).getBoundingClientRect(); attributes.rr_width = `${width}px`; diff --git a/src/types.ts b/src/types.ts index a5abef35..d8546e84 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,7 +20,7 @@ export type documentTypeNode = { }; export type attributes = { - [key: string]: string | boolean; + [key: string]: string | number | boolean; }; export type elementNode = { type: NodeType.Element; diff --git a/typings/types.d.ts b/typings/types.d.ts index 1c819ee8..90271c14 100644 --- a/typings/types.d.ts +++ b/typings/types.d.ts @@ -17,7 +17,7 @@ export declare type documentTypeNode = { systemId: string; }; export declare type attributes = { - [key: string]: string | boolean; + [key: string]: string | number | boolean; }; export declare type elementNode = { type: NodeType.Element;