UNPKG

7.28 kBJavaScriptView Raw
1import "core-js/modules/es.error.cause.js";
2import "core-js/modules/es.array.push.js";
3function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
4function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
5function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
6import { stopImmediatePropagation as _stopImmediatePropagation } from "./helpers/dom/event.mjs";
7/**
8 * Counter which tracks unregistered listeners (useful for detecting memory leaks).
9 *
10 * @type {number}
11 */
12let listenersCounter = 0;
13
14/**
15 * Event DOM manager for internal use in Handsontable.
16 *
17 * @class EventManager
18 */
19class EventManager {
20 /**
21 * @param {object} [context=null] An object to which event listeners will be stored.
22 * @private
23 */
24 constructor() {
25 let context = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
26 /**
27 * @type {object}
28 */
29 _defineProperty(this, "context", void 0);
30 this.context = context || this;
31
32 // TODO it modify external object. Rethink that.
33 if (!this.context.eventListeners) {
34 this.context.eventListeners = []; // TODO perf It would be more performant if every instance of EventManager tracked its own listeners only
35 }
36 }
37
38 /**
39 * Register specified listener (`eventName`) to the element.
40 *
41 * @param {Element} element Target element.
42 * @param {string} eventName Event name.
43 * @param {Function} callback Function which will be called after event occur.
44 * @param {AddEventListenerOptions|boolean} [options] Listener options if object or useCapture if boolean.
45 * @returns {Function} Returns function which you can easily call to remove that event.
46 */
47 addEventListener(element, eventName, callback) {
48 let options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
49 /**
50 * @private
51 * @param {Event} event The event object.
52 */
53 function callbackProxy(event) {
54 callback.call(this, extendEvent(event));
55 }
56 this.context.eventListeners.push({
57 element,
58 event: eventName,
59 callback,
60 callbackProxy,
61 options,
62 eventManager: this
63 });
64 element.addEventListener(eventName, callbackProxy, options);
65 listenersCounter += 1;
66 return () => {
67 this.removeEventListener(element, eventName, callback);
68 };
69 }
70
71 /**
72 * Remove the event listener previously registered.
73 *
74 * @param {Element} element Target element.
75 * @param {string} eventName Event name.
76 * @param {Function} callback Function to remove from the event target. It must be the same as during registration listener.
77 * @param {boolean} [onlyOwnEvents] Whether whould remove only events registered using this instance of EventManager.
78 */
79 removeEventListener(element, eventName, callback) {
80 let onlyOwnEvents = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
81 let len = this.context.eventListeners.length;
82 let tmpEvent;
83 while (len) {
84 len -= 1;
85 tmpEvent = this.context.eventListeners[len];
86 if (tmpEvent.event === eventName && tmpEvent.element === element) {
87 if (callback && callback !== tmpEvent.callback) {
88 /* eslint-disable no-continue */
89 continue;
90 }
91 // TODO rethink that, main bulk is that it needs multi instances to handle same context, but with a different scopes.
92 // TODO I suppose much more efficient way will be comparing string with scope id, or any similar approach.
93 if (onlyOwnEvents && tmpEvent.eventManager !== this) {
94 continue;
95 }
96 this.context.eventListeners.splice(len, 1);
97 tmpEvent.element.removeEventListener(tmpEvent.event, tmpEvent.callbackProxy, tmpEvent.options);
98 listenersCounter -= 1;
99 }
100 }
101 }
102
103 /**
104 * Clear all previously registered events.
105 *
106 * @private
107 * @since 0.15.0-beta3
108 * @param {boolean} [onlyOwnEvents] Whether whould remove only events registered using this instance of EventManager.
109 */
110 clearEvents() {
111 let onlyOwnEvents = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
112 if (!this.context) {
113 return;
114 }
115 let len = this.context.eventListeners.length;
116 while (len) {
117 len -= 1;
118 const event = this.context.eventListeners[len];
119 if (onlyOwnEvents && event.eventManager !== this) {
120 continue;
121 }
122 this.context.eventListeners.splice(len, 1);
123 event.element.removeEventListener(event.event, event.callbackProxy, event.options);
124 listenersCounter -= 1;
125 }
126 }
127
128 /**
129 * Clear all previously registered events.
130 */
131 clear() {
132 this.clearEvents();
133 }
134
135 /**
136 * Destroy instance of EventManager, clearing all events of the context.
137 */
138 destroy() {
139 this.clearEvents();
140 this.context = null;
141 }
142
143 /**
144 * Destroy instance of EventManager, clearing only the own events.
145 */
146 destroyWithOwnEventsOnly() {
147 this.clearEvents(true);
148 this.context = null;
149 }
150
151 /**
152 * Trigger event at the specified target element.
153 *
154 * @param {Element} element Target element.
155 * @param {string} eventName Event name.
156 */
157 fireEvent(element, eventName) {
158 let rootDocument = element.document;
159 let rootWindow = element;
160 if (!rootDocument) {
161 rootDocument = element.ownerDocument ? element.ownerDocument : element;
162 rootWindow = rootDocument.defaultView;
163 }
164 const options = {
165 bubbles: true,
166 cancelable: eventName !== 'mousemove',
167 view: rootWindow,
168 detail: 0,
169 screenX: 0,
170 screenY: 0,
171 clientX: 1,
172 clientY: 1,
173 ctrlKey: false,
174 altKey: false,
175 shiftKey: false,
176 metaKey: false,
177 button: 0,
178 relatedTarget: undefined
179 };
180 let event;
181 if (rootDocument.createEvent) {
182 event = rootDocument.createEvent('MouseEvents');
183 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 || rootDocument.body.parentNode);
184 } else {
185 event = rootDocument.createEventObject();
186 }
187 if (element.dispatchEvent) {
188 element.dispatchEvent(event);
189 } else {
190 element.fireEvent(`on${eventName}`, event);
191 }
192 }
193}
194
195/**
196 * @private
197 * @param {Event} event The event object.
198 * @returns {Event}
199 */
200function extendEvent(event) {
201 const nativeStopImmediatePropagation = event.stopImmediatePropagation;
202 event.stopImmediatePropagation = function () {
203 nativeStopImmediatePropagation.apply(this);
204 _stopImmediatePropagation(this);
205 };
206 return event;
207}
208export default EventManager;
209
210/**
211 * @private
212 * @returns {number}
213 */
214export function getListenersCounter() {
215 return listenersCounter;
216}
\No newline at end of file