From 802f76db8a1039c5cf9b03ad40299cc46ce7f80b Mon Sep 17 00:00:00 2001 From: Adam Goldsmith Date: Mon, 18 Sep 2017 12:04:55 -0400 Subject: [PATCH] Basic playmat stuff: deck/discard, hand, card movement --- interact.js | 5978 ++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 12 + script.js | 141 ++ server.js | 91 + style.css | 42 + 5 files changed, 6264 insertions(+) create mode 100644 interact.js create mode 100644 package.json create mode 100644 script.js create mode 100644 server.js create mode 100644 style.css diff --git a/interact.js b/interact.js new file mode 100644 index 0000000..66d7aab --- /dev/null +++ b/interact.js @@ -0,0 +1,5978 @@ +/** + * interact.js v1.2.9 + * + * Copyright (c) 2012-2015 Taye Adeyemi + * Open source under the MIT License. + * https://raw.github.com/taye/interact.js/master/LICENSE + */ +(function (realWindow) { + 'use strict'; + + // return early if there's no window to work with (eg. Node.js) + if (!realWindow) { return; } + + var // get wrapped window if using Shadow DOM polyfill + window = (function () { + // create a TextNode + var el = realWindow.document.createTextNode(''); + + // check if it's wrapped by a polyfill + if (el.ownerDocument !== realWindow.document + && typeof realWindow.wrap === 'function' + && realWindow.wrap(el) === el) { + // return wrapped window + return realWindow.wrap(realWindow); + } + + // no Shadow DOM polyfil or native implementation + return realWindow; + }()), + + document = window.document, + DocumentFragment = window.DocumentFragment || blank, + SVGElement = window.SVGElement || blank, + SVGSVGElement = window.SVGSVGElement || blank, + SVGElementInstance = window.SVGElementInstance || blank, + HTMLElement = window.HTMLElement || window.Element, + + PointerEvent = (window.PointerEvent || window.MSPointerEvent), + pEventTypes, + + hypot = Math.hypot || function (x, y) { return Math.sqrt(x * x + y * y); }, + + tmpXY = {}, // reduce object creation in getXY() + + documents = [], // all documents being listened to + + interactables = [], // all set interactables + interactions = [], // all interactions + + dynamicDrop = false, + + // { + // type: { + // selectors: ['selector', ...], + // contexts : [document, ...], + // listeners: [[listener, useCapture], ...] + // } + // } + delegatedEvents = {}, + + defaultOptions = { + base: { + accept : null, + actionChecker : null, + styleCursor : true, + preventDefault: 'auto', + origin : { x: 0, y: 0 }, + deltaSource : 'page', + allowFrom : null, + ignoreFrom : null, + _context : document, + dropChecker : null + }, + + drag: { + enabled: false, + manualStart: true, + max: Infinity, + maxPerElement: 1, + + snap: null, + restrict: null, + inertia: null, + autoScroll: null, + + axis: 'xy' + }, + + drop: { + enabled: false, + accept: null, + overlap: 'pointer' + }, + + resize: { + enabled: false, + manualStart: false, + max: Infinity, + maxPerElement: 1, + + snap: null, + restrict: null, + inertia: null, + autoScroll: null, + + square: false, + preserveAspectRatio: false, + axis: 'xy', + + // use default margin + margin: NaN, + + // object with props left, right, top, bottom which are + // true/false values to resize when the pointer is over that edge, + // CSS selectors to match the handles for each direction + // or the Elements for each handle + edges: null, + + // a value of 'none' will limit the resize rect to a minimum of 0x0 + // 'negate' will alow the rect to have negative width/height + // 'reposition' will keep the width/height positive by swapping + // the top and bottom edges and/or swapping the left and right edges + invert: 'none' + }, + + gesture: { + manualStart: false, + enabled: false, + max: Infinity, + maxPerElement: 1, + + restrict: null + }, + + perAction: { + manualStart: false, + max: Infinity, + maxPerElement: 1, + + snap: { + enabled : false, + endOnly : false, + range : Infinity, + targets : null, + offsets : null, + + relativePoints: null + }, + + restrict: { + enabled: false, + endOnly: false + }, + + autoScroll: { + enabled : false, + container : null, // the item that is scrolled (Window or HTMLElement) + margin : 60, + speed : 300 // the scroll speed in pixels per second + }, + + inertia: { + enabled : false, + resistance : 10, // the lambda in exponential decay + minSpeed : 100, // target speed must be above this for inertia to start + endSpeed : 10, // the speed at which inertia is slow enough to stop + allowResume : true, // allow resuming an action in inertia phase + zeroResumeDelta : true, // if an action is resumed after launch, set dx/dy to 0 + smoothEndDuration: 300 // animate to snap/restrict endOnly if there's no inertia + } + }, + + _holdDuration: 600 + }, + + // Things related to autoScroll + autoScroll = { + interaction: null, + i: null, // the handle returned by window.setInterval + x: 0, y: 0, // Direction each pulse is to scroll in + + // scroll the window by the values in scroll.x/y + scroll: function () { + var options = autoScroll.interaction.target.options[autoScroll.interaction.prepared.name].autoScroll, + container = options.container || getWindow(autoScroll.interaction.element), + now = new Date().getTime(), + // change in time in seconds + dtx = (now - autoScroll.prevTimeX) / 1000, + dty = (now - autoScroll.prevTimeY) / 1000, + vx, vy, sx, sy; + + // displacement + if (options.velocity) { + vx = options.velocity.x; + vy = options.velocity.y; + } + else { + vx = vy = options.speed + } + + sx = vx * dtx; + sy = vy * dty; + + if (sx >= 1 || sy >= 1) { + if (isWindow(container)) { + container.scrollBy(autoScroll.x * sx, autoScroll.y * sy); + } + else if (container) { + container.scrollLeft += autoScroll.x * sx; + container.scrollTop += autoScroll.y * sy; + } + + if (sx >=1) autoScroll.prevTimeX = now; + if (sy >= 1) autoScroll.prevTimeY = now; + } + + if (autoScroll.isScrolling) { + cancelFrame(autoScroll.i); + autoScroll.i = reqFrame(autoScroll.scroll); + } + }, + + isScrolling: false, + prevTimeX: 0, + prevTimeY: 0, + + start: function (interaction) { + autoScroll.isScrolling = true; + cancelFrame(autoScroll.i); + + autoScroll.interaction = interaction; + autoScroll.prevTimeX = new Date().getTime(); + autoScroll.prevTimeY = new Date().getTime(); + autoScroll.i = reqFrame(autoScroll.scroll); + }, + + stop: function () { + autoScroll.isScrolling = false; + cancelFrame(autoScroll.i); + } + }, + + // Does the browser support touch input? + supportsTouch = (('ontouchstart' in window) || window.DocumentTouch && document instanceof window.DocumentTouch), + + // Does the browser support PointerEvents + // Avoid PointerEvent bugs introduced in Chrome 55 + supportsPointerEvent = PointerEvent && !/Chrome/.test(navigator.userAgent), + + // Less Precision with touch input + margin = supportsTouch || supportsPointerEvent? 20: 10, + + pointerMoveTolerance = 1, + + // for ignoring browser's simulated mouse events + prevTouchTime = 0, + + // Allow this many interactions to happen simultaneously + maxInteractions = Infinity, + + // Check if is IE9 or older + actionCursors = (document.all && !window.atob) ? { + drag : 'move', + resizex : 'e-resize', + resizey : 's-resize', + resizexy: 'se-resize', + + resizetop : 'n-resize', + resizeleft : 'w-resize', + resizebottom : 's-resize', + resizeright : 'e-resize', + resizetopleft : 'se-resize', + resizebottomright: 'se-resize', + resizetopright : 'ne-resize', + resizebottomleft : 'ne-resize', + + gesture : '' + } : { + drag : 'move', + resizex : 'ew-resize', + resizey : 'ns-resize', + resizexy: 'nwse-resize', + + resizetop : 'ns-resize', + resizeleft : 'ew-resize', + resizebottom : 'ns-resize', + resizeright : 'ew-resize', + resizetopleft : 'nwse-resize', + resizebottomright: 'nwse-resize', + resizetopright : 'nesw-resize', + resizebottomleft : 'nesw-resize', + + gesture : '' + }, + + actionIsEnabled = { + drag : true, + resize : true, + gesture: true + }, + + // because Webkit and Opera still use 'mousewheel' event type + wheelEvent = 'onmousewheel' in document? 'mousewheel': 'wheel', + + eventTypes = [ + 'dragstart', + 'dragmove', + 'draginertiastart', + 'dragend', + 'dragenter', + 'dragleave', + 'dropactivate', + 'dropdeactivate', + 'dropmove', + 'drop', + 'resizestart', + 'resizemove', + 'resizeinertiastart', + 'resizeend', + 'gesturestart', + 'gesturemove', + 'gestureinertiastart', + 'gestureend', + + 'down', + 'move', + 'up', + 'cancel', + 'tap', + 'doubletap', + 'hold' + ], + + globalEvents = {}, + + // Opera Mobile must be handled differently + isOperaMobile = navigator.appName == 'Opera' && + supportsTouch && + navigator.userAgent.match('Presto'), + + // scrolling doesn't change the result of getClientRects on iOS 7 + isIOS7 = (/iP(hone|od|ad)/.test(navigator.platform) + && /OS 7[^\d]/.test(navigator.appVersion)), + + // prefix matchesSelector + prefixedMatchesSelector = 'matches' in Element.prototype? + 'matches': 'webkitMatchesSelector' in Element.prototype? + 'webkitMatchesSelector': 'mozMatchesSelector' in Element.prototype? + 'mozMatchesSelector': 'oMatchesSelector' in Element.prototype? + 'oMatchesSelector': 'msMatchesSelector', + + // will be polyfill function if browser is IE8 + ie8MatchesSelector, + + // native requestAnimationFrame or polyfill + reqFrame = realWindow.requestAnimationFrame, + cancelFrame = realWindow.cancelAnimationFrame, + + // Events wrapper + events = (function () { + var useAttachEvent = ('attachEvent' in window) && !('addEventListener' in window), + addEvent = useAttachEvent? 'attachEvent': 'addEventListener', + removeEvent = useAttachEvent? 'detachEvent': 'removeEventListener', + on = useAttachEvent? 'on': '', + + elements = [], + targets = [], + attachedListeners = []; + + function add (element, type, listener, useCapture) { + var elementIndex = indexOf(elements, element), + target = targets[elementIndex]; + + if (!target) { + target = { + events: {}, + typeCount: 0 + }; + + elementIndex = elements.push(element) - 1; + targets.push(target); + + attachedListeners.push((useAttachEvent ? { + supplied: [], + wrapped : [], + useCount: [] + } : null)); + } + + if (!target.events[type]) { + target.events[type] = []; + target.typeCount++; + } + + if (!contains(target.events[type], listener)) { + var ret; + + if (useAttachEvent) { + var listeners = attachedListeners[elementIndex], + listenerIndex = indexOf(listeners.supplied, listener); + + var wrapped = listeners.wrapped[listenerIndex] || function (event) { + if (!event.immediatePropagationStopped) { + event.target = event.srcElement; + event.currentTarget = element; + + event.preventDefault = event.preventDefault || preventDef; + event.stopPropagation = event.stopPropagation || stopProp; + event.stopImmediatePropagation = event.stopImmediatePropagation || stopImmProp; + + if (/mouse|click/.test(event.type)) { + event.pageX = event.clientX + getWindow(element).document.documentElement.scrollLeft; + event.pageY = event.clientY + getWindow(element).document.documentElement.scrollTop; + } + + listener(event); + } + }; + + ret = element[addEvent](on + type, wrapped, Boolean(useCapture)); + + if (listenerIndex === -1) { + listeners.supplied.push(listener); + listeners.wrapped.push(wrapped); + listeners.useCount.push(1); + } + else { + listeners.useCount[listenerIndex]++; + } + } + else { + ret = element[addEvent](type, listener, useCapture || false); + } + target.events[type].push(listener); + + return ret; + } + } + + function remove (element, type, listener, useCapture) { + var i, + elementIndex = indexOf(elements, element), + target = targets[elementIndex], + listeners, + listenerIndex, + wrapped = listener; + + if (!target || !target.events) { + return; + } + + if (useAttachEvent) { + listeners = attachedListeners[elementIndex]; + listenerIndex = indexOf(listeners.supplied, listener); + wrapped = listeners.wrapped[listenerIndex]; + } + + 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++) { + remove(element, type, target.events[type][i], Boolean(useCapture)); + } + return; + } else { + for (i = 0; i < len; i++) { + if (target.events[type][i] === listener) { + element[removeEvent](on + type, wrapped, useCapture || false); + target.events[type].splice(i, 1); + + if (useAttachEvent && listeners) { + listeners.useCount[listenerIndex]--; + if (listeners.useCount[listenerIndex] === 0) { + listeners.supplied.splice(listenerIndex, 1); + listeners.wrapped.splice(listenerIndex, 1); + listeners.useCount.splice(listenerIndex, 1); + } + } + + break; + } + } + } + + if (target.events[type] && target.events[type].length === 0) { + target.events[type] = null; + target.typeCount--; + } + } + + if (!target.typeCount) { + targets.splice(elementIndex, 1); + elements.splice(elementIndex, 1); + attachedListeners.splice(elementIndex, 1); + } + } + + function preventDef () { + this.returnValue = false; + } + + function stopProp () { + this.cancelBubble = true; + } + + function stopImmProp () { + this.cancelBubble = true; + this.immediatePropagationStopped = true; + } + + return { + add: add, + remove: remove, + useAttachEvent: useAttachEvent, + + _elements: elements, + _targets: targets, + _attachedListeners: attachedListeners + }; + }()); + + function blank () {} + + function isElement (o) { + if (!o || (typeof o !== 'object')) { return false; } + + var _window = getWindow(o) || window; + + return (/object|function/.test(typeof _window.Element) + ? o instanceof _window.Element //DOM2 + : o.nodeType === 1 && typeof o.nodeName === "string"); + } + function isWindow (thing) { return thing === window || !!(thing && thing.Window) && (thing instanceof thing.Window); } + function isDocFrag (thing) { return !!thing && thing instanceof DocumentFragment; } + function isArray (thing) { + return isObject(thing) + && (typeof thing.length !== undefined) + && isFunction(thing.splice); + } + function isObject (thing) { return !!thing && (typeof thing === 'object'); } + function isFunction (thing) { return typeof thing === 'function'; } + function isNumber (thing) { return typeof thing === 'number' ; } + function isBool (thing) { return typeof thing === 'boolean' ; } + function isString (thing) { return typeof thing === 'string' ; } + + function trySelector (value) { + if (!isString(value)) { return false; } + + // an exception will be raised if it is invalid + document.querySelector(value); + return true; + } + + function extend (dest, source) { + for (var prop in source) { + dest[prop] = source[prop]; + } + return dest; + } + + var prefixedPropREs = { + webkit: /(Movement[XY]|Radius[XY]|RotationAngle|Force)$/ + }; + + function pointerExtend (dest, source) { + for (var prop in source) { + var deprecated = false; + + // skip deprecated prefixed properties + for (var vendor in prefixedPropREs) { + if (prop.indexOf(vendor) === 0 && prefixedPropREs[vendor].test(prop)) { + deprecated = true; + break; + } + } + + if (!deprecated) { + dest[prop] = source[prop]; + } + } + return dest; + } + + function copyCoords (dest, src) { + dest.page = dest.page || {}; + dest.page.x = src.page.x; + dest.page.y = src.page.y; + + dest.client = dest.client || {}; + dest.client.x = src.client.x; + dest.client.y = src.client.y; + + dest.timeStamp = src.timeStamp; + } + + function setEventXY (targetObj, pointers, interaction) { + var pointer = (pointers.length > 1 + ? pointerAverage(pointers) + : pointers[0]); + + getPageXY(pointer, tmpXY, interaction); + targetObj.page.x = tmpXY.x; + targetObj.page.y = tmpXY.y; + + getClientXY(pointer, tmpXY, interaction); + targetObj.client.x = tmpXY.x; + targetObj.client.y = tmpXY.y; + + targetObj.timeStamp = new Date().getTime(); + } + + function setEventDeltas (targetObj, prev, cur) { + targetObj.page.x = cur.page.x - prev.page.x; + targetObj.page.y = cur.page.y - prev.page.y; + targetObj.client.x = cur.client.x - prev.client.x; + targetObj.client.y = cur.client.y - prev.client.y; + targetObj.timeStamp = new Date().getTime() - prev.timeStamp; + + // set pointer velocity + var dt = Math.max(targetObj.timeStamp / 1000, 0.001); + targetObj.page.speed = hypot(targetObj.page.x, targetObj.page.y) / dt; + targetObj.page.vx = targetObj.page.x / dt; + targetObj.page.vy = targetObj.page.y / dt; + + targetObj.client.speed = hypot(targetObj.client.x, targetObj.page.y) / dt; + targetObj.client.vx = targetObj.client.x / dt; + targetObj.client.vy = targetObj.client.y / dt; + } + + function isNativePointer (pointer) { + return (pointer instanceof window.Event + || (supportsTouch && window.Touch && pointer instanceof window.Touch)); + } + + // Get specified X/Y coords for mouse or event.touches[0] + function getXY (type, pointer, xy) { + xy = xy || {}; + type = type || 'page'; + + xy.x = pointer[type + 'X']; + xy.y = pointer[type + 'Y']; + + return xy; + } + + function getPageXY (pointer, page) { + page = page || {}; + + // Opera Mobile handles the viewport and scrolling oddly + if (isOperaMobile && isNativePointer(pointer)) { + getXY('screen', pointer, page); + + page.x += window.scrollX; + page.y += window.scrollY; + } + else { + getXY('page', pointer, page); + } + + return page; + } + + function getClientXY (pointer, client) { + client = client || {}; + + if (isOperaMobile && isNativePointer(pointer)) { + // Opera Mobile handles the viewport and scrolling oddly + getXY('screen', pointer, client); + } + else { + getXY('client', pointer, client); + } + + return client; + } + + function getScrollXY (win) { + win = win || window; + return { + x: win.scrollX || win.document.documentElement.scrollLeft, + y: win.scrollY || win.document.documentElement.scrollTop + }; + } + + function getPointerId (pointer) { + return isNumber(pointer.pointerId)? pointer.pointerId : pointer.identifier; + } + + function getActualElement (element) { + return (element instanceof SVGElementInstance + ? element.correspondingUseElement + : element); + } + + function getWindow (node) { + if (isWindow(node)) { + return node; + } + + var rootNode = (node.ownerDocument || node); + + return rootNode.defaultView || rootNode.parentWindow || window; + } + + function getElementClientRect (element) { + var clientRect = (element instanceof SVGElement + ? element.getBoundingClientRect() + : element.getClientRects()[0]); + + return clientRect && { + left : clientRect.left, + right : clientRect.right, + top : clientRect.top, + bottom: clientRect.bottom, + width : clientRect.width || clientRect.right - clientRect.left, + height: clientRect.height || clientRect.bottom - clientRect.top + }; + } + + function getElementRect (element) { + var clientRect = getElementClientRect(element); + + if (!isIOS7 && clientRect) { + var scroll = getScrollXY(getWindow(element)); + + clientRect.left += scroll.x; + clientRect.right += scroll.x; + clientRect.top += scroll.y; + clientRect.bottom += scroll.y; + } + + return clientRect; + } + + function getTouchPair (event) { + var touches = []; + + // array of touches is supplied + if (isArray(event)) { + touches[0] = event[0]; + touches[1] = event[1]; + } + // an event + else { + if (event.type === 'touchend') { + if (event.touches.length === 1) { + touches[0] = event.touches[0]; + touches[1] = event.changedTouches[0]; + } + else if (event.touches.length === 0) { + touches[0] = event.changedTouches[0]; + touches[1] = event.changedTouches[1]; + } + } + else { + touches[0] = event.touches[0]; + touches[1] = event.touches[1]; + } + } + + return touches; + } + + function pointerAverage (pointers) { + var average = { + pageX : 0, + pageY : 0, + clientX: 0, + clientY: 0, + screenX: 0, + screenY: 0 + }; + var prop; + + for (var i = 0; i < pointers.length; i++) { + for (prop in average) { + average[prop] += pointers[i][prop]; + } + } + for (prop in average) { + average[prop] /= pointers.length; + } + + return average; + } + + function touchBBox (event) { + if (!event.length && !(event.touches && event.touches.length > 1)) { + return; + } + + var touches = getTouchPair(event), + minX = Math.min(touches[0].pageX, touches[1].pageX), + minY = Math.min(touches[0].pageY, touches[1].pageY), + maxX = Math.max(touches[0].pageX, touches[1].pageX), + maxY = Math.max(touches[0].pageY, touches[1].pageY); + + return { + x: minX, + y: minY, + left: minX, + top: minY, + width: maxX - minX, + height: maxY - minY + }; + } + + function touchDistance (event, deltaSource) { + deltaSource = deltaSource || defaultOptions.deltaSource; + + var sourceX = deltaSource + 'X', + sourceY = deltaSource + 'Y', + touches = getTouchPair(event); + + + var dx = touches[0][sourceX] - touches[1][sourceX], + dy = touches[0][sourceY] - touches[1][sourceY]; + + return hypot(dx, dy); + } + + function touchAngle (event, prevAngle, deltaSource) { + deltaSource = deltaSource || defaultOptions.deltaSource; + + var sourceX = deltaSource + 'X', + sourceY = deltaSource + 'Y', + touches = getTouchPair(event), + dx = touches[0][sourceX] - touches[1][sourceX], + dy = touches[0][sourceY] - touches[1][sourceY], + angle = 180 * Math.atan(dy / dx) / Math.PI; + + if (isNumber(prevAngle)) { + var dr = angle - prevAngle, + drClamped = dr % 360; + + if (drClamped > 315) { + angle -= 360 + (angle / 360)|0 * 360; + } + else if (drClamped > 135) { + angle -= 180 + (angle / 360)|0 * 360; + } + else if (drClamped < -315) { + angle += 360 + (angle / 360)|0 * 360; + } + else if (drClamped < -135) { + angle += 180 + (angle / 360)|0 * 360; + } + } + + return angle; + } + + function getOriginXY (interactable, element) { + var origin = interactable + ? interactable.options.origin + : defaultOptions.origin; + + if (origin === 'parent') { + origin = parentElement(element); + } + else if (origin === 'self') { + origin = interactable.getRect(element); + } + else if (trySelector(origin)) { + origin = closest(element, origin) || { x: 0, y: 0 }; + } + + if (isFunction(origin)) { + origin = origin(interactable && element); + } + + if (isElement(origin)) { + origin = getElementRect(origin); + } + + origin.x = ('x' in origin)? origin.x : origin.left; + origin.y = ('y' in origin)? origin.y : origin.top; + + return origin; + } + + // http://stackoverflow.com/a/5634528/2280888 + function _getQBezierValue(t, p1, p2, p3) { + var iT = 1 - t; + return iT * iT * p1 + 2 * iT * t * p2 + t * t * p3; + } + + function getQuadraticCurvePoint(startX, startY, cpX, cpY, endX, endY, position) { + return { + x: _getQBezierValue(position, startX, cpX, endX), + y: _getQBezierValue(position, startY, cpY, endY) + }; + } + + // http://gizma.com/easing/ + function easeOutQuad (t, b, c, d) { + t /= d; + return -c * t*(t-2) + b; + } + + function nodeContains (parent, child) { + while (child) { + if (child === parent) { + return true; + } + + child = child.parentNode; + } + + return false; + } + + function closest (child, selector) { + var parent = parentElement(child); + + while (isElement(parent)) { + if (matchesSelector(parent, selector)) { return parent; } + + parent = parentElement(parent); + } + + return null; + } + + function parentElement (node) { + var parent = node.parentNode; + + if (isDocFrag(parent)) { + // skip past #shado-root fragments + while ((parent = parent.host) && isDocFrag(parent)) {} + + return parent; + } + + return parent; + } + + function inContext (interactable, element) { + return interactable._context === element.ownerDocument + || nodeContains(interactable._context, element); + } + + function testIgnore (interactable, interactableElement, element) { + var ignoreFrom = interactable.options.ignoreFrom; + + if (!ignoreFrom || !isElement(element)) { return false; } + + if (isString(ignoreFrom)) { + return matchesUpTo(element, ignoreFrom, interactableElement); + } + else if (isElement(ignoreFrom)) { + return nodeContains(ignoreFrom, element); + } + + return false; + } + + function testAllow (interactable, interactableElement, element) { + var allowFrom = interactable.options.allowFrom; + + if (!allowFrom) { return true; } + + if (!isElement(element)) { return false; } + + if (isString(allowFrom)) { + return matchesUpTo(element, allowFrom, interactableElement); + } + else if (isElement(allowFrom)) { + return nodeContains(allowFrom, element); + } + + return false; + } + + function checkAxis (axis, interactable) { + if (!interactable) { return false; } + + var thisAxis = interactable.options.drag.axis; + + return (axis === 'xy' || thisAxis === 'xy' || thisAxis === axis); + } + + function checkSnap (interactable, action) { + var options = interactable.options; + + if (/^resize/.test(action)) { + action = 'resize'; + } + + return options[action].snap && options[action].snap.enabled; + } + + function checkRestrict (interactable, action) { + var options = interactable.options; + + if (/^resize/.test(action)) { + action = 'resize'; + } + + return options[action].restrict && options[action].restrict.enabled; + } + + function checkAutoScroll (interactable, action) { + var options = interactable.options; + + if (/^resize/.test(action)) { + action = 'resize'; + } + + return options[action].autoScroll && options[action].autoScroll.enabled; + } + + function withinInteractionLimit (interactable, element, action) { + var options = interactable.options, + maxActions = options[action.name].max, + maxPerElement = options[action.name].maxPerElement, + activeInteractions = 0, + targetCount = 0, + targetElementCount = 0; + + for (var i = 0, len = interactions.length; i < len; i++) { + var interaction = interactions[i], + otherAction = interaction.prepared.name, + active = interaction.interacting(); + + if (!active) { continue; } + + activeInteractions++; + + if (activeInteractions >= maxInteractions) { + return false; + } + + if (interaction.target !== interactable) { continue; } + + targetCount += (otherAction === action.name)|0; + + if (targetCount >= maxActions) { + return false; + } + + if (interaction.element === element) { + targetElementCount++; + + if (otherAction !== action.name || targetElementCount >= maxPerElement) { + return false; + } + } + } + + return maxInteractions > 0; + } + + // Test for the element that's "above" all other qualifiers + function indexOfDeepestElement (elements) { + var dropzone, + deepestZone = elements[0], + index = deepestZone? 0: -1, + parent, + deepestZoneParents = [], + dropzoneParents = [], + child, + i, + n; + + for (i = 1; i < elements.length; i++) { + dropzone = elements[i]; + + // an element might belong to multiple selector dropzones + if (!dropzone || dropzone === deepestZone) { + continue; + } + + if (!deepestZone) { + deepestZone = dropzone; + index = i; + continue; + } + + // check if the deepest or current are document.documentElement or document.rootElement + // - if the current dropzone is, do nothing and continue + if (dropzone.parentNode === dropzone.ownerDocument) { + continue; + } + // - if deepest is, update with the current dropzone and continue to next + else if (deepestZone.parentNode === dropzone.ownerDocument) { + deepestZone = dropzone; + index = i; + continue; + } + + if (!deepestZoneParents.length) { + parent = deepestZone; + while (parent.parentNode && parent.parentNode !== parent.ownerDocument) { + deepestZoneParents.unshift(parent); + parent = parent.parentNode; + } + } + + // if this element is an svg element and the current deepest is + // an HTMLElement + if (deepestZone instanceof HTMLElement + && dropzone instanceof SVGElement + && !(dropzone instanceof SVGSVGElement)) { + + if (dropzone === deepestZone.parentNode) { + continue; + } + + parent = dropzone.ownerSVGElement; + } + else { + parent = dropzone; + } + + dropzoneParents = []; + + while (parent.parentNode !== parent.ownerDocument) { + dropzoneParents.unshift(parent); + parent = parent.parentNode; + } + + n = 0; + + // get (position of last common ancestor) + 1 + while (dropzoneParents[n] && dropzoneParents[n] === deepestZoneParents[n]) { + n++; + } + + var parents = [ + dropzoneParents[n - 1], + dropzoneParents[n], + deepestZoneParents[n] + ]; + + child = parents[0].lastChild; + + while (child) { + if (child === parents[1]) { + deepestZone = dropzone; + index = i; + deepestZoneParents = []; + + break; + } + else if (child === parents[2]) { + break; + } + + child = child.previousSibling; + } + } + + return index; + } + + function Interaction () { + this.target = null; // current interactable being interacted with + this.element = null; // the target element of the interactable + this.dropTarget = null; // the dropzone a drag target might be dropped into + this.dropElement = null; // the element at the time of checking + this.prevDropTarget = null; // the dropzone that was recently dragged away from + this.prevDropElement = null; // the element at the time of checking + + this.prepared = { // action that's ready to be fired on next move event + name : null, + axis : null, + edges: null + }; + + this.matches = []; // all selectors that are matched by target element + this.matchElements = []; // corresponding elements + + this.inertiaStatus = { + active : false, + smoothEnd : false, + ending : false, + + startEvent: null, + upCoords: {}, + + xe: 0, ye: 0, + sx: 0, sy: 0, + + t0: 0, + vx0: 0, vys: 0, + duration: 0, + + resumeDx: 0, + resumeDy: 0, + + lambda_v0: 0, + one_ve_v0: 0, + i : null + }; + + if (isFunction(Function.prototype.bind)) { + this.boundInertiaFrame = this.inertiaFrame.bind(this); + this.boundSmoothEndFrame = this.smoothEndFrame.bind(this); + } + else { + var that = this; + + this.boundInertiaFrame = function () { return that.inertiaFrame(); }; + this.boundSmoothEndFrame = function () { return that.smoothEndFrame(); }; + } + + this.activeDrops = { + dropzones: [], // the dropzones that are mentioned below + elements : [], // elements of dropzones that accept the target draggable + rects : [] // the rects of the elements mentioned above + }; + + // keep track of added pointers + this.pointers = []; + this.pointerIds = []; + this.downTargets = []; + this.downTimes = []; + this.holdTimers = []; + + // 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.tapTime = 0; // time of the most recent tap event + this.prevTap = null; + + this.startOffset = { left: 0, right: 0, top: 0, bottom: 0 }; + this.restrictOffset = { left: 0, right: 0, top: 0, bottom: 0 }; + this.snapOffsets = []; + + this.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 + }; + + this.snapStatus = { + x : 0, y : 0, + dx : 0, dy : 0, + realX : 0, realY : 0, + snappedX: 0, snappedY: 0, + targets : [], + locked : false, + changed : false + }; + + this.restrictStatus = { + dx : 0, dy : 0, + restrictedX: 0, restrictedY: 0, + snap : null, + restricted : false, + changed : false + }; + + this.restrictStatus.snap = this.snapStatus; + + this.pointerIsDown = false; + this.pointerWasMoved = false; + this.gesturing = false; + this.dragging = false; + this.resizing = false; + this.resizeAxes = 'xy'; + + this.mouse = false; + + interactions.push(this); + } + + Interaction.prototype = { + getPageXY : function (pointer, xy) { return getPageXY(pointer, xy, this); }, + getClientXY: function (pointer, xy) { return getClientXY(pointer, xy, this); }, + setEventXY : function (target, ptr) { return setEventXY(target, ptr, this); }, + + pointerOver: function (pointer, event, eventTarget) { + if (this.prepared.name || !this.mouse) { return; } + + var curMatches = [], + curMatchElements = [], + prevTargetElement = this.element; + + this.addPointer(pointer); + + if (this.target + && (testIgnore(this.target, this.element, eventTarget) + || !testAllow(this.target, this.element, eventTarget))) { + // if the eventTarget should be ignored or shouldn't be allowed + // clear the previous target + this.target = null; + this.element = null; + this.matches = []; + this.matchElements = []; + } + + var elementInteractable = interactables.get(eventTarget), + elementAction = (elementInteractable + && !testIgnore(elementInteractable, eventTarget, eventTarget) + && testAllow(elementInteractable, eventTarget, eventTarget) + && validateAction( + elementInteractable.getAction(pointer, event, this, eventTarget), + elementInteractable)); + + if (elementAction && !withinInteractionLimit(elementInteractable, eventTarget, elementAction)) { + elementAction = null; + } + + function pushCurMatches (interactable, selector) { + if (interactable + && isElement(eventTarget) + && inContext(interactable, eventTarget) + && !testIgnore(interactable, eventTarget, eventTarget) + && testAllow(interactable, eventTarget, eventTarget) + && matchesSelector(eventTarget, selector)) { + + curMatches.push(interactable); + curMatchElements.push(eventTarget); + } + } + + if (elementAction) { + this.target = elementInteractable; + this.element = eventTarget; + this.matches = []; + this.matchElements = []; + } + else { + interactables.forEachSelector(pushCurMatches); + + if (this.validateSelector(pointer, event, curMatches, curMatchElements)) { + this.matches = curMatches; + this.matchElements = curMatchElements; + + this.pointerHover(pointer, event, this.matches, this.matchElements); + events.add(eventTarget, + supportsPointerEvent? pEventTypes.move : 'mousemove', + listeners.pointerHover); + } + else if (this.target) { + if (nodeContains(prevTargetElement, eventTarget)) { + this.pointerHover(pointer, event, this.matches, this.matchElements); + events.add(this.element, + supportsPointerEvent? pEventTypes.move : 'mousemove', + listeners.pointerHover); + } + else { + this.target = null; + this.element = null; + this.matches = []; + this.matchElements = []; + } + } + } + }, + + // Check what action would be performed on pointerMove target if a mouse + // button were pressed and change the cursor accordingly + pointerHover: function (pointer, event, eventTarget, curEventTarget, matches, matchElements) { + var target = this.target; + + if (!this.prepared.name && this.mouse) { + + var action; + + // update pointer coords for defaultActionChecker to use + this.setEventXY(this.curCoords, [pointer]); + + if (matches) { + action = this.validateSelector(pointer, event, matches, matchElements); + } + else if (target) { + action = validateAction(target.getAction(this.pointers[0], event, this, this.element), this.target); + } + + if (target && target.options.styleCursor) { + if (action) { + target._doc.documentElement.style.cursor = getActionCursor(action); + } + else { + target._doc.documentElement.style.cursor = ''; + } + } + } + else if (this.prepared.name) { + this.checkAndPreventDefault(event, target, this.element); + } + }, + + pointerOut: function (pointer, event, eventTarget) { + if (this.prepared.name) { return; } + + // Remove temporary event listeners for selector Interactables + if (!interactables.get(eventTarget)) { + events.remove(eventTarget, + supportsPointerEvent? pEventTypes.move : 'mousemove', + listeners.pointerHover); + } + + if (this.target && this.target.options.styleCursor && !this.interacting()) { + this.target._doc.documentElement.style.cursor = ''; + } + }, + + selectorDown: function (pointer, event, eventTarget, curEventTarget) { + var that = this, + // copy event to be used in timeout for IE8 + eventCopy = events.useAttachEvent? extend({}, event) : event, + element = eventTarget, + pointerIndex = this.addPointer(pointer), + action; + + this.holdTimers[pointerIndex] = setTimeout(function () { + that.pointerHold(events.useAttachEvent? eventCopy : pointer, eventCopy, eventTarget, curEventTarget); + }, defaultOptions._holdDuration); + + this.pointerIsDown = true; + + // Check if the down event hits the current inertia target + if (this.inertiaStatus.active && this.target.selector) { + // climb up the DOM tree from the event target + while (isElement(element)) { + + // if this element is the current inertia target element + if (element === this.element + // and the prospective action is the same as the ongoing one + && validateAction(this.target.getAction(pointer, event, this, this.element), this.target).name === this.prepared.name) { + + // stop inertia so that the next move will be a normal one + cancelFrame(this.inertiaStatus.i); + this.inertiaStatus.active = false; + + this.collectEventTargets(pointer, event, eventTarget, 'down'); + return; + } + element = parentElement(element); + } + } + + // do nothing if interacting + if (this.interacting()) { + this.collectEventTargets(pointer, event, eventTarget, 'down'); + return; + } + + function pushMatches (interactable, selector, context) { + var elements = ie8MatchesSelector + ? context.querySelectorAll(selector) + : undefined; + + if (inContext(interactable, element) + && !testIgnore(interactable, element, eventTarget) + && testAllow(interactable, element, eventTarget) + && matchesSelector(element, selector, elements)) { + + that.matches.push(interactable); + that.matchElements.push(element); + } + } + + // update pointer coords for defaultActionChecker to use + this.setEventXY(this.curCoords, [pointer]); + this.downEvent = event; + + while (isElement(element) && !action) { + this.matches = []; + this.matchElements = []; + + interactables.forEachSelector(pushMatches); + + action = this.validateSelector(pointer, event, this.matches, this.matchElements); + element = parentElement(element); + } + + if (action) { + this.prepared.name = action.name; + this.prepared.axis = action.axis; + this.prepared.edges = action.edges; + + this.collectEventTargets(pointer, event, eventTarget, 'down'); + + return this.pointerDown(pointer, event, eventTarget, curEventTarget, action); + } + else { + // do these now since pointerDown isn't being called from here + this.downTimes[pointerIndex] = new Date().getTime(); + this.downTargets[pointerIndex] = eventTarget; + pointerExtend(this.downPointer, pointer); + + copyCoords(this.prevCoords, this.curCoords); + this.pointerWasMoved = false; + } + + this.collectEventTargets(pointer, event, eventTarget, 'down'); + }, + + // Determine action to be performed on next pointerMove and add appropriate + // style and event Listeners + pointerDown: function (pointer, event, eventTarget, curEventTarget, forceAction) { + if (!forceAction && !this.inertiaStatus.active && this.pointerWasMoved && this.prepared.name) { + this.checkAndPreventDefault(event, this.target, this.element); + + return; + } + + this.pointerIsDown = true; + this.downEvent = event; + + var pointerIndex = this.addPointer(pointer), + action; + + // If it is the second touch of a multi-touch gesture, keep the + // target the same and get a new action if a target was set by the + // first touch + if (this.pointerIds.length > 1 && this.target._element === this.element) { + var newAction = validateAction(forceAction || this.target.getAction(pointer, event, this, this.element), this.target); + + if (withinInteractionLimit(this.target, this.element, newAction)) { + action = newAction; + } + + this.prepared.name = null; + } + // Otherwise, set the target if there is no action prepared + else if (!this.prepared.name) { + var interactable = interactables.get(curEventTarget); + + if (interactable + && !testIgnore(interactable, curEventTarget, eventTarget) + && testAllow(interactable, curEventTarget, eventTarget) + && (action = validateAction(forceAction || interactable.getAction(pointer, event, this, curEventTarget), interactable, eventTarget)) + && withinInteractionLimit(interactable, curEventTarget, action)) { + this.target = interactable; + this.element = curEventTarget; + } + } + + var target = this.target, + options = target && target.options; + + if (target && (forceAction || !this.prepared.name)) { + action = action || validateAction(forceAction || target.getAction(pointer, event, this, curEventTarget), target, this.element); + + this.setEventXY(this.startCoords, this.pointers); + + if (!action) { return; } + + if (options.styleCursor) { + target._doc.documentElement.style.cursor = getActionCursor(action); + } + + this.resizeAxes = action.name === 'resize'? action.axis : null; + + if (action === 'gesture' && this.pointerIds.length < 2) { + action = null; + } + + this.prepared.name = action.name; + this.prepared.axis = action.axis; + this.prepared.edges = action.edges; + + this.snapStatus.snappedX = this.snapStatus.snappedY = + this.restrictStatus.restrictedX = this.restrictStatus.restrictedY = NaN; + + this.downTimes[pointerIndex] = new Date().getTime(); + this.downTargets[pointerIndex] = eventTarget; + pointerExtend(this.downPointer, pointer); + + copyCoords(this.prevCoords, this.startCoords); + this.pointerWasMoved = false; + + this.checkAndPreventDefault(event, target, this.element); + } + // if inertia is active try to resume action + else if (this.inertiaStatus.active + && curEventTarget === this.element + && validateAction(target.getAction(pointer, event, this, this.element), target).name === this.prepared.name) { + + cancelFrame(this.inertiaStatus.i); + this.inertiaStatus.active = false; + + this.checkAndPreventDefault(event, target, this.element); + } + }, + + setModifications: function (coords, preEnd) { + var target = this.target, + shouldMove = true, + shouldSnap = checkSnap(target, this.prepared.name) && (!target.options[this.prepared.name].snap.endOnly || preEnd), + shouldRestrict = checkRestrict(target, this.prepared.name) && (!target.options[this.prepared.name].restrict.endOnly || preEnd); + + if (shouldSnap ) { this.setSnapping (coords); } else { this.snapStatus .locked = false; } + if (shouldRestrict) { this.setRestriction(coords); } else { this.restrictStatus.restricted = false; } + + if (shouldSnap && this.snapStatus.locked && !this.snapStatus.changed) { + shouldMove = shouldRestrict && this.restrictStatus.restricted && this.restrictStatus.changed; + } + else if (shouldRestrict && this.restrictStatus.restricted && !this.restrictStatus.changed) { + shouldMove = false; + } + + return shouldMove; + }, + + setStartOffsets: function (action, interactable, element) { + var rect = interactable.getRect(element), + origin = getOriginXY(interactable, element), + snap = interactable.options[this.prepared.name].snap, + restrict = interactable.options[this.prepared.name].restrict, + width, height; + + if (rect) { + this.startOffset.left = this.startCoords.page.x - rect.left; + this.startOffset.top = this.startCoords.page.y - rect.top; + + this.startOffset.right = rect.right - this.startCoords.page.x; + this.startOffset.bottom = rect.bottom - this.startCoords.page.y; + + if ('width' in rect) { width = rect.width; } + else { width = rect.right - rect.left; } + if ('height' in rect) { height = rect.height; } + else { height = rect.bottom - rect.top; } + } + else { + this.startOffset.left = this.startOffset.top = this.startOffset.right = this.startOffset.bottom = 0; + } + + this.snapOffsets.splice(0); + + var snapOffset = snap && snap.offset === 'startCoords' + ? { + x: this.startCoords.page.x - origin.x, + y: this.startCoords.page.y - origin.y + } + : snap && snap.offset || { x: 0, y: 0 }; + + if (rect && snap && snap.relativePoints && snap.relativePoints.length) { + for (var i = 0; i < snap.relativePoints.length; i++) { + this.snapOffsets.push({ + x: this.startOffset.left - (width * snap.relativePoints[i].x) + snapOffset.x, + y: this.startOffset.top - (height * snap.relativePoints[i].y) + snapOffset.y + }); + } + } + else { + this.snapOffsets.push(snapOffset); + } + + if (rect && restrict.elementRect) { + this.restrictOffset.left = this.startOffset.left - (width * restrict.elementRect.left); + this.restrictOffset.top = this.startOffset.top - (height * restrict.elementRect.top); + + this.restrictOffset.right = this.startOffset.right - (width * (1 - restrict.elementRect.right)); + this.restrictOffset.bottom = this.startOffset.bottom - (height * (1 - restrict.elementRect.bottom)); + } + else { + this.restrictOffset.left = this.restrictOffset.top = this.restrictOffset.right = this.restrictOffset.bottom = 0; + } + }, + + /*\ + * 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.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. + - interactable (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: function (action, interactable, 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 (indexOf(interactions, this) === -1) { + interactions.push(this); + } + + // set the startCoords if there was no prepared action + if (!this.prepared.name) { + this.setEventXY(this.startCoords, this.pointers); + } + + this.prepared.name = action.name; + this.prepared.axis = action.axis; + this.prepared.edges = action.edges; + this.target = interactable; + this.element = element; + + this.setStartOffsets(action.name, interactable, element); + this.setModifications(this.startCoords.page); + + this.prevEvent = this[this.prepared.name + 'Start'](this.downEvent); + }, + + pointerMove: function (pointer, event, eventTarget, curEventTarget, preEnd) { + if (this.inertiaStatus.active) { + var pageUp = this.inertiaStatus.upCoords.page; + var clientUp = this.inertiaStatus.upCoords.client; + + var inertiaPosition = { + pageX : pageUp.x + this.inertiaStatus.sx, + pageY : pageUp.y + this.inertiaStatus.sy, + clientX: clientUp.x + this.inertiaStatus.sx, + clientY: clientUp.y + this.inertiaStatus.sy + }; + + this.setEventXY(this.curCoords, [inertiaPosition]); + } + else { + this.recordPointer(pointer); + this.setEventXY(this.curCoords, this.pointers); + } + + var 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); + + var dx, dy, + pointerIndex = this.mouse? 0 : indexOf(this.pointerIds, getPointerId(pointer)); + + // 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 = hypot(dx, dy) > pointerMoveTolerance; + } + + if (!duplicateMove && (!this.pointerIsDown || this.pointerWasMoved)) { + if (this.pointerIsDown) { + clearTimeout(this.holdTimers[pointerIndex]); + } + + this.collectEventTargets(pointer, event, eventTarget, 'move'); + } + + if (!this.pointerIsDown) { return; } + + if (duplicateMove && this.pointerWasMoved && !preEnd) { + this.checkAndPreventDefault(event, this.target, this.element); + return; + } + + // set pointer coordinate, time changes and speeds + setEventDeltas(this.pointerDelta, this.prevCoords, this.curCoords); + + if (!this.prepared.name) { return; } + + if (this.pointerWasMoved + // ignore movement while inertia is active + && (!this.inertiaStatus.active || (pointer instanceof InteractEvent && /inertiastart/.test(pointer.type)))) { + + // if just starting an action, calculate the pointer speed now + if (!this.interacting()) { + setEventDeltas(this.pointerDelta, this.prevCoords, this.curCoords); + + // check if a drag is in the correct axis + if (this.prepared.name === 'drag') { + var absX = Math.abs(dx), + absY = Math.abs(dy), + targetAxis = this.target.options.drag.axis, + axis = (absX > absY ? 'x' : absX < absY ? 'y' : 'xy'); + + // if the movement isn't in the axis of the interactable + if (axis !== 'xy' && targetAxis !== 'xy' && targetAxis !== axis) { + // cancel the prepared action + this.prepared.name = null; + + // then try to get a drag from another ineractable + + var element = eventTarget; + + // check element interactables + while (isElement(element)) { + var elementInteractable = interactables.get(element); + + if (elementInteractable + && elementInteractable !== this.target + && !elementInteractable.options.drag.manualStart + && elementInteractable.getAction(this.downPointer, this.downEvent, this, element).name === 'drag' + && checkAxis(axis, elementInteractable)) { + + this.prepared.name = 'drag'; + this.target = elementInteractable; + this.element = element; + break; + } + + element = parentElement(element); + } + + // if there's no drag from element interactables, + // check the selector interactables + if (!this.prepared.name) { + var thisInteraction = this; + + var getDraggable = function (interactable, selector, context) { + var elements = ie8MatchesSelector + ? context.querySelectorAll(selector) + : undefined; + + if (interactable === thisInteraction.target) { return; } + + if (inContext(interactable, eventTarget) + && !interactable.options.drag.manualStart + && !testIgnore(interactable, element, eventTarget) + && testAllow(interactable, element, eventTarget) + && matchesSelector(element, selector, elements) + && interactable.getAction(thisInteraction.downPointer, thisInteraction.downEvent, thisInteraction, element).name === 'drag' + && checkAxis(axis, interactable) + && withinInteractionLimit(interactable, element, 'drag')) { + + return interactable; + } + }; + + element = eventTarget; + + while (isElement(element)) { + var selectorInteractable = interactables.forEachSelector(getDraggable); + + if (selectorInteractable) { + this.prepared.name = 'drag'; + this.target = selectorInteractable; + this.element = element; + break; + } + + element = parentElement(element); + } + } + } + } + } + + var starting = !!this.prepared.name && !this.interacting(); + + if (starting + && (this.target.options[this.prepared.name].manualStart + || !withinInteractionLimit(this.target, this.element, this.prepared))) { + this.stop(event); + return; + } + + if (this.prepared.name && this.target) { + if (starting) { + this.start(this.prepared, this.target, this.element); + } + + var shouldMove = this.setModifications(this.curCoords.page, preEnd); + + // move if snapping or restriction doesn't prevent it + if (shouldMove || starting) { + this.prevEvent = this[this.prepared.name + 'Move'](event); + } + + this.checkAndPreventDefault(event, this.target, this.element); + } + } + + copyCoords(this.prevCoords, this.curCoords); + + if (this.dragging || this.resizing) { + this.autoScrollMove(pointer); + } + }, + + dragStart: function (event) { + var dragEvent = new InteractEvent(this, event, 'drag', 'start', this.element); + + this.dragging = true; + this.target.fire(dragEvent); + + // reset active dropzones + this.activeDrops.dropzones = []; + this.activeDrops.elements = []; + this.activeDrops.rects = []; + + if (!this.dynamicDrop) { + this.setActiveDrops(this.element); + } + + var dropEvents = this.getDropEvents(event, dragEvent); + + if (dropEvents.activate) { + this.fireActiveDrops(dropEvents.activate); + } + + return dragEvent; + }, + + dragMove: function (event) { + var target = this.target, + dragEvent = new InteractEvent(this, event, 'drag', 'move', this.element), + draggableElement = this.element, + drop = this.getDrop(dragEvent, event, draggableElement); + + this.dropTarget = drop.dropzone; + this.dropElement = drop.element; + + var dropEvents = this.getDropEvents(event, dragEvent); + + target.fire(dragEvent); + + if (dropEvents.leave) { this.prevDropTarget.fire(dropEvents.leave); } + if (dropEvents.enter) { this.dropTarget.fire(dropEvents.enter); } + if (dropEvents.move ) { this.dropTarget.fire(dropEvents.move ); } + + this.prevDropTarget = this.dropTarget; + this.prevDropElement = this.dropElement; + + return dragEvent; + }, + + resizeStart: function (event) { + var resizeEvent = new InteractEvent(this, event, 'resize', 'start', this.element); + + if (this.prepared.edges) { + var startRect = this.target.getRect(this.element); + + /* + * When using the `resizable.square` or `resizable.preserveAspectRatio` options, resizing from one edge + * will affect another. E.g. with `resizable.square`, resizing to make the right edge larger will make + * the bottom edge larger by the same amount. We call these 'linked' edges. Any linked edges will depend + * on the active edges and the edge being interacted with. + */ + if (this.target.options.resize.square || this.target.options.resize.preserveAspectRatio) { + var linkedEdges = extend({}, this.prepared.edges); + + linkedEdges.top = linkedEdges.top || (linkedEdges.left && !linkedEdges.bottom); + linkedEdges.left = linkedEdges.left || (linkedEdges.top && !linkedEdges.right ); + linkedEdges.bottom = linkedEdges.bottom || (linkedEdges.right && !linkedEdges.top ); + linkedEdges.right = linkedEdges.right || (linkedEdges.bottom && !linkedEdges.left ); + + this.prepared._linkedEdges = linkedEdges; + } + else { + this.prepared._linkedEdges = null; + } + + // if using `resizable.preserveAspectRatio` option, record aspect ratio at the start of the resize + if (this.target.options.resize.preserveAspectRatio) { + this.resizeStartAspectRatio = startRect.width / startRect.height; + } + + this.resizeRects = { + start : startRect, + current : extend({}, startRect), + restricted: extend({}, startRect), + previous : extend({}, startRect), + delta : { + left: 0, right : 0, width : 0, + top : 0, bottom: 0, height: 0 + } + }; + + resizeEvent.rect = this.resizeRects.restricted; + resizeEvent.deltaRect = this.resizeRects.delta; + } + + this.target.fire(resizeEvent); + + this.resizing = true; + + return resizeEvent; + }, + + resizeMove: function (event) { + var resizeEvent = new InteractEvent(this, event, 'resize', 'move', this.element); + + var edges = this.prepared.edges, + invert = this.target.options.resize.invert, + invertible = invert === 'reposition' || invert === 'negate'; + + if (edges) { + var dx = resizeEvent.dx, + dy = resizeEvent.dy, + + start = this.resizeRects.start, + current = this.resizeRects.current, + restricted = this.resizeRects.restricted, + delta = this.resizeRects.delta, + previous = extend(this.resizeRects.previous, restricted), + + originalEdges = edges; + + // `resize.preserveAspectRatio` takes precedence over `resize.square` + if (this.target.options.resize.preserveAspectRatio) { + var resizeStartAspectRatio = this.resizeStartAspectRatio; + + edges = this.prepared._linkedEdges; + + if ((originalEdges.left && originalEdges.bottom) + || (originalEdges.right && originalEdges.top)) { + dy = -dx / resizeStartAspectRatio; + } + else if (originalEdges.left || originalEdges.right) { dy = dx / resizeStartAspectRatio; } + else if (originalEdges.top || originalEdges.bottom) { dx = dy * resizeStartAspectRatio; } + } + else if (this.target.options.resize.square) { + edges = this.prepared._linkedEdges; + + if ((originalEdges.left && originalEdges.bottom) + || (originalEdges.right && originalEdges.top)) { + dy = -dx; + } + else if (originalEdges.left || originalEdges.right) { dy = dx; } + else if (originalEdges.top || originalEdges.bottom) { dx = dy; } + } + + // update the 'current' rect without modifications + if (edges.top ) { current.top += dy; } + if (edges.bottom) { current.bottom += dy; } + if (edges.left ) { current.left += dx; } + if (edges.right ) { current.right += dx; } + + if (invertible) { + // if invertible, copy the current rect + extend(restricted, current); + + if (invert === 'reposition') { + // swap edge values if necessary to keep width/height positive + var swap; + + if (restricted.top > restricted.bottom) { + swap = restricted.top; + + restricted.top = restricted.bottom; + restricted.bottom = swap; + } + if (restricted.left > restricted.right) { + swap = restricted.left; + + restricted.left = restricted.right; + restricted.right = swap; + } + } + } + else { + // if not invertible, restrict to minimum of 0x0 rect + restricted.top = Math.min(current.top, start.bottom); + restricted.bottom = Math.max(current.bottom, start.top); + restricted.left = Math.min(current.left, start.right); + restricted.right = Math.max(current.right, start.left); + } + + restricted.width = restricted.right - restricted.left; + restricted.height = restricted.bottom - restricted.top ; + + for (var edge in restricted) { + delta[edge] = restricted[edge] - previous[edge]; + } + + resizeEvent.edges = this.prepared.edges; + resizeEvent.rect = restricted; + resizeEvent.deltaRect = delta; + } + + this.target.fire(resizeEvent); + + return resizeEvent; + }, + + gestureStart: function (event) { + var gestureEvent = new InteractEvent(this, event, 'gesture', 'start', this.element); + + gestureEvent.ds = 0; + + this.gesture.startDistance = this.gesture.prevDistance = gestureEvent.distance; + this.gesture.startAngle = this.gesture.prevAngle = gestureEvent.angle; + this.gesture.scale = 1; + + this.gesturing = true; + + this.target.fire(gestureEvent); + + return gestureEvent; + }, + + gestureMove: function (event) { + if (!this.pointerIds.length) { + return this.prevEvent; + } + + var gestureEvent; + + gestureEvent = new InteractEvent(this, event, 'gesture', 'move', this.element); + gestureEvent.ds = gestureEvent.scale - this.gesture.scale; + + this.target.fire(gestureEvent); + + this.gesture.prevAngle = gestureEvent.angle; + this.gesture.prevDistance = gestureEvent.distance; + + if (gestureEvent.scale !== Infinity && + gestureEvent.scale !== null && + gestureEvent.scale !== undefined && + !isNaN(gestureEvent.scale)) { + + this.gesture.scale = gestureEvent.scale; + } + + return gestureEvent; + }, + + pointerHold: function (pointer, event, eventTarget) { + this.collectEventTargets(pointer, event, eventTarget, 'hold'); + }, + + pointerUp: function (pointer, event, eventTarget, curEventTarget) { + var pointerIndex = this.mouse? 0 : indexOf(this.pointerIds, getPointerId(pointer)); + + clearTimeout(this.holdTimers[pointerIndex]); + + this.collectEventTargets(pointer, event, eventTarget, 'up' ); + this.collectEventTargets(pointer, event, eventTarget, 'tap'); + + this.pointerEnd(pointer, event, eventTarget, curEventTarget); + + this.removePointer(pointer); + }, + + pointerCancel: function (pointer, event, eventTarget, curEventTarget) { + var pointerIndex = this.mouse? 0 : indexOf(this.pointerIds, getPointerId(pointer)); + + clearTimeout(this.holdTimers[pointerIndex]); + + this.collectEventTargets(pointer, event, eventTarget, 'cancel'); + this.pointerEnd(pointer, event, eventTarget, curEventTarget); + + this.removePointer(pointer); + }, + + // http://www.quirksmode.org/dom/events/click.html + // >Events leading to dblclick + // + // IE8 doesn't fire down event before dblclick. + // This workaround tries to fire a tap and doubletap after dblclick + ie8Dblclick: function (pointer, event, eventTarget) { + if (this.prevTap + && event.clientX === this.prevTap.clientX + && event.clientY === this.prevTap.clientY + && eventTarget === this.prevTap.target) { + + this.downTargets[0] = eventTarget; + this.downTimes[0] = new Date().getTime(); + this.collectEventTargets(pointer, event, eventTarget, 'tap'); + } + }, + + // End interact move events and stop auto-scroll unless inertia is enabled + pointerEnd: function (pointer, event, eventTarget, curEventTarget) { + var endEvent, + target = this.target, + options = target && target.options, + inertiaOptions = options && this.prepared.name && options[this.prepared.name].inertia, + inertiaStatus = this.inertiaStatus; + + if (this.interacting()) { + + if (inertiaStatus.active && !inertiaStatus.ending) { return; } + + var pointerSpeed, + now = new Date().getTime(), + inertiaPossible = false, + inertia = false, + smoothEnd = false, + endSnap = checkSnap(target, this.prepared.name) && options[this.prepared.name].snap.endOnly, + endRestrict = checkRestrict(target, this.prepared.name) && options[this.prepared.name].restrict.endOnly, + dx = 0, + dy = 0, + startEvent; + + if (this.dragging) { + if (options.drag.axis === 'x' ) { pointerSpeed = Math.abs(this.pointerDelta.client.vx); } + else if (options.drag.axis === 'y' ) { pointerSpeed = Math.abs(this.pointerDelta.client.vy); } + else /*options.drag.axis === 'xy'*/{ pointerSpeed = this.pointerDelta.client.speed; } + } + else { + pointerSpeed = this.pointerDelta.client.speed; + } + + // check if inertia should be started + inertiaPossible = (inertiaOptions && inertiaOptions.enabled + && this.prepared.name !== 'gesture' + && event !== inertiaStatus.startEvent); + + inertia = (inertiaPossible + && (now - this.curCoords.timeStamp) < 50 + && pointerSpeed > inertiaOptions.minSpeed + && pointerSpeed > inertiaOptions.endSpeed); + + if (inertiaPossible && !inertia && (endSnap || endRestrict)) { + + var snapRestrict = {}; + + snapRestrict.snap = snapRestrict.restrict = snapRestrict; + + if (endSnap) { + this.setSnapping(this.curCoords.page, snapRestrict); + if (snapRestrict.locked) { + dx += snapRestrict.dx; + dy += snapRestrict.dy; + } + } + + if (endRestrict) { + this.setRestriction(this.curCoords.page, snapRestrict); + if (snapRestrict.restricted) { + dx += snapRestrict.dx; + dy += snapRestrict.dy; + } + } + + if (dx || dy) { + smoothEnd = true; + } + } + + if (inertia || smoothEnd) { + copyCoords(inertiaStatus.upCoords, this.curCoords); + + this.pointers[0] = inertiaStatus.startEvent = startEvent = + new InteractEvent(this, event, this.prepared.name, 'inertiastart', this.element); + + inertiaStatus.t0 = now; + + target.fire(inertiaStatus.startEvent); + + if (inertia) { + inertiaStatus.vx0 = this.pointerDelta.client.vx; + inertiaStatus.vy0 = this.pointerDelta.client.vy; + inertiaStatus.v0 = pointerSpeed; + + this.calcInertia(inertiaStatus); + + var page = extend({}, this.curCoords.page), + origin = getOriginXY(target, this.element), + statusObject; + + page.x = page.x + inertiaStatus.xe - origin.x; + page.y = page.y + inertiaStatus.ye - origin.y; + + statusObject = { + useStatusXY: true, + x: page.x, + y: page.y, + dx: 0, + dy: 0, + snap: null + }; + + statusObject.snap = statusObject; + + dx = dy = 0; + + if (endSnap) { + var snap = this.setSnapping(this.curCoords.page, statusObject); + + if (snap.locked) { + dx += snap.dx; + dy += snap.dy; + } + } + + if (endRestrict) { + var restrict = this.setRestriction(this.curCoords.page, statusObject); + + if (restrict.restricted) { + dx += restrict.dx; + dy += restrict.dy; + } + } + + inertiaStatus.modifiedXe += dx; + inertiaStatus.modifiedYe += dy; + + inertiaStatus.i = reqFrame(this.boundInertiaFrame); + } + else { + inertiaStatus.smoothEnd = true; + inertiaStatus.xe = dx; + inertiaStatus.ye = dy; + + inertiaStatus.sx = inertiaStatus.sy = 0; + + inertiaStatus.i = reqFrame(this.boundSmoothEndFrame); + } + + inertiaStatus.active = true; + return; + } + + if (endSnap || endRestrict) { + // fire a move event at the snapped coordinates + this.pointerMove(pointer, event, eventTarget, curEventTarget, true); + } + } + + if (this.dragging) { + endEvent = new InteractEvent(this, event, 'drag', 'end', this.element); + + var draggableElement = this.element, + drop = this.getDrop(endEvent, event, draggableElement); + + this.dropTarget = drop.dropzone; + this.dropElement = drop.element; + + var dropEvents = this.getDropEvents(event, endEvent); + + if (dropEvents.leave) { this.prevDropTarget.fire(dropEvents.leave); } + if (dropEvents.enter) { this.dropTarget.fire(dropEvents.enter); } + if (dropEvents.drop ) { this.dropTarget.fire(dropEvents.drop ); } + if (dropEvents.deactivate) { + this.fireActiveDrops(dropEvents.deactivate); + } + + target.fire(endEvent); + } + else if (this.resizing) { + endEvent = new InteractEvent(this, event, 'resize', 'end', this.element); + target.fire(endEvent); + } + else if (this.gesturing) { + endEvent = new InteractEvent(this, event, 'gesture', 'end', this.element); + target.fire(endEvent); + } + + this.stop(event); + }, + + collectDrops: function (element) { + var drops = [], + elements = [], + i; + + element = element || this.element; + + // collect all dropzones and their elements which qualify for a drop + for (i = 0; i < interactables.length; i++) { + if (!interactables[i].options.drop.enabled) { continue; } + + var current = interactables[i], + accept = current.options.drop.accept; + + // test the draggable element against the dropzone's accept setting + if ((isElement(accept) && accept !== element) + || (isString(accept) + && !matchesSelector(element, accept))) { + + continue; + } + + // query for new elements if necessary + var dropElements = current.selector? current._context.querySelectorAll(current.selector) : [current._element]; + + for (var j = 0, len = dropElements.length; j < len; j++) { + var currentElement = dropElements[j]; + + if (currentElement === element) { + continue; + } + + drops.push(current); + elements.push(currentElement); + } + } + + return { + dropzones: drops, + elements: elements + }; + }, + + fireActiveDrops: function (event) { + var i, + current, + currentElement, + prevElement; + + // loop through all active dropzones and trigger event + for (i = 0; i < this.activeDrops.dropzones.length; i++) { + current = this.activeDrops.dropzones[i]; + currentElement = this.activeDrops.elements [i]; + + // prevent trigger of duplicate events on same element + if (currentElement !== prevElement) { + // set current element as event target + event.target = currentElement; + current.fire(event); + } + prevElement = currentElement; + } + }, + + // Collect a new set of possible drops and save them in activeDrops. + // setActiveDrops should always be called when a drag has just started or a + // drag event happens while dynamicDrop is true + setActiveDrops: function (dragElement) { + // get dropzones and their elements that could receive the draggable + var possibleDrops = this.collectDrops(dragElement, true); + + this.activeDrops.dropzones = possibleDrops.dropzones; + this.activeDrops.elements = possibleDrops.elements; + this.activeDrops.rects = []; + + for (var i = 0; i < this.activeDrops.dropzones.length; i++) { + this.activeDrops.rects[i] = this.activeDrops.dropzones[i].getRect(this.activeDrops.elements[i]); + } + }, + + getDrop: function (dragEvent, event, dragElement) { + var validDrops = []; + + if (dynamicDrop) { + this.setActiveDrops(dragElement); + } + + // collect all dropzones and their elements which qualify for a drop + for (var j = 0; j < this.activeDrops.dropzones.length; j++) { + var current = this.activeDrops.dropzones[j], + currentElement = this.activeDrops.elements [j], + rect = this.activeDrops.rects [j]; + + validDrops.push(current.dropCheck(dragEvent, event, this.target, dragElement, currentElement, rect) + ? currentElement + : null); + } + + // get the most appropriate dropzone based on DOM depth and order + var dropIndex = indexOfDeepestElement(validDrops), + dropzone = this.activeDrops.dropzones[dropIndex] || null, + element = this.activeDrops.elements [dropIndex] || null; + + return { + dropzone: dropzone, + element: element + }; + }, + + getDropEvents: function (pointerEvent, dragEvent) { + var dropEvents = { + enter : null, + leave : null, + activate : null, + deactivate: null, + move : null, + drop : null + }; + + if (this.dropElement !== this.prevDropElement) { + // if there was a prevDropTarget, create a dragleave event + if (this.prevDropTarget) { + dropEvents.leave = { + target : this.prevDropElement, + dropzone : this.prevDropTarget, + relatedTarget: dragEvent.target, + draggable : dragEvent.interactable, + dragEvent : dragEvent, + interaction : this, + timeStamp : dragEvent.timeStamp, + type : 'dragleave' + }; + + dragEvent.dragLeave = this.prevDropElement; + dragEvent.prevDropzone = this.prevDropTarget; + } + // if the dropTarget is not null, create a dragenter event + if (this.dropTarget) { + dropEvents.enter = { + target : this.dropElement, + dropzone : this.dropTarget, + relatedTarget: dragEvent.target, + draggable : dragEvent.interactable, + dragEvent : dragEvent, + interaction : this, + timeStamp : dragEvent.timeStamp, + type : 'dragenter' + }; + + dragEvent.dragEnter = this.dropElement; + dragEvent.dropzone = this.dropTarget; + } + } + + if (dragEvent.type === 'dragend' && this.dropTarget) { + dropEvents.drop = { + target : this.dropElement, + dropzone : this.dropTarget, + relatedTarget: dragEvent.target, + draggable : dragEvent.interactable, + dragEvent : dragEvent, + interaction : this, + timeStamp : dragEvent.timeStamp, + type : 'drop' + }; + + dragEvent.dropzone = this.dropTarget; + } + if (dragEvent.type === 'dragstart') { + dropEvents.activate = { + target : null, + dropzone : null, + relatedTarget: dragEvent.target, + draggable : dragEvent.interactable, + dragEvent : dragEvent, + interaction : this, + timeStamp : dragEvent.timeStamp, + type : 'dropactivate' + }; + } + if (dragEvent.type === 'dragend') { + dropEvents.deactivate = { + target : null, + dropzone : null, + relatedTarget: dragEvent.target, + draggable : dragEvent.interactable, + dragEvent : dragEvent, + interaction : this, + timeStamp : dragEvent.timeStamp, + type : 'dropdeactivate' + }; + } + if (dragEvent.type === 'dragmove' && this.dropTarget) { + dropEvents.move = { + target : this.dropElement, + dropzone : this.dropTarget, + relatedTarget: dragEvent.target, + draggable : dragEvent.interactable, + dragEvent : dragEvent, + interaction : this, + dragmove : dragEvent, + timeStamp : dragEvent.timeStamp, + type : 'dropmove' + }; + dragEvent.dropzone = this.dropTarget; + } + + return dropEvents; + }, + + currentAction: function () { + return (this.dragging && 'drag') || (this.resizing && 'resize') || (this.gesturing && 'gesture') || null; + }, + + interacting: function () { + return this.dragging || this.resizing || this.gesturing; + }, + + clearTargets: function () { + this.target = this.element = null; + + this.dropTarget = this.dropElement = this.prevDropTarget = this.prevDropElement = null; + }, + + stop: function (event) { + if (this.interacting()) { + autoScroll.stop(); + this.matches = []; + this.matchElements = []; + + var target = this.target; + + if (target.options.styleCursor) { + target._doc.documentElement.style.cursor = ''; + } + + // prevent Default only if were previously interacting + if (event && isFunction(event.preventDefault)) { + this.checkAndPreventDefault(event, target, this.element); + } + + if (this.dragging) { + this.activeDrops.dropzones = this.activeDrops.elements = this.activeDrops.rects = null; + } + } + + this.clearTargets(); + + this.pointerIsDown = this.snapStatus.locked = this.dragging = this.resizing = this.gesturing = false; + this.prepared.name = this.prevEvent = null; + this.inertiaStatus.resumeDx = this.inertiaStatus.resumeDy = 0; + + // remove pointers if their ID isn't in this.pointerIds + for (var i = 0; i < this.pointers.length; i++) { + if (indexOf(this.pointerIds, getPointerId(this.pointers[i])) === -1) { + this.pointers.splice(i, 1); + } + } + }, + + inertiaFrame: function () { + var inertiaStatus = this.inertiaStatus, + options = this.target.options[this.prepared.name].inertia, + lambda = options.resistance, + t = new Date().getTime() / 1000 - inertiaStatus.t0; + + if (t < inertiaStatus.te) { + + var progress = 1 - (Math.exp(-lambda * t) - inertiaStatus.lambda_v0) / inertiaStatus.one_ve_v0; + + if (inertiaStatus.modifiedXe === inertiaStatus.xe && inertiaStatus.modifiedYe === inertiaStatus.ye) { + inertiaStatus.sx = inertiaStatus.xe * progress; + inertiaStatus.sy = inertiaStatus.ye * progress; + } + else { + var quadPoint = getQuadraticCurvePoint( + 0, 0, + inertiaStatus.xe, inertiaStatus.ye, + inertiaStatus.modifiedXe, inertiaStatus.modifiedYe, + progress); + + inertiaStatus.sx = quadPoint.x; + inertiaStatus.sy = quadPoint.y; + } + + this.pointerMove(inertiaStatus.startEvent, inertiaStatus.startEvent); + + inertiaStatus.i = reqFrame(this.boundInertiaFrame); + } + else { + inertiaStatus.ending = true; + + inertiaStatus.sx = inertiaStatus.modifiedXe; + inertiaStatus.sy = inertiaStatus.modifiedYe; + + this.pointerMove(inertiaStatus.startEvent, inertiaStatus.startEvent); + this.pointerEnd(inertiaStatus.startEvent, inertiaStatus.startEvent); + + inertiaStatus.active = inertiaStatus.ending = false; + } + }, + + smoothEndFrame: function () { + var inertiaStatus = this.inertiaStatus, + t = new Date().getTime() - inertiaStatus.t0, + duration = this.target.options[this.prepared.name].inertia.smoothEndDuration; + + if (t < duration) { + inertiaStatus.sx = easeOutQuad(t, 0, inertiaStatus.xe, duration); + inertiaStatus.sy = easeOutQuad(t, 0, inertiaStatus.ye, duration); + + this.pointerMove(inertiaStatus.startEvent, inertiaStatus.startEvent); + + inertiaStatus.i = reqFrame(this.boundSmoothEndFrame); + } + else { + inertiaStatus.ending = true; + + inertiaStatus.sx = inertiaStatus.xe; + inertiaStatus.sy = inertiaStatus.ye; + + this.pointerMove(inertiaStatus.startEvent, inertiaStatus.startEvent); + this.pointerEnd(inertiaStatus.startEvent, inertiaStatus.startEvent); + + inertiaStatus.smoothEnd = + inertiaStatus.active = inertiaStatus.ending = false; + } + }, + + addPointer: function (pointer) { + var id = getPointerId(pointer), + index = this.mouse? 0 : indexOf(this.pointerIds, id); + + if (index === -1) { + index = this.pointerIds.length; + } + + this.pointerIds[index] = id; + this.pointers[index] = pointer; + + return index; + }, + + removePointer: function (pointer) { + var id = getPointerId(pointer), + index = this.mouse? 0 : indexOf(this.pointerIds, id); + + if (index === -1) { return; } + + this.pointers .splice(index, 1); + this.pointerIds .splice(index, 1); + this.downTargets.splice(index, 1); + this.downTimes .splice(index, 1); + this.holdTimers .splice(index, 1); + }, + + recordPointer: function (pointer) { + var index = this.mouse? 0: indexOf(this.pointerIds, getPointerId(pointer)); + + if (index === -1) { return; } + + this.pointers[index] = pointer; + }, + + collectEventTargets: function (pointer, event, eventTarget, eventType) { + var pointerIndex = this.mouse? 0 : indexOf(this.pointerIds, getPointerId(pointer)); + + // do not fire a tap event if the pointer was moved before being lifted + if (eventType === 'tap' && (this.pointerWasMoved + // or if the pointerup target is different to the pointerdown target + || !(this.downTargets[pointerIndex] && this.downTargets[pointerIndex] === eventTarget))) { + return; + } + + var targets = [], + elements = [], + element = eventTarget; + + function collectSelectors (interactable, selector, context) { + var els = ie8MatchesSelector + ? context.querySelectorAll(selector) + : undefined; + + if (interactable._iEvents[eventType] + && isElement(element) + && inContext(interactable, element) + && !testIgnore(interactable, element, eventTarget) + && testAllow(interactable, element, eventTarget) + && matchesSelector(element, selector, els)) { + + targets.push(interactable); + elements.push(element); + } + } + + while (element) { + if (interact.isSet(element) && interact(element)._iEvents[eventType]) { + targets.push(interact(element)); + elements.push(element); + } + + interactables.forEachSelector(collectSelectors); + + element = parentElement(element); + } + + // create the tap event even if there are no listeners so that + // doubletap can still be created and fired + if (targets.length || eventType === 'tap') { + this.firePointers(pointer, event, eventTarget, targets, elements, eventType); + } + }, + + firePointers: function (pointer, event, eventTarget, targets, elements, eventType) { + var pointerIndex = this.mouse? 0 : indexOf(this.pointerIds, getPointerId(pointer)), + pointerEvent = {}, + i, + // for tap events + interval, createNewDoubleTap; + + // if it's a doubletap then the event properties would have been + // copied from the tap event and provided as the pointer argument + if (eventType === 'doubletap') { + pointerEvent = pointer; + } + else { + pointerExtend(pointerEvent, event); + if (event !== pointer) { + pointerExtend(pointerEvent, pointer); + } + + pointerEvent.preventDefault = preventOriginalDefault; + pointerEvent.stopPropagation = InteractEvent.prototype.stopPropagation; + pointerEvent.stopImmediatePropagation = InteractEvent.prototype.stopImmediatePropagation; + pointerEvent.interaction = this; + + pointerEvent.timeStamp = new Date().getTime(); + pointerEvent.originalEvent = event; + pointerEvent.originalPointer = pointer; + pointerEvent.type = eventType; + pointerEvent.pointerId = getPointerId(pointer); + pointerEvent.pointerType = this.mouse? 'mouse' : !supportsPointerEvent? 'touch' + : isString(pointer.pointerType) + ? pointer.pointerType + : [,,'touch', 'pen', 'mouse'][pointer.pointerType]; + } + + if (eventType === 'tap') { + pointerEvent.dt = pointerEvent.timeStamp - this.downTimes[pointerIndex]; + + interval = pointerEvent.timeStamp - this.tapTime; + createNewDoubleTap = !!(this.prevTap && this.prevTap.type !== 'doubletap' + && this.prevTap.target === pointerEvent.target + && interval < 500); + + pointerEvent.double = createNewDoubleTap; + + this.tapTime = pointerEvent.timeStamp; + } + + for (i = 0; i < targets.length; i++) { + pointerEvent.currentTarget = elements[i]; + pointerEvent.interactable = targets[i]; + targets[i].fire(pointerEvent); + + if (pointerEvent.immediatePropagationStopped + ||(pointerEvent.propagationStopped && elements[i + 1] !== pointerEvent.currentTarget)) { + break; + } + } + + if (createNewDoubleTap) { + var doubleTap = {}; + + extend(doubleTap, pointerEvent); + + doubleTap.dt = interval; + doubleTap.type = 'doubletap'; + + this.collectEventTargets(doubleTap, event, eventTarget, 'doubletap'); + + this.prevTap = doubleTap; + } + else if (eventType === 'tap') { + this.prevTap = pointerEvent; + } + }, + + validateSelector: function (pointer, event, matches, matchElements) { + for (var i = 0, len = matches.length; i < len; i++) { + var match = matches[i], + matchElement = matchElements[i], + action = validateAction(match.getAction(pointer, event, this, matchElement), match); + + if (action && withinInteractionLimit(match, matchElement, action)) { + this.target = match; + this.element = matchElement; + + return action; + } + } + }, + + setSnapping: function (pageCoords, status) { + var snap = this.target.options[this.prepared.name].snap, + targets = [], + target, + page, + i; + + status = status || this.snapStatus; + + if (status.useStatusXY) { + page = { x: status.x, y: status.y }; + } + else { + var origin = getOriginXY(this.target, this.element); + + page = extend({}, pageCoords); + + page.x -= origin.x; + page.y -= origin.y; + } + + status.realX = page.x; + status.realY = page.y; + + page.x = page.x - this.inertiaStatus.resumeDx; + page.y = page.y - this.inertiaStatus.resumeDy; + + var len = snap.targets? snap.targets.length : 0; + + for (var relIndex = 0; relIndex < this.snapOffsets.length; relIndex++) { + var relative = { + x: page.x - this.snapOffsets[relIndex].x, + y: page.y - this.snapOffsets[relIndex].y + }; + + for (i = 0; i < len; i++) { + if (isFunction(snap.targets[i])) { + target = snap.targets[i](relative.x, relative.y, this); + } + else { + target = snap.targets[i]; + } + + if (!target) { continue; } + + targets.push({ + x: isNumber(target.x) ? (target.x + this.snapOffsets[relIndex].x) : relative.x, + y: isNumber(target.y) ? (target.y + this.snapOffsets[relIndex].y) : relative.y, + + range: isNumber(target.range)? target.range: snap.range + }); + } + } + + var closest = { + target: null, + inRange: false, + distance: 0, + range: 0, + dx: 0, + dy: 0 + }; + + for (i = 0, len = targets.length; i < len; i++) { + target = targets[i]; + + var range = target.range, + dx = target.x - page.x, + dy = target.y - page.y, + distance = hypot(dx, dy), + inRange = distance <= range; + + // Infinite targets 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.target || (inRange + // is the closest target in range? + ? (closest.inRange && range !== Infinity + // the pointer is relatively deeper in this target + ? distance / range < closest.distance / closest.range + // this target has Infinite range and the closest doesn't + : (range === Infinity && closest.range !== Infinity) + // OR this target is closer that the previous closest + || distance < closest.distance) + // The other is not in range and the pointer is closer to this target + : (!closest.inRange && distance < closest.distance))) { + + if (range === Infinity) { + inRange = true; + } + + closest.target = target; + closest.distance = distance; + closest.range = range; + closest.inRange = inRange; + closest.dx = dx; + closest.dy = dy; + + status.range = range; + } + } + + var snapChanged; + + if (closest.target) { + snapChanged = (status.snappedX !== closest.target.x || status.snappedY !== closest.target.y); + + status.snappedX = closest.target.x; + status.snappedY = closest.target.y; + } + else { + snapChanged = true; + + status.snappedX = NaN; + status.snappedY = NaN; + } + + status.dx = closest.dx; + status.dy = closest.dy; + + status.changed = (snapChanged || (closest.inRange && !status.locked)); + status.locked = closest.inRange; + + return status; + }, + + setRestriction: function (pageCoords, status) { + var target = this.target, + restrict = target && target.options[this.prepared.name].restrict, + restriction = restrict && restrict.restriction, + page; + + if (!restriction) { + return status; + } + + status = status || this.restrictStatus; + + page = status.useStatusXY + ? page = { x: status.x, y: status.y } + : page = extend({}, pageCoords); + + if (status.snap && status.snap.locked) { + page.x += status.snap.dx || 0; + page.y += status.snap.dy || 0; + } + + page.x -= this.inertiaStatus.resumeDx; + page.y -= this.inertiaStatus.resumeDy; + + status.dx = 0; + status.dy = 0; + status.restricted = false; + + var rect, restrictedX, restrictedY; + + if (isString(restriction)) { + if (restriction === 'parent') { + restriction = parentElement(this.element); + } + else if (restriction === 'self') { + restriction = target.getRect(this.element); + } + else { + restriction = closest(this.element, restriction); + } + + if (!restriction) { return status; } + } + + if (isFunction(restriction)) { + restriction = restriction(page.x, page.y, this.element); + } + + if (isElement(restriction)) { + restriction = getElementRect(restriction); + } + + rect = restriction; + + if (!restriction) { + restrictedX = page.x; + restrictedY = page.y; + } + // object is assumed to have + // x, y, width, height or + // left, top, right, bottom + else if ('x' in restriction && 'y' in restriction) { + restrictedX = Math.max(Math.min(rect.x + rect.width - this.restrictOffset.right , page.x), rect.x + this.restrictOffset.left); + restrictedY = Math.max(Math.min(rect.y + rect.height - this.restrictOffset.bottom, page.y), rect.y + this.restrictOffset.top ); + } + else { + restrictedX = Math.max(Math.min(rect.right - this.restrictOffset.right , page.x), rect.left + this.restrictOffset.left); + restrictedY = Math.max(Math.min(rect.bottom - this.restrictOffset.bottom, page.y), rect.top + this.restrictOffset.top ); + } + + status.dx = restrictedX - page.x; + status.dy = restrictedY - page.y; + + status.changed = status.restrictedX !== restrictedX || status.restrictedY !== restrictedY; + status.restricted = !!(status.dx || status.dy); + + status.restrictedX = restrictedX; + status.restrictedY = restrictedY; + + return status; + }, + + checkAndPreventDefault: function (event, interactable, element) { + if (!(interactable = interactable || this.target)) { return; } + + var options = interactable.options, + prevent = options.preventDefault; + + if (prevent === 'auto' && element && !/^(input|select|textarea)$/i.test(event.target.nodeName)) { + // do not preventDefault on pointerdown if the prepared action is a drag + // and dragging can only start from a certain direction - this allows + // a touch to pan the viewport if a drag isn't in the right direction + if (/down|start/i.test(event.type) + && this.prepared.name === 'drag' && options.drag.axis !== 'xy') { + + return; + } + + // with manualStart, only preventDefault while interacting + if (options[this.prepared.name] && options[this.prepared.name].manualStart + && !this.interacting()) { + return; + } + + event.preventDefault(); + return; + } + + if (prevent === 'always') { + event.preventDefault(); + return; + } + }, + + calcInertia: function (status) { + var inertiaOptions = this.target.options[this.prepared.name].inertia, + lambda = inertiaOptions.resistance, + inertiaDur = -Math.log(inertiaOptions.endSpeed / status.v0) / lambda; + + status.x0 = this.prevEvent.pageX; + status.y0 = this.prevEvent.pageY; + status.t0 = status.startEvent.timeStamp / 1000; + status.sx = status.sy = 0; + + status.modifiedXe = status.xe = (status.vx0 - inertiaDur) / lambda; + status.modifiedYe = status.ye = (status.vy0 - inertiaDur) / lambda; + status.te = inertiaDur; + + status.lambda_v0 = lambda / status.v0; + status.one_ve_v0 = 1 - inertiaOptions.endSpeed / status.v0; + }, + + autoScrollMove: function (pointer) { + if (!(this.interacting() + && checkAutoScroll(this.target, this.prepared.name))) { + return; + } + + if (this.inertiaStatus.active) { + autoScroll.x = autoScroll.y = 0; + return; + } + + var top, + right, + bottom, + left, + options = this.target.options[this.prepared.name].autoScroll, + container = options.container || getWindow(this.element); + + if (isWindow(container)) { + left = pointer.clientX < autoScroll.margin; + top = pointer.clientY < autoScroll.margin; + right = pointer.clientX > container.innerWidth - autoScroll.margin; + bottom = pointer.clientY > container.innerHeight - autoScroll.margin; + } + else { + var rect = getElementClientRect(container); + + left = pointer.clientX < rect.left + autoScroll.margin; + top = pointer.clientY < rect.top + autoScroll.margin; + right = pointer.clientX > rect.right - autoScroll.margin; + bottom = pointer.clientY > rect.bottom - autoScroll.margin; + } + + autoScroll.x = (right ? 1: left? -1: 0); + autoScroll.y = (bottom? 1: top? -1: 0); + + if (!autoScroll.isScrolling) { + // set the autoScroll properties to those of the target + autoScroll.margin = options.margin; + autoScroll.speed = options.speed; + + autoScroll.start(this); + } + }, + + _updateEventTargets: function (target, currentTarget) { + this._eventTarget = target; + this._curEventTarget = currentTarget; + } + + }; + + function getInteractionFromPointer (pointer, eventType, eventTarget) { + var i = 0, len = interactions.length, + mouseEvent = (/mouse/i.test(pointer.pointerType || eventType) + // MSPointerEvent.MSPOINTER_TYPE_MOUSE + || pointer.pointerType === 4), + interaction; + + var id = getPointerId(pointer); + + // try to resume inertia with a new pointer + if (/down|start/i.test(eventType)) { + for (i = 0; i < len; i++) { + interaction = interactions[i]; + + var element = eventTarget; + + if (interaction.inertiaStatus.active && interaction.target.options[interaction.prepared.name].inertia.allowResume + && (interaction.mouse === mouseEvent)) { + while (element) { + // if the element is the interaction element + if (element === interaction.element) { + return interaction; + } + element = parentElement(element); + } + } + } + } + + // if it's a mouse interaction + if (mouseEvent || !(supportsTouch || supportsPointerEvent)) { + + // find a mouse interaction that's not in inertia phase + for (i = 0; i < len; i++) { + if (interactions[i].mouse && !interactions[i].inertiaStatus.active) { + return interactions[i]; + } + } + + // find any interaction specifically for mouse. + // if the eventType is a mousedown, and inertia is active + // ignore the interaction + for (i = 0; i < len; i++) { + if (interactions[i].mouse && !(/down/.test(eventType) && interactions[i].inertiaStatus.active)) { + return interaction; + } + } + + // create a new interaction for mouse + interaction = new Interaction(); + interaction.mouse = true; + + return interaction; + } + + // get interaction that has this pointer + for (i = 0; i < len; i++) { + if (contains(interactions[i].pointerIds, id)) { + return interactions[i]; + } + } + + // at this stage, a pointerUp should not return an interaction + if (/up|end|out/i.test(eventType)) { + return null; + } + + // get first idle interaction + for (i = 0; i < len; i++) { + interaction = interactions[i]; + + if ((!interaction.prepared.name || (interaction.target.options.gesture.enabled)) + && !interaction.interacting() + && !(!mouseEvent && interaction.mouse)) { + + return interaction; + } + } + + return new Interaction(); + } + + function doOnInteractions (method) { + return (function (event) { + var interaction, + eventTarget = getActualElement(event.path + ? event.path[0] + : event.target), + curEventTarget = getActualElement(event.currentTarget), + i; + + if (supportsTouch && /touch/.test(event.type)) { + prevTouchTime = new Date().getTime(); + + for (i = 0; i < event.changedTouches.length; i++) { + var pointer = event.changedTouches[i]; + + interaction = getInteractionFromPointer(pointer, event.type, eventTarget); + + if (!interaction) { continue; } + + interaction._updateEventTargets(eventTarget, curEventTarget); + + interaction[method](pointer, event, eventTarget, curEventTarget); + } + } + else { + if (!supportsPointerEvent && /mouse/.test(event.type)) { + // ignore mouse events while touch interactions are active + for (i = 0; i < interactions.length; i++) { + if (!interactions[i].mouse && interactions[i].pointerIsDown) { + return; + } + } + + // try to ignore mouse events that are simulated by the browser + // after a touch event + if (new Date().getTime() - prevTouchTime < 500) { + return; + } + } + + interaction = getInteractionFromPointer(event, event.type, eventTarget); + + if (!interaction) { return; } + + interaction._updateEventTargets(eventTarget, curEventTarget); + + interaction[method](event, event, eventTarget, curEventTarget); + } + }); + } + + function InteractEvent (interaction, event, action, phase, element, related) { + var client, + page, + target = interaction.target, + snapStatus = interaction.snapStatus, + restrictStatus = interaction.restrictStatus, + pointers = interaction.pointers, + deltaSource = (target && target.options || defaultOptions).deltaSource, + sourceX = deltaSource + 'X', + sourceY = deltaSource + 'Y', + options = target? target.options: defaultOptions, + origin = getOriginXY(target, element), + starting = phase === 'start', + ending = phase === 'end', + coords = starting? interaction.startCoords : interaction.curCoords; + + element = element || interaction.element; + + page = extend({}, coords.page); + client = extend({}, coords.client); + + page.x -= origin.x; + page.y -= origin.y; + + client.x -= origin.x; + client.y -= origin.y; + + var relativePoints = options[action].snap && options[action].snap.relativePoints ; + + if (checkSnap(target, action) && !(starting && relativePoints && relativePoints.length)) { + this.snap = { + range : snapStatus.range, + locked : snapStatus.locked, + x : snapStatus.snappedX, + y : snapStatus.snappedY, + realX : snapStatus.realX, + realY : snapStatus.realY, + dx : snapStatus.dx, + dy : snapStatus.dy + }; + + if (snapStatus.locked) { + page.x += snapStatus.dx; + page.y += snapStatus.dy; + client.x += snapStatus.dx; + client.y += snapStatus.dy; + } + } + + if (checkRestrict(target, action) && !(starting && options[action].restrict.elementRect) && restrictStatus.restricted) { + page.x += restrictStatus.dx; + page.y += restrictStatus.dy; + client.x += restrictStatus.dx; + client.y += restrictStatus.dy; + + this.restrict = { + dx: restrictStatus.dx, + dy: restrictStatus.dy + }; + } + + this.pageX = page.x; + this.pageY = page.y; + this.clientX = client.x; + this.clientY = client.y; + + this.x0 = interaction.startCoords.page.x - origin.x; + this.y0 = interaction.startCoords.page.y - origin.y; + this.clientX0 = interaction.startCoords.client.x - origin.x; + this.clientY0 = interaction.startCoords.client.y - origin.y; + this.ctrlKey = event.ctrlKey; + this.altKey = event.altKey; + this.shiftKey = event.shiftKey; + this.metaKey = event.metaKey; + this.button = event.button; + this.buttons = event.buttons; + this.target = element; + this.t0 = interaction.downTimes[0]; + this.type = action + (phase || ''); + + this.interaction = interaction; + this.interactable = target; + + var inertiaStatus = interaction.inertiaStatus; + + if (inertiaStatus.active) { + this.detail = 'inertia'; + } + + if (related) { + this.relatedTarget = related; + } + + // end event dx, dy is difference between start and end points + if (ending) { + if (deltaSource === 'client') { + this.dx = client.x - interaction.startCoords.client.x; + this.dy = client.y - interaction.startCoords.client.y; + } + else { + this.dx = page.x - interaction.startCoords.page.x; + this.dy = page.y - interaction.startCoords.page.y; + } + } + else if (starting) { + this.dx = 0; + this.dy = 0; + } + // copy properties from previousmove if starting inertia + else if (phase === 'inertiastart') { + this.dx = interaction.prevEvent.dx; + this.dy = interaction.prevEvent.dy; + } + else { + if (deltaSource === 'client') { + this.dx = client.x - interaction.prevEvent.clientX; + this.dy = client.y - interaction.prevEvent.clientY; + } + else { + this.dx = page.x - interaction.prevEvent.pageX; + this.dy = page.y - interaction.prevEvent.pageY; + } + } + if (interaction.prevEvent && interaction.prevEvent.detail === 'inertia' + && !inertiaStatus.active + && options[action].inertia && options[action].inertia.zeroResumeDelta) { + + inertiaStatus.resumeDx += this.dx; + inertiaStatus.resumeDy += this.dy; + + this.dx = this.dy = 0; + } + + if (action === 'resize' && interaction.resizeAxes) { + if (options.resize.square) { + if (interaction.resizeAxes === 'y') { + this.dx = this.dy; + } + else { + this.dy = this.dx; + } + this.axes = 'xy'; + } + else { + this.axes = interaction.resizeAxes; + + if (interaction.resizeAxes === 'x') { + this.dy = 0; + } + else if (interaction.resizeAxes === 'y') { + this.dx = 0; + } + } + } + else if (action === 'gesture') { + this.touches = [pointers[0], pointers[1]]; + + if (starting) { + this.distance = touchDistance(pointers, deltaSource); + this.box = touchBBox(pointers); + this.scale = 1; + this.ds = 0; + this.angle = touchAngle(pointers, undefined, deltaSource); + this.da = 0; + } + else if (ending || event instanceof InteractEvent) { + this.distance = interaction.prevEvent.distance; + this.box = interaction.prevEvent.box; + this.scale = interaction.prevEvent.scale; + this.ds = this.scale - 1; + this.angle = interaction.prevEvent.angle; + this.da = this.angle - interaction.gesture.startAngle; + } + else { + this.distance = touchDistance(pointers, deltaSource); + this.box = touchBBox(pointers); + this.scale = this.distance / interaction.gesture.startDistance; + this.angle = touchAngle(pointers, interaction.gesture.prevAngle, deltaSource); + + this.ds = this.scale - interaction.gesture.prevScale; + this.da = this.angle - interaction.gesture.prevAngle; + } + } + + if (starting) { + this.timeStamp = interaction.downTimes[0]; + this.dt = 0; + this.duration = 0; + this.speed = 0; + this.velocityX = 0; + this.velocityY = 0; + } + else if (phase === 'inertiastart') { + this.timeStamp = interaction.prevEvent.timeStamp; + this.dt = interaction.prevEvent.dt; + this.duration = interaction.prevEvent.duration; + this.speed = interaction.prevEvent.speed; + this.velocityX = interaction.prevEvent.velocityX; + this.velocityY = interaction.prevEvent.velocityY; + } + else { + this.timeStamp = new Date().getTime(); + this.dt = this.timeStamp - interaction.prevEvent.timeStamp; + this.duration = this.timeStamp - interaction.downTimes[0]; + + if (event instanceof InteractEvent) { + var dx = this[sourceX] - interaction.prevEvent[sourceX], + dy = this[sourceY] - interaction.prevEvent[sourceY], + dt = this.dt / 1000; + + this.speed = hypot(dx, dy) / dt; + this.velocityX = dx / dt; + this.velocityY = dy / dt; + } + // if normal move or end event, use previous user event coords + else { + // speed and velocity in pixels per second + this.speed = interaction.pointerDelta[deltaSource].speed; + this.velocityX = interaction.pointerDelta[deltaSource].vx; + this.velocityY = interaction.pointerDelta[deltaSource].vy; + } + } + + if ((ending || phase === 'inertiastart') + && interaction.prevEvent.speed > 600 && this.timeStamp - interaction.prevEvent.timeStamp < 150) { + + var angle = 180 * Math.atan2(interaction.prevEvent.velocityY, interaction.prevEvent.velocityX) / Math.PI, + overlap = 22.5; + + if (angle < 0) { + angle += 360; + } + + var left = 135 - overlap <= angle && angle < 225 + overlap, + up = 225 - overlap <= angle && angle < 315 + overlap, + + right = !left && (315 - overlap <= angle || angle < 45 + overlap), + down = !up && 45 - overlap <= angle && angle < 135 + overlap; + + this.swipe = { + up : up, + down : down, + left : left, + right: right, + angle: angle, + speed: interaction.prevEvent.speed, + velocity: { + x: interaction.prevEvent.velocityX, + y: interaction.prevEvent.velocityY + } + }; + } + } + + InteractEvent.prototype = { + preventDefault: blank, + stopImmediatePropagation: function () { + this.immediatePropagationStopped = this.propagationStopped = true; + }, + stopPropagation: function () { + this.propagationStopped = true; + } + }; + + function preventOriginalDefault () { + this.originalEvent.preventDefault(); + } + + function getActionCursor (action) { + var cursor = ''; + + if (action.name === 'drag') { + cursor = actionCursors.drag; + } + if (action.name === 'resize') { + if (action.axis) { + cursor = actionCursors[action.name + action.axis]; + } + else if (action.edges) { + var cursorKey = 'resize', + edgeNames = ['top', 'bottom', 'left', 'right']; + + for (var i = 0; i < 4; i++) { + if (action.edges[edgeNames[i]]) { + cursorKey += edgeNames[i]; + } + } + + cursor = actionCursors[cursorKey]; + } + } + + return cursor; + } + + function checkResizeEdge (name, value, page, element, interactableElement, rect, margin) { + // false, '', undefined, null + if (!value) { return false; } + + // true value, use pointer coords and element rect + if (value === true) { + // if dimensions are negative, "switch" edges + var width = isNumber(rect.width)? rect.width : rect.right - rect.left, + height = isNumber(rect.height)? rect.height : rect.bottom - rect.top; + + if (width < 0) { + if (name === 'left' ) { name = 'right'; } + else if (name === 'right') { name = 'left' ; } + } + if (height < 0) { + if (name === 'top' ) { name = 'bottom'; } + else if (name === 'bottom') { name = 'top' ; } + } + + if (name === 'left' ) { return page.x < ((width >= 0? rect.left: rect.right ) + margin); } + if (name === 'top' ) { return page.y < ((height >= 0? rect.top : rect.bottom) + margin); } + + if (name === 'right' ) { return page.x > ((width >= 0? rect.right : rect.left) - margin); } + if (name === 'bottom') { return page.y > ((height >= 0? rect.bottom: rect.top ) - margin); } + } + + // the remaining checks require an element + if (!isElement(element)) { return false; } + + return isElement(value) + // the value is an element to use as a resize handle + ? value === element + // otherwise check if element matches value as selector + : matchesUpTo(element, value, interactableElement); + } + + function defaultActionChecker (pointer, interaction, element) { + var rect = this.getRect(element), + shouldResize = false, + action = null, + resizeAxes = null, + resizeEdges, + page = extend({}, interaction.curCoords.page), + options = this.options; + + if (!rect) { return null; } + + if (actionIsEnabled.resize && options.resize.enabled) { + var resizeOptions = options.resize; + + resizeEdges = { + left: false, right: false, top: false, bottom: false + }; + + // if using resize.edges + if (isObject(resizeOptions.edges)) { + for (var edge in resizeEdges) { + resizeEdges[edge] = checkResizeEdge(edge, + resizeOptions.edges[edge], + page, + interaction._eventTarget, + element, + rect, + resizeOptions.margin || margin); + } + + resizeEdges.left = resizeEdges.left && !resizeEdges.right; + resizeEdges.top = resizeEdges.top && !resizeEdges.bottom; + + shouldResize = resizeEdges.left || resizeEdges.right || resizeEdges.top || resizeEdges.bottom; + } + else { + var right = options.resize.axis !== 'y' && page.x > (rect.right - margin), + bottom = options.resize.axis !== 'x' && page.y > (rect.bottom - margin); + + shouldResize = right || bottom; + resizeAxes = (right? 'x' : '') + (bottom? 'y' : ''); + } + } + + action = shouldResize + ? 'resize' + : actionIsEnabled.drag && options.drag.enabled + ? 'drag' + : null; + + if (actionIsEnabled.gesture + && interaction.pointerIds.length >=2 + && !(interaction.dragging || interaction.resizing)) { + action = 'gesture'; + } + + if (action) { + return { + name: action, + axis: resizeAxes, + edges: resizeEdges + }; + } + + return null; + } + + // Check if action is enabled globally and the current target supports it + // If so, return the validated action. Otherwise, return null + function validateAction (action, interactable) { + if (!isObject(action)) { return null; } + + var actionName = action.name, + options = interactable.options; + + if (( (actionName === 'resize' && options.resize.enabled ) + || (actionName === 'drag' && options.drag.enabled ) + || (actionName === 'gesture' && options.gesture.enabled)) + && actionIsEnabled[actionName]) { + + if (actionName === 'resize' || actionName === 'resizeyx') { + actionName = 'resizexy'; + } + + return action; + } + return null; + } + + var listeners = {}, + interactionListeners = [ + 'dragStart', 'dragMove', 'resizeStart', 'resizeMove', 'gestureStart', 'gestureMove', + 'pointerOver', 'pointerOut', 'pointerHover', 'selectorDown', + 'pointerDown', 'pointerMove', 'pointerUp', 'pointerCancel', 'pointerEnd', + 'addPointer', 'removePointer', 'recordPointer', 'autoScrollMove' + ]; + + for (var i = 0, len = interactionListeners.length; i < len; i++) { + var name = interactionListeners[i]; + + listeners[name] = doOnInteractions(name); + } + + // bound to the interactable context when a DOM event + // listener is added to a selector interactable + function delegateListener (event, useCapture) { + var fakeEvent = {}, + delegated = delegatedEvents[event.type], + eventTarget = getActualElement(event.path + ? event.path[0] + : event.target), + element = eventTarget; + + useCapture = useCapture? true: false; + + // duplicate the event so that currentTarget can be changed + for (var prop in event) { + fakeEvent[prop] = event[prop]; + } + + fakeEvent.originalEvent = event; + fakeEvent.preventDefault = preventOriginalDefault; + + // climb up document tree looking for selector matches + while (isElement(element)) { + for (var i = 0; i < delegated.selectors.length; i++) { + var selector = delegated.selectors[i], + context = delegated.contexts[i]; + + if (matchesSelector(element, selector) + && nodeContains(context, eventTarget) + && nodeContains(context, element)) { + + var listeners = delegated.listeners[i]; + + fakeEvent.currentTarget = element; + + for (var j = 0; j < listeners.length; j++) { + if (listeners[j][1] === useCapture) { + listeners[j][0](fakeEvent); + } + } + } + } + + element = parentElement(element); + } + } + + function delegateUseCapture (event) { + return delegateListener.call(this, event, true); + } + + interactables.indexOfElement = function indexOfElement (element, context) { + context = context || document; + + for (var i = 0; i < this.length; i++) { + var interactable = this[i]; + + if ((interactable.selector === element + && (interactable._context === context)) + || (!interactable.selector && interactable._element === element)) { + + return i; + } + } + return -1; + }; + + interactables.get = function interactableGet (element, options) { + return this[this.indexOfElement(element, options && options.context)]; + }; + + interactables.forEachSelector = function (callback) { + for (var i = 0; i < this.length; i++) { + var interactable = this[i]; + + if (!interactable.selector) { + continue; + } + + var ret = callback(interactable, interactable.selector, interactable._context, i, this); + + if (ret !== undefined) { + return ret; + } + } + }; + + /*\ + * interact + [ method ] + * + * The methods of this variable can be used to set elements as + * interactables and also to change various default settings. + * + * Calling it as a function and passing an element or a valid CSS selector + * string returns an Interactable object which has various methods to + * configure it. + * + - element (Element | string) The HTML or SVG Element to interact with or CSS selector + = (object) An @Interactable + * + > Usage + | interact(document.getElementById('draggable')).draggable(true); + | + | var rectables = interact('rect'); + | rectables + | .gesturable(true) + | .on('gesturemove', function (event) { + | // something cool... + | }) + | .autoScroll(true); + \*/ + function interact (element, options) { + return interactables.get(element, options) || new Interactable(element, options); + } + + /*\ + * Interactable + [ property ] + ** + * Object type returned by @interact + \*/ + function Interactable (element, options) { + this._element = element; + this._iEvents = this._iEvents || {}; + + var _window; + + if (trySelector(element)) { + this.selector = element; + + var context = options && options.context; + + _window = context? getWindow(context) : window; + + if (context && (_window.Node + ? context instanceof _window.Node + : (isElement(context) || context === _window.document))) { + + this._context = context; + } + } + else { + _window = getWindow(element); + + if (isElement(element, _window)) { + + if (supportsPointerEvent) { + events.add(this._element, pEventTypes.down, listeners.pointerDown ); + events.add(this._element, pEventTypes.move, listeners.pointerHover); + } + else { + events.add(this._element, 'mousedown' , listeners.pointerDown ); + events.add(this._element, 'mousemove' , listeners.pointerHover); + events.add(this._element, 'touchstart', listeners.pointerDown ); + events.add(this._element, 'touchmove' , listeners.pointerHover); + } + } + } + + this._doc = _window.document; + + if (!contains(documents, this._doc)) { + listenToDocument(this._doc); + } + + interactables.push(this); + + this.set(options); + } + + Interactable.prototype = { + setOnEvents: function (action, phases) { + if (action === 'drop') { + if (isFunction(phases.ondrop) ) { this.ondrop = phases.ondrop ; } + if (isFunction(phases.ondropactivate) ) { this.ondropactivate = phases.ondropactivate ; } + if (isFunction(phases.ondropdeactivate)) { this.ondropdeactivate = phases.ondropdeactivate; } + if (isFunction(phases.ondragenter) ) { this.ondragenter = phases.ondragenter ; } + if (isFunction(phases.ondragleave) ) { this.ondragleave = phases.ondragleave ; } + if (isFunction(phases.ondropmove) ) { this.ondropmove = phases.ondropmove ; } + } + else { + action = 'on' + action; + + if (isFunction(phases.onstart) ) { this[action + 'start' ] = phases.onstart ; } + if (isFunction(phases.onmove) ) { this[action + 'move' ] = phases.onmove ; } + if (isFunction(phases.onend) ) { this[action + 'end' ] = phases.onend ; } + if (isFunction(phases.oninertiastart)) { this[action + 'inertiastart' ] = phases.oninertiastart ; } + } + + return this; + }, + + /*\ + * Interactable.draggable + [ method ] + * + * Gets or sets whether drag actions can be performed on the + * Interactable + * + = (boolean) Indicates if this can be the target of drag events + | var isDraggable = interact('ul li').draggable(); + * or + - options (boolean | object) #optional true/false or An object with event listeners to be fired on drag events (object makes the Interactable draggable) + = (object) This Interactable + | interact(element).draggable({ + | onstart: function (event) {}, + | onmove : function (event) {}, + | onend : function (event) {}, + | + | // the axis in which the first movement must be + | // for the drag sequence to start + | // 'xy' by default - any direction + | axis: 'x' || 'y' || 'xy', + | + | // max number of drags that can happen concurrently + | // with elements of this Interactable. Infinity by default + | max: Infinity, + | + | // max number of drags that can target the same element+Interactable + | // 1 by default + | maxPerElement: 2 + | }); + \*/ + draggable: function (options) { + if (isObject(options)) { + this.options.drag.enabled = options.enabled === false? false: true; + this.setPerAction('drag', options); + this.setOnEvents('drag', options); + + if (/^x$|^y$|^xy$/.test(options.axis)) { + this.options.drag.axis = options.axis; + } + else if (options.axis === null) { + delete this.options.drag.axis; + } + + return this; + } + + if (isBool(options)) { + this.options.drag.enabled = options; + + return this; + } + + return this.options.drag; + }, + + setPerAction: function (action, options) { + // for all the default per-action options + for (var option in options) { + // if this option exists for this action + if (option in defaultOptions[action]) { + // if the option in the options arg is an object value + if (isObject(options[option])) { + // duplicate the object + this.options[action][option] = extend(this.options[action][option] || {}, options[option]); + + if (isObject(defaultOptions.perAction[option]) && 'enabled' in defaultOptions.perAction[option]) { + this.options[action][option].enabled = options[option].enabled === false? false : true; + } + } + else if (isBool(options[option]) && isObject(defaultOptions.perAction[option])) { + this.options[action][option].enabled = options[option]; + } + else if (options[option] !== undefined) { + // or if it's not undefined, do a plain assignment + this.options[action][option] = options[option]; + } + } + } + }, + + /*\ + * Interactable.dropzone + [ method ] + * + * Returns or sets whether elements can be dropped onto this + * Interactable to trigger drop events + * + * Dropzones can receive the following events: + * - `dropactivate` and `dropdeactivate` when an acceptable drag starts and ends + * - `dragenter` and `dragleave` when a draggable enters and leaves the dropzone + * - `dragmove` when a draggable that has entered the dropzone is moved + * - `drop` when a draggable is dropped into this dropzone + * + * Use the `accept` option to allow only elements that match the given CSS selector or element. + * + * Use the `overlap` option to set how drops are checked for. The allowed values are: + * - `'pointer'`, the pointer must be over the dropzone (default) + * - `'center'`, the draggable element's center must be over the dropzone + * - a number from 0-1 which is the `(intersection area) / (draggable area)`. + * e.g. `0.5` for drop to happen when half of the area of the + * draggable is over the dropzone + * + - options (boolean | object | null) #optional The new value to be set. + | interact('.drop').dropzone({ + | accept: '.can-drop' || document.getElementById('single-drop'), + | overlap: 'pointer' || 'center' || zeroToOne + | } + = (boolean | object) The current setting or this Interactable + \*/ + dropzone: function (options) { + if (isObject(options)) { + this.options.drop.enabled = options.enabled === false? false: true; + this.setOnEvents('drop', options); + + if (/^(pointer|center)$/.test(options.overlap)) { + this.options.drop.overlap = options.overlap; + } + else if (isNumber(options.overlap)) { + this.options.drop.overlap = Math.max(Math.min(1, options.overlap), 0); + } + if ('accept' in options) { + this.options.drop.accept = options.accept; + } + if ('checker' in options) { + this.options.drop.checker = options.checker; + } + + return this; + } + + if (isBool(options)) { + this.options.drop.enabled = options; + + return this; + } + + return this.options.drop; + }, + + dropCheck: function (dragEvent, event, draggable, draggableElement, dropElement, rect) { + var dropped = false; + + // if the dropzone has no rect (eg. display: none) + // call the custom dropChecker or just return false + if (!(rect = rect || this.getRect(dropElement))) { + return (this.options.drop.checker + ? this.options.drop.checker(dragEvent, event, dropped, this, dropElement, draggable, draggableElement) + : false); + } + + var dropOverlap = this.options.drop.overlap; + + if (dropOverlap === 'pointer') { + var page = getPageXY(dragEvent), + origin = getOriginXY(draggable, draggableElement), + horizontal, + vertical; + + page.x += origin.x; + page.y += origin.y; + + horizontal = (page.x > rect.left) && (page.x < rect.right); + vertical = (page.y > rect.top ) && (page.y < rect.bottom); + + dropped = horizontal && vertical; + } + + var dragRect = draggable.getRect(draggableElement); + + if (dropOverlap === 'center') { + var cx = dragRect.left + dragRect.width / 2, + cy = dragRect.top + dragRect.height / 2; + + dropped = cx >= rect.left && cx <= rect.right && cy >= rect.top && cy <= rect.bottom; + } + + if (isNumber(dropOverlap)) { + var overlapArea = (Math.max(0, Math.min(rect.right , dragRect.right ) - Math.max(rect.left, dragRect.left)) + * Math.max(0, Math.min(rect.bottom, dragRect.bottom) - Math.max(rect.top , dragRect.top ))), + overlapRatio = overlapArea / (dragRect.width * dragRect.height); + + dropped = overlapRatio >= dropOverlap; + } + + if (this.options.drop.checker) { + dropped = this.options.drop.checker(dragEvent, event, dropped, this, dropElement, draggable, draggableElement); + } + + return dropped; + }, + + /*\ + * Interactable.dropChecker + [ method ] + * + * DEPRECATED. Use interactable.dropzone({ checker: function... }) instead. + * + * Gets or sets the function used to check if a dragged element is + * over this Interactable. + * + - checker (function) #optional The function that will be called when checking for a drop + = (Function | Interactable) The checker function or this Interactable + * + * The checker function takes the following arguments: + * + - dragEvent (InteractEvent) The related dragmove or dragend event + - event (TouchEvent | PointerEvent | MouseEvent) The user move/up/end Event related to the dragEvent + - dropped (boolean) The value from the default drop checker + - dropzone (Interactable) The dropzone interactable + - dropElement (Element) The dropzone element + - draggable (Interactable) The Interactable being dragged + - draggableElement (Element) The actual element that's being dragged + * + > Usage: + | interact(target) + | .dropChecker(function(dragEvent, // related dragmove or dragend event + | event, // TouchEvent/PointerEvent/MouseEvent + | dropped, // bool result of the default checker + | dropzone, // dropzone Interactable + | dropElement, // dropzone elemnt + | draggable, // draggable Interactable + | draggableElement) {// draggable element + | + | return dropped && event.target.hasAttribute('allow-drop'); + | } + \*/ + dropChecker: function (checker) { + if (isFunction(checker)) { + this.options.drop.checker = checker; + + return this; + } + if (checker === null) { + delete this.options.getRect; + + return this; + } + + return this.options.drop.checker; + }, + + /*\ + * Interactable.accept + [ method ] + * + * Deprecated. add an `accept` property to the options object passed to + * @Interactable.dropzone instead. + * + * Gets or sets the Element or CSS selector match that this + * Interactable accepts if it is a dropzone. + * + - newValue (Element | string | null) #optional + * If it is an Element, then only that element can be dropped into this dropzone. + * If it is a string, the element being dragged must match it as a selector. + * If it is null, the accept options is cleared - it accepts any element. + * + = (string | Element | null | Interactable) The current accept option if given `undefined` or this Interactable + \*/ + accept: function (newValue) { + if (isElement(newValue)) { + this.options.drop.accept = newValue; + + return this; + } + + // test if it is a valid CSS selector + if (trySelector(newValue)) { + this.options.drop.accept = newValue; + + return this; + } + + if (newValue === null) { + delete this.options.drop.accept; + + return this; + } + + return this.options.drop.accept; + }, + + /*\ + * Interactable.resizable + [ method ] + * + * Gets or sets whether resize actions can be performed on the + * Interactable + * + = (boolean) Indicates if this can be the target of resize elements + | var isResizeable = interact('input[type=text]').resizable(); + * or + - options (boolean | object) #optional true/false or An object with event listeners to be fired on resize events (object makes the Interactable resizable) + = (object) This Interactable + | interact(element).resizable({ + | onstart: function (event) {}, + | onmove : function (event) {}, + | onend : function (event) {}, + | + | edges: { + | top : true, // Use pointer coords to check for resize. + | left : false, // Disable resizing from left edge. + | bottom: '.resize-s',// Resize if pointer target matches selector + | right : handleEl // Resize if pointer target is the given Element + | }, + | + | // Width and height can be adjusted independently. When `true`, width and + | // height are adjusted at a 1:1 ratio. + | square: false, + | + | // Width and height can be adjusted independently. When `true`, width and + | // height maintain the aspect ratio they had when resizing started. + | preserveAspectRatio: false, + | + | // a value of 'none' will limit the resize rect to a minimum of 0x0 + | // 'negate' will allow the rect to have negative width/height + | // 'reposition' will keep the width/height positive by swapping + | // the top and bottom edges and/or swapping the left and right edges + | invert: 'none' || 'negate' || 'reposition' + | + | // limit multiple resizes. + | // See the explanation in the @Interactable.draggable example + | max: Infinity, + | maxPerElement: 1, + | }); + \*/ + resizable: function (options) { + if (isObject(options)) { + this.options.resize.enabled = options.enabled === false? false: true; + this.setPerAction('resize', options); + this.setOnEvents('resize', options); + + if (/^x$|^y$|^xy$/.test(options.axis)) { + this.options.resize.axis = options.axis; + } + else if (options.axis === null) { + this.options.resize.axis = defaultOptions.resize.axis; + } + + if (isBool(options.preserveAspectRatio)) { + this.options.resize.preserveAspectRatio = options.preserveAspectRatio; + } + else if (isBool(options.square)) { + this.options.resize.square = options.square; + } + + return this; + } + if (isBool(options)) { + this.options.resize.enabled = options; + + return this; + } + return this.options.resize; + }, + + /*\ + * Interactable.squareResize + [ method ] + * + * Deprecated. Add a `square: true || false` property to @Interactable.resizable instead + * + * Gets or sets whether resizing is forced 1:1 aspect + * + = (boolean) Current setting + * + * or + * + - newValue (boolean) #optional + = (object) this Interactable + \*/ + squareResize: function (newValue) { + if (isBool(newValue)) { + this.options.resize.square = newValue; + + return this; + } + + if (newValue === null) { + delete this.options.resize.square; + + return this; + } + + return this.options.resize.square; + }, + + /*\ + * Interactable.gesturable + [ method ] + * + * Gets or sets whether multitouch gestures can be performed on the + * Interactable's element + * + = (boolean) Indicates if this can be the target of gesture events + | var isGestureable = interact(element).gesturable(); + * or + - options (boolean | object) #optional true/false or An object with event listeners to be fired on gesture events (makes the Interactable gesturable) + = (object) this Interactable + | interact(element).gesturable({ + | onstart: function (event) {}, + | onmove : function (event) {}, + | onend : function (event) {}, + | + | // limit multiple gestures. + | // See the explanation in @Interactable.draggable example + | max: Infinity, + | maxPerElement: 1, + | }); + \*/ + gesturable: function (options) { + if (isObject(options)) { + this.options.gesture.enabled = options.enabled === false? false: true; + this.setPerAction('gesture', options); + this.setOnEvents('gesture', options); + + return this; + } + + if (isBool(options)) { + this.options.gesture.enabled = options; + + return this; + } + + return this.options.gesture; + }, + + /*\ + * Interactable.autoScroll + [ method ] + ** + * Deprecated. Add an `autoscroll` property to the options object + * passed to @Interactable.draggable or @Interactable.resizable instead. + * + * Returns or sets whether dragging and resizing near the edges of the + * window/container trigger autoScroll for this Interactable + * + = (object) Object with autoScroll properties + * + * or + * + - options (object | boolean) #optional + * options can be: + * - an object with margin, distance and interval properties, + * - true or false to enable or disable autoScroll or + = (Interactable) this Interactable + \*/ + autoScroll: function (options) { + if (isObject(options)) { + options = extend({ actions: ['drag', 'resize']}, options); + } + else if (isBool(options)) { + options = { actions: ['drag', 'resize'], enabled: options }; + } + + return this.setOptions('autoScroll', options); + }, + + /*\ + * Interactable.snap + [ method ] + ** + * Deprecated. Add a `snap` property to the options object passed + * to @Interactable.draggable or @Interactable.resizable instead. + * + * Returns or sets if and how action coordinates are snapped. By + * default, snapping is relative to the pointer coordinates. You can + * change this by setting the + * [`elementOrigin`](https://github.com/taye/interact.js/pull/72). + ** + = (boolean | object) `false` if snap is disabled; object with snap properties if snap is enabled + ** + * or + ** + - options (object | boolean | null) #optional + = (Interactable) this Interactable + > Usage + | interact(document.querySelector('#thing')).snap({ + | targets: [ + | // snap to this specific point + | { + | x: 100, + | y: 100, + | range: 25 + | }, + | // give this function the x and y page coords and snap to the object returned + | function (x, y) { + | return { + | x: x, + | y: (75 + 50 * Math.sin(x * 0.04)), + | range: 40 + | }; + | }, + | // create a function that snaps to a grid + | interact.createSnapGrid({ + | x: 50, + | y: 50, + | range: 10, // optional + | offset: { x: 5, y: 10 } // optional + | }) + | ], + | // do not snap during normal movement. + | // Instead, trigger only one snapped move event + | // immediately before the end event. + | endOnly: true, + | + | relativePoints: [ + | { x: 0, y: 0 }, // snap relative to the top left of the element + | { x: 1, y: 1 }, // and also to the bottom right + | ], + | + | // offset the snap target coordinates + | // can be an object with x/y or 'startCoords' + | offset: { x: 50, y: 50 } + | } + | }); + \*/ + snap: function (options) { + var ret = this.setOptions('snap', options); + + if (ret === this) { return this; } + + return ret.drag; + }, + + setOptions: function (option, options) { + var actions = options && isArray(options.actions) + ? options.actions + : ['drag']; + + var i; + + if (isObject(options) || isBool(options)) { + for (i = 0; i < actions.length; i++) { + var action = /resize/.test(actions[i])? 'resize' : actions[i]; + + if (!isObject(this.options[action])) { continue; } + + var thisOption = this.options[action][option]; + + if (isObject(options)) { + extend(thisOption, options); + thisOption.enabled = options.enabled === false? false: true; + + if (option === 'snap') { + if (thisOption.mode === 'grid') { + thisOption.targets = [ + interact.createSnapGrid(extend({ + offset: thisOption.gridOffset || { x: 0, y: 0 } + }, thisOption.grid || {})) + ]; + } + else if (thisOption.mode === 'anchor') { + thisOption.targets = thisOption.anchors; + } + else if (thisOption.mode === 'path') { + thisOption.targets = thisOption.paths; + } + + if ('elementOrigin' in options) { + thisOption.relativePoints = [options.elementOrigin]; + } + } + } + else if (isBool(options)) { + thisOption.enabled = options; + } + } + + return this; + } + + var ret = {}, + allActions = ['drag', 'resize', 'gesture']; + + for (i = 0; i < allActions.length; i++) { + if (option in defaultOptions[allActions[i]]) { + ret[allActions[i]] = this.options[allActions[i]][option]; + } + } + + return ret; + }, + + + /*\ + * Interactable.inertia + [ method ] + ** + * Deprecated. Add an `inertia` property to the options object passed + * to @Interactable.draggable or @Interactable.resizable instead. + * + * Returns or sets if and how events continue to run after the pointer is released + ** + = (boolean | object) `false` if inertia is disabled; `object` with inertia properties if inertia is enabled + ** + * or + ** + - options (object | boolean | null) #optional + = (Interactable) this Interactable + > Usage + | // enable and use default settings + | interact(element).inertia(true); + | + | // enable and use custom settings + | interact(element).inertia({ + | // value greater than 0 + | // high values slow the object down more quickly + | resistance : 16, + | + | // the minimum launch speed (pixels per second) that results in inertia start + | minSpeed : 200, + | + | // inertia will stop when the object slows down to this speed + | endSpeed : 20, + | + | // boolean; should actions be resumed when the pointer goes down during inertia + | allowResume : true, + | + | // boolean; should the jump when resuming from inertia be ignored in event.dx/dy + | zeroResumeDelta: false, + | + | // if snap/restrict are set to be endOnly and inertia is enabled, releasing + | // the pointer without triggering inertia will animate from the release + | // point to the snaped/restricted point in the given amount of time (ms) + | smoothEndDuration: 300, + | + | // an array of action types that can have inertia (no gesture) + | actions : ['drag', 'resize'] + | }); + | + | // reset custom settings and use all defaults + | interact(element).inertia(null); + \*/ + inertia: function (options) { + var ret = this.setOptions('inertia', options); + + if (ret === this) { return this; } + + return ret.drag; + }, + + getAction: function (pointer, event, interaction, element) { + var action = this.defaultActionChecker(pointer, interaction, element); + + if (this.options.actionChecker) { + return this.options.actionChecker(pointer, event, action, this, element, interaction); + } + + return action; + }, + + defaultActionChecker: defaultActionChecker, + + /*\ + * Interactable.actionChecker + [ method ] + * + * Gets or sets the function used to check action to be performed on + * pointerDown + * + - checker (function | null) #optional A function which takes a pointer event, defaultAction string, interactable, element and interaction as parameters and returns an object with name property 'drag' 'resize' or 'gesture' and optionally an `edges` object with boolean 'top', 'left', 'bottom' and right props. + = (Function | Interactable) The checker function or this Interactable + * + | interact('.resize-drag') + | .resizable(true) + | .draggable(true) + | .actionChecker(function (pointer, event, action, interactable, element, interaction) { + | + | if (interact.matchesSelector(event.target, '.drag-handle') { + | // force drag with handle target + | action.name = drag; + | } + | else { + | // resize from the top and right edges + | action.name = 'resize'; + | action.edges = { top: true, right: true }; + | } + | + | return action; + | }); + \*/ + actionChecker: function (checker) { + if (isFunction(checker)) { + this.options.actionChecker = checker; + + return this; + } + + if (checker === null) { + delete this.options.actionChecker; + + return this; + } + + return this.options.actionChecker; + }, + + /*\ + * Interactable.getRect + [ method ] + * + * The default function to get an Interactables bounding rect. Can be + * overridden using @Interactable.rectChecker. + * + - element (Element) #optional The element to measure. + = (object) The object's bounding rectangle. + o { + o top : 0, + o left : 0, + o bottom: 0, + o right : 0, + o width : 0, + o height: 0 + o } + \*/ + getRect: function rectCheck (element) { + element = element || this._element; + + if (this.selector && !(isElement(element))) { + element = this._context.querySelector(this.selector); + } + + return getElementRect(element); + }, + + /*\ + * Interactable.rectChecker + [ method ] + * + * Returns or sets the function used to calculate the interactable's + * element's rectangle + * + - checker (function) #optional A function which returns this Interactable's bounding rectangle. See @Interactable.getRect + = (function | object) The checker function or this Interactable + \*/ + rectChecker: function (checker) { + if (isFunction(checker)) { + this.getRect = checker; + + return this; + } + + if (checker === null) { + delete this.options.getRect; + + return this; + } + + return this.getRect; + }, + + /*\ + * Interactable.styleCursor + [ method ] + * + * Returns or sets whether the action that would be performed when the + * mouse on the element are checked on `mousemove` so that the cursor + * may be styled appropriately + * + - newValue (boolean) #optional + = (boolean | Interactable) The current setting or this Interactable + \*/ + styleCursor: function (newValue) { + if (isBool(newValue)) { + this.options.styleCursor = newValue; + + return this; + } + + if (newValue === null) { + delete this.options.styleCursor; + + return this; + } + + return this.options.styleCursor; + }, + + /*\ + * Interactable.preventDefault + [ method ] + * + * Returns or sets whether to prevent the browser's default behaviour + * in response to pointer events. Can be set to: + * - `'always'` to always prevent + * - `'never'` to never prevent + * - `'auto'` to let interact.js try to determine what would be best + * + - newValue (string) #optional `true`, `false` or `'auto'` + = (string | Interactable) The current setting or this Interactable + \*/ + preventDefault: function (newValue) { + if (/^(always|never|auto)$/.test(newValue)) { + this.options.preventDefault = newValue; + return this; + } + + if (isBool(newValue)) { + this.options.preventDefault = newValue? 'always' : 'never'; + return this; + } + + return this.options.preventDefault; + }, + + /*\ + * Interactable.origin + [ method ] + * + * Gets or sets the origin of the Interactable's element. The x and y + * of the origin will be subtracted from action event coordinates. + * + - origin (object | string) #optional An object eg. { x: 0, y: 0 } or string 'parent', 'self' or any CSS selector + * OR + - origin (Element) #optional An HTML or SVG Element whose rect will be used + ** + = (object) The current origin or this Interactable + \*/ + origin: function (newValue) { + if (trySelector(newValue)) { + this.options.origin = newValue; + return this; + } + else if (isObject(newValue)) { + this.options.origin = newValue; + return this; + } + + return this.options.origin; + }, + + /*\ + * Interactable.deltaSource + [ method ] + * + * Returns or sets the mouse coordinate types used to calculate the + * movement of the pointer. + * + - newValue (string) #optional Use 'client' if you will be scrolling while interacting; Use 'page' if you want autoScroll to work + = (string | object) The current deltaSource or this Interactable + \*/ + deltaSource: function (newValue) { + if (newValue === 'page' || newValue === 'client') { + this.options.deltaSource = newValue; + + return this; + } + + return this.options.deltaSource; + }, + + /*\ + * Interactable.restrict + [ method ] + ** + * Deprecated. Add a `restrict` property to the options object passed to + * @Interactable.draggable, @Interactable.resizable or @Interactable.gesturable instead. + * + * Returns or sets the rectangles within which actions on this + * interactable (after snap calculations) are restricted. By default, + * restricting is relative to the pointer coordinates. You can change + * this by setting the + * [`elementRect`](https://github.com/taye/interact.js/pull/72). + ** + - options (object) #optional an object with keys drag, resize, and/or gesture whose values are rects, Elements, CSS selectors, or 'parent' or 'self' + = (object) The current restrictions object or this Interactable + ** + | interact(element).restrict({ + | // the rect will be `interact.getElementRect(element.parentNode)` + | drag: element.parentNode, + | + | // x and y are relative to the the interactable's origin + | resize: { x: 100, y: 100, width: 200, height: 200 } + | }) + | + | interact('.draggable').restrict({ + | // the rect will be the selected element's parent + | drag: 'parent', + | + | // do not restrict during normal movement. + | // Instead, trigger only one restricted move event + | // immediately before the end event. + | endOnly: true, + | + | // https://github.com/taye/interact.js/pull/72#issue-41813493 + | elementRect: { top: 0, left: 0, bottom: 1, right: 1 } + | }); + \*/ + restrict: function (options) { + if (!isObject(options)) { + return this.setOptions('restrict', options); + } + + var actions = ['drag', 'resize', 'gesture'], + ret; + + for (var i = 0; i < actions.length; i++) { + var action = actions[i]; + + if (action in options) { + var perAction = extend({ + actions: [action], + restriction: options[action] + }, options); + + ret = this.setOptions('restrict', perAction); + } + } + + return ret; + }, + + /*\ + * Interactable.context + [ method ] + * + * Gets the selector context Node of the Interactable. The default is `window.document`. + * + = (Node) The context Node of this Interactable + ** + \*/ + context: function () { + return this._context; + }, + + _context: document, + + /*\ + * Interactable.ignoreFrom + [ method ] + * + * If the target of the `mousedown`, `pointerdown` or `touchstart` + * event or any of it's parents match the given CSS selector or + * Element, no drag/resize/gesture is started. + * + - newValue (string | Element | null) #optional a CSS selector string, an Element or `null` to not ignore any elements + = (string | Element | object) The current ignoreFrom value or this Interactable + ** + | interact(element, { ignoreFrom: document.getElementById('no-action') }); + | // or + | interact(element).ignoreFrom('input, textarea, a'); + \*/ + ignoreFrom: function (newValue) { + if (trySelector(newValue)) { // CSS selector to match event.target + this.options.ignoreFrom = newValue; + return this; + } + + if (isElement(newValue)) { // specific element + this.options.ignoreFrom = newValue; + return this; + } + + return this.options.ignoreFrom; + }, + + /*\ + * Interactable.allowFrom + [ method ] + * + * A drag/resize/gesture is started only If the target of the + * `mousedown`, `pointerdown` or `touchstart` event or any of it's + * parents match the given CSS selector or Element. + * + - newValue (string | Element | null) #optional a CSS selector string, an Element or `null` to allow from any element + = (string | Element | object) The current allowFrom value or this Interactable + ** + | interact(element, { allowFrom: document.getElementById('drag-handle') }); + | // or + | interact(element).allowFrom('.handle'); + \*/ + allowFrom: function (newValue) { + if (trySelector(newValue)) { // CSS selector to match event.target + this.options.allowFrom = newValue; + return this; + } + + if (isElement(newValue)) { // specific element + this.options.allowFrom = newValue; + return this; + } + + return this.options.allowFrom; + }, + + /*\ + * Interactable.element + [ method ] + * + * If this is not a selector Interactable, it returns the element this + * interactable represents + * + = (Element) HTML / SVG Element + \*/ + element: function () { + return this._element; + }, + + /*\ + * Interactable.fire + [ method ] + * + * Calls listeners for the given InteractEvent type bound globally + * and directly to this Interactable + * + - iEvent (InteractEvent) The InteractEvent object to be fired on this Interactable + = (Interactable) this Interactable + \*/ + fire: function (iEvent) { + if (!(iEvent && iEvent.type) || !contains(eventTypes, iEvent.type)) { + return this; + } + + var listeners, + i, + len, + onEvent = 'on' + iEvent.type, + funcName = ''; + + // Interactable#on() listeners + if (iEvent.type in this._iEvents) { + listeners = this._iEvents[iEvent.type]; + + for (i = 0, len = listeners.length; i < len && !iEvent.immediatePropagationStopped; i++) { + funcName = listeners[i].name; + listeners[i](iEvent); + } + } + + // interactable.onevent listener + if (isFunction(this[onEvent])) { + funcName = this[onEvent].name; + this[onEvent](iEvent); + } + + // interact.on() listeners + if (iEvent.type in globalEvents && (listeners = globalEvents[iEvent.type])) { + + for (i = 0, len = listeners.length; i < len && !iEvent.immediatePropagationStopped; i++) { + funcName = listeners[i].name; + listeners[i](iEvent); + } + } + + return this; + }, + + /*\ + * Interactable.on + [ method ] + * + * Binds a listener for an InteractEvent or DOM event. + * + - eventType (string | array | object) The types of events to listen for + - listener (function) The function to be called on the given event(s) + - useCapture (boolean) #optional useCapture flag for addEventListener + = (object) This Interactable + \*/ + on: function (eventType, listener, useCapture) { + var i; + + if (isString(eventType) && eventType.search(' ') !== -1) { + eventType = eventType.trim().split(/ +/); + } + + if (isArray(eventType)) { + for (i = 0; i < eventType.length; i++) { + this.on(eventType[i], listener, useCapture); + } + + return this; + } + + if (isObject(eventType)) { + for (var prop in eventType) { + this.on(prop, eventType[prop], listener); + } + + return this; + } + + if (eventType === 'wheel') { + eventType = wheelEvent; + } + + // convert to boolean + useCapture = useCapture? true: false; + + if (contains(eventTypes, eventType)) { + // if this type of event was never bound to this Interactable + if (!(eventType in this._iEvents)) { + this._iEvents[eventType] = [listener]; + } + else { + this._iEvents[eventType].push(listener); + } + } + // delegated event for selector + else if (this.selector) { + if (!delegatedEvents[eventType]) { + delegatedEvents[eventType] = { + selectors: [], + contexts : [], + listeners: [] + }; + + // add delegate listener functions + for (i = 0; i < documents.length; i++) { + events.add(documents[i], eventType, delegateListener); + events.add(documents[i], eventType, delegateUseCapture, true); + } + } + + var delegated = delegatedEvents[eventType], + index; + + for (index = delegated.selectors.length - 1; index >= 0; index--) { + if (delegated.selectors[index] === this.selector + && delegated.contexts[index] === this._context) { + break; + } + } + + if (index === -1) { + index = delegated.selectors.length; + + delegated.selectors.push(this.selector); + delegated.contexts .push(this._context); + delegated.listeners.push([]); + } + + // keep listener and useCapture flag + delegated.listeners[index].push([listener, useCapture]); + } + else { + events.add(this._element, eventType, listener, useCapture); + } + + return this; + }, + + /*\ + * Interactable.off + [ method ] + * + * Removes an InteractEvent or DOM event listener + * + - eventType (string | array | object) The types of events that were listened for + - listener (function) The listener function to be removed + - useCapture (boolean) #optional useCapture flag for removeEventListener + = (object) This Interactable + \*/ + off: function (eventType, listener, useCapture) { + var i; + + if (isString(eventType) && eventType.search(' ') !== -1) { + eventType = eventType.trim().split(/ +/); + } + + if (isArray(eventType)) { + for (i = 0; i < eventType.length; i++) { + this.off(eventType[i], listener, useCapture); + } + + return this; + } + + if (isObject(eventType)) { + for (var prop in eventType) { + this.off(prop, eventType[prop], listener); + } + + return this; + } + + var eventList, + index = -1; + + // convert to boolean + useCapture = useCapture? true: false; + + if (eventType === 'wheel') { + eventType = wheelEvent; + } + + // if it is an action event type + if (contains(eventTypes, eventType)) { + eventList = this._iEvents[eventType]; + + if (eventList && (index = indexOf(eventList, listener)) !== -1) { + this._iEvents[eventType].splice(index, 1); + } + } + // delegated event + else if (this.selector) { + var delegated = delegatedEvents[eventType], + matchFound = false; + + if (!delegated) { return this; } + + // count from last index of delegated to 0 + for (index = delegated.selectors.length - 1; index >= 0; index--) { + // look for matching selector and context Node + if (delegated.selectors[index] === this.selector + && delegated.contexts[index] === this._context) { + + var listeners = delegated.listeners[index]; + + // each item of the listeners array is an array: [function, useCaptureFlag] + for (i = listeners.length - 1; i >= 0; i--) { + var fn = listeners[i][0], + useCap = listeners[i][1]; + + // check if the listener functions and useCapture flags match + if (fn === listener && useCap === useCapture) { + // remove the listener from the array of listeners + listeners.splice(i, 1); + + // if all listeners for this interactable have been removed + // remove the interactable from the delegated arrays + if (!listeners.length) { + delegated.selectors.splice(index, 1); + delegated.contexts .splice(index, 1); + delegated.listeners.splice(index, 1); + + // remove delegate function from context + events.remove(this._context, eventType, delegateListener); + events.remove(this._context, eventType, delegateUseCapture, true); + + // remove the arrays if they are empty + if (!delegated.selectors.length) { + delegatedEvents[eventType] = null; + } + } + + // only remove one listener + matchFound = true; + break; + } + } + + if (matchFound) { break; } + } + } + } + // remove listener from this Interatable's element + else { + events.remove(this._element, eventType, listener, useCapture); + } + + return this; + }, + + /*\ + * Interactable.set + [ method ] + * + * Reset the options of this Interactable + - options (object) The new settings to apply + = (object) This Interactable + \*/ + set: function (options) { + if (!isObject(options)) { + options = {}; + } + + this.options = extend({}, defaultOptions.base); + + var i, + actions = ['drag', 'drop', 'resize', 'gesture'], + methods = ['draggable', 'dropzone', 'resizable', 'gesturable'], + perActions = extend(extend({}, defaultOptions.perAction), options[action] || {}); + + for (i = 0; i < actions.length; i++) { + var action = actions[i]; + + this.options[action] = extend({}, defaultOptions[action]); + + this.setPerAction(action, perActions); + + this[methods[i]](options[action]); + } + + var settings = [ + 'accept', 'actionChecker', 'allowFrom', 'deltaSource', + 'dropChecker', 'ignoreFrom', 'origin', 'preventDefault', + 'rectChecker', 'styleCursor' + ]; + + for (i = 0, len = settings.length; i < len; i++) { + var setting = settings[i]; + + this.options[setting] = defaultOptions.base[setting]; + + if (setting in options) { + this[setting](options[setting]); + } + } + + return this; + }, + + /*\ + * Interactable.unset + [ method ] + * + * Remove this interactable from the list of interactables and remove + * it's drag, drop, resize and gesture capabilities + * + = (object) @interact + \*/ + unset: function () { + events.remove(this._element, 'all'); + + if (!isString(this.selector)) { + events.remove(this, 'all'); + if (this.options.styleCursor) { + this._element.style.cursor = ''; + } + } + else { + // remove delegated events + for (var type in delegatedEvents) { + var delegated = delegatedEvents[type]; + + for (var i = 0; i < delegated.selectors.length; i++) { + if (delegated.selectors[i] === this.selector + && delegated.contexts[i] === this._context) { + + delegated.selectors.splice(i, 1); + delegated.contexts .splice(i, 1); + delegated.listeners.splice(i, 1); + + // remove the arrays if they are empty + if (!delegated.selectors.length) { + delegatedEvents[type] = null; + } + } + + events.remove(this._context, type, delegateListener); + events.remove(this._context, type, delegateUseCapture, true); + + break; + } + } + } + + this.dropzone(false); + + interactables.splice(indexOf(interactables, this), 1); + + return interact; + } + }; + + function warnOnce (method, message) { + var warned = false; + + return function () { + if (!warned) { + window.console.warn(message); + warned = true; + } + + return method.apply(this, arguments); + }; + } + + Interactable.prototype.snap = warnOnce(Interactable.prototype.snap, + 'Interactable#snap is deprecated. See the new documentation for snapping at http://interactjs.io/docs/snapping'); + Interactable.prototype.restrict = warnOnce(Interactable.prototype.restrict, + 'Interactable#restrict is deprecated. See the new documentation for resticting at http://interactjs.io/docs/restriction'); + Interactable.prototype.inertia = warnOnce(Interactable.prototype.inertia, + 'Interactable#inertia is deprecated. See the new documentation for inertia at http://interactjs.io/docs/inertia'); + Interactable.prototype.autoScroll = warnOnce(Interactable.prototype.autoScroll, + 'Interactable#autoScroll is deprecated. See the new documentation for autoScroll at http://interactjs.io/docs/#autoscroll'); + Interactable.prototype.squareResize = warnOnce(Interactable.prototype.squareResize, + 'Interactable#squareResize is deprecated. See http://interactjs.io/docs/#resize-square'); + + Interactable.prototype.accept = warnOnce(Interactable.prototype.accept, + 'Interactable#accept is deprecated. use Interactable#dropzone({ accept: target }) instead'); + Interactable.prototype.dropChecker = warnOnce(Interactable.prototype.dropChecker, + 'Interactable#dropChecker is deprecated. use Interactable#dropzone({ dropChecker: checkerFunction }) instead'); + Interactable.prototype.context = warnOnce(Interactable.prototype.context, + 'Interactable#context as a method is deprecated. It will soon be a DOM Node instead'); + + /*\ + * interact.isSet + [ method ] + * + * Check if an element has been set + - element (Element) The Element being searched for + = (boolean) Indicates if the element or CSS selector was previously passed to interact + \*/ + interact.isSet = function(element, options) { + return interactables.indexOfElement(element, options && options.context) !== -1; + }; + + /*\ + * interact.on + [ method ] + * + * Adds a global listener for an InteractEvent or adds a DOM event to + * `document` + * + - type (string | array | object) The types of events to listen for + - listener (function) The function to be called on the given event(s) + - useCapture (boolean) #optional useCapture flag for addEventListener + = (object) interact + \*/ + interact.on = function (type, listener, useCapture) { + if (isString(type) && type.search(' ') !== -1) { + type = type.trim().split(/ +/); + } + + if (isArray(type)) { + for (var i = 0; i < type.length; i++) { + interact.on(type[i], listener, useCapture); + } + + return interact; + } + + if (isObject(type)) { + for (var prop in type) { + interact.on(prop, type[prop], listener); + } + + return interact; + } + + // if it is an InteractEvent type, add listener to globalEvents + if (contains(eventTypes, type)) { + // if this type of event was never bound + if (!globalEvents[type]) { + globalEvents[type] = [listener]; + } + else { + globalEvents[type].push(listener); + } + } + // If non InteractEvent type, addEventListener to document + else { + events.add(document, type, listener, useCapture); + } + + return interact; + }; + + /*\ + * interact.off + [ method ] + * + * Removes a global InteractEvent listener or DOM event from `document` + * + - type (string | array | object) The types of events that were listened for + - listener (function) The listener function to be removed + - useCapture (boolean) #optional useCapture flag for removeEventListener + = (object) interact + \*/ + interact.off = function (type, listener, useCapture) { + if (isString(type) && type.search(' ') !== -1) { + type = type.trim().split(/ +/); + } + + if (isArray(type)) { + for (var i = 0; i < type.length; i++) { + interact.off(type[i], listener, useCapture); + } + + return interact; + } + + if (isObject(type)) { + for (var prop in type) { + interact.off(prop, type[prop], listener); + } + + return interact; + } + + if (!contains(eventTypes, type)) { + events.remove(document, type, listener, useCapture); + } + else { + var index; + + if (type in globalEvents + && (index = indexOf(globalEvents[type], listener)) !== -1) { + globalEvents[type].splice(index, 1); + } + } + + return interact; + }; + + /*\ + * interact.enableDragging + [ method ] + * + * Deprecated. + * + * Returns or sets whether dragging is enabled for any Interactables + * + - newValue (boolean) #optional `true` to allow the action; `false` to disable action for all Interactables + = (boolean | object) The current setting or interact + \*/ + interact.enableDragging = warnOnce(function (newValue) { + if (newValue !== null && newValue !== undefined) { + actionIsEnabled.drag = newValue; + + return interact; + } + return actionIsEnabled.drag; + }, 'interact.enableDragging is deprecated and will soon be removed.'); + + /*\ + * interact.enableResizing + [ method ] + * + * Deprecated. + * + * Returns or sets whether resizing is enabled for any Interactables + * + - newValue (boolean) #optional `true` to allow the action; `false` to disable action for all Interactables + = (boolean | object) The current setting or interact + \*/ + interact.enableResizing = warnOnce(function (newValue) { + if (newValue !== null && newValue !== undefined) { + actionIsEnabled.resize = newValue; + + return interact; + } + return actionIsEnabled.resize; + }, 'interact.enableResizing is deprecated and will soon be removed.'); + + /*\ + * interact.enableGesturing + [ method ] + * + * Deprecated. + * + * Returns or sets whether gesturing is enabled for any Interactables + * + - newValue (boolean) #optional `true` to allow the action; `false` to disable action for all Interactables + = (boolean | object) The current setting or interact + \*/ + interact.enableGesturing = warnOnce(function (newValue) { + if (newValue !== null && newValue !== undefined) { + actionIsEnabled.gesture = newValue; + + return interact; + } + return actionIsEnabled.gesture; + }, 'interact.enableGesturing is deprecated and will soon be removed.'); + + interact.eventTypes = eventTypes; + + /*\ + * interact.debug + [ method ] + * + * Returns debugging data + = (object) An object with properties that outline the current state and expose internal functions and variables + \*/ + interact.debug = function () { + var interaction = interactions[0] || new Interaction(); + + return { + interactions : interactions, + target : interaction.target, + dragging : interaction.dragging, + resizing : interaction.resizing, + gesturing : interaction.gesturing, + prepared : interaction.prepared, + matches : interaction.matches, + matchElements : interaction.matchElements, + + prevCoords : interaction.prevCoords, + startCoords : interaction.startCoords, + + pointerIds : interaction.pointerIds, + pointers : interaction.pointers, + addPointer : listeners.addPointer, + removePointer : listeners.removePointer, + recordPointer : listeners.recordPointer, + + snap : interaction.snapStatus, + restrict : interaction.restrictStatus, + inertia : interaction.inertiaStatus, + + downTime : interaction.downTimes[0], + downEvent : interaction.downEvent, + downPointer : interaction.downPointer, + prevEvent : interaction.prevEvent, + + Interactable : Interactable, + interactables : interactables, + pointerIsDown : interaction.pointerIsDown, + defaultOptions : defaultOptions, + defaultActionChecker : defaultActionChecker, + + actionCursors : actionCursors, + dragMove : listeners.dragMove, + resizeMove : listeners.resizeMove, + gestureMove : listeners.gestureMove, + pointerUp : listeners.pointerUp, + pointerDown : listeners.pointerDown, + pointerMove : listeners.pointerMove, + pointerHover : listeners.pointerHover, + + eventTypes : eventTypes, + + events : events, + globalEvents : globalEvents, + delegatedEvents : delegatedEvents, + + prefixedPropREs : prefixedPropREs + }; + }; + + // expose the functions used to calculate multi-touch properties + interact.getPointerAverage = pointerAverage; + interact.getTouchBBox = touchBBox; + interact.getTouchDistance = touchDistance; + interact.getTouchAngle = touchAngle; + + interact.getElementRect = getElementRect; + interact.getElementClientRect = getElementClientRect; + interact.matchesSelector = matchesSelector; + interact.closest = closest; + + /*\ + * interact.margin + [ method ] + * + * Deprecated. Use `interact(target).resizable({ margin: number });` instead. + * Returns or sets the margin for autocheck resizing used in + * @Interactable.getAction. That is the distance from the bottom and right + * edges of an element clicking in which will start resizing + * + - newValue (number) #optional + = (number | interact) The current margin value or interact + \*/ + interact.margin = warnOnce(function (newvalue) { + if (isNumber(newvalue)) { + margin = newvalue; + + return interact; + } + return margin; + }, + 'interact.margin is deprecated. Use interact(target).resizable({ margin: number }); instead.') ; + + /*\ + * interact.supportsTouch + [ method ] + * + = (boolean) Whether or not the browser supports touch input + \*/ + interact.supportsTouch = function () { + return supportsTouch; + }; + + /*\ + * interact.supportsPointerEvent + [ method ] + * + = (boolean) Whether or not the browser supports PointerEvents + \*/ + interact.supportsPointerEvent = function () { + return supportsPointerEvent; + }; + + /*\ + * interact.stop + [ method ] + * + * Cancels all interactions (end events are not fired) + * + - event (Event) An event on which to call preventDefault() + = (object) interact + \*/ + interact.stop = function (event) { + for (var i = interactions.length - 1; i >= 0; i--) { + interactions[i].stop(event); + } + + return interact; + }; + + /*\ + * interact.dynamicDrop + [ method ] + * + * Returns or sets whether the dimensions of dropzone elements are + * calculated on every dragmove or only on dragstart for the default + * dropChecker + * + - newValue (boolean) #optional True to check on each move. False to check only before start + = (boolean | interact) The current setting or interact + \*/ + interact.dynamicDrop = function (newValue) { + if (isBool(newValue)) { + //if (dragging && dynamicDrop !== newValue && !newValue) { + //calcRects(dropzones); + //} + + dynamicDrop = newValue; + + return interact; + } + return dynamicDrop; + }; + + /*\ + * interact.pointerMoveTolerance + [ method ] + * Returns or sets the distance the pointer must be moved before an action + * sequence occurs. This also affects tolerance for tap events. + * + - newValue (number) #optional The movement from the start position must be greater than this value + = (number | Interactable) The current setting or interact + \*/ + interact.pointerMoveTolerance = function (newValue) { + if (isNumber(newValue)) { + pointerMoveTolerance = newValue; + + return this; + } + + return pointerMoveTolerance; + }; + + /*\ + * interact.maxInteractions + [ method ] + ** + * Returns or sets the maximum number of concurrent interactions allowed. + * By default only 1 interaction is allowed at a time (for backwards + * compatibility). To allow multiple interactions on the same Interactables + * and elements, you need to enable it in the draggable, resizable and + * gesturable `'max'` and `'maxPerElement'` options. + ** + - newValue (number) #optional Any number. newValue <= 0 means no interactions. + \*/ + interact.maxInteractions = function (newValue) { + if (isNumber(newValue)) { + maxInteractions = newValue; + + return this; + } + + return maxInteractions; + }; + + interact.createSnapGrid = function (grid) { + return function (x, y) { + var offsetX = 0, + offsetY = 0; + + if (isObject(grid.offset)) { + offsetX = grid.offset.x; + offsetY = grid.offset.y; + } + + var gridx = Math.round((x - offsetX) / grid.x), + gridy = Math.round((y - offsetY) / grid.y), + + newX = gridx * grid.x + offsetX, + newY = gridy * grid.y + offsetY; + + return { + x: newX, + y: newY, + range: grid.range + }; + }; + }; + + function endAllInteractions (event) { + for (var i = 0; i < interactions.length; i++) { + interactions[i].pointerEnd(event, event); + } + } + + function listenToDocument (doc) { + if (contains(documents, doc)) { return; } + + var win = doc.defaultView || doc.parentWindow; + + // add delegate event listener + for (var eventType in delegatedEvents) { + events.add(doc, eventType, delegateListener); + events.add(doc, eventType, delegateUseCapture, true); + } + + if (supportsPointerEvent) { + if (PointerEvent === win.MSPointerEvent) { + pEventTypes = { + up: 'MSPointerUp', down: 'MSPointerDown', over: 'mouseover', + out: 'mouseout', move: 'MSPointerMove', cancel: 'MSPointerCancel' }; + } + else { + pEventTypes = { + up: 'pointerup', down: 'pointerdown', over: 'pointerover', + out: 'pointerout', move: 'pointermove', cancel: 'pointercancel' }; + } + + events.add(doc, pEventTypes.down , listeners.selectorDown ); + events.add(doc, pEventTypes.move , listeners.pointerMove ); + events.add(doc, pEventTypes.over , listeners.pointerOver ); + events.add(doc, pEventTypes.out , listeners.pointerOut ); + events.add(doc, pEventTypes.up , listeners.pointerUp ); + events.add(doc, pEventTypes.cancel, listeners.pointerCancel); + + // autoscroll + events.add(doc, pEventTypes.move, listeners.autoScrollMove); + } + else { + events.add(doc, 'mousedown', listeners.selectorDown); + events.add(doc, 'mousemove', listeners.pointerMove ); + events.add(doc, 'mouseup' , listeners.pointerUp ); + events.add(doc, 'mouseover', listeners.pointerOver ); + events.add(doc, 'mouseout' , listeners.pointerOut ); + + events.add(doc, 'touchstart' , listeners.selectorDown ); + events.add(doc, 'touchmove' , listeners.pointerMove ); + events.add(doc, 'touchend' , listeners.pointerUp ); + events.add(doc, 'touchcancel', listeners.pointerCancel); + + // autoscroll + events.add(doc, 'mousemove', listeners.autoScrollMove); + events.add(doc, 'touchmove', listeners.autoScrollMove); + } + + events.add(win, 'blur', endAllInteractions); + + try { + if (win.frameElement) { + var parentDoc = win.frameElement.ownerDocument, + parentWindow = parentDoc.defaultView; + + events.add(parentDoc , 'mouseup' , listeners.pointerEnd); + events.add(parentDoc , 'touchend' , listeners.pointerEnd); + events.add(parentDoc , 'touchcancel' , listeners.pointerEnd); + events.add(parentDoc , 'pointerup' , listeners.pointerEnd); + events.add(parentDoc , 'MSPointerUp' , listeners.pointerEnd); + events.add(parentWindow, 'blur' , endAllInteractions ); + } + } + catch (error) { + interact.windowParentError = error; + } + + // prevent native HTML5 drag on interact.js target elements + events.add(doc, 'dragstart', function (event) { + for (var i = 0; i < interactions.length; i++) { + var interaction = interactions[i]; + + if (interaction.element + && (interaction.element === event.target + || nodeContains(interaction.element, event.target))) { + + interaction.checkAndPreventDefault(event, interaction.target, interaction.element); + return; + } + } + }); + + if (events.useAttachEvent) { + // For IE's lack of Event#preventDefault + events.add(doc, 'selectstart', function (event) { + var interaction = interactions[0]; + + if (interaction.currentAction()) { + interaction.checkAndPreventDefault(event); + } + }); + + // For IE's bad dblclick event sequence + events.add(doc, 'dblclick', doOnInteractions('ie8Dblclick')); + } + + documents.push(doc); + } + + listenToDocument(document); + + function indexOf (array, target) { + for (var i = 0, len = array.length; i < len; i++) { + if (array[i] === target) { + return i; + } + } + + return -1; + } + + function contains (array, target) { + return indexOf(array, target) !== -1; + } + + function matchesSelector (element, selector, nodeList) { + if (ie8MatchesSelector) { + return ie8MatchesSelector(element, selector, nodeList); + } + + // remove /deep/ from selectors if shadowDOM polyfill is used + if (window !== realWindow) { + selector = selector.replace(/\/deep\//g, ' '); + } + + return element[prefixedMatchesSelector](selector); + } + + function matchesUpTo (element, selector, limit) { + while (isElement(element)) { + if (matchesSelector(element, selector)) { + return true; + } + + element = parentElement(element); + + if (element === limit) { + return matchesSelector(element, selector); + } + } + + return false; + } + + // For IE8's lack of an Element#matchesSelector + // taken from http://tanalin.com/en/blog/2012/12/matches-selector-ie8/ and modified + if (!(prefixedMatchesSelector in Element.prototype) || !isFunction(Element.prototype[prefixedMatchesSelector])) { + ie8MatchesSelector = function (element, selector, elems) { + elems = elems || element.parentNode.querySelectorAll(selector); + + for (var i = 0, len = elems.length; i < len; i++) { + if (elems[i] === element) { + return true; + } + } + + return false; + }; + } + + // requestAnimationFrame polyfill + (function() { + var lastTime = 0, + vendors = ['ms', 'moz', 'webkit', 'o']; + + for(var x = 0; x < vendors.length && !realWindow.requestAnimationFrame; ++x) { + reqFrame = realWindow[vendors[x]+'RequestAnimationFrame']; + cancelFrame = realWindow[vendors[x]+'CancelAnimationFrame'] || realWindow[vendors[x]+'CancelRequestAnimationFrame']; + } + + if (!reqFrame) { + reqFrame = function(callback) { + var currTime = new Date().getTime(), + timeToCall = Math.max(0, 16 - (currTime - lastTime)), + id = setTimeout(function() { callback(currTime + timeToCall); }, + timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + } + + if (!cancelFrame) { + cancelFrame = function(id) { + clearTimeout(id); + }; + } + }()); + + /* global exports: true, module, define */ + + // http://documentcloud.github.io/underscore/docs/underscore.html#section-11 + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = interact; + } + exports.interact = interact; + } + // AMD + else if (typeof define === 'function' && define.amd) { + define('interact', function() { + return interact; + }); + } + else { + realWindow.interact = interact; + } + +} (typeof window === 'undefined'? undefined : window)); diff --git a/package.json b/package.json new file mode 100644 index 0000000..ce8d14d --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "final", + "version": "1.0.0", + "description": "", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" + }, + "author": "", + "license": "ISC" +} diff --git a/script.js b/script.js new file mode 100644 index 0000000..7fc0804 --- /dev/null +++ b/script.js @@ -0,0 +1,141 @@ +let deckName, deckJSON, cardCount, deckWidth, deckHeight, + piles = {'deck': [], discard: []}; + +window.addEventListener('load', () => { + deckName = document.querySelector('#card-container').getAttribute("data-deckName"); + let xhr = new XMLHttpRequest(); + xhr.addEventListener("load", () => { + deckJSON = JSON.parse(xhr.responseText); + piles['deck'] = deckJSON.ObjectStates[0].DeckIDs.map(c => c - 100); + cardCount = piles['deck'].length; + shuffle(piles['deck']); + deckWidth = deckJSON.ObjectStates[0].CustomDeck["1"].NumWidth; + deckHeight = deckJSON.ObjectStates[0].CustomDeck["1"].NumHeight; + console.log(deckName); + }); + xhr.open("GET", "/" + deckName + ".json"); + xhr.send(); +}); + +let cardInteract = interact('.card') + .draggable({ + restrict: { + restriction: "parent", + endOnly: true, + elementRect: { top: 0, left: 0, bottom: 1, right: 1 } + }, + + snap: { + targets: [() => { + // TODO: maybe change to dropzone? + let pos = document.body.getBoundingClientRect(), + style = window.getComputedStyle(hand), + handHeight = parseInt(style.getPropertyValue("height")); + return {y: pos.bottom, + range: handHeight/2}; + }], + relativePoints: [{x: 0.5 , y: 1}] + }, + + onmove: dragMoveListener + }) + .on('hold', event => { + let target = event.target, + transform = target.style.transform; + // Scale up for visibility + if (transform.includes("scale(2)")) { + // translate the element + target.style.webkitTransform = + target.style.transform = transform.replace(" scale(2)", ""); + } + else { + target.style.webkitTransform = + target.style.transform = transform + ' scale(2)'; + } + }); + +interact('.card-pile') + .dropzone({ + accept: '.card', + ondrop: event => { + let pile = piles[event.target.getAttribute('data-pile')]; + pile.push(event.relatedTarget.getAttribute('data-num')); + event.relatedTarget.parentElement.removeChild(event.relatedTarget); + + // update deck text + event.target.innerHTML = `DECK
${pile.length}/${cardCount}`; + } + }) + .draggable({manualStart: true}) + .on('move', event => { + let interaction = event.interaction; + let pile = piles[event.target.getAttribute('data-pile')]; + + // if the pointer was moved while being held down + // and an interaction hasn't started yet + if (interaction.pointerIsDown && + !interaction.interacting() && + pile.length > 0) { + // draw a new card + let newCard = document.createElement('div'); + newCard.className = "card"; + let style = window.getComputedStyle(newCard); + let cardNum = pile.pop(); + newCard.setAttribute('data-num', cardNum); + newCard.style.backgroundPosition = ` + ${(cardNum % deckWidth) * parseInt(style.getPropertyValue("width"))}px + ${Math.floor(cardNum/deckHeight) * parseInt(style.getPropertyValue("height"))}px`; + newCard.style.backgroundImage = `url('${deckName}.png')`; + newCard.style.backgroundSize = `${deckWidth * 100}% ${deckHeight * 100}%`; + + // insert the card to the page + document.querySelector("#card-container").appendChild(newCard); + + // update deck text + event.target.innerHTML = `DECK
${pile.length}/${cardCount}`; + + // start a drag interaction targeting the clone + interaction.start({name: 'drag'}, cardInteract, newCard); + } + }); + +// Fisher-Yates shuffle from https://bost.ocks.org/mike/shuffle/ +function shuffle(array) { + let m = array.length, t, i; + + // While there remain elements to shuffle… + while (m) { + // Pick a remaining element… + i = Math.floor(Math.random() * m--); + + // And swap it with the current element. + t = array[m]; + array[m] = array[i]; + array[i] = t; + } + + return array; +} + +function dragMoveListener (event) { + let interaction = event.interaction; + + let target = event.target, + // keep the dragged position in the data-x/data-y attributes + x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx, + y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy; + + // translate the element + target.style.webkitTransform = + target.style.transform = + 'translate(' + x + 'px, ' + y + 'px)'; + + // raise to top + target.parentElement.removeChild(target); + document.querySelector("#card-container").appendChild(target); + + // update the posiion attributes + target.setAttribute('data-x', x); + target.setAttribute('data-y', y); +} + diff --git a/server.js b/server.js new file mode 100644 index 0000000..b9fcf48 --- /dev/null +++ b/server.js @@ -0,0 +1,91 @@ +// jshint node:true +// jshint esversion:6 +"use strict"; + +const http = require('http'), + qs = require('querystring'), + fs = require('fs'), + url = require('url'), + https = require('https'), + port = 8080; + +const deckName = "the_Unholy_Priest_update_2"; + +const server = http.createServer((req, res) => { + const uri = url.parse(req.url); + + if (req.method === 'POST') { + handlePost(res, req, uri); + } + else { + switch( uri.pathname ) { + case '/': + case '/index.html': + sendIndex(res, ""); + break; + case '/style.css': + sendFile(res, 'style.css', 'text/css'); + break; + case '/script.js': + sendFile(res, 'script.js', 'application/javascript'); + break; + case '/interact.js': + sendFile(res, 'interact.js', 'application/javascript'); + break; + case '/' + deckName + '.png': + sendFile(res, deckName + '.png', 'image/png'); + break; + case '/' + deckName + '.json': + sendFile(res, deckName + '.json', 'application/json'); + break; + default: + res.writeHead(404, {'Content-type': "text/html; charset=utf-8"}); + const html = ` + + 404 Not Found + + + +

Error 404: Path ${uri.pathname} not found

+ You seem to have gone to the wrong place, would you like to go + back to the main page? + `; + res.end(html, 'utf-8'); + } + } + +}); + +server.listen(process.env.PORT || port); +console.log('listening on 8080'); + +function sendIndex(res) { + let cards = []; + const html = ` + + + + + + + +
+
DECK
+
+
+
+ + `; + res.writeHead(200, {'contentType': 'text/html; charset=utf-8'}); + res.end(html, 'utf-8'); +} + +function sendFile(res, filename, contentType='text/html; charset=utf-8') { + fs.readFile(filename, (error, content) => { + res.writeHead(200, {'Content-type': contentType}); + res.end(content, 'utf-8'); + if (error) { + console.error(error); + } + }); +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..a5bc7d0 --- /dev/null +++ b/style.css @@ -0,0 +1,42 @@ +body { + margin: 0 0 0 0; +} +.card { + position: absolute; + border-radius: 5px; + width: 142px; + height: 200px; +} + +#card-container { + width: 100%; + height: 100%; +} + +#hand { + position: absolute; + bottom: 0px; + width: 100%; + height: 20%; + background-color: #ccc; +} + +.card-pile { + width: 100px; + height: 100px; + color: #eee; + display: flex; + justify-content: center; + align-items: center; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.deck { + background-color: blue; +} + +.discard { + background-color: red; +}