/* eslint complexity: 0 */ /** * @fileoverview Utility module for handling DOM events. * @author NHN FE Development Lab */ 'use strict'; var util = require('tui-code-snippet'); var eventKey = '_evt', DRAG_START = ['touchstart', 'mousedown']; var domevent = { /** * Bind dom events. * @param {HTMLElement} obj HTMLElement to bind events. * @param {(string|object)} types Space splitted events names or eventName:handler object. * @param {*} fn handler function or context for handler method. * @param {*} [context] context object for handler method. */ on: function(obj, types, fn, context) { if (util.isString(types)) { util.forEach(types.split(' '), function(type) { domevent._on(obj, type, fn, context); }); return; } util.forEachOwnProperties(types, function(handler, type) { domevent._on(obj, type, handler, fn); }); }, /** * DOM event binding. * @param {HTMLElement} obj HTMLElement to bind events. * @param {String} type The name of events. * @param {*} fn handler function * @param {*} [context] context object for handler method. * @private */ _on: function(obj, type, fn, context) { var id, handler, originHandler; id = type + util.stamp(fn) + (context ? '_' + util.stamp(context) : ''); if (obj[eventKey] && obj[eventKey][id]) { return; } handler = function(e) { fn.call(context || obj, e || window.event); }; originHandler = handler; if ('addEventListener' in obj) { if (type === 'mouseenter' || type === 'mouseleave') { handler = function(e) { e = e || window.event; if (!domevent._checkMouse(obj, e)) { return; } originHandler(e); }; obj.addEventListener((type === 'mouseenter') ? 'mouseover' : 'mouseout', handler, false); } else { if (type === 'mousewheel') { obj.addEventListener('DOMMouseScroll', handler, false); } obj.addEventListener(type, handler, false); } } else if ('attachEvent' in obj) { obj.attachEvent('on' + type, handler); } obj[eventKey] = obj[eventKey] || {}; obj[eventKey][id] = handler; }, /** * Unbind DOM Event handler. * @param {HTMLElement} obj HTMLElement to unbind. * @param {(string|object)} types Space splitted events names or eventName:handler object. * @param {*} fn handler function or context for handler method. * @param {*} [context] context object for handler method. */ off: function(obj, types, fn, context) { if (util.isString(types)) { util.forEach(types.split(' '), function(type) { domevent._off(obj, type, fn, context); }); return; } util.forEachOwnProperties(types, function(handler, type) { domevent._off(obj, type, handler, fn); }); }, /** * Unbind DOM event handler. * @param {HTMLElement} obj HTMLElement to unbind. * @param {String} type The name of event to unbind. * @param {function()} fn Event handler that supplied when binding. * @param {*} context context object that supplied when binding. * @private */ _off: function(obj, type, fn, context) { var id = type + util.stamp(fn) + (context ? '_' + util.stamp(context) : ''), handler = obj[eventKey] && obj[eventKey][id]; if (!handler) { return; } if ('removeEventListener' in obj) { if (type === 'mouseenter' || type === 'mouseleave') { obj.removeEventListener((type === 'mouseenter') ? 'mouseover' : 'mouseout', handler, false); } else { if (type === 'mousewheel') { obj.removeEventListener('DOMMouseScroll', handler, false); } obj.removeEventListener(type, handler, false); } } else if ('detachEvent' in obj) { try { obj.detachEvent('on' + type, handler); } catch (e) {} //eslint-disable-line } delete obj[eventKey][id]; if (util.keys(obj[eventKey]).length) { return; } delete obj[eventKey]; }, /** * Bind DOM event. this event will unbind after invokes. * @param {HTMLElement} obj HTMLElement to bind events. * @param {(string|object)} types Space splitted events names or eventName:handler object. * @param {*} fn handler function or context for handler method. * @param {*} [context] context object for handler method. */ once: function(obj, types, fn, context) { var self = this; if (util.isObject(types)) { util.forEachOwnProperties(types, function(handler, type) { domevent.once(obj, type, handler, fn); }); return; } /** * Handler for temporary usage for once implementation */ function onceHandler() { fn.apply(context || obj, arguments); self._off(obj, types, onceHandler, context); } domevent.on(obj, types, onceHandler, context); }, /** * Cancel event bubbling. * @param {Event} e Event object. */ stopPropagation: function(e) { if (e.stopPropagation) { e.stopPropagation(); } else { e.cancelBubble = true; } }, /** * Cancel browser default actions. * @param {Event} e Event object. */ preventDefault: function(e) { if (e.preventDefault) { e.preventDefault(); } else { e.returnValue = false; } }, /** * Syntatic sugar of stopPropagation and preventDefault * @param {Event} e Event object. */ stop: function(e) { domevent.preventDefault(e); domevent.stopPropagation(e); }, /** * Stop scroll events. * @param {HTMLElement} el HTML element to prevent scroll. */ disableScrollPropagation: function(el) { domevent.on(el, 'mousewheel MozMousePixelScroll', domevent.stopPropagation); }, /** * Stop all events related with click. * @param {HTMLElement} el HTML element to prevent all event related with click. */ disableClickPropagation: function(el) { domevent.on(el, DRAG_START.join(' ') + ' click dblclick', domevent.stopPropagation); }, /** * Get mouse position from mouse event. * * If supplied relatveElement parameter then return relative position based on element. * @param {Event} mouseEvent Mouse event object * @param {HTMLElement} relativeElement HTML element that calculate relative position. * @returns {number[]} mouse position. */ getMousePosition: function(mouseEvent, relativeElement) { var rect; if (!relativeElement) { return [mouseEvent.clientX, mouseEvent.clientY]; } rect = relativeElement.getBoundingClientRect(); return [ mouseEvent.clientX - rect.left - relativeElement.clientLeft, mouseEvent.clientY - rect.top - relativeElement.clientTop ]; }, /** * Normalize mouse wheel event that different each browsers. * @param {MouseEvent} e Mouse wheel event. * @returns {Number} delta */ getWheelDelta: function(e) { var delta = 0; if (e.wheelDelta) { delta = e.wheelDelta / 120; } if (e.detail) { delta = -e.detail / 3; } return delta; }, /** * prevent firing mouseleave event when mouse entered child elements. * @param {HTMLElement} el HTML element * @param {MouseEvent} e Mouse event * @returns {Boolean} leave? * @private */ _checkMouse: function(el, e) { var related = e.relatedTarget; if (!related) { return true; } try { while (related && (related !== el)) { related = related.parentNode; } } catch (err) { return false; } return (related !== el); }, /** * Trigger specific events to html element. * @param {HTMLElement} obj HTMLElement * @param {string} type Event type name * @param {object} [eventData] Event data */ trigger: function(obj, type, eventData) { var rMouseEvent = /(mouse|click)/; if (util.isUndefined(eventData) && rMouseEvent.exec(type)) { eventData = domevent.mouseEvent(type); } if (obj.dispatchEvent) { obj.dispatchEvent(eventData); } else if (obj.fireEvent) { obj.fireEvent('on' + type, eventData); } }, /** * Create virtual mouse event. * * Tested at * * - IE7 ~ IE11 * - Chrome * - Firefox * - Safari * @param {string} type Event type * @param {object} [eventObj] Event data * @returns {MouseEvent} Virtual mouse event. */ mouseEvent: function(type, eventObj) { var evt, e; e = util.extend({ bubbles: true, cancelable: (type !== 'mousemove'), view: window, wheelDelta: 0, detail: 0, screenX: 0, screenY: 0, clientX: 0, clientY: 0, ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, button: 0, relatedTarget: undefined // eslint-disable-line }, eventObj); if (typeof document.createEvent === 'function') { evt = document.createEvent('MouseEvents'); evt.initMouseEvent(type, e.bubbles, e.cancelable, e.view, e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.button, document.body.parentNode ); } else if (document.createEventObject) { evt = document.createEventObject(); util.forEach(e, function(value, propName) { evt[propName] = value; }, this); evt.button = {0: 1, 1: 4, 2: 2}[evt.button] || evt.button; } return evt; }, /** * Normalize mouse event's button attributes. * * Can detect which button is clicked by this method. * * Meaning of return numbers * * - 0: primary mouse button * - 1: wheel button or center button * - 2: secondary mouse button * @param {MouseEvent} mouseEvent - The mouse event object want to know. * @returns {number} - The value of meaning which button is clicked? */ getMouseButton: function(mouseEvent) { var button, primary = '0,1,3,5,7', secondary = '2,6', wheel = '4'; /* istanbul ignore else */ if (document.implementation.hasFeature('MouseEvents', '2.0')) { return mouseEvent.button; } button = String(mouseEvent.button); if (primary.indexOf(button) > -1) { return 0; } if (secondary.indexOf(button) > -1) { return 2; } if (~wheel.indexOf(button)) { return 1; } return -1; }, /** * Get target from event object * * @param {Event} event - The event object * @returns {object} - The event target object */ getEventTarget: function(event) { return event.target || event.srcElement; } }; module.exports = domevent;