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