1/*! skrollr v0.3.11 https://github.com/Prinzhorn/skrollr | free to use under terms of MIT license */ 2(function(window, document, undefined) { 3 'use strict'; 4 5 //Used as a dummy function for event listeners. 6 var NOOP = function() {}; 7 8 //Minify optimization. 9 var hasProp = Object.prototype.hasOwnProperty; 10 var documentElement = document.documentElement; 11 var body = document.body; 12 13 var HIDDEN_CLASS = 'hidden'; 14 var SKROLLABLE_CLASS = 'skrollable'; 15 var DEFAULT_EASING = 'linear'; 16 var DEFAULT_DURATION = 1000; 17 18 var requestAnimFrame = 19 window.requestAnimationFrame || 20 window.webkitRequestAnimationFrame || 21 window.mozRequestAnimationFrame || 22 window.oRequestAnimationFrame || 23 window.msRequestAnimationFrame || 24 function(fn) { 25 //Just 30 fps, because that's enough for those legacy browsers 26 window.setTimeout(fn, 1000 / 30); 27 }; 28 29 var rxTrim = /^\s*(.*)\s$/; 30 31 //Find data-<number>, as well as data-end and data-end-<number> 32 var rxKeyframeAttribute = /^data(-end)?-?(\d+)?$/; 33 34 var rxPropSplit = /:|;/g; 35 36 //Easing function names follow the property in square brackets. 37 var rxPropEasing = /^([a-z\-]+)\[(\w+)\]$/; 38 39 var rxCamelCase = /-([a-z])/g; 40 41 //Numeric values with optional sign. 42 var rxNumericValue = /(:?\+|-)?[\d.]+/g; 43 44 //Finds rgb(a) colors, which don't use the percentage notation. 45 var rxRGBAIntegerColor = /rgba?\(\s*-?\d+\s*,\s*-?\d+\s*,\s*-?\d+/g; 46 47 //Finds all gradients. 48 var rxGradient = /[a-z\-]+-gradient/g; 49 50 //Only relevant prefixes. May be extended. 51 //Could be dangerous if there will ever be a CSS property which actually starts with "ms". Don't hope so. 52 var rxPrefixes = /^O|Moz|webkit|ms/; 53 54 var theCSSPrefix; 55 var theDashedCSSPrefix; 56 57 //Detect prefix for current browser by finding the first property using a prefix. 58 if(window.getComputedStyle) { 59 var style = window.getComputedStyle(body, null); 60 61 for(var k in style) { 62 //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. 63 theCSSPrefix = (k.match(rxPrefixes) || (+k == k && style[k].match(rxPrefixes))); 64 65 if(theCSSPrefix) { 66 break; 67 } 68 } 69 } 70 71 //Empty string if no prefix detected 72 theCSSPrefix = (theCSSPrefix || [''])[0]; 73 74 //Will be "--" if no prefix detected. No problem, browser will ignore "--transform" and stuff. 75 theDashedCSSPrefix = '-' + theCSSPrefix.toLowerCase() + '-'; 76 77 //Cleanup. 78 rxPrefixes = undefined; 79 80 //Built-in easing functions. 81 var easings = { 82 begin: function() { 83 return 0; 84 }, 85 end: function() { 86 return 1; 87 }, 88 linear: function(p) { 89 return p; 90 }, 91 quadratic: function(p) { 92 return p * p; 93 }, 94 cubic: function(p) { 95 return p * p * p; 96 }, 97 swing: function(p) { 98 return (-Math.cos(p * Math.PI) / 2) + 0.5; 99 }, 100 //see https://www.desmos.com/calculator/tbr20s8vd2 for how I did this 101 bounce: function(p) { 102 var a; 103 104 switch(true) { 105 case (p <= 0.5083): 106 a = 3; break; 107 case (p <= 0.8489): 108 a = 9; break; 109 case (p <= 0.96208): 110 a = 27; break; 111 case (p <= 0.99981): 112 a = 91; break; 113 default: 114 return 1; 115 } 116 117 return 1 - Math.abs(3 * Math.cos(p * a * 1.028) / a); 118 } 119 }; 120 121 /** 122 * Constructor. 123 */ 124 function Skrollr(options) { 125 _instance = this; 126 127 options = options || {}; 128 129 //We allow defining custom easings or overwrite existing 130 if(options.easing) { 131 for(var e in options.easing) { 132 easings[e] = options.easing[e]; 133 } 134 } 135 136 _listeners = { 137 //Function to be called right before rendering. 138 beforerender: options.beforerender || NOOP, 139 140 //Function to be called right after finishing rendering. 141 render: options.render || NOOP 142 }; 143 144 //true is default, thus undefined is true as well. 145 _forceHeight = options.forceHeight !== false; 146 147 if(_forceHeight) { 148 _scale = options.scale || 1; 149 } 150 151 var allElements = document.getElementsByTagName('*'); 152 153 //Iterate over all elements in document. 154 for(var i = 0; i < allElements.length; i++) { 155 var el = allElements[i]; 156 var keyFrames = []; 157 158 if(!el.attributes) { 159 continue; 160 } 161 162 //Iterate over all attributes and search for key frame attributes. 163 for (var k = 0; k < el.attributes.length; k++) { 164 var attr = el.attributes[k]; 165 var match = attr.name.match(rxKeyframeAttribute); 166 167 if(match !== null) { 168 var frame = (match[2] | 0) * _scale; 169 170 var kf = { 171 frame: frame, 172 props: attr.value 173 }; 174 175 keyFrames.push(kf); 176 177 //special handling for data-end. 178 if(match[1] === '-end') { 179 kf.dataEnd = kf.frame; 180 _dataEndKeyFrames.push(kf); 181 } 182 183 if(frame > _maxKeyFrame) { 184 _maxKeyFrame = frame; 185 } 186 } 187 } 188 189 //Does this element have key frames? 190 if(keyFrames.length) { 191 _skrollables.push({ 192 element: el, 193 keyFrames: keyFrames 194 }); 195 196 _addClass(el, SKROLLABLE_CLASS); 197 } 198 } 199 200 var updateDataEnd = function() { 201 //Set all data-end key frames to max key frame 202 for(var i = 0; i < _dataEndKeyFrames.length; i++) { 203 var kf = _dataEndKeyFrames[i]; 204 kf.frame = _maxKeyFrame - kf.dataEnd; 205 } 206 }; 207 208 var onResize; 209 210 if(_forceHeight) { 211 //Add a dummy element in order to get a large enough scrollbar. 212 var dummy = document.createElement('div'); 213 var dummyStyle = dummy.style; 214 215 dummyStyle.width = '1px'; 216 dummyStyle.position = 'absolute'; 217 dummyStyle.right = dummyStyle.top = dummyStyle.zIndex = '0'; 218 219 body.appendChild(dummy); 220 221 //Update height of dummy div when window size is changed. 222 onResize = function() { 223 dummyStyle.height = (_maxKeyFrame + documentElement.clientHeight) + 'px'; 224 updateDataEnd(); 225 }; 226 } else { 227 onResize = function() { 228 _maxKeyFrame = body.scrollHeight - documentElement.clientHeight; 229 updateDataEnd(); 230 _forceRender = true; 231 }; 232 } 233 234 _addEvent('resize', onResize); 235 onResize(); 236 237 //Now that we got all key frame numbers right, actually parse the properties. 238 for(var i = 0; i < _skrollables.length; i++) { 239 var sk = _skrollables[i]; 240 241 //Make sure they are in order 242 sk.keyFrames.sort(_keyFrameComparator); 243 244 //Parse the property string to objects 245 _parseProps(sk); 246 247 //Fill key frames with missing properties from left and right 248 _fillProps(sk); 249 } 250 251 //Clean up 252 allElements = options = undefined; 253 254 //Let's go 255 _render(); 256 257 return _instance; 258 } 259 260 /** 261 * Animates scroll top to new position. 262 */ 263 Skrollr.prototype.animateTo = function(top, options) { 264 options = options || {}; 265 266 var now = _now(); 267 268 //Setting this to a new value will automatically prevent the current animation to stop, if any. 269 _scrollAnimation = { 270 startTop: _instance.getScrollTop(), 271 topDiff: top - _instance.getScrollTop(),
272 targetTop: top, 273 duration: options.duration || DEFAULT_DURATION, 274 startTime: now, 275 endTime: now + (options.duration || DEFAULT_DURATION), 276 easing: easings[options.easing || DEFAULT_EASING], 277 done: options.done || NOOP 278 }; 279 280 //Don't queue the animation if there's nothing to animate. 281 if(!_scrollAnimation.topDiff) { 282 _scrollAnimation.done.call(_instance); 283 _scrollAnimation = undefined; 284 } 285 }; 286 287 Skrollr.prototype.setScrollTop = function(top) { 288 window.scroll(0, top); 289 }; 290 291 Skrollr.prototype.getScrollTop = function(top) { 292 return window.pageYOffset || documentElement.scrollTop || body.scrollTop || 0; 293 }; 294 295 Skrollr.prototype.on = function(name, fn) { 296 _listeners[name] = fn || NOOP; 297 }; 298 299 Skrollr.prototype.off = function(name) { 300 _listeners[name] = NOOP; 301 }; 302 303 /* 304 Private methods. 305 */ 306 307 /** 308 * Calculate and sets the style properties for the element at the given frame 309 */ 310 var _calcSteps = function(skrollable, frame) { 311 var frames = skrollable.keyFrames; 312 313 //We are before the first frame, don't do anything 314 if(frame < frames[0].frame) { 315 _addClass(skrollable.element, HIDDEN_CLASS); 316 } 317 //We are after or at the last frame, the element gets all props from last key frame 318 else if(frame >= frames[frames.length - 1].frame) { 319 _removeClass(skrollable.element, HIDDEN_CLASS); 320 321 var last = frames[frames.length - 1]; 322 var value; 323 324 for(var key in last.props) { 325 if(hasProp.call(last.props, key)) { 326 value = _interpolateString(last.props[key].value); 327 328 _setStyle(skrollable.element, key, value); 329 } 330 } 331 } 332 //We are between two frames 333 else { 334 _removeClass(skrollable.element, HIDDEN_CLASS); 335 336 //Find out between which two key frames we are right now 337 for(var i = 0; i < frames.length - 1; i++) { 338 if(frame >= frames[i].frame && frame <= frames[i + 1].frame) { 339 var left = frames[i]; 340 var right = frames[i + 1]; 341 342 for(var key in left.props) { 343 if(hasProp.call(left.props, key)) { 344 var progress = (frame - left.frame) / (right.frame - left.frame); 345 346 //Transform the current progress using the given easing function. 347 progress = left.props[key].easing(progress); 348 349 //Interpolate between the two values 350 var value = _calcInterpolation(left.props[key].value, right.props[key].value, progress); 351 352 value = _interpolateString(value); 353 354 _setStyle(skrollable.element, key, value); 355 } 356 } 357 358 break; 359 } 360 } 361 } 362 }; 363 364 /** 365 * Renders all elements 366 */ 367 var _render = function() { 368 //If there's an animation, which ends in current render call, call the callback after rendering; 369 var afterAnimationCallback = NOOP; 370 371 //Before actually rendering handle the scroll animation, if any. 372 if(_scrollAnimation) { 373 var now = _now(); 374 375 //It's over 376 if(now >= _scrollAnimation.endTime) { 377 _instance.setScrollTop(_scrollAnimation.targetTop); 378 afterAnimationCallback = _scrollAnimation.done; 379 _scrollAnimation = undefined; 380 } else { 381 //Map the current progress to the new progress using given easing function. 382 var progress = _scrollAnimation.easing((now - _scrollAnimation.startTime) / _scrollAnimation.duration); 383 384 _instance.setScrollTop((_scrollAnimation.startTop + progress * _scrollAnimation.topDiff) | 0); 385 } 386 } 387 388 _curTop = _instance.getScrollTop(); 389 390 //In OSX it's possible to have a negative scrolltop, so, we set it to zero. 391 if(_curTop < 0) { 392 _curTop = 0; 393 } 394 395 //Did the scroll position even change? 396 if(_forceRender || _lastTop !== _curTop) { 397 //Remember in which direction are we scrolling? 398 _direction = (_curTop >= _lastTop) ? 'down' : 'up'; 399 400 _forceRender = false; 401 402 var listenerParams = { 403 curTop: _curTop, 404 lastTop: _lastTop, 405 maxTop: _maxKeyFrame, 406 direction: _direction 407 }; 408 409 //Tell the listener we are about to render. 410 var continueRendering = _listeners.beforerender.call(_instance, listenerParams); 411 412 //The beforerender listener function is able the cancel rendering. 413 if(continueRendering !== false) { 414 for(var i = 0; i < _skrollables.length; i++) { 415 _calcSteps(_skrollables[i], _curTop); 416 } 417 418 //Remember when we last rendered.
419 _lastTop = _curTop; 420 421 _listeners.render.call(_instance, listenerParams); 422 } 423 424 afterAnimationCallback.call(_instance); 425 } 426 427 requestAnimFrame(function() { 428 _render(); 429 }); 430 }; 431 432 /** 433 * Parses the properties for each key frame of the given skrollable. 434 */ 435 var _parseProps = function(skrollable) { 436 //Iterate over all key frames 437 for(var i = 0; i < skrollable.keyFrames.length; i++) { 438 var frame = skrollable.keyFrames[i]; 439 440 //Get all properties and values in an array 441 var allProps = frame.props.split(rxPropSplit); 442 443 var prop; 444 var value; 445 var easing; 446 447 frame.props = {}; 448 449 //Iterate over all props and values (+2 because [prop,value,prop,value,...]) 450 for(var k = 0; k < allProps.length - 1; k += 2) { 451 prop = allProps[k]; 452 value = allProps[k + 1]; 453 easing = prop.match(rxPropEasing); 454 455 //Is there an easing specified for this prop? 456 if(easing !== null) { 457 prop = easing[1]; 458 easing = easing[2]; 459 } else { 460 easing = DEFAULT_EASING; 461 462 } 463 464 //Exclamation point at first position forces the value to be taken literal. 465 value = value.indexOf('!') ? _parseProp(value) : [value.slice(1)]; 466 467 //Save the prop for this key frame with his value and easing function 468 frame.props[prop] = { 469 value: value, 470 easing: easings[easing] 471 }; 472 } 473 } 474 }; 475 476 /** 477 * Parses a value extracting numeric values and generating a format string 478 * for later interpolation of the new values in old string. 479 * 480 * @param val The CSS value to be parsed. 481 * @return Something like ["rgba(?%,?%, ?%,?)", 100, 50, 0, .7] 482 * where the first element is the format string later used 483 * and all following elements are the numeric value. 484 */ 485 var _parseProp = function(val) { 486 var numbers = []; 487 488 //One special case, where floats don't work. 489 //We replace all occurences of rgba colors 490 //which don't use percentage notation with the percentage notation. 491 rxRGBAIntegerColor.lastIndex = 0; 492 val = val.replace(rxRGBAIntegerColor, function(rgba) { 493 return rgba.replace(rxNumericValue, function(n) { 494 return n / 255 * 100 + '%'; 495 }); 496 }); 497 498 //Handle prefixing of "gradient" values. 499 //For now only the prefixed value will be set. Unprefixed isn't supported anyway. 500 rxGradient.lastIndex = 0; 501 val = val.replace(rxGradient, function(s) { 502 return theDashedCSSPrefix + s; 503 }); 504 505 506 //Now parse ANY number inside this string and create a format string. 507 val = val.replace(rxNumericValue, function(n) { 508 numbers.push(+n); 509 return '?'; 510 }); 511 512 //Add the formatstring as first value. 513 numbers.unshift(val); 514 515 return numbers; 516 }; 517 518 /** 519 * Fills the key frames with missing left and right hand properties. 520 * If key frame 1 has property X and key frame 2 is missing X, 521 * but key frame 3 has X again, then we need to assign X to key frame 2 too. 522 * 523 * @param sk A skrollable. 524 */ 525 var _fillProps = function(sk) { 526 //Will collect the properties key frame by key frame 527 var propList = {}; 528 529 //Iterate over all key frames from left to right 530 for(var i = 0; i < sk.keyFrames.length; i++) { 531 _fillPropForFrame(sk.keyFrames[i], propList); 532 } 533 534 //Now do the same from right to fill the last gaps 535 536 propList = {}; 537 538 //Iterate over all key frames from right to left 539 for(var i = sk.keyFrames.length - 1; i >= 0; i--) { 540 _fillPropForFrame(sk.keyFrames[i], propList); 541 } 542 }; 543 544 var _fillPropForFrame = function(frame, propList) { 545 //For each key frame iterate over all right hand properties and assign them, 546 //but only if the current key frame doesn't have the property by itself 547 for(var key in propList) { 548 //The current frame misses this property, so assign it. 549 if(!hasProp.call(frame.props, key)) { 550 frame.props[key] = propList[key]; 551 } 552 } 553 554 //Iterate over all props of the current frame and collect them 555 for(var key in frame.props) { 556 propList[key] = frame.props[key]; 557 } 558 }; 559 560 /** 561 * Calculates the new values for two given values array. 562 */ 563 var _calcInterpolation = function(val1, val2, progress) { 564 //They both need to have the same length 565 if(val1.length !== val2.length) { 566 throw 'Can\'t interpolate between "' + val1[0] + '" and "' + val2[0] + '"'; 567 } 568
569 //Add the format string as first element. 570 var interpolated = [val1[0]]; 571 572 for(var i = 1; i < val1.length; i++) { 573 //That's the line where the two numbers are actually interpolated. 574 interpolated[i] = val1[i] + ((val2[i] - val1[i]) * progress); 575 } 576 577 return interpolated; 578 }; 579 580 /** 581 * Interpolates the numeric values into the format string. 582 */ 583 var _interpolateString = function(val) { 584 var i = 1; 585 586 return val[0].replace(/\?/g, function() { 587 return val[i++]; 588 }); 589 }; 590 591 /** 592 * Set the CSS property on the given element. Sets prefixed properties as well. 593 */ 594 var _setStyle = function(el, prop, val) { 595 var style = el.style; 596 597 //Camel case. 598 prop = prop.replace(rxCamelCase, function(str, p1) { 599 return p1.toUpperCase(); 600 }).replace('-', ''); 601 602 //Make sure z-index gets a <integer>. 603 //This is the only <integer> case we need to handle. 604 if(prop === 'zIndex') { 605 //Floor 606 style[prop] = '' + (val | 0); 607 } 608 else { 609 //Need try-catch for old IE. 610 try { 611 //Set prefixed property. 612 style[theCSSPrefix + prop.slice(0,1).toUpperCase() + prop.slice(1)] = val; 613 614 //Set unprefixed. 615 style[prop] = val; 616 } catch(ignore) {} 617 } 618 619 //Plugin entry point. 620 if(_plugins.setStyle) { 621 for(var i = 0; i < _plugins.setStyle.length; i++) { 622 _plugins.setStyle[0].call(this, el, prop, val); 623 } 624 } 625 }; 626 627 /** 628 * Adds a CSS class. 629 */ 630 var _addClass = function(el, name) { 631 if(_untrim(el.className).indexOf(_untrim(name)) === -1) { 632 el.className = _trim((el.className + ' ' + name)); 633 } 634 }; 635 636 /** 637 * Cross browser event handling. 638 */ 639 var _addEvent = function(name, fn) { 640 if(window.addEventListener) { 641 window.addEventListener(name, fn, false); 642 } else { 643 window.attachEvent('on' + name, fn); 644 } 645 }; 646 647 /** 648 * Removes a CSS class. 649 */ 650 var _removeClass = function(el, name) { 651 el.className = _trim((_untrim(el.className)).replace(_untrim(name), ' ')); 652 }; 653 654 var _trim = function(a) { 655 return a.replace(rxTrim, '$1'); 656 }; 657 658 /** 659 * Adds a space before and after the string. 660 */ 661 var _untrim = function(a) { 662 return ' ' + a + ' '; 663 }; 664 665 var _now = function() { 666 return +new Date(); 667 }; 668 669 var _keyFrameComparator = function(a, b) { 670 return a.frame - b.frame; 671 }; 672 673 /* 674 * Private variables. 675 */ 676 //Singleton 677 var _instance; 678 679 //Will contain all plugin-functions. 680 var _plugins = {}; 681 682 /* 683 A list of all elements which should be animated associated with their the metadata. 684 Exmaple skrollable with two key frames animating from 100px width to 20px: 685 686 skrollable = { 687 element: <the DOM element>, 688 keyFrames: [ 689 { 690 frame: 100, 691 props: { 692 width: { 693 value: ['?px', 100], 694 easing: <reference to easing function> 695 } 696 } 697 }, 698 { 699 frame: 200, 700 props: { 701 width: { 702 value: ['?px', 20], 703 easing: <reference to easing function> 704 } 705 } 706 } 707 ] 708 }; 709 */ 710 var _skrollables = []; 711 712 //Will contain references to all "data-end" key frames. 713 var _dataEndKeyFrames = []; 714 715 var _listeners; 716 var _forceHeight; 717 var _maxKeyFrame = 0; 718 719 var _scale = 1; 720 721 //Current direction (up/down). 722 var _direction = 'down'; 723 724 //The last top offset value. Needed to determine direction. 725 var _lastTop = -1; 726 727 //The current top offset, needed for async rendering. 728 var _curTop = 0; 729 730 //Will contain data about a running scrollbar animation, if any. 731 var _scrollAnimation; 732 733 //Can be set by any operation/event to force rendering even if the scrollbar didn't move. 734 var _forceRender; 735 736 /* 737 * Global api. 738 */ 739 window.skrollr = { 740 //Main entry point. 741 init: function(options) { 742 return _instance || new Skrollr(options); 743 }, 744 //Plugin api. 745 plugin: function(entryPoint, fn) { 746 //Each entry point may contain multiple plugin-functions. 747 if(_plugins[entryPoint]) { 748 _plugins[entryPoint].push(fn); 749 } else { 750 _plugins[entryPoint] = [fn]; 751 } 752 }, 753 VERSION: '0.3.11' 754 }; 755}(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.