168 lines
3.9 KiB
TypeScript
168 lines
3.9 KiB
TypeScript
import LAYOUT_CAUSES from './layoutCauses';
|
|
|
|
type Entities = keyof typeof LAYOUT_CAUSES;
|
|
type Phase =
|
|
'measure'
|
|
| 'mutate';
|
|
type ErrorHandler = (error: Error) => any;
|
|
|
|
// eslint-disable-next-line no-console
|
|
const DEFAULT_ERROR_HANDLER = console.error;
|
|
|
|
let onError: ErrorHandler = DEFAULT_ERROR_HANDLER;
|
|
|
|
const nativeMethods = new Map<string, AnyFunction>();
|
|
|
|
let phase: Phase = 'measure';
|
|
let isStrict = false;
|
|
let observer: MutationObserver | undefined;
|
|
|
|
export function setPhase(newPhase: Phase) {
|
|
phase = newPhase;
|
|
}
|
|
|
|
export function getPhase() {
|
|
return phase;
|
|
}
|
|
|
|
export function enableStrict() {
|
|
if (isStrict) return;
|
|
|
|
isStrict = true;
|
|
setupLayoutDetectors();
|
|
setupMutationObserver();
|
|
}
|
|
|
|
export function disableStrict() {
|
|
if (!isStrict) return;
|
|
|
|
clearMutationObserver();
|
|
clearLayoutDetectors();
|
|
isStrict = false;
|
|
}
|
|
|
|
export function forceMeasure(cb: () => any) {
|
|
if (phase !== 'mutate') {
|
|
throw new Error('The current phase is \'measure\'');
|
|
}
|
|
|
|
phase = 'measure';
|
|
const result = cb();
|
|
phase = 'mutate';
|
|
|
|
return result;
|
|
}
|
|
|
|
export function setHandler(handler?: ErrorHandler) {
|
|
onError = handler || DEFAULT_ERROR_HANDLER;
|
|
}
|
|
|
|
function setupLayoutDetectors() {
|
|
Object.entries(LAYOUT_CAUSES).forEach(([name, causes]) => {
|
|
const entity = window[name as Entities];
|
|
if (!entity) return;
|
|
const prototype = typeof entity === 'object' ? entity : entity.prototype;
|
|
|
|
if ('props' in causes) {
|
|
causes.props.forEach((prop) => {
|
|
const nativeGetter = Object.getOwnPropertyDescriptor(prototype, prop)?.get;
|
|
if (!nativeGetter) {
|
|
return;
|
|
}
|
|
|
|
nativeMethods.set(`${name}#${prop}`, nativeGetter);
|
|
|
|
Object.defineProperty(prototype, prop, {
|
|
get() {
|
|
onMeasure(prop);
|
|
|
|
return nativeGetter.call(this);
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
if ('methods' in causes) {
|
|
causes.methods.forEach((method) => {
|
|
const nativeMethod = (prototype as any)[method]!;
|
|
nativeMethods.set(`${name}#${method}`, nativeMethod);
|
|
|
|
// eslint-disable-next-line func-names
|
|
(prototype as any)[method] = function (...args: any[]) {
|
|
onMeasure(method);
|
|
|
|
return nativeMethod.apply(this, args);
|
|
};
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function clearLayoutDetectors() {
|
|
Object.entries(LAYOUT_CAUSES).forEach(([name, causes]) => {
|
|
const entity = window[name as Entities];
|
|
if (!entity) return;
|
|
const prototype = typeof entity === 'object' ? entity : entity.prototype;
|
|
|
|
if ('props' in causes) {
|
|
causes.props.forEach((prop) => {
|
|
const nativeGetter = nativeMethods.get(`${name}#${prop}`);
|
|
if (!nativeGetter) {
|
|
return;
|
|
}
|
|
|
|
Object.defineProperty(prototype, prop, { get: nativeGetter });
|
|
});
|
|
}
|
|
|
|
if ('methods' in causes) {
|
|
causes.methods.forEach((method) => {
|
|
(prototype as any)[method] = nativeMethods.get(`${name}#${method}`)!;
|
|
});
|
|
}
|
|
});
|
|
|
|
nativeMethods.clear();
|
|
}
|
|
|
|
function setupMutationObserver() {
|
|
observer = new MutationObserver((mutations) => {
|
|
if (phase !== 'mutate') {
|
|
mutations.forEach(({ target, type, attributeName }) => {
|
|
if (!document.contains(target)) {
|
|
return;
|
|
}
|
|
|
|
if (type === 'childList' && target instanceof HTMLElement && target.contentEditable) {
|
|
return;
|
|
}
|
|
|
|
if (attributeName?.startsWith('data-')) {
|
|
return;
|
|
}
|
|
|
|
// eslint-disable-next-line no-console
|
|
onError(new Error(`Unexpected mutation detected: \`${type === 'attributes' ? attributeName : type}\``));
|
|
});
|
|
}
|
|
});
|
|
|
|
observer.observe(document.body, {
|
|
childList: true,
|
|
attributes: true,
|
|
subtree: true,
|
|
characterData: false,
|
|
});
|
|
}
|
|
|
|
function clearMutationObserver() {
|
|
observer?.disconnect();
|
|
observer = undefined;
|
|
}
|
|
|
|
function onMeasure(propName: string) {
|
|
if (phase !== 'measure') {
|
|
onError(new Error(`Unexpected measurement detected: \`${propName}\``));
|
|
}
|
|
}
|