Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

impl #650, CSS declaration observer #671

Merged
merged 1 commit into from
Aug 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/rrweb/src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,16 @@ function record<T = eventWithTime>(
},
}),
),
styleDeclarationCb: (r) =>
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.StyleDeclaration,
...r,
},
}),
),
canvasMutationCb: (p) =>
wrappedEmit(
wrapEvent({
Expand Down
85 changes: 77 additions & 8 deletions packages/rrweb/src/record/observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
fontCallback,
fontParam,
Mirror,
styleDeclarationCallback,
} from '../types';
import MutationBuffer from './mutation';
import { IframeManager } from './iframe-manager';
Expand Down Expand Up @@ -472,16 +473,18 @@ function initInputObserver(
};
}

function getNestedCSSRulePositions(rule: CSSStyleRule): number[] {
const positions: Array<number> = [];
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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -725,6 +782,7 @@ function mergeHooks(o: observerParam, hooks: hooksParam) {
inputCb,
mediaInteractionCb,
styleSheetRuleCb,
styleDeclarationCb,
canvasMutationCb,
fontCb,
} = o;
Expand Down Expand Up @@ -776,6 +834,12 @@ function mergeHooks(o: observerParam, hooks: hooksParam) {
}
styleSheetRuleCb(...p);
};
o.styleDeclarationCb = (...p: Arguments<styleDeclarationCallback>) => {
if (hooks.styleDeclaration) {
hooks.styleDeclaration(...p);
}
styleDeclarationCb(...p);
};
o.canvasMutationCb = (...p: Arguments<canvasMutationCallback>) => {
if (hooks.canvasMutation) {
hooks.canvasMutation(...p);
Expand Down Expand Up @@ -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)
: () => {};
Expand All @@ -873,6 +941,7 @@ export function initObservers(
inputHandler();
mediaInteractionHandler();
styleSheetObserver();
styleDeclarationObserver();
canvasMutationObserver();
fontObserver();
pluginHandlers.forEach((h) => h());
Expand Down
85 changes: 77 additions & 8 deletions packages/rrweb/src/replay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,12 @@ const defaultMouseTailConfig = {
} as const;

function indicatesTouchDevice(e: eventWithTime) {
return e.type == EventType.IncrementalSnapshot &&
return (
e.type == EventType.IncrementalSnapshot &&
(e.data.source == IncrementalSource.TouchMove ||
(e.data.source == IncrementalSource.MouseInteraction &&
e.data.type == MouseInteractions.TouchStart));
(e.data.source == IncrementalSource.MouseInteraction &&
e.data.type == MouseInteractions.TouchStart))
);
}

export class Replayer {
Expand Down Expand Up @@ -266,7 +268,6 @@ export class Replayer {
if (this.service.state.context.events.find(indicatesTouchDevice)) {
this.mouse.classList.add('touch-device');
}

}

public on(event: string, handler: Handler) {
Expand Down Expand Up @@ -489,7 +490,13 @@ export class Replayer {
castFn();
}
if (this.mousePos) {
this.moveAndHover(this.mousePos.x, this.mousePos.y, this.mousePos.id, true, this.mousePos.debugData);
this.moveAndHover(
this.mousePos.x,
this.mousePos.y,
this.mousePos.id,
true,
this.mousePos.debugData,
);
}
this.mousePos = null;
if (this.touchActive === true) {
Expand Down Expand Up @@ -952,14 +959,14 @@ export class Replayer {
void this.mouse.offsetWidth;
this.mouse.classList.add('active');
} else if (d.type === MouseInteractions.TouchStart) {
void this.mouse.offsetWidth; // needed for the position update of moveAndHover to apply without the .touch-active transition
void this.mouse.offsetWidth; // needed for the position update of moveAndHover to apply without the .touch-active transition
this.mouse.classList.add('touch-active');
} else if (d.type === MouseInteractions.TouchEnd) {
this.mouse.classList.remove('touch-active');
}
}
break;
case MouseInteractions.TouchCancel:
case MouseInteractions.TouchCancel:
if (isSync) {
this.touchActive = false;
} else {
Expand Down Expand Up @@ -1138,6 +1145,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;
Expand Down Expand Up @@ -1581,7 +1644,13 @@ export class Replayer {
}
}

private moveAndHover(x: number, y: number, id: number, isSync: boolean, debugData: incrementalData) {
private moveAndHover(
x: number,
y: number,
id: number,
isSync: boolean,
debugData: incrementalData,
) {
const target = this.mirror.getNode(id);
if (!target) {
return this.debugNodeNotFound(debugData, id);
Expand Down
30 changes: 29 additions & 1 deletion packages/rrweb/src/replay/virtual-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export enum StyleRuleType {
Insert,
Remove,
Snapshot,
SetProperty,
RemoveProperty,
}

type InsertRule = {
Expand All @@ -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<InsertRule | RemoveRule | SnapshotRule>;
export type VirtualStyleRules = Array<
InsertRule | RemoveRule | SnapshotRule | SetPropertyRule | RemovePropertyRule
>;
export type VirtualStyleRulesMap = Map<INode, VirtualStyleRules>;

export function getNestedRule(
Expand Down Expand Up @@ -88,6 +104,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);
}
});
}
Expand Down
Loading