interactjs
Version:
Drag and drop, resizing and multi-touch gestures with inertia and snapping for modern browsers (and also IE8+)
361 lines (288 loc) • 9.89 kB
JavaScript
const is = require('./is');
const domUtils = require('./domUtils');
const pExtend = require('./pointerExtend');
const { window, getWindow } = require('./window');
const { indexOf, contains } = require('./arr');
const useAttachEvent = ('attachEvent' in window) && !('addEventListener' in window);
const addEvent = useAttachEvent? 'attachEvent': 'addEventListener';
const removeEvent = useAttachEvent? 'detachEvent': 'removeEventListener';
const on = useAttachEvent? 'on': '';
const elements = [];
const targets = [];
const attachedListeners = [];
// {
// type: {
// selectors: ['selector', ...],
// contexts : [document, ...],
// listeners: [[listener, capture, passive], ...]
// }
// }
const delegatedEvents = {};
const documents = [];
const supportsOptions = !useAttachEvent && (() => {
let supported = false;
window.document.createElement('div').addEventListener('test', null, {
get capture () { supported = true; },
});
return supported;
})();
function add (element, type, listener, optionalArg) {
const options = getOptions(optionalArg);
let elementIndex = indexOf(elements, element);
let target = targets[elementIndex];
if (!target) {
target = {
events: {},
typeCount: 0,
};
elementIndex = elements.push(element) - 1;
targets.push(target);
attachedListeners.push(useAttachEvent
? {
supplied: [],
wrapped : [],
useCount: [],
}
: null);
}
if (!target.events[type]) {
target.events[type] = [];
target.typeCount++;
}
if (!contains(target.events[type], listener)) {
let ret;
if (useAttachEvent) {
const { supplied, wrapped, useCount } = attachedListeners[elementIndex];
const listenerIndex = indexOf(supplied, listener);
const wrappedListener = wrapped[listenerIndex] || function (event) {
if (!event.immediatePropagationStopped) {
event.target = event.srcElement;
event.currentTarget = element;
event.preventDefault = event.preventDefault || preventDef;
event.stopPropagation = event.stopPropagation || stopProp;
event.stopImmediatePropagation = event.stopImmediatePropagation || stopImmProp;
if (/mouse|click/.test(event.type)) {
event.pageX = event.clientX + getWindow(element).document.documentElement.scrollLeft;
event.pageY = event.clientY + getWindow(element).document.documentElement.scrollTop;
}
listener(event);
}
};
ret = element[addEvent](on + type, wrappedListener, !!options.capture);
if (listenerIndex === -1) {
supplied.push(listener);
wrapped.push(wrappedListener);
useCount.push(1);
}
else {
useCount[listenerIndex]++;
}
}
else {
ret = element[addEvent](type, listener, supportsOptions? options : !!options.capture);
}
target.events[type].push(listener);
return ret;
}
}
function remove (element, type, listener, optionalArg) {
const options = getOptions(optionalArg);
const elementIndex = indexOf(elements, element);
const target = targets[elementIndex];
if (!target || !target.events) {
return;
}
let wrappedListener = listener;
let listeners;
let listenerIndex;
if (useAttachEvent) {
listeners = attachedListeners[elementIndex];
listenerIndex = indexOf(listeners.supplied, listener);
wrappedListener = listeners.wrapped[listenerIndex];
}
if (type === 'all') {
for (type in target.events) {
if (target.events.hasOwnProperty(type)) {
remove(element, type, 'all');
}
}
return;
}
if (target.events[type]) {
const len = target.events[type].length;
if (listener === 'all') {
for (let i = 0; i < len; i++) {
remove(element, type, target.events[type][i], options);
}
return;
}
else {
for (let i = 0; i < len; i++) {
if (target.events[type][i] === listener) {
element[removeEvent](on + type, wrappedListener, supportsOptions? options : !!options.capture);
target.events[type].splice(i, 1);
if (useAttachEvent && listeners) {
listeners.useCount[listenerIndex]--;
if (listeners.useCount[listenerIndex] === 0) {
listeners.supplied.splice(listenerIndex, 1);
listeners.wrapped.splice(listenerIndex, 1);
listeners.useCount.splice(listenerIndex, 1);
}
}
break;
}
}
}
if (target.events[type] && target.events[type].length === 0) {
target.events[type] = null;
target.typeCount--;
}
}
if (!target.typeCount) {
targets.splice(elementIndex, 1);
elements.splice(elementIndex, 1);
attachedListeners.splice(elementIndex, 1);
}
}
function addDelegate (selector, context, type, listener, optionalArg) {
const options = getOptions(optionalArg);
if (!delegatedEvents[type]) {
delegatedEvents[type] = {
selectors: [],
contexts : [],
listeners: [],
};
// add delegate listener functions
for (let i = 0; i < documents.length; i++) {
add(documents[i], type, delegateListener);
add(documents[i], type, delegateUseCapture, true);
}
}
const delegated = delegatedEvents[type];
let index;
for (index = delegated.selectors.length - 1; index >= 0; index--) {
if (delegated.selectors[index] === selector
&& delegated.contexts[index] === context) {
break;
}
}
if (index === -1) {
index = delegated.selectors.length;
delegated.selectors.push(selector);
delegated.contexts .push(context);
delegated.listeners.push([]);
}
// keep listener and capture and passive flags
delegated.listeners[index].push([listener, !!options.capture, options.passive]);
}
function removeDelegate (selector, context, type, listener, optionalArg) {
const options = getOptions(optionalArg);
const delegated = delegatedEvents[type];
let matchFound = false;
let index;
if (!delegated) { return; }
// count from last index of delegated to 0
for (index = delegated.selectors.length - 1; index >= 0; index--) {
// look for matching selector and context Node
if (delegated.selectors[index] === selector
&& delegated.contexts[index] === context) {
const listeners = delegated.listeners[index];
// each item of the listeners array is an array: [function, capture, passive]
for (let i = listeners.length - 1; i >= 0; i--) {
const [fn, capture, passive] = listeners[i];
// check if the listener functions and capture and passive flags match
if (fn === listener && capture === !!options.capture && passive === options.passive) {
// remove the listener from the array of listeners
listeners.splice(i, 1);
// if all listeners for this interactable have been removed
// remove the interactable from the delegated arrays
if (!listeners.length) {
delegated.selectors.splice(index, 1);
delegated.contexts .splice(index, 1);
delegated.listeners.splice(index, 1);
// remove delegate function from context
remove(context, type, delegateListener);
remove(context, type, delegateUseCapture, true);
// remove the arrays if they are empty
if (!delegated.selectors.length) {
delegatedEvents[type] = null;
}
}
// only remove one listener
matchFound = true;
break;
}
}
if (matchFound) { break; }
}
}
}
// bound to the interactable context when a DOM event
// listener is added to a selector interactable
function delegateListener (event, optionalArg) {
const options = getOptions(optionalArg);
const fakeEvent = {};
const delegated = delegatedEvents[event.type];
const eventTarget = (domUtils.getActualElement(event.path
? event.path[0]
: event.target));
let element = eventTarget;
// duplicate the event so that currentTarget can be changed
pExtend(fakeEvent, event);
fakeEvent.originalEvent = event;
fakeEvent.preventDefault = preventOriginalDefault;
// climb up document tree looking for selector matches
while (is.element(element)) {
for (let i = 0; i < delegated.selectors.length; i++) {
const selector = delegated.selectors[i];
const context = delegated.contexts[i];
if (domUtils.matchesSelector(element, selector)
&& domUtils.nodeContains(context, eventTarget)
&& domUtils.nodeContains(context, element)) {
const listeners = delegated.listeners[i];
fakeEvent.currentTarget = element;
for (let j = 0; j < listeners.length; j++) {
const [fn, capture, passive] = listeners[j];
if (capture === !!options.capture && passive === options.passive) {
fn(fakeEvent);
}
}
}
}
element = domUtils.parentNode(element);
}
}
function delegateUseCapture (event) {
return delegateListener.call(this, event, true);
}
function preventDef () {
this.returnValue = false;
}
function preventOriginalDefault () {
this.originalEvent.preventDefault();
}
function stopProp () {
this.cancelBubble = true;
}
function stopImmProp () {
this.cancelBubble = true;
this.immediatePropagationStopped = true;
}
function getOptions (param) {
return is.object(param)? param : { capture: param };
}
module.exports = {
add,
remove,
addDelegate,
removeDelegate,
delegateListener,
delegateUseCapture,
delegatedEvents,
documents,
useAttachEvent,
supportsOptions,
_elements: elements,
_targets: targets,
_attachedListeners: attachedListeners,
};