UNPKG

interactjs

Version:

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

512 lines (419 loc) 14.4 kB
const scope = require('./scope'); const utils = require('./utils'); const events = require('./utils/events'); const browser = require('./utils/browser'); 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 () { 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.mouse = false; 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, }); } /*\ * Interaction.start [ method ] * * 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.<action>able({ manualStart: false })` to always * [start actions manually](https://github.com/taye/interact.js/issues/114) * - action (object) The action to be performed - drag, resize, etc. - target (Interactable) The Interactable to target - element (Element) The DOM Element to target = (object) interact ** | 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 (action, target, element) { if (this.interacting() || !this.pointerIsDown || this.pointerIds.length < (action.name === 'gesture'? 2 : 1)) { return; } // if this interaction had been removed after stopping // add it back if (utils.indexOf(scope.interactions, 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 && this.curCoords.page.y === this.prevCoords.page.y && this.curCoords.client.x === this.prevCoords.client.x && this.curCoords.client.y === this.prevCoords.client.y); let dx; let dy; // register movement greater than pointerMoveTolerance if (this.pointerIsDown && !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); } } } /*\ * Interaction.doMove [ method ] * * 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. * ** | 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(); | } | }); \*/ 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); } /*\ * Interaction.end [ method ] * * Stop the current action and fire an end event. Inertial movement does * not happen. * - event (PointerEvent) #optional ** | 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(); | } | }); \*/ end (event) { event = event || this.prevEvent; if (this.interacting()) { signals.fire('action-end', { event, interaction: this, }); } this.stop(); } 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) { return utils.indexOf(this.pointerIds, utils.getPointerId(pointer)); } updatePointer (pointer, event, down = event && /(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 id = utils.getPointerId(pointer); const index = this.mouse? 0 : utils.indexOf(this.pointerIds, id); 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 (let i = 0, len = methodNames.length; i < len; i++) { const method = methodNames[i]; listeners[method] = doOnInteractions(method); } function doOnInteractions (method) { return (function (event) { const [eventTarget, curEventTarget] = utils.getEventTargets(event); const matches = []; // [ [pointer, interaction], ...] if (browser.supportsTouch && /touch/.test(event.type)) { prevTouchTime = new Date().getTime(); for (let i = 0; i < event.changedTouches.length; i++) { const pointer = event.changedTouches[i]; const interaction = finder.search(pointer, event.type, eventTarget); matches.push([pointer, interaction || new Interaction()]); } } else { let invalidPointer = false; if (!browser.supportsPointerEvent && /mouse/.test(event.type)) { // ignore mouse events while touch interactions are active for (let i = 0; i < scope.interactions.length && !invalidPointer; i++) { invalidPointer = !scope.interactions[i].mouse && 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 < 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(); interaction.mouse = (/mouse/i.test(event.pointerType || event.type) // MSPointerEvent.MSPOINTER_TYPE_MOUSE || event.pointerType === 4 || !event.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 (let i = 0; i < scope.interactions.length; i++) { const interaction = scope.interactions[i]; interaction.end(event); signals.fire('endall', { event, interaction }); } } const docEvents = { /* 'eventType': listenerFunc */ }; const pEventTypes = browser.pEventTypes; if (scope.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]); } } 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 && 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;