1// DOM.event.move 2// 3// 2.0.0 4// 5// Stephen Band 6// 7// Triggers 'movestart', 'move' and 'moveend' events after 8// mousemoves following a mousedown cross a distance threshold, 9// similar to the native 'dragstart', 'drag' and 'dragend' events. 10// Move events are throttled to animation frames. Move event objects 11// have the properties: 12// 13// pageX: 14// pageY: Page coordinates of pointer. 15// startX: 16// startY: Page coordinates of pointer at movestart. 17// distX: 18// distY: Distance the pointer has moved since movestart. 19// deltaX: 20// deltaY: Distance the finger has moved since last event. 21// velocityX: 22// velocityY: Average velocity over last few events. 23 24 25(function(fn) { 26 if (typeof define === 'function' && define.amd) { 27 define([], fn); 28 } else if ((typeof module !== "undefined" && module !== null) && module.exports) { 29 module.exports = fn; 30 } else { 31 fn(); 32 } 33})(function(){ 34 var assign = Object.assign || window.jQuery && jQuery.extend; 35 36 // Number of pixels a pressed pointer travels before movestart 37 // event is fired. 38 var threshold = 8; 39 40 // Shim for requestAnimationFrame, falling back to timer. See: 41 // see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ 42 var requestFrame = (function(){ 43 return ( 44 window.requestAnimationFrame || 45 window.webkitRequestAnimationFrame || 46 window.mozRequestAnimationFrame || 47 window.oRequestAnimationFrame || 48 window.msRequestAnimationFrame || 49 function(fn, element){ 50 return window.setTimeout(function(){ 51 fn(); 52 }, 25); 53 } 54 ); 55 })(); 56 57 var ignoreTags = { 58 textarea: true, 59 input: true, 60 select: true, 61 button: true 62 }; 63 64 var mouseevents = { 65 move: 'mousemove', 66 cancel: 'mouseup dragstart', 67 end: 'mouseup' 68 }; 69 70 var touchevents = { 71 move: 'touchmove', 72 cancel: 'touchend', 73 end: 'touchend' 74 }; 75 76 var rspaces = /\s+/; 77 78 79 // DOM Events 80 81 var eventOptions = { bubbles: true, cancelable: true }; 82 83 var eventsSymbol = Symbol('events'); 84 85 function createEvent(type) { 86 return new CustomEvent(type, eventOptions); 87 } 88 89 function getEvents(node) { 90 return node[eventsSymbol] || (node[eventsSymbol] = {}); 91 } 92 93 function on(node, types, fn, data, selector) { 94 types = types.split(rspaces); 95 96 var events = getEvents(node); 97 var handlers, type; 98 99 function handler(e) { fn(e, data); } 100 101 for (type of types) { 102 handlers = events[type] || (events[type] = []); 103 handlers.push([fn, handler]); 104 node.addEventListener(type, handler); 105 } 106 } 107 108 function off(node, types, fn, selector) { 109 types = types.split(rspaces); 110 111 var events = getEvents(node); 112 var type, handlers, i; 113 114 if (!events) { return; } 115 116 for (type of types) { 117 handlers = events[type]; 118 if (!handlers) { continue; } 119 i = handlers.length; 120 while (i--) { 121 if (handlers[i][0] === fn) { 122 node.removeEventListener(type, handlers[i][1]); 123 handlers.splice(i, 1); 124 } 125 } 126 } 127 } 128 129 function trigger(node, type, properties) { 130 // Don't cache events. It prevents you from triggering an event of a 131 // given type from inside the handler of another event of that type. 132 var event = createEvent(type); 133 if (properties) { assign(event, properties); } 134 node.dispatchEvent(event); 135 } 136 137 138 // Constructors 139 140 function Timer(fn){ 141 var callback = fn, 142 active = false, 143 running = false; 144 145 function trigger(time) { 146 if (active){ 147 callback(); 148 requestFrame(trigger); 149 running = true; 150 active = false; 151 } 152 else { 153 running = false; 154 } 155 } 156 157 this.kick = function(fn) { 158 active = true; 159 if (!running) { trigger(); } 160 }; 161 162 this.end = function(fn) { 163 var cb = callback; 164 165 if (!fn) { return; } 166 167 // If the timer is not running, simply call the end callback. 168 if (!running) { 169 fn(); 170 } 171 // If the timer is running, and has been kicked lately, then 172 // queue up the current callback and the end callback, otherwise 173 // just the end callback. 174 else { 175 callback = active ? 176 function(){ cb(); fn(); } : 177 fn ; 178 179 active = true; 180 } 181 }; 182 } 183 184 185 // Functions 186 187 function noop() {} 188 189 function preventDefault(e) { 190 e.preventDefault(); 191 } 192 193 function isIgnoreTag(e) { 194 return !!ignoreTags[e.target.tagName.toLowerCase()]; 195 } 196 197 function isPrimaryButton(e) { 198 // Ignore mousedowns on any button other than the left (or primary) 199 // mouse button, or when a modifier key is pressed. 200 return (e.which === 1 && !e.ctrlKey && !e.altKey); 201 } 202 203 function identifiedTouch(touchList, id) { 204 var i, l; 205 206 if (touchList.identifiedTouch) { 207 return touchList.identifiedTouch(id); 208 } 209 210 // touchList.identifiedTouch() does not exist in 211 // webkit yet⦠we must do the search ourselves... 212 213 i = -1; 214 l = touchList.length; 215 216 while (++i < l) { 217 if (touchList[i].identifier === id) { 218 return touchList[i]; 219 } 220 } 221 } 222 223 function changedTouch(e, data) { 224 var touch = identifiedTouch(e.changedTouches, data.identifier); 225 226 // This isn't the touch you're looking for. 227 if (!touch) { return; } 228 229 // Chrome Android (at least) includes touches that have not 230 // changed in e.changedTouches. That's a bit annoying. Check 231 // that this touch has changed. 232 if (touch.pageX === data.pageX && touch.pageY === data.pageY) { return; } 233 234 return touch; 235 } 236 237 238 // Handlers that decide when the first movestart is triggered 239 240 function mousedown(e){ 241 // Ignore non-primary buttons 242 if (!isPrimaryButton(e)) { return; } 243 244 // Ignore form and interactive elements 245 if (isIgnoreTag(e)) { return; } 246 247 on(document, mouseevents.move, mousemove, e); 248 on(document, mouseevents.cancel, mouseend, e); 249 } 250 251 function mousemove(e, data){ 252 checkThreshold(e, data, e, removeMouse); 253 } 254 255 function mouseend(e, data) { 256 removeMouse(); 257 } 258 259 function removeMouse() { 260 off(document, mouseevents.move, mousemove); 261 off(document, mouseevents.cancel, mouseend); 262 } 263 264 function touchstart(e) { 265 // Don't get in the way of interaction with form elements 266 if (ignoreTags[e.target.tagName.toLowerCase()]) { return; } 267 268 var touch = e.changedTouches[0]; 269
270 // iOS live updates the touch objects whereas Android gives us copies. 271 // That means we can't trust the touchstart object to stay the same, 272 // so we must copy the data. This object acts as a template for 273 // movestart, move and moveend event objects. 274 var data = { 275 target: touch.target, 276 pageX: touch.pageX, 277 pageY: touch.pageY, 278 identifier: touch.identifier, 279 280 // The only way to make handlers individually unbindable is by 281 // making them unique. 282 touchmove: function(e, data) { touchmove(e, data); }, 283 touchend: function(e, data) { touchend(e, data); } 284 }; 285 286 on(document, touchevents.move, data.touchmove, data); 287 on(document, touchevents.cancel, data.touchend, data); 288 } 289 290 function touchmove(e, data) { 291 var touch = changedTouch(e, data); 292 if (!touch) { return; } 293 checkThreshold(e, data, touch, removeTouch); 294 } 295 296 function touchend(e, data) { 297 var touch = identifiedTouch(e.changedTouches, data.identifier); 298 if (!touch) { return; } 299 removeTouch(data); 300 } 301 302 function removeTouch(data) { 303 off(document, touchevents.move, data.touchmove); 304 off(document, touchevents.cancel, data.touchend); 305 } 306 307 function checkThreshold(e, data, touch, fn) { 308 var distX = touch.pageX - data.pageX; 309 var distY = touch.pageY - data.pageY; 310 311 // Do nothing if the threshold has not been crossed. 312 if ((distX * distX) + (distY * distY) < (threshold * threshold)) { return; } 313 314 triggerStart(e, data, touch, distX, distY, fn); 315 } 316 317 function triggerStart(e, data, touch, distX, distY, fn) { 318 var touches = e.targetTouches; 319 var time = e.timeStamp - data.timeStamp; 320 321 // Create a movestart object with some special properties that 322 // are passed only to the movestart handlers. 323 var template = { 324 altKey: e.altKey, 325 ctrlKey: e.ctrlKey, 326 shiftKey: e.shiftKey, 327 startX: data.pageX, 328 startY: data.pageY, 329 distX: distX, 330 distY: distY, 331 deltaX: distX, 332 deltaY: distY, 333 pageX: touch.pageX, 334 pageY: touch.pageY, 335 velocityX: distX / time, 336 velocityY: distY / time, 337 identifier: data.identifier, 338 targetTouches: touches, 339 finger: touches ? touches.length : 1, 340 enableMove: function() { 341 this.moveEnabled = true; 342 this.enableMove = noop; 343 e.preventDefault(); 344 } 345 }; 346 347 // Trigger the movestart event. 348 trigger(data.target, 'movestart', template); 349 350 // Unbind handlers that tracked the touch or mouse up till now. 351 fn(data); 352 } 353 354 355 // Handlers that control what happens following a movestart 356 357 function activeMousemove(e, data) { 358 var timer = data.timer; 359 360 data.touch = e; 361 data.timeStamp = e.timeStamp; 362 timer.kick(); 363 } 364 365 function activeMouseend(e, data) { 366 var target = data.target; 367 var event = data.event; 368 var timer = data.timer; 369 370 removeActiveMouse(); 371 372 endEvent(target, event, timer, function() { 373 // Unbind the click suppressor, waiting until after mouseup 374 // has been handled. 375 setTimeout(function(){ 376 off(target, 'click', preventDefault); 377 }, 0); 378 }); 379 } 380 381 function removeActiveMouse() { 382 off(document, mouseevents.move, activeMousemove); 383 off(document, mouseevents.end, activeMouseend); 384 } 385 386 function activeTouchmove(e, data) { 387 var event = data.event; 388 var timer = data.timer; 389 var touch = changedTouch(e, event); 390 391 if (!touch) { return; } 392 393 // Stop the interface from gesturing 394 e.preventDefault(); 395 396 event.targetTouches = e.targetTouches; 397 data.touch = touch; 398 data.timeStamp = e.timeStamp; 399 400 timer.kick(); 401 } 402 403 function activeTouchend(e, data) { 404 var target = data.target; 405 var event = data.event; 406 var timer = data.timer; 407 var touch = identifiedTouch(e.changedTouches, event.identifier); 408 409 // This isn't the touch you're looking for. 410 if (!touch) { return; } 411 412 removeActiveTouch(data); 413 endEvent(target, event, timer); 414 } 415 416 function removeActiveTouch(data) { 417 off(document, touchevents.move, data.activeTouchmove); 418 off(document, touchevents.end, data.activeTouchend); 419 } 420 421 422 // Logic for triggering move and moveend events 423 424 function updateEvent(event, touch, timeStamp) { 425 var time = timeStamp - event.timeStamp; 426 427 event.distX = touch.pageX - event.startX; 428 event.distY = touch.pageY - event.startY; 429 event.deltaX = touch.pageX - event.pageX; 430 event.deltaY = touch.pageY - event.pageY; 431 432 // Average the velocity of the last few events using a decay 433 // curve to even out spurious jumps in values. 434 event.velocityX = 0.3 * event.velocityX + 0.7 * event.deltaX / time; 435 event.velocityY = 0.3 * event.velocityY + 0.7 * event.deltaY / time; 436 event.pageX = touch.pageX; 437 event.pageY = touch.pageY; 438 } 439 440 function endEvent(target, event, timer, fn) { 441 timer.end(function(){ 442 trigger(target, 'moveend', event); 443 return fn && fn(); 444 }); 445 } 446 447 448 // Set up the DOM 449 450 function movestart(e) { 451 if (e.defaultPrevented) { return; } 452 if (!e.moveEnabled) { return; } 453 454 var event = { 455 startX: e.startX, 456 startY: e.startY, 457 pageX: e.pageX, 458 pageY: e.pageY, 459 distX: e.distX, 460 distY: e.distY, 461 deltaX: e.deltaX, 462 deltaY: e.deltaY, 463 velocityX: e.velocityX, 464 velocityY: e.velocityY, 465 identifier: e.identifier, 466 targetTouches: e.targetTouches, 467 finger: e.finger 468 }; 469 470 var data = { 471 target: e.target, 472 event: event, 473 timer: new Timer(update), 474 touch: undefined, 475 timeStamp: e.timeStamp 476 }; 477 478 function update(time) { 479 updateEvent(event, data.touch, data.timeStamp); 480 trigger(data.target, 'move', event); 481 } 482 483 if (e.identifier === undefined) { 484 // We're dealing with a mouse event. 485 // Stop clicks from propagating during a move 486 on(e.target, 'click', preventDefault); 487 on(document, mouseevents.move, activeMousemove, data); 488 on(document, mouseevents.end, activeMouseend, data); 489 } 490 else { 491 // In order to unbind correct handlers they have to be unique 492 data.activeTouchmove = function(e, data) { activeTouchmove(e, data); }; 493 data.activeTouchend = function(e, data) { activeTouchend(e, data); }; 494 495 // We're dealing with a touch. 496 on(document, touchevents.move, data.activeTouchmove, data); 497 on(document, touchevents.end, data.activeTouchend, data); 498 } 499 } 500 501 on(document, 'mousedown', mousedown); 502 on(document, 'touchstart', touchstart); 503 on(document, 'movestart', movestart); 504 505 506 // jQuery special events 507 // 508 // jQuery event objects are copies of DOM event objects. They need 509 // a little help copying the move properties across. 510 511 if (!window.jQuery) { return; } 512 513 var properties = ("startX startY pageX pageY distX distY deltaX deltaY velocityX velocityY").split(' '); 514 515 function enableMove1(e) { e.enableMove(); } 516 function enableMove2(e) { e.enableMove(); } 517 function enableMove3(e) { e.enableMove(); } 518 519 function add(handleObj) { 520 var handler = handleObj.handler; 521 522 handleObj.handler = function(e) { 523 // Copy move properties across from originalEvent 524 var property; 525 for (property of properties) { 526 e[property] = e.originalEvent[property]; 527 } 528 handler.apply(this, arguments); 529 }; 530 } 531 532 jQuery.event.special.movestart = { 533 setup: function() { 534 // Movestart must be enabled to allow other move events 535 on(this, 'movestart', enableMove1); 536 537 // Do listen to DOM events 538 return false;
539 }, 540 541 teardown: function() { 542 off(this, 'movestart', enableMove1); 543 return false; 544 }, 545 546 add: add 547 }; 548 549 jQuery.event.special.move = { 550 setup: function() { 551 on(this, 'movestart', enableMove2); 552 return false; 553 }, 554 555 teardown: function() { 556 off(this, 'movestart', enableMove2); 557 return false; 558 }, 559 560 add: add 561 }; 562 563 jQuery.event.special.moveend = { 564 setup: function() { 565 on(this, 'movestart', enableMove3); 566 return false; 567 }, 568 569 teardown: function() { 570 off(this, 'movestart', enableMove3); 571 return false; 572 }, 573 574 add: add 575 }; 576});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.