From 0da6cd908c1f7225501b56a5b5d7eef6db4fb5f3 Mon Sep 17 00:00:00 2001 From: John Pham Date: Fri, 13 Aug 2021 11:48:39 -0700 Subject: [PATCH] Record and replay nested stylesheet rules --- package.json | 2 +- src/record/observer.ts | 54 ++++++++++++++++++++++++++++++++++++ src/replay/index.ts | 16 ++++++++++- src/replay/virtual-styles.ts | 44 ++++++++++++++++++++++++++--- src/types.ts | 6 ++-- 5 files changed, 113 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index bc265d92..984bb6c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@highlight-run/rrweb", - "version": "0.12.4", + "version": "0.12.5", "description": "record and replay the web", "scripts": { "test": "npm run bundle:browser && cross-env TS_NODE_CACHE=false TS_NODE_FILES=true mocha -r ts-node/register -r ignore-styles -r jsdom-global/register test/**.test.ts", diff --git a/src/record/observer.ts b/src/record/observer.ts index 6261b73e..49915eda 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -496,6 +496,23 @@ 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); + pos.unshift(index); + } else { + const rules = Array.from(rule.parentStyleSheet!.cssRules); + const index = rules.indexOf(rule); + pos.unshift(index); + } + return pos; + } + return recurse(rule, positions); +} + function initStyleSheetObserver( cb: styleSheetRuleCallback, mirror: Mirror, @@ -524,9 +541,46 @@ function initStyleSheetObserver( return deleteRule.apply(this, arguments); }; + const groupingInsertRule = CSSGroupingRule.prototype.insertRule; + CSSGroupingRule.prototype.insertRule = function ( + rule: string, + index?: number, + ) { + const id = mirror.getId(this.parentStyleSheet.ownerNode as INode); + if (id !== -1) { + cb({ + id, + adds: [ + { + rule, + index: [ + ...getNestedCSSRulePositions(this), + index || 0, // defaults to 0 + ], + }, + ], + }); + } + return groupingInsertRule.apply(this, arguments); + }; + + const groupingDeleteRule = CSSGroupingRule.prototype.deleteRule; + CSSGroupingRule.prototype.deleteRule = function (index: number) { + const id = mirror.getId(this.parentStyleSheet.ownerNode as INode); + if (id !== -1) { + cb({ + id, + removes: [{ index: [...getNestedCSSRulePositions(this), index] }], + }); + } + return groupingDeleteRule.apply(this, arguments); + }; + return () => { CSSStyleSheet.prototype.insertRule = insertRule; CSSStyleSheet.prototype.deleteRule = deleteRule; + CSSGroupingRule.prototype.insertRule = groupingInsertRule; + CSSGroupingRule.prototype.deleteRule = groupingDeleteRule; }; } diff --git a/src/replay/index.ts b/src/replay/index.ts index 85a3b1f5..097f7086 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -1093,10 +1093,16 @@ export class Replayer { d.adds.forEach(({ rule, index }) => { if (styleSheet) { try { - const _index = + let _index; + if (Array.isArray(index)) { + const positions = [...index]; + _index = positions.pop(); + } else { + _index = index === undefined ? undefined : Math.min(index, styleSheet.cssRules.length); + } try { styleSheet.insertRule(rule, _index); } catch (e) { @@ -1123,7 +1129,15 @@ export class Replayer { rules?.push({ index, type: StyleRuleType.Remove }); } else { try { + if (Array.isArray(index)) { + const positions = [...index]; + const _index = positions.pop(); + if (_index) { + styleSheet?.deleteRule(_index); + } + } else { styleSheet?.deleteRule(index); + } } catch (e) { /** * same as insertRule diff --git a/src/replay/virtual-styles.ts b/src/replay/virtual-styles.ts index a371edd1..8a63837c 100644 --- a/src/replay/virtual-styles.ts +++ b/src/replay/virtual-styles.ts @@ -9,11 +9,11 @@ export enum StyleRuleType { type InsertRule = { cssText: string; type: StyleRuleType.Insert; - index?: number; + index?: number | number[]; }; type RemoveRule = { type: StyleRuleType.Remove; - index: number; + index: number | number[]; }; type SnapshotRule = { type: StyleRuleType.Snapshot; @@ -23,6 +23,22 @@ type SnapshotRule = { export type VirtualStyleRules = Array; export type VirtualStyleRulesMap = Map; +function getNestedRule( + rules: CSSRuleList, + position: number[], +): CSSGroupingRule { + const rule = rules[position[0]] as CSSGroupingRule; + if (position.length === 1) { + return rule; + } else { + return getNestedRule( + ((rule as CSSGroupingRule).cssRules[position[1]] as CSSGroupingRule) + .cssRules, + position.slice(2), + ); + } +} + export function applyVirtualStyleRulesToNode( storedRules: VirtualStyleRules, styleNode: HTMLStyleElement, @@ -30,7 +46,17 @@ export function applyVirtualStyleRulesToNode( storedRules.forEach((rule) => { if (rule.type === StyleRuleType.Insert) { try { - styleNode.sheet?.insertRule(rule.cssText, rule.index); + if (Array.isArray(rule.index)) { + const positions = [...rule.index]; + const insertAt = positions.pop(); + const nestedRule = getNestedRule( + styleNode.sheet!.cssRules, + positions, + ); + nestedRule.insertRule(rule.cssText, insertAt); + } else { + styleNode.sheet?.insertRule(rule.cssText, rule.index); + } } catch (e) { /** * sometimes we may capture rules with browser prefix @@ -39,7 +65,17 @@ export function applyVirtualStyleRulesToNode( } } else if (rule.type === StyleRuleType.Remove) { try { - styleNode.sheet?.deleteRule(rule.index); + if (Array.isArray(rule.index)) { + const positions = [...rule.index]; + const deleteAt = positions.pop(); + const nestedRule = getNestedRule( + styleNode.sheet!.cssRules, + positions, + ); + nestedRule.deleteRule(deleteAt || 0); + } else { + styleNode.sheet?.deleteRule(rule.index); + } } catch (e) { /** * accessing styleSheet rules may cause SecurityError diff --git a/src/types.ts b/src/types.ts index c90b5402..0a0eab9d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -387,11 +387,11 @@ export type scrollCallback = (p: scrollPosition) => void; export type styleSheetAddRule = { rule: string; - index?: number; + index?: number | number[]; }; export type styleSheetDeleteRule = { - index: number; + index: number | number[]; }; export type styleSheetRuleParam = { @@ -582,4 +582,4 @@ declare global { interface Window { HIG_CONFIGURATION: HighlightConfiguration; } -} \ No newline at end of file +}