1/*! 2 * skrollr core 3 * 4 * Alexander Prinzhorn - https://github.com/Prinzhorn/skrollr 5 * 6 * Free to use under terms of MIT license 7 */ 8(function(window, document, undefined) { 9 'use strict'; 10 11 /* 12 * Global api. 13 */ 14 var skrollr = { 15 get: function() { 16 return _instance; 17 }, 18 //Main entry point. 19 init: function(options) { 20 return _instance || new Skrollr(options); 21 }, 22 VERSION: '0.6.30' 23 }; 24 25 //Minify optimization. 26 var hasProp = Object.prototype.hasOwnProperty; 27 var Math = window.Math; 28 var getStyle = window.getComputedStyle; 29 30 //They will be filled when skrollr gets initialized. 31 var documentElement; 32 var body; 33 34 var EVENT_TOUCHSTART = 'touchstart'; 35 var EVENT_TOUCHMOVE = 'touchmove'; 36 var EVENT_TOUCHCANCEL = 'touchcancel'; 37 var EVENT_TOUCHEND = 'touchend'; 38 39 var SKROLLABLE_CLASS = 'skrollable'; 40 var SKROLLABLE_BEFORE_CLASS = SKROLLABLE_CLASS + '-before'; 41 var SKROLLABLE_BETWEEN_CLASS = SKROLLABLE_CLASS + '-between'; 42 var SKROLLABLE_AFTER_CLASS = SKROLLABLE_CLASS + '-after'; 43 44 var SKROLLR_CLASS = 'skrollr'; 45 var NO_SKROLLR_CLASS = 'no-' + SKROLLR_CLASS; 46 var SKROLLR_DESKTOP_CLASS = SKROLLR_CLASS + '-desktop'; 47 var SKROLLR_MOBILE_CLASS = SKROLLR_CLASS + '-mobile'; 48 49 var DEFAULT_EASING = 'linear'; 50 var DEFAULT_DURATION = 1000;//ms 51 var DEFAULT_MOBILE_DECELERATION = 0.004;//pixel/ms² 52 53 var DEFAULT_SKROLLRBODY = 'skrollr-body'; 54 55 var DEFAULT_SMOOTH_SCROLLING_DURATION = 200;//ms 56 57 var ANCHOR_START = 'start'; 58 var ANCHOR_END = 'end'; 59 var ANCHOR_CENTER = 'center'; 60 var ANCHOR_BOTTOM = 'bottom'; 61 62 //The property which will be added to the DOM element to hold the ID of the skrollable. 63 var SKROLLABLE_ID_DOM_PROPERTY = '___skrollable_id'; 64 65 var rxTouchIgnoreTags = /^(?:input|textarea|button|select)$/i; 66 67 var rxTrim = /^\s+|\s+$/g; 68 69 //Find all data-attributes. data-[_constant]-[offset]-[anchor]-[anchor]. 70 var rxKeyframeAttribute = /^data(?:-(_\w+))?(?:-?(-?\d*\.?\d+p?))?(?:-?(start|end|top|center|bottom))?(?:-?(top|center|bottom))?$/; 71 72 var rxPropValue = /\s*(@?[\w\-\[\]]+)\s*:\s*(.+?)\s*(?:;|$)/gi; 73 74 //Easing function names follow the property in square brackets. 75 var rxPropEasing = /^(@?[a-z\-]+)\[(\w+)\]$/; 76 77 var rxCamelCase = /-([a-z0-9_])/g; 78 var rxCamelCaseFn = function(str, letter) { 79 return letter.toUpperCase(); 80 }; 81 82 //Numeric values with optional sign.
83 var rxNumericValue = /[\-+]?[\d]*\.?[\d]+/g; 84 85 //Used to replace occurences of {?} with a number. 86 var rxInterpolateString = /\{\?\}/g; 87 88 //Finds rgb(a) colors, which don't use the percentage notation. 89 var rxRGBAIntegerColor = /rgba?\(\s*-?\d+\s*,\s*-?\d+\s*,\s*-?\d+/g; 90 91 //Finds all gradients. 92 var rxGradient = /[a-z\-]+-gradient/g; 93 94 //Vendor prefix. Will be set once skrollr gets initialized. 95 var theCSSPrefix = ''; 96 var theDashedCSSPrefix = ''; 97 98 //Will be called once (when skrollr gets initialized). 99 var detectCSSPrefix = function() { 100 //Only relevant prefixes. May be extended. 101 //Could be dangerous if there will ever be a CSS property which actually starts with "ms". Don't hope so. 102 var rxPrefixes = /^(?:O|Moz|webkit|ms)|(?:-(?:o|moz|webkit|ms)-)/; 103 104 //Detect prefix for current browser by finding the first property using a prefix. 105 if(!getStyle) { 106 return; 107 } 108 109 var style = getStyle(body, null); 110 111 for(var k in style) { 112 //We check the key and if the key is a number, we check the value as well, because safari's getComputedStyle returns some weird array-like thingy. 113 theCSSPrefix = (k.match(rxPrefixes) || (+k == k && style[k].match(rxPrefixes))); 114 115 if(theCSSPrefix) { 116 break; 117 } 118 } 119 120 //Did we even detect a prefix? 121 if(!theCSSPrefix) { 122 theCSSPrefix = theDashedCSSPrefix = ''; 123 124 return; 125 } 126 127 theCSSPrefix = theCSSPrefix[0]; 128 129 //We could have detected either a dashed prefix or this camelCaseish-inconsistent stuff. 130 if(theCSSPrefix.slice(0,1) === '-') { 131 theDashedCSSPrefix = theCSSPrefix; 132 133 //There's no logic behind these. Need a look up. 134 theCSSPrefix = ({ 135 '-webkit-': 'webkit', 136 '-moz-': 'Moz',
137 '-ms-': 'ms', 138 '-o-': 'O' 139 })[theCSSPrefix]; 140 } else { 141 theDashedCSSPrefix = '-' + theCSSPrefix.toLowerCase() + '-'; 142 } 143 }; 144 145 var polyfillRAF = function() { 146 var requestAnimFrame = window.requestAnimationFrame || window[theCSSPrefix.toLowerCase() + 'RequestAnimationFrame']; 147 148 var lastTime = _now(); 149 150 if(_isMobile || !requestAnimFrame) { 151 requestAnimFrame = function(callback) { 152 //How long did it take to render? 153 var deltaTime = _now() - lastTime; 154 var delay = Math.max(0, 1000 / 60 - deltaTime); 155 156 return window.setTimeout(function() { 157 lastTime = _now(); 158 callback(); 159 }, delay); 160 }; 161 } 162 163 return requestAnimFrame; 164 }; 165 166 var polyfillCAF = function() { 167 var cancelAnimFrame = window.cancelAnimationFrame || window[theCSSPrefix.toLowerCase() + 'CancelAnimationFrame']; 168 169 if(_isMobile || !cancelAnimFrame) { 170 cancelAnimFrame = function(timeout) { 171 return window.clearTimeout(timeout); 172 }; 173 } 174 175 return cancelAnimFrame; 176 }; 177 178 //Built-in easing functions. 179 var easings = { 180 begin: function() { 181 return 0; 182 }, 183 end: function() { 184 return 1; 185 }, 186 linear: function(p) { 187 return p; 188 }, 189 quadratic: function(p) { 190 return p * p; 191 }, 192 cubic: function(p) { 193 return p * p * p; 194 }, 195 swing: function(p) { 196 return (-Math.cos(p * Math.PI) / 2) + 0.5; 197 }, 198 sqrt: function(p) { 199 return Math.sqrt(p); 200 }, 201 outCubic: function(p) { 202 return (Math.pow((p - 1), 3) + 1); 203 }, 204 //see https://www.desmos.com/calculator/tbr20s8vd2 for how I did this 205 bounce: function(p) { 206 var a; 207 208 if(p <= 0.5083) { 209 a = 3; 210 } else if(p <= 0.8489) { 211 a = 9; 212 } else if(p <= 0.96208) { 213 a = 27; 214 } else if(p <= 0.99981) { 215 a = 91; 216 } else { 217 return 1; 218 } 219 220 return 1 - Math.abs(3 * Math.cos(p * a * 1.028) / a); 221 } 222 }; 223 224 /** 225 * Constructor. 226 */ 227 function Skrollr(options) { 228 documentElement = document.documentElement; 229 body = document.body; 230 231 detectCSSPrefix(); 232 233 _instance = this; 234 235 options = options || {}; 236 237 _constants = options.constants || {}; 238 239 //We allow defining custom easings or overwrite existing. 240 if(options.easing) { 241 for(var e in options.easing) { 242 easings[e] = options.easing[e]; 243 } 244 } 245 246 _edgeStrategy = options.edgeStrategy || 'set'; 247 248 _listeners = { 249 //Function to be called right before rendering. 250 beforerender: options.beforerender, 251 252 //Function to be called right after finishing rendering. 253 render: options.render, 254 255 //Function to be called whenever an element with the `data-emit-events` attribute passes a keyframe. 256 keyframe: options.keyframe 257 }; 258 259 //forceHeight is true by default 260 _forceHeight = options.forceHeight !== false; 261 262 if(_forceHeight) { 263 _scale = options.scale || 1; 264 } 265 266 _mobileDeceleration = options.mobileDeceleration || DEFAULT_MOBILE_DECELERATION; 267 268 _smoothScrollingEnabled = options.smoothScrolling !== false; 269 _smoothScrollingDuration = options.smoothScrollingDuration || DEFAULT_SMOOTH_SCROLLING_DURATION; 270 271 //Dummy object. Will be overwritten in the _render method when smooth scrolling is calculated. 272 _smoothScrolling = { 273 targetTop: _instance.getScrollTop() 274 }; 275 276 //A custom check function may be passed. 277 _isMobile = ((options.mobileCheck || function() { 278 return (/Android|iPhone|iPad|iPod|BlackBerry/i).test(navigator.userAgent || navigator.vendor || window.opera); 279 })()); 280 281 if(_isMobile) { 282 _skrollrBody = document.getElementById(options.skrollrBody || DEFAULT_SKROLLRBODY); 283 284 //Detect 3d transform if there's a skrollr-body (only needed for #skrollr-body). 285 if(_skrollrBody) { 286 _detect3DTransforms(); 287 } 288 289 _initMobile(); 290 _updateClass(documentElement, [SKROLLR_CLASS, SKROLLR_MOBILE_CLASS], [NO_SKROLLR_CLASS]); 291 } else { 292 _updateClass(documentElement, [SKROLLR_CLASS, SKROLLR_DESKTOP_CLASS], [NO_SKROLLR_CLASS]); 293 } 294 295 //Triggers parsing of elements and a first reflow. 296 _instance.refresh(); 297 298 _addEvent(window, 'resize orientationchange', function() { 299 var width = documentElement.clientWidth; 300 var height = documentElement.clientHeight; 301 302 //Only reflow if the size actually changed (#271).
303 if(height !== _lastViewportHeight || width !== _lastViewportWidth) { 304 _lastViewportHeight = height; 305 _lastViewportWidth = width; 306 307 _requestReflow = true; 308 } 309 }); 310 311 var requestAnimFrame = polyfillRAF(); 312 313 //Let's go. 314 (function animloop(){ 315 _render(); 316 _animFrame = requestAnimFrame(animloop); 317 }()); 318 319 return _instance; 320 } 321 322 /** 323 * (Re)parses some or all elements. 324 */ 325 Skrollr.prototype.refresh = function(elements) { 326 var elementIndex; 327 var elementsLength; 328 var ignoreID = false; 329 330 //Completely reparse anything without argument. 331 if(elements === undefined) { 332 //Ignore that some elements may already have a skrollable ID. 333 ignoreID = true; 334 335 _skrollables = []; 336 _skrollableIdCounter = 0; 337 338 elements = document.getElementsByTagName('*'); 339 } else if(elements.length === undefined) { 340 //We also accept a single element as parameter. 341 elements = [elements]; 342 } 343 344 elementIndex = 0; 345 elementsLength = elements.length; 346 347 for(; elementIndex < elementsLength; elementIndex++) { 348 var el = elements[elementIndex]; 349 var anchorTarget = el; 350 var keyFrames = []; 351 352 //If this particular element should be smooth scrolled. 353 var smoothScrollThis = _smoothScrollingEnabled; 354 355 //The edge strategy for this particular element. 356 var edgeStrategy = _edgeStrategy; 357 358 //If this particular element should emit keyframe events. 359 var emitEvents = false; 360 361 //If we're reseting the counter, remove any old element ids that may be hanging around. 362 if(ignoreID && SKROLLABLE_ID_DOM_PROPERTY in el) { 363 delete el[SKROLLABLE_ID_DOM_PROPERTY]; 364 } 365 366 if(!el.attributes) { 367 continue; 368 } 369 370 //Iterate over all attributes and search for key frame attributes. 371 var attributeIndex = 0; 372 var attributesLength = el.attributes.length; 373 374 for (; attributeIndex < attributesLength; attributeIndex++) { 375 var attr = el.attributes[attributeIndex]; 376 377 if(attr.name === 'data-anchor-target') { 378 anchorTarget = document.querySelector(attr.value); 379 380 if(anchorTarget === null) { 381 throw 'Unable to find anchor target "' + attr.value + '"'; 382 } 383 384 continue; 385 } 386 387 //Global smooth scrolling can be overridden by the element attribute. 388 if(attr.name === 'data-smooth-scrolling') { 389 smoothScrollThis = attr.value !== 'off'; 390 391 continue; 392 } 393 394 //Global edge strategy can be overridden by the element attribute. 395 if(attr.name === 'data-edge-strategy') { 396 edgeStrategy = attr.value; 397 398 continue; 399 } 400 401 //Is this element tagged with the `data-emit-events` attribute? 402 if(attr.name === 'data-emit-events') { 403 emitEvents = true; 404 405 continue; 406 } 407 408 var match = attr.name.match(rxKeyframeAttribute); 409 410 if(match === null) { 411 continue; 412 } 413 414 var kf = { 415 props: attr.value, 416 //Point back to the element as well. 417 element: el, 418 //The name of the event which this keyframe will fire, if emitEvents is 419 eventType: attr.name.replace(rxCamelCase, rxCamelCaseFn) 420 }; 421 422 keyFrames.push(kf); 423 424 var constant = match[1]; 425 426 if(constant) { 427 //Strip the underscore prefix. 428 kf.constant = constant.substr(1); 429 } 430 431 //Get the key frame offset. 432 var offset = match[2]; 433 434 //Is it a percentage offset? 435 if(/p$/.test(offset)) { 436 kf.isPercentage = true; 437 kf.offset = (offset.slice(0, -1) | 0) / 100; 438 } else { 439 kf.offset = (offset | 0); 440 } 441 442 var anchor1 = match[3]; 443 444 //If second anchor is not set, the first will be taken for both. 445 var anchor2 = match[4] || anchor1; 446 447 //"absolute" (or "classic") mode, where numbers mean absolute scroll offset. 448 if(!anchor1 || anchor1 === ANCHOR_START || anchor1 === ANCHOR_END) { 449 kf.mode = 'absolute'; 450 451 //data-end needs to be calculated after all key frames are known. 452 if(anchor1 === ANCHOR_END) { 453 kf.isEnd = true; 454 } else if(!kf.isPercentage) {
455 //For data-start we can already set the key frame w/o calculations. 456 //#59: "scale" options should only affect absolute mode. 457 kf.offset = kf.offset * _scale; 458 } 459 } 460 //"relative" mode, where numbers are relative to anchors. 461 else { 462 kf.mode = 'relative'; 463 kf.anchors = [anchor1, anchor2]; 464 } 465 } 466 467 //Does this element have key frames? 468 if(!keyFrames.length) { 469 continue; 470 } 471 472 //Will hold the original style and class attributes before we controlled the element (see #80). 473 var styleAttr, classAttr; 474 475 var id; 476 477 if(!ignoreID && SKROLLABLE_ID_DOM_PROPERTY in el) { 478 //We already have this element under control. Grab the corresponding skrollable id. 479 id = el[SKROLLABLE_ID_DOM_PROPERTY]; 480 styleAttr = _skrollables[id].styleAttr; 481 classAttr = _skrollables[id].classAttr; 482 } else { 483 //It's an unknown element. Asign it a new skrollable id. 484 id = (el[SKROLLABLE_ID_DOM_PROPERTY] = _skrollableIdCounter++); 485 styleAttr = el.style.cssText; 486 classAttr = _getClass(el); 487 } 488 489 _skrollables[id] = { 490 element: el, 491 styleAttr: styleAttr, 492 classAttr: classAttr, 493 anchorTarget: anchorTarget, 494 keyFrames: keyFrames, 495 smoothScrolling: smoothScrollThis, 496 edgeStrategy: edgeStrategy, 497 emitEvents: emitEvents, 498 lastFrameIndex: -1 499 }; 500 501 _updateClass(el, [SKROLLABLE_CLASS], []); 502 } 503 504 //Reflow for the first time. 505 _reflow(); 506 507 //Now that we got all key frame numbers right, actually parse the properties. 508 elementIndex = 0; 509 elementsLength = elements.length; 510 511 for(; elementIndex < elementsLength; elementIndex++) { 512 var sk = _skrollables[elements[elementIndex][SKROLLABLE_ID_DOM_PROPERTY]]; 513 514 if(sk === undefined) { 515 continue; 516 } 517 518 //Parse the property string to objects 519 _parseProps(sk); 520 521 //Fill key frames with missing properties from left and right 522 _fillProps(sk); 523 } 524 525 return _instance; 526 }; 527 528 /** 529 * Transform "relative" mode to "absolute" mode. 530 * That is, calculate anchor position and offset of element. 531 */ 532 Skrollr.prototype.relativeToAbsolute = function(element, viewportAnchor, elementAnchor) { 533 var viewportHeight = documentElement.clientHeight; 534 var box = element.getBoundingClientRect(); 535 var absolute = box.top; 536 537 //#100: IE doesn't supply "height" with getBoundingClientRect. 538 var boxHeight = box.bottom - box.top; 539 540 if(viewportAnchor === ANCHOR_BOTTOM) { 541 absolute -= viewportHeight; 542 } else if(viewportAnchor === ANCHOR_CENTER) { 543 absolute -= viewportHeight / 2; 544 } 545 546 if(elementAnchor === ANCHOR_BOTTOM) { 547 absolute += boxHeight; 548 } else if(elementAnchor === ANCHOR_CENTER) { 549 absolute += boxHeight / 2; 550 } 551 552 //Compensate scrolling since getBoundingClientRect is relative to viewport. 553 absolute += _instance.getScrollTop(); 554 555 return (absolute + 0.5) | 0; 556 }; 557 558 /** 559 * Animates scroll top to new position. 560 */ 561 Skrollr.prototype.animateTo = function(top, options) { 562 options = options || {}; 563 564 var now = _now(); 565 var scrollTop = _instance.getScrollTop(); 566 var duration = options.duration === undefined ? DEFAULT_DURATION : options.duration; 567 568 //Setting this to a new value will automatically cause the current animation to stop, if any. 569 _scrollAnimation = { 570 startTop: scrollTop, 571 topDiff: top - scrollTop, 572 targetTop: top, 573 duration: duration, 574 startTime: now, 575 endTime: now + duration, 576 easing: easings[options.easing || DEFAULT_EASING], 577 done: options.done 578 }; 579 580 //Don't queue the animation if there's nothing to animate. 581 if(!_scrollAnimation.topDiff) { 582 if(_scrollAnimation.done) { 583 _scrollAnimation.done.call(_instance, false); 584 } 585 586 _scrollAnimation = undefined; 587 } 588 589 return _instance; 590 }; 591 592 /** 593 * Stops animateTo animation. 594 */ 595 Skrollr.prototype.stopAnimateTo = function() { 596 if(_scrollAnimation && _scrollAnimation.done) { 597 _scrollAnimation.done.call(_instance, true); 598 } 599 600 _scrollAnimation = undefined; 601 }; 602 603 /** 604 * Returns if an animation caused by animateTo is currently running. 605 */ 606 Skrollr.prototype.isAnimatingTo = function() { 607 return !!_scrollAnimation; 608 }; 609 610 Skrollr.prototype.isMobile = function() { 611 return _isMobile; 612 }; 613 614 Skrollr.prototype.setScrollTop = function(top, force) { 615 _forceRender = (force === true); 616 617 if(_isMobile) { 618 _mobileOffset = Math.min(Math.max(top, 0), _maxKeyFrame); 619 } else { 620 window.scrollTo(0, top); 621 } 622 623 return _instance; 624 }; 625 626 Skrollr.prototype.getScrollTop = function() { 627 if(_isMobile) { 628 return _mobileOffset; 629 } else { 630 return window.pageYOffset || documentElement.scrollTop || body.scrollTop || 0; 631 } 632 }; 633 634 Skrollr.prototype.getMaxScrollTop = function() { 635 return _maxKeyFrame; 636 }; 637 638 Skrollr.prototype.on = function(name, fn) { 639 _listeners[name] = fn; 640 641 return _instance; 642 }; 643 644 Skrollr.prototype.off = function(name) { 645 delete _listeners[name]; 646 647 return _instance; 648 }; 649 650 Skrollr.prototype.destroy = function() { 651 var cancelAnimFrame = polyfillCAF();
652 cancelAnimFrame(_animFrame); 653 _removeAllEvents(); 654 655 _updateClass(documentElement, [NO_SKROLLR_CLASS], [SKROLLR_CLASS, SKROLLR_DESKTOP_CLASS, SKROLLR_MOBILE_CLASS]); 656 657 var skrollableIndex = 0; 658 var skrollablesLength = _skrollables.length; 659 660 for(; skrollableIndex < skrollablesLength; skrollableIndex++) { 661 _reset(_skrollables[skrollableIndex].element); 662 } 663 664 documentElement.style.overflow = body.style.overflow = ''; 665 documentElement.style.height = body.style.height = ''; 666 667 if(_skrollrBody) { 668 skrollr.setStyle(_skrollrBody, 'transform', 'none'); 669 } 670 671 _instance = undefined; 672 _skrollrBody = undefined; 673 _listeners = undefined; 674 _forceHeight = undefined; 675 _maxKeyFrame = 0; 676 _scale = 1; 677 _constants = undefined; 678 _mobileDeceleration = undefined; 679 _direction = 'down'; 680 _lastTop = -1; 681 _lastViewportWidth = 0; 682 _lastViewportHeight = 0; 683 _requestReflow = false; 684 _scrollAnimation = undefined; 685 _smoothScrollingEnabled = undefined; 686 _smoothScrollingDuration = undefined; 687 _smoothScrolling = undefined; 688 _forceRender = undefined; 689 _skrollableIdCounter = 0; 690 _edgeStrategy = undefined; 691 _isMobile = false; 692 _mobileOffset = 0; 693 _translateZ = undefined; 694 }; 695 696 /* 697 Private methods. 698 */ 699 700 var _initMobile = function() { 701 var initialElement; 702 var initialTouchY; 703 var initialTouchX; 704 var currentElement; 705 var currentTouchY; 706 var currentTouchX; 707 var lastTouchY; 708 var deltaY; 709 710 var initialTouchTime; 711 var currentTouchTime; 712 var lastTouchTime; 713 var deltaTime; 714 715 _addEvent(documentElement, [EVENT_TOUCHSTART, EVENT_TOUCHMOVE, EVENT_TOUCHCANCEL, EVENT_TOUCHEND].join(' '), function(e) { 716 var touch = e.changedTouches[0]; 717 718 currentElement = e.target; 719 720 //We don't want text nodes. 721 while(currentElement.nodeType === 3) { 722 currentElement = currentElement.parentNode; 723 } 724 725 currentTouchY = touch.clientY; 726 currentTouchX = touch.clientX; 727 currentTouchTime = e.timeStamp; 728 729 if(!rxTouchIgnoreTags.test(currentElement.tagName)) { 730 e.preventDefault(); 731 } 732 733 switch(e.type) { 734 case EVENT_TOUCHSTART: 735 //The last element we tapped on. 736 if(initialElement) { 737 initialElement.blur(); 738 } 739 740 _instance.stopAnimateTo(); 741 742 initialElement = currentElement; 743 744 initialTouchY = lastTouchY = currentTouchY; 745 initialTouchX = currentTouchX; 746 initialTouchTime = currentTouchTime; 747 748 break; 749 case EVENT_TOUCHMOVE: 750 //Prevent default event on touchIgnore elements in case they don't have focus yet. 751 if(rxTouchIgnoreTags.test(currentElement.tagName) && document.activeElement !== currentElement) { 752 e.preventDefault(); 753 } 754 755 deltaY = currentTouchY - lastTouchY; 756 deltaTime = currentTouchTime - lastTouchTime; 757 758 _instance.setScrollTop(_mobileOffset - deltaY, true); 759 760 lastTouchY = currentTouchY; 761 lastTouchTime = currentTouchTime; 762 break; 763 default: 764 case EVENT_TOUCHCANCEL: 765 case EVENT_TOUCHEND: 766 var distanceY = initialTouchY - currentTouchY; 767 var distanceX = initialTouchX - currentTouchX; 768 var distance2 = distanceX * distanceX + distanceY * distanceY; 769 770 //Check if it was more like a tap (moved less than 7px). 771 if(distance2 < 49) { 772 if(!rxTouchIgnoreTags.test(initialElement.tagName)) { 773 initialElement.focus(); 774 775 //It was a tap, click the element. 776 var clickEvent = document.createEvent('MouseEvents'); 777 clickEvent.initMouseEvent('click', true, true, e.view, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, 0, null); 778 initialElement.dispatchEvent(clickEvent); 779 } 780 781 return; 782 } 783 784 initialElement = undefined; 785 786 var speed = deltaY / deltaTime; 787 788 //Cap speed at 3 pixel/ms. 789 speed = Math.max(Math.min(speed, 3), -3); 790 791 var duration = Math.abs(speed / _mobileDeceleration); 792 var targetOffset = speed * duration + 0.5 * _mobileDeceleration * duration * duration; 793 var targetTop = _instance.getScrollTop() - targetOffset; 794 795 //Relative duration change for when scrolling above bounds. 796 var targetRatio = 0; 797 798 //Change duration proportionally when scrolling would leave bounds. 799 if(targetTop > _maxKeyFrame) { 800 targetRatio = (_maxKeyFrame - targetTop) / targetOffset; 801 802 targetTop = _maxKeyFrame; 803 } else if(targetTop < 0) { 804 targetRatio = -targetTop / targetOffset; 805 806 targetTop = 0; 807 } 808 809 duration = duration * (1 - targetRatio); 810 811 _instance.animateTo((targetTop + 0.5) | 0, {easing: 'outCubic', duration: duration}); 812 break; 813 } 814 }); 815 816 //Just in case there has already been some native scrolling, reset it. 817 window.scrollTo(0, 0); 818 documentElement.style.overflow = body.style.overflow = 'hidden'; 819 }; 820 821 /**
822 * Updates key frames which depend on others / need to be updated on resize. 823 * That is "end" in "absolute" mode and all key frames in "relative" mode. 824 * Also handles constants, because they may change on resize. 825 */ 826 var _updateDependentKeyFrames = function() { 827 var viewportHeight = documentElement.clientHeight; 828 var processedConstants = _processConstants(); 829 var skrollable; 830 var element; 831 var anchorTarget; 832 var keyFrames; 833 var keyFrameIndex; 834 var keyFramesLength; 835 var kf; 836 var skrollableIndex; 837 var skrollablesLength; 838 var offset; 839 var constantValue; 840 841 //First process all relative-mode elements and find the max key frame. 842 skrollableIndex = 0; 843 skrollablesLength = _skrollables.length; 844 845 for(; skrollableIndex < skrollablesLength; skrollableIndex++) { 846 skrollable = _skrollables[skrollableIndex]; 847 element = skrollable.element; 848 anchorTarget = skrollable.anchorTarget; 849 keyFrames = skrollable.keyFrames; 850 851 keyFrameIndex = 0; 852 keyFramesLength = keyFrames.length; 853 854 for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) { 855 kf = keyFrames[keyFrameIndex]; 856 857 offset = kf.offset; 858 constantValue = processedConstants[kf.constant] || 0; 859 860 kf.frame = offset; 861 862 if(kf.isPercentage) { 863 //Convert the offset to percentage of the viewport height. 864 offset = offset * viewportHeight; 865 866 //Absolute + percentage mode. 867 kf.frame = offset; 868 } 869 870 if(kf.mode === 'relative') { 871 _reset(element); 872 873 kf.frame = _instance.relativeToAbsolute(anchorTarget, kf.anchors[0], kf.anchors[1]) - offset; 874 875 _reset(element, true); 876 } 877 878 kf.frame += constantValue; 879 880 //Only search for max key frame when forceHeight is enabled. 881 if(_forceHeight) { 882 //Find the max key frame, but don't use one of the data-end ones for comparison. 883 if(!kf.isEnd && kf.frame > _maxKeyFrame) { 884 _maxKeyFrame = kf.frame; 885 } 886 } 887 } 888 } 889 890 //#133: The document can be larger than the maxKeyFrame we found. 891 _maxKeyFrame = Math.max(_maxKeyFrame, _getDocumentHeight()); 892 893 //Now process all data-end keyframes. 894 skrollableIndex = 0; 895 skrollablesLength = _skrollables.length; 896 897 for(; skrollableIndex < skrollablesLength; skrollableIndex++) { 898 skrollable = _skrollables[skrollableIndex]; 899 keyFrames = skrollable.keyFrames; 900 901 keyFrameIndex = 0; 902 keyFramesLength = keyFrames.length; 903 904 for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) { 905 kf = keyFrames[keyFrameIndex]; 906 907 constantValue = processedConstants[kf.constant] || 0; 908 909 if(kf.isEnd) { 910 kf.frame = _maxKeyFrame - kf.offset + constantValue; 911 } 912 } 913 914 skrollable.keyFrames.sort(_keyFrameComparator); 915 } 916 }; 917 918 /** 919 * Calculates and sets the style properties for the element at the given frame. 920 * @param fakeFrame The frame to render at when smooth scrolling is enabled. 921 * @param actualFrame The actual frame we are at. 922 */ 923 var _calcSteps = function(fakeFrame, actualFrame) { 924 //Iterate over all skrollables. 925 var skrollableIndex = 0; 926 var skrollablesLength = _skrollables.length; 927 928 for(; skrollableIndex < skrollablesLength; skrollableIndex++) { 929 var skrollable = _skrollables[skrollableIndex]; 930 var element = skrollable.element; 931 var frame = skrollable.smoothScrolling ? fakeFrame : actualFrame; 932 var frames = skrollable.keyFrames; 933 var framesLength = frames.length; 934 var firstFrame = frames[0]; 935 var lastFrame = frames[frames.length - 1]; 936 var beforeFirst = frame < firstFrame.frame; 937 var afterLast = frame > lastFrame.frame; 938 var firstOrLastFrame = beforeFirst ? firstFrame : lastFrame; 939 var emitEvents = skrollable.emitEvents; 940 var lastFrameIndex = skrollable.lastFrameIndex; 941 var key; 942 var value; 943 944 //If we are before/after the first/last frame, set the styles according to the given edge strategy. 945 if(beforeFirst || afterLast) { 946 //Check if we already handled this edge case last time. 947 //Note: using setScrollTop it's possible that we jumped from one edge to the other. 948 if(beforeFirst && skrollable.edge === -1 || afterLast && skrollable.edge === 1) { 949 continue; 950 } 951 952 //Add the skrollr-before or -after class. 953 if(beforeFirst) {
954 _updateClass(element, [SKROLLABLE_BEFORE_CLASS], [SKROLLABLE_AFTER_CLASS, SKROLLABLE_BETWEEN_CLASS]); 955 956 //This handles the special case where we exit the first keyframe. 957 if(emitEvents && lastFrameIndex > -1) { 958 _emitEvent(element, firstFrame.eventType, _direction); 959 skrollable.lastFrameIndex = -1; 960 } 961 } else { 962 _updateClass(element, [SKROLLABLE_AFTER_CLASS], [SKROLLABLE_BEFORE_CLASS, SKROLLABLE_BETWEEN_CLASS]); 963 964 //This handles the special case where we exit the last keyframe. 965 if(emitEvents && lastFrameIndex < framesLength) { 966 _emitEvent(element, lastFrame.eventType, _direction); 967 skrollable.lastFrameIndex = framesLength; 968 } 969 } 970 971 //Remember that we handled the edge case (before/after the first/last keyframe). 972 skrollable.edge = beforeFirst ? -1 : 1; 973 974 switch(skrollable.edgeStrategy) { 975 case 'reset': 976 _reset(element); 977 continue; 978 case 'ease': 979 //Handle this case like it would be exactly at first/last keyframe and just pass it on. 980 frame = firstOrLastFrame.frame; 981 break; 982 default: 983 case 'set': 984 var props = firstOrLastFrame.props; 985 986 for(key in props) { 987 if(hasProp.call(props, key)) { 988 value = _interpolateString(props[key].value); 989 990 //Set style or attribute. 991 if(key.indexOf('@') === 0) { 992 element.setAttribute(key.substr(1), value); 993 } else { 994 skrollr.setStyle(element, key, value); 995 } 996 } 997 } 998 999 continue; 1000 } 1001 } else { 1002 //Did we handle an edge last time? 1003 if(skrollable.edge !== 0) { 1004 _updateClass(element, [SKROLLABLE_CLASS, SKROLLABLE_BETWEEN_CLASS], [SKROLLABLE_BEFORE_CLASS, SKROLLABLE_AFTER_CLASS]); 1005 skrollable.edge = 0; 1006 } 1007 } 1008 1009 //Find out between which two key frames we are right now. 1010 var keyFrameIndex = 0; 1011 1012 for(; keyFrameIndex < framesLength - 1; keyFrameIndex++) { 1013 if(frame >= frames[keyFrameIndex].frame && frame <= frames[keyFrameIndex + 1].frame) { 1014 var left = frames[keyFrameIndex]; 1015 var right = frames[keyFrameIndex + 1]; 1016 1017 for(key in left.props) { 1018 if(hasProp.call(left.props, key)) { 1019 var progress = (frame - left.frame) / (right.frame - left.frame); 1020 1021 //Transform the current progress using the given easing function. 1022 progress = left.props[key].easing(progress); 1023 1024 //Interpolate between the two values 1025 value = _calcInterpolation(left.props[key].value, right.props[key].value, progress); 1026 1027 value = _interpolateString(value); 1028 1029 //Set style or attribute. 1030 if(key.indexOf('@') === 0) { 1031 element.setAttribute(key.substr(1), value); 1032 } else { 1033 skrollr.setStyle(element, key, value); 1034 } 1035 } 1036 } 1037 1038 //Are events enabled on this element? 1039 //This code handles the usual cases of scrolling through different keyframes. 1040 //The special cases of before first and after last keyframe are handled above. 1041 if(emitEvents) { 1042 //Did we pass a new keyframe? 1043 if(lastFrameIndex !== keyFrameIndex) { 1044 if(_direction === 'down') { 1045 _emitEvent(element, left.eventType, _direction); 1046 } else { 1047 _emitEvent(element, right.eventType, _direction); 1048 } 1049 1050 skrollable.lastFrameIndex = keyFrameIndex; 1051 } 1052 } 1053 1054 break; 1055 } 1056 } 1057 } 1058 }; 1059 1060 /** 1061 * Renders all elements. 1062 */ 1063 var _render = function() { 1064 if(_requestReflow) { 1065 _requestReflow = false; 1066 _reflow(); 1067 } 1068 1069 //We may render something else than the actual scrollbar position. 1070 var renderTop = _instance.getScrollTop(); 1071 1072 //If there's an animation, which ends in current render call, call the callback after rendering. 1073 var afterAnimationCallback; 1074 var now = _now(); 1075 var progress; 1076 1077 //Before actually rendering handle the scroll animation, if any. 1078 if(_scrollAnimation) { 1079 //It's over 1080 if(now >= _scrollAnimation.endTime) { 1081 renderTop = _scrollAnimation.targetTop; 1082 afterAnimationCallback = _scrollAnimation.done; 1083 _scrollAnimation = undefined; 1084 } else { 1085 //Map the current progress to the new progress using given easing function. 1086 progress = _scrollAnimation.easing((now - _scrollAnimation.startTime) / _scrollAnimation.duration); 1087 1088 renderTop = (_scrollAnimation.startTop + progress * _scrollAnimation.topDiff) | 0; 1089 } 1090 1091 _instance.setScrollTop(renderTop, true); 1092 } 1093 //Smooth scrolling only if there's no animation running and if we're not forcing the rendering. 1094 else if(!_forceRender) { 1095 var smoothScrollingDiff = _smoothScrolling.targetTop - renderTop; 1096 1097 //The user scrolled, start new smooth scrolling. 1098 if(smoothScrollingDiff) { 1099 _smoothScrolling = { 1100 startTop: _lastTop, 1101 topDiff: renderTop - _lastTop, 1102 targetTop: renderTop, 1103 startTime: _lastRenderCall, 1104 endTime: _lastRenderCall + _smoothScrollingDuration 1105 }; 1106 } 1107 1108 //Interpolate the internal scroll position (not the actual scrollbar). 1109 if(now <= _smoothScrolling.endTime) { 1110 //Map the current progress to the new progress using easing function. 1111 progress = easings.sqrt((now - _smoothScrolling.startTime) / _smoothScrollingDuration); 1112 1113 renderTop = (_smoothScrolling.startTop + progress * _smoothScrolling.to
1113pDiff) | 0; 1114 } 1115 } 1116 1117 //Did the scroll position even change? 1118 if(_forceRender || _lastTop !== renderTop) { 1119 //Remember in which direction are we scrolling? 1120 _direction = (renderTop > _lastTop) ? 'down' : (renderTop < _lastTop ? 'up' : _direction); 1121 1122 _forceRender = false; 1123 1124 var listenerParams = { 1125 curTop: renderTop, 1126 lastTop: _lastTop, 1127 maxTop: _maxKeyFrame, 1128 direction: _direction 1129 }; 1130 1131 //Tell the listener we are about to render. 1132 var continueRendering = _listeners.beforerender && _listeners.beforerender.call(_instance, listenerParams); 1133 1134 //The beforerender listener function is able the cancel rendering. 1135 if(continueRendering !== false) { 1136 //Now actually interpolate all the styles. 1137 _calcSteps(renderTop, _instance.getScrollTop()); 1138 1139 //That's were we actually "scroll" on mobile. 1140 if(_isMobile && _skrollrBody) { 1141 //Set the transform ("scroll it"). 1142 skrollr.setStyle(_skrollrBody, 'transform', 'translate(0, ' + -(_mobileOffset) + 'px) ' + _translateZ); 1143 } 1144 1145 //Remember when we last rendered. 1146 _lastTop = renderTop; 1147 1148 if(_listeners.render) { 1149 _listeners.render.call(_instance, listenerParams); 1150 } 1151 } 1152 1153 if(afterAnimationCallback) { 1154 afterAnimationCallback.call(_instance, false); 1155 } 1156 } 1157 1158 _lastRenderCall = now; 1159 }; 1160 1161 /** 1162 * Parses the properties for each key frame of the given skrollable. 1163 */ 1164 var _parseProps = function(skrollable) { 1165 //Iterate over all key frames 1166 var keyFrameIndex = 0; 1167 var keyFramesLength = skrollable.keyFrames.length; 1168 1169 for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) { 1170 var frame = skrollable.keyFrames[keyFrameIndex]; 1171 var easing; 1172 var value; 1173 var prop; 1174 var props = {}; 1175 1176 var match; 1177 1178 while((match = rxPropValue.exec(frame.props)) !== null) { 1179 prop = match[1]; 1180 value = match[2]; 1181 1182 easing = prop.match(rxPropEasing); 1183 1184 //Is there an easing specified for this prop? 1185 if(easing !== null) { 1186 prop = easing[1]; 1187 easing = easing[2]; 1188 } else { 1189 easing = DEFAULT_EASING; 1190 } 1191 1192 //Exclamation point at first position forces the value to be taken literal. 1193 value = value.indexOf('!') ? _parseProp(value) : [value.slice(1)]; 1194 1195 //Save the prop for this key frame with his value and easing function 1196 props[prop] = { 1197 value: value, 1198 easing: easings[easing] 1199 }; 1200 } 1201 1202 frame.props = props; 1203 } 1204 }; 1205 1206 /** 1207 * Parses a value extracting numeric values and generating a format string 1208 * for later interpolation of the new values in old string. 1209 * 1210 * @param val The CSS value to be parsed. 1211 * @return Something like ["rgba(?%,?%, ?%,?)", 100, 50, 0, .7] 1212 * where the first element is the format string later used 1213 * and all following elements are the numeric value. 1214 */ 1215 var _parseProp = function(val) { 1216 var numbers = []; 1217 1218 //One special case, where floats don't work. 1219 //We replace all occurences of rgba colors 1220 //which don't use percentage notation with the percentage notation. 1221 rxRGBAIntegerColor.lastIndex = 0; 1222 val = val.replace(rxRGBAIntegerColor, function(rgba) { 1223 return rgba.replace(rxNumericValue, function(n) { 1224 return n / 255 * 100 + '%'; 1225 }); 1226 }); 1227 1228 //Handle prefixing of "gradient" values. 1229 //For now only the prefixed value will be set. Unprefixed isn't supported anyway. 1230 if(theDashedCSSPrefix) { 1231 rxGradient.lastIndex = 0; 1232 val = val.replace(rxGradient, function(s) { 1233 return theDashedCSSPrefix + s; 1234 }); 1235 } 1236 1237 //Now parse ANY number inside this string and create a format string. 1238 val = val.replace(rxNumericValue, function(n) { 1239 numbers.push(+n); 1240 return '{?}'; 1241 }); 1242 1243 //Add the formatstring as first value. 1244 numbers.unshift(val); 1245 1246 return numbers; 1247 }; 1248 1249 /** 1250 * Fills the key frames with missing left and right hand properties. 1251 * If key frame 1 has property X and key frame 2 is missing X, 1252 * but key frame 3 has X again, then we need to assign X to key frame 2 too. 1253 * 1254 * @param sk A skrollable. 1255 */ 1256 var _fillProps = function(sk) { 1257 //Will collect the properties key frame by key frame 1258 var propList = {}; 1259 var keyFrameIndex; 1260 var keyFramesLength; 1261 1262 //Iterate over all key frames from left to right 1263 keyFrameIndex = 0; 1264 keyFramesLength = sk.keyFrames.length; 1265 1266 for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) { 1267 _fillPropForFrame(sk.keyFrames[keyFrameIndex], propList); 1268 } 1269 1270 //Now do the same from right to fill the last gaps 1271 1272 propList = {}; 1273 1274 //Iterate over all key frames from right to left 1275 keyFrameIndex = sk.keyFrames.length - 1; 1276 1277 for(; keyFrameIndex >= 0; keyFrameIndex--) { 1278 _fillPropForFrame(sk.keyFrames[keyFrameIndex], propList); 1279 } 1280 }; 1281 1282 var _fillPropForFrame = function(frame, propList) { 1283 var key; 1284 1285 //For each key frame iterate over all right hand properties and assign them, 1286 //but only if the current key frame doesn't have the property by itself 1287 for(key in propList) { 1288 //The current frame misses this property, so assign it. 1289 if(!hasProp.call(frame.props, key)) { 1290 frame.props[key] = propList[key]; 1291 } 1292 } 1293 1294 //Iterate over all props of the current frame and collect them 1295 for(key in frame.props) { 1296 propList[key] = frame.props[key]; 1297 } 1298 }; 1299 1300 /** 1301 * Calculates the new values for two given values array. 1302 */ 1303 var _calcInterpolation = function(val1, val2, progress) { 1304 var valueIndex; 1305 var val1Length = val1.length; 1306 1307 //They both need to have the same length 1308 if(val1Length !== val2.length) { 1309 throw 'Can\'t interpolate between "' + val1[0] + '" and "' + val2[0] + '"'; 1310 } 1311
1312 //Add the format string as first element. 1313 var interpolated = [val1[0]]; 1314 1315 valueIndex = 1; 1316 1317 for(; valueIndex < val1Length; valueIndex++) { 1318 //That's the line where the two numbers are actually interpolated. 1319 interpolated[valueIndex] = val1[valueIndex] + ((val2[valueIndex] - val1[valueIndex]) * progress); 1320 } 1321 1322 return interpolated; 1323 }; 1324 1325 /** 1326 * Interpolates the numeric values into the format string. 1327 */ 1328 var _interpolateString = function(val) { 1329 var valueIndex = 1; 1330 1331 rxInterpolateString.lastIndex = 0; 1332 1333 return val[0].replace(rxInterpolateString, function() { 1334 return val[valueIndex++]; 1335 }); 1336 }; 1337 1338 /** 1339 * Resets the class and style attribute to what it was before skrollr manipulated the element. 1340 * Also remembers the values it had before reseting, in order to undo the reset. 1341 */ 1342 var _reset = function(elements, undo) { 1343 //We accept a single element or an array of elements. 1344 elements = [].concat(elements); 1345 1346 var skrollable; 1347 var element; 1348 var elementsIndex = 0; 1349 var elementsLength = elements.length; 1350 1351 for(; elementsIndex < elementsLength; elementsIndex++) { 1352 element = elements[elementsIndex]; 1353 skrollable = _skrollables[element[SKROLLABLE_ID_DOM_PROPERTY]]; 1354 1355 //Couldn't find the skrollable for this DOM element. 1356 if(!skrollable) { 1357 continue; 1358 } 1359 1360 if(undo) { 1361 //Reset class and style to the "dirty" (set by skrollr) values. 1362 element.style.cssText = skrollable.dirtyStyleAttr; 1363 _updateClass(element, skrollable.dirtyClassAttr); 1364 } else { 1365 //Remember the "dirty" (set by skrollr) class and style. 1366 skrollable.dirtyStyleAttr = element.style.cssText; 1367 skrollable.dirtyClassAttr = _getClass(element); 1368 1369 //Reset class and style to what it originally was. 1370 element.style.cssText = skrollable.styleAttr; 1371 _updateClass(element, skrollable.classAttr); 1372 } 1373 } 1374 }; 1375 1376 /** 1377 * Detects support for 3d transforms by applying it to the skrollr-body. 1378 */ 1379 var _detect3DTransforms = function() { 1380 _translateZ = 'translateZ(0)'; 1381 skrollr.setStyle(_skrollrBody, 'transform', _translateZ); 1382 1383 var computedStyle = getStyle(_skrollrBody); 1384 var computedTransform = computedStyle.getPropertyValue('transform'); 1385 var computedTransformWithPrefix = computedStyle.getPropertyValue(theDashedCSSPrefix + 'transform'); 1386 var has3D = (computedTransform && computedTransform !== 'none') || (computedTransformWithPrefix && computedTransformWithPrefix !== 'none'); 1387 1388 if(!has3D) { 1389 _translateZ = ''; 1390 } 1391 }; 1392 1393 /** 1394 * Set the CSS property on the given element. Sets prefixed properties as well. 1395 */ 1396 skrollr.setStyle = function(el, prop, val) { 1397 var style = el.style; 1398 1399 //Camel case. 1400 prop = prop.replace(rxCamelCase, rxCamelCaseFn).replace('-', ''); 1401 1402 //Make sure z-index gets a <integer>. 1403 //This is the only <integer> case we need to handle. 1404 if(prop === 'zIndex') { 1405 if(isNaN(val)) { 1406 //If it's not a number, don't touch it. 1407 //It could for example be "auto" (#351). 1408 style[prop] = val; 1409 } else { 1410 //Floor the number. 1411 style[prop] = '' + (val | 0); 1412 } 1413 } 1414 //#64: "float" can't be set across browsers. Needs to use "cssFloat" for all except IE. 1415 else if(prop === 'float') { 1416 style.styleFloat = style.cssFloat = val; 1417 } 1418 else { 1419 //Need try-catch for old IE. 1420 try { 1421 //Set prefixed property if there's a prefix. 1422 if(theCSSPrefix) { 1423 style[theCSSPrefix + prop.slice(0,1).toUpperCase() + prop.slice(1)] = val; 1424 } 1425 1426 //Set unprefixed. 1427 style[prop] = val; 1428 } catch(ignore) {} 1429 } 1430 }; 1431 1432 /** 1433 * Cross browser event handling. 1434 */ 1435 var _addEvent = skrollr.addEvent = function(element, names, callback) { 1436 var intermediate = function(e) { 1437 //Normalize IE event stuff. 1438 e = e || window.event; 1439 1440 if(!e.target) { 1441 e.target = e.srcElement; 1442 } 1443 1444 if(!e.preventDefault) { 1445 e.preventDefault = function() { 1446 e.returnValue = false; 1447 e.defaultPrevented = true; 1448 }; 1449 } 1450 1451 return callback.call(this, e); 1452 }; 1453 1454 names = names.split(' '); 1455 1456 var name; 1457 var nameCounter = 0; 1458 var namesLength = names.length; 1459 1460 for(; nameCounter < namesLength; nameCounter++) { 1461 name = names[nameCounter]; 1462 1463 if(element.addEventListener) { 1464 element.addEventListener(name, callback, false); 1465 } else { 1466 element.attachEvent('on' + name, intermediate); 1467 } 1468 1469 //Remember the events to be able to flush them later. 1470 _registeredEvents.push({ 1471 element: element, 1472 name: name, 1473 listener: callback 1474 }); 1475 } 1476 }; 1477 1478 var _removeEvent = skrollr.removeEvent = function(element, names, callback) { 1479 names = names.split(' '); 1480 1481 var nameCounter = 0; 1482 var namesLength = names.length; 1483 1484 for(; nameCounter < namesLength; nameCounter++) { 1485 if(element.removeEventListener) { 1486 element.removeEventListener(names[nameCounter], callback, false); 1487 } else { 1488 element.detachEvent('on' + names[nameCounter], callback); 1489 } 1490 } 1491 }; 1492 1493 var _removeAllEvents = function() { 1494 var eventData; 1495 var eventCounter = 0; 1496 var eventsLength = _registeredEvents.length; 1497 1498 for(; eventCounter < eventsLength; eventCounter++) { 1499 eventData = _registeredEvents[eventCounter]; 1500 1501 _removeEvent(eventData.element, eventData.name, eventData.listener); 1502 } 1503 1504 _registeredEvents = []; 1505 }; 1506 1507 var _emitEvent = function(element, name, direction) { 1508 if(_listeners.keyframe) { 1509 _listeners.keyframe.call(_instance, element, name, direction); 1510 } 1511 }; 1512 1513 var _reflow = function() { 1514 var pos = _instance.getScrollTop(); 1515 1516 //Will be recalculated by _updateDependentKeyFrames. 1517 _maxKeyFrame = 0; 1518 1519 if(_forceHeight && !_isMobile) { 1520 //un-"force" the height to not mess with the calculations in _updateDependentKeyFrames (#216). 1521 body.style.height = ''; 1522 } 1523 1524 _updateDependentKeyFrames(); 1525 1526 if(_forceHeight && !_isMobile) { 1527 //"force" the height. 1528 body.style.height = (_maxKeyFrame + documentElement.clientHeight) + 'px'; 1529 } 1530 1531 //The scroll offset may now be larger than needed (on desktop the browser/os prevents scrolling farther than the bottom). 1532 if(_isMobile) { 1533 _instance.setScrollTop(Math.min(_instance.getScrollTop(), _maxKeyFrame)); 1534 } else { 1535 //Remember and reset the scroll pos (#217). 1536 _instance.setScrollTop(pos, true); 1537 } 1538 1539 _forceRender = true; 1540 }; 1541 1542 /* 1543 * Returns a copy of the constants object where all functions and strings have been evaluated. 1544 */ 1545 var _processConstants = function() {
1546 var viewportHeight = documentElement.clientHeight; 1547 var copy = {}; 1548 var prop; 1549 var value; 1550 1551 for(prop in _constants) { 1552 value = _constants[prop]; 1553 1554 if(typeof value === 'function') { 1555 value = value.call(_instance); 1556 } 1557 //Percentage offset. 1558 else if((/p$/).test(value)) { 1559 value = (value.slice(0, -1) / 100) * viewportHeight; 1560 } 1561 1562 copy[prop] = value; 1563 } 1564 1565 return copy; 1566 }; 1567 1568 /* 1569 * Returns the height of the document. 1570 */ 1571 var _getDocumentHeight = function() { 1572 var skrollrBodyHeight = 0; 1573 var bodyHeight; 1574 1575 if(_skrollrBody) { 1576 skrollrBodyHeight = Math.max(_skrollrBody.offsetHeight, _skrollrBody.scrollHeight); 1577 } 1578 1579 bodyHeight = Math.max(skrollrBodyHeight, body.scrollHeight, body.offsetHeight, documentElement.scrollHeight, documentElement.offsetHeight, documentElement.clientHeight); 1580 1581 return bodyHeight - documentElement.clientHeight; 1582 }; 1583 1584 /** 1585 * Returns a string of space separated classnames for the current element. 1586 * Works with SVG as well. 1587 */ 1588 var _getClass = function(element) { 1589 var prop = 'className'; 1590 1591 //SVG support by using className.baseVal instead of just className. 1592 if(window.SVGElement && element instanceof window.SVGElement) { 1593 element = element[prop]; 1594 prop = 'baseVal'; 1595 } 1596 1597 return element[prop]; 1598 }; 1599 1600 /** 1601 * Adds and removes a CSS classes. 1602 * Works with SVG as well.
1603 * add and remove are arrays of strings, 1604 * or if remove is ommited add is a string and overwrites all classes. 1605 */ 1606 var _updateClass = function(element, add, remove) { 1607 var prop = 'className'; 1608 1609 //SVG support by using className.baseVal instead of just className. 1610 if(window.SVGElement && element instanceof window.SVGElement) { 1611 element = element[prop]; 1612 prop = 'baseVal'; 1613 } 1614 1615 //When remove is ommited, we want to overwrite/set the classes. 1616 if(remove === undefined) { 1617 element[prop] = add; 1618 return; 1619 } 1620 1621 //Cache current classes. We will work on a string before passing back to DOM. 1622 var val = element[prop]; 1623 1624 //All classes to be removed. 1625 var classRemoveIndex = 0; 1626 var removeLength = remove.length; 1627 1628 for(; classRemoveIndex < removeLength; classRemoveIndex++) { 1629 val = _untrim(val).replace(_untrim(remove[classRemoveIndex]), ' '); 1630 } 1631 1632 val = _trim(val); 1633 1634 //All classes to be added. 1635 var classAddIndex = 0; 1636 var addLength = add.length; 1637 1638 for(; classAddIndex < addLength; classAddIndex++) { 1639 //Only add if el not already has class. 1640 if(_untrim(val).indexOf(_untrim(add[classAddIndex])) === -1) { 1641 val += ' ' + add[classAddIndex]; 1642 } 1643 } 1644 1645 element[prop] = _trim(val); 1646 }; 1647 1648 var _trim = function(a) { 1649 return a.replace(rxTrim, ''); 1650 }; 1651 1652 /** 1653 * Adds a space before and after the string. 1654 */ 1655 var _untrim = function(a) { 1656 return ' ' + a + ' '; 1657 }; 1658 1659 var _now = Date.now || function() { 1660 return +new Date(); 1661 }; 1662 1663 var _keyFrameComparator = function(a, b) { 1664 return a.frame - b.frame; 1665 }; 1666 1667 /* 1668 * Private variables. 1669 */ 1670 1671 //Singleton 1672 var _instance; 1673 1674 /* 1675 A list of all elements which should be animated associated with their the metadata. 1676 Exmaple skrollable with two key frames animating from 100px width to 20px: 1677 1678 skrollable = { 1679 element: <the DOM element>, 1680 styleAttr: <style attribute of the element before skrollr>, 1681 classAttr: <class attribute of the element before skrollr>, 1682 keyFrames: [ 1683 { 1684 frame: 100, 1685 props: { 1686 width: { 1687 value: ['{?}px', 100], 1688 easing: <reference to easing function> 1689 } 1690 }, 1691 mode: "absolute" 1692 }, 1693 { 1694 frame: 200, 1695 props: { 1696 width: { 1697 value: ['{?}px', 20], 1698 easing: <reference to easing function> 1699 } 1700 }, 1701 mode: "absolute" 1702 } 1703 ] 1704 }; 1705 */ 1706 var _skrollables; 1707 1708 var _skrollrBody; 1709 1710 var _listeners; 1711 var _forceHeight; 1712 var _maxKeyFrame = 0; 1713 1714 var _scale = 1; 1715 var _constants; 1716 1717 var _mobileDeceleration; 1718 1719 //Current direction (up/down). 1720 var _direction = 'down'; 1721 1722 //The last top offset value. Needed to determine direction. 1723 var _lastTop = -1; 1724 1725 //The last time we called the render method (doesn't mean we rendered!). 1726 var _lastRenderCall = _now(); 1727 1728 //For detecting if it actually resized (#271). 1729 var _lastViewportWidth = 0; 1730 var _lastViewportHeight = 0; 1731 1732 var _requestReflow = false; 1733 1734 //Will contain data about a running scrollbar animation, if any. 1735 var _scrollAnimation; 1736 1737 var _smoothScrollingEnabled; 1738 1739 var _smoothScrollingDuration; 1740 1741 //Will contain settins for smooth scrolling if enabled. 1742 var _smoothScrolling; 1743 1744 //Can be set by any operation/event to force rendering even if the scrollbar didn't move. 1745 var _forceRender; 1746 1747 //Each skrollable gets an unique ID incremented for each skrollable. 1748 //The ID is the index in the _skrollables array. 1749 var _skrollableIdCounter = 0; 1750 1751 var _edgeStrategy; 1752 1753 1754 //Mobile specific vars. Will be stripped by UglifyJS when not in use. 1755 var _isMobile = false; 1756 1757 //The virtual scroll offset when using mobile scrolling. 1758 var _mobileOffset = 0; 1759 1760 //If the browser supports 3d transforms, this will be filled with 'translateZ(0)' (empty string otherwise). 1761 var _translateZ; 1762 1763 //Will contain data about registered events by skrollr. 1764 var _registeredEvents = []; 1765 1766 //Animation frame id returned by RequestAnimationFrame (or timeout when RAF is not supported). 1767 var _animFrame; 1768 1769 //Expose skrollr as either a global variable or a require.js module. 1770 if(typeof define === 'function' && define.amd) { 1771 define([], function () { 1772 return skrollr; 1773 }); 1774 } else if (typeof module !== 'undefined' && module.exports) { 1775 module.exports = skrollr; 1776 } else { 1777 window.skrollr = skrollr; 1778 } 1779 1780}(window, document));
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.