@reactual/handsontable
Version:
Spreadsheet-like data grid editor
269 lines (231 loc) • 6.86 kB
JavaScript
import {polymerWrap, closest} from './helpers/dom/element';
import {hasOwnProperty} from './helpers/object';
import {isWebComponentSupportedNatively} from './helpers/feature';
import {stopImmediatePropagation as _stopImmediatePropagation} from './helpers/dom/event';
/**
* Counter which tracks unregistered listeners (useful for detecting memory leaks).
*
* @type {Number}
*/
let listenersCounter = 0;
/**
* Event DOM manager for internal use in Handsontable.
*
* @class EventManager
* @util
*/
class EventManager {
/**
* @param {Object} [context=null]
* @private
*/
constructor(context = null) {
this.context = context || this;
if (!this.context.eventListeners) {
this.context.eventListeners = [];
}
}
/**
* Register specified listener (`eventName`) to the element.
*
* @param {Element} element Target element.
* @param {String} eventName Event name.
* @param {Function} callback Function which will be called after event occur.
* @returns {Function} Returns function which you can easily call to remove that event
*/
addEventListener(element, eventName, callback) {
let context = this.context;
function callbackProxy(event) {
event = extendEvent(context, event);
callback.call(this, event);
}
this.context.eventListeners.push({
element,
event: eventName,
callback,
callbackProxy,
});
if (window.addEventListener) {
element.addEventListener(eventName, callbackProxy, false);
} else {
element.attachEvent(`on${eventName}`, callbackProxy);
}
listenersCounter++;
return () => {
this.removeEventListener(element, eventName, callback);
};
}
/**
* Remove the event listener previously registered.
*
* @param {Element} element Target element.
* @param {String} eventName Event name.
* @param {Function} callback Function to remove from the event target. It must be the same as during registration listener.
*/
removeEventListener(element, eventName, callback) {
let len = this.context.eventListeners.length;
let tmpEvent;
while (len--) {
tmpEvent = this.context.eventListeners[len];
if (tmpEvent.event == eventName && tmpEvent.element == element) {
if (callback && callback != tmpEvent.callback) {
/* eslint-disable no-continue */
continue;
}
this.context.eventListeners.splice(len, 1);
if (tmpEvent.element.removeEventListener) {
tmpEvent.element.removeEventListener(tmpEvent.event, tmpEvent.callbackProxy, false);
} else {
tmpEvent.element.detachEvent(`on${tmpEvent.event}`, tmpEvent.callbackProxy);
}
listenersCounter--;
}
}
}
/**
* Clear all previously registered events.
*
* @private
* @since 0.15.0-beta3
*/
clearEvents() {
if (!this.context) {
return;
}
let len = this.context.eventListeners.length;
while (len--) {
let event = this.context.eventListeners[len];
if (event) {
this.removeEventListener(event.element, event.event, event.callback);
}
}
}
/**
* Clear all previously registered events.
*/
clear() {
this.clearEvents();
}
/**
* Destroy instance of EventManager.
*/
destroy() {
this.clearEvents();
this.context = null;
}
/**
* Trigger event at the specified target element.
*
* @param {Element} element Target element.
* @param {String} eventName Event name.
*/
fireEvent(element, eventName) {
let options = {
bubbles: true,
cancelable: (eventName !== 'mousemove'),
view: window,
detail: 0,
screenX: 0,
screenY: 0,
clientX: 1,
clientY: 1,
ctrlKey: false,
altKey: false,
shiftKey: false,
metaKey: false,
button: 0,
relatedTarget: undefined,
};
var event;
if (document.createEvent) {
event = document.createEvent('MouseEvents');
event.initMouseEvent(eventName, options.bubbles, options.cancelable,
options.view, options.detail,
options.screenX, options.screenY, options.clientX, options.clientY,
options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
options.button, options.relatedTarget || document.body.parentNode);
} else {
event = document.createEventObject();
}
if (element.dispatchEvent) {
element.dispatchEvent(event);
} else {
element.fireEvent(`on${eventName}`, event);
}
}
}
/**
* @param {Object} context
* @param {Event} event
* @private
* @returns {*}
*/
function extendEvent(context, event) {
let componentName = 'HOT-TABLE';
let isHotTableSpotted;
let fromElement;
let realTarget;
let target;
let len;
let nativeStopImmediatePropagation;
event.isTargetWebComponent = false;
event.realTarget = event.target;
nativeStopImmediatePropagation = event.stopImmediatePropagation;
event.stopImmediatePropagation = function() {
nativeStopImmediatePropagation.apply(this);
_stopImmediatePropagation(this);
};
if (!EventManager.isHotTableEnv) {
return event;
}
event = polymerWrap(event);
len = event.path ? event.path.length : 0;
while (len--) {
if (event.path[len].nodeName === componentName) {
isHotTableSpotted = true;
} else if (isHotTableSpotted && event.path[len].shadowRoot) {
target = event.path[len];
break;
}
if (len === 0 && !target) {
target = event.path[len];
}
}
if (!target) {
target = event.target;
}
event.isTargetWebComponent = true;
if (isWebComponentSupportedNatively()) {
event.realTarget = event.srcElement || event.toElement;
} else if (hasOwnProperty(context, 'hot') || context.isHotTableEnv || context.wtTable) {
// Polymer doesn't support `event.target` property properly we must emulate it ourselves
if (hasOwnProperty(context, 'hot')) {
// Custom element
fromElement = context.hot ? context.hot.view.wt.wtTable.TABLE : null;
} else if (context.isHotTableEnv) {
// Handsontable.Core
fromElement = context.view.activeWt.wtTable.TABLE.parentNode.parentNode;
} else if (context.wtTable) {
// Walkontable
fromElement = context.wtTable.TABLE.parentNode.parentNode;
}
realTarget = closest(event.target, [componentName], fromElement);
if (realTarget) {
event.realTarget = fromElement.querySelector(componentName) || event.target;
} else {
event.realTarget = event.target;
}
}
Object.defineProperty(event, 'target', {
get() {
return polymerWrap(target);
},
enumerable: true,
configurable: true,
});
return event;
}
export default EventManager;
export function getListenersCounter() {
return listenersCounter;
};