fixto.js (26931B)
1 var fixto = (function ($, window, document) { 2 3 // Start Computed Style. Please do not modify this module here. Modify it from its own repo. See address below. 4 5 /*! Computed Style - v0.1.0 - 2012-07-19 6 * https://github.com/bbarakaci/computed-style 7 * Copyright (c) 2012 Burak Barakaci; Licensed MIT */ 8 var computedStyle = (function () { 9 var computedStyle = { 10 getAll: function (element) { 11 return document.defaultView.getComputedStyle(element); 12 }, 13 get: function (element, name) { 14 return this.getAll(element)[name]; 15 }, 16 toFloat: function (value) { 17 return parseFloat(value, 10) || 0; 18 }, 19 getFloat: function (element, name) { 20 return this.toFloat(this.get(element, name)); 21 }, 22 _getAllCurrentStyle: function (element) { 23 return element.currentStyle; 24 } 25 }; 26 27 if (document.documentElement.currentStyle) { 28 computedStyle.getAll = computedStyle._getAllCurrentStyle; 29 } 30 31 return computedStyle; 32 33 }()); 34 35 // End Computed Style. Modify whatever you want to. 36 37 var mimicNode = (function () { 38 /* 39 Class Mimic Node 40 Dependency : Computed Style 41 Tries to mimick a dom node taking his styles, dimensions. May go to his repo if gets mature. 42 */ 43 44 function MimicNode(element) { 45 this.element = element; 46 this.replacer = document.createElement('div'); 47 this.replacer.style.visibility = 'hidden'; 48 this.hide(); 49 element.parentNode.insertBefore(this.replacer, element); 50 } 51 52 MimicNode.prototype = { 53 replace: function () { 54 var rst = this.replacer.style; 55 var styles = computedStyle.getAll(this.element); 56 57 // rst.width = computedStyle.width(this.element) + 'px'; 58 // rst.height = this.element.offsetHeight + 'px'; 59 60 // Setting offsetWidth 61 rst.width = this._width(); 62 rst.height = this._height(); 63 64 // Adopt margins 65 rst.marginTop = styles.marginTop; 66 rst.marginBottom = styles.marginBottom; 67 rst.marginLeft = styles.marginLeft; 68 rst.marginRight = styles.marginRight; 69 70 // Adopt positioning 71 rst.cssFloat = styles.cssFloat; 72 rst.styleFloat = styles.styleFloat; //ie8; 73 rst.position = styles.position; 74 rst.top = styles.top; 75 rst.right = styles.right; 76 rst.bottom = styles.bottom; 77 rst.left = styles.left; 78 // rst.borderStyle = styles.borderStyle; 79 80 rst.display = styles.display; 81 82 }, 83 84 hide: function () { 85 this.replacer.style.display = 'none'; 86 }, 87 88 _width: function () { 89 return this.element.getBoundingClientRect().width + 'px'; 90 }, 91 92 _widthOffset: function () { 93 return this.element.offsetWidth + 'px'; 94 }, 95 96 _height: function () { 97 return jQuery(this.element).outerHeight() + 'px'; 98 }, 99 100 _heightOffset: function () { 101 return this.element.offsetHeight + 'px'; 102 }, 103 104 destroy: function () { 105 $(this.replacer).remove(); 106 107 // set properties to null to break references 108 for (var prop in this) { 109 if (this.hasOwnProperty(prop)) { 110 this[prop] = null; 111 } 112 } 113 } 114 }; 115 116 var bcr = document.documentElement.getBoundingClientRect(); 117 if (!bcr.width) { 118 MimicNode.prototype._width = MimicNode.prototype._widthOffset; 119 MimicNode.prototype._height = MimicNode.prototype._heightOffset; 120 } 121 122 return { 123 MimicNode: MimicNode, 124 computedStyle: computedStyle 125 }; 126 }()); 127 128 // Class handles vendor prefixes 129 function Prefix() { 130 // Cached vendor will be stored when it is detected 131 this._vendor = null; 132 133 //this._dummy = document.createElement('div'); 134 } 135 136 Prefix.prototype = { 137 138 _vendors: { 139 webkit: { 140 cssPrefix: '-webkit-', 141 jsPrefix: 'Webkit' 142 }, 143 moz: { 144 cssPrefix: '-moz-', 145 jsPrefix: 'Moz' 146 }, 147 ms: { 148 cssPrefix: '-ms-', 149 jsPrefix: 'ms' 150 }, 151 opera: { 152 cssPrefix: '-o-', 153 jsPrefix: 'O' 154 } 155 }, 156 157 _prefixJsProperty: function (vendor, prop) { 158 return vendor.jsPrefix + prop[0].toUpperCase() + prop.substr(1); 159 }, 160 161 _prefixValue: function (vendor, value) { 162 return vendor.cssPrefix + value; 163 }, 164 165 _valueSupported: function (prop, value, dummy) { 166 // IE8 will throw Illegal Argument when you attempt to set a not supported value. 167 try { 168 dummy.style[prop] = value; 169 return dummy.style[prop] === value; 170 } catch (er) { 171 return false; 172 } 173 }, 174 175 /** 176 * Returns true if the property is supported 177 * @param {string} prop Property name 178 * @returns {boolean} 179 */ 180 propertySupported: function (prop) { 181 // Supported property will return either inine style value or an empty string. 182 // Undefined means property is not supported. 183 return document.documentElement.style[prop] !== undefined; 184 }, 185 186 /** 187 * Returns prefixed property name for js usage 188 * @param {string} prop Property name 189 * @returns {string|null} 190 */ 191 getJsProperty: function (prop) { 192 // Try native property name first. 193 if (this.propertySupported(prop)) { 194 return prop; 195 } 196 197 // Prefix it if we know the vendor already 198 if (this._vendor) { 199 return this._prefixJsProperty(this._vendor, prop); 200 } 201 202 // We don't know the vendor, try all the possibilities 203 var prefixed; 204 for (var vendor in this._vendors) { 205 prefixed = this._prefixJsProperty(this._vendors[vendor], prop); 206 if (this.propertySupported(prefixed)) { 207 // Vendor detected. Cache it. 208 this._vendor = this._vendors[vendor]; 209 return prefixed; 210 } 211 } 212 213 // Nothing worked 214 return null; 215 }, 216 217 /** 218 * Returns supported css value for css property. Could be used to check support or get prefixed value string. 219 * @param {string} prop Property 220 * @param {string} value Value name 221 * @returns {string|null} 222 */ 223 getCssValue: function (prop, value) { 224 // Create dummy element to test value 225 var dummy = document.createElement('div'); 226 227 // Get supported property name 228 var jsProperty = this.getJsProperty(prop); 229 230 // Try unprefixed value 231 if (this._valueSupported(jsProperty, value, dummy)) { 232 return value; 233 } 234 235 var prefixedValue; 236 237 // If we know the vendor already try prefixed value 238 if (this._vendor) { 239 prefixedValue = this._prefixValue(this._vendor, value); 240 if (this._valueSupported(jsProperty, prefixedValue, dummy)) { 241 return prefixedValue; 242 } 243 } 244 245 // Try all vendors 246 for (var vendor in this._vendors) { 247 prefixedValue = this._prefixValue(this._vendors[vendor], value); 248 if (this._valueSupported(jsProperty, prefixedValue, dummy)) { 249 // Vendor detected. Cache it. 250 this._vendor = this._vendors[vendor]; 251 return prefixedValue; 252 } 253 } 254 // No support for value 255 return null; 256 } 257 }; 258 259 var prefix = new Prefix(); 260 261 // We will need this frequently. Lets have it as a global until we encapsulate properly. 262 var transformJsProperty = prefix.getJsProperty('transform'); 263 264 // Will hold if browser creates a positioning context for fixed elements. 265 var fixedPositioningContext; 266 267 // Checks if browser creates a positioning context for fixed elements. 268 // Transform rule will create a positioning context on browsers who follow the spec. 269 // Ie for example will fix it according to documentElement 270 // TODO: Other css rules also effects. perspective creates at chrome but not in firefox. transform-style preserve3d effects. 271 function checkFixedPositioningContextSupport() { 272 var support = false; 273 var parent = document.createElement('div'); 274 var child = document.createElement('div'); 275 parent.appendChild(child); 276 parent.style[transformJsProperty] = 'translate(0)'; 277 // Make sure there is space on top of parent 278 parent.style.marginTop = '10px'; 279 parent.style.visibility = 'hidden'; 280 child.style.position = 'fixed'; 281 child.style.top = 0; 282 document.body.appendChild(parent); 283 var rect = child.getBoundingClientRect(); 284 // If offset top is greater than 0 meand transformed element created a positioning context. 285 if (rect.top > 0) { 286 support = true; 287 } 288 // Remove dummy content 289 document.body.removeChild(parent); 290 return support; 291 } 292 293 // It will return null if position sticky is not supported 294 var nativeStickyValue = prefix.getCssValue('position', 'sticky'); 295 296 // It will return null if position fixed is not supported 297 var fixedPositionValue = prefix.getCssValue('position', 'fixed'); 298 299 // Dirty business 300 var ie = navigator.appName === 'Microsoft Internet Explorer'; 301 var ieversion; 302 303 if (ie) { 304 ieversion = parseFloat(navigator.appVersion.split("MSIE")[1]); 305 } 306 307 function FixTo(child, parent, options) { 308 this.child = child; 309 this._$child = $(child); 310 this.parent = parent; 311 this.options = { 312 className: 'fixto-fixed', 313 top: 0 314 }; 315 this._setOptions(options); 316 } 317 318 FixTo.prototype = { 319 // Returns the total outerHeight of the elements passed to mind option. Will return 0 if none. 320 _mindtop: function () { 321 var top = 0; 322 if (this._$mind) { 323 var el; 324 var rect; 325 var height; 326 for (var i = 0, l = this._$mind.length; i < l; i++) { 327 el = this._$mind[i]; 328 rect = el.getBoundingClientRect(); 329 if (rect.height) { 330 top += rect.height; 331 } else { 332 var styles = computedStyle.getAll(el); 333 top += el.offsetHeight + computedStyle.toFloat(styles.marginTop) + computedStyle.toFloat(styles.marginBottom); 334 } 335 } 336 } 337 return top; 338 }, 339 340 // Public method to stop the behaviour of this instance. 341 stop: function () { 342 this._stop(); 343 this._running = false; 344 }, 345 346 // Public method starts the behaviour of this instance. 347 start: function () { 348 349 // Start only if it is not running not to attach event listeners multiple times. 350 if (!this._running) { 351 this._start(); 352 this._running = true; 353 } 354 }, 355 356 //Public method to destroy fixto behaviour 357 destroy: function () { 358 this.stop(); 359 360 this._destroy(); 361 362 // Remove jquery data from the element 363 this._$child.removeData('fixto-instance'); 364 365 // set properties to null to break references 366 for (var prop in this) { 367 if (this.hasOwnProperty(prop)) { 368 this[prop] = null; 369 } 370 } 371 }, 372 373 _setOptions: function (options) { 374 $.extend(this.options, options); 375 if (this.options.mind) { 376 this._$mind = $(this.options.mind); 377 } 378 if (this.options.zIndex) { 379 this.child.style.zIndex = this.options.zIndex; 380 } 381 }, 382 383 setOptions: function (options) { 384 this._setOptions(options); 385 this.refresh(); 386 }, 387 388 // Methods could be implemented by subclasses 389 390 _stop: function () { 391 392 }, 393 394 _start: function () { 395 396 }, 397 398 _destroy: function () { 399 400 }, 401 402 refresh: function () { 403 404 } 405 }; 406 407 // Class FixToContainer 408 function FixToContainer(child, parent, options) { 409 FixTo.call(this, child, parent, options); 410 this._replacer = new mimicNode.MimicNode(child); 411 this._ghostNode = this._replacer.replacer; 412 413 this._saveStyles(); 414 415 this._saveViewportHeight(); 416 417 // Create anonymous functions and keep references to register and unregister events. 418 this._proxied_onscroll = this._bind(this._onscroll, this); 419 this._proxied_onresize = this._bind(this._onresize, this); 420 421 this.start(); 422 } 423 424 FixToContainer.prototype = new FixTo(); 425 426 $.extend(FixToContainer.prototype, { 427 428 // Returns an anonymous function that will call the given function in the given context 429 _bind: function (fn, context) { 430 return function () { 431 return fn.call(context); 432 }; 433 }, 434 435 // at ie8 maybe only in vm window resize event fires everytime an element is resized. 436 _toresize: ieversion === 8 ? document.documentElement : window, 437 438 _onscroll: function _onscroll() { 439 this._scrollTop = document.documentElement.scrollTop || document.body.scrollTop; 440 this._parentBottom = (this.parent.offsetHeight + this._fullOffset('offsetTop', this.parent)); 441 442 // if (this.options.mindBottomPadding !== false) { 443 // this._parentBottom -= computedStyle.getFloat(this.parent, 'paddingBottom'); 444 // } 445 446 447 // if (this.options.toBottom) { 448 // this._fix(); 449 // this._adjust(); 450 // return 451 // } 452 453 // if (this.options.toBottom) { 454 // this.options.top = this._viewportHeight - computedStyle.toFloat(computedStyle.getAll(this.child).height) - this.options.topSpacing; 455 // } 456 457 if (!this.fixed) { 458 459 var childStyles = computedStyle.getAll(this.child); 460 461 if (( 462 this._scrollTop < this._parentBottom && 463 this._scrollTop > (this._fullOffset('offsetTop', this.child) - this.options.top - this._mindtop()) && 464 this._viewportHeight > (this.child.offsetHeight + computedStyle.toFloat(childStyles.marginTop) + computedStyle.toFloat(childStyles.marginBottom)) 465 ) || this.options.toBottom) { 466 467 this._fix(); 468 this._adjust(); 469 } 470 } else { 471 if (this.options.toBottom) { 472 if (this._scrollTop >= this._fullOffset('offsetTop', this._ghostNode)) { 473 this._unfix(); 474 return; 475 } 476 477 } else { 478 if (this._scrollTop > this._parentBottom || this._scrollTop <= (this._fullOffset('offsetTop', this._ghostNode) - this.options.top - this._mindtop())) { 479 this._unfix(); 480 return; 481 } 482 } 483 this._adjust(); 484 } 485 }, 486 487 _adjust: function _adjust() { 488 var top = 0; 489 var mindTop = this._mindtop(); 490 var diff = 0; 491 var childStyles = computedStyle.getAll(this.child); 492 var context = null; 493 494 if (fixedPositioningContext) { 495 // Get positioning context. 496 context = this._getContext(); 497 if (context) { 498 // There is a positioning context. Top should be according to the context. 499 top = Math.abs(context.getBoundingClientRect().top); 500 } 501 } 502 503 diff = (this._parentBottom - this._scrollTop) - (this.child.offsetHeight + computedStyle.toFloat(childStyles.marginBottom) + mindTop + this.options.top); 504 505 if (diff > 0) { 506 diff = 0; 507 } 508 509 if (this.options.toBottom) { 510 // this.child.style.top = (diff + mindTop + top + this.options.top) - computedStyle.toFloat(childStyles.marginTop) + 'px'; 511 } else { 512 var _top = this.options.top; 513 if (_top === 0) { 514 _top = $('body').offset().top; 515 } 516 517 this.child.style.top = Math.round((diff + mindTop + top + _top) - computedStyle.toFloat(childStyles.marginTop))+ 'px'; 518 } 519 }, 520 521 // Calculate cumulative offset of the element. 522 // Optionally according to context 523 _fullOffset: function _fullOffset(offsetName, elm, context) { 524 var offset = elm[offsetName]; 525 var offsetParent = elm.offsetParent; 526 527 // Add offset of the ascendent tree until we reach to the document root or to the given context 528 while (offsetParent !== null && offsetParent !== context) { 529 offset = offset + offsetParent[offsetName]; 530 offsetParent = offsetParent.offsetParent; 531 } 532 533 return offset; 534 }, 535 536 // Get positioning context of the element. 537 // We know that the closest parent that a transform rule applied will create a positioning context. 538 _getContext: function () { 539 var parent; 540 var element = this.child; 541 var context = null; 542 var styles; 543 544 // Climb up the treee until reaching the context 545 while (!context) { 546 parent = element.parentNode; 547 if (parent === document.documentElement) { 548 return null; 549 } 550 551 styles = computedStyle.getAll(parent); 552 // Element has a transform rule 553 if (styles[transformJsProperty] !== 'none') { 554 context = parent; 555 break; 556 } 557 element = parent; 558 } 559 return context; 560 }, 561 562 _fix: function _fix() { 563 var child = this.child; 564 var childStyle = child.style; 565 var childStyles = computedStyle.getAll(child); 566 var left = child.getBoundingClientRect().left; 567 var width = childStyles.width; 568 this.options._original 569 570 this._saveStyles(); 571 572 if (document.documentElement.currentStyle) { 573 // Function for ie<9. When hasLayout is not triggered in ie7, he will report currentStyle as auto, clientWidth as 0. Thus using offsetWidth. 574 // Opera also falls here 575 576 width = (child.offsetWidth) 577 if (childStyles.boxSizing !== "border-box") { 578 width = width - (computedStyle.toFloat(childStyles.paddingLeft) + computedStyle.toFloat(childStyles.paddingRight) + computedStyle.toFloat(childStyles.borderLeftWidth) + computedStyle.toFloat(childStyles.borderRightWidth)); 579 } 580 581 width += "px"; 582 583 } 584 585 // Ie still fixes the container according to the viewport. 586 if (fixedPositioningContext) { 587 var context = this._getContext(); 588 // if(context) { 589 // // There is a positioning context. Left should be according to the context. 590 // left = child.getBoundingClientRect().left - context.getBoundingClientRect().left; 591 // } else { 592 left = this._$child.offset().left; 593 // } 594 } 595 596 this._replacer.replace(); 597 598 childStyle.left = /*left + "px"; */ (left - computedStyle.toFloat(childStyles.marginLeft)) + 'px'; 599 childStyle.width = width; 600 601 childStyle.position = 'fixed'; 602 if (this.options.toBottom) { 603 childStyle.top = ""; 604 childStyle.bottom = this.options.top + computedStyle.toFloat(childStyles.marginBottom) + 'px'; 605 } else { 606 childStyle.bottom = ""; 607 var _top = this.options.top; 608 609 if (_top === 0) { 610 _top = $('body').offset().top; 611 } 612 childStyle.top = this._mindtop() + _top - computedStyle.toFloat(childStyles.marginTop) + 'px'; 613 614 } 615 this._$child.addClass(this.options.className); 616 this.fixed = true; 617 this._$child.trigger('fixto-added'); 618 }, 619 620 _unfix: function _unfix() { 621 var childStyle = this.child.style; 622 this._replacer.hide(); 623 childStyle.position = this._childOriginalPosition; 624 childStyle.top = this._childOriginalTop; 625 childStyle.bottom = this._childOriginalBottom; 626 childStyle.width = this._childOriginalWidth; 627 childStyle.left = this._childOriginalLeft; 628 if (!this.options.always) { 629 this._$child.removeClass(this.options.className); 630 this._$child.trigger('fixto-removed'); 631 } 632 this.fixed = false; 633 }, 634 635 _saveStyles: function () { 636 var childStyle = this.child.style; 637 this._childOriginalPosition = childStyle.position; 638 if (this.options.toBottom) { 639 this._childOriginalTop = ""; 640 this._childOriginalBottom = childStyle.bottom; 641 } else { 642 this._childOriginalTop = childStyle.top; 643 this._childOriginalBottom = ""; 644 } 645 this._childOriginalWidth = childStyle.width; 646 this._childOriginalLeft = childStyle.left; 647 }, 648 649 _onresize: function () { 650 this.refresh(); 651 }, 652 653 _saveViewportHeight: function () { 654 // ie8 doesn't support innerHeight 655 this._viewportHeight = window.innerHeight || document.documentElement.clientHeight; 656 }, 657 658 _stop: function () { 659 // Unfix the container immediately. 660 this._unfix(); 661 // remove event listeners 662 $(window).unbind('scroll.fixto mousewheel', this._proxied_onscroll); 663 $(this._toresize).unbind('resize.fixto', this._proxied_onresize); 664 }, 665 666 _start: function () { 667 // Trigger onscroll to have the effect immediately. 668 this._onscroll(); 669 670 // Attach event listeners 671 $(window).bind('scroll.fixto mousewheel', this._proxied_onscroll); 672 $(this._toresize).bind('resize.fixto', this._proxied_onresize); 673 }, 674 675 _destroy: function () { 676 // Destroy mimic node instance 677 this._replacer.destroy(); 678 }, 679 680 refresh: function () { 681 this._saveViewportHeight(); 682 this._unfix(); 683 this._onscroll(); 684 } 685 }); 686 687 function NativeSticky(child, parent, options) { 688 FixTo.call(this, child, parent, options); 689 this.start(); 690 } 691 692 NativeSticky.prototype = new FixTo(); 693 694 $.extend(NativeSticky.prototype, { 695 _start: function () { 696 697 var childStyles = computedStyle.getAll(this.child); 698 699 this._childOriginalPosition = childStyles.position; 700 this._childOriginalTop = childStyles.top; 701 702 this.child.style.position = nativeStickyValue; 703 this.refresh(); 704 }, 705 706 _stop: function () { 707 this.child.style.position = this._childOriginalPosition; 708 this.child.style.top = this._childOriginalTop; 709 }, 710 711 refresh: function () { 712 this.child.style.top = this._mindtop() + this.options.top + 'px'; 713 } 714 }); 715 716 717 718 var fixTo = function fixTo(childElement, parentElement, options) { 719 if ((nativeStickyValue && !options) || (nativeStickyValue && options && options.useNativeSticky !== false)) { 720 // Position sticky supported and user did not disabled the usage of it. 721 return new NativeSticky(childElement, parentElement, options); 722 } else if (fixedPositionValue) { 723 // Position fixed supported 724 725 if (fixedPositioningContext === undefined) { 726 // We don't know yet if browser creates fixed positioning contexts. Check it. 727 fixedPositioningContext = checkFixedPositioningContextSupport(); 728 } 729 730 return new FixToContainer(childElement, parentElement, options); 731 } else { 732 return 'Neither fixed nor sticky positioning supported'; 733 } 734 }; 735 736 /* 737 No support for ie lt 8 738 */ 739 740 if (ieversion < 8) { 741 fixTo = function () { 742 return 'not supported'; 743 }; 744 } 745 746 // Let it be a jQuery Plugin 747 $.fn.fixTo = function (targetSelector, options) { 748 749 var $targets = $(targetSelector); 750 751 var i = 0; 752 return this.each(function () { 753 754 // Check the data of the element. 755 var instance = $(this).data('fixto-instance'); 756 757 // If the element is not bound to an instance, create the instance and save it to elements data. 758 if (!instance) { 759 $(this).data('fixto-instance', fixTo(this, $targets[i], options)); 760 } else { 761 // If we already have the instance here, expect that targetSelector parameter will be a string 762 // equal to a public methods name. Run the method on the instance without checking if 763 // it exists or it is a public method or not. Cause nasty errors when necessary. 764 var method = targetSelector; 765 instance[method].call(instance, options); 766 } 767 i++; 768 }); 769 }; 770 771 /* 772 Expose 773 */ 774 775 return { 776 FixToContainer: FixToContainer, 777 fixTo: fixTo, 778 computedStyle: computedStyle, 779 mimicNode: mimicNode 780 }; 781 782 783 }(window.jQuery, window, document));