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