1// noinspection JSIncompatibleTypesComparison 2 3/* 4 Slip - swiping and reordering in lists of elements on touch screens, no fuss. 5 6 Fires these events on list elements: 7 8 ⢠slip:swipe 9 When swipe has been done and user has lifted finger off the screen. 10 If you execute event.preventDefault() the element will be animated back to original position. 11 Otherwise it will be animated off the list and set to display:none. 12 13 ⢠slip:beforeswipe 14 Fired before first swipe movement starts. 15 If you execute event.preventDefault() then element will not move at all. 16 17 ⢠slip:cancelswipe 18 Fired after the user has started to swipe, but lets go without actually swiping left or right. 19 20 ⢠slip:animateswipe 21 Fired while swiping, before the user has let go of the element. 22 event.detail.x contains the amount of movement in the x direction. 23 If you execute event.preventDefault() then the element will not move to this position. 24 This can be useful for saturating the amount of swipe, or preventing movement in one direction, but allowing it in the other. 25 26 ⢠slip:reorder 27 Element has been dropped in new location. event.detail contains the following: 28 ⢠insertBefore: DOM node before which element has been dropped (null is the end of the list). Use with node.insertBefore(). 29 ⢠spliceIndex: Index of element before which current element has been dropped, not counting the element iself. 30 For use with Array.splice() if the list is reflecting objects in some array. 31 ⢠originalIndex: The original index of the element before it was reordered. 32 33 ⢠slip:beforereorder 34 When reordering movement starts. 35 Element being reordered gets class `slip-reordering`. 36 If you execute event.preventDefault() then the element will not move at all. 37 38 ⢠slip:beforewait 39 If you execute event.preventDefault() then reordering will begin immediately, blocking ability to scroll the page. 40 41 ⢠slip:tap 42 When element was tapped without being swiped/reordered. You can check `event.target` to limit that behavior to drag handles. 43 44 45 Usage: 46 47 CSS: 48 You should set `user-select:none` (and WebKit prefixes, sigh) on list elements, 49 otherwise unstoppable and glitchy text selection in iOS will get in the way. 50 51 You should set `overflow-x: hidden` on the container or body to prevent horizontal scrollbar 52 appearing when elements are swiped off the list. 53 54 55 var list = document.querySelector('ul#slippylist'); 56 new Slip(list); 57 58 list.addEventListener('slip:beforeswipe', function(e) { 59 if (shouldNotSwipe(e.target)) e.preventDefault(); 60 }); 61 62 list.addEventListener('slip:swipe', function(e) { 63 // e.target swiped 64 if (thatWasSwipeToRemove) { 65 e.target.parentNode.removeChild(e.target); 66 } else { 67 e.preventDefault(); // will animate back to original position 68 } 69 }); 70 71 list.addEventListener('slip:beforereorder', function(e) { 72 if (shouldNotReorder(e.target)) e.preventDefault(); 73 }); 74 75 list.addEventListener('slip:reorder', function(e) { 76 // e.target reordered. 77 if (reorderedOK) { 78 e.target.parentNode.insertBefore(e.target, e.detail.insertBefore); 79 } else { 80 e.preventDefault(); 81 } 82 }); 83 84 Requires: 85 ⢠Touch events 86 ⢠CSS transforms 87 ⢠Function.bind() 88 89 Caveats: 90 ⢠Elements must not change size while reordering or swiping takes place (otherwise it will be visually out of sync) 91*/ 92/*! @license 93 Slip.js 1.2.0 94 95 © 2014 Kornel LesiÅski <[email protected]>. All rights reserved. 96 97 Redistribution and use in source and binary forms, with or without modification, 98 are permitted provided that the following conditions are met: 99 100 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 101 102 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and 103 the following disclaimer in the documentation and/or other materials provided with the distribution. 104 105 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 106 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 107 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
108 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 109 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 110 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 111 USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 112*/ 113 114window['Slip'] = (function(){ 115 'use strict'; 116 117 var accessibilityDefaults = { 118 // Set values to false if you don't want Slip to manage them 119 container: { 120 role: "listbox", 121 tabIndex: 0, 122 focus: false, // focuses after drop 123 }, 124 items: { 125 role: "option", // If "option" flattens items, try "group": 126 // https://www.marcozehe.de/2013/03/08/sometimes-you-have-to-use-illegal-wai-aria-to-make-stuff-work/ 127 tabIndex: -1, // 0 will make every item tabbable, which isn't always useful 128 focus: false, // focuses when dragging 129 }, 130 }; 131 132 var damnYouChrome = /Chrome\/[3-5]/.test(navigator.userAgent); // For bugs that can't be programmatically detected :( Intended to catch all versions of Chrome 30-40 133 var needsBodyHandlerHack = damnYouChrome; // Otherwise I _sometimes_ don't get any touchstart events and only clicks instead. 134 135 /* When dragging elements down in Chrome (tested 34-37) dragged element may appear below stationary elements. 136 Looks like WebKit bug #61824, but iOS Safari doesn't have that problem. */ 137 var compositorDoesNotOrderLayers = damnYouChrome; 138 139 // -webkit-mess 140 var testElementStyle = document.createElement('div').style; 141 142 var transitionJSPropertyName = "transition" in testElementStyle ? "transition" : "webkitTransition"; 143 var transformJSPropertyName = "transform" in testElementStyle ? "transform" : "webkitTransform"; 144 var transformCSSPropertyName = transformJSPropertyName === "webkitTransform" ? "-webkit-transform" : "transform"; 145 var userSelectJSPropertyName = "userSelect" in testElementStyle ? "userSelect" : "webkitUserSelect"; 146 147 testElementStyle[transformJSPropertyName] = 'translateZ(0)'; 148 var hwLayerMagicStyle = testElementStyle[transformJSPropertyName] ? 'translateZ(0) ' : ''; 149 var hwTopLayerMagicStyle = testElementStyle[transformJSPropertyName] ? 'translateZ(1px) ' : ''; 150 testElementStyle = null; 151 152 var globalInstances = 0; 153 var attachedBodyHandlerHack = false; 154 var nullHandler = function(){}; 155 156 function Slip(container, options) { 157 if ('string' === typeof container) container = document.querySelector(container); 158 if (!container || !container.addEventListener) throw new Error("Please specify DOM node to attach to"); 159 160 if (!this || this === window) return new Slip(container, options); 161 162 this.options = options = options || {}; 163 this.options.keepSwipingPercent = options.keepSwipingPercent || 0; 164 this.options.minimumSwipeVelocity = options.minimumSwipeVelocity || 1; 165 this.options.minimumSwipeTime = options.minimumSwipeTime || 110; 166 this.options.ignoredElements = options.ignoredElements || []; 167 this.options.accessibility = options.accessibility || accessibilityDefaults; 168 this.options.accessibility.container = options.accessibility.container || accessibilityDefaults.container; 169 170 this.options.accessibility.container.role = options.accessibility.container.role !== undefined ? 171 options.accessibility.container.role : 172 accessibilityDefaults.container.role; 173 174 this.options.accessibility.container.tabIndex = options.accessibility.container.tabIndex !== undefined ? 175 options.accessibility.container.tabIndex : 176 accessibilityDefaults.container.tabIndex; 177 178 this.options.accessibility.container.focus = options.accessibility.container.focus !== undefined ? 179 options.accessibility.container.focus : 180 accessibilityDefaults.container.focus; 181 182 this.options.accessibility.items = options.accessibility.items || accessibilityDefaults.items; 183 184 this.options.accessibility.items.role = options.accessibility.items.role !== undefined ? 185 options.accessibility.items.role : 186 accessibilityDefaults.items.role; 187 188 this.options.accessibility.items.tabIndex = options.accessibility.items.tabIndex !== undefined ? 189 options.accessibility.items.tabIndex : 190 accessibilityDefaults.items.tabIndex; 191 192 this.options.accessibility.items.role = options.accessibility.items.focus !== undefined ? 193 options.accessibility.items.focus : 194 accessibilityDefaults.items.focus; 195 196 if (!Array.isArray(this.options.ignoredElements)) throw new Error("ignoredElements must be an Array"); 197 198 // Functions used for as event handlers need usable `this` and must not change to be removable 199 this.cancel = this.setState.bind(this, this.states.idle); 200 this.onTouchStart = this.onTouchStart.bind(this); 201 this.onTouchMove = this.onTouchMove.bind(this); 202 this.onTouchEnd = this.onTouchEnd.bind(this); 203 this.onMouseDown = this.onMouseDown.bind(this); 204 this.onMouseMove = this.onMouseMove.bind(this); 205 this.onMouseUp = this.onMouseUp.bind(this); 206 this.onMouseLeave = this.onMouseLeave.bind(this); 207 this.onSelection = this.onSelection.bind(this); 208 this.onContainerFocus = this.onContainerFocus.bind(this); 209 210 this.setState(this.states.idle); 211 this.attach(container); 212 } 213 214 function getTransform(node) { 215 var transform = node.style[transformJSPropertyName]; 216 if (transform) { 217 return { 218 value: transform, 219 original: transform, 220 }; 221 } 222 223 if (window.getComputedStyle) { 224 var style = window.getComputedStyle(node).getPropertyValue(transformCSSPropertyName); 225 if (style && style !== 'none') return {value:style, original:''}; 226 } 227 return {value:'', original:''}; 228 } 229 230 function findIndex(target, nodes) { 231 var originalIndex = 0; 232 var listCount = 0; 233 234 for (var i=0; i < nodes.length; i++) { 235 if (nodes[i].nodeType === 1) { 236 listCount++; 237 if (nodes[i] === target.node) { 238 originalIndex = listCount-1; 239 } 240 } 241 } 242 243 return originalIndex; 244 } 245 246 // All functions in states are going to be executed in context of Slip object 247 Slip.prototype = { 248 249 container: null, 250 options: {}, 251 state: null, 252 253 target: null, // the tapped/swiped/reordered node with height and backed up styles 254 255 usingTouch: false, // there's no good way to detect touchscreen preference other than receiving a touch event (really, trust me). 256 mouseHandlersAttached: false, 257 258 startPosition: null, // x,y,time where first touch began 259 latestPosition: null, // x,y,time where the finger is currently 260 previousPosition: null, // x,y,time where the finger was ~100ms ago (for velocity calculation) 261 262 canPreventScrolling: false, 263 264 states: { 265 idle: function idleStateInit() { 266 this.removeMouseHandlers(); 267 if (this.target) { 268 this.target.node.style.willChange = ''; 269 this.target = null; 270 } 271 this.usingTouch = false;
272 273 return { 274 allowTextSelection: true, 275 }; 276 }, 277 278 undecided: function undecidedStateInit() { 279 this.target.height = this.target.node.offsetHeight; 280 this.target.node.style.willChange = transformCSSPropertyName; 281 this.target.node.style[transitionJSPropertyName] = ''; 282 283 if (!this.dispatch(this.target.originalTarget, 'beforewait')) { 284 if (this.dispatch(this.target.originalTarget, 'beforereorder')) { 285 this.setState(this.states.reorder); 286 } 287 } else { 288 var holdTimer = setTimeout(function(){ 289 var move = this.getAbsoluteMovement(); 290 if (this.canPreventScrolling && move.x < 15 && move.y < 25) { 291 if (this.dispatch(this.target.originalTarget, 'beforereorder')) { 292 this.setState(this.states.reorder); 293 } 294 } 295 }.bind(this), 300); 296 } 297 298 return { 299 leaveState: function() { 300 clearTimeout(holdTimer); 301 }, 302 303 onMove: function() { 304 var move = this.getAbsoluteMovement(); 305 306 if (move.x > 20 && move.y < Math.max(100, this.target.height)) { 307 if (this.dispatch(this.target.originalTarget, 'beforeswipe', {directionX: move.directionX, directionY: move.directionY})) { 308 this.setState(this.states.swipe); 309 return false; 310 } else { 311 this.setState(this.states.idle); 312 } 313 } 314 if (move.y > 20) { 315 this.setState(this.states.idle); 316 } 317 318 // Chrome likes sideways scrolling :( 319 if (move.x > move.y*1.2) return false; 320 }, 321 322 onLeave: function() { 323 this.setState(this.states.idle); 324 }, 325 326 onEnd: function() { 327 var allowDefault = this.dispatch(this.target.originalTarget, 'tap'); 328 this.setState(this.states.idle); 329 return allowDefault; 330 }, 331 }; 332 }, 333 334 swipe: function swipeStateInit() { 335 var swipeSuccess = false; 336 var container = this.container; 337 338 var originalIndex = findIndex(this.target, this.container.childNodes); 339 340 container.classList.add('slip-swiping-container'); 341 function removeClass() { 342 container.classList.remove('slip-swiping-container'); 343 } 344 345 this.target.height = this.target.node.offsetHeight; 346 347 return { 348 leaveState: function() { 349 if (swipeSuccess) { 350 this.animateSwipe(function(target){ 351 target.node.style[transformJSPropertyName] = target.baseTransform.original; 352 target.node.style[transitionJSPropertyName] = ''; 353 if (this.dispatch(target.node, 'afterswipe')) { 354 removeClass(); 355 return true; 356 } else { 357 this.animateToZero(undefined, target); 358 } 359 }.bind(this)); 360 } else { 361 this.animateToZero(removeClass); 362 } 363 }, 364 365 onMove: function() { 366 var move = this.getTotalMovement(); 367 368 if (Math.abs(move.y) < this.target.height+20) { 369 if (this.dispatch(this.target.node, 'animateswipe', {x: move.x, originalIndex: originalIndex})) { 370 this.target.node.style[transformJSPropertyName] = 'translate(' + move.x + 'px,0) ' + hwLayerMagicStyle + this.target.baseTransform.value; 371 } 372 return false;
373 } else { 374 this.dispatch(this.target.node, 'cancelswipe'); 375 this.setState(this.states.idle); 376 } 377 }, 378 379 onLeave: function() { 380 this.state.onEnd.call(this); 381 }, 382 383 onEnd: function() { 384 var move = this.getAbsoluteMovement(); 385 var velocity = move.x / move.time; 386 387 // How far out has the item been swiped? 388 var swipedPercent = Math.abs((this.startPosition.x - this.previousPosition.x) / this.container.clientWidth) * 100; 389 390 var swiped = (velocity > this.options.minimumSwipeVelocity && move.time > this.options.minimumSwipeTime) || (this.options.keepSwipingPercent && swipedPercent > this.options.keepSwipingPercent); 391 392 if (swiped) { 393 if (this.dispatch(this.target.node, 'swipe', {direction: move.directionX, originalIndex: originalIndex})) { 394 swipeSuccess = true; // can't animate here, leaveState overrides anim 395 } 396 } else { 397 this.dispatch(this.target.node, 'cancelswipe'); 398 } 399 this.setState(this.states.idle); 400 return !swiped; 401 }, 402 }; 403 }, 404 405 reorder: function reorderStateInit() { 406 if (this.target.node.focus && this.options.accessibility.items.focus) { 407 this.target.node.focus(); 408 } 409 410 this.target.height = this.target.node.offsetHeight; 411 412 var nodes; 413 if (this.options.ignoredElements.length) { 414 var container = this.container; 415 var query = container.tagName.toLowerCase(); 416 if (container.getAttribute('id')) { 417 query = '#' + container.getAttribute('id'); 418 } else if (container.classList.length) { 419 query += '.' + container.getAttribute('class').replace(' ', '.'); 420 } 421 query += ' > '; 422 this.options.ignoredElements.forEach(function (selector) { 423 query += ':not(' + selector + ')'; 424 }); 425 try { 426 nodes = container.parentNode.querySelectorAll(query); 427 } catch(err) { 428 if (err instanceof DOMException && err.name === 'SyntaxError') 429 throw new Error('ignoredElements you specified contain invalid query'); 430 else 431 throw err; 432 } 433 } else { 434 nodes = this.container.childNodes; 435 } 436 var originalIndex = findIndex(this.target, nodes); 437 var mouseOutsideTimer; 438 var zero = this.target.node.offsetTop + this.target.height/2; 439 var otherNodes = []; 440 for(var i=0; i < nodes.length; i++) { 441 if (nodes[i].nodeType != 1 || nodes[i] === this.target.node) continue; 442 var t = nodes[i].offsetTop; 443 nodes[i].style[transitionJSPropertyName] = transformCSSPropertyName + ' 0.2s ease-in-out'; 444 otherNodes.push({ 445 node: nodes[i], 446 baseTransform: getTransform(nodes[i]), 447 pos: t + (t < zero ? nodes[i].offsetHeight : 0) - zero, 448 }); 449 } 450 451 this.target.node.classList.add('slip-reordering'); 452 this.target.node.style.zIndex = '99999'; 453 this.target.node.style[userSelectJSPropertyName] = 'none'; 454 if (compositorDoesNotOrderLayers) { 455 // Chrome's compositor doesn't sort 2D layers 456 this.container.style.webkitTransformStyle = 'preserve-3d'; 457 } 458 459 function onMove() { 460 /*jshint validthis:true */ 461 462 this.updateScrolling(); 463 464 if (mouseOutsideTimer) { 465 // don't care where the mouse is as long as it moves 466 clearTimeout(mouseOutsideTimer); mouseOutsideTimer = null; 467 } 468 469 var move = this.getTotalMovement(); 470 this.target.node.style[transformJSPropertyName] = 'translate(0,' + move.y + 'px) ' + hwTopLayerMagicStyle + this.target.baseTransform.value; 471 472 var height = this.target.height;
473 otherNodes.forEach(function(o){ 474 var off = 0; 475 if (o.pos < 0 && move.y < 0 && o.pos > move.y) { 476 off = height; 477 } 478 else if (o.pos > 0 && move.y > 0 && o.pos < move.y) { 479 off = -height; 480 } 481 // FIXME: should change accelerated/non-accelerated state lazily 482 o.node.style[transformJSPropertyName] = off ? 'translate(0,'+off+'px) ' + hwLayerMagicStyle + o.baseTransform.value : o.baseTransform.original; 483 }); 484 return false; 485 } 486 487 onMove.call(this); 488 489 return { 490 leaveState: function() { 491 if (mouseOutsideTimer) clearTimeout(mouseOutsideTimer); 492 493 if (compositorDoesNotOrderLayers) { 494 this.container.style.webkitTransformStyle = ''; 495 } 496 497 if (this.container.focus && this.options.accessibility.container.focus) { 498 this.container.focus(); 499 } 500 501 this.target.node.classList.remove('slip-reordering'); 502 this.target.node.style[userSelectJSPropertyName] = ''; 503 504 this.animateToZero(function(target){ 505 target.node.style.zIndex = ''; 506 }); 507 otherNodes.forEach(function(o){ 508 o.node.style[transformJSPropertyName] = o.baseTransform.original; 509 o.node.style[transitionJSPropertyName] = ''; // FIXME: animate to new position 510 }); 511 }, 512 513 onMove: onMove, 514 515 onLeave: function() { 516 // don't let element get stuck if mouse left the window 517 // but don't cancel immediately as it'd be annoying near window edges 518 if (mouseOutsideTimer) clearTimeout(mouseOutsideTimer); 519 mouseOutsideTimer = setTimeout(function(){ 520 mouseOutsideTimer = null; 521 this.cancel(); 522 }.bind(this), 700); 523 }, 524 525 onEnd: function() { 526 var move = this.getTotalMovement(); 527 var i, spliceIndex; 528 if (move.y < 0) { 529 for (i=0; i < otherNodes.length; i++) { 530 if (otherNodes[i].pos > move.y) { 531 break; 532 } 533 } 534 spliceIndex = i; 535 } else { 536 for (i=otherNodes.length-1; i >= 0; i--) { 537 if (otherNodes[i].pos < move.y) { 538 break; 539 } 540 } 541 spliceIndex = i+1; 542 } 543 544 this.dispatch(this.target.node, 'reorder', { 545 spliceIndex: spliceIndex, 546 originalIndex: originalIndex, 547 insertBefore: otherNodes[spliceIndex] ? otherNodes[spliceIndex].node : null, 548 }); 549 550 this.setState(this.states.idle); 551 return false; 552 }, 553 }; 554 }, 555 }, 556 557 attach: function(container) { 558 globalInstances++; 559 if (this.container) this.detach(); 560 561 // In some cases taps on list elements send *only* click events and no touch events. Spotted only in Chrome 32+ 562 // Having event listener on body seems to solve the issue (although AFAIK may disable smooth scrolling as a side-effect) 563 if (!attachedBodyHandlerHack && needsBodyHandlerHack) { 564 attachedBodyHandlerHack = true; 565 document.body.addEventListener('touchstart', nullHandler, false); 566 } 567 568 this.container = container; 569 570 // Accessibility 571 if (false !== this.options.accessibility.container.tabIndex) { 572 this.container.tabIndex = this.options.accessibility.container.tabIndex; 573 } 574 if (this.options.accessibility.container.role) { 575 this.container.setAttribute('role', this.options.accessibility.container.role); 576 } 577 this.setChildNodesRoles(); 578 this.container.addEventListener('focus', this.onContainerFocus, false); 579 580 this.otherNodes = []; 581 582 // selection on iOS interferes with reordering 583 document.addEventListener("selectionchange", this.onSelection, false); 584 585 // cancel is called e.g. when iOS detects multitasking gesture 586 this.container.addEventListener('touchcancel', this.cancel, false); 587 this.container.addEventListener('touchstart', this.onTouchStart, false); 588 this.container.addEventListener('touchmove', this.onTouchMove, false); 589 this.container.addEventListener('touchend', this.onTouchEnd, false); 590 this.container.addEventListener('mousedown', this.onMouseDown, false); 591 // mousemove and mouseup are attached dynamically 592 }, 593 594 detach: function() { 595 this.cancel(); 596 597 this.container.removeEventListener('mousedown', this.onMouseDown, false); 598 this.container.removeEventListener('touchend', this.onTouchEnd, false); 599 this.container.removeEventListener('touchmove', this.onTouchMove, false); 600 this.container.removeEventListener('touchstart', this.onTouchStart, false); 601 this.container.removeEventListener('touchcancel', this.cancel, false); 602 603 document.removeEventListener("selectionchange", this.onSelection, false); 604 605 if (false !== this.options.accessibility.container.tabIndex) { 606 this.container.removeAttribute('tabIndex'); 607 } 608 if (this.options.accessibility.container.role) { 609 this.container.removeAttribute('role'); 610 } 611 this.unSetChildNodesRoles(); 612 613 globalInstances--; 614 if (!globalInstances && attachedBodyHandlerHack) { 615 attachedBodyHandlerHack = false;
616 document.body.removeEventListener('touchstart', nullHandler, false); 617 } 618 }, 619 620 setState: function(newStateCtor){ 621 if (this.state) { 622 if (this.state.ctor === newStateCtor) return; 623 if (this.state.leaveState) this.state.leaveState.call(this); 624 } 625 626 // Must be re-entrant in case ctor changes state 627 var prevState = this.state; 628 var nextState = newStateCtor.call(this); 629 if (this.state === prevState) { 630 nextState.ctor = newStateCtor; 631 this.state = nextState; 632 } 633 }, 634 635 findTargetNode: function(targetNode) { 636 while(targetNode && targetNode.parentNode !== this.container) { 637 targetNode = targetNode.parentNode; 638 } 639 return targetNode; 640 }, 641 642 onContainerFocus: function(e) { 643 e.stopPropagation(); 644 this.setChildNodesRoles(); 645 }, 646 647 setChildNodesRoles: function() { 648 var nodes = this.container.childNodes; 649 for(var i=0; i < nodes.length; i++) { 650 if (nodes[i].nodeType != 1) continue; 651 if (this.options.accessibility.items.role) { 652 nodes[i].setAttribute('role', this.options.accessibility.items.role); 653 } 654 if (false !== this.options.accessibility.items.tabIndex) { 655 nodes[i].tabIndex = this.options.accessibility.items.tabIndex; 656 } 657 } 658 }, 659 660 unSetChildNodesRoles: function() { 661 var nodes = this.container.childNodes; 662 for(var i=0; i < nodes.length; i++) { 663 if (nodes[i].nodeType != 1) continue; 664 if (this.options.accessibility.items.role) { 665 nodes[i].removeAttribute('role'); 666 } 667 if (false !== this.options.accessibility.items.tabIndex) { 668 nodes[i].removeAttribute('tabIndex'); 669 } 670 } 671 }, 672 onSelection: function(e) { 673 e.stopPropagation(); 674 var isRelated = e.target === document || this.findTargetNode(e); 675 var iOS = /(iPhone|iPad|iPod)/i.test(navigator.userAgent) && !/(Android|Windows)/i.test(navigator.userAgent); 676 if (!isRelated) return; 677 678 if (iOS) { 679 // iOS doesn't allow selection to be prevented 680 this.setState(this.states.idle); 681 } else { 682 if (!this.state.allowTextSelection) { 683 e.preventDefault(); 684 } 685 } 686 }, 687 688 addMouseHandlers: function() { 689 // unlike touch events, mousemove/up is not conveniently fired on the same element, 690 // but I don't need to listen to unrelated events all the time 691 if (!this.mouseHandlersAttached) { 692 this.mouseHandlersAttached = true; 693 document.documentElement.addEventListener('mouseleave', this.onMouseLeave, false); 694 window.addEventListener('mousemove', this.onMouseMove, true); 695 window.addEventListener('mouseup', this.onMouseUp, true); 696 window.addEventListener('blur', this.cancel, false); 697 } 698 }, 699 700 removeMouseHandlers: function() { 701 if (this.mouseHandlersAttached) { 702 this.mouseHandlersAttached = false; 703 document.documentElement.removeEventListener('mouseleave', this.onMouseLeave, false); 704 window.removeEventListener('mousemove', this.onMouseMove, true); 705 window.removeEventListener('mouseup', this.onMouseUp, true); 706 window.removeEventListener('blur', this.cancel, false); 707 } 708 }, 709 710 onMouseLeave: function(e) { 711 e.stopPropagation(); 712 if (this.usingTouch) return; 713 714 if (e.target === document.documentElement || e.relatedTarget === document.documentElement) { 715 if (this.state.onLeave) { 716 this.state.onLeave.call(this); 717 } 718 } 719 }, 720 721 onMouseDown: function(e) { 722 e.stopPropagation(); 723 if (this.usingTouch || e.button != 0 || !this.setTarget(e)) return; 724 725 this.addMouseHandlers(); // mouseup, etc. 726 727 this.canPreventScrolling = true; // or rather it doesn't apply to mouse 728 729 this.startAtPosition({ 730 x: e.clientX, 731 y: e.clientY, 732 time: e.timeStamp, 733 }); 734 }, 735 736 onTouchStart: function(e) { 737 e.stopPropagation(); 738 this.usingTouch = true; 739 this.canPreventScrolling = true; 740 741 // This implementation cares only about single touch 742 if (e.touches.length > 1) { 743 this.setState(this.states.idle); 744 return; 745 } 746 747 if (!this.setTarget(e)) return; 748 749 this.startAtPosition({ 750 x: e.touches[0].clientX, 751 y: e.touches[0].clientY, 752 time: e.timeStamp, 753 }); 754 }, 755 756 setTarget: function(e) { 757 var targetNode = this.findTargetNode(e.target); 758 if (!targetNode) { 759 this.setState(this.states.idle); 760 return false;
761 } 762 763 // scrollContainer may be explicitly set via options, otherwise search upwards for a parent with an overflow-y property 764 // fallback to document.scrollingElement (or documentElement on IE), and do not use document.body 765 var scrollContainer = this.options.scrollContainer; 766 if (!scrollContainer) { 767 var top = document.scrollingElement || document.documentElement; 768 scrollContainer = targetNode.parentNode; 769 while (scrollContainer) { 770 if (scrollContainer == top) break; 771 if (scrollContainer != document.body && scrollContainer.scrollHeight > scrollContainer.clientHeight && window.getComputedStyle(scrollContainer)['overflow-y'] != 'visible') break; 772 scrollContainer = scrollContainer.parentNode; 773 } 774 scrollContainer = scrollContainer || top; 775 } 776 777 this.target = { 778 originalTarget: e.target, 779 node: targetNode, 780 scrollContainer: scrollContainer, 781 origScrollTop: scrollContainer.scrollTop, 782 origScrollHeight: scrollContainer.scrollHeight, 783 baseTransform: getTransform(targetNode), 784 }; 785 return true; 786 }, 787 788 startAtPosition: function(pos) { 789 this.startPosition = this.previousPosition = this.latestPosition = pos; 790 this.setState(this.states.undecided); 791 }, 792 793 updatePosition: function(e, pos) { 794 if (this.target == null) { 795 return; 796 } 797 this.latestPosition = pos; 798 799 if (this.state.onMove) { 800 if (this.state.onMove.call(this) === false) { 801 e.preventDefault(); 802 } 803 } 804 805 // sample latestPosition 100ms for velocity 806 if (this.latestPosition.time - this.previousPosition.time > 100) { 807 this.previousPosition = this.latestPosition; 808 } 809 }, 810 811 onMouseMove: function(e) { 812 e.stopPropagation(); 813 this.updatePosition(e, { 814 x: e.clientX, 815 y: e.clientY, 816 time: e.timeStamp, 817 }); 818 }, 819 820 onTouchMove: function(e) { 821 e.stopPropagation(); 822 this.updatePosition(e, { 823 x: e.touches[0].clientX, 824 y: e.touches[0].clientY, 825 time: e.timeStamp, 826 }); 827 828 // In Apple's touch model only the first move event after touchstart can prevent scrolling (and event.cancelable is broken) 829 this.canPreventScrolling = false; 830 }, 831 832 onMouseUp: function(e) { 833 e.stopPropagation(); 834 if (this.usingTouch || e.button !== 0) return; 835 836 if (this.state.onEnd && false === this.state.onEnd.call(this)) { 837 e.preventDefault(); 838 } 839 }, 840 841 onTouchEnd: function(e) { 842 e.stopPropagation(); 843 if (e.touches.length > 1) { 844 this.cancel(); 845 } else if (this.state.onEnd && false === this.state.onEnd.call(this)) { 846 e.preventDefault(); 847 } 848 }, 849 850 getTotalMovement: function() { 851 var scrollOffset = this.target.scrollContainer.scrollTop - this.target.origScrollTop; 852 return { 853 x: this.latestPosition.x - this.startPosition.x, 854 y: this.latestPosition.y - this.startPosition.y + scrollOffset, 855 time: this.latestPosition.time - this.startPosition.time, 856 }; 857 }, 858 859 getAbsoluteMovement: function() { 860 var move = this.getTotalMovement(); 861 return { 862 x: Math.abs(move.x), 863 y: Math.abs(move.y), 864 time: move.time, 865 directionX: move.x < 0 ? 'left' : 'right', 866 directionY: move.y < 0 ? 'up' : 'down', 867 }; 868 }, 869 870 updateScrolling: function() { 871 var triggerOffset = 40, 872 offset = 0; 873
874 var scrollable = this.target.scrollContainer, 875 containerRect = scrollable.getBoundingClientRect(), 876 targetRect = this.target.node.getBoundingClientRect(), 877 bottomOffset = Math.min(containerRect.bottom, window.innerHeight) - targetRect.bottom, 878 topOffset = targetRect.top - Math.max(containerRect.top, 0), 879 maxScrollTop = this.target.origScrollHeight - Math.min(scrollable.clientHeight, window.innerHeight); 880 881 if (bottomOffset < triggerOffset) { 882 offset = Math.min(triggerOffset, triggerOffset - bottomOffset); 883 } 884 else if (topOffset < triggerOffset) { 885 offset = Math.max(-triggerOffset, topOffset - triggerOffset); 886 } 887 888 scrollable.scrollTop = Math.max(0, Math.min(maxScrollTop, scrollable.scrollTop + offset)); 889 }, 890 891 dispatch: function(targetNode, eventName, detail) { 892 var event = document.createEvent('CustomEvent'); 893 if (event && event.initCustomEvent) { 894 event.initCustomEvent('slip:' + eventName, true, true, detail); 895 } else { 896 event = document.createEvent('Event'); 897 event.initEvent('slip:' + eventName, true, true); 898 event.detail = detail; 899 } 900 return targetNode.dispatchEvent(event); 901 }, 902 903 getSiblings: function(target) { 904 var siblings = []; 905 var tmp = target.node.nextSibling; 906 while(tmp) { 907 if (tmp.nodeType == 1) siblings.push({ 908 node: tmp, 909 baseTransform: getTransform(tmp), 910 }); 911 tmp = tmp.nextSibling; 912 } 913 return siblings; 914 }, 915 916 animateToZero: function(callback, target) { 917 // save, because this.target/container could change during animation 918 target = target || this.target; 919 920 target.node.style[transitionJSPropertyName] = transformCSSPropertyName + ' 0.1s ease-out'; 921 target.node.style[transformJSPropertyName] = 'translate(0,0) ' + hwLayerMagicStyle + target.baseTransform.value; 922 setTimeout(function(){ 923 target.node.style[transitionJSPropertyName] = ''; 924 target.node.style[transformJSPropertyName] = target.baseTransform.original; 925 if (callback) callback.call(this, target); 926 }.bind(this), 101); 927 }, 928 929 animateSwipe: function(callback) { 930 var target = this.target; 931 var siblings = this.getSiblings(target); 932 var emptySpaceTransformStyle = 'translate(0,' + this.target.height + 'px) ' + hwLayerMagicStyle + ' '; 933 934 // FIXME: animate with real velocity 935 target.node.style[transitionJSPropertyName] = 'all 0.1s linear'; 936 target.node.style[transformJSPropertyName] = ' translate(' + (this.getTotalMovement().x > 0 ? '' : '-') + '100%,0) ' + hwLayerMagicStyle + target.baseTransform.value; 937 938 setTimeout(function(){ 939 if (callback.call(this, target)) { 940 siblings.forEach(function(o){ 941 o.node.style[transitionJSPropertyName] = ''; 942 o.node.style[transformJSPropertyName] = emptySpaceTransformStyle + o.baseTransform.value; 943 }); 944 setTimeout(function(){ 945 siblings.forEach(function(o){ 946 o.node.style[transitionJSPropertyName] = transformCSSPropertyName + ' 0.1s ease-in-out'; 947 o.node.style[transformJSPropertyName] = 'translate(0,0) ' + hwLayerMagicStyle + o.baseTransform.value; 948 }); 949 setTimeout(function(){ 950 siblings.forEach(function(o){ 951 o.node.style[transitionJSPropertyName] = ''; 952 o.node.style[transformJSPropertyName] = o.baseTransform.original; 953 }); 954 }, 101); 955 }, 1); 956 } 957 }.bind(this), 101); 958 }, 959 }; 960 961 // AMD 962 if ('function' === typeof define && define.amd) { 963 define(function(){ 964 return Slip; 965 }); 966 } 967 // CJS 968 if ('object' === typeof module && module.exports) { 969 module.exports = Slip; 970 } 971 return Slip; 972})(); 973
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.