259 lines
9.9 KiB
JavaScript
259 lines
9.9 KiB
JavaScript
|
import { trapOn, trapOff } from './traps';
|
||
|
import { getEventTarget } from './utils';
|
||
|
// currently `once` and `passive` is not supported
|
||
|
function createDelegate() {
|
||
|
if (typeof window === 'undefined') {
|
||
|
return {
|
||
|
on: () => { },
|
||
|
off: () => { }
|
||
|
};
|
||
|
}
|
||
|
const propagationStopped = new WeakMap();
|
||
|
const immediatePropagationStopped = new WeakMap();
|
||
|
function trackPropagation() {
|
||
|
propagationStopped.set(this, true);
|
||
|
}
|
||
|
function trackImmediate() {
|
||
|
propagationStopped.set(this, true);
|
||
|
immediatePropagationStopped.set(this, true);
|
||
|
}
|
||
|
function spy(event, propName, fn) {
|
||
|
const source = event[propName];
|
||
|
event[propName] = function () {
|
||
|
fn.apply(event, arguments);
|
||
|
return source.apply(event, arguments);
|
||
|
};
|
||
|
return event;
|
||
|
}
|
||
|
function unspy(event, propName) {
|
||
|
event[propName] = Event.prototype[propName];
|
||
|
}
|
||
|
const currentTargets = new WeakMap();
|
||
|
const currentTargetDescriptor = Object.getOwnPropertyDescriptor(Event.prototype, 'currentTarget');
|
||
|
function getCurrentTarget() {
|
||
|
var _a;
|
||
|
return (_a = currentTargets.get(this)) !== null && _a !== void 0 ? _a : null;
|
||
|
}
|
||
|
function defineCurrentTarget(event, getter) {
|
||
|
if (currentTargetDescriptor === undefined)
|
||
|
return;
|
||
|
Object.defineProperty(event, 'currentTarget', {
|
||
|
configurable: true,
|
||
|
enumerable: true,
|
||
|
get: getter !== null && getter !== void 0 ? getter : currentTargetDescriptor.get
|
||
|
});
|
||
|
}
|
||
|
const phaseToTypeToElToHandlers = {
|
||
|
bubble: {},
|
||
|
capture: {}
|
||
|
};
|
||
|
const typeToWindowEventHandlers = {};
|
||
|
function createUnifiedHandler() {
|
||
|
const delegeteHandler = function (e) {
|
||
|
const { type, eventPhase, bubbles } = e;
|
||
|
const target = getEventTarget(e);
|
||
|
if (eventPhase === 2)
|
||
|
return;
|
||
|
const phase = eventPhase === 1 ? 'capture' : 'bubble';
|
||
|
let cursor = target;
|
||
|
const path = [];
|
||
|
// collecting bubble path
|
||
|
while (true) {
|
||
|
if (cursor === null)
|
||
|
cursor = window;
|
||
|
path.push(cursor);
|
||
|
if (cursor === window) {
|
||
|
break;
|
||
|
}
|
||
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||
|
cursor = (cursor.parentNode || null);
|
||
|
}
|
||
|
const captureElToHandlers = phaseToTypeToElToHandlers.capture[type];
|
||
|
const bubbleElToHandlers = phaseToTypeToElToHandlers.bubble[type];
|
||
|
spy(e, 'stopPropagation', trackPropagation);
|
||
|
spy(e, 'stopImmediatePropagation', trackImmediate);
|
||
|
defineCurrentTarget(e, getCurrentTarget);
|
||
|
if (phase === 'capture') {
|
||
|
if (captureElToHandlers === undefined)
|
||
|
return;
|
||
|
// capture
|
||
|
for (let i = path.length - 1; i >= 0; --i) {
|
||
|
if (propagationStopped.has(e))
|
||
|
break;
|
||
|
const target = path[i];
|
||
|
const handlers = captureElToHandlers.get(target);
|
||
|
if (handlers !== undefined) {
|
||
|
currentTargets.set(e, target);
|
||
|
for (const handler of handlers) {
|
||
|
if (immediatePropagationStopped.has(e))
|
||
|
break;
|
||
|
handler(e);
|
||
|
}
|
||
|
}
|
||
|
if (i === 0 && !bubbles && bubbleElToHandlers !== undefined) {
|
||
|
const bubbleHandlers = bubbleElToHandlers.get(target);
|
||
|
if (bubbleHandlers !== undefined) {
|
||
|
for (const handler of bubbleHandlers) {
|
||
|
if (immediatePropagationStopped.has(e))
|
||
|
break;
|
||
|
handler(e);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else if (phase === 'bubble') {
|
||
|
if (bubbleElToHandlers === undefined)
|
||
|
return;
|
||
|
// bubble
|
||
|
for (let i = 0; i < path.length; ++i) {
|
||
|
if (propagationStopped.has(e))
|
||
|
break;
|
||
|
const target = path[i];
|
||
|
const handlers = bubbleElToHandlers.get(target);
|
||
|
if (handlers !== undefined) {
|
||
|
currentTargets.set(e, target);
|
||
|
for (const handler of handlers) {
|
||
|
if (immediatePropagationStopped.has(e))
|
||
|
break;
|
||
|
handler(e);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
unspy(e, 'stopPropagation');
|
||
|
unspy(e, 'stopImmediatePropagation');
|
||
|
defineCurrentTarget(e);
|
||
|
};
|
||
|
delegeteHandler.displayName = 'evtdUnifiedHandler';
|
||
|
return delegeteHandler;
|
||
|
}
|
||
|
function createUnifiedWindowEventHandler() {
|
||
|
const delegateHandler = function (e) {
|
||
|
const { type, eventPhase } = e;
|
||
|
if (eventPhase !== 2)
|
||
|
return;
|
||
|
const handlers = typeToWindowEventHandlers[type];
|
||
|
if (handlers === undefined)
|
||
|
return;
|
||
|
handlers.forEach((handler) => handler(e));
|
||
|
};
|
||
|
delegateHandler.displayName = 'evtdUnifiedWindowEventHandler';
|
||
|
return delegateHandler;
|
||
|
}
|
||
|
const unifiedHandler = createUnifiedHandler();
|
||
|
const unfiendWindowEventHandler = createUnifiedWindowEventHandler();
|
||
|
function ensureElToHandlers(phase, type) {
|
||
|
const phaseHandlers = phaseToTypeToElToHandlers[phase];
|
||
|
if (phaseHandlers[type] === undefined) {
|
||
|
phaseHandlers[type] = new Map();
|
||
|
window.addEventListener(type, unifiedHandler, phase === 'capture');
|
||
|
}
|
||
|
return phaseHandlers[type];
|
||
|
}
|
||
|
function ensureWindowEventHandlers(type) {
|
||
|
const windowEventHandlers = typeToWindowEventHandlers[type];
|
||
|
if (windowEventHandlers === undefined) {
|
||
|
typeToWindowEventHandlers[type] = new Set();
|
||
|
window.addEventListener(type, unfiendWindowEventHandler);
|
||
|
}
|
||
|
return typeToWindowEventHandlers[type];
|
||
|
}
|
||
|
function ensureHandlers(elToHandlers, el) {
|
||
|
let elHandlers = elToHandlers.get(el);
|
||
|
if (elHandlers === undefined) {
|
||
|
elToHandlers.set(el, (elHandlers = new Set()));
|
||
|
}
|
||
|
return elHandlers;
|
||
|
}
|
||
|
function handlerExist(el, phase, type, handler) {
|
||
|
const elToHandlers = phaseToTypeToElToHandlers[phase][type];
|
||
|
// phase ${type} event has handlers
|
||
|
if (elToHandlers !== undefined) {
|
||
|
const handlers = elToHandlers.get(el);
|
||
|
// phase using el with ${type} event has handlers
|
||
|
if (handlers !== undefined) {
|
||
|
if (handlers.has(handler))
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
function windowEventHandlerExist(type, handler) {
|
||
|
const handlers = typeToWindowEventHandlers[type];
|
||
|
if (handlers !== undefined) {
|
||
|
if (handlers.has(handler)) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
function on(type, el, handler, options) {
|
||
|
let mergedHandler;
|
||
|
if (typeof options === 'object' && options.once === true) {
|
||
|
mergedHandler = (e) => {
|
||
|
off(type, el, mergedHandler, options);
|
||
|
handler(e);
|
||
|
};
|
||
|
}
|
||
|
else {
|
||
|
mergedHandler = handler;
|
||
|
}
|
||
|
const trapped = trapOn(type, el, mergedHandler, options);
|
||
|
if (trapped)
|
||
|
return;
|
||
|
const phase = options === true ||
|
||
|
(typeof options === 'object' && options.capture === true)
|
||
|
? 'capture'
|
||
|
: 'bubble';
|
||
|
const elToHandlers = ensureElToHandlers(phase, type);
|
||
|
const handlers = ensureHandlers(elToHandlers, el);
|
||
|
if (!handlers.has(mergedHandler))
|
||
|
handlers.add(mergedHandler);
|
||
|
if (el === window) {
|
||
|
const windowEventHandlers = ensureWindowEventHandlers(type);
|
||
|
if (!windowEventHandlers.has(mergedHandler)) {
|
||
|
windowEventHandlers.add(mergedHandler);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
function off(type, el, handler, options) {
|
||
|
const trapped = trapOff(type, el, handler, options);
|
||
|
if (trapped)
|
||
|
return;
|
||
|
const capture = options === true ||
|
||
|
(typeof options === 'object' && options.capture === true);
|
||
|
const phase = capture ? 'capture' : 'bubble';
|
||
|
const elToHandlers = ensureElToHandlers(phase, type);
|
||
|
const handlers = ensureHandlers(elToHandlers, el);
|
||
|
if (el === window) {
|
||
|
const mirrorPhase = capture ? 'bubble' : 'capture';
|
||
|
if (!handlerExist(el, mirrorPhase, type, handler) &&
|
||
|
windowEventHandlerExist(type, handler)) {
|
||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||
|
const windowEventHandlers = typeToWindowEventHandlers[type];
|
||
|
windowEventHandlers.delete(handler);
|
||
|
if (windowEventHandlers.size === 0) {
|
||
|
window.removeEventListener(type, unfiendWindowEventHandler);
|
||
|
typeToWindowEventHandlers[type] = undefined;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (handlers.has(handler))
|
||
|
handlers.delete(handler);
|
||
|
if (handlers.size === 0) {
|
||
|
elToHandlers.delete(el);
|
||
|
}
|
||
|
if (elToHandlers.size === 0) {
|
||
|
window.removeEventListener(type, unifiedHandler, phase === 'capture');
|
||
|
phaseToTypeToElToHandlers[phase][type] = undefined;
|
||
|
}
|
||
|
}
|
||
|
return {
|
||
|
on: on,
|
||
|
off: off
|
||
|
};
|
||
|
}
|
||
|
const { on, off } = createDelegate();
|
||
|
export { on, off };
|