refactoring observer options (#826)
This commit is contained in:
@@ -2,26 +2,21 @@ import {
|
|||||||
INode,
|
INode,
|
||||||
serializeNodeWithId,
|
serializeNodeWithId,
|
||||||
transformAttribute,
|
transformAttribute,
|
||||||
MaskInputOptions,
|
|
||||||
SlimDOMOptions,
|
|
||||||
IGNORED_NODE,
|
IGNORED_NODE,
|
||||||
isShadowRoot,
|
isShadowRoot,
|
||||||
needMaskingText,
|
needMaskingText,
|
||||||
maskInputValue,
|
maskInputValue,
|
||||||
MaskTextFn,
|
|
||||||
MaskInputFn,
|
|
||||||
} from 'rrweb-snapshot';
|
} from 'rrweb-snapshot';
|
||||||
import {
|
import {
|
||||||
mutationRecord,
|
mutationRecord,
|
||||||
blockClass,
|
|
||||||
maskTextClass,
|
|
||||||
mutationCallBack,
|
|
||||||
textCursor,
|
textCursor,
|
||||||
attributeCursor,
|
attributeCursor,
|
||||||
removedNodeMutation,
|
removedNodeMutation,
|
||||||
addedNodeMutation,
|
addedNodeMutation,
|
||||||
Mirror,
|
Mirror,
|
||||||
styleAttributeValue,
|
styleAttributeValue,
|
||||||
|
observerParam,
|
||||||
|
MutationBufferParam,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import {
|
import {
|
||||||
isBlocked,
|
isBlocked,
|
||||||
@@ -30,9 +25,6 @@ import {
|
|||||||
isIframeINode,
|
isIframeINode,
|
||||||
hasShadowRoot,
|
hasShadowRoot,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { IframeManager } from './iframe-manager';
|
|
||||||
import { CanvasManager } from './observers/canvas/canvas-manager';
|
|
||||||
import { ShadowDomManager } from './shadow-dom-manager';
|
|
||||||
|
|
||||||
type DoubleLinkedListNode = {
|
type DoubleLinkedListNode = {
|
||||||
previous: DoubleLinkedListNode | null;
|
previous: DoubleLinkedListNode | null;
|
||||||
@@ -163,61 +155,47 @@ export default class MutationBuffer {
|
|||||||
private movedSet = new Set<Node>();
|
private movedSet = new Set<Node>();
|
||||||
private droppedSet = new Set<Node>();
|
private droppedSet = new Set<Node>();
|
||||||
|
|
||||||
private emissionCallback: mutationCallBack;
|
private mutationCb: observerParam['mutationCb'];
|
||||||
private blockClass: blockClass;
|
private blockClass: observerParam['blockClass'];
|
||||||
private blockSelector: string | null;
|
private blockSelector: observerParam['blockSelector'];
|
||||||
private maskTextClass: maskTextClass;
|
private maskTextClass: observerParam['maskTextClass'];
|
||||||
private maskTextSelector: string | null;
|
private maskTextSelector: observerParam['maskTextSelector'];
|
||||||
private inlineStylesheet: boolean;
|
private inlineStylesheet: observerParam['inlineStylesheet'];
|
||||||
private maskInputOptions: MaskInputOptions;
|
private maskInputOptions: observerParam['maskInputOptions'];
|
||||||
private maskTextFn: MaskTextFn | undefined;
|
private maskTextFn: observerParam['maskTextFn'];
|
||||||
private maskInputFn: MaskInputFn | undefined;
|
private maskInputFn: observerParam['maskInputFn'];
|
||||||
private recordCanvas: boolean;
|
private recordCanvas: observerParam['recordCanvas'];
|
||||||
private inlineImages: boolean;
|
private inlineImages: observerParam['inlineImages'];
|
||||||
private slimDOMOptions: SlimDOMOptions;
|
private slimDOMOptions: observerParam['slimDOMOptions'];
|
||||||
private doc: Document;
|
private doc: observerParam['doc'];
|
||||||
|
private mirror: observerParam['mirror'];
|
||||||
|
private iframeManager: observerParam['iframeManager'];
|
||||||
|
private shadowDomManager: observerParam['shadowDomManager'];
|
||||||
|
private canvasManager: observerParam['canvasManager'];
|
||||||
|
|
||||||
private mirror: Mirror;
|
public init(options: MutationBufferParam) {
|
||||||
private iframeManager: IframeManager;
|
([
|
||||||
private shadowDomManager: ShadowDomManager;
|
'mutationCb',
|
||||||
private canvasManager: CanvasManager;
|
'blockClass',
|
||||||
|
'blockSelector',
|
||||||
public init(
|
'maskTextClass',
|
||||||
cb: mutationCallBack,
|
'maskTextSelector',
|
||||||
blockClass: blockClass,
|
'inlineStylesheet',
|
||||||
blockSelector: string | null,
|
'maskInputOptions',
|
||||||
maskTextClass: maskTextClass,
|
'maskTextFn',
|
||||||
maskTextSelector: string | null,
|
'maskInputFn',
|
||||||
inlineStylesheet: boolean,
|
'recordCanvas',
|
||||||
maskInputOptions: MaskInputOptions,
|
'inlineImages',
|
||||||
maskTextFn: MaskTextFn | undefined,
|
'slimDOMOptions',
|
||||||
maskInputFn: MaskInputFn | undefined,
|
'doc',
|
||||||
recordCanvas: boolean,
|
'mirror',
|
||||||
inlineImages: boolean,
|
'iframeManager',
|
||||||
slimDOMOptions: SlimDOMOptions,
|
'shadowDomManager',
|
||||||
doc: Document,
|
'canvasManager',
|
||||||
mirror: Mirror,
|
] as const).forEach((key) => {
|
||||||
iframeManager: IframeManager,
|
// just a type trick, the runtime result is correct
|
||||||
shadowDomManager: ShadowDomManager,
|
this[key] = options[key] as never;
|
||||||
canvasManager: CanvasManager,
|
});
|
||||||
) {
|
|
||||||
this.blockClass = blockClass;
|
|
||||||
this.blockSelector = blockSelector;
|
|
||||||
this.maskTextClass = maskTextClass;
|
|
||||||
this.maskTextSelector = maskTextSelector;
|
|
||||||
this.inlineStylesheet = inlineStylesheet;
|
|
||||||
this.maskInputOptions = maskInputOptions;
|
|
||||||
this.maskTextFn = maskTextFn;
|
|
||||||
this.maskInputFn = maskInputFn;
|
|
||||||
this.recordCanvas = recordCanvas;
|
|
||||||
this.inlineImages = inlineImages;
|
|
||||||
this.slimDOMOptions = slimDOMOptions;
|
|
||||||
this.emissionCallback = cb;
|
|
||||||
this.doc = doc;
|
|
||||||
this.mirror = mirror;
|
|
||||||
this.iframeManager = iframeManager;
|
|
||||||
this.shadowDomManager = shadowDomManager;
|
|
||||||
this.canvasManager = canvasManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public freeze() {
|
public freeze() {
|
||||||
@@ -441,7 +419,7 @@ export default class MutationBuffer {
|
|||||||
this.droppedSet = new Set<Node>();
|
this.droppedSet = new Set<Node>();
|
||||||
this.movedMap = {};
|
this.movedMap = {};
|
||||||
|
|
||||||
this.emissionCallback(payload);
|
this.mutationCb(payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
private processMutation = (m: mutationRecord) => {
|
private processMutation = (m: mutationRecord) => {
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
import {
|
import { INode, MaskInputOptions, maskInputValue } from 'rrweb-snapshot';
|
||||||
INode,
|
|
||||||
MaskInputOptions,
|
|
||||||
SlimDOMOptions,
|
|
||||||
maskInputValue,
|
|
||||||
MaskInputFn,
|
|
||||||
MaskTextFn,
|
|
||||||
} from 'rrweb-snapshot';
|
|
||||||
import { FontFaceSet } from 'css-font-loading-module';
|
import { FontFaceSet } from 'css-font-loading-module';
|
||||||
import {
|
import {
|
||||||
throttle,
|
throttle,
|
||||||
@@ -31,25 +24,19 @@ import {
|
|||||||
inputValue,
|
inputValue,
|
||||||
inputCallback,
|
inputCallback,
|
||||||
hookResetter,
|
hookResetter,
|
||||||
blockClass,
|
|
||||||
maskTextClass,
|
|
||||||
IncrementalSource,
|
IncrementalSource,
|
||||||
hooksParam,
|
hooksParam,
|
||||||
Arguments,
|
Arguments,
|
||||||
mediaInteractionCallback,
|
mediaInteractionCallback,
|
||||||
MediaInteractions,
|
MediaInteractions,
|
||||||
SamplingStrategy,
|
|
||||||
canvasMutationCallback,
|
canvasMutationCallback,
|
||||||
fontCallback,
|
fontCallback,
|
||||||
fontParam,
|
fontParam,
|
||||||
Mirror,
|
|
||||||
styleDeclarationCallback,
|
styleDeclarationCallback,
|
||||||
IWindow,
|
IWindow,
|
||||||
|
MutationBufferParam,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import MutationBuffer from './mutation';
|
import MutationBuffer from './mutation';
|
||||||
import { IframeManager } from './iframe-manager';
|
|
||||||
import { ShadowDomManager } from './shadow-dom-manager';
|
|
||||||
import { CanvasManager } from './observers/canvas/canvas-manager';
|
|
||||||
|
|
||||||
type WindowWithStoredMutationObserver = IWindow & {
|
type WindowWithStoredMutationObserver = IWindow & {
|
||||||
__rrMutationObserver?: MutationObserver;
|
__rrMutationObserver?: MutationObserver;
|
||||||
@@ -87,47 +74,13 @@ function getEventTarget(event: Event): EventTarget | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function initMutationObserver(
|
export function initMutationObserver(
|
||||||
cb: mutationCallBack,
|
options: MutationBufferParam,
|
||||||
doc: Document,
|
|
||||||
blockClass: blockClass,
|
|
||||||
blockSelector: string | null,
|
|
||||||
maskTextClass: maskTextClass,
|
|
||||||
maskTextSelector: string | null,
|
|
||||||
inlineStylesheet: boolean,
|
|
||||||
maskInputOptions: MaskInputOptions,
|
|
||||||
maskTextFn: MaskTextFn | undefined,
|
|
||||||
maskInputFn: MaskInputFn | undefined,
|
|
||||||
recordCanvas: boolean,
|
|
||||||
inlineImages: boolean,
|
|
||||||
slimDOMOptions: SlimDOMOptions,
|
|
||||||
mirror: Mirror,
|
|
||||||
iframeManager: IframeManager,
|
|
||||||
shadowDomManager: ShadowDomManager,
|
|
||||||
canvasManager: CanvasManager,
|
|
||||||
rootEl: Node,
|
rootEl: Node,
|
||||||
): MutationObserver {
|
): MutationObserver {
|
||||||
const mutationBuffer = new MutationBuffer();
|
const mutationBuffer = new MutationBuffer();
|
||||||
mutationBuffers.push(mutationBuffer);
|
mutationBuffers.push(mutationBuffer);
|
||||||
// see mutation.ts for details
|
// see mutation.ts for details
|
||||||
mutationBuffer.init(
|
mutationBuffer.init(options);
|
||||||
cb,
|
|
||||||
blockClass,
|
|
||||||
blockSelector,
|
|
||||||
maskTextClass,
|
|
||||||
maskTextSelector,
|
|
||||||
inlineStylesheet,
|
|
||||||
maskInputOptions,
|
|
||||||
maskTextFn,
|
|
||||||
maskInputFn,
|
|
||||||
recordCanvas,
|
|
||||||
inlineImages,
|
|
||||||
slimDOMOptions,
|
|
||||||
doc,
|
|
||||||
mirror,
|
|
||||||
iframeManager,
|
|
||||||
shadowDomManager,
|
|
||||||
canvasManager,
|
|
||||||
);
|
|
||||||
let mutationObserverCtor =
|
let mutationObserverCtor =
|
||||||
window.MutationObserver ||
|
window.MutationObserver ||
|
||||||
/**
|
/**
|
||||||
@@ -167,12 +120,12 @@ export function initMutationObserver(
|
|||||||
return observer;
|
return observer;
|
||||||
}
|
}
|
||||||
|
|
||||||
function initMoveObserver(
|
function initMoveObserver({
|
||||||
cb: mousemoveCallBack,
|
mousemoveCb,
|
||||||
sampling: SamplingStrategy,
|
sampling,
|
||||||
doc: Document,
|
doc,
|
||||||
mirror: Mirror,
|
mirror,
|
||||||
): listenerHandler {
|
}: observerParam): listenerHandler {
|
||||||
if (sampling.mousemove === false) {
|
if (sampling.mousemove === false) {
|
||||||
return () => {};
|
return () => {};
|
||||||
}
|
}
|
||||||
@@ -194,7 +147,7 @@ function initMoveObserver(
|
|||||||
| IncrementalSource.Drag,
|
| IncrementalSource.Drag,
|
||||||
) => {
|
) => {
|
||||||
const totalOffset = Date.now() - timeBaseline!;
|
const totalOffset = Date.now() - timeBaseline!;
|
||||||
cb(
|
mousemoveCb(
|
||||||
positions.map((p) => {
|
positions.map((p) => {
|
||||||
p.timeOffset -= totalOffset;
|
p.timeOffset -= totalOffset;
|
||||||
return p;
|
return p;
|
||||||
@@ -246,13 +199,13 @@ function initMoveObserver(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function initMouseInteractionObserver(
|
function initMouseInteractionObserver({
|
||||||
cb: mouseInteractionCallBack,
|
mouseInteractionCb,
|
||||||
doc: Document,
|
doc,
|
||||||
mirror: Mirror,
|
mirror,
|
||||||
blockClass: blockClass,
|
blockClass,
|
||||||
sampling: SamplingStrategy,
|
sampling,
|
||||||
): listenerHandler {
|
}: observerParam): listenerHandler {
|
||||||
if (sampling.mouseInteraction === false) {
|
if (sampling.mouseInteraction === false) {
|
||||||
return () => {};
|
return () => {};
|
||||||
}
|
}
|
||||||
@@ -275,7 +228,7 @@ function initMouseInteractionObserver(
|
|||||||
}
|
}
|
||||||
const id = mirror.getId(target as INode);
|
const id = mirror.getId(target as INode);
|
||||||
const { clientX, clientY } = e;
|
const { clientX, clientY } = e;
|
||||||
cb({
|
mouseInteractionCb({
|
||||||
type: MouseInteractions[eventKey],
|
type: MouseInteractions[eventKey],
|
||||||
id,
|
id,
|
||||||
x: clientX,
|
x: clientX,
|
||||||
@@ -300,13 +253,16 @@ function initMouseInteractionObserver(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initScrollObserver(
|
export function initScrollObserver({
|
||||||
cb: scrollCallback,
|
scrollCb,
|
||||||
doc: Document,
|
doc,
|
||||||
mirror: Mirror,
|
mirror,
|
||||||
blockClass: blockClass,
|
blockClass,
|
||||||
sampling: SamplingStrategy,
|
sampling,
|
||||||
): listenerHandler {
|
}: Pick<
|
||||||
|
observerParam,
|
||||||
|
'scrollCb' | 'doc' | 'mirror' | 'blockClass' | 'sampling'
|
||||||
|
>): listenerHandler {
|
||||||
const updatePosition = throttle<UIEvent>((evt) => {
|
const updatePosition = throttle<UIEvent>((evt) => {
|
||||||
const target = getEventTarget(evt);
|
const target = getEventTarget(evt);
|
||||||
if (!target || isBlocked(target as Node, blockClass)) {
|
if (!target || isBlocked(target as Node, blockClass)) {
|
||||||
@@ -315,13 +271,13 @@ export function initScrollObserver(
|
|||||||
const id = mirror.getId(target as INode);
|
const id = mirror.getId(target as INode);
|
||||||
if (target === doc) {
|
if (target === doc) {
|
||||||
const scrollEl = (doc.scrollingElement || doc.documentElement)!;
|
const scrollEl = (doc.scrollingElement || doc.documentElement)!;
|
||||||
cb({
|
scrollCb({
|
||||||
id,
|
id,
|
||||||
x: scrollEl.scrollLeft,
|
x: scrollEl.scrollLeft,
|
||||||
y: scrollEl.scrollTop,
|
y: scrollEl.scrollTop,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
cb({
|
scrollCb({
|
||||||
id,
|
id,
|
||||||
x: (target as HTMLElement).scrollLeft,
|
x: (target as HTMLElement).scrollLeft,
|
||||||
y: (target as HTMLElement).scrollTop,
|
y: (target as HTMLElement).scrollTop,
|
||||||
@@ -331,16 +287,16 @@ export function initScrollObserver(
|
|||||||
return on('scroll', updatePosition, doc);
|
return on('scroll', updatePosition, doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initViewportResizeObserver(
|
function initViewportResizeObserver({
|
||||||
cb: viewportResizeCallback,
|
viewportResizeCb,
|
||||||
): listenerHandler {
|
}: observerParam): listenerHandler {
|
||||||
let lastH = -1;
|
let lastH = -1;
|
||||||
let lastW = -1;
|
let lastW = -1;
|
||||||
const updateDimension = throttle(() => {
|
const updateDimension = throttle(() => {
|
||||||
const height = getWindowHeight();
|
const height = getWindowHeight();
|
||||||
const width = getWindowWidth();
|
const width = getWindowWidth();
|
||||||
if (lastH !== height || lastW !== width) {
|
if (lastH !== height || lastW !== width) {
|
||||||
cb({
|
viewportResizeCb({
|
||||||
width: Number(width),
|
width: Number(width),
|
||||||
height: Number(height),
|
height: Number(height),
|
||||||
});
|
});
|
||||||
@@ -362,17 +318,17 @@ function wrapEventWithUserTriggeredFlag(
|
|||||||
|
|
||||||
export const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT'];
|
export const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT'];
|
||||||
const lastInputValueMap: WeakMap<EventTarget, inputValue> = new WeakMap();
|
const lastInputValueMap: WeakMap<EventTarget, inputValue> = new WeakMap();
|
||||||
function initInputObserver(
|
function initInputObserver({
|
||||||
cb: inputCallback,
|
inputCb,
|
||||||
doc: Document,
|
doc,
|
||||||
mirror: Mirror,
|
mirror,
|
||||||
blockClass: blockClass,
|
blockClass,
|
||||||
ignoreClass: string,
|
ignoreClass,
|
||||||
maskInputOptions: MaskInputOptions,
|
maskInputOptions,
|
||||||
maskInputFn: MaskInputFn | undefined,
|
maskInputFn,
|
||||||
sampling: SamplingStrategy,
|
sampling,
|
||||||
userTriggeredOnInput: boolean,
|
userTriggeredOnInput,
|
||||||
): listenerHandler {
|
}: observerParam): listenerHandler {
|
||||||
function eventHandler(event: Event) {
|
function eventHandler(event: Event) {
|
||||||
let target = getEventTarget(event);
|
let target = getEventTarget(event);
|
||||||
const userTriggered = event.isTrusted;
|
const userTriggered = event.isTrusted;
|
||||||
@@ -451,7 +407,7 @@ function initInputObserver(
|
|||||||
) {
|
) {
|
||||||
lastInputValueMap.set(target, v);
|
lastInputValueMap.set(target, v);
|
||||||
const id = mirror.getId(target as INode);
|
const id = mirror.getId(target as INode);
|
||||||
cb({
|
inputCb({
|
||||||
...v,
|
...v,
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
@@ -531,9 +487,8 @@ function getNestedCSSRulePositions(rule: CSSRule): number[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initStyleSheetObserver(
|
function initStyleSheetObserver(
|
||||||
cb: styleSheetRuleCallback,
|
{ styleSheetRuleCb, mirror }: observerParam,
|
||||||
win: IWindow,
|
{ win }: { win: IWindow },
|
||||||
mirror: Mirror,
|
|
||||||
): listenerHandler {
|
): listenerHandler {
|
||||||
const insertRule = win.CSSStyleSheet.prototype.insertRule;
|
const insertRule = win.CSSStyleSheet.prototype.insertRule;
|
||||||
win.CSSStyleSheet.prototype.insertRule = function (
|
win.CSSStyleSheet.prototype.insertRule = function (
|
||||||
@@ -542,7 +497,7 @@ function initStyleSheetObserver(
|
|||||||
) {
|
) {
|
||||||
const id = mirror.getId(this.ownerNode as INode);
|
const id = mirror.getId(this.ownerNode as INode);
|
||||||
if (id !== -1) {
|
if (id !== -1) {
|
||||||
cb({
|
styleSheetRuleCb({
|
||||||
id,
|
id,
|
||||||
adds: [{ rule, index }],
|
adds: [{ rule, index }],
|
||||||
});
|
});
|
||||||
@@ -554,7 +509,7 @@ function initStyleSheetObserver(
|
|||||||
win.CSSStyleSheet.prototype.deleteRule = function (index: number) {
|
win.CSSStyleSheet.prototype.deleteRule = function (index: number) {
|
||||||
const id = mirror.getId(this.ownerNode as INode);
|
const id = mirror.getId(this.ownerNode as INode);
|
||||||
if (id !== -1) {
|
if (id !== -1) {
|
||||||
cb({
|
styleSheetRuleCb({
|
||||||
id,
|
id,
|
||||||
removes: [{ index }],
|
removes: [{ index }],
|
||||||
});
|
});
|
||||||
@@ -599,7 +554,7 @@ function initStyleSheetObserver(
|
|||||||
type.prototype.insertRule = function (rule: string, index?: number) {
|
type.prototype.insertRule = function (rule: string, index?: number) {
|
||||||
const id = mirror.getId(this.parentStyleSheet.ownerNode as INode);
|
const id = mirror.getId(this.parentStyleSheet.ownerNode as INode);
|
||||||
if (id !== -1) {
|
if (id !== -1) {
|
||||||
cb({
|
styleSheetRuleCb({
|
||||||
id,
|
id,
|
||||||
adds: [
|
adds: [
|
||||||
{
|
{
|
||||||
@@ -618,7 +573,7 @@ function initStyleSheetObserver(
|
|||||||
type.prototype.deleteRule = function (index: number) {
|
type.prototype.deleteRule = function (index: number) {
|
||||||
const id = mirror.getId(this.parentStyleSheet.ownerNode as INode);
|
const id = mirror.getId(this.parentStyleSheet.ownerNode as INode);
|
||||||
if (id !== -1) {
|
if (id !== -1) {
|
||||||
cb({
|
styleSheetRuleCb({
|
||||||
id,
|
id,
|
||||||
removes: [{ index: [...getNestedCSSRulePositions(this), index] }],
|
removes: [{ index: [...getNestedCSSRulePositions(this), index] }],
|
||||||
});
|
});
|
||||||
@@ -638,9 +593,8 @@ function initStyleSheetObserver(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initStyleDeclarationObserver(
|
function initStyleDeclarationObserver(
|
||||||
cb: styleDeclarationCallback,
|
{ styleDeclarationCb, mirror }: observerParam,
|
||||||
win: IWindow,
|
{ win }: { win: IWindow },
|
||||||
mirror: Mirror,
|
|
||||||
): listenerHandler {
|
): listenerHandler {
|
||||||
const setProperty = win.CSSStyleDeclaration.prototype.setProperty;
|
const setProperty = win.CSSStyleDeclaration.prototype.setProperty;
|
||||||
win.CSSStyleDeclaration.prototype.setProperty = function (
|
win.CSSStyleDeclaration.prototype.setProperty = function (
|
||||||
@@ -653,7 +607,7 @@ function initStyleDeclarationObserver(
|
|||||||
(this.parentRule?.parentStyleSheet?.ownerNode as unknown) as INode,
|
(this.parentRule?.parentStyleSheet?.ownerNode as unknown) as INode,
|
||||||
);
|
);
|
||||||
if (id !== -1) {
|
if (id !== -1) {
|
||||||
cb({
|
styleDeclarationCb({
|
||||||
id,
|
id,
|
||||||
set: {
|
set: {
|
||||||
property,
|
property,
|
||||||
@@ -675,7 +629,7 @@ function initStyleDeclarationObserver(
|
|||||||
(this.parentRule?.parentStyleSheet?.ownerNode as unknown) as INode,
|
(this.parentRule?.parentStyleSheet?.ownerNode as unknown) as INode,
|
||||||
);
|
);
|
||||||
if (id !== -1) {
|
if (id !== -1) {
|
||||||
cb({
|
styleDeclarationCb({
|
||||||
id,
|
id,
|
||||||
remove: {
|
remove: {
|
||||||
property,
|
property,
|
||||||
@@ -692,12 +646,12 @@ function initStyleDeclarationObserver(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function initMediaInteractionObserver(
|
function initMediaInteractionObserver({
|
||||||
mediaInteractionCb: mediaInteractionCallback,
|
mediaInteractionCb,
|
||||||
blockClass: blockClass,
|
blockClass,
|
||||||
mirror: Mirror,
|
mirror,
|
||||||
sampling: SamplingStrategy,
|
sampling,
|
||||||
): listenerHandler {
|
}: observerParam): listenerHandler {
|
||||||
const handler = (type: MediaInteractions) =>
|
const handler = (type: MediaInteractions) =>
|
||||||
throttle((event: Event) => {
|
throttle((event: Event) => {
|
||||||
const target = getEventTarget(event);
|
const target = getEventTarget(event);
|
||||||
@@ -724,7 +678,7 @@ function initMediaInteractionObserver(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function initFontObserver(cb: fontCallback, doc: Document): listenerHandler {
|
function initFontObserver({ fontCb, doc }: observerParam): listenerHandler {
|
||||||
const win = doc.defaultView as IWindow;
|
const win = doc.defaultView as IWindow;
|
||||||
if (!win) {
|
if (!win) {
|
||||||
return () => {};
|
return () => {};
|
||||||
@@ -759,7 +713,7 @@ function initFontObserver(cb: fontCallback, doc: Document): listenerHandler {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const p = fontMap.get(fontFace);
|
const p = fontMap.get(fontFace);
|
||||||
if (p) {
|
if (p) {
|
||||||
cb(p);
|
fontCb(p);
|
||||||
fontMap.delete(fontFace);
|
fontMap.delete(fontFace);
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
@@ -869,78 +823,19 @@ export function initObservers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
mergeHooks(o, hooks);
|
mergeHooks(o, hooks);
|
||||||
const mutationObserver = initMutationObserver(
|
const mutationObserver = initMutationObserver(o, o.doc);
|
||||||
o.mutationCb,
|
const mousemoveHandler = initMoveObserver(o);
|
||||||
o.doc,
|
const mouseInteractionHandler = initMouseInteractionObserver(o);
|
||||||
o.blockClass,
|
const scrollHandler = initScrollObserver(o);
|
||||||
o.blockSelector,
|
const viewportResizeHandler = initViewportResizeObserver(o);
|
||||||
o.maskTextClass,
|
const inputHandler = initInputObserver(o);
|
||||||
o.maskTextSelector,
|
const mediaInteractionHandler = initMediaInteractionObserver(o);
|
||||||
o.inlineStylesheet,
|
|
||||||
o.maskInputOptions,
|
|
||||||
o.maskTextFn,
|
|
||||||
o.maskInputFn,
|
|
||||||
o.recordCanvas,
|
|
||||||
o.inlineImages,
|
|
||||||
o.slimDOMOptions,
|
|
||||||
o.mirror,
|
|
||||||
o.iframeManager,
|
|
||||||
o.shadowDomManager,
|
|
||||||
o.canvasManager,
|
|
||||||
o.doc,
|
|
||||||
);
|
|
||||||
const mousemoveHandler = initMoveObserver(
|
|
||||||
o.mousemoveCb,
|
|
||||||
o.sampling,
|
|
||||||
o.doc,
|
|
||||||
o.mirror,
|
|
||||||
);
|
|
||||||
const mouseInteractionHandler = initMouseInteractionObserver(
|
|
||||||
o.mouseInteractionCb,
|
|
||||||
o.doc,
|
|
||||||
o.mirror,
|
|
||||||
o.blockClass,
|
|
||||||
o.sampling,
|
|
||||||
);
|
|
||||||
const scrollHandler = initScrollObserver(
|
|
||||||
o.scrollCb,
|
|
||||||
o.doc,
|
|
||||||
o.mirror,
|
|
||||||
o.blockClass,
|
|
||||||
o.sampling,
|
|
||||||
);
|
|
||||||
const viewportResizeHandler = initViewportResizeObserver(o.viewportResizeCb);
|
|
||||||
const inputHandler = initInputObserver(
|
|
||||||
o.inputCb,
|
|
||||||
o.doc,
|
|
||||||
o.mirror,
|
|
||||||
o.blockClass,
|
|
||||||
o.ignoreClass,
|
|
||||||
o.maskInputOptions,
|
|
||||||
o.maskInputFn,
|
|
||||||
o.sampling,
|
|
||||||
o.userTriggeredOnInput,
|
|
||||||
);
|
|
||||||
const mediaInteractionHandler = initMediaInteractionObserver(
|
|
||||||
o.mediaInteractionCb,
|
|
||||||
o.blockClass,
|
|
||||||
o.mirror,
|
|
||||||
o.sampling,
|
|
||||||
);
|
|
||||||
|
|
||||||
const styleSheetObserver = initStyleSheetObserver(
|
const styleSheetObserver = initStyleSheetObserver(o, { win: currentWindow });
|
||||||
o.styleSheetRuleCb,
|
const styleDeclarationObserver = initStyleDeclarationObserver(o, {
|
||||||
currentWindow,
|
win: currentWindow,
|
||||||
o.mirror,
|
});
|
||||||
);
|
const fontObserver = o.collectFonts ? initFontObserver(o) : () => {};
|
||||||
const styleDeclarationObserver = initStyleDeclarationObserver(
|
|
||||||
o.styleDeclarationCb,
|
|
||||||
currentWindow,
|
|
||||||
o.mirror,
|
|
||||||
);
|
|
||||||
const fontObserver = o.collectFonts
|
|
||||||
? initFontObserver(o.fontCb, o.doc)
|
|
||||||
: () => {};
|
|
||||||
// plugins
|
// plugins
|
||||||
const pluginHandlers: listenerHandler[] = [];
|
const pluginHandlers: listenerHandler[] = [];
|
||||||
for (const plugin of o.plugins) {
|
for (const plugin of o.plugins) {
|
||||||
|
|||||||
@@ -1,36 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
mutationCallBack,
|
mutationCallBack,
|
||||||
blockClass,
|
|
||||||
maskTextClass,
|
|
||||||
Mirror,
|
Mirror,
|
||||||
scrollCallback,
|
scrollCallback,
|
||||||
|
MutationBufferParam,
|
||||||
SamplingStrategy,
|
SamplingStrategy,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import {
|
|
||||||
MaskInputOptions,
|
|
||||||
SlimDOMOptions,
|
|
||||||
MaskTextFn,
|
|
||||||
MaskInputFn,
|
|
||||||
} from 'rrweb-snapshot';
|
|
||||||
import { IframeManager } from './iframe-manager';
|
|
||||||
import { initMutationObserver, initScrollObserver } from './observer';
|
import { initMutationObserver, initScrollObserver } from './observer';
|
||||||
import { CanvasManager } from './observers/canvas/canvas-manager';
|
|
||||||
|
|
||||||
type BypassOptions = {
|
type BypassOptions = Omit<
|
||||||
blockClass: blockClass;
|
MutationBufferParam,
|
||||||
blockSelector: string | null;
|
'doc' | 'mutationCb' | 'mirror' | 'shadowDomManager'
|
||||||
maskTextClass: maskTextClass;
|
> & {
|
||||||
maskTextSelector: string | null;
|
|
||||||
inlineStylesheet: boolean;
|
|
||||||
maskInputOptions: MaskInputOptions;
|
|
||||||
maskTextFn: MaskTextFn | undefined;
|
|
||||||
maskInputFn: MaskInputFn | undefined;
|
|
||||||
recordCanvas: boolean;
|
|
||||||
inlineImages: boolean;
|
|
||||||
sampling: SamplingStrategy;
|
sampling: SamplingStrategy;
|
||||||
slimDOMOptions: SlimDOMOptions;
|
|
||||||
iframeManager: IframeManager;
|
|
||||||
canvasManager: CanvasManager;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ShadowDomManager {
|
export class ShadowDomManager {
|
||||||
@@ -53,33 +34,22 @@ export class ShadowDomManager {
|
|||||||
|
|
||||||
public addShadowRoot(shadowRoot: ShadowRoot, doc: Document) {
|
public addShadowRoot(shadowRoot: ShadowRoot, doc: Document) {
|
||||||
initMutationObserver(
|
initMutationObserver(
|
||||||
this.mutationCb,
|
{
|
||||||
doc,
|
...this.bypassOptions,
|
||||||
this.bypassOptions.blockClass,
|
doc,
|
||||||
this.bypassOptions.blockSelector,
|
mutationCb: this.mutationCb,
|
||||||
this.bypassOptions.maskTextClass,
|
mirror: this.mirror,
|
||||||
this.bypassOptions.maskTextSelector,
|
shadowDomManager: this,
|
||||||
this.bypassOptions.inlineStylesheet,
|
},
|
||||||
this.bypassOptions.maskInputOptions,
|
|
||||||
this.bypassOptions.maskTextFn,
|
|
||||||
this.bypassOptions.maskInputFn,
|
|
||||||
this.bypassOptions.recordCanvas,
|
|
||||||
this.bypassOptions.inlineImages,
|
|
||||||
this.bypassOptions.slimDOMOptions,
|
|
||||||
this.mirror,
|
|
||||||
this.bypassOptions.iframeManager,
|
|
||||||
this,
|
|
||||||
this.bypassOptions.canvasManager,
|
|
||||||
shadowRoot,
|
shadowRoot,
|
||||||
);
|
);
|
||||||
initScrollObserver(
|
initScrollObserver({
|
||||||
this.scrollCb,
|
...this.bypassOptions,
|
||||||
|
scrollCb: this.scrollCb,
|
||||||
// https://gist.github.com/praveenpuglia/0832da687ed5a5d7a0907046c9ef1813
|
// https://gist.github.com/praveenpuglia/0832da687ed5a5d7a0907046c9ef1813
|
||||||
// scroll is not allowed to pass the boundary, so we need to listen the shadow document
|
// scroll is not allowed to pass the boundary, so we need to listen the shadow document
|
||||||
(shadowRoot as unknown) as Document,
|
doc: (shadowRoot as unknown) as Document,
|
||||||
this.mirror,
|
mirror: this.mirror,
|
||||||
this.bypassOptions.blockClass,
|
});
|
||||||
this.bypassOptions.sampling,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,6 +277,27 @@ export type observerParam = {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MutationBufferParam = Pick<
|
||||||
|
observerParam,
|
||||||
|
| 'mutationCb'
|
||||||
|
| 'blockClass'
|
||||||
|
| 'blockSelector'
|
||||||
|
| 'maskTextClass'
|
||||||
|
| 'maskTextSelector'
|
||||||
|
| 'inlineStylesheet'
|
||||||
|
| 'maskInputOptions'
|
||||||
|
| 'maskTextFn'
|
||||||
|
| 'maskInputFn'
|
||||||
|
| 'recordCanvas'
|
||||||
|
| 'inlineImages'
|
||||||
|
| 'slimDOMOptions'
|
||||||
|
| 'doc'
|
||||||
|
| 'mirror'
|
||||||
|
| 'iframeManager'
|
||||||
|
| 'shadowDomManager'
|
||||||
|
| 'canvasManager'
|
||||||
|
>;
|
||||||
|
|
||||||
export type hooksParam = {
|
export type hooksParam = {
|
||||||
mutation?: mutationCallBack;
|
mutation?: mutationCallBack;
|
||||||
mousemove?: mousemoveCallBack;
|
mousemove?: mousemoveCallBack;
|
||||||
@@ -406,7 +427,7 @@ export type SerializedWebGlArg =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
rr_type: string;
|
rr_type: string;
|
||||||
args: Array<SerializedWebGlArg>;
|
args: SerializedWebGlArg[];
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
rr_type: string;
|
rr_type: string;
|
||||||
|
|||||||
@@ -21,7 +21,10 @@
|
|||||||
"no-empty": false,
|
"no-empty": false,
|
||||||
"max-classes-per-file": false,
|
"max-classes-per-file": false,
|
||||||
"semicolon": false,
|
"semicolon": false,
|
||||||
"trailing-comma": false
|
"trailing-comma": false,
|
||||||
|
"curly": false,
|
||||||
|
"no-namespace": false,
|
||||||
|
"interface-name": false
|
||||||
},
|
},
|
||||||
"rulesDirectory": []
|
"rulesDirectory": []
|
||||||
}
|
}
|
||||||
|
|||||||
10
packages/rrweb/typings/record/mutation.d.ts
vendored
10
packages/rrweb/typings/record/mutation.d.ts
vendored
@@ -1,8 +1,4 @@
|
|||||||
import { MaskInputOptions, SlimDOMOptions, MaskTextFn, MaskInputFn } from 'rrweb-snapshot';
|
import { mutationRecord, MutationBufferParam } from '../types';
|
||||||
import { mutationRecord, blockClass, maskTextClass, mutationCallBack, Mirror } from '../types';
|
|
||||||
import { IframeManager } from './iframe-manager';
|
|
||||||
import { CanvasManager } from './observers/canvas/canvas-manager';
|
|
||||||
import { ShadowDomManager } from './shadow-dom-manager';
|
|
||||||
export default class MutationBuffer {
|
export default class MutationBuffer {
|
||||||
private frozen;
|
private frozen;
|
||||||
private locked;
|
private locked;
|
||||||
@@ -14,7 +10,7 @@ export default class MutationBuffer {
|
|||||||
private addedSet;
|
private addedSet;
|
||||||
private movedSet;
|
private movedSet;
|
||||||
private droppedSet;
|
private droppedSet;
|
||||||
private emissionCallback;
|
private mutationCb;
|
||||||
private blockClass;
|
private blockClass;
|
||||||
private blockSelector;
|
private blockSelector;
|
||||||
private maskTextClass;
|
private maskTextClass;
|
||||||
@@ -31,7 +27,7 @@ export default class MutationBuffer {
|
|||||||
private iframeManager;
|
private iframeManager;
|
||||||
private shadowDomManager;
|
private shadowDomManager;
|
||||||
private canvasManager;
|
private canvasManager;
|
||||||
init(cb: mutationCallBack, blockClass: blockClass, blockSelector: string | null, maskTextClass: maskTextClass, maskTextSelector: string | null, inlineStylesheet: boolean, maskInputOptions: MaskInputOptions, maskTextFn: MaskTextFn | undefined, maskInputFn: MaskInputFn | undefined, recordCanvas: boolean, inlineImages: boolean, slimDOMOptions: SlimDOMOptions, doc: Document, mirror: Mirror, iframeManager: IframeManager, shadowDomManager: ShadowDomManager, canvasManager: CanvasManager): void;
|
init(options: MutationBufferParam): void;
|
||||||
freeze(): void;
|
freeze(): void;
|
||||||
unfreeze(): void;
|
unfreeze(): void;
|
||||||
isFrozen(): boolean;
|
isFrozen(): boolean;
|
||||||
|
|||||||
10
packages/rrweb/typings/record/observer.d.ts
vendored
10
packages/rrweb/typings/record/observer.d.ts
vendored
@@ -1,11 +1,7 @@
|
|||||||
import { MaskInputOptions, SlimDOMOptions, MaskInputFn, MaskTextFn } from 'rrweb-snapshot';
|
import { observerParam, listenerHandler, hooksParam, MutationBufferParam } from '../types';
|
||||||
import { mutationCallBack, observerParam, listenerHandler, scrollCallback, blockClass, maskTextClass, hooksParam, SamplingStrategy, Mirror } from '../types';
|
|
||||||
import MutationBuffer from './mutation';
|
import MutationBuffer from './mutation';
|
||||||
import { IframeManager } from './iframe-manager';
|
|
||||||
import { ShadowDomManager } from './shadow-dom-manager';
|
|
||||||
import { CanvasManager } from './observers/canvas/canvas-manager';
|
|
||||||
export declare const mutationBuffers: MutationBuffer[];
|
export declare const mutationBuffers: MutationBuffer[];
|
||||||
export declare function initMutationObserver(cb: mutationCallBack, doc: Document, blockClass: blockClass, blockSelector: string | null, maskTextClass: maskTextClass, maskTextSelector: string | null, inlineStylesheet: boolean, maskInputOptions: MaskInputOptions, maskTextFn: MaskTextFn | undefined, maskInputFn: MaskInputFn | undefined, recordCanvas: boolean, inlineImages: boolean, slimDOMOptions: SlimDOMOptions, mirror: Mirror, iframeManager: IframeManager, shadowDomManager: ShadowDomManager, canvasManager: CanvasManager, rootEl: Node): MutationObserver;
|
export declare function initMutationObserver(options: MutationBufferParam, rootEl: Node): MutationObserver;
|
||||||
export declare function initScrollObserver(cb: scrollCallback, doc: Document, mirror: Mirror, blockClass: blockClass, sampling: SamplingStrategy): listenerHandler;
|
export declare function initScrollObserver({ scrollCb, doc, mirror, blockClass, sampling, }: Pick<observerParam, 'scrollCb' | 'doc' | 'mirror' | 'blockClass' | 'sampling'>): listenerHandler;
|
||||||
export declare const INPUT_TAGS: string[];
|
export declare const INPUT_TAGS: string[];
|
||||||
export declare function initObservers(o: observerParam, hooks?: hooksParam): listenerHandler;
|
export declare function initObservers(o: observerParam, hooks?: hooksParam): listenerHandler;
|
||||||
|
|||||||
@@ -1,37 +1,32 @@
|
|||||||
import {
|
import { blockClass, canvasMutationCallback, IWindow, Mirror } from '../../../types';
|
||||||
blockClass,
|
|
||||||
canvasMutationCallback,
|
|
||||||
IWindow,
|
|
||||||
Mirror,
|
|
||||||
} from '../../../types';
|
|
||||||
export declare type RafStamps = {
|
export declare type RafStamps = {
|
||||||
latestId: number;
|
latestId: number;
|
||||||
invokeId: number | null;
|
invokeId: number | null;
|
||||||
};
|
};
|
||||||
export declare class CanvasManager {
|
export declare class CanvasManager {
|
||||||
private pendingCanvasMutations;
|
private pendingCanvasMutations;
|
||||||
private rafStamps;
|
private rafStamps;
|
||||||
private mirror;
|
private mirror;
|
||||||
private mutationCb;
|
private mutationCb;
|
||||||
private resetObservers;
|
private resetObservers;
|
||||||
private frozen;
|
private frozen;
|
||||||
private locked;
|
private locked;
|
||||||
reset(): void;
|
reset(): void;
|
||||||
freeze(): void;
|
freeze(): void;
|
||||||
unfreeze(): void;
|
unfreeze(): void;
|
||||||
lock(): void;
|
lock(): void;
|
||||||
unlock(): void;
|
unlock(): void;
|
||||||
constructor(options: {
|
constructor(options: {
|
||||||
recordCanvas: boolean | number;
|
recordCanvas: boolean | number;
|
||||||
mutationCb: canvasMutationCallback;
|
mutationCb: canvasMutationCallback;
|
||||||
win: IWindow;
|
win: IWindow;
|
||||||
blockClass: blockClass;
|
blockClass: blockClass;
|
||||||
mirror: Mirror;
|
mirror: Mirror;
|
||||||
});
|
});
|
||||||
private processMutation;
|
private processMutation;
|
||||||
private initCanvasMutationObserver;
|
private initCanvasMutationObserver;
|
||||||
private startPendingCanvasMutationFlusher;
|
private startPendingCanvasMutationFlusher;
|
||||||
private startRAFTimestamping;
|
private startRAFTimestamping;
|
||||||
flushPendingCanvasMutations(): void;
|
flushPendingCanvasMutations(): void;
|
||||||
flushPendingCanvasMutationFor(canvas: HTMLCanvasElement, id: number): void;
|
flushPendingCanvasMutationFor(canvas: HTMLCanvasElement, id: number): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,6 @@
|
|||||||
import { mutationCallBack, blockClass, maskTextClass, Mirror, scrollCallback, SamplingStrategy } from '../types';
|
import { mutationCallBack, Mirror, scrollCallback, MutationBufferParam, SamplingStrategy } from '../types';
|
||||||
import { MaskInputOptions, SlimDOMOptions, MaskTextFn, MaskInputFn } from 'rrweb-snapshot';
|
declare type BypassOptions = Omit<MutationBufferParam, 'doc' | 'mutationCb' | 'mirror' | 'shadowDomManager'> & {
|
||||||
import { IframeManager } from './iframe-manager';
|
|
||||||
import { CanvasManager } from './observers/canvas/canvas-manager';
|
|
||||||
declare type BypassOptions = {
|
|
||||||
blockClass: blockClass;
|
|
||||||
blockSelector: string | null;
|
|
||||||
maskTextClass: maskTextClass;
|
|
||||||
maskTextSelector: string | null;
|
|
||||||
inlineStylesheet: boolean;
|
|
||||||
maskInputOptions: MaskInputOptions;
|
|
||||||
maskTextFn: MaskTextFn | undefined;
|
|
||||||
maskInputFn: MaskInputFn | undefined;
|
|
||||||
recordCanvas: boolean;
|
|
||||||
inlineImages: boolean;
|
|
||||||
sampling: SamplingStrategy;
|
sampling: SamplingStrategy;
|
||||||
slimDOMOptions: SlimDOMOptions;
|
|
||||||
iframeManager: IframeManager;
|
|
||||||
canvasManager: CanvasManager;
|
|
||||||
};
|
};
|
||||||
export declare class ShadowDomManager {
|
export declare class ShadowDomManager {
|
||||||
private mutationCb;
|
private mutationCb;
|
||||||
|
|||||||
3
packages/rrweb/typings/types.d.ts
vendored
3
packages/rrweb/typings/types.d.ts
vendored
@@ -196,6 +196,7 @@ export declare type observerParam = {
|
|||||||
options: unknown;
|
options: unknown;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
export declare type MutationBufferParam = Pick<observerParam, 'mutationCb' | 'blockClass' | 'blockSelector' | 'maskTextClass' | 'maskTextSelector' | 'inlineStylesheet' | 'maskInputOptions' | 'maskTextFn' | 'maskInputFn' | 'recordCanvas' | 'inlineImages' | 'slimDOMOptions' | 'doc' | 'mirror' | 'iframeManager' | 'shadowDomManager' | 'canvasManager'>;
|
||||||
export declare type hooksParam = {
|
export declare type hooksParam = {
|
||||||
mutation?: mutationCallBack;
|
mutation?: mutationCallBack;
|
||||||
mousemove?: mousemoveCallBack;
|
mousemove?: mousemoveCallBack;
|
||||||
@@ -299,7 +300,7 @@ export declare type SerializedWebGlArg = {
|
|||||||
src: string;
|
src: string;
|
||||||
} | {
|
} | {
|
||||||
rr_type: string;
|
rr_type: string;
|
||||||
args: Array<SerializedWebGlArg>;
|
args: SerializedWebGlArg[];
|
||||||
} | {
|
} | {
|
||||||
rr_type: string;
|
rr_type: string;
|
||||||
index: number;
|
index: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user