UNPKG

interactjs

Version:

Drag and drop, resizing and multi-touch gestures with inertia and snapping for modern browsers (and also IE9+)

567 lines (456 loc) 22.3 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>src/Interaction.js - Documentation</title> <script src="scripts/prettify/prettify.js"></script> <script src="scripts/prettify/lang-css.js"></script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"> <link type="text/css" rel="stylesheet" href="styles/prettify.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> </head> <body> <input type="checkbox" id="nav-trigger" class="nav-trigger" /> <label for="nav-trigger" class="navicon-button x"> <div class="navicon"></div> </label> <label for="nav-trigger" class="overlay"></label> <nav> <li class="nav-link nav-home-link"><a href="index.html">Home</a></li><li class="nav-heading">Classes</li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="Interactable.html">Interactable</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interactable.html#actionChecker">actionChecker</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interactable.html#context">context</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interactable.html#deltaSource">deltaSource</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interactable.html#draggable">draggable</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interactable.html#dropzone">dropzone</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interactable.html#fire">fire</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interactable.html#gesturable">gesturable</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interactable.html#getRect">getRect</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interactable.html#off">off</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interactable.html#on">on</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interactable.html#origin">origin</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interactable.html#preventDefault">preventDefault</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interactable.html#rectChecker">rectChecker</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interactable.html#resizable">resizable</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interactable.html#set">set</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interactable.html#styleCursor">styleCursor</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interactable.html#unset">unset</a></span></li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="InteractEvent.html">InteractEvent</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="InteractEvent.html#stopImmediatePropagation">stopImmediatePropagation</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="InteractEvent.html#stopPropagation">stopPropagation</a></span></li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="Interaction.html">Interaction</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interaction.html#doMove">doMove</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interaction.html#end">end</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interaction.html#start">start</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="Interaction.html#stop">stop</a></span></li><li class="nav-heading"><span class="nav-item-type type-class">C</span><span class="nav-item-name"><a href="module.exports_module.exports.html">exports</a></span></li><li class="nav-heading">Modules</li><li class="nav-heading"><span class="nav-item-type type-module">M</span><span class="nav-item-name"><a href="module-interact.html">interact</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="module-interact.html#.debug">debug</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="module-interact.html#.dynamicDrop">dynamicDrop</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="module-interact.html#.isSet">isSet</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="module-interact.html#.maxInteractions">maxInteractions</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="module-interact.html#.off">off</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="module-interact.html#.on">on</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="module-interact.html#.pointerMoveTolerance">pointerMoveTolerance</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="module-interact.html#.stop">stop</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="module-interact.html#.supportsPointerEvent">supportsPointerEvent</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="module-interact.html#.supportsTouch">supportsTouch</a></span></li><li class="nav-heading"><a href="global.html">Globals</a></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#interact">interact</a></span></li> </nav> <div class="code-col-bg"></div> <div id="main"> <h1 class="page-title">src/Interaction.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>const scope = require('./scope'); const utils = require('./utils'); const events = require('./utils/events'); const browser = require('./utils/browser'); const domObjects = require('./utils/domObjects'); const finder = require('./utils/interactionFinder'); const signals = require('./utils/Signals').new(); const listeners = {}; const methodNames = [ 'pointerDown', 'pointerMove', 'pointerUp', 'updatePointer', 'removePointer', ]; // for ignoring browser's simulated mouse events let prevTouchTime = 0; // all active and idle interactions scope.interactions = []; class Interaction { /** */ constructor ({ pointerType }) { this.target = null; // current interactable being interacted with this.element = null; // the target element of the interactable this.prepared = { // action that's ready to be fired on next move event name : null, axis : null, edges: null, }; // keep track of added pointers this.pointers = []; this.pointerIds = []; this.downTargets = []; this.downTimes = []; // Previous native pointer move event coordinates this.prevCoords = { page : { x: 0, y: 0 }, client : { x: 0, y: 0 }, timeStamp: 0, }; // current native pointer move event coordinates this.curCoords = { page : { x: 0, y: 0 }, client : { x: 0, y: 0 }, timeStamp: 0, }; // Starting InteractEvent pointer coordinates this.startCoords = { page : { x: 0, y: 0 }, client : { x: 0, y: 0 }, timeStamp: 0, }; // Change in coordinates and time of the pointer this.pointerDelta = { page : { x: 0, y: 0, vx: 0, vy: 0, speed: 0 }, client : { x: 0, y: 0, vx: 0, vy: 0, speed: 0 }, timeStamp: 0, }; this.downEvent = null; // pointerdown/mousedown/touchstart event this.downPointer = {}; this._eventTarget = null; this._curEventTarget = null; this.prevEvent = null; // previous action event this.pointerIsDown = false; this.pointerWasMoved = false; this._interacting = false; this._ending = false; this.pointerType = pointerType; signals.fire('new', this); scope.interactions.push(this); } pointerDown (pointer, event, eventTarget) { const pointerIndex = this.updatePointer(pointer, event, true); signals.fire('down', { pointer, event, eventTarget, pointerIndex, interaction: this, }); } /** * ```js * interact(target) * .draggable({ * // disable the default drag start by down->move * manualStart: true * }) * // start dragging after the user holds the pointer down * .on('hold', function (event) { * var interaction = event.interaction; * * if (!interaction.interacting()) { * interaction.start({ name: 'drag' }, * event.interactable, * event.currentTarget); * } * }); * ``` * * Start an action with the given Interactable and Element as tartgets. The * action must be enabled for the target Interactable and an appropriate * number of pointers must be held down - 1 for drag/resize, 2 for gesture. * * Use it with `interactable.&lt;action>able({ manualStart: false })` to always * [start actions manually](https://github.com/taye/interact.js/issues/114) * * @param {object} action The action to be performed - drag, resize, etc. * @param {Interactable} target The Interactable to target * @param {Element} element The DOM Element to target * @return {object} interact */ start (action, target, element) { if (this.interacting() || !this.pointerIsDown || this.pointerIds.length &lt; (action.name === 'gesture'? 2 : 1)) { return; } // if this interaction had been removed after stopping // add it back if (scope.interactions.indexOf(this) === -1) { scope.interactions.push(this); } utils.copyAction(this.prepared, action); this.target = target; this.element = element; signals.fire('action-start', { interaction: this, event: this.downEvent, }); } pointerMove (pointer, event, eventTarget) { if (!this.simulation) { this.updatePointer(pointer); utils.setCoords(this.curCoords, this.pointers); } const duplicateMove = (this.curCoords.page.x === this.prevCoords.page.x &amp;&amp; this.curCoords.page.y === this.prevCoords.page.y &amp;&amp; this.curCoords.client.x === this.prevCoords.client.x &amp;&amp; this.curCoords.client.y === this.prevCoords.client.y); let dx; let dy; // register movement greater than pointerMoveTolerance if (this.pointerIsDown &amp;&amp; !this.pointerWasMoved) { dx = this.curCoords.client.x - this.startCoords.client.x; dy = this.curCoords.client.y - this.startCoords.client.y; this.pointerWasMoved = utils.hypot(dx, dy) > Interaction.pointerMoveTolerance; } const signalArg = { pointer, pointerIndex: this.getPointerIndex(pointer), event, eventTarget, dx, dy, duplicate: duplicateMove, interaction: this, interactingBeforeMove: this.interacting(), }; if (!duplicateMove) { // set pointer coordinate, time changes and speeds utils.setCoordDeltas(this.pointerDelta, this.prevCoords, this.curCoords); } signals.fire('move', signalArg); if (!duplicateMove) { // if interacting, fire an 'action-move' signal etc if (this.interacting()) { this.doMove(signalArg); } if (this.pointerWasMoved) { utils.copyCoords(this.prevCoords, this.curCoords); } } } /** * ```js * interact(target) * .draggable(true) * .on('dragmove', function (event) { * if (someCondition) { * // change the snap settings * event.interactable.draggable({ snap: { targets: [] }}); * // fire another move event with re-calculated snap * event.interaction.doMove(); * } * }); * ``` * * Force a move of the current action at the same coordinates. Useful if * snap/restrict has been changed and you want a movement with the new * settings. */ doMove (signalArg) { signalArg = utils.extend({ pointer: this.pointers[0], event: this.prevEvent, eventTarget: this._eventTarget, interaction: this, }, signalArg || {}); signals.fire('before-action-move', signalArg); if (!this._dontFireMove) { signals.fire('action-move', signalArg); } this._dontFireMove = false; } // End interact move events and stop auto-scroll unless simulation is running pointerUp (pointer, event, eventTarget, curEventTarget) { const pointerIndex = this.getPointerIndex(pointer); signals.fire(/cancel$/i.test(event.type)? 'cancel' : 'up', { pointer, pointerIndex, event, eventTarget, curEventTarget, interaction: this, }); if (!this.simulation) { this.end(event); } this.pointerIsDown = false; this.removePointer(pointer, event); } /** * ```js * interact(target) * .draggable(true) * .on('move', function (event) { * if (event.pageX > 1000) { * // end the current action * event.interaction.end(); * // stop all further listeners from being called * event.stopImmediatePropagation(); * } * }); * ``` * * Stop the current action and fire an end event. Inertial movement does * not happen. * * @param {PointerEvent} [event] */ end (event) { this._ending = true; event = event || this.prevEvent; if (this.interacting()) { signals.fire('action-end', { event, interaction: this, }); } this.stop(); this._ending = false; } currentAction () { return this._interacting? this.prepared.name: null; } interacting () { return this._interacting; } /** */ stop () { signals.fire('stop', { interaction: this }); if (this._interacting) { signals.fire('stop-active', { interaction: this }); signals.fire('stop-' + this.prepared.name, { interaction: this }); } this.target = this.element = null; this._interacting = false; this.prepared.name = this.prevEvent = null; } getPointerIndex (pointer) { // mouse and pen interactions may have only one pointer if (this.pointerType === 'mouse' || this.pointerType === 'pen') { return 0; } return this.pointerIds.indexOf(utils.getPointerId(pointer)); } updatePointer (pointer, event, down = event &amp;&amp; /(down|start)$/i.test(event.type)) { const id = utils.getPointerId(pointer); let index = this.getPointerIndex(pointer); if (index === -1) { index = this.pointerIds.length; this.pointerIds[index] = id; } if (down) { signals.fire('update-pointer-down', { pointer, event, down, pointerId: id, pointerIndex: index, interaction: this, }); } this.pointers[index] = pointer; return index; } removePointer (pointer, event) { const index = this.getPointerIndex(pointer); if (index === -1) { return; } signals.fire('remove-pointer', { pointer, event, pointerIndex: index, interaction: this, }); this.pointers .splice(index, 1); this.pointerIds .splice(index, 1); this.downTargets.splice(index, 1); this.downTimes .splice(index, 1); } _updateEventTargets (target, currentTarget) { this._eventTarget = target; this._curEventTarget = currentTarget; } } for (const method of methodNames) { listeners[method] = doOnInteractions(method); } function doOnInteractions (method) { return (function (event) { const pointerType = utils.getPointerType(event); const [eventTarget, curEventTarget] = utils.getEventTargets(event); const matches = []; // [ [pointer, interaction], ...] if (browser.supportsTouch &amp;&amp; /touch/.test(event.type)) { prevTouchTime = new Date().getTime(); for (const changedTouch of event.changedTouches) { const pointer = changedTouch; const interaction = finder.search(pointer, event.type, eventTarget); matches.push([pointer, interaction || new Interaction({ pointerType })]); } } else { let invalidPointer = false; if (!browser.supportsPointerEvent &amp;&amp; /mouse/.test(event.type)) { // ignore mouse events while touch interactions are active for (let i = 0; i &lt; scope.interactions.length &amp;&amp; !invalidPointer; i++) { invalidPointer = scope.interactions[i].pointerType !== 'mouse' &amp;&amp; scope.interactions[i].pointerIsDown; } // try to ignore mouse events that are simulated by the browser // after a touch event invalidPointer = invalidPointer || (new Date().getTime() - prevTouchTime &lt; 500) // on iOS and Firefox Mobile, MouseEvent.timeStamp is zero if simulated || event.timeStamp === 0; } if (!invalidPointer) { let interaction = finder.search(event, event.type, eventTarget); if (!interaction) { interaction = new Interaction({ pointerType }); } matches.push([event, interaction]); } } for (const [pointer, interaction] of matches) { interaction._updateEventTargets(eventTarget, curEventTarget); interaction[method](pointer, event, eventTarget, curEventTarget); } }); } function endAll (event) { for (const interaction of scope.interactions) { interaction.end(event); signals.fire('endall', { event, interaction }); } } const docEvents = { /* 'eventType': listenerFunc */ }; const pEventTypes = browser.pEventTypes; if (domObjects.PointerEvent) { docEvents[pEventTypes.down ] = listeners.pointerDown; docEvents[pEventTypes.move ] = listeners.pointerMove; docEvents[pEventTypes.up ] = listeners.pointerUp; docEvents[pEventTypes.cancel] = listeners.pointerUp; } else { docEvents.mousedown = listeners.pointerDown; docEvents.mousemove = listeners.pointerMove; docEvents.mouseup = listeners.pointerUp; docEvents.touchstart = listeners.pointerDown; docEvents.touchmove = listeners.pointerMove; docEvents.touchend = listeners.pointerUp; docEvents.touchcancel = listeners.pointerUp; } docEvents.blur = endAll; function onDocSignal ({ doc }, signalName) { const eventMethod = signalName.indexOf('add') === 0 ? events.add : events.remove; // delegate event listener for (const eventType in scope.delegatedEvents) { eventMethod(doc, eventType, events.delegateListener); eventMethod(doc, eventType, events.delegateUseCapture, true); } for (const eventType in docEvents) { eventMethod(doc, eventType, docEvents[eventType], browser.isIOS ? { passive: false } : undefined); } } signals.on('update-pointer-down', ({ interaction, pointer, pointerId, pointerIndex, event, eventTarget, down }) => { interaction.pointerIds[pointerIndex] = pointerId; interaction.pointers[pointerIndex] = pointer; if (down) { interaction.pointerIsDown = true; } if (!interaction.interacting()) { utils.setCoords(interaction.startCoords, interaction.pointers); utils.copyCoords(interaction.curCoords , interaction.startCoords); utils.copyCoords(interaction.prevCoords, interaction.startCoords); interaction.downEvent = event; interaction.downTimes[pointerIndex] = interaction.curCoords.timeStamp; interaction.downTargets[pointerIndex] = eventTarget || event &amp;&amp; utils.getEventTargets(event)[0]; interaction.pointerWasMoved = false; utils.pointerExtend(interaction.downPointer, pointer); } }); scope.signals.on('add-document' , onDocSignal); scope.signals.on('remove-document', onDocSignal); Interaction.pointerMoveTolerance = 1; Interaction.doOnInteractions = doOnInteractions; Interaction.endAll = endAll; Interaction.signals = signals; Interaction.docEvents = docEvents; scope.endAllInteractions = endAll; module.exports = Interaction; </code></pre> </article> </section> </div> <script>prettyPrint();</script> <script src="scripts/linenumber.js"></script> </body> </html>