UNPKG

interact-js

Version:

A small interaction event unifiying library

447 lines (369 loc) 12.8 kB
var interactions = [], minMoveDistance = 5, interact, maximumMovesToPersist = 1000, // Should be plenty.. propertiesToCopy = 'target,pageX,pageY,clientX,clientY,offsetX,offsetY,screenX,screenY,shiftKey,x,y'.split(','), // Stuff that will be on every interaction. d = typeof document !== 'undefined' ? document : null; function Interact(){ this._elements = []; } Interact.prototype.on = function(eventName, target, callback){ if(!target){ return; } target._interactEvents = target._interactEvents || {}; target._interactEvents[eventName] = target._interactEvents[eventName] || [] target._interactEvents[eventName].push({ callback: callback, interact: this }); return this; }; Interact.prototype.emit = function(eventName, target, event, interaction){ if(!target){ return; } var interact = this, currentTarget = target; interaction.originalEvent = event; interaction.preventDefault = function(){ event.preventDefault(); } interaction.stopPropagation = function(){ event.stopPropagation(); } while(currentTarget){ currentTarget._interactEvents && currentTarget._interactEvents[eventName] && currentTarget._interactEvents[eventName].forEach(function(listenerInfo){ if(listenerInfo.interact === interact){ listenerInfo.callback.call(interaction, interaction); } }); currentTarget = currentTarget.parentNode; } return this; }; Interact.prototype.off = Interact.prototype.removeListener = function(eventName, target, callback){ if(!target || !target._interactEvents || !target._interactEvents[eventName]){ return; } var interactListeners = target._interactEvents[eventName], listenerInfo; for(var i = 0; i < interactListeners.length; i++) { listenerInfo = interactListeners[i]; if(listenerInfo.interact === interact && listenerInfo.callback === callback){ interactListeners.splice(i,1); i--; } } return this; }; interact = new Interact(); // For some reason touch browsers never change the event target during a touch. // This is, lets face it, fucking stupid. function getActualTarget() { var scrollX = window.scrollX, scrollY = window.scrollY; // IE is stupid and doesn't support scrollX/Y if(scrollX === undefined){ scrollX = d.body.scrollLeft; scrollY = d.body.scrollTop; } return d.elementFromPoint(this.pageX - window.scrollX, this.pageY - window.scrollY); } function getMoveDistance(x1,y1,x2,y2){ var adj = Math.abs(x1 - x2), opp = Math.abs(y1 - y2); return Math.sqrt(Math.pow(adj,2) + Math.pow(opp,2)); } function destroyInteraction(interaction){ for(var i = 0; i < interactions.length; i++){ if(interactions[i].identifier === interaction.identifier){ interactions.splice(i,1); } } } function getInteraction(identifier){ for(var i = 0; i < interactions.length; i++){ if(interactions[i].identifier === identifier){ return interactions[i]; } } } function setInheritedData(interaction, data){ for(var i = 0; i < propertiesToCopy.length; i++) { interaction[propertiesToCopy[i]] = data[propertiesToCopy[i]] } } function getAngle(deltaPoint){ return Math.atan2(deltaPoint.x, -deltaPoint.y) * 180 / Math.PI; } function Interaction(event, interactionInfo){ // If there is no event (eg: desktop) just make the identifier undefined if(!event){ event = {}; } // If there is no extra info about the interaction (eg: desktop) just use the event itself if(!interactionInfo){ interactionInfo = event; } // If there is another interaction with the same ID, something went wrong. // KILL IT WITH FIRE! var oldInteraction = getInteraction(interactionInfo.identifier); oldInteraction && oldInteraction.destroy(); this.identifier = interactionInfo.identifier; this.moves = []; interactions.push(this); } Interaction.prototype = { constructor: Interaction, getActualTarget: getActualTarget, destroy: function(){ interact.on('destroy', this.target, this, this); destroyInteraction(this); }, start: function(event, interactionInfo){ // If there is no extra info about the interaction (eg: desktop) just use the event itself if(!interactionInfo){ interactionInfo = event; } var lastStart = { time: new Date(), phase: 'start' }; setInheritedData(lastStart, interactionInfo); this.lastStart = lastStart; setInheritedData(this, interactionInfo); this.phase = 'start'; interact.emit('start', event.target, event, this); return this; }, move: function(event, interactionInfo){ // If there is no extra info about the interaction (eg: desktop) just use the event itself if(!interactionInfo){ interactionInfo = event; } var currentTouch = { time: new Date(), phase: 'move' }; setInheritedData(currentTouch, interactionInfo); // Update the interaction setInheritedData(this, interactionInfo); this.moves.push(currentTouch); // Memory saver, culls any moves that are over the maximum to keep. this.moves = this.moves.slice(-maximumMovesToPersist); var moveDelta = this.getMoveDelta(), angle = 0; if(moveDelta){ angle = getAngle(moveDelta); } this.angle = currentTouch.angle = angle; this.phase = 'move'; interact.emit('move', event.target, event, this); return this; }, drag: function(event, interactionInfo){ // If there is no extra info about the interaction (eg: desktop) just use the event itself if(!interactionInfo){ interactionInfo = event; } var currentTouch = { time: new Date(), phase: 'drag' }; setInheritedData(currentTouch, interactionInfo); // Update the interaction setInheritedData(this, interactionInfo); if(!this.moves){ this.moves = []; } this.moves.push(currentTouch); // Memory saver, culls any moves that are over the maximum to keep. this.moves = this.moves.slice(-maximumMovesToPersist); if(!this.dragStarted && getMoveDistance(this.lastStart.pageX, this.lastStart.pageY, currentTouch.pageX, currentTouch.pageY) > minMoveDistance){ this.dragStarted = true; } var moveDelta = this.getMoveDelta(), angle = 0; if(moveDelta){ angle = getAngle(moveDelta); } this.angle = currentTouch.angle = angle; if(this.dragStarted){ this.phase = 'drag'; interact.emit('drag', event.target, event, this); } return this; }, end: function(event, interactionInfo){ if(!interactionInfo){ interactionInfo = event; } // Update the interaction setInheritedData(this, interactionInfo); if(!this.moves){ this.moves = []; } // Update the interaction setInheritedData(this, interactionInfo); this.phase = 'end'; interact.emit('end', event.target, event, this); return this; }, cancel: function(event, interactionInfo){ if(!interactionInfo){ interactionInfo = event; } // Update the interaction setInheritedData(this, interactionInfo); this.phase = 'cancel'; interact.emit('cancel', event.target, event, this); return this; }, getMoveDistance: function(){ if(this.moves.length > 1){ var current = this.moves[this.moves.length-1], previous = this.moves[this.moves.length-2]; return getMoveDistance(current.pageX, current.pageY, previous.pageX, previous.pageY); } }, getMoveDelta: function(){ var current = this.moves[this.moves.length-1], previous = this.moves[this.moves.length-2] || this.lastStart; if(!current || !previous){ return; } return { x: current.pageX - previous.pageX, y: current.pageY - previous.pageY }; }, getSpeed: function(){ if(this.moves.length > 1){ var current = this.moves[this.moves.length-1], previous = this.moves[this.moves.length-2]; return this.getMoveDistance() / (current.time - previous.time); } return 0; }, getCurrentAngle: function(blend){ var phase = this.phase, currentPosition, lastAngle, i = this.moves.length-1, angle, firstAngle, angles = [], blendSteps = 20/(this.getSpeed()*2+1), stepsUsed = 1; if(this.moves && this.moves.length){ currentPosition = this.moves[i]; angle = firstAngle = currentPosition.angle; if(blend && this.moves.length > 1){ while( --i > 0 && this.moves.length - i < blendSteps && this.moves[i].phase === phase ){ lastAngle = this.moves[i].angle; if(Math.abs(lastAngle - firstAngle) > 180){ angle -= lastAngle; }else{ angle += lastAngle; } stepsUsed++; } angle = angle/stepsUsed; } } if(angle === Infinity){ return firstAngle; } return angle; }, getAllInteractions: function(){ return interactions.slice(); } }; function start(event){ var touch; for(var i = 0; i < event.changedTouches.length; i++){ touch = event.changedTouches[i]; new Interaction(event, event.changedTouches[i]).start(event, touch); } } function drag(event){ var touch; for(var i = 0; i < event.changedTouches.length; i++){ touch = event.changedTouches[i]; getInteraction(touch.identifier).drag(event, touch); } } function end(event){ var touch; for(var i = 0; i < event.changedTouches.length; i++){ touch = event.changedTouches[i]; getInteraction(touch.identifier).end(event, touch).destroy(); } } function cancel(event){ var touch; for(var i = 0; i < event.changedTouches.length; i++){ touch = event.changedTouches[i]; getInteraction(touch.identifier).cancel(event, touch).destroy(); } } addEvent(d, 'touchstart', start); addEvent(d, 'touchmove', drag); addEvent(d, 'touchend', end); addEvent(d, 'touchcancel', cancel); var mouseIsDown = false; addEvent(d, 'mousedown', function(event){ mouseIsDown = true; if(!interactions.length){ new Interaction(event); } var interaction = getInteraction(); if(!interaction){ return; } getInteraction().start(event); }); addEvent(d, 'mousemove', function(event){ if(!interactions.length){ new Interaction(event); } var interaction = getInteraction(); if(!interaction){ return; } if(mouseIsDown){ interaction.drag(event); }else{ interaction.move(event); } }); addEvent(d, 'mouseup', function(event){ mouseIsDown = false; var interaction = getInteraction(); if(!interaction){ return; } interaction.end(event, null); interaction.destroy(); }); function addEvent(element, type, callback) { if(element == null){ return; } if(element.addEventListener){ element.addEventListener(type, callback, { passive: false }); } else if(d.attachEvent){ element.attachEvent("on"+ type, callback, { passive: false }); } } module.exports = interact;