impl #650, CSS declaration observer (#671)

This commit is contained in:
yz-yu
2026-04-01 12:00:00 +08:00
committed by GitHub
parent 0d5b46068d
commit 0efa7b0491
5 changed files with 217 additions and 18 deletions

View File

@@ -352,6 +352,16 @@ function record<T = eventWithTime>(
},
}),
),
styleDeclarationCb: (r) =>
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.StyleDeclaration,
...r,
},
}),
),
canvasMutationCb: (p) =>
wrappedEmit(
wrapEvent({

View File

@@ -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<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;
@@ -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<styleDeclarationCallback>) => {
if (hooks.styleDeclaration) {
hooks.styleDeclaration(...p);
}
styleDeclarationCb(...p);
};
o.canvasMutationCb = (...p: Arguments<canvasMutationCallback>) => {
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());

View File

@@ -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 {
@@ -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) {
@@ -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) {
@@ -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 {
@@ -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;
@@ -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);

View File

@@ -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<InsertRule | RemoveRule | SnapshotRule>;
export type VirtualStyleRules = Array<
InsertRule | RemoveRule | SnapshotRule | SetPropertyRule | RemovePropertyRule
>;
export type VirtualStyleRulesMap = Map<INode, VirtualStyleRules>;
export function getNestedRule(
@@ -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);
}
});
}

View File

@@ -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;
};
@@ -407,6 +415,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 = {