diff --git a/package.json b/package.json index d94974b5..39b72148 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@highlight-run/rrweb", - "version": "0.12.7", + "version": "0.12.8", "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/index.ts b/src/record/index.ts index 5f9f245c..bda9aaa3 100644 --- a/src/record/index.ts +++ b/src/record/index.ts @@ -201,7 +201,7 @@ function record( sampling, slimDOMOptions, iframeManager, - enableStrictPrivacy + enableStrictPrivacy, }, mirror, }); @@ -355,6 +355,16 @@ function record( }, }), ), + styleDeclarationCb: (r) => + wrappedEmit( + wrapEvent({ + type: EventType.IncrementalSnapshot, + data: { + source: IncrementalSource.StyleDeclaration, + ...r, + }, + }), + ), canvasMutationCb: (p) => wrappedEmit( wrapEvent({ @@ -408,7 +418,7 @@ function record( }), ), })) || [], - enableStrictPrivacy + enableStrictPrivacy, }, hooks, ); diff --git a/src/record/observer.ts b/src/record/observer.ts index 49915eda..5a7eb59d 100644 --- a/src/record/observer.ts +++ b/src/record/observer.ts @@ -43,6 +43,7 @@ import { fontCallback, fontParam, Mirror, + styleDeclarationCallback, } from '../types'; import MutationBuffer from './mutation'; import { IframeManager } from './iframe-manager'; @@ -496,16 +497,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; @@ -584,6 +587,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, @@ -749,6 +806,7 @@ function mergeHooks(o: observerParam, hooks: hooksParam) { inputCb, mediaInteractionCb, styleSheetRuleCb, + styleDeclarationCb, canvasMutationCb, fontCb, } = o; @@ -800,6 +858,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); @@ -879,6 +943,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) : () => {}; @@ -898,6 +966,7 @@ export function initObservers( inputHandler(); mediaInteractionHandler(); styleSheetObserver(); + styleDeclarationObserver(); canvasMutationObserver(); fontObserver(); pluginHandlers.forEach((h) => h()); diff --git a/src/replay/index.ts b/src/replay/index.ts index e36db248..46285c9b 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -29,6 +29,7 @@ import { Mirror, ElementState, SessionInterval, + mouseMovePos, } from '../types'; import { createMirror, @@ -71,6 +72,15 @@ const defaultMouseTailConfig = { strokeStyle: 'red', } as const; +function indicatesTouchDevice(e: eventWithTime) { + return ( + e.type == EventType.IncrementalSnapshot && + (e.data.source == IncrementalSource.TouchMove || + (e.data.source == IncrementalSource.MouseInteraction && + e.data.type == MouseInteractions.TouchStart)) + ); +} + export class Replayer { public wrapper: HTMLDivElement; public iframe: HTMLIFrameElement; @@ -111,6 +121,9 @@ export class Replayer { private newDocumentQueue: addedNodeMutation[] = []; + private mousePos: mouseMovePos | null = null; + private touchActive: boolean | null = null; + constructor( events: Array, config?: Partial, @@ -141,6 +154,7 @@ export class Replayer { this.handleResize = this.handleResize.bind(this); this.getCastFn = this.getCastFn.bind(this); + this.applyEventsSynchronously = this.applyEventsSynchronously.bind(this); this.emitter.on(ReplayerEvents.Resize, this.handleResize as Handler); this.setupDom(); @@ -194,6 +208,7 @@ export class Replayer { }, { getCastFn: this.getCastFn, + applyEventsSynchronously: this.applyEventsSynchronously, emitter: this.emitter, }, ); @@ -247,6 +262,9 @@ export class Replayer { ); }, 1); } + if (this.service.state.context.events.find(indicatesTouchDevice)) { + this.mouse.classList.add('touch-device'); + } } public on(event: string, handler: Handler) { @@ -465,6 +483,9 @@ export class Replayer { const event = this.config.unpackFn ? this.config.unpackFn(rawEvent as string) : (rawEvent as eventWithTime); + if (indicatesTouchDevice(event)) { + this.mouse.classList.add('touch-device'); + } Promise.resolve().then(() => this.service.send({ type: 'ADD_EVENT', payload: { event } }), ); @@ -527,6 +548,49 @@ export class Replayer { } } + private applyEventsSynchronously(events: Array) { + for (const event of events) { + switch (event.type) { + case EventType.DomContentLoaded: + case EventType.Load: + case EventType.Custom: + continue; + case EventType.FullSnapshot: + case EventType.Meta: + case EventType.Plugin: + break; + case EventType.IncrementalSnapshot: + switch (event.data.source) { + case IncrementalSource.MediaInteraction: + continue; + default: + break; + } + break; + default: + break; + } + const castFn = this.getCastFn(event, true); + castFn(); + } + if (this.mousePos) { + this.moveAndHover( + this.mousePos.x, + this.mousePos.y, + this.mousePos.id, + true, + this.mousePos.debugData, + ); + } + this.mousePos = null; + if (this.touchActive === true) { + this.mouse.classList.add('touch-active'); + } else if (this.touchActive === false) { + this.mouse.classList.remove('touch-active'); + } + this.touchActive = null; + } + private getCastFn(event: eventWithTime, isSync = false) { let castFn: undefined | (() => void); switch (event.type) { @@ -917,12 +981,17 @@ export class Replayer { case IncrementalSource.MouseMove: if (isSync) { const lastPosition = d.positions[d.positions.length - 1]; - this.moveAndHover(d, lastPosition.x, lastPosition.y, lastPosition.id); + this.mousePos = { + x: lastPosition.x, + y: lastPosition.y, + id: lastPosition.id, + debugData: d, + }; } else { d.positions.forEach((p) => { const action = { doAction: () => { - this.moveAndHover(d, p.x, p.y, p.id); + this.moveAndHover(p.x, p.y, p.id, isSync, d); }, delay: p.timeOffset + @@ -971,19 +1040,51 @@ export class Replayer { case MouseInteractions.Click: case MouseInteractions.TouchStart: case MouseInteractions.TouchEnd: - /** - * Click has no visual impact when replaying and may - * trigger navigation when apply to an link. - * So we will not call click(), instead we add an - * animation to the mouse element which indicate user - * clicked at this moment. - */ - if (!isSync) { - this.moveAndHover(d, d.x, d.y, d.id); - this.mouse.classList.remove('active'); - // tslint:disable-next-line - void this.mouse.offsetWidth; - this.mouse.classList.add('active'); + if (isSync) { + if (d.type === MouseInteractions.TouchStart) { + this.touchActive = true; + } else if (d.type === MouseInteractions.TouchEnd) { + this.touchActive = false; + } + this.mousePos = { + x: d.x, + y: d.y, + id: d.id, + debugData: d, + }; + } else { + console.log(d.type); + if (d.type === MouseInteractions.TouchStart) { + // don't draw a trail as user has lifted finger and is placing at a new point + this.tailPositions.length = 0; + } + this.moveAndHover(d.x, d.y, d.id, isSync, d); + if (d.type === MouseInteractions.Click) { + /* + * don't want target.click() here as could trigger an iframe navigation + * instead any effects of the click should already be covered by mutations + */ + /* + * removal and addition of .active class (along with void line to trigger repaint) + * triggers the 'click' css animation in styles/style.css + */ + this.mouse.classList.remove('active'); + // tslint:disable-next-line + 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 + this.mouse.classList.add('touch-active'); + } else if (d.type === MouseInteractions.TouchEnd) { + this.mouse.classList.remove('touch-active'); + } + } + break; + case MouseInteractions.TouchCancel: + if (isSync) { + this.touchActive = false; + } else { + this.mouse.classList.remove('touch-active'); } break; default: @@ -1158,6 +1259,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; @@ -1559,10 +1716,16 @@ export class Replayer { } } - private moveAndHover(d: incrementalData, x: number, y: number, id: number) { + private moveAndHover( + x: number, + y: number, + id: number, + isSync: boolean, + debugData: incrementalData, + ) { const target = this.mirror.getNode(id); if (!target) { - return this.debugNodeNotFound(d, id); + return this.debugNodeNotFound(debugData, id); } const base = getBaseDimension(target, this.iframe); @@ -1571,7 +1734,9 @@ export class Replayer { this.mouse.style.left = `${_x}px`; this.mouse.style.top = `${_y}px`; - this.drawMouseTail({ x: _x, y: _y }); + if (!isSync) { + this.drawMouseTail({ x: _x, y: _y }); + } this.hoverElements((target as Node) as Element); } diff --git a/src/replay/machine.ts b/src/replay/machine.ts index 9a9d38fa..2dbc512b 100644 --- a/src/replay/machine.ts +++ b/src/replay/machine.ts @@ -9,7 +9,6 @@ import { IncrementalSource, } from '../types'; import { Timer, addDelay } from './timer'; -import { needCastInSyncMode } from '../utils'; export type PlayerContext = { events: eventWithTime[]; @@ -77,11 +76,12 @@ export function discardPriorSnapshots( type PlayerAssets = { emitter: Emitter; + applyEventsSynchronously(events: Array): void; getCastFn(event: eventWithTime, isSync: boolean): () => void; }; export function createPlayerService( context: PlayerContext, - { getCastFn, emitter }: PlayerAssets, + { getCastFn, applyEventsSynchronously, emitter }: PlayerAssets, ) { const playerMachine = createMachine( { @@ -186,6 +186,7 @@ export function createPlayerService( emitter.emit(ReplayerEvents.PlayBack); } + const syncEvents = new Array(); const actions = new Array(); for (const event of neededEvents) { if ( @@ -196,14 +197,10 @@ export function createPlayerService( ) { continue; } - const isSync = event.timestamp < baselineTime; - if (isSync && !needCastInSyncMode(event)) { - continue; - } - const castFn = getCastFn(event, isSync); - if (isSync) { - castFn(); + if (event.timestamp < baselineTime) { + syncEvents.push(event); } else { + const castFn = getCastFn(event, false); actions.push({ doAction: () => { castFn(); @@ -213,6 +210,7 @@ export function createPlayerService( }); } } + applyEventsSynchronously(syncEvents); emitter.emit(ReplayerEvents.Flush); timer.addActions(actions); timer.start(); diff --git a/src/replay/styles/style.css b/src/replay/styles/style.css index a7424954..5b961c68 100644 --- a/src/replay/styles/style.css +++ b/src/replay/styles/style.css @@ -84,11 +84,12 @@ position: absolute; width: 20px; height: 20px; - transition: 0.05s linear; + transition: left 0.05s linear, top 0.05s linear; background-size: contain; background-position: center center; background-repeat: no-repeat; background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMi4wLjEsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCINCgkgdmlld0JveD0iMCAwIDMwMCAzMDAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMwMCAzMDA7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+DQoJLnN0MHtmaWxsOiNGRkZGRkY7fQ0KCS5zdDF7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjg7c3Ryb2tlLW1pdGVybGltaXQ6MTA7fQ0KPC9zdHlsZT4NCjxnIGlkPSJMYXllcl8yIj4NCgk8cG9seWdvbiBjbGFzcz0ic3QwIiBwb2ludHM9IjE0LDE3IDk4LjMsMjY0IDE3NCwxOTIgMjYxLjcsMjc5LjUgMjc5LjUsMjYxLjcgMTkyLDE3NCAyNjQsOTguMyAJIi8+DQo8L2c+DQo8ZyBpZD0iTGF5ZXJfMSI+DQoJPHRpdGxlPkRlc2lnbl90bnA8L3RpdGxlPg0KCTxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik0yOTIuMywyNTcuNWwtODcuOC04Ny43TDI2NiwxMDhjMi4zLTIuNCwyLjItNi4yLTAuMi04LjVjLTAuNS0wLjUtMS4xLTAuOS0xLjgtMS4yTDE0LjEsNi40DQoJCUMxMSw1LjIsNy41LDYuNyw2LjQsOS44Yy0wLjUsMS40LTAuNSwyLjksMCw0LjNsOTIsMjQ5LjljMS4xLDMuMSw0LjYsNC43LDcuNywzLjZjMC44LTAuMywxLjYtMC44LDIuMi0xLjRsNjEuNS02MS43bDg3LjcsODcuOA0KCQljMi4zLDIuMyw2LjEsMi4zLDguNSwwbDI2LjMtMjYuM0MyOTQuNiwyNjMuNywyOTQuNiwyNTkuOSwyOTIuMywyNTcuNUMyOTIuMywyNTcuNSwyOTIuMywyNTcuNSwyOTIuMywyNTcuNXogTTI2MS43LDI3OS41TDE3NCwxOTINCgkJYy0yLjMtMi4zLTYuMS0yLjMtOC41LDBsLTU5LjEsNTkuMWwtODQuMy0yMjlsMjI4LjcsODRMMTkyLDE2NS41Yy0yLjQsMi4zLTIuNCw2LjEsMCw4LjVjMCwwLDAsMCwwLDBsODcuNSw4Ny43TDI2MS43LDI3OS41eiIvPg0KPC9nPg0KPC9zdmc+DQo='); + border-color: transparent; /* otherwise we transition from black when .touch-device class is added */ } .replayer-mouse::after { content: ''; @@ -97,12 +98,39 @@ height: 20px; border-radius: 10px; background: rgb(73, 80, 246); - transform: translate(-10px, -10px); + border-radius: 100%; + transform: translate(-50%, -50%); opacity: 0.3; } .replayer-mouse.active::after { animation: click 0.2s ease-in-out 1; } +.replayer-mouse.touch-device { + background-image: none; /* there's no passive cursor on touch-only screens */ + width: 25px; + height: 25px; + border-width: 2px; + border-style: solid; + border-radius: 100%; + margin-left: -37px; + margin-top: -37px; + border-color: rgba(255, 255, 255, 0%); + background-color: rgba(0, 0, 0, 0%); + transition: left 0s linear, top 0s linear, border-color 0.2s ease-in-out, + background-color 0.2s ease-in-out; +} +.replayer-mouse.touch-device.touch-active { + border-color: rgba(255, 255, 255, 25%); + background-color: rgba(0, 0, 0, 45%); + transition: left 0.25s linear, top 0.25s linear, border-color 0.2s ease-in-out, + background-color 0.2s ease-in-out; +} +.replayer-mouse.touch-device::after { + opacity: 0; /* there's no passive cursor on touch-only screens */ +} +.replayer-mouse.touch-device.active::after { + animation: touch-click 0.2s ease-in-out 1; +} .replayer-mouse-tail { position: absolute; pointer-events: none; @@ -112,17 +140,26 @@ opacity: 0.3; width: 20px; height: 20px; - border-radius: 10px; - transform: translate(-10px, -10px); } 50% { opacity: 0.5; width: 10px; height: 10px; - border-radius: 5px; - transform: translate(-5px, -5px); } } +@keyframes touch-click { + 0% { + opacity: 0; + width: 20px; + height: 20px; + } + 50% { + opacity: 0.5; + width: 10px; + height: 10px; + } +} + .rr-player { position: relative; background: white; diff --git a/src/replay/virtual-styles.ts b/src/replay/virtual-styles.ts index c6f7b19a..8dab9a8f 100644 --- a/src/replay/virtual-styles.ts +++ b/src/replay/virtual-styles.ts @@ -4,6 +4,8 @@ export enum StyleRuleType { Insert, Remove, Snapshot, + SetProperty, + RemoveProperty, } type InsertRule = { @@ -20,7 +22,22 @@ type SnapshotRule = { cssTexts: string[]; }; -export type VirtualStyleRules = Array; +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 | SetPropertyRule | RemovePropertyRule +>; export type VirtualStyleRulesMap = Map; export function getNestedRule( @@ -88,6 +105,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/src/types.ts b/src/types.ts index 0a0eab9d..3039d316 100644 --- a/src/types.ts +++ b/src/types.ts @@ -97,6 +97,7 @@ export enum IncrementalSource { Font, Log, Drag, + StyleDeclaration, } export type mutationData = { @@ -136,6 +137,10 @@ export type styleSheetRuleData = { source: IncrementalSource.StyleSheetRule; } & styleSheetRuleParam; +export type styleDeclarationData = { + source: IncrementalSource.StyleDeclaration; +} & styleDeclarationParam; + export type canvasMutationData = { source: IncrementalSource.CanvasMutation; } & canvasMutationParam; @@ -154,7 +159,8 @@ export type incrementalData = | mediaInteractionData | styleSheetRuleData | canvasMutationData - | fontData; + | fontData + | styleDeclarationData; export type event = | domContentLoadedEvent @@ -252,6 +258,7 @@ export type observerParam = { maskTextFn?: MaskTextFn; inlineStylesheet: boolean; styleSheetRuleCb: styleSheetRuleCallback; + styleDeclarationCb: styleDeclarationCallback; canvasMutationCb: canvasMutationCallback; fontCb: fontCallback; sampling: SamplingStrategy; @@ -280,6 +287,7 @@ export type hooksParam = { input?: inputCallback; mediaInteaction?: mediaInteractionCallback; styleSheetRule?: styleSheetRuleCallback; + styleDeclaration?: styleDeclarationCallback; canvasMutation?: canvasMutationCallback; font?: fontCallback; }; @@ -355,6 +363,13 @@ export type mousePosition = { timeOffset: number; }; +export type mouseMovePos = { + x: number; + y: number; + id: number; + debugData: incrementalData; +}; + export enum MouseInteractions { MouseUp, MouseDown, @@ -366,6 +381,7 @@ export enum MouseInteractions { TouchStart, TouchMove_Departed, // we will start a separate observer for touch move event TouchEnd, + TouchCancel, } type mouseInteractionParam = { @@ -402,6 +418,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 = { diff --git a/src/utils.ts b/src/utils.ts index af57a410..e5ed5675 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -314,38 +314,6 @@ export function polyfill(win = window) { } } -export function needCastInSyncMode(event: eventWithTime): boolean { - switch (event.type) { - case EventType.DomContentLoaded: - case EventType.Load: - case EventType.Custom: - return false; - case EventType.FullSnapshot: - case EventType.Meta: - case EventType.Plugin: - return true; - default: - break; - } - - switch (event.data.source) { - case IncrementalSource.MouseMove: - case IncrementalSource.MouseInteraction: - case IncrementalSource.TouchMove: - case IncrementalSource.MediaInteraction: - return false; - case IncrementalSource.ViewportResize: - case IncrementalSource.StyleSheetRule: - case IncrementalSource.Scroll: - case IncrementalSource.Input: - return true; - default: - break; - } - - return true; -} - export type TreeNode = { id: number; mutation: addedNodeMutation; diff --git a/typings/utils.d.ts b/typings/utils.d.ts index d714f4c6..b9e3ea52 100644 --- a/typings/utils.d.ts +++ b/typings/utils.d.ts @@ -1,69 +1,121 @@ -import { Mirror, throttleOptions, listenerHandler, hookResetter, blockClass, eventWithTime, addedNodeMutation, removedNodeMutation, textMutation, attributeMutation, mutationData, scrollData, inputData, DocumentDimension } from './types'; +import { + Mirror, + throttleOptions, + listenerHandler, + hookResetter, + blockClass, + eventWithTime, + addedNodeMutation, + removedNodeMutation, + textMutation, + attributeMutation, + mutationData, + scrollData, + inputData, + DocumentDimension, +} from './types'; import { INode, serializedNodeWithId } from './snapshot'; -export declare function on(type: string, fn: EventListenerOrEventListenerObject, target?: Document | Window): listenerHandler; +export declare function on( + type: string, + fn: EventListenerOrEventListenerObject, + target?: Document | Window, +): listenerHandler; export declare function createMirror(): Mirror; export declare let _mirror: Mirror; -export declare function throttle(func: (arg: T) => void, wait: number, options?: throttleOptions): (arg: T) => void; -export declare function hookSetter(target: T, key: string | number | symbol, d: PropertyDescriptor, isRevoked?: boolean, win?: Window & typeof globalThis): hookResetter; -export declare function patch(source: { +export declare function throttle( + func: (arg: T) => void, + wait: number, + options?: throttleOptions, +): (arg: T) => void; +export declare function hookSetter( + target: T, + key: string | number | symbol, + d: PropertyDescriptor, + isRevoked?: boolean, + win?: Window & typeof globalThis, +): hookResetter; +export declare function patch( + source: { [key: string]: any; -}, name: string, replacement: (...args: any[]) => any): () => void; + }, + name: string, + replacement: (...args: any[]) => any, +): () => void; export declare function getWindowHeight(): number; export declare function getWindowWidth(): number; -export declare function isBlocked(node: Node | null, blockClass: blockClass): boolean; +export declare function isBlocked( + node: Node | null, + blockClass: blockClass, +): boolean; export declare function isIgnored(n: Node | INode): boolean; -export declare function isAncestorRemoved(target: INode, mirror: Mirror): boolean; -export declare function isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent; +export declare function isAncestorRemoved( + target: INode, + mirror: Mirror, +): boolean; +export declare function isTouchEvent( + event: MouseEvent | TouchEvent, +): event is TouchEvent; export declare function polyfill(win?: Window & typeof globalThis): void; -export declare function needCastInSyncMode(event: eventWithTime): boolean; export declare type TreeNode = { - id: number; - mutation: addedNodeMutation; - parent?: TreeNode; - children: Record; - texts: textMutation[]; - attributes: attributeMutation[]; + id: number; + mutation: addedNodeMutation; + parent?: TreeNode; + children: Record; + texts: textMutation[]; + attributes: attributeMutation[]; }; export declare class TreeIndex { - tree: Record; - private removeNodeMutations; - private textMutations; - private attributeMutations; - private indexes; - private removeIdSet; - private scrollMap; - private inputMap; - constructor(); - add(mutation: addedNodeMutation): void; - remove(mutation: removedNodeMutation, mirror: Mirror): void; - text(mutation: textMutation): void; - attribute(mutation: attributeMutation): void; - scroll(d: scrollData): void; - input(d: inputData): void; - flush(): { - mutationData: mutationData; - scrollMap: TreeIndex['scrollMap']; - inputMap: TreeIndex['inputMap']; - }; - private reset; + tree: Record; + private removeNodeMutations; + private textMutations; + private attributeMutations; + private indexes; + private removeIdSet; + private scrollMap; + private inputMap; + constructor(); + add(mutation: addedNodeMutation): void; + remove(mutation: removedNodeMutation, mirror: Mirror): void; + text(mutation: textMutation): void; + attribute(mutation: attributeMutation): void; + scroll(d: scrollData): void; + input(d: inputData): void; + flush(): { + mutationData: mutationData; + scrollMap: TreeIndex['scrollMap']; + inputMap: TreeIndex['inputMap']; + }; + private reset; } declare type ResolveTree = { - value: addedNodeMutation; - children: ResolveTree[]; - parent: ResolveTree | null; + value: addedNodeMutation; + children: ResolveTree[]; + parent: ResolveTree | null; }; -export declare function queueToResolveTrees(queue: addedNodeMutation[]): ResolveTree[]; -export declare function iterateResolveTree(tree: ResolveTree, cb: (mutation: addedNodeMutation) => unknown): void; +export declare function queueToResolveTrees( + queue: addedNodeMutation[], +): ResolveTree[]; +export declare function iterateResolveTree( + tree: ResolveTree, + cb: (mutation: addedNodeMutation) => unknown, +): void; declare type HTMLIFrameINode = HTMLIFrameElement & { - __sn: serializedNodeWithId; + __sn: serializedNodeWithId; }; export declare type AppendedIframe = { - mutationInQueue: addedNodeMutation; - builtNode: HTMLIFrameINode; + mutationInQueue: addedNodeMutation; + builtNode: HTMLIFrameINode; }; -export declare function isIframeINode(node: INode | ShadowRoot): node is HTMLIFrameINode; -export declare function getBaseDimension(node: Node, rootIframe: Node): DocumentDimension; -export declare function hasShadowRoot(n: T): n is T & { - shadowRoot: ShadowRoot; +export declare function isIframeINode( + node: INode | ShadowRoot, +): node is HTMLIFrameINode; +export declare function getBaseDimension( + node: Node, + rootIframe: Node, +): DocumentDimension; +export declare function hasShadowRoot( + n: T, +): n is T & { + shadowRoot: ShadowRoot; }; export {};