Move mutation processing into it's own class (#223)
* Move mutation processing into it's own object. This should stand on it's own as a refactor, but is intended as a basis for exposing the new MutationBuffer object to further outside control e.g. to 'mute' or batch up mutation emission when the page becomes inactive from a https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API point of view * The `processMutations` function needed to be bound to the `mutationBuffer` object, as otherwise `this` referred to the `MutationObserver` object itself * Neglected to add this output of `npm run typings` * Get around the binding problem by using Arrow function expressions * Prettier formatting
This commit is contained in:
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* Some utils to handle the mutation observer DOM records.
|
||||
* It should be more clear to extend the native data structure
|
||||
* like Set and Map, but currently Typescript does not support
|
||||
* that.
|
||||
*/
|
||||
|
||||
import { INode } from 'rrweb-snapshot';
|
||||
import { removedNodeMutation } from '../types';
|
||||
import { mirror } from '../utils';
|
||||
|
||||
export function deepDelete(addsSet: Set<Node>, n: Node) {
|
||||
addsSet.delete(n);
|
||||
n.childNodes.forEach(childN => deepDelete(addsSet, childN));
|
||||
}
|
||||
|
||||
export function isParentRemoved(
|
||||
removes: removedNodeMutation[],
|
||||
n: Node,
|
||||
): boolean {
|
||||
const { parentNode } = n;
|
||||
if (!parentNode) {
|
||||
return false;
|
||||
}
|
||||
const parentId = mirror.getId((parentNode as Node) as INode);
|
||||
if (removes.some(r => r.id === parentId)) {
|
||||
return true;
|
||||
}
|
||||
return isParentRemoved(removes, parentNode);
|
||||
}
|
||||
|
||||
export function isAncestorInSet(set: Set<Node>, n: Node): boolean {
|
||||
const { parentNode } = n;
|
||||
if (!parentNode) {
|
||||
return false;
|
||||
}
|
||||
if (set.has(parentNode)) {
|
||||
return true;
|
||||
}
|
||||
return isAncestorInSet(set, parentNode);
|
||||
}
|
||||
309
src/record/mutation.ts
Normal file
309
src/record/mutation.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import { INode, serializeNodeWithId, transformAttribute } from 'rrweb-snapshot';
|
||||
import {
|
||||
mutationRecord,
|
||||
blockClass,
|
||||
mutationCallBack,
|
||||
textCursor,
|
||||
attributeCursor,
|
||||
removedNodeMutation,
|
||||
addedNodeMutation,
|
||||
} from '../types';
|
||||
import { mirror, isBlocked, isAncestorRemoved } from '../utils';
|
||||
|
||||
const moveKey = (id: number, parentId: number) => `${id}@${parentId}`;
|
||||
function isINode(n: Node | INode): n is INode {
|
||||
return '__sn' in n;
|
||||
}
|
||||
|
||||
/**
|
||||
* controls behaviour of a MutationObserver
|
||||
*/
|
||||
export default class MutationBuffer {
|
||||
private texts: textCursor[] = [];
|
||||
private attributes: attributeCursor[] = [];
|
||||
private removes: removedNodeMutation[] = [];
|
||||
private adds: addedNodeMutation[] = [];
|
||||
|
||||
private movedMap: Record<string, true> = {};
|
||||
|
||||
/**
|
||||
* the browser MutationObserver emits multiple mutations after
|
||||
* a delay for performance reasons, making tracing added nodes hard
|
||||
* in our `processMutations` callback function.
|
||||
* For example, if we append an element el_1 into body, and then append
|
||||
* another element el_2 into el_1, these two mutations may be passed to the
|
||||
* callback function together when the two operations were done.
|
||||
* Generally we need to trace child nodes of newly added nodes, but in this
|
||||
* case if we count el_2 as el_1's child node in the first mutation record,
|
||||
* then we will count el_2 again in the second mutation record which was
|
||||
* duplicated.
|
||||
* To avoid of duplicate counting added nodes, we use a Set to store
|
||||
* added nodes and its child nodes during iterate mutation records. Then
|
||||
* collect added nodes from the Set which have no duplicate copy. But
|
||||
* this also causes newly added nodes will not be serialized with id ASAP,
|
||||
* which means all the id related calculation should be lazy too.
|
||||
*/
|
||||
private addedSet = new Set<Node>();
|
||||
private movedSet = new Set<Node>();
|
||||
private droppedSet = new Set<Node>();
|
||||
|
||||
private emissionCallback: mutationCallBack;
|
||||
private blockClass: blockClass;
|
||||
private inlineStylesheet: boolean;
|
||||
private maskAllInputs: boolean;
|
||||
|
||||
constructor(
|
||||
cb: mutationCallBack,
|
||||
blockClass: blockClass,
|
||||
inlineStylesheet: boolean,
|
||||
maskAllInputs: boolean,
|
||||
) {
|
||||
this.blockClass = blockClass;
|
||||
this.inlineStylesheet = inlineStylesheet;
|
||||
this.maskAllInputs = maskAllInputs;
|
||||
this.emissionCallback = cb;
|
||||
}
|
||||
|
||||
public processMutations = (mutations: mutationRecord[]) => {
|
||||
mutations.forEach(this.processMutation);
|
||||
|
||||
/**
|
||||
* Sometimes child node may be pushed before its newly added
|
||||
* parent, so we init a queue to store these nodes.
|
||||
*/
|
||||
const addQueue: Node[] = [];
|
||||
const pushAdd = (n: Node) => {
|
||||
const parentId = mirror.getId((n.parentNode as Node) as INode);
|
||||
const nextId =
|
||||
n.nextSibling && mirror.getId((n.nextSibling as unknown) as INode);
|
||||
if (parentId === -1 || nextId === -1) {
|
||||
return addQueue.push(n);
|
||||
}
|
||||
this.adds.push({
|
||||
parentId,
|
||||
nextId,
|
||||
node: serializeNodeWithId(
|
||||
n,
|
||||
document,
|
||||
mirror.map,
|
||||
this.blockClass,
|
||||
true,
|
||||
this.inlineStylesheet,
|
||||
this.maskAllInputs,
|
||||
)!,
|
||||
});
|
||||
};
|
||||
|
||||
for (const n of this.movedSet) {
|
||||
pushAdd(n);
|
||||
}
|
||||
|
||||
for (const n of this.addedSet) {
|
||||
if (
|
||||
!isAncestorInSet(this.droppedSet, n) &&
|
||||
!isParentRemoved(this.removes, n)
|
||||
) {
|
||||
pushAdd(n);
|
||||
} else if (isAncestorInSet(this.movedSet, n)) {
|
||||
pushAdd(n);
|
||||
} else {
|
||||
this.droppedSet.add(n);
|
||||
}
|
||||
}
|
||||
|
||||
while (addQueue.length) {
|
||||
if (
|
||||
addQueue.every(
|
||||
(n) => mirror.getId((n.parentNode as Node) as INode) === -1,
|
||||
)
|
||||
) {
|
||||
/**
|
||||
* If all nodes in queue could not find a serialized parent,
|
||||
* it may be a bug or corner case. We need to escape the
|
||||
* dead while loop at once.
|
||||
*/
|
||||
break;
|
||||
}
|
||||
pushAdd(addQueue.shift()!);
|
||||
}
|
||||
|
||||
this.emit();
|
||||
};
|
||||
|
||||
private processMutation = (m: mutationRecord) => {
|
||||
switch (m.type) {
|
||||
case 'characterData': {
|
||||
const value = m.target.textContent;
|
||||
if (!isBlocked(m.target, this.blockClass) && value !== m.oldValue) {
|
||||
this.texts.push({
|
||||
value,
|
||||
node: m.target,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'attributes': {
|
||||
const value = (m.target as HTMLElement).getAttribute(m.attributeName!);
|
||||
if (isBlocked(m.target, this.blockClass) || value === m.oldValue) {
|
||||
return;
|
||||
}
|
||||
let item: attributeCursor | undefined = this.attributes.find(
|
||||
(a) => a.node === m.target,
|
||||
);
|
||||
if (!item) {
|
||||
item = {
|
||||
node: m.target,
|
||||
attributes: {},
|
||||
};
|
||||
this.attributes.push(item);
|
||||
}
|
||||
// overwrite attribute if the mutations was triggered in same time
|
||||
item.attributes[m.attributeName!] = transformAttribute(
|
||||
document,
|
||||
m.attributeName!,
|
||||
value!,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'childList': {
|
||||
m.addedNodes.forEach((n) => this.genAdds(n, m.target));
|
||||
m.removedNodes.forEach((n) => {
|
||||
const nodeId = mirror.getId(n as INode);
|
||||
const parentId = mirror.getId(m.target as INode);
|
||||
if (isBlocked(n, this.blockClass)) {
|
||||
return;
|
||||
}
|
||||
// removed node has not been serialized yet, just remove it from the Set
|
||||
if (this.addedSet.has(n)) {
|
||||
deepDelete(this.addedSet, n);
|
||||
this.droppedSet.add(n);
|
||||
} else if (this.addedSet.has(m.target) && nodeId === -1) {
|
||||
/**
|
||||
* If target was newly added and removed child node was
|
||||
* not serialized, it means the child node has been removed
|
||||
* before callback fired, so we can ignore it because
|
||||
* newly added node will be serialized without child nodes.
|
||||
* TODO: verify this
|
||||
*/
|
||||
} else if (isAncestorRemoved(m.target as INode)) {
|
||||
/**
|
||||
* If parent id was not in the mirror map any more, it
|
||||
* means the parent node has already been removed. So
|
||||
* the node is also removed which we do not need to track
|
||||
* and replay.
|
||||
*/
|
||||
} else if (
|
||||
this.movedSet.has(n) &&
|
||||
this.movedMap[moveKey(nodeId, parentId)]
|
||||
) {
|
||||
deepDelete(this.movedSet, n);
|
||||
} else {
|
||||
this.removes.push({
|
||||
parentId,
|
||||
id: nodeId,
|
||||
});
|
||||
}
|
||||
mirror.removeNodeFromMap(n as INode);
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private genAdds = (n: Node | INode, target?: Node | INode) => {
|
||||
if (isBlocked(n, this.blockClass)) {
|
||||
return;
|
||||
}
|
||||
if (isINode(n)) {
|
||||
this.movedSet.add(n);
|
||||
let targetId: number | null = null;
|
||||
if (target && isINode(target)) {
|
||||
targetId = target.__sn.id;
|
||||
}
|
||||
if (targetId) {
|
||||
this.movedMap[moveKey(n.__sn.id, targetId)] = true;
|
||||
}
|
||||
} else {
|
||||
this.addedSet.add(n);
|
||||
this.droppedSet.delete(n);
|
||||
}
|
||||
n.childNodes.forEach((childN) => this.genAdds(childN));
|
||||
};
|
||||
|
||||
public emit = () => {
|
||||
const payload = {
|
||||
texts: this.texts
|
||||
.map((text) => ({
|
||||
id: mirror.getId(text.node as INode),
|
||||
value: text.value,
|
||||
}))
|
||||
// text mutation's id was not in the mirror map means the target node has been removed
|
||||
.filter((text) => mirror.has(text.id)),
|
||||
attributes: this.attributes
|
||||
.map((attribute) => ({
|
||||
id: mirror.getId(attribute.node as INode),
|
||||
attributes: attribute.attributes,
|
||||
}))
|
||||
// attribute mutation's id was not in the mirror map means the target node has been removed
|
||||
.filter((attribute) => mirror.has(attribute.id)),
|
||||
removes: this.removes,
|
||||
adds: this.adds,
|
||||
};
|
||||
// payload may be empty if the mutations happened in some blocked elements
|
||||
if (
|
||||
!payload.texts.length &&
|
||||
!payload.attributes.length &&
|
||||
!payload.removes.length &&
|
||||
!payload.adds.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.emissionCallback(payload);
|
||||
|
||||
// reset
|
||||
this.texts = [];
|
||||
this.attributes = [];
|
||||
this.removes = [];
|
||||
this.adds = [];
|
||||
this.addedSet = new Set<Node>();
|
||||
this.movedSet = new Set<Node>();
|
||||
this.droppedSet = new Set<Node>();
|
||||
this.movedMap = {};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Some utils to handle the mutation observer DOM records.
|
||||
* It should be more clear to extend the native data structure
|
||||
* like Set and Map, but currently Typescript does not support
|
||||
* that.
|
||||
*/
|
||||
function deepDelete(addsSet: Set<Node>, n: Node) {
|
||||
addsSet.delete(n);
|
||||
n.childNodes.forEach((childN) => deepDelete(addsSet, childN));
|
||||
}
|
||||
|
||||
function isParentRemoved(removes: removedNodeMutation[], n: Node): boolean {
|
||||
const { parentNode } = n;
|
||||
if (!parentNode) {
|
||||
return false;
|
||||
}
|
||||
const parentId = mirror.getId((parentNode as Node) as INode);
|
||||
if (removes.some((r) => r.id === parentId)) {
|
||||
return true;
|
||||
}
|
||||
return isParentRemoved(removes, parentNode);
|
||||
}
|
||||
|
||||
function isAncestorInSet(set: Set<Node>, n: Node): boolean {
|
||||
const { parentNode } = n;
|
||||
if (!parentNode) {
|
||||
return false;
|
||||
}
|
||||
if (set.has(parentNode)) {
|
||||
return true;
|
||||
}
|
||||
return isAncestorInSet(set, parentNode);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { INode, serializeNodeWithId, transformAttribute } from 'rrweb-snapshot';
|
||||
import { INode } from 'rrweb-snapshot';
|
||||
import {
|
||||
mirror,
|
||||
throttle,
|
||||
@@ -7,13 +7,10 @@ import {
|
||||
getWindowHeight,
|
||||
getWindowWidth,
|
||||
isBlocked,
|
||||
isAncestorRemoved,
|
||||
isTouchEvent,
|
||||
} from '../utils';
|
||||
import {
|
||||
mutationCallBack,
|
||||
removedNodeMutation,
|
||||
addedNodeMutation,
|
||||
observerParam,
|
||||
mousemoveCallBack,
|
||||
mousePosition,
|
||||
@@ -26,8 +23,6 @@ import {
|
||||
inputValue,
|
||||
inputCallback,
|
||||
hookResetter,
|
||||
textCursor,
|
||||
attributeCursor,
|
||||
blockClass,
|
||||
IncrementalSource,
|
||||
hooksParam,
|
||||
@@ -35,241 +30,22 @@ import {
|
||||
mediaInteractionCallback,
|
||||
MediaInteractions,
|
||||
} from '../types';
|
||||
import { deepDelete, isParentRemoved, isAncestorInSet } from './collection';
|
||||
import MutationBuffer from './mutation';
|
||||
|
||||
const moveKey = (id: number, parentId: number) => `${id}@${parentId}`;
|
||||
function isINode(n: Node | INode): n is INode {
|
||||
return '__sn' in n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutation observer will merge several mutations into an array and pass
|
||||
* it to the callback function, this may make tracing added nodes hard.
|
||||
* For example, if we append an element el_1 into body, and then append
|
||||
* another element el_2 into el_1, these two mutations may be passed to the
|
||||
* callback function together when the two operations were done.
|
||||
* Generally we need trace child nodes of newly added node, but in this
|
||||
* case if we count el_2 as el_1's child node in the first mutation record,
|
||||
* then we will count el_2 again in the secoond mutation record which was
|
||||
* duplicated.
|
||||
* To avoid of duplicate counting added nodes, we will use a Set to store
|
||||
* added nodes and its child nodes during iterate mutation records. Then
|
||||
* collect added nodes from the Set which will has no duplicate copy. But
|
||||
* this also cause newly added node will not be serialized with id ASAP,
|
||||
* which means all the id related calculation should be lazy too.
|
||||
* @param cb mutationCallBack
|
||||
*/
|
||||
function initMutationObserver(
|
||||
cb: mutationCallBack,
|
||||
blockClass: blockClass,
|
||||
inlineStylesheet: boolean,
|
||||
maskAllInputs: boolean,
|
||||
): MutationObserver {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
const texts: textCursor[] = [];
|
||||
const attributes: attributeCursor[] = [];
|
||||
let removes: removedNodeMutation[] = [];
|
||||
const adds: addedNodeMutation[] = [];
|
||||
|
||||
const addedSet = new Set<Node>();
|
||||
const movedSet = new Set<Node>();
|
||||
const droppedSet = new Set<Node>();
|
||||
|
||||
const movedMap: Record<string, true> = {};
|
||||
|
||||
const genAdds = (n: Node | INode, target?: Node | INode) => {
|
||||
if (isBlocked(n, blockClass)) {
|
||||
return;
|
||||
}
|
||||
if (isINode(n)) {
|
||||
movedSet.add(n);
|
||||
let targetId: number | null = null;
|
||||
if (target && isINode(target)) {
|
||||
targetId = target.__sn.id;
|
||||
}
|
||||
if (targetId) {
|
||||
movedMap[moveKey(n.__sn.id, targetId)] = true;
|
||||
}
|
||||
} else {
|
||||
addedSet.add(n);
|
||||
droppedSet.delete(n);
|
||||
}
|
||||
n.childNodes.forEach((childN) => genAdds(childN));
|
||||
};
|
||||
|
||||
mutations.forEach((mutation) => {
|
||||
const {
|
||||
type,
|
||||
target,
|
||||
oldValue,
|
||||
addedNodes,
|
||||
removedNodes,
|
||||
attributeName,
|
||||
} = mutation;
|
||||
switch (type) {
|
||||
case 'characterData': {
|
||||
const value = target.textContent;
|
||||
if (!isBlocked(target, blockClass) && value !== oldValue) {
|
||||
texts.push({
|
||||
value,
|
||||
node: target,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'attributes': {
|
||||
const value = (target as HTMLElement).getAttribute(attributeName!);
|
||||
if (isBlocked(target, blockClass) || value === oldValue) {
|
||||
return;
|
||||
}
|
||||
let item: attributeCursor | undefined = attributes.find(
|
||||
(a) => a.node === target,
|
||||
);
|
||||
if (!item) {
|
||||
item = {
|
||||
node: target,
|
||||
attributes: {},
|
||||
};
|
||||
attributes.push(item);
|
||||
}
|
||||
// overwrite attribute if the mutations was triggered in same time
|
||||
item.attributes[attributeName!] = transformAttribute(
|
||||
document,
|
||||
attributeName!,
|
||||
value!,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'childList': {
|
||||
addedNodes.forEach((n) => genAdds(n, target));
|
||||
removedNodes.forEach((n) => {
|
||||
const nodeId = mirror.getId(n as INode);
|
||||
const parentId = mirror.getId(target as INode);
|
||||
if (isBlocked(n, blockClass)) {
|
||||
return;
|
||||
}
|
||||
// removed node has not been serialized yet, just remove it from the Set
|
||||
if (addedSet.has(n)) {
|
||||
deepDelete(addedSet, n);
|
||||
droppedSet.add(n);
|
||||
} else if (addedSet.has(target) && nodeId === -1) {
|
||||
/**
|
||||
* If target was newly added and removed child node was
|
||||
* not serialized, it means the child node has been removed
|
||||
* before callback fired, so we can ignore it because
|
||||
* newly added node will be serialized without child nodes.
|
||||
* TODO: verify this
|
||||
*/
|
||||
} else if (isAncestorRemoved(target as INode)) {
|
||||
/**
|
||||
* If parent id was not in the mirror map any more, it
|
||||
* means the parent node has already been removed. So
|
||||
* the node is also removed which we do not need to track
|
||||
* and replay.
|
||||
*/
|
||||
} else if (movedSet.has(n) && movedMap[moveKey(nodeId, parentId)]) {
|
||||
deepDelete(movedSet, n);
|
||||
} else {
|
||||
removes.push({
|
||||
parentId,
|
||||
id: nodeId,
|
||||
});
|
||||
}
|
||||
mirror.removeNodeFromMap(n as INode);
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Sometimes child node may be pushed before its newly added
|
||||
* parent, so we init a queue to store these nodes.
|
||||
*/
|
||||
const addQueue: Node[] = [];
|
||||
const pushAdd = (n: Node) => {
|
||||
const parentId = mirror.getId((n.parentNode as Node) as INode);
|
||||
const nextId =
|
||||
n.nextSibling && mirror.getId((n.nextSibling as unknown) as INode);
|
||||
if (parentId === -1 || nextId === -1) {
|
||||
return addQueue.push(n);
|
||||
}
|
||||
adds.push({
|
||||
parentId,
|
||||
nextId,
|
||||
node: serializeNodeWithId(
|
||||
n,
|
||||
document,
|
||||
mirror.map,
|
||||
blockClass,
|
||||
true,
|
||||
inlineStylesheet,
|
||||
maskAllInputs,
|
||||
)!,
|
||||
});
|
||||
};
|
||||
|
||||
for (const n of movedSet) {
|
||||
pushAdd(n);
|
||||
}
|
||||
|
||||
for (const n of addedSet) {
|
||||
if (!isAncestorInSet(droppedSet, n) && !isParentRemoved(removes, n)) {
|
||||
pushAdd(n);
|
||||
} else if (isAncestorInSet(movedSet, n)) {
|
||||
pushAdd(n);
|
||||
} else {
|
||||
droppedSet.add(n);
|
||||
}
|
||||
}
|
||||
|
||||
while (addQueue.length) {
|
||||
if (
|
||||
addQueue.every(
|
||||
(n) => mirror.getId((n.parentNode as Node) as INode) === -1,
|
||||
)
|
||||
) {
|
||||
/**
|
||||
* If all nodes in queue could not find a serialized parent,
|
||||
* it may be a bug or corner case. We need to escape the
|
||||
* dead while loop at once.
|
||||
*/
|
||||
break;
|
||||
}
|
||||
pushAdd(addQueue.shift()!);
|
||||
}
|
||||
|
||||
const payload = {
|
||||
texts: texts
|
||||
.map((text) => ({
|
||||
id: mirror.getId(text.node as INode),
|
||||
value: text.value,
|
||||
}))
|
||||
// text mutation's id was not in the mirror map means the target node has been removed
|
||||
.filter((text) => mirror.has(text.id)),
|
||||
attributes: attributes
|
||||
.map((attribute) => ({
|
||||
id: mirror.getId(attribute.node as INode),
|
||||
attributes: attribute.attributes,
|
||||
}))
|
||||
// attribute mutation's id was not in the mirror map means the target node has been removed
|
||||
.filter((attribute) => mirror.has(attribute.id)),
|
||||
removes,
|
||||
adds,
|
||||
};
|
||||
// payload may be empty if the mutations happened in some blocked elements
|
||||
if (
|
||||
!payload.texts.length &&
|
||||
!payload.attributes.length &&
|
||||
!payload.removes.length &&
|
||||
!payload.adds.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
cb(payload);
|
||||
});
|
||||
// see mutation.ts for details
|
||||
const mutationBuffer = new MutationBuffer(
|
||||
cb,
|
||||
blockClass,
|
||||
inlineStylesheet,
|
||||
maskAllInputs,
|
||||
);
|
||||
const observer = new MutationObserver(mutationBuffer.processMutations);
|
||||
observer.observe(document, {
|
||||
attributes: true,
|
||||
attributeOldValue: true,
|
||||
|
||||
10
src/types.ts
10
src/types.ts
@@ -166,6 +166,16 @@ export type hooksParam = {
|
||||
styleSheetRule?: styleSheetRuleCallback;
|
||||
};
|
||||
|
||||
// https://dom.spec.whatwg.org/#interface-mutationrecord
|
||||
export type mutationRecord = {
|
||||
type: string,
|
||||
target: Node,
|
||||
oldValue: string | null,
|
||||
addedNodes: NodeList,
|
||||
removedNodes: NodeList,
|
||||
attributeName: string | null,
|
||||
}
|
||||
|
||||
export type textCursor = {
|
||||
node: Node;
|
||||
value: string | null;
|
||||
|
||||
20
typings/record/mutation.d.ts
vendored
Normal file
20
typings/record/mutation.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
import { mutationRecord, blockClass, mutationCallBack } from '../types';
|
||||
export default class MutationBuffer {
|
||||
private texts;
|
||||
private attributes;
|
||||
private removes;
|
||||
private adds;
|
||||
private movedMap;
|
||||
private addedSet;
|
||||
private movedSet;
|
||||
private droppedSet;
|
||||
private emissionCallback;
|
||||
private blockClass;
|
||||
private inlineStylesheet;
|
||||
private maskAllInputs;
|
||||
constructor(cb: mutationCallBack, blockClass: blockClass, inlineStylesheet: boolean, maskAllInputs: boolean);
|
||||
processMutations(mutations: mutationRecord[]): void;
|
||||
private processMutation;
|
||||
private genAdds;
|
||||
emit(): void;
|
||||
}
|
||||
8
typings/types.d.ts
vendored
8
typings/types.d.ts
vendored
@@ -127,6 +127,14 @@ export declare type hooksParam = {
|
||||
mediaInteaction?: mediaInteractionCallback;
|
||||
styleSheetRule?: styleSheetRuleCallback;
|
||||
};
|
||||
export declare type mutationRecord = {
|
||||
type: string;
|
||||
target: Node;
|
||||
oldValue: string | null;
|
||||
addedNodes: NodeList;
|
||||
removedNodes: NodeList;
|
||||
attributeName: string | null;
|
||||
};
|
||||
export declare type textCursor = {
|
||||
node: Node;
|
||||
value: string | null;
|
||||
|
||||
Reference in New Issue
Block a user