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