/**
* DOM event utility module.
* @fileoverview Module for handle DOM events
* @author NHN Ent. FE Development Lab <dl_javascript@nhnent.com>
*/
import {getRect} from './domutil';
import util from 'tui-code-snippet';
const EVENT_KEY = '_feEventKey';
/**
* @module
* @ignore
*/
/**
* Get event collection for specific HTML element
* @param {HTMLElement} element - HTML element
* @param {string} [type] - event type
* @returns {(object|Map)}
*/
function safeEvent(element, type) {
let events = element[EVENT_KEY];
if (!events) {
events = element[EVENT_KEY] = {};
}
if (type) {
let handlerMap = events[type];
if (!handlerMap) {
handlerMap = events[type] = new util.Map();
}
events = handlerMap;
}
return events;
}
/**
* Memorize DOM event handler for unbinding
* @param {HTMLElement} element - element to bind events
* @param {string} type - events name
* @param {function} keyFn - handler function that user passed at on() use
* @param {function} valueFn - handler function that wrapped by domevent for
* implementing some features
*/
function memorizeHandler(element, type, keyFn, valueFn) {
const map = safeEvent(element, type);
let items = map.get(keyFn);
if (items) {
items.push(valueFn);
} else {
items = [valueFn];
map.set(keyFn, items);
}
}
/**
* Forget memorized DOM event handlers
* @param {HTMLElement} element - element to bind events
* @param {string} type - events name
* @param {function} keyFn - handler function that user passed at on() use
*/
function forgetHandler(element, type, keyFn) {
safeEvent(element, type).delete(keyFn);
}
/**
* Bind DOM events
* @param {HTMLElement} element - element to bind events
* @param {string} type - events name
* @param {function} handler - handler function or context for handler
* method
* @param {object} [context] context - context for handler method.
*/
function bindEvent(element, type, handler, context) {
/**
* Event handler
* @param {Event} e - event object
*/
function eventHandler(e) {
handler.call(context || element, e || window.event);
}
/**
* Event handler for normalize mouseenter event
* @param {MouseEvent} e - event object
*/
function mouseEnterHandler(e) {
e = e || window.event;
if (checkMouse(element, e)) {
eventHandler(e);
}
}
if ('addEventListener' in element) {
if (type === 'mouseenter' || type === 'mouseleave') {
type = (type === 'mouseenter') ? 'mouseover' : 'mouseout';
element.addEventListener(type, mouseEnterHandler);
memorizeHandler(element, type, handler, mouseEnterHandler);
} else {
element.addEventListener(type, eventHandler);
memorizeHandler(element, type, handler, eventHandler);
}
} else if ('attachEvent' in element) {
element.attachEvent(`on${type}`, eventHandler);
memorizeHandler(element, type, handler, eventHandler);
}
}
/**
* Unbind DOM events
* @param {HTMLElement} element - element to unbind events
* @param {string} type - events name
* @param {function} handler - handler function or context for handler
* method
*/
function unbindEvent(element, type, handler) {
const events = safeEvent(element, type);
const items = events.get(handler);
if (!items) {
return;
}
forgetHandler(element, type, handler);
util.forEach(items, func => {
if ('removeEventListener' in element) {
element.removeEventListener(type, func);
} else if ('detachEvent' in element) {
element.detachEvent(`on${type}`, func);
}
});
}
/**
* Bind DOM events
* @param {HTMLElement} element - element to bind events
* @param {(string|object)} types - Space splitted events names or
* eventName:handler object
* @param {(function|object)} handler - handler function or context for handler
* method
* @param {object} [context] context - context for handler method.
* @name on
* @memberof tui.dom
* @function
*/
export function on(element, types, handler, context) {
if (util.isString(types)) {
util.forEach(types.split(/\s+/g), type => {
bindEvent(element, type, handler, context);
});
return;
}
util.forEach(types, (func, type) => {
bindEvent(element, type, func, handler);
});
}
/**
* Bind DOM event. this event will unbind after invokes.
* @param {HTMLElement} element - HTMLElement to bind events.
* @param {(string|object)} types - Space splitted events names or
* eventName:handler object.
* @param {*} handler - handler function or context for handler method.
* @param {*} [context] - context object for handler method.
* @name once
* @memberof tui.dom
* @function
*/
export function once(element, types, handler, context) {
if (util.isObject(types)) {
for (const [fn, type] of types) {
once(element, type, fn, handler);
}
return;
}
const onceHandler = (...args) => {
handler.apply(context || element, args);
off(element, types, onceHandler, context);
};
on(element, types, onceHandler, context);
}
/**
* Unbind DOM events
* @param {HTMLElement} element - element to unbindbind events
* @param {(string|object)} types - Space splitted events names or
* eventName:handler object
* @param {(function|object)} handler - handler function or context for handler
* method
* @name off
* @memberof tui.dom
* @function
*/
export function off(element, types, handler) {
if (util.isString(types)) {
util.forEach(types.split(/\s+/g), type => {
unbindEvent(element, type, handler);
});
return;
}
util.forEach(types, (func, type) => {
unbindEvent(element, type, func);
});
}
/**
* Check mouse was leave event element with ignoreing child nodes
* @param {HTMLElement} element - element to check
* @param {MouseEvent} e - mouse event
* @returns {boolean} whether mouse leave element?
* @name checkMouse
* @memberof tui.dom
* @function
*/
export function checkMouse(element, e) {
let related = e.relatedTarget;
if (!related) {
return true;
}
try {
while (related && (related !== element)) {
related = related.parentNode;
}
} catch (err) {
return false;
}
return (related !== element);
}
const primaryButton = ['0', '1', '3', '5', '7'];
const secondaryButton = ['2', '6'];
const wheelButton = ['4'];
const isStandardMouseEvent = !_isIE8AndEarlier();
/**
* test if browser is IE8 and earlier(IE6 or IE7)
* @returns {boolean} - whether browser is IE6 ~ 8 or not
* @private
*/
export function _isIE8AndEarlier() {
return (navigator.userAgent.indexOf('msie 8') > -1)
|| (navigator.userAgent.indexOf('msie 7') > -1)
|| (navigator.userAgent.indexOf('msie 6') > -1);
}
/**
* 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?
* @name getMouseButton
* @memberof tui.dom
* @function
*/
export function getMouseButton(mouseEvent) {
if (isStandardMouseEvent) {
return mouseEvent.button;
}
return _getMouseButtonIE8AndEarlier(mouseEvent);
}
/**
* Normalize return value of mouseEvent.button
* Make same to standard MouseEvent's button value
* @param {DispCEventObj} mouseEvent - mouse event object
* @returns {number|null} - id indicating which mouse button is clicked
* @private
*/
export function _getMouseButtonIE8AndEarlier(mouseEvent) {
const button = String(mouseEvent.button);
if (util.inArray(button, primaryButton) > -1) {
return 0;
} else if (util.inArray(button, secondaryButton) > -1) {
return 2;
} else if (util.inArray(button, wheelButton) > -1) {
return 1;
}
return null;
}
/**
* Get mouse position from mouse event
*
* If supplied relatveElement parameter then return relative position based on
* element
* @param {(MouseEvent|object|number[])} position - mouse position object
* @param {HTMLElement} relativeElement HTML element that calculate relative
* position
* @returns {number[]} mouse position
* @name getMousePosition
* @memberof tui.dom
* @function
*/
export function getMousePosition(position, relativeElement) {
const isArray = util.isArray(position);
const clientX = isArray ? position[0] : position.clientX;
const clientY = isArray ? position[1] : position.clientY;
if (!relativeElement) {
return [clientX, clientY];
}
const rect = getRect(relativeElement);
return [
clientX - rect.left - relativeElement.clientLeft,
clientY - rect.top - relativeElement.clientTop
];
}