@rawify/vector3
Version:
The RAW JavaScript 3D Vector library
1,591 lines (1,339 loc) • 68 kB
JavaScript
/*
* Copyright (c) 2012, 2013 Taye Adeyemi
* Open source under the MIT License.
* https://raw.github.com/biographer/interact.js/master/LICENSE
*/
/**
* @namespace interact.js
* @name interact
* @function interact
* @param {HTMLElement | svgelement} element The previously set document element
* @returns {Interactable | null} Returns an Interactable if the element passed
* was previously set. Returns null otherwise.
* @description The properties of this variable can be used to set elements as
* interactables and also to change various settings. Calling it as
* a function with an element which was previously set returns an
* Interactable object which has various methods to configure it.
*/
window.interact = (function () {
'use strict';
var document = window.document,
console = window.console,
SVGElement = window.SVGElement || blank,
SVGSVGElement = window.SVGSVGElement || blank,
HTMLElement = window.HTMLElement || window.Element,
// Previous interact move event mouse/touch position
prevX = 0,
prevY = 0,
prevClientX = 0,
prevClientY = 0,
// Previos interact start event mouse/touch position
x0 = 0,
y0 = 0,
clientX0 = 0,
clientY0 = 0,
gesture = {
start: {
x: 0,
y: 0
},
startDistance: 0, // distance between two touches of touchStart
prevDistance : 0,
distance : 0,
scale: 1, // gesture.distance / gesture.startDistance
startAngle: 0, // angle of line joining two touches
prevAngle : 0 // angle of the previous gesture event
},
interactables = [], // array of all set interactables
dropzones = [], // array of all dropzone interactables
target = null, // current interactable being interacted with
dropTarget = null, // the dropzone a drag target might be dropped into
prevDropTarget = null, // the dropzone that was recently dragged away from
// All things relating to autoScroll
autoScroll = {
isEnabled: true,
margin : 60,
interval : 20, // pause in ms between each scroll pulse
i : null, // the handle returned by window.setInterval
distance : 10, // the distance in x and y that the page is scrolled
x: 0, // Direction each pulse is to scroll in
y: 0,
// scroll the window by the values in scroll.x/y
autoScroll: function () {
window.scrollBy(autoScroll.x, autoScroll.y);
},
edgeMove: function (event) {
if (autoScroll.isEnabled && (dragging || resizing)) {
var top = event.clientY < autoScroll.margin,
right = event.clientX > (window.innerWidth - autoScroll.margin),
bottom = event.clientY > (window.innerHeight - autoScroll.margin),
left = event.clientX < autoScroll.margin,
options = target.options;
autoScroll.x = autoScroll.distance * (right ? 1: left? -1: 0);
autoScroll.y = autoScroll.distance * (bottom? 1: top? -1: 0);
if (!autoScroll.isScrolling && options.autoScroll) {
autoScroll.start();
}
}
},
isScrolling: false,
start: function () {
autoScroll.isScrolling = true;
window.clearInterval(autoScroll.i);
autoScroll.i = window.setInterval(autoScroll.autoScroll, autoScroll.interval);
},
stop: function () {
window.clearInterval(autoScroll.i);
autoScroll.isScrolling = false;
}
},
// aww snap
snap = {
enabled: false,
mode: 'grid',
range: Infinity,
grid: {
x: 100,
y: 100
},
gridOffset: {
x: 0,
y: 0
},
anchors: [],
locked: false,
x : 0,
y : 0,
dx: 0,
dy: 0,
realX: 0,
realY: 0
},
// Does the browser support touch input?
supportsTouch = 'createTouch' in document,
// Less Precision with touch input
margin = supportsTouch ? 20 : 10,
mouseIsDown = false,
mouseWasMoved = false,
imPropStopped = false,
gesturing = false,
dragging = false,
dynamicDrop = false,
resizing = false,
resizeAxes = 'xy',
// What to do depending on action returned by getAction() of node
// Dictates what styles should be used and what mouseMove event Listner
// is to be added after mouseDown
actions = {
drag: {
cursor : 'move',
moveListener: dragMove
},
resizex: {
cursor : 'e-resize',
moveListener: resizeMove
},
resizey: {
cursor : 's-resize',
moveListener: resizeMove
},
resizexy: {
cursor : 'se-resize',
moveListener: resizeMove
},
gesture: {
cursor : '',
moveListener: gestureMove
}
},
actionIsEnabled = {
drag : true,
resize : true,
gesture: true
},
// Action that's ready to be fired on next move event
prepared = null,
styleCursor = true,
// User interaction event types. will be set depending on touch input is
// supported
downEvent,
upEvent,
moveEvent,
overEvent,
outEvent,
enterEvent,
leaveEvent,
// because Webkit and Opera still use 'mousewheel' event type
wheelEvent = 'onmousewheel' in document? 'mousewheel': 'wheel',
eventTypes = [
'resizestart',
'resizemove',
'resizeend',
'dragstart',
'dragmove',
'dragend',
'dragenter',
'dragleave',
'drop',
'gesturestart',
'gesturemove',
'gestureend',
'click'
],
globalEvents = [],
fireStates = {
onevent : 0,
directBind: 1,
globalBind: 2
},
// Opera must be handled differently
isOperaMobile = navigator.appName == 'Opera' &&
supportsTouch &&
navigator.userAgent.match('Presto'),
// used for adding event listeners to window and document
windowTarget = {
_element: window,
events : {}
},
docTarget = {
_element: document,
events : {}
},
// Events wrapper
events = (function () {
var Event = window.Event,
useAttachEvent = 'attachEvent' in window && !('addEventListener' in window),
addEvent = !useAttachEvent? 'addEventListener': 'attachEvent',
removeEvent = !useAttachEvent? 'removeEventListener': 'detachEvent',
on = useAttachEvent? 'on': '',
elements = [],
targets = [];
if (!('indexOf' in Array.prototype)) {
Array.prototype.indexOf = function(elt /*, from*/) {
var len = this.length >>> 0;
var from = Number(arguments[1]) || 0;
from = (from < 0)?
Math.ceil(from):
Math.floor(from);
if (from < 0) {
from += len;
}
for (; from < len; from++) {
if (from in this && this[from] === elt) {
return from;
}
}
return -1;
};
}
if (!('stopPropagation' in Event.prototype)) {
Event.prototype.stopPropagation = function () {
this.cancelBubble = true;
};
Event.prototype.stopImmediatePropagation = function () {
this.cancelBubble = true;
this.immediatePropagationStopped = true;
};
}
if (!('preventDefault' in Event.prototype)) {
Event.prototype.preventDefault = function () {
this.returnValue = false;
};
}
if (!('hasOwnProperty' in Event.prototype)) {
Event.prototype.hasOwnProperty = Object.prototype.hasOwnProperty;
}
function add (element, type, listener, useCapture) {
var target = targets[elements.indexOf(element)];
if (!target) {
target = {
events: {},
typeCount: 0
};
elements.push(element);
targets.push(target);
}
if (!(type in target.events)) {
target.events[type] = [];
target.typeCount++;
}
if (target.events[type].indexOf(listener) === -1) {
var ret;
if (useAttachEvent) {
ret = element[addEvent](on + type, function (event) {
if (!event.immediatePropagationStopped) {
event.target = event.srcElement;
event.currentTarget = element;
if (event.type.match(/mouse|click/)) {
event.pageX = event.clientX + document.documentElement.scrollLeft;
event.pageY = event.clientY + document.documentElement.scrollTop;
}
listener(event);
}
}, listener, useCapture || false);
}
else {
ret = element[addEvent](type, listener, useCapture || false);
}
target.events[type].push(listener);
return ret;
}
}
function remove (element, type, listener, useCapture) {
var i,
target = targets[elements.indexOf(element)];
if (!target || !target.events) {
return;
}
if (type === 'all') {
for (type in target.events) {
if (target.events.hasOwnProperty(type)) {
remove(element, type, 'all');
}
}
return;
}
if (target.events[type]) {
var len = target.events[type].length;
if (listener === 'all') {
for (i = 0; i < len; i++) {
element[removeEvent](on + type, target.events[type][i], useCapture || false);
}
target.events[type] = null;
target.typeCount--;
} else {
for (i = 0; i < len; i++) {
if (target.events[type][i] === listener) {
element[removeEvent](on + type, target.events[type][i], useCapture || false);
target.events[type].splice(i, 1);
break;
}
}
}
if (target.events[type] && target.events[type].length === 0) {
target.events[type] = null;
target.typeCount--;
}
}
if (!target.typeCount) {
targets.splice(targets.indexOf(target), 1);
elements.splice(elements.indexOf(element), 1);
}
}
return {
add: function (target, type, listener, useCapture) {
add(target._element, type, listener, useCapture);
},
remove: function (target, type, listener, useCapture) {
remove(target._element, type, listener, useCapture);
},
useAttachEvent: useAttachEvent
};
}());
// Set event types to be used depending on input available
if (supportsTouch) {
downEvent = 'touchstart',
upEvent = 'touchend',
moveEvent = 'touchmove',
overEvent = 'touchover',
outEvent = 'touchout';
enterEvent = 'touchover',
leaveEvent = 'touchout';
}
else {
downEvent = 'mousedown',
upEvent = 'mouseup',
moveEvent = 'mousemove',
overEvent = 'mouseover',
outEvent = 'mouseout';
enterEvent = 'touchenter',
leaveEvent = 'touchleave';
}
/**
* @private
* @returns{String} action to be performed - drag/resize[axes]/gesture
*/
function actionCheck (event) {
var clientRect,
right,
bottom,
action,
page = getPageXY(event),
scroll = getScrollXY(),
x = page.x - scroll.x,
y = page.y - scroll.y,
options = target.options;
clientRect = (target._element instanceof SVGElement)?
target._element.getBoundingClientRect():
target._element.getClientRects()[0];
if (actionIsEnabled.resize && options.resizeable) {
right = x > (clientRect.right - margin);
bottom = y > (clientRect.bottom - margin);
}
if (actionIsEnabled.gesture &&
event.touches && event.touches.length >= 2 &&
!(dragging || resizing)) {
action = 'gesture';
}
else {
resizeAxes = (right?'x': '') + (bottom?'y': '');
action = (resizeAxes)?
'resize' + resizeAxes:
actionIsEnabled.drag && options.draggable?
'drag':
null;
}
return action;
}
function setPrevXY (event) {
prevX = event.pageX;
prevY = event.pageY;
prevClientX = event.clientX;
prevClientY = event.clientY;
}
// Get specified X/Y coords for mouse or event.touches[0]
function getXY (event, type) {
var touch,
x,
y;
type = type || 'page';
if (event.touches) {
touch = (event.touches.length)?
event.touches[0]:
event.changedTouches[0];
x = touch[type + 'X'];
y = touch[type + 'Y'];
}
else {
x = event[type + 'X'];
y = event[type + 'Y'];
}
// Opera Mobile handles the viewport and scrolling oddly
if (isOperaMobile) {
x -= window.scrollX;
y -= window.scrollY;
}
return {
x: x,
y: y
};
}
function getPageXY (event) {
return getXY(event, 'page');
}
function getClientXY (event) {
return getXY(event, 'client');
}
function getScrollXY () {
return {
x: window.scrollX || document.documentElement.scrollLeft,
y: window.scrollY || document.documentElement.scrollTop
};
}
function touchAverage (event) {
var i,
touches = event.touches,
pageX = 0,
pageY = 0,
clientX = 0,
clientY = 0;
for (i = 0; i < touches.length; i++) {
pageX += touches[i].pageX / touches.length;
pageY += touches[i].pageY / touches.length;
clientX += touches[i].clientX / touches.length;
clientY += touches[i].clientY / touches.length;
}
return {
pageX: pageX,
pageY: pageY,
clientX: clientX,
clientY: clientY
};
}
function getTouchBBox (event) {
if (!event.touches.length) {
return;
}
var i,
touches = event.touches,
minX = event.touches[0].pageX,
minY = event.touches[0].pageY,
maxX = minX,
maxY = minY;
for (i = 1; i < touches.length; i++) {
minX = minX > event.touches[i].pageX?
minX:
event.touches[i].pageX;
minY = minX > event.touches[i].pageX?
minY:
event.touches[i].pageY;
}
return {
left: minX,
top: minY,
width: maxX - minX,
height: maxY - minY
};
}
function touchDistance (event) {
var dx = event.touches[0].pageX,
dy = event.touches[0].pageY;
if (event.type === 'touchend' && event.touches.length === 1) {
dx -= event.changedTouches[0].pageX;
dy -= event.changedTouches[0].pageY;
}
else {
dx -= event.touches[1].pageX;
dy -= event.touches[1].pageY;
}
return Math.sqrt(dx * dx + dy * dy);
}
function touchAngle (event) {
var dx = event.touches[0].pageX,
dy = event.touches[0].pageY;
if (event.type === 'touchend' && event.touches.length === 1) {
dx -= event.changedTouches[0].pageX;
dy -= event.changedTouches[0].pageY;
}
else {
dx -= event.touches[1].pageX;
dy -= event.touches[1].pageY;
}
return 180 * -Math.atan(dy / dx) / Math.PI;
}
function calcDropRects (dropzones) {
for (var i = 0, len = dropzones.length; i < len; i++) {
var dropzone = dropzones[i],
scroll = isOperaMobile?
{x: 0, y: 0}:
getScrollXY(),
clientRect = (dropzone._element instanceof SVGElement)?
dropzone._element.getBoundingClientRect():
dropzone._element.getClientRects()[0];
dropzone.dropRect = {
left : clientRect.left + scroll.x,
right : clientRect.right + scroll.x,
top : clientRect.top + scroll.y,
bottom: clientRect.bottom + scroll.y,
width : clientRect.width,
height: clientRect.height
};
}
}
// Test for the dropzone element that's "above" all other qualifiers
function resolveDrops (drops) {
if (drops.length) {
var dropzone,
deepestZone = drops[0],
parent,
deepestZoneParents = [],
dropzoneParents = [],
child,
i,
n;
for (i = 1; i < drops.length; i++) {
dropzone = drops[i];
if (!deepestZoneParents.length) {
parent = deepestZone._element;
while (parent.parentNode !== document) {
deepestZoneParents.unshift(parent);
parent = parent.parentNode;
}
}
// if this dropzone is an svg element and the current deepest is
// an HTMLElement
if (deepestZone._element instanceof HTMLElement &&
dropzone._element instanceof SVGElement &&
!(dropzone._element instanceof SVGSVGElement)) {
if (dropzone._element.ownerSVGElement.parentNode ===
deepestZone._element.parentNode) {
continue;
}
parent = dropzone._element.ownerSVGElement;
}
else {
parent = dropzone._element;
}
dropzoneParents = [];
while (parent.parentNode !== document) {
dropzoneParents.unshift(parent);
parent = parent.parentNode;
}
// get (position of last common ancestor) + 1
n = 0;
while(dropzoneParents[n] &&
dropzoneParents[n] === deepestZoneParents[n]) {
n++;
}
parent = [
dropzoneParents[n - 1],
dropzoneParents[n],
deepestZoneParents[n]
];
child = parent[0].lastChild;
while (child) {
if (child === parent[1]) {
deepestZone = dropzone;
deepestZoneParents = [];
break;
}
else if (child === parent[2]) {
break;
}
child = child.previousSibling;
}
}
return deepestZone;
}
}
function InteractEvent (event, action, phase) {
var client,
page,
options = target.options;
if (action === 'gesture') {
var average = touchAverage(event);
page = {x: average.pageX, y: average.pageY};
client = {x: average.clientX, y: average.clientY};
}
else {
client = getClientXY(event);
page = getPageXY(event);
if (snap.enabled && snap.locked) {
page.x += snap.dx;
page.y += snap.dy;
client.x += snap.dx;
client.y += snap.dy;
}
}
this.x0 = x0;
this.y0 = y0;
this.clientX0 = clientX0;
this.clientY0 = clientY0;
this.pageX = page.x;
this.pageY = page.y;
this.clientX = client.x;
this.clientY = client.y;
this.ctrlKey = event.ctrlKey;
this.altKey = event.altKey;
this.shiftKey = event.shiftKey;
this.metaKey = event.metaKey;
this.button = event.button;
this.target = action === 'drop' && dropTarget._element || target._element;
this.timeStamp = new Date().getTime();
this.type = action + (phase || '');
// start/end event dx, dy is difference between start and current points
if (phase === 'start' || phase === 'end' || action === 'drop') {
this.dx = page.x - x0;
this.dy = page.y - y0;
}
else {
this.dx = page.x - prevX;
this.dy = page.y - prevY;
}
if (action === 'resize') {
if (options.squareResize || event.shiftKey) {
if (resizeAxes === 'y') {
this.dx = this.dy;
}
else {
this.dy = this.dx;
}
this.axes = 'xy';
}
else {
this.axes = resizeAxes;
if (resizeAxes === 'x') {
this.dy = 0;
}
else if (resizeAxes === 'y') {
this.dx = 0;
}
}
}
else if (action === 'gesture') {
this.touches = event.touches;
this.distance = touchDistance(event);
this.box = getTouchBBox(event);
this.angle = touchAngle(event);
if (phase === 'start') {
this.scale = 1;
this.ds = 0;
this.rotation = 0;
}
else {
this.scale = this.distance / gesture.startDistance;
if (phase === 'end') {
this.rotation = this.angle - gesture.startAngle;
this.ds = this.scale - 1;
}
else {
this.rotation = this.angle - gesture.prevAngle;
this.ds = this.scale - gesture.prevScale;
}
}
}
else if (action === 'drop' ||
(action === 'drag' && (phase === 'enter' || phase === 'leave'))) {
this.draggable = target._element;
}
}
function blank () {}
InteractEvent.prototype = {
preventDefault: blank,
stopImmediatePropagation: function (event) {
imPropStopped = true;
},
stopPropagation: blank
};
// Check if action is enabled globally and the current target supports it
// If so, return the validated action. Otherwise, return null
function validateAction (action) {
if (typeof action !== 'string') { return null; }
var actionType = action.indexOf('resize') !== -1? 'resize': action,
options = target.options;
if (((actionType === 'resize' && options.resizeable) ||
(action === 'drag' && options.draggable) ||
(action === 'gesture' && options.gestureable)) &&
actionIsEnabled[actionType]) {
if (action === 'resize' || action === 'resizeyx') {
action = 'resizexy';
}
return action;
}
return null;
}
/**
* @private
* @event
* Determine action to be performed on next mouseMove and add appropriate
* style and event Liseners
*/
function mouseDown (event, forceAction) {
// document and window can't be interacted with but their Interactables
// can be used for binding events
if (!((events.useAttachEvent? event.currentTarget: this) instanceof window.Element)) {
return;
}
var action = '',
average,
page,
client;
if (event.touches) {
average = touchAverage(event);
page = {
x: average.pageX,
y: average.pageY
};
client = {
x: average.clientX,
y: average.clientY
};
}
else {
page = getPageXY(event);
client = getClientXY(event);
}
// If it is the second touch of a multi-touch gesture, keep the target
// the same if a target was set by the first touch
// (not always the case with simulated touches)
// Otherwise, set the target if the mouse is not down
if ((event.touches && event.touches.length < 2 && !target) ||
!(mouseIsDown)) {
target = interactables.get(events.useAttachEvent? event.currentTarget: this);
}
if (target && !(dragging || resizing || gesturing)) {
var options = target.options;
x0 = prevX = page.x;
y0 = prevY = page.y;
clientX0 = prevClientX = client.x;
clientY0 = prevClientY = client.y;
action = validateAction(forceAction || options.getAction(event));
if (!action) {
return event;
}
// Register that the mouse is down after succesfully validating
// action. This way, a new target can be gotten in the next
// downEvent propagation
mouseIsDown = true;
mouseWasMoved = false;
if (styleCursor) {
document.documentElement.style.cursor =
target._element.style.cursor =
actions[action].cursor;
}
resizeAxes = (action === 'resizexy')?
'xy':
(action === 'resizex')?
'x':
(action === 'resizey')?
'y':
'';
prepared = (action in actions)? action: null;
event.preventDefault();
}
}
function mouseMove (event) {
if (mouseIsDown) {
if (x0 === prevX && y0 === prevY) {
mouseWasMoved = true;
}
if (prepared && target) {
if (snap.enabled) {
var page = getPageXY(event),
inRange,
snapChanged;
snap.realX = page.x;
snap.realY = page.y;
// change to infinite range when range is negative
if (snap.range < 0) { snap.range = Infinity; }
if (snap.mode === 'anchor' && snap.anchors.length) {
var closest = {
anchor: null,
distance: 0,
range: 0,
distX: 0,
distY: 0
},
distX,
distY;
for (var i = 0, len = snap.anchors.length; i < len; i++) {
var anchor = snap.anchors[i],
distX = anchor.x - page.x,
distY = anchor.y - page.y,
range = typeof anchor.range === 'number'? anchor.range: snap.range,
distance = Math.sqrt(distX * distX + distY * distY);
inRange = distance < range;
// Infinite anchors count as being out of range
// compared to non infinite ones that are in range
if (range === Infinity && closest.inRange && closest.range !== Infinity) {
inRange = false;
}
if (!closest.anchor || (inRange?
// is the closest anchor in range?
(closest.inRange && range !== Infinity)?
// the pointer is relatively deeper in this anchor
distance / range < closest.distance / closest.range:
//the pointer is closer to this anchor
distance < closest.distance:
// The other is not in range and the pointer is closer to this anchor
(!closest.inRange && distance < closest.distance))) {
if (range === Infinity) {
inRange = true;
}
closest = {
anchor: anchor,
distance: distance,
range: range,
inRange: inRange,
distX: distX,
distY: distY
};
}
}
inRange = closest.inRange;
snapChanged = (closest.anchor.x !== snap.x || closest.anchor.y !== snap.y);
snap.x = closest.anchor.x;
snap.y = closest.anchor.y;
snap.dx = closest.distX;
snap.dy = closest.distY;
snap.anchors.closest = closest.anchor;
}
else {
var gridx = Math.round((page.x - snap.gridOffset.x) / snap.grid.x),
gridy = Math.round((page.y - snap.gridOffset.y) / snap.grid.y),
newX = gridx * snap.grid.x + snap.gridOffset.x,
newY = gridy * snap.grid.y + snap.gridOffset.y,
distX = newX - page.x,
distY = newY - page.y,
distance = Math.sqrt(distX * distX + distY * distY);
inRange = distance < snap.range;
snapChanged = (newX !== snap.x || newY !== snap.y);
snap.x = newX;
snap.y = newY;
snap.dx = distX;
snap.dy = distY;
}
if ((snapChanged || !snap.locked) && inRange) {
snap.locked = true;
actions[prepared].moveListener(event);
}
else if (snapChanged || !inRange) {
snap.locked = false;
actions[prepared].moveListener(event);
}
}
else {
actions[prepared].moveListener(event);
}
}
}
if (dragging || resizing) {
autoScroll.edgeMove(event);
}
}
function dragMove (event) {
event.preventDefault();
var dragEvent,
dragEnterEvent,
dragLeaveEvent,
leaveDropTarget;
if (!dragging) {
dragEvent = new InteractEvent(event, 'drag', 'start');
dragging = true;
if (!dynamicDrop) {
calcDropRects(dropzones);
}
}
else {
dragEvent = new InteractEvent(event, 'drag', 'move');
if (dropzones.length) {
var i,
drops = [];
// collect all dropzones that qualify for a drop
for (i = 0; i < dropzones.length; i++) {
if (dropzones[i].dropCheck(event)) {
drops.push(dropzones[i]);
}
}
// get the most apprpriate dropzone based on DOM depth and order
dropTarget = resolveDrops(drops);
// if the current dropTarget is not the same as the previous one
if (dropTarget !== prevDropTarget) {
// if there was a prevDropTarget, create a dragleave event
if (prevDropTarget) {
dragLeaveEvent = new InteractEvent(event, 'drag', 'leave');
dragEvent.dragLeave = prevDropTarget._element;
leaveDropTarget = prevDropTarget;
prevDropTarget = null;
}
// if the dropTarget is not null, create a dragenter event
if (dropTarget) {
dragEnterEvent = new InteractEvent(event, 'drag', 'enter');
dragEvent.dragEnter = dropTarget._element;
prevDropTarget = dropTarget;
}
}
}
}
target.fire(dragEvent);
if (dragLeaveEvent) {
leaveDropTarget.fire(dragLeaveEvent);
}
if (dragEnterEvent) {
dropTarget.fire(dragEnterEvent);
}
setPrevXY(dragEvent);
}
function resizeMove (event) {
event.preventDefault();
var resizeEvent;
if (!resizing) {
resizeEvent = new InteractEvent(event, 'resize', 'start');
target.fire(resizeEvent);
resizing = true;
}
else {
resizeEvent = new InteractEvent(event, 'resize', 'move');
target.fire(resizeEvent);
}
setPrevXY(resizeEvent);
}
function gestureMove (event) {
if (event.touches.length < 2) {
return;
}
event.preventDefault();
var gestureEvent;
if (!gesturing) {
gestureEvent = new InteractEvent(event, 'gesture', 'start');
gestureEvent.ds = 0;
gesture.startDistance = gestureEvent.distance;
gesture.startAngle = gestureEvent.angle;
gesture.scale = 1;
target.fire(gestureEvent);
gesturing = true;
}
else {
gestureEvent = new InteractEvent(event, 'gesture', 'move');
gestureEvent.ds = gestureEvent.scale - gesture.scale;
target.fire(gestureEvent);
}
setPrevXY(gestureEvent);
gesture.prevAngle = gestureEvent.angle;
gesture.prevDistance = gestureEvent.distance;
if (gestureEvent.scale !== Infinity &&
gestureEvent.scale !== null &&
gestureEvent.scale !== undefined &&
!isNaN(gestureEvent.scale)) {
gesture.scale = gestureEvent.scale;
}
}
/**
* @private
* @event
* Check what action would be performed on mouseMove target if the mouse
* button were pressed and change the cursor accordingly
*/
function mouseHover (event) {
if (!(mouseIsDown || dragging || resizing || gesturing) &&
(target = interactables.get(event.target))) {
var options = target.options;
if (((actionIsEnabled.drag && options.draggable) ||
(actionIsEnabled.resize && options.resizeable)) &&
options.checkOnHover) {
var action = validateAction(options.getAction(event));
if (styleCursor) {
if (action) {
target._element.style.cursor = actions[action].cursor;
}
else {
target._element.style.cursor = '';
}
}
}
else if (dragging || resizing || gesturing) {
event.preventDefault();
}
}
}
/**
* @private
* @event
* End interact move events and stop auto-scroll
*/
function docMouseUp (event) {
var endEvent;
if (dragging) {
endEvent = new InteractEvent(event, 'drag', 'end');
var dropEvent;
if (dropzones.length) {
var i,
drops = [];
// collect all dropzones that qualify for a drop
for (i = 0; i < dropzones.length; i++) {
if (dropzones[i].dropCheck(event)) {
drops.push(dropzones[i]);
}
}
// get the most apprpriate dropzone based on DOM depth and order
if ((dropTarget = resolveDrops(drops))) {
dropEvent = new InteractEvent(event, 'drop');
endEvent.dropzone = dropTarget._element;
}
// otherwise, if there was a prevDropTarget (perhaps if for
// some reason this dragend happens without the mouse moving
// out of the previousdroptarget)
else if (prevDropTarget) {
var dragLeaveEvent = new InteractEvent(event, 'drag', 'leave');
prevDropTarget.fire(dragLeaveEvent);
endEvent.dragLeave = prevDropTarget._element;
}
}
target.fire(endEvent);
if (dropEvent) {
dropTarget.fire(dropEvent);
}
}
else if (resizing) {
endEvent = new InteractEvent(event, 'resize', 'end');
target.fire(endEvent);
}
else if (gesturing) {
endEvent = new InteractEvent(event, 'gesture', 'end');
endEvent.ds = endEvent.scale;
target.fire(endEvent);
}
else if (event.type === upEvent && target && mouseIsDown && !mouseWasMoved) {
var click = {};
for (var prop in event) {
if (event.hasOwnProperty(prop)) {
click[prop] = event[prop];
}
}
click.type = 'click';
target.fire(click);
}
mouseIsDown = snap.locked = dragging = resizing = gesturing = false;
mouseWasMoved = true;
if (target) {
if (styleCursor) {
document.documentElement.style.cursor = '';
target._element.style.cursor = '';
}
autoScroll.stop();
clearTargets();
// prevent Default only if were previously interacting
event.preventDefault();
}
prepared = null;
return event;
}
interactables.indexOfElement = dropzones.indexOfElement = function (element) {
var i;
for (i = 0; i < this.length; i++) {
if (this[i]._element === element) {
return i;
}
}
return -1;
};
interactables.get = dropzones.get = function (element) {
var i = this.indexOfElement(element) ;
return interactables[i];
};
function clearTargets () {
target = dropTarget = prevDropTarget = null;
}
function interact (element) {
if (typeof element === 'string') {
element = document.getElementById(element);
}
return interactables.get(element) || new Interactable(element);
}
/**
* A class for inheritance and easier setting of an Interactable's options
*
* @class IOptions
*/
function IOptions (options) {
for (var option in IOptions.prototype) {
if (options.hasOwnProperty(option) && typeof options[option] === typeof IOptions.prototype[option]) {
this[option] = options[option];
}
}
}
IOptions.prototype = {
draggable : false,
dropzone : false,
resizeable : false,
gestureable : false,
squareResize: false,
autoScroll : true,
getAction : actionCheck,
checkOnHover: true
};
/**
* Object type returned by interact(element)
*
* @class Interactable
* @name Interactable
*/
function Interactable (element, options) {
this._element = element,
this._iEvents = this._iEvents || {};
events.add(this, moveEvent, mouseHover);
events.add(this, downEvent, mouseDown);
interactables.push(this);
this.set(options);
}
Interactable.prototype = {
setOnEvents: function (action, phases) {
var start = phases.onstart || phases.onStart,
move = phases.onmove || phases.onMove,
end = phases.onend || phases.onEnd;
action = 'on' + action;
if (typeof start === 'function') { this[action + 'start'] = start; }
if (typeof move === 'function') { this[action + 'move' ] = move ; }
if (typeof end === 'function') { this[action + 'end' ] = end ; }
},
/**
* Returns or sets whether drag actions can be performed on the
* Interactable
*
* @function
* @param {bool} options
* @returns {bool | Interactable}
*/
draggable: function (options) {
if (typeof options === 'object') {
this.options.draggable = true;
this.setOnEvents('drag', options);
return this;
}
if (typeof options === 'boolean') {
this.options.draggable = options;
return this;
}
return this.options.draggable;
},
/**
* Returns or sets whether elements can be dropped onto this
* Interactable to trigger interactdrop events
*
* @function
* @param {bool} options The new value to be set. Passing null returns
* the current value
* @returns {bool | Interactable}
*/
dropzone: function (options) {
if (typeof options === 'object') {
var ondrop = options.ondrop || options.onDrop;
if (typeof ondrop === 'function') { this.ondrop = ondrop; }
this.options.dropzone = true;
dropzones.push(this);
if (!dynamicDrop) {
calcDropRects([this]);
}
return this;
}
if (typeof options === 'boolean') {
if (options) {
dropzones.push(this);
if (!dynamicDrop) {
calcDropRects([this]);
}
}
else {
var index = dropzones.indexOf(this);
if (index !== -1) {
dropzones.splice(index, 1);
}
}
this.options.dropzone = options;
return this;
}
return this.options.dropzone;
},
/**
* The default function to determine if an interactdragend event occured
* over this Interactable's element
*
* @function
* @param {MouseEvent | TouchEvent} event The event that ends an
* interactdrag
* @returns {bool}
*/
dropCheck: function (event) {
if (target !== this) {
var horizontal,
vertical;
if (dynamicDrop) {
var clientRect = (this._element instanceof SVGElement)?
this._element.getBoundingClientRect():
this._element.getClientRects()[0],
client = (isOperaMobile)?
getPageXY(event):
getClientXY(event);
horizontal = (client.x > clientRect.left) && (client.x < clientRect.right);
vertical = (client.y > clientRect.top ) && (client.y < clientRect.bottom);
return horizontal && vertical;
}
else {
var page = getPageXY(event);
horizontal = (page.x > this.dropRect.left) && (page.x < this.dropRect.right);
vertical = (page.y > this.dropRect.top ) && (page.y < this.dropRect.bottom);
return horizontal && vertical;
}
}
},
/**
* Returns or sets the function used to check if a dragged element is
* dropped over this Interactable
*
* @function
* @param {function} newValue A function which takes a mouseUp/touchEnd
* event as a parameter and returns
* @returns {Function | Interactable}
*/
dropChecker: function (newValue) {
if (typeof newValue === 'function') {
this.dropChecker = newValue;
return this;
}
return this.dropChecker;
},
/**
* Returns or sets whether resize actions can be performed on the
* Interactable
*
* @function
* @param {} options An object with event listeners to be fired on resize events
* @returns {bool | Interactable}
*/
resizeable: function (options) {
if (typeof options === 'object') {
this.options.resizeable = true;
this.setOnEvents('resize', options);
return this;
}
if (typeof options === 'boolean') {
this.options.resizeable = options;
return this;
}
return this.options.resizeable;
},
/**
* Returns or sets whether resizing is forced 1:1 aspect
*
* @function
* @param {bool} newValue
* @returns {bool | Interactable}
*/
squareResize: function (newValue) {
if (newValue !== null && newValue !== undefined) {
this.options.squareResize = newValue;
return this;
}
return this.options.squareResize;
},
/**
* Returns or sets whether multitouch gestures can be performed on the
* Interactables element
*
* @function
* @param {bool} options
* @returns {bool | Interactable}
*/
gestureable: function (options) {
if (typeof options === 'object') {
this.options.gestureable = true;
this.setOnEvents('gesture', options);
return this;
}
if (options !== null && options !== undefined) {
this.options.gestureable = options;
return this;
}
return this.options.gestureable;
},
/**
* Returns or sets whether dragging and resizing near the edges of the
* screen will trigger autoScroll
*
* @function
* @param {bool} newValue
* @returns {bool | Interactable}
*/
autoScroll: function (newValue) {
if (newValue !== null && newValue !== undefined) {
this.options.autoScroll = newValue;
return this;
}
return this.options.autoScroll;
},
/**
* Returns or sets the function used to check action to be performed on
* mouseDown/touchStart
*
* @function
* @param {function} newValue
* @returns {Function | Interactable}
*/
actionChecker: function (newValue) {
if (typeof newValue === 'function') {
this.options.getAction = newValue;
return this;
}
return this.options.getAction;
},
/**
* Returns or sets whether the action that would be performed when the
* mouse hovers over the element are checked. If so, the cursor may be
* styled appropriately
*
* @function
* @param {function} newValue
* @returns {Function | Interactable}
*/
checkOnHover: function (newValue) {
if (newValue !== null && newValue !== undefined) {