UNPKG

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
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, };