From 60fe27a57ebea1fbe3c6329d7ff3b8b445e0abba Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Wed, 18 Aug 2021 13:40:28 +0800 Subject: [PATCH] impl #650, CSS declaration observer --- packages/rrweb/src/record/index.ts | 10 +++ packages/rrweb/src/record/observer.ts | 85 +++++++++++++++++++-- packages/rrweb/src/replay/index.ts | 62 ++++++++++++++- packages/rrweb/src/replay/virtual-styles.ts | 30 +++++++- packages/rrweb/src/types.ts | 25 +++++- 5 files changed, 199 insertions(+), 13 deletions(-) diff --git a/packages/rrweb/src/record/index.ts b/packages/rrweb/src/record/index.ts index d99097c383..5930913a8a 100644 --- a/packages/rrweb/src/record/index.ts +++ b/packages/rrweb/src/record/index.ts @@ -352,6 +352,16 @@ function record( }, }), ), + styleDeclarationCb: (r) => + wrappedEmit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleDeclaration, + ...r, + }, + }), + ), canvasMutationCb: (p) => wrappedEmit( wrapEvent({ diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts index 9281c63c8c..afa38201be 100644 --- a/packages/rrweb/src/record/observer.ts +++ b/packages/rrweb/src/record/observer.ts @@ -43,6 +43,7 @@ import { fontCallback, fontParam, Mirror, + styleDeclarationCallback, } from '../types'; import MutationBuffer from './mutation'; import { IframeManager } from './iframe-manager'; @@ -472,16 +473,18 @@ function initInputObserver( }; } -function getNestedCSSRulePositions(rule: CSSStyleRule): number[] { - const positions: Array = []; - function recurse(rule: CSSRule, pos: number[]) { - if (rule.parentRule instanceof CSSGroupingRule) { - const rules = Array.from((rule.parentRule as CSSGroupingRule).cssRules); - const index = rules.indexOf(rule); +function getNestedCSSRulePositions(rule: CSSRule): number[] { + const positions: number[] = []; + function recurse(childRule: CSSRule, pos: number[]) { + if (childRule.parentRule instanceof CSSGroupingRule) { + const rules = Array.from( + (childRule.parentRule as CSSGroupingRule).cssRules, + ); + const index = rules.indexOf(childRule); pos.unshift(index); } else { - const rules = Array.from(rule.parentStyleSheet!.cssRules); - const index = rules.indexOf(rule); + const rules = Array.from(childRule.parentStyleSheet!.cssRules); + const index = rules.indexOf(childRule); pos.unshift(index); } return pos; @@ -560,6 +563,60 @@ function initStyleSheetObserver( }; } +function initStyleDeclarationObserver( + cb: styleDeclarationCallback, + mirror: Mirror, +): listenerHandler { + const setProperty = CSSStyleDeclaration.prototype.setProperty; + CSSStyleDeclaration.prototype.setProperty = function ( + this: CSSStyleDeclaration, + property, + value, + priority, + ) { + const id = mirror.getId( + (this.parentRule?.parentStyleSheet?.ownerNode as unknown) as INode, + ); + if (id !== -1) { + cb({ + id, + set: { + property, + value, + priority, + }, + index: getNestedCSSRulePositions(this.parentRule!), + }); + } + return setProperty.apply(this, arguments); + }; + + const removeProperty = CSSStyleDeclaration.prototype.removeProperty; + CSSStyleDeclaration.prototype.removeProperty = function ( + this: CSSStyleDeclaration, + property, + ) { + const id = mirror.getId( + (this.parentRule?.parentStyleSheet?.ownerNode as unknown) as INode, + ); + if (id !== -1) { + cb({ + id, + remove: { + property, + }, + index: getNestedCSSRulePositions(this.parentRule!), + }); + } + return removeProperty.apply(this, arguments); + }; + + return () => { + CSSStyleDeclaration.prototype.setProperty = setProperty; + CSSStyleDeclaration.prototype.removeProperty = removeProperty; + }; +} + function initMediaInteractionObserver( mediaInteractionCb: mediaInteractionCallback, blockClass: blockClass, @@ -725,6 +782,7 @@ function mergeHooks(o: observerParam, hooks: hooksParam) { inputCb, mediaInteractionCb, styleSheetRuleCb, + styleDeclarationCb, canvasMutationCb, fontCb, } = o; @@ -776,6 +834,12 @@ function mergeHooks(o: observerParam, hooks: hooksParam) { } styleSheetRuleCb(...p); }; + o.styleDeclarationCb = (...p: Arguments) => { + if (hooks.styleDeclaration) { + hooks.styleDeclaration(...p); + } + styleDeclarationCb(...p); + }; o.canvasMutationCb = (...p: Arguments) => { if (hooks.canvasMutation) { hooks.canvasMutation(...p); @@ -854,6 +918,10 @@ export function initObservers( o.styleSheetRuleCb, o.mirror, ); + const styleDeclarationObserver = initStyleDeclarationObserver( + o.styleDeclarationCb, + o.mirror, + ); const canvasMutationObserver = o.recordCanvas ? initCanvasMutationObserver(o.canvasMutationCb, o.blockClass, o.mirror) : () => {}; @@ -873,6 +941,7 @@ export function initObservers( inputHandler(); mediaInteractionHandler(); styleSheetObserver(); + styleDeclarationObserver(); canvasMutationObserver(); fontObserver(); pluginHandlers.forEach((h) => h()); diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 6d822569b6..fc81376abc 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -989,9 +989,9 @@ export class Replayer { nestedRule.insertRule(rule, insertAt); } else { const _index = - index === undefined - ? undefined - : Math.min(index, styleSheet.cssRules.length); + index === undefined + ? undefined + : Math.min(index, styleSheet.cssRules.length); styleSheet.insertRule(rule, _index); } } catch (e) { @@ -1037,6 +1037,62 @@ export class Replayer { } break; } + case IncrementalSource.StyleDeclaration: { + // same with StyleSheetRule + const target = this.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + + const styleEl = (target as Node) as HTMLStyleElement; + const parent = (target.parentNode as unknown) as INode; + const usingVirtualParent = this.fragmentParentMap.has(parent); + + const styleSheet = usingVirtualParent ? null : styleEl.sheet; + let rules: VirtualStyleRules = []; + + if (!styleSheet) { + if (this.virtualStyleRulesMap.has(target)) { + rules = this.virtualStyleRulesMap.get(target) as VirtualStyleRules; + } else { + rules = []; + this.virtualStyleRulesMap.set(target, rules); + } + } + + if (d.set) { + if (styleSheet) { + const rule = (getNestedRule( + styleSheet.rules, + d.index, + ) as unknown) as CSSStyleRule; + rule.style.setProperty(d.set.property, d.set.value, d.set.priority); + } else { + rules.push({ + type: StyleRuleType.SetProperty, + index: d.index, + ...d.set, + }); + } + } + + if (d.remove) { + if (styleSheet) { + const rule = (getNestedRule( + styleSheet.rules, + d.index, + ) as unknown) as CSSStyleRule; + rule.style.removeProperty(d.remove.property); + } else { + rules.push({ + type: StyleRuleType.RemoveProperty, + index: d.index, + ...d.remove, + }); + } + } + break; + } case IncrementalSource.CanvasMutation: { if (!this.config.UNSAFE_replayCanvas) { return; diff --git a/packages/rrweb/src/replay/virtual-styles.ts b/packages/rrweb/src/replay/virtual-styles.ts index e9086eedd6..d91eeff279 100644 --- a/packages/rrweb/src/replay/virtual-styles.ts +++ b/packages/rrweb/src/replay/virtual-styles.ts @@ -4,6 +4,8 @@ export enum StyleRuleType { Insert, Remove, Snapshot, + SetProperty, + RemoveProperty, } type InsertRule = { @@ -19,8 +21,22 @@ type SnapshotRule = { type: StyleRuleType.Snapshot; cssTexts: string[]; }; +type SetPropertyRule = { + type: StyleRuleType.SetProperty; + index: number[]; + property: string; + value: string | null; + priority: string | undefined; +}; +type RemovePropertyRule = { + type: StyleRuleType.RemoveProperty; + index: number[]; + property: string; +}; -export type VirtualStyleRules = Array; +export type VirtualStyleRules = Array< + InsertRule | RemoveRule | SnapshotRule | SetPropertyRule | RemovePropertyRule +>; export type VirtualStyleRulesMap = Map; export function getNestedRule( @@ -84,6 +100,18 @@ export function applyVirtualStyleRulesToNode( } } else if (rule.type === StyleRuleType.Snapshot) { restoreSnapshotOfStyleRulesToNode(rule.cssTexts, styleNode); + } else if (rule.type === StyleRuleType.SetProperty) { + const nativeRule = (getNestedRule( + styleNode.sheet!.cssRules, + rule.index, + ) as unknown) as CSSStyleRule; + nativeRule.style.setProperty(rule.property, rule.value, rule.priority); + } else if (rule.type === StyleRuleType.RemoveProperty) { + const nativeRule = (getNestedRule( + styleNode.sheet!.cssRules, + rule.index, + ) as unknown) as CSSStyleRule; + nativeRule.style.removeProperty(rule.property); } }); } diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts index a79e701f7d..02d5f29cf3 100644 --- a/packages/rrweb/src/types.ts +++ b/packages/rrweb/src/types.ts @@ -90,6 +90,7 @@ export enum IncrementalSource { Font, Log, Drag, + StyleDeclaration, } export type mutationData = { @@ -129,6 +130,10 @@ export type styleSheetRuleData = { source: IncrementalSource.StyleSheetRule; } & styleSheetRuleParam; +export type styleDeclarationData = { + source: IncrementalSource.StyleDeclaration; +} & styleDeclarationParam; + export type canvasMutationData = { source: IncrementalSource.CanvasMutation; } & canvasMutationParam; @@ -147,7 +152,8 @@ export type incrementalData = | mediaInteractionData | styleSheetRuleData | canvasMutationData - | fontData; + | fontData + | styleDeclarationData; export type event = | domContentLoadedEvent @@ -244,6 +250,7 @@ export type observerParam = { maskTextFn?: MaskTextFn; inlineStylesheet: boolean; styleSheetRuleCb: styleSheetRuleCallback; + styleDeclarationCb: styleDeclarationCallback; canvasMutationCb: canvasMutationCallback; fontCb: fontCallback; sampling: SamplingStrategy; @@ -271,6 +278,7 @@ export type hooksParam = { input?: inputCallback; mediaInteaction?: mediaInteractionCallback; styleSheetRule?: styleSheetRuleCallback; + styleDeclaration?: styleDeclarationCallback; canvasMutation?: canvasMutationCallback; font?: fontCallback; }; @@ -399,6 +407,21 @@ export type styleSheetRuleParam = { export type styleSheetRuleCallback = (s: styleSheetRuleParam) => void; +export type styleDeclarationParam = { + id: number; + index: number[]; + set?: { + property: string; + value: string | null; + priority: string | undefined; + }; + remove?: { + property: string; + }; +}; + +export type styleDeclarationCallback = (s: styleDeclarationParam) => void; + export type canvasMutationCallback = (p: canvasMutationParam) => void; export type canvasMutationParam = {