add input event observer and hook the value setter
This commit is contained in:
@@ -92,6 +92,16 @@ function record(options: recordOptions) {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
inputCb: v =>
|
||||||
|
emit(
|
||||||
|
wrapEvent({
|
||||||
|
type: EventType.IncrementalSnapshot,
|
||||||
|
data: {
|
||||||
|
source: IncrementalSource.Input,
|
||||||
|
...v,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { INode, serializeNodeWithId } from 'rrweb-snapshot';
|
import { INode, serializeNodeWithId } from 'rrweb-snapshot';
|
||||||
import { mirror, throttle, on } from '../utils';
|
import { mirror, throttle, on, hookSetter } from '../utils';
|
||||||
import {
|
import {
|
||||||
mutationCallBack,
|
mutationCallBack,
|
||||||
textMutation,
|
textMutation,
|
||||||
@@ -14,6 +14,9 @@ import {
|
|||||||
listenerHandler,
|
listenerHandler,
|
||||||
scrollCallback,
|
scrollCallback,
|
||||||
viewportResizeCallback,
|
viewportResizeCallback,
|
||||||
|
inputValue,
|
||||||
|
inputCallback,
|
||||||
|
hookResetter,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
function initMutationObserver(cb: mutationCallBack): MutationObserver {
|
function initMutationObserver(cb: mutationCallBack): MutationObserver {
|
||||||
@@ -214,6 +217,87 @@ function initViewportResizeObserver(
|
|||||||
return on('resize', updateDimension, window);
|
return on('resize', updateDimension, window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const INPUT_TAGS = ['INPUT', 'TEXTAREA', 'SELECT'];
|
||||||
|
const HOOK_PROPERTIES: Array<[HTMLElement, string]> = [
|
||||||
|
[HTMLInputElement.prototype, 'value'],
|
||||||
|
[HTMLInputElement.prototype, 'checked'],
|
||||||
|
[HTMLSelectElement.prototype, 'value'],
|
||||||
|
[HTMLTextAreaElement.prototype, 'value'],
|
||||||
|
];
|
||||||
|
const lastInputValueMap: WeakMap<EventTarget, inputValue> = new WeakMap();
|
||||||
|
function initInputObserver(cb: inputCallback): listenerHandler {
|
||||||
|
function eventHandler(event: Event) {
|
||||||
|
const { target } = event;
|
||||||
|
if (
|
||||||
|
!target ||
|
||||||
|
!(target as Element).tagName ||
|
||||||
|
INPUT_TAGS.indexOf((target as Element).tagName) < 0
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const type: string | undefined = (target as HTMLInputElement).type;
|
||||||
|
const text = (target as HTMLInputElement).value;
|
||||||
|
let isChecked = false;
|
||||||
|
if (type === 'radio' || type === 'checkbox') {
|
||||||
|
isChecked = (target as HTMLInputElement).checked;
|
||||||
|
}
|
||||||
|
cbWithDedup(target, { text, isChecked });
|
||||||
|
// if a radio was checked
|
||||||
|
// the other radios with the same name attribute will be unchecked.
|
||||||
|
const name: string | undefined = (target as HTMLInputElement).name;
|
||||||
|
if (type === 'radio' && name && isChecked) {
|
||||||
|
document
|
||||||
|
.querySelectorAll(`input[type="radio"][name="${name}"]`)
|
||||||
|
.forEach(el => {
|
||||||
|
if (el !== target) {
|
||||||
|
cbWithDedup(el, {
|
||||||
|
text: (el as HTMLInputElement).value,
|
||||||
|
isChecked: !isChecked,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function cbWithDedup(target: EventTarget, v: inputValue) {
|
||||||
|
const lastInputValue = lastInputValueMap.get(target);
|
||||||
|
if (
|
||||||
|
!lastInputValue ||
|
||||||
|
lastInputValue.text !== v.text ||
|
||||||
|
lastInputValue.isChecked !== v.isChecked
|
||||||
|
) {
|
||||||
|
lastInputValueMap.set(target, v);
|
||||||
|
const id = mirror.getId(target as INode);
|
||||||
|
cb({
|
||||||
|
...v,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handlers: Array<listenerHandler | hookResetter> = [
|
||||||
|
'input',
|
||||||
|
'change',
|
||||||
|
].map(eventName => on(eventName, eventHandler));
|
||||||
|
const propertyDescriptor = Object.getOwnPropertyDescriptor(
|
||||||
|
HTMLInputElement.prototype,
|
||||||
|
'value',
|
||||||
|
);
|
||||||
|
if (propertyDescriptor && propertyDescriptor.set) {
|
||||||
|
handlers.push(
|
||||||
|
...HOOK_PROPERTIES.map(p =>
|
||||||
|
hookSetter<HTMLElement>(p[0], p[1], {
|
||||||
|
set() {
|
||||||
|
// mock to a normal event
|
||||||
|
eventHandler({ target: this } as Event);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
handlers.forEach(h => h());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default function initObservers(o: observerParam) {
|
export default function initObservers(o: observerParam) {
|
||||||
const mutationObserver = initMutationObserver(o.mutationCb);
|
const mutationObserver = initMutationObserver(o.mutationCb);
|
||||||
const mousemoveHandler = initMousemoveObserver(o.mousemoveCb);
|
const mousemoveHandler = initMousemoveObserver(o.mousemoveCb);
|
||||||
@@ -222,11 +306,13 @@ export default function initObservers(o: observerParam) {
|
|||||||
);
|
);
|
||||||
const scrollHandler = initScrollObserver(o.scrollCb);
|
const scrollHandler = initScrollObserver(o.scrollCb);
|
||||||
const viewportResizeHandler = initViewportResizeObserver(o.viewportResizeCb);
|
const viewportResizeHandler = initViewportResizeObserver(o.viewportResizeCb);
|
||||||
|
const inputHandler = initInputObserver(o.inputCb);
|
||||||
return {
|
return {
|
||||||
mutationObserver,
|
mutationObserver,
|
||||||
mousemoveHandler,
|
mousemoveHandler,
|
||||||
mouseInteractionHandler,
|
mouseInteractionHandler,
|
||||||
scrollHandler,
|
scrollHandler,
|
||||||
viewportResizeHandler,
|
viewportResizeHandler,
|
||||||
|
inputHandler,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/types.ts
18
src/types.ts
@@ -37,6 +37,7 @@ export enum IncrementalSource {
|
|||||||
MouseInteraction,
|
MouseInteraction,
|
||||||
Scroll,
|
Scroll,
|
||||||
ViewportResize,
|
ViewportResize,
|
||||||
|
Input,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type mutationData = {
|
export type mutationData = {
|
||||||
@@ -60,12 +61,18 @@ export type viewportResizeData = {
|
|||||||
source: IncrementalSource.ViewportResize;
|
source: IncrementalSource.ViewportResize;
|
||||||
} & viewportResizeDimention;
|
} & viewportResizeDimention;
|
||||||
|
|
||||||
|
export type inputData = {
|
||||||
|
source: IncrementalSource.Input;
|
||||||
|
id: number;
|
||||||
|
} & inputValue;
|
||||||
|
|
||||||
export type incrementalData =
|
export type incrementalData =
|
||||||
| mutationData
|
| mutationData
|
||||||
| mousemoveData
|
| mousemoveData
|
||||||
| mouseInteractionData
|
| mouseInteractionData
|
||||||
| scrollData
|
| scrollData
|
||||||
| viewportResizeData;
|
| viewportResizeData
|
||||||
|
| inputData;
|
||||||
|
|
||||||
export type event =
|
export type event =
|
||||||
| domContentLoadedEvent
|
| domContentLoadedEvent
|
||||||
@@ -87,6 +94,7 @@ export type observerParam = {
|
|||||||
mouseInteractionCb: mouseInteractionCallBack;
|
mouseInteractionCb: mouseInteractionCallBack;
|
||||||
scrollCb: scrollCallback;
|
scrollCb: scrollCallback;
|
||||||
viewportResizeCb: viewportResizeCallback;
|
viewportResizeCb: viewportResizeCallback;
|
||||||
|
inputCb: inputCallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type textMutation = {
|
export type textMutation = {
|
||||||
@@ -167,6 +175,13 @@ export type viewportResizeDimention = {
|
|||||||
|
|
||||||
export type viewportResizeCallback = (d: viewportResizeDimention) => void;
|
export type viewportResizeCallback = (d: viewportResizeDimention) => void;
|
||||||
|
|
||||||
|
export type inputValue = {
|
||||||
|
text: string;
|
||||||
|
isChecked: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type inputCallback = (v: inputValue & { id: number }) => void;
|
||||||
|
|
||||||
export type Mirror = {
|
export type Mirror = {
|
||||||
map: idNodeMap;
|
map: idNodeMap;
|
||||||
getId: (n: INode) => number;
|
getId: (n: INode) => number;
|
||||||
@@ -180,3 +195,4 @@ export type throttleOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type listenerHandler = () => void;
|
export type listenerHandler = () => void;
|
||||||
|
export type hookResetter = () => void;
|
||||||
|
|||||||
27
src/utils.ts
27
src/utils.ts
@@ -1,4 +1,9 @@
|
|||||||
import { Mirror, throttleOptions, listenerHandler } from './types';
|
import {
|
||||||
|
Mirror,
|
||||||
|
throttleOptions,
|
||||||
|
listenerHandler,
|
||||||
|
hookResetter,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
export function on(
|
export function on(
|
||||||
type: string,
|
type: string,
|
||||||
@@ -17,13 +22,14 @@ export const mirror: Mirror = {
|
|||||||
getNode(id) {
|
getNode(id) {
|
||||||
return mirror.map[id];
|
return mirror.map[id];
|
||||||
},
|
},
|
||||||
|
// TODO: use a weakmap to get rid of manually memory management
|
||||||
removeNodeFromMap(n) {
|
removeNodeFromMap(n) {
|
||||||
const id = n.__sn && n.__sn.id;
|
const id = n.__sn && n.__sn.id;
|
||||||
delete mirror.map[id];
|
delete mirror.map[id];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// copy from underscore
|
// copy from underscore and modified
|
||||||
export function throttle<T>(
|
export function throttle<T>(
|
||||||
func: (arg: T) => void,
|
func: (arg: T) => void,
|
||||||
wait: number,
|
wait: number,
|
||||||
@@ -56,3 +62,20 @@ export function throttle<T>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hookSetter<T>(
|
||||||
|
target: T,
|
||||||
|
key: string | number | symbol,
|
||||||
|
d: PropertyDescriptor,
|
||||||
|
): hookResetter {
|
||||||
|
const original = Object.getOwnPropertyDescriptor(target, key);
|
||||||
|
Object.defineProperty(target, key, {
|
||||||
|
set(value) {
|
||||||
|
d.set!.call(this, value);
|
||||||
|
if (original && original.set) {
|
||||||
|
original.set.call(this, value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return () => hookSetter(target, key, original || {});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user