src/event/trigger.js
/**
* @module trigger
*/
import { win, each } from '../util';
import { contains } from '../dom/contains';
const reMouseEvent = /^(mouse(down|up|over|out|enter|leave|move)|contextmenu|(dbl)?click)$/;
const reKeyEvent = /^key(down|press|up)$/;
/**
* Trigger event at element(s)
*
* @param {String} type Type of the event
* @param {Object} data Data to be sent with the event (`params.detail` will be set to this).
* @param {Object} [params] Event parameters (optional)
* @param {Boolean} params.bubbles=true Does the event bubble up through the DOM or not.
* @param {Boolean} params.cancelable=true Is the event cancelable or not.
* @param {Mixed} params.detail=undefined Additional information about the event.
* @return {Object} The wrapped collection
* @chainable
* @example
* $('.item').trigger('anyEventType');
*/
export const trigger = function(type, data, {bubbles = true, cancelable = true, preventDefault = false} = {}) {
const EventConstructor = getEventConstructor(type);
const event = new EventConstructor(type, {
bubbles,
cancelable,
preventDefault,
detail: data
});
event._preventDefault = preventDefault;
return each(this, element => {
if(!bubbles || isEventBubblingInDetachedTree || isAttachedToDocument(element)) {
dispatchEvent(element, event);
} else {
triggerForPath(element, type, {
bubbles,
cancelable,
preventDefault,
detail: data
});
}
});
};
const getEventConstructor = type => isSupportsOtherEventConstructors ? (reMouseEvent.test(type) ? MouseEvent : (reKeyEvent.test(type) ? KeyboardEvent : CustomEvent)) : CustomEvent;
/**
* Trigger event at first element in the collection. Similar to `trigger()`, except:
*
* - Event does not bubble
* - Default event behavior is prevented
* - Only triggers handler for first matching element
*
* @param {String} type Type of the event
* @param {Object} data Data to be sent with the event
* @example
* $('form').triggerHandler('submit');
*/
export const triggerHandler = function(type, data) {
if(this[0]) {
trigger.call(this[0], type, data, {
bubbles: false,
preventDefault: true
});
}
};
/**
* Check whether the element is attached to or detached from) the document
*
* @private
* @param {Node} element Element to test
* @return {Boolean}
*/
const isAttachedToDocument = element => {
if(element === window || element === document) {
return true;
}
return contains(element.ownerDocument.documentElement, element);
};
/**
* Dispatch the event at the element and its ancestors.
* Required to support delegated events in browsers that don't bubble events in detached DOM trees.
*
* @private
* @param {Node} element First element to dispatch the event at
* @param {String} type Type of the event
* @param {Object} [params] Event parameters (optional)
* @param {Boolean} params.bubbles=true Does the event bubble up through the DOM or not.
* Will be set to false (but shouldn't matter since events don't bubble anyway).
* @param {Boolean} params.cancelable=true Is the event cancelable or not.
* @param {Mixed} params.detail=undefined Additional information about the event.
*/
const triggerForPath = (element, type, params = {}) => {
params.bubbles = false;
const event = new CustomEvent(type, params);
event._target = element;
do {
dispatchEvent(element, event);
} while(element = element.parentNode); // eslint-disable-line no-cond-assign
};
/**
* Dispatch event to element, but call direct event methods instead if available
* (e.g. "blur()", "submit()") and if the event is non-cancelable.
*
* @private
* @param {Node} element Element to dispatch the event at
* @param {Object} event Event to dispatch
*/
const directEventMethods = ['blur', 'focus', 'select', 'submit'];
const dispatchEvent = (element, event) => {
if(directEventMethods.indexOf(event.type) !== -1 && typeof element[event.type] === 'function' && !event._preventDefault && !event.cancelable) {
element[event.type]();
} else {
element.dispatchEvent(event);
}
};
/**
* Polyfill for CustomEvent, borrowed from [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill).
* Needed to support IE (9, 10, 11) & PhantomJS
*/
(() => {
const CustomEvent = function(event, params = {
bubbles: false,
cancelable: false,
detail: undefined
}) {
let customEvent = document.createEvent('CustomEvent');
customEvent.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
return customEvent;
};
CustomEvent.prototype = win.CustomEvent && win.CustomEvent.prototype;
win.CustomEvent = CustomEvent;
})();
/*
* Are events bubbling in detached DOM trees?
* @private
*/
const isEventBubblingInDetachedTree = (() =>{
let isBubbling = false;
const doc = win.document;
if(doc) {
const parent = doc.createElement('div');
const child = parent.cloneNode();
parent.appendChild(child);
parent.addEventListener('e', function() {
isBubbling = true;
});
child.dispatchEvent(new CustomEvent('e', {bubbles: true}));
}
return isBubbling;
})();
const isSupportsOtherEventConstructors = (() => {
try {
new MouseEvent('click');
} catch(e) {
return false;
}
return true;
})();