1/** 2 * Overscroll v1.7.2 3 * A jQuery Plugin that emulates the iPhone scrolling experience in a browser. 4 * http://azoffdesign.com/overscroll 5 * 6 * Intended for use with the latest jQuery 7 * http://code.jquery.com/jquery-latest.js 8 * 9 * Copyright 2013, Jonathan Azoff 10 * Licensed under the MIT license. 11 * https://github.com/azoff/overscroll/blob/master/mit.license 12 * 13 * For API documentation, see the README file 14 * http://azof.fr/pYCzuM 15 * 16 * Date: Tuesday, March 18th 2013 17 */ 18(function(global, dom, browser, math, wait, cancel, namespace, $, none){ 19 20 // We want to run this plug-in in strict-mode 21 // so that we may benefit from its optimizations 22 'use strict'; 23 24 // The key used to bind-instance specific data to an object 25 var datakey = 'overscroll'; 26 27 // create <body> node if there's not one present (e.g., for test runners) 28 if (dom.body === null) { 29 dom.documentElement.appendChild( 30 dom.createElement('body') 31 ); 32 } 33 34 // quick fix for IE 8 and below since getComputedStyle() is not supported 35 // TODO: find a better solution 36 if (!global.getComputedStyle) { 37 global.getComputedStyle = function (el, pseudo) { 38 this.el = el; 39 this.getPropertyValue = function (prop) { 40 var re = /(\-([a-z]){1})/g; 41 if (prop == 'float') prop = 'styleFloat'; 42 if (re.test(prop)) { 43 prop = prop.replace(re, function () { 44 return arguments[2].toUpperCase(); 45 }); 46 } 47 return el.currentStyle[prop] ? el.currentStyle[prop] : null; 48 }; 49 return this; 50 }; 51 } 52 53 // runs feature detection for overscroll 54 var compat = { 55 animate: (function(){ 56 var fn = global.requestAnimationFrame || 57 global.webkitRequestAnimationFrame || 58 global.mozRequestAnimationFrame || 59 global.oRequestAnimationFrame || 60 global.msRequestAnimationFrame || 61 function(callback) { wait(callback, 1000/60); }; 62 return function(callback) { 63 fn.call(global, callback); 64 }; 65 })(), 66 overflowScrolling: (function(){ 67 var style = ''; 68 var div = dom.createElement('div'); 69 var prefixes = ['webkit', 'moz', 'o', 'ms']; 70 dom.body.appendChild(div); 71 $.each(prefixes, function(i, prefix){ 72 div.style[prefix + 'OverflowScrolling'] = 'touch'; 73 }); 74 div.style.overflowScrolling = 'touch'; 75 var computedStyle = global.getComputedStyle(div); 76 if (!!computedStyle.overflowScrolling) { 77 style = 'overflow-scrolling'; 78 } else { 79 $.each(prefixes, function(i, prefix){ 80 if (!!computedStyle[prefix + 'OverflowScrolling']) { 81 style = '-' + prefix + '-overflow-scrolling'; 82 } 83 return !style; 84 }); 85 } 86 div.parentNode.removeChild(div); 87 return style; 88 })(), 89 cursor: (function() { 90 var div = dom.createElement('div'); 91 var prefixes = ['webkit', 'moz']; 92 var gmail = 'https://mail.google.com/mail/images/2/'; 93 var style = { 94 grab: 'url('+gmail+'openhand.cur), move', 95 grabbing: 'url('+gmail+'closedhand.cur), move' 96 }; 97 dom.body.appendChild(div); 98 $.each(prefixes, function(i, prefix){ 99 var found, cursor = '-' + prefix + '-grab'; 100 div.style.cursor = cursor; 101 var computedStyle = global.getComputedStyle(div); 102 found = computedStyle.cursor === cursor; 103 if (found) { 104 style = { 105 grab: '-' + prefix + '-grab', 106 grabbing: '-' + prefix + '-grabbing' 107 }; 108 } 109 return !found; 110 }); 111 div.parentNode.removeChild(div); 112 return style; 113 })() 114 }; 115 116 // These are all the events that could possibly 117 // be used by the plug-in 118 var events = { 119 drag: 'mousemove touchmove', 120 end: 'mouseup mouseleave click touchend touchcancel', 121 hover: 'mouseenter mouseleave', 122 ignored: 'select dragstart drag', 123 scroll: 'scroll', 124 start: 'mousedown touchstart', 125 wheel: 'mousewheel DOMMouseScroll' 126 }; 127 128 // These settings are used to tweak drift settings 129 // for the plug-in 130 var settings = { 131 captureThreshold: 3, 132 driftDecay: 1.1,
133 driftSequences: 22, 134 driftTimeout: 100, 135 scrollDelta: 15, 136 thumbOpacity: 0.7, 137 thumbThickness: 6, 138 thumbTimeout: 400, 139 wheelDelta: 20, 140 wheelTicks: 120 141 }; 142 143 // These defaults are used to complement any options 144 // passed into the plug-in entry point 145 var defaults = { 146 cancelOn: 'select,input,textarea', 147 direction: 'multi', 148 dragHold: false, 149 hoverThumbs: false, 150 scrollDelta: settings.scrollDelta, 151 showThumbs: true, 152 persistThumbs: false, 153 captureWheel: true, 154 wheelDelta: settings.wheelDelta, 155 wheelDirection: 'multi', 156 zIndex: 999 157 }; 158 159 // Triggers a DOM event on the overscrolled element. 160 // All events are namespaced under the overscroll name 161 function triggerEvent(event, target) { 162 target.trigger('overscroll:' + event); 163 } 164 165 // Utility function to return a timestamp 166 function time() { 167 return (new Date()).getTime(); 168 } 169 170 // Captures the position from an event, modifies the properties 171 // of the second argument to persist the position, and then 172 // returns the modified object 173 function capturePosition(event, position, index) { 174 position.x = event.pageX; 175 position.y = event.pageY; 176 position.time = time(); 177 position.index = index; 178 return position; 179 } 180 181 // Used to move the thumbs around an overscrolled element 182 function moveThumbs(thumbs, sizing, left, top) { 183 184 var ml, mt; 185 186 if (thumbs && thumbs.added) { 187 if (thumbs.horizontal) { 188 ml = left * (1 + sizing.container.width / sizing.container.scrollWidth); 189 mt = top + sizing.thumbs.horizontal.top; 190 thumbs.horizontal.css('margin', mt + 'px 0 0 ' + ml + 'px'); 191 } 192 if (thumbs.vertical) { 193 ml = left + sizing.thumbs.vertical.left; 194 mt = top * (1 + sizing.container.height / sizing.container.scrollHeight); 195 thumbs.vertical.css('margin', mt + 'px 0 0 ' + ml + 'px'); 196 } 197 } 198 199 } 200 201 // Used to toggle the thumbs on and off 202 // of an overscrolled element 203 function toggleThumbs(thumbs, options, dragging) { 204 if (thumbs && thumbs.added && !options.persistThumbs) { 205 if (dragging) { 206 if (thumbs.vertical) { 207 thumbs.vertical.stop(true, true).fadeTo('fast', settings.thumbOpacity); 208 } 209 if (thumbs.horizontal) { 210 thumbs.horizontal.stop(true, true).fadeTo('fast', settings.thumbOpacity); 211 } 212 } else { 213 if (thumbs.vertical) { 214 thumbs.vertical.fadeTo('fast', 0); 215 } 216 if (thumbs.horizontal) { 217 thumbs.horizontal.fadeTo('fast', 0); 218 } 219 } 220 } 221 } 222 223 // Defers click event listeners to after a mouseup event. 224 // Used to avoid unintentional clicks 225 function deferClick(target) { 226 var clicks, key = 'events'; 227 var events = $._data ? $._data(target[0], key) : target.data(key); 228 if (events && events.click) { 229 clicks = events.click.slice(); 230 target.off('click').one('click', function(){ 231 $.each(clicks, function(i, click){ 232 target.click(click); 233 }); return false; 234 }); 235 } 236 } 237 238 // Toggles thumbs on hover. This event is only triggered 239 // if the hoverThumbs option is set 240 function hover(event) { 241 var data = event.data, 242 thumbs = data.thumbs, 243 options = data.options, 244 dragging = event.type === 'mouseenter'; 245 toggleThumbs(thumbs, options, dragging); 246 } 247 248 // This function is only ever used when the overscrolled element 249 // scrolled outside of the scope of this plugin. 250 function scroll(event) { 251 var data = event.data; 252 if (!data.flags.dragged) { 253 /*jshint validthis:true */ 254 moveThumbs(data.thumbs, data.sizing, this.scrollLeft, this.scrollTop); 255 } 256 } 257 258 // handles mouse wheel scroll events 259 function wheel(event) { 260 261 // prevent any default wheel behavior 262 event.preventDefault(); 263 264 var data = event.data, 265 options = data.options, 266 sizing = data.sizing, 267 thumbs = data.thumbs, 268 dwheel = data.wheel, 269 flags = data.flags, 270 original = event.originalEvent, 271 delta = 0, deltaX = 0, deltaY = 0; 272 273 // stop any drifts 274 flags.drifting = false; 275 276 // normalize the wheel ticks 277 if (original.detail) { 278 delta = -original.detail; 279 if (original.detailX) { 280 deltaX = -original.detailX; 281 } 282 if (original.detailY) { 283 deltaY = -original.detailY; 284 } 285 } else if (original.wheelDelta) { 286 delta = original.wheelDelta / settings.wheelTicks; 287 if (original.wheelDeltaX) { 288 deltaX = original.wheelDeltaX / settings.wheelTicks; 289 } 290 if (original.wheelDeltaY) { 291 deltaY = original.wheelDeltaY / settings.wheelTicks; 292 } 293 } 294 295 // apply a pixel delta to each tick 296 delta *= options.wheelDelta; 297 deltaX *= options.wheelDelta; 298 deltaY *= options.wheelDelta; 299 300 // initialize flags if this is the first tick 301 if (!dwheel) { 302 data.target.data(datakey).dragging = flags.dragging = true; 303 data.wheel = dwheel = { timeout: null }; 304 toggleThumbs(thumbs, options, true); 305 } 306 307 // actually modify scroll offsets 308 if (options.wheelDirection === 'vertical'){ 309 /*jshint validthis:true */ 310 this.scrollTop -= delta; 311 } else if ( options.wheelDirection === 'horizontal') { 312 this.scrollLeft -= delta; 313 } else { 314 this.scrollLeft -= deltaX; 315 this.scrollTop -= deltaY || delta; 316 } 317 318 if (dwheel.timeout) { cancel(dwheel.timeout); } 319 320 moveThumbs(thumbs, sizing, this.scrollLeft, this.scrollTop); 321 322 dwheel.timeout = wait(function() { 323 data.target.data(datakey).dragging = flags.dragging = false; 324 toggleThumbs(thumbs, options, data.wheel = null); 325 }, settings.thumbTimeout); 326 327 } 328 329 // updates the current scroll offset during a mouse move 330 function drag(event) { 331 332 event.preventDefault(); 333 334 var data = event.data, 335 touches = event.originalEvent.touches, 336 options = data.options, 337 sizing = data.sizing, 338 thumbs = data.thumbs, 339 position = data.position, 340 flags = data.flags, 341 target = data.target.get(0); 342 343 344 // correct page coordinates for touch devices 345 if (touches && touches.length) { 346 event = touches[0]; 347 } 348 349 if (!flags.dragged) { 350 toggleThumbs(thumbs, options, true); 351 } 352 353 flags.dragged = true; 354 355 if (options.direction !== 'vertical') { 356 target.scrollLeft -= (event.pageX - position.x); 357 } 358 359 if (data.options.direction !== 'horizontal') { 360 target.scrollTop -= (event.pageY - position.y); 361 } 362 363 capturePosition(event, data.position); 364 365 if (--data.capture.index <= 0) { 366 data.target.data(datakey).dragging = flags.dragging = true; 367 capturePosition(event, data.capture, settings.captureThreshold); 368 } 369 370 moveThumbs(thumbs, sizing, target.scrollLeft, target.scrollTop); 371 372 } 373 374 // sends the overscrolled element into a drift 375 function drift(target, event, callback) { 376 377 var data = event.data, dx, dy, xMod, yMod, 378 capture = data.capture, 379 options = data.options, 380 sizing = data.sizing, 381 thumbs = data.thumbs, 382 elapsed = time() - capture.time, 383 scrollLeft = target.scrollLeft, 384 scrollTop = target.scrollTop, 385 decay = settings.driftDecay; 386 387 // only drift if enough time has passed since 388 // the last capture event 389 if (elapsed > settings.driftTimeout) { 390 callback(data); return; 391 } 392 393 // determine offset between last capture and current time 394 dx = options.scrollDelta * (event.pageX - capture.x);
395 dy = options.scrollDelta * (event.pageY - capture.y); 396 397 // update target scroll offsets 398 if (options.direction !== 'vertical') { 399 scrollLeft -= dx; 400 } if (options.direction !== 'horizontal') { 401 scrollTop -= dy; 402 } 403 404 // split the distance to travel into a set of sequences 405 xMod = dx / settings.driftSequences; 406 yMod = dy / settings.driftSequences; 407 408 triggerEvent('driftstart', data.target); 409 410 data.drifting = true; 411 412 // animate the drift sequence 413 compat.animate(function render() { 414 if (data.drifting) { 415 var min = 1, max = -1; 416 data.drifting = false; 417 if (yMod > min && target.scrollTop > scrollTop || yMod < max && target.scrollTop < scrollTop) { 418 data.drifting = true; 419 target.scrollTop -= yMod; 420 yMod /= decay; 421 } 422 if (xMod > min && target.scrollLeft > scrollLeft || xMod < max && target.scrollLeft < scrollLeft) { 423 data.drifting = true; 424 target.scrollLeft -= xMod; 425 xMod /= decay; 426 } 427 moveThumbs(thumbs, sizing, target.scrollLeft, target.scrollTop); 428 compat.animate(render); 429 } else { 430 triggerEvent('driftend', data.target); 431 callback(data); 432 } 433 }); 434 435 } 436 437 // starts the drag operation and binds the mouse move handler 438 function start(event) { 439 440 var data = event.data, 441 touches = event.originalEvent.touches, 442 target = data.target, 443 dstart = data.start = $(event.target), 444 flags = data.flags; 445 446 // stop any drifts 447 flags.drifting = false; 448 449 // only start drag if the user has not explictly banned it. 450 if (dstart.size() && !dstart.is(data.options.cancelOn)) { 451 452 // without this the simple "click" event won't be recognized on touch clients 453 if (!touches) { event.preventDefault(); } 454 455 if (!compat.overflowScrolling) { 456 target.css('cursor', compat.cursor.grabbing); 457 target.data(datakey).dragging = flags.dragging = flags.dragged = false; 458 459 // apply the drag listeners to the doc or target 460 if(data.options.dragHold) { 461 $(document).on(events.drag, data, drag); 462 } else { 463 target.on(events.drag, data, drag); 464 } 465 } 466 467 data.position = capturePosition(event, {}); 468 data.capture = capturePosition(event, {}, settings.captureThreshold); 469 triggerEvent('dragstart', target); 470 471 } 472 473 } 474 475 // ends the drag operation and unbinds the mouse move handler 476 function stop(event) { 477 478 var data = event.data, 479 target = data.target, 480 options = data.options, 481 flags = data.flags, 482 thumbs = data.thumbs, 483 484 // hides the thumbs after the animation is done 485 done = function () { 486 if (thumbs && !options.hoverThumbs) { 487 toggleThumbs(thumbs, options, false); 488 } 489 }; 490 491 // remove drag listeners from doc or target 492 if(options.dragHold) { 493 $(document).unbind(events.drag, drag); 494 } else { 495 target.unbind(events.drag, drag); 496 } 497 498 // only fire events and drift if we started with a 499 // valid position 500 if (data.position) { 501 502 triggerEvent('dragend', target); 503 504 // only drift if a drag passed our threshold 505 if (flags.dragging && !compat.overflowScrolling) { 506 drift(target.get(0), event, done); 507 } else { 508 done(); 509 } 510 511 } 512 513 // only if we moved, and the mouse down is the same as 514 // the mouse up target do we defer the event 515 if (flags.dragging && !compat.overflowScrolling && data.start && data.start.is(event.target)) { 516 deferClick(data.start); 517 } 518 519 // clear all internal flags and settings 520 target.data(datakey).dragging = 521 data.start = 522 data.capture = 523 data.position = 524 flags.dragged = 525 flags.dragging = false; 526 527 // set the cursor back to normal 528 target.css('cursor', compat.cursor.grab); 529 530 } 531 532 // Ensures that a full set of options are provided 533 // for the plug-in. Also does some validation 534 function getOptions(options) { 535 536 // fill in missing values with defaults 537 options = $.extend({}, defaults, options); 538 539 // check for inconsistent directional restrictions 540 if (options.direction !== 'multi' && options.direction !== options.wheelDirection) { 541 options.wheelDirection = options.direction; 542 } 543 544 // ensure positive values for deltas 545 options.scrollDelta = math.abs(parseFloat(options.scrollDelta)); 546 options.wheelDelta = math.abs(parseFloat(options.wheelDelta)); 547
548 // fix values for scroll offset 549 options.scrollLeft = options.scrollLeft === none ? null : math.abs(parseFloat(options.scrollLeft)); 550 options.scrollTop = options.scrollTop === none ? null : math.abs(parseFloat(options.scrollTop)); 551 552 return options; 553 554 } 555 556 // Returns the sizing information (bounding box) for the 557 // target DOM element 558 function getSizing(target) { 559 560 var $target = $(target), 561 width = $target.width(), 562 height = $target.height(), 563 scrollWidth = width >= target.scrollWidth ? width : target.scrollWidth, 564 scrollHeight = height >= target.scrollHeight ? height : target.scrollHeight, 565 hasScroll = scrollWidth > width || scrollHeight > height; 566 567 return { 568 valid: hasScroll, 569 container: { 570 width: width, 571 height: height, 572 scrollWidth: scrollWidth, 573 scrollHeight: scrollHeight 574 }, 575 thumbs: { 576 horizontal: { 577 width: width * width / scrollWidth, 578 height: settings.thumbThickness, 579 corner: settings.thumbThickness / 2, 580 left: 0, 581 top: height - settings.thumbThickness 582 }, 583 vertical: { 584 width: settings.thumbThickness, 585 height: height * height / scrollHeight, 586 corner: settings.thumbThickness / 2, 587 left: width - settings.thumbThickness, 588 top: 0 589 } 590 } 591 }; 592 593 } 594 595 // Attempts to get (or implicitly creates) the 596 // remover function for the target passed 597 // in as an argument 598 function getRemover(target, orCreate) { 599 600 var $target = $(target), thumbs, 601 data = $target.data(datakey) || {}, 602 style = $target.attr('style'), 603 fallback = orCreate ? function () { 604 605 data = $target.data(datakey); 606 thumbs = data.thumbs; 607 608 // restore original styles (if any) 609 if (style) { 610 $target.attr('style', style); 611 } else { 612 $target.removeAttr('style'); 613 } 614 615 // remove any created thumbs 616 if (thumbs) { 617 if (thumbs.horizontal) { thumbs.horizontal.remove(); } 618 if (thumbs.vertical) { thumbs.vertical.remove(); } 619 } 620 621 // remove any bound overscroll events and data 622 $target 623 .removeData(datakey) 624 .off(events.wheel, wheel) 625 .off(events.start, start) 626 .off(events.end, stop) 627 .off(events.ignored, ignore); 628 629 } : $.noop; 630 631 return $.isFunction(data.remover) ? data.remover : fallback; 632 633 } 634 635 // Genterates CSS specific to a particular thumb. 636 // It requires sizing data and options 637 function getThumbCss(size, options) { 638 return { 639 position: 'absolute', 640 opacity: options.persistThumbs ? settings.thumbOpacity : 0, 641 'background-color': 'black', 642 width: size.width + 'px', 643 height: size.height + 'px', 644 'border-radius': size.corner + 'px', 645 'margin': size.top + 'px 0 0 ' + size.left + 'px', 646 'z-index': options.zIndex 647 }; 648 } 649 650 // Creates the DOM elements used as "thumbs" within 651 // the target container. 652 function createThumbs(target, sizing, options) { 653 654 var div = '<div/>', 655 thumbs = {}, 656 css = false; 657 658 if (sizing.container.scrollWidth > 0 && options.direction !== 'vertical') { 659 css = getThumbCss(sizing.thumbs.horizontal, options); 660 thumbs.horizontal = $(div).css(css).addClass('resizable-horizontal').prependTo(target); 661 } 662 663 if (sizing.container.scrollHeight > 0 && options.direction !== 'horizontal') { 664 css = getThumbCss(sizing.thumbs.vertical, options); 665 thumbs.vertical = $(div).css(css).prependTo(target); 666 } 667 668 thumbs.added = !!css; 669 670 return thumbs; 671 672 } 673 674 // ignores events on the overscroll element 675 function ignore(event) { 676 event.preventDefault(); 677 } 678 679 // This function takes a jQuery element, some 680 // (optional) options, and sets up event metadata 681 // for each instance the plug-in affects 682 function setup(target, options) { 683 684 // create initial data properties for this instance 685 options = getOptions(options); 686 var sizing = getSizing(target), 687 thumbs, data = { 688 options: options, sizing: sizing, 689 flags: { dragging: false }, 690 remover: getRemover(target, true) 691 }; 692 693 // only apply handlers if the overscrolled element 694 // actually has an area to scroll 695 if (sizing.valid) { 696 // provide a circular-reference, enable events, and 697 // apply any required CSS 698 data.target = target = $(target).css({ 699 position: 'relative', 700 cursor: compat.cursor.grab 701 }).on(events.start, data, start) 702 .on(events.end, data, stop) 703 .on(events.ignored, data, ignore); 704 705 // apply the stop listeners for drag end 706 if(options.dragHold) { 707 $(document).on(events.end, data, stop); 708 } else { 709 data.target.on(events.end, data, stop); 710 } 711
712 // apply any user-provided scroll offsets 713 if (options.scrollLeft !== null) { 714 target.scrollLeft(options.scrollLeft); 715 } if (options.scrollTop !== null) { 716 target.scrollTop(options.scrollTop); 717 } 718 719 // use native oversroll, if it exists 720 if (compat.overflowScrolling) { 721 target.css(compat.overflowScrolling, 'touch'); 722 } else { 723 target.on(events.scroll, data, scroll); 724 } 725 726 // check to see if the user would like mousewheel support 727 if (options.captureWheel) { 728 target.on(events.wheel, data, wheel); 729 } 730 731 // add thumbs and listeners (if we're showing them) 732 if (options.showThumbs) { 733 if (compat.overflowScrolling) { 734 target.css('overflow', 'scroll'); 735 } else { 736 target.css('overflow', 'hidden'); 737 data.thumbs = thumbs = createThumbs(target, sizing, options); 738 if (thumbs.added) { 739 moveThumbs(thumbs, sizing, target.scrollLeft(), target.scrollTop()); 740 if (options.hoverThumbs) { 741 target.on(events.hover, data, hover); 742 } 743 } 744 } 745 } else { 746 target.css('overflow', 'hidden'); 747 } 748 749 target.data(datakey, data); 750 } 751 752 } 753 754 // Removes any event listeners and other instance-specific 755 // data from the target. It attempts to leave the target 756 // at the state it found it. 757 function teardown(target) { 758 getRemover(target)(); 759 } 760 761 // This is the entry-point for enabling the plug-in; 762 // You can find it's exposure point at the end 763 // of this closure 764 function overscroll(options) { 765 /*jshint validthis:true */ 766 return this.removeOverscroll().each(function() { 767 setup(this, options); 768 }); 769 } 770 771 // This is the entry-point for disabling the plug-in; 772 // You can find it's exposure point at the end 773 // of this closure 774 function removeOverscroll() { 775 /*jshint validthis:true */ 776 return this.each(function () { 777 teardown(this); 778 }); 779 } 780 781 // Extend overscroll to expose settings to the user 782 overscroll.settings = settings; 783 784 // Extend jQuery's prototype to expose the plug-in. 785 // If the supports native overflowScrolling, overscroll will not 786 // attempt to override the browser's built in support 787 $.extend(namespace, { 788 overscroll: overscroll, 789 removeOverscroll: removeOverscroll 790 }); 791 792})(window, document, navigator, Math, setTimeout, clearTimeout, jQuery.fn, jQuery);
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.