Spaces:
Runtime error
Runtime error
// DOM.event.move | |
// | |
// 2.0.0 | |
// | |
// Stephen Band | |
// | |
// Triggers 'movestart', 'move' and 'moveend' events after | |
// mousemoves following a mousedown cross a distance threshold, | |
// similar to the native 'dragstart', 'drag' and 'dragend' events. | |
// Move events are throttled to animation frames. Move event objects | |
// have the properties: | |
// | |
// pageX: | |
// pageY: Page coordinates of pointer. | |
// startX: | |
// startY: Page coordinates of pointer at movestart. | |
// distX: | |
// distY: Distance the pointer has moved since movestart. | |
// deltaX: | |
// deltaY: Distance the finger has moved since last event. | |
// velocityX: | |
// velocityY: Average velocity over last few events. | |
(function(fn) { | |
if (typeof define === 'function' && define.amd) { | |
define([], fn); | |
} else if ((typeof module !== "undefined" && module !== null) && module.exports) { | |
module.exports = fn; | |
} else { | |
fn(); | |
} | |
})(function(){ | |
var assign = Object.assign || window.jQuery && jQuery.extend; | |
// Number of pixels a pressed pointer travels before movestart | |
// event is fired. | |
var threshold = 8; | |
// Shim for requestAnimationFrame, falling back to timer. See: | |
// see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ | |
var requestFrame = (function(){ | |
return ( | |
window.requestAnimationFrame || | |
window.webkitRequestAnimationFrame || | |
window.mozRequestAnimationFrame || | |
window.oRequestAnimationFrame || | |
window.msRequestAnimationFrame || | |
function(fn, element){ | |
return window.setTimeout(function(){ | |
fn(); | |
}, 25); | |
} | |
); | |
})(); | |
// Shim for customEvent | |
// see https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill | |
(function () { | |
if ( typeof window.CustomEvent === "function" ) return false; | |
function CustomEvent ( event, params ) { | |
params = params || { bubbles: false, cancelable: false, detail: undefined }; | |
var evt = document.createEvent( 'CustomEvent' ); | |
evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); | |
return evt; | |
} | |
CustomEvent.prototype = window.Event.prototype; | |
window.CustomEvent = CustomEvent; | |
})(); | |
var ignoreTags = { | |
textarea: true, | |
input: true, | |
select: true, | |
button: true | |
}; | |
var mouseevents = { | |
move: 'mousemove', | |
cancel: 'mouseup dragstart', | |
end: 'mouseup' | |
}; | |
var touchevents = { | |
move: 'touchmove', | |
cancel: 'touchend', | |
end: 'touchend' | |
}; | |
var rspaces = /\s+/; | |
// DOM Events | |
var eventOptions = { bubbles: true, cancelable: true }; | |
var eventsSymbol = typeof Symbol === "function" ? Symbol('events') : {}; | |
function createEvent(type) { | |
return new CustomEvent(type, eventOptions); | |
} | |
function getEvents(node) { | |
return node[eventsSymbol] || (node[eventsSymbol] = {}); | |
} | |
function on(node, types, fn, data, selector) { | |
types = types.split(rspaces); | |
var events = getEvents(node); | |
var i = types.length; | |
var handlers, type; | |
function handler(e) { fn(e, data); } | |
while (i--) { | |
type = types[i]; | |
handlers = events[type] || (events[type] = []); | |
handlers.push([fn, handler]); | |
node.addEventListener(type, handler); | |
} | |
} | |
function off(node, types, fn, selector) { | |
types = types.split(rspaces); | |
var events = getEvents(node); | |
var i = types.length; | |
var type, handlers, k; | |
if (!events) { return; } | |
while (i--) { | |
type = types[i]; | |
handlers = events[type]; | |
if (!handlers) { continue; } | |
k = handlers.length; | |
while (k--) { | |
if (handlers[k][0] === fn) { | |
node.removeEventListener(type, handlers[k][1]); | |
handlers.splice(k, 1); | |
} | |
} | |
} | |
} | |
function trigger(node, type, properties) { | |
// Don't cache events. It prevents you from triggering an event of a | |
// given type from inside the handler of another event of that type. | |
var event = createEvent(type); | |
if (properties) { assign(event, properties); } | |
node.dispatchEvent(event); | |
} | |
// Constructors | |
function Timer(fn){ | |
var callback = fn, | |
active = false, | |
running = false; | |
function trigger(time) { | |
if (active){ | |
callback(); | |
requestFrame(trigger); | |
running = true; | |
active = false; | |
} | |
else { | |
running = false; | |
} | |
} | |
this.kick = function(fn) { | |
active = true; | |
if (!running) { trigger(); } | |
}; | |
this.end = function(fn) { | |
var cb = callback; | |
if (!fn) { return; } | |
// If the timer is not running, simply call the end callback. | |
if (!running) { | |
fn(); | |
} | |
// If the timer is running, and has been kicked lately, then | |
// queue up the current callback and the end callback, otherwise | |
// just the end callback. | |
else { | |
callback = active ? | |
function(){ cb(); fn(); } : | |
fn ; | |
active = true; | |
} | |
}; | |
} | |
// Functions | |
function noop() {} | |
function preventDefault(e) { | |
e.preventDefault(); | |
} | |
function isIgnoreTag(e) { | |
return !!ignoreTags[e.target.tagName.toLowerCase()]; | |
} | |
function isPrimaryButton(e) { | |
// Ignore mousedowns on any button other than the left (or primary) | |
// mouse button, or when a modifier key is pressed. | |
return (e.which === 1 && !e.ctrlKey && !e.altKey); | |
} | |
function identifiedTouch(touchList, id) { | |
var i, l; | |
if (touchList.identifiedTouch) { | |
return touchList.identifiedTouch(id); | |
} | |
// touchList.identifiedTouch() does not exist in | |
// webkit yet… we must do the search ourselves... | |
i = -1; | |
l = touchList.length; | |
while (++i < l) { | |
if (touchList[i].identifier === id) { | |
return touchList[i]; | |
} | |
} | |
} | |
function changedTouch(e, data) { | |
var touch = identifiedTouch(e.changedTouches, data.identifier); | |
// This isn't the touch you're looking for. | |
if (!touch) { return; } | |
// Chrome Android (at least) includes touches that have not | |
// changed in e.changedTouches. That's a bit annoying. Check | |
// that this touch has changed. | |
if (touch.pageX === data.pageX && touch.pageY === data.pageY) { return; } | |
return touch; | |
} | |
// Handlers that decide when the first movestart is triggered | |
function mousedown(e){ | |
// Ignore non-primary buttons | |
if (!isPrimaryButton(e)) { return; } | |
// Ignore form and interactive elements | |
if (isIgnoreTag(e)) { return; } | |
on(document, mouseevents.move, mousemove, e); | |
on(document, mouseevents.cancel, mouseend, e); | |
} | |
function mousemove(e, data){ | |
checkThreshold(e, data, e, removeMouse); | |
} | |
function mouseend(e, data) { | |
removeMouse(); | |
} | |
function removeMouse() { | |
off(document, mouseevents.move, mousemove); | |
off(document, mouseevents.cancel, mouseend); | |
} | |
function touchstart(e) { | |
// Don't get in the way of interaction with form elements | |
if (ignoreTags[e.target.tagName.toLowerCase()]) { return; } | |
var touch = e.changedTouches[0]; | |
// iOS live updates the touch objects whereas Android gives us copies. | |
// That means we can't trust the touchstart object to stay the same, | |
// so we must copy the data. This object acts as a template for | |
// movestart, move and moveend event objects. | |
var data = { | |
target: touch.target, | |
pageX: touch.pageX, | |
pageY: touch.pageY, | |
identifier: touch.identifier, | |
// The only way to make handlers individually unbindable is by | |
// making them unique. | |
touchmove: function(e, data) { touchmove(e, data); }, | |
touchend: function(e, data) { touchend(e, data); } | |
}; | |
on(document, touchevents.move, data.touchmove, data); | |
on(document, touchevents.cancel, data.touchend, data); | |
} | |
function touchmove(e, data) { | |
var touch = changedTouch(e, data); | |
if (!touch) { return; } | |
checkThreshold(e, data, touch, removeTouch); | |
} | |
function touchend(e, data) { | |
var touch = identifiedTouch(e.changedTouches, data.identifier); | |
if (!touch) { return; } | |
removeTouch(data); | |
} | |
function removeTouch(data) { | |
off(document, touchevents.move, data.touchmove); | |
off(document, touchevents.cancel, data.touchend); | |
} | |
function checkThreshold(e, data, touch, fn) { | |
var distX = touch.pageX - data.pageX; | |
var distY = touch.pageY - data.pageY; | |
// Do nothing if the threshold has not been crossed. | |
if ((distX * distX) + (distY * distY) < (threshold * threshold)) { return; } | |
triggerStart(e, data, touch, distX, distY, fn); | |
} | |
function triggerStart(e, data, touch, distX, distY, fn) { | |
var touches = e.targetTouches; | |
var time = e.timeStamp - data.timeStamp; | |
// Create a movestart object with some special properties that | |
// are passed only to the movestart handlers. | |
var template = { | |
altKey: e.altKey, | |
ctrlKey: e.ctrlKey, | |
shiftKey: e.shiftKey, | |
startX: data.pageX, | |
startY: data.pageY, | |
distX: distX, | |
distY: distY, | |
deltaX: distX, | |
deltaY: distY, | |
pageX: touch.pageX, | |
pageY: touch.pageY, | |
velocityX: distX / time, | |
velocityY: distY / time, | |
identifier: data.identifier, | |
targetTouches: touches, | |
finger: touches ? touches.length : 1, | |
enableMove: function() { | |
this.moveEnabled = true; | |
this.enableMove = noop; | |
e.preventDefault(); | |
} | |
}; | |
// Trigger the movestart event. | |
trigger(data.target, 'movestart', template); | |
// Unbind handlers that tracked the touch or mouse up till now. | |
fn(data); | |
} | |
// Handlers that control what happens following a movestart | |
function activeMousemove(e, data) { | |
var timer = data.timer; | |
data.touch = e; | |
data.timeStamp = e.timeStamp; | |
timer.kick(); | |
} | |
function activeMouseend(e, data) { | |
var target = data.target; | |
var event = data.event; | |
var timer = data.timer; | |
removeActiveMouse(); | |
endEvent(target, event, timer, function() { | |
// Unbind the click suppressor, waiting until after mouseup | |
// has been handled. | |
setTimeout(function(){ | |
off(target, 'click', preventDefault); | |
}, 0); | |
}); | |
} | |
function removeActiveMouse() { | |
off(document, mouseevents.move, activeMousemove); | |
off(document, mouseevents.end, activeMouseend); | |
} | |
function activeTouchmove(e, data) { | |
var event = data.event; | |
var timer = data.timer; | |
var touch = changedTouch(e, event); | |
if (!touch) { return; } | |
// Stop the interface from gesturing | |
e.preventDefault(); | |
event.targetTouches = e.targetTouches; | |
data.touch = touch; | |
data.timeStamp = e.timeStamp; | |
timer.kick(); | |
} | |
function activeTouchend(e, data) { | |
var target = data.target; | |
var event = data.event; | |
var timer = data.timer; | |
var touch = identifiedTouch(e.changedTouches, event.identifier); | |
// This isn't the touch you're looking for. | |
if (!touch) { return; } | |
removeActiveTouch(data); | |
endEvent(target, event, timer); | |
} | |
function removeActiveTouch(data) { | |
off(document, touchevents.move, data.activeTouchmove); | |
off(document, touchevents.end, data.activeTouchend); | |
} | |
// Logic for triggering move and moveend events | |
function updateEvent(event, touch, timeStamp) { | |
var time = timeStamp - event.timeStamp; | |
event.distX = touch.pageX - event.startX; | |
event.distY = touch.pageY - event.startY; | |
event.deltaX = touch.pageX - event.pageX; | |
event.deltaY = touch.pageY - event.pageY; | |
// Average the velocity of the last few events using a decay | |
// curve to even out spurious jumps in values. | |
event.velocityX = 0.3 * event.velocityX + 0.7 * event.deltaX / time; | |
event.velocityY = 0.3 * event.velocityY + 0.7 * event.deltaY / time; | |
event.pageX = touch.pageX; | |
event.pageY = touch.pageY; | |
} | |
function endEvent(target, event, timer, fn) { | |
timer.end(function(){ | |
trigger(target, 'moveend', event); | |
return fn && fn(); | |
}); | |
} | |
// Set up the DOM | |
function movestart(e) { | |
if (e.defaultPrevented) { return; } | |
if (!e.moveEnabled) { return; } | |
var event = { | |
startX: e.startX, | |
startY: e.startY, | |
pageX: e.pageX, | |
pageY: e.pageY, | |
distX: e.distX, | |
distY: e.distY, | |
deltaX: e.deltaX, | |
deltaY: e.deltaY, | |
velocityX: e.velocityX, | |
velocityY: e.velocityY, | |
identifier: e.identifier, | |
targetTouches: e.targetTouches, | |
finger: e.finger | |
}; | |
var data = { | |
target: e.target, | |
event: event, | |
timer: new Timer(update), | |
touch: undefined, | |
timeStamp: e.timeStamp | |
}; | |
function update(time) { | |
updateEvent(event, data.touch, data.timeStamp); | |
trigger(data.target, 'move', event); | |
} | |
if (e.identifier === undefined) { | |
// We're dealing with a mouse event. | |
// Stop clicks from propagating during a move | |
on(e.target, 'click', preventDefault); | |
on(document, mouseevents.move, activeMousemove, data); | |
on(document, mouseevents.end, activeMouseend, data); | |
} | |
else { | |
// In order to unbind correct handlers they have to be unique | |
data.activeTouchmove = function(e, data) { activeTouchmove(e, data); }; | |
data.activeTouchend = function(e, data) { activeTouchend(e, data); }; | |
// We're dealing with a touch. | |
on(document, touchevents.move, data.activeTouchmove, data); | |
on(document, touchevents.end, data.activeTouchend, data); | |
} | |
} | |
on(document, 'mousedown', mousedown); | |
on(document, 'touchstart', touchstart); | |
on(document, 'movestart', movestart); | |
// jQuery special events | |
// | |
// jQuery event objects are copies of DOM event objects. They need | |
// a little help copying the move properties across. | |
if (!window.jQuery) { return; } | |
var properties = ("startX startY pageX pageY distX distY deltaX deltaY velocityX velocityY").split(' '); | |
function enableMove1(e) { e.enableMove(); } | |
function enableMove2(e) { e.enableMove(); } | |
function enableMove3(e) { e.enableMove(); } | |
function add(handleObj) { | |
var handler = handleObj.handler; | |
handleObj.handler = function(e) { | |
// Copy move properties across from originalEvent | |
var i = properties.length; | |
var property; | |
while(i--) { | |
property = properties[i]; | |
e[property] = e.originalEvent[property]; | |
} | |
handler.apply(this, arguments); | |
}; | |
} | |
jQuery.event.special.movestart = { | |
setup: function() { | |
// Movestart must be enabled to allow other move events | |
on(this, 'movestart', enableMove1); | |
// Do listen to DOM events | |
return false; | |
}, | |
teardown: function() { | |
off(this, 'movestart', enableMove1); | |
return false; | |
}, | |
add: add | |
}; | |
jQuery.event.special.move = { | |
setup: function() { | |
on(this, 'movestart', enableMove2); | |
return false; | |
}, | |
teardown: function() { | |
off(this, 'movestart', enableMove2); | |
return false; | |
}, | |
add: add | |
}; | |
jQuery.event.special.moveend = { | |
setup: function() { | |
on(this, 'movestart', enableMove3); | |
return false; | |
}, | |
teardown: function() { | |
off(this, 'movestart', enableMove3); | |
return false; | |
}, | |
add: add | |
}; | |
}); | |