eventManager.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. import { polymerWrap, closest } from './helpers/dom/element';
  2. import { hasOwnProperty } from './helpers/object';
  3. import { isWebComponentSupportedNatively } from './helpers/feature';
  4. import { stopImmediatePropagation as _stopImmediatePropagation } from './helpers/dom/event';
  5. /**
  6. * Counter which tracks unregistered listeners (useful for detecting memory leaks).
  7. *
  8. * @type {Number}
  9. */
  10. let listenersCounter = 0;
  11. /**
  12. * Event DOM manager for internal use in Handsontable.
  13. *
  14. * @class EventManager
  15. * @util
  16. */
  17. class EventManager {
  18. /**
  19. * @param {Object} [context=null]
  20. * @private
  21. */
  22. constructor(context = null) {
  23. this.context = context || this;
  24. if (!this.context.eventListeners) {
  25. this.context.eventListeners = [];
  26. }
  27. }
  28. /**
  29. * Register specified listener (`eventName`) to the element.
  30. *
  31. * @param {Element} element Target element.
  32. * @param {String} eventName Event name.
  33. * @param {Function} callback Function which will be called after event occur.
  34. * @returns {Function} Returns function which you can easily call to remove that event
  35. */
  36. addEventListener(element, eventName, callback) {
  37. const context = this.context;
  38. function callbackProxy(event) {
  39. callback.call(this, extendEvent(context, event));
  40. }
  41. this.context.eventListeners.push({
  42. element,
  43. event: eventName,
  44. callback,
  45. callbackProxy,
  46. });
  47. element.addEventListener(eventName, callbackProxy, false);
  48. listenersCounter += 1;
  49. return () => {
  50. this.removeEventListener(element, eventName, callback);
  51. };
  52. }
  53. /**
  54. * Remove the event listener previously registered.
  55. *
  56. * @param {Element} element Target element.
  57. * @param {String} eventName Event name.
  58. * @param {Function} callback Function to remove from the event target. It must be the same as during registration listener.
  59. */
  60. removeEventListener(element, eventName, callback) {
  61. let len = this.context.eventListeners.length;
  62. let tmpEvent;
  63. while (len) {
  64. len -= 1;
  65. tmpEvent = this.context.eventListeners[len];
  66. if (tmpEvent.event === eventName && tmpEvent.element === element) {
  67. if (callback && callback !== tmpEvent.callback) {
  68. /* eslint-disable no-continue */
  69. continue;
  70. }
  71. this.context.eventListeners.splice(len, 1);
  72. tmpEvent.element.removeEventListener(tmpEvent.event, tmpEvent.callbackProxy, false);
  73. listenersCounter -= 1;
  74. }
  75. }
  76. }
  77. /**
  78. * Clear all previously registered events.
  79. *
  80. * @private
  81. * @since 0.15.0-beta3
  82. */
  83. clearEvents() {
  84. if (!this.context) {
  85. return;
  86. }
  87. let len = this.context.eventListeners.length;
  88. while (len) {
  89. len -= 1;
  90. const event = this.context.eventListeners[len];
  91. if (event) {
  92. this.removeEventListener(event.element, event.event, event.callback);
  93. }
  94. }
  95. }
  96. /**
  97. * Clear all previously registered events.
  98. */
  99. clear() {
  100. this.clearEvents();
  101. }
  102. /**
  103. * Destroy instance of EventManager.
  104. */
  105. destroy() {
  106. this.clearEvents();
  107. this.context = null;
  108. }
  109. /**
  110. * Trigger event at the specified target element.
  111. *
  112. * @param {Element} element Target element.
  113. * @param {String} eventName Event name.
  114. */
  115. fireEvent(element, eventName) {
  116. const options = {
  117. bubbles: true,
  118. cancelable: (eventName !== 'mousemove'),
  119. view: window,
  120. detail: 0,
  121. screenX: 0,
  122. screenY: 0,
  123. clientX: 1,
  124. clientY: 1,
  125. ctrlKey: false,
  126. altKey: false,
  127. shiftKey: false,
  128. metaKey: false,
  129. button: 0,
  130. relatedTarget: undefined,
  131. };
  132. let event;
  133. if (document.createEvent) {
  134. event = document.createEvent('MouseEvents');
  135. event.initMouseEvent(eventName, options.bubbles, options.cancelable,
  136. options.view, options.detail,
  137. options.screenX, options.screenY, options.clientX, options.clientY,
  138. options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
  139. options.button, options.relatedTarget || document.body.parentNode);
  140. } else {
  141. event = document.createEventObject();
  142. }
  143. if (element.dispatchEvent) {
  144. element.dispatchEvent(event);
  145. } else {
  146. element.fireEvent(`on${eventName}`, event);
  147. }
  148. }
  149. }
  150. /**
  151. * @param {Object} context
  152. * @param {Event} event
  153. * @private
  154. * @returns {*}
  155. */
  156. function extendEvent(context, event) {
  157. const componentName = 'HOT-TABLE';
  158. let isHotTableSpotted;
  159. let fromElement;
  160. let realTarget;
  161. let target;
  162. let len;
  163. event.isTargetWebComponent = false;
  164. event.realTarget = event.target;
  165. const nativeStopImmediatePropagation = event.stopImmediatePropagation;
  166. event.stopImmediatePropagation = function() {
  167. nativeStopImmediatePropagation.apply(this);
  168. _stopImmediatePropagation(this);
  169. };
  170. if (!EventManager.isHotTableEnv) {
  171. return event;
  172. }
  173. // eslint-disable-next-line no-param-reassign
  174. event = polymerWrap(event);
  175. len = event.path ? event.path.length : 0;
  176. while (len) {
  177. len -= 1;
  178. if (event.path[len].nodeName === componentName) {
  179. isHotTableSpotted = true;
  180. } else if (isHotTableSpotted && event.path[len].shadowRoot) {
  181. target = event.path[len];
  182. break;
  183. }
  184. if (len === 0 && !target) {
  185. target = event.path[len];
  186. }
  187. }
  188. if (!target) {
  189. target = event.target;
  190. }
  191. event.isTargetWebComponent = true;
  192. if (isWebComponentSupportedNatively()) {
  193. event.realTarget = event.srcElement || event.toElement;
  194. } else if (hasOwnProperty(context, 'hot') || context.isHotTableEnv || context.wtTable) {
  195. // Polymer doesn't support `event.target` property properly we must emulate it ourselves
  196. if (hasOwnProperty(context, 'hot')) {
  197. // Custom element
  198. fromElement = context.hot ? context.hot.view.wt.wtTable.TABLE : null;
  199. } else if (context.isHotTableEnv) {
  200. // Handsontable.Core
  201. fromElement = context.view.activeWt.wtTable.TABLE.parentNode.parentNode;
  202. } else if (context.wtTable) {
  203. // Walkontable
  204. fromElement = context.wtTable.TABLE.parentNode.parentNode;
  205. }
  206. realTarget = closest(event.target, [componentName], fromElement);
  207. if (realTarget) {
  208. event.realTarget = fromElement.querySelector(componentName) || event.target;
  209. } else {
  210. event.realTarget = event.target;
  211. }
  212. }
  213. Object.defineProperty(event, 'target', {
  214. get() {
  215. return polymerWrap(target);
  216. },
  217. enumerable: true,
  218. configurable: true,
  219. });
  220. return event;
  221. }
  222. export default EventManager;
  223. export function getListenersCounter() {
  224. return listenersCounter;
  225. }