interact-js
Version:
A small interaction event unifiying library
447 lines (369 loc) • 12.8 kB
JavaScript
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;