1/* 2 * HC-Sticky 3 * ========= 4 * Version: 2.2.7 5 * Author: Some Web Media 6 * Author URL: https://github.com/somewebmedia 7 * Plugin URL: https://github.com/somewebmedia/hc-sticky 8 * Description: JavaScript library that makes any element on your page visible while you scroll 9 * License: MIT 10 */ 11 12'use strict'; 13 14(function(global, factory) { 15 if (typeof module === 'object' && typeof module.exports === 'object') { 16 if (global.document) { 17 module.exports = factory(global); 18 } 19 else { 20 throw new Error('HC-Sticky requires a browser to run.'); 21 } 22 } 23 else if (typeof define === 'function' && define.amd) { 24 define('hcSticky', [], factory(global)); 25 } 26 else { 27 factory(global); 28 } 29})(typeof window !== 'undefined' ? window : this, (window) => { 30 const document = window.document; 31 32 const DEFAULT_OPTIONS = { 33 top: 0, 34 bottom: 0, 35 bottomEnd: 0, 36 innerTop: 0, 37 innerSticker: null, 38 stickyClass: 'sticky', 39 stickTo: null, 40 followScroll: true, 41 responsive: null, 42 mobileFirst: false, 43 onStart: null, 44 onStop: null, 45 onBeforeResize: null, 46 onResize: null, 47 resizeDebounce: 100, 48 disable: false 49 }; 50 51 const deprecated = (() => { 52 const pluginName = 'HC Sticky'; 53 54 return (what, instead, type) => { 55 console.warn( 56 '%c' + pluginName + ':' 57 + '%c ' + type 58 + "%c '"+ what + "'" 59 + '%c is now deprecated and will be removed. Use' 60 + "%c '" + instead + "'" 61 + '%c instead.', 62 'color: #fa253b', 63 'color: default', 64 'color: #5595c6', 65 'color: default', 66 'color: #5595c6', 67 'color: default'); 68 }; 69 })(); 70 71 const hcSticky = function(elem, userSettings) { 72 userSettings = userSettings || {}; 73 74 // use querySeletor if string is passed 75 if (typeof elem === 'string') { 76 elem = document.querySelector(elem); 77 } 78 79 // check if element exist 80 if (!elem) return false; 81 82 if (userSettings.queries) { 83 deprecated('queries', 'responsive', 'option'); 84 } 85 86 if (userSettings.queryFlow) { 87 deprecated('queryFlow', 'mobileFirst', 'option'); 88 } 89 90 let STICKY_OPTIONS = {}; 91 const Helpers = hcSticky.Helpers; 92 const elemParent = elem.parentNode; 93 94 // parent can't be static 95 if (Helpers.getStyle(elemParent, 'position') === 'static') { 96 elemParent.style.position = 'relative'; 97 } 98 99 const setOptions = (options) => { 100 options = options || {}; 101 102 if (Helpers.isEmptyObject(options) && !Helpers.isEmptyObject(STICKY_OPTIONS)) { 103 // nothing to set 104 return; 105 } 106 107 // extend options 108 STICKY_OPTIONS = Object.assign({}, DEFAULT_OPTIONS, STICKY_OPTIONS, options); 109 }; 110 111 const resetOptions = (options) => { 112 STICKY_OPTIONS = Object.assign({}, DEFAULT_OPTIONS, options || {}); 113 }; 114 115 const getOptions = (option) => { 116 return option ? STICKY_OPTIONS[option] : Object.assign({}, STICKY_OPTIONS); 117 }; 118 119 const isDisabled = () => { 120 return STICKY_OPTIONS.disable; 121 }; 122 123 const applyQueries = () => { 124 const mediaQueries = STICKY_OPTIONS.responsive || STICKY_OPTIONS.queries; 125 126 if (mediaQueries) { 127 const window_width = window.innerWidth; 128 129 // reset settings 130 resetOptions(userSettings); 131 132 if (STICKY_OPTIONS.mobileFirst) { 133 for (const width in mediaQueries) { 134 if (window_width >= width && !Helpers.isEmptyObject(mediaQueries[width])) { 135 setOptions(mediaQueries[width]); 136 } 137 } 138 } 139 else { 140 const queriesArr = []; 141 142 // convert to array so we can reverse loop it 143 for (const b in mediaQueries) { 144 const q = {}; 145 146 q[b] = mediaQueries[b]; 147 queriesArr.push(q); 148 } 149 150 for (let i = queriesArr.length - 1; i >= 0; i--) { 151 const query = queriesArr[i]; 152 const breakpoint = Object.keys(query)[0]; 153 154 if (window_width <= breakpoint && !Helpers.isEmptyObject(query[breakpoint])) { 155 setOptions(query[breakpoint]); 156 } 157 } 158 } 159 } 160 }; 161 162 // our helper function for getting necessary styles 163 const getStickyCss = (el) => { 164 const cascadedStyle = Helpers.getCascadedStyle(el); 165 const computedStyle = Helpers.getStyle(el); 166 167 const css = { 168 height: el.offsetHeight + 'px', 169 left: cascadedStyle.left, 170 right: cascadedStyle.right, 171 top: cascadedStyle.top, 172 bottom: cascadedStyle.bottom, 173 position: computedStyle.position, 174 display: computedStyle.display, 175 verticalAlign: computedStyle.verticalAlign, 176 boxSizing: computedStyle.boxSizing, 177 marginLeft: cascadedStyle.marginLeft, 178 marginRight: cascadedStyle.marginRight, 179 marginTop: cascadedStyle.marginTop, 180 marginBottom: cascadedStyle.marginBottom, 181 paddingLeft: cascadedStyle.paddingLeft, 182 paddingRight: cascadedStyle.paddingRight 183 }; 184 185 if (cascadedStyle['float']) { 186 css['float'] = cascadedStyle['float'] || 'none'; 187 } 188 189 if (cascadedStyle.cssFloat) { 190 css['cssFloat'] = cascadedStyle.cssFloat || 'none'; 191 } 192 193 // old firefox box-sizing 194 if (computedStyle.MozBoxSizing) { 195 css['MozBoxSizing'] = computedStyle.MozBoxSizing; 196 } 197 198 css['width'] = cascadedStyle.width !== 'auto' ? cascadedStyle.width : (css.boxSizing === 'border-box' || css.MozBoxSizing === 'border-box' ? el.offsetWidth + 'px' : computedStyle.width); 199 200 return css; 201 }; 202 203 const Sticky = { 204 css: {}, 205 position: null, // so we don't need to check css all the time 206 stick: (args) => { 207 args = args || {}; 208 209 // check if element is already sticky 210 if (Helpers.hasClass(elem, STICKY_OPTIONS.stickyClass)) { 211 return; 212 } 213 214 if (Spacer.isAttached === false) { 215 Spacer.attach(); 216 } 217 218 Sticky.position = 'fixed'; 219 220 // apply styles 221 elem.style.position = 'fixed'; 222 elem.style.left = Spacer.offsetLeft + 'px'; 223 elem.style.width = Spacer.width; 224 225 if (typeof args.bottom === 'undefined') { 226 elem.style.bottom = 'auto'; 227 } 228 else { 229 elem.style.bottom = args.bottom + 'px'; 230 } 231 232 if (typeof args.top === 'undefined') { 233 elem.style.top = 'auto'; 234 } 235 else { 236 elem.style.top = args.top + 'px'; 237 } 238 239 // add sticky class 240 if (elem.classList) { 241 elem.classList.add(STICKY_OPTIONS.stickyClass); 242 } 243 else { 244 elem.className += ' ' + STICKY_OPTIONS.stickyClass; 245 } 246 247 // fire 'start' event 248 if (STICKY_OPTIONS.onStart) { 249 STICKY_OPTIONS.onStart.call(elem, Object.assign({}, STICKY_OPTIONS)); 250 } 251 }, 252 release: (args) => { 253 args = args || {}; 254 args.stop = args.stop || false; 255 256 // check if we've already done this 257 if (args.stop !== true && Sticky.position !== 'fixed' && Sticky.position !== null && ( 258 (typeof args.top === 'undefined' && typeof args.bottom === 'undefined') || 259 (typeof args.top !== 'undefined' && (parseInt(Helpers.getStyle(elem, 'top')) || 0) === args.top) || 260 (typeof args.bottom !== 'undefined' && (parseInt(Helpers.getStyle(elem, 'bottom')) || 0) === args.bottom) 261 )) { 262 return; 263 } 264 265 if (args.stop === true) { 266 // remove spacer 267 if (Spacer.isAttached === true) { 268 Spacer.detach(); 269 } 270 } 271 else { 272 // check spacer 273 if (Spacer.isAttached === false) { 274 Spacer.attach(); 275 } 276 } 277 278 const position = args.position || Sticky.css.position; 279 280 // remember position 281 Sticky.position = position; 282 283 // apply styles 284 elem.style.position = position; 285 elem.style.left = args.stop === true ? Sticky.css.left : Spacer.positionLeft + 'px'; 286 elem.style.width = position !== 'absolute' ? Sticky.css.width : Spacer.width; 287 288 if (typeof args.bottom === 'undefined') { 289 elem.style.bottom = args.stop === true ? '' : 'auto'; 290 } 291 else { 292 elem.style.bottom = args.bottom + 'px'; 293 } 294 295 if (typeof args.top === 'undefined') { 296 elem.style.top = args.stop === true ? '' : 'auto'; 297 } 298 else { 299 elem.style.top = args.top + 'px'; 300 } 301 302 // remove sticky class 303 if (elem.classList) { 304 elem.classList.remove(STICKY_OPTIONS.stickyClass); 305 } 306 else { 307 elem.className = elem.className.replace(new RegExp('(^|\\b)' + STICKY_OPTIONS.stickyClass.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); 308 } 309 310 // fire 'stop' event 311 if (STICKY_OPTIONS.onStop) { 312 STICKY_OPTIONS.onStop.call(elem, Object.assign({}, STICKY_OPTIONS)); 313 } 314 } 315 }; 316 317 const Spacer = { 318 el: document.createElement('div'), 319 offsetLeft: null, 320 positionLeft: null, 321 width: null, 322 isAttached: false, 323 init: () => { 324 Spacer.el.className = 'sticky-spacer'; 325 326 // copy styles from sticky element 327 for (const prop in Sticky.css) { 328 Spacer.el.style[prop] = Sticky.css[prop]; 329 } 330 331 // just to be sure the spacer is behind everything 332 Spacer.el.style['z-index'] = '-1'; 333 334 const elemStyle = Helpers.getStyle(elem); 335 336 // get spacer offset and position 337 Spacer.offsetLeft = Helpers.offset(elem).left - (parseInt(elemStyle.marginLeft) || 0); 338 Spacer.positionLeft = Helpers.position(elem).left; 339 340 // get spacer width 341 Spacer.width = Helpers.getStyle(elem, 'width'); 342 }, 343 attach: () => { 344 // insert spacer to DOM 345 elemParent.insertBefore(Spacer.el, elem); 346 Spacer.isAttached = true; 347 }, 348 detach: () => { 349 // remove spacer from DOM 350 Spacer.el = elemParent.removeChild(Spacer.el); 351 Spacer.isAttached = false; 352 } 353 }; 354 355 // define our private variables 356 let stickTo_document;
357 let container; 358 let inner_sticker; 359 360 let container_height; 361 let container_offsetTop; 362 363 let elemParent_offsetTop; 364 365 let window_height; 366 367 let options_top; 368 let options_bottom; 369 370 let stick_top; 371 let stick_bottom; 372 373 let top_limit; 374 let bottom_limit; 375 376 let largerSticky; 377 let sticky_height; 378 let sticky_offsetTop; 379 380 let calcContainerHeight; 381 let calcStickyHeight; 382 383 const calcSticky = () => { 384 // get/set element styles 385 Sticky.css = getStickyCss(elem); 386 387 // init or reinit spacer 388 Spacer.init(); 389 390 // check if referring element is document 391 stickTo_document = STICKY_OPTIONS.stickTo && (STICKY_OPTIONS.stickTo === 'document' 392 || (STICKY_OPTIONS.stickTo.nodeType && STICKY_OPTIONS.stickTo.nodeType === 9) 393 || (typeof STICKY_OPTIONS.stickTo === 'object' && STICKY_OPTIONS.stickTo instanceof (typeof HTMLDocument !== 'undefined' ? HTMLDocument : Document))) 394 ? true : false; 395 396 // select referred container 397 container = STICKY_OPTIONS.stickTo 398 ? stickTo_document 399 ? document 400 : Helpers.getElement(STICKY_OPTIONS.stickTo) 401 : elemParent; 402 403 // get sticky height 404 calcStickyHeight = () => { 405 const height = elem.offsetHeight + (parseInt(Sticky.css.marginTop) || 0) + (parseInt(Sticky.css.marginBottom) || 0); 406 const h_diff = (sticky_height || 0) - height; 407 408 if (h_diff >= -1 && h_diff <= 1) { 409 // sometimes element height changes by 1px when it get fixed position, so don't return new value 410 return sticky_height; 411 } 412 else { 413 return height; 414 } 415 }; 416 417 sticky_height = calcStickyHeight(); 418 419 // get container height 420 calcContainerHeight = () => { 421 return !stickTo_document ? container.offsetHeight : Math.max(document.documentElement.clientHeight, document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight); 422 }; 423 424 container_height = calcContainerHeight(); 425 426 container_offsetTop = !stickTo_document ? Helpers.offset(container).top : 0; 427 elemParent_offsetTop = !STICKY_OPTIONS.stickTo 428 ? container_offsetTop // parent is container 429 : !stickTo_document 430 ? Helpers.offset(elemParent).top 431 : 0; 432 window_height = window.innerHeight; 433 sticky_offsetTop = elem.offsetTop - (parseInt(Sticky.css.marginTop) || 0); 434 435 // get inner sticker element 436 inner_sticker = Helpers.getElement(STICKY_OPTIONS.innerSticker); 437 438 // top 439 options_top = isNaN(STICKY_OPTIONS.top) && STICKY_OPTIONS.top.indexOf('%') > -1 440 ? (parseFloat(STICKY_OPTIONS.top) / 100) * window_height 441 : STICKY_OPTIONS.top; 442 443 // bottom 444 options_bottom = isNaN(STICKY_OPTIONS.bottom) && STICKY_OPTIONS.bottom.indexOf('%') > -1 445 ? (parseFloat(STICKY_OPTIONS.bottom) / 100) * window_height 446 : STICKY_OPTIONS.bottom; 447 448 // calculate sticky breakpoints 449 stick_top = inner_sticker 450 ? inner_sticker.offsetTop 451 : STICKY_OPTIONS.innerTop 452 ? STICKY_OPTIONS.innerTop 453 : 0; 454 455 stick_bottom = isNaN(STICKY_OPTIONS.bottomEnd) && STICKY_OPTIONS.bottomEnd.indexOf('%') > -1 456 ? (parseFloat(STICKY_OPTIONS.bottomEnd) / 100) * window_height 457 : STICKY_OPTIONS.bottomEnd; 458 459 top_limit = container_offsetTop - options_top + stick_top + sticky_offsetTop; 460 }; 461 462 // store scroll position so we can determine scroll direction 463 let last_pos = window.pageYOffset || document.documentElement.scrollTop; 464 let diff_y = 0; 465 let scroll_dir; 466 467 const runSticky = () => { 468 // always calculate sticky and container height in case of DOM change 469 sticky_height = calcStickyHeight(); 470 container_height = calcContainerHeight(); 471 472 bottom_limit = container_offsetTop + container_height - options_top - stick_bottom; 473 474 // check if sticky is bigger than container
475 largerSticky = sticky_height > window_height; 476 477 const offset_top = window.pageYOffset || document.documentElement.scrollTop; 478 const sticky_top = Helpers.offset(elem).top; 479 const sticky_window_top = sticky_top - offset_top; 480 let bottom_distance; 481 482 // get scroll direction 483 scroll_dir = offset_top < last_pos ? 'up' : 'down'; 484 diff_y = offset_top - last_pos; 485 last_pos = offset_top; 486 487 if (offset_top > top_limit) { 488 // http://geek-and-poke.com/geekandpoke/2012/7/27/simply-explained.html 489 if (bottom_limit + options_top + (largerSticky ? options_bottom : 0) - (STICKY_OPTIONS.followScroll && largerSticky ? 0 : options_top) <= offset_top + sticky_height - stick_top - ((sticky_height - stick_top > window_height - (top_limit - stick_top) && STICKY_OPTIONS.followScroll) ? (((bottom_distance = sticky_height - window_height - stick_top) > 0) ? bottom_distance : 0) : 0)) { // bottom reached end 490 Sticky.release({ 491 position: 'absolute', 492 //top: bottom_limit - sticky_height - top_limit + stick_top + sticky_offsetTop 493 bottom: elemParent_offsetTop + elemParent.offsetHeight - bottom_limit - options_top 494 }); 495 } 496 else if (largerSticky && STICKY_OPTIONS.followScroll) { // sticky is bigger than container and follows scroll 497 if (scroll_dir === 'down') { // scroll down 498 if (sticky_window_top + sticky_height + options_bottom <= window_height + .9) { // stick on bottom 499 // fix subpixel precision with adding .9 pixels 500 Sticky.stick({ 501 //top: window_height - sticky_height - options_bottom 502 bottom: options_bottom 503 }); 504 } 505 else if (Sticky.position === 'fixed') { // bottom reached window bottom 506 Sticky.release({ 507 position: 'absolute', 508 top: sticky_top - options_top - top_limit - diff_y + stick_top 509 }); 510 } 511 } 512 else { // scroll up 513 if (Math.ceil(sticky_window_top + stick_top) < 0 && Sticky.position === 'fixed') { // top reached window top 514 Sticky.release({ 515 position: 'absolute', 516 top: sticky_top - options_top - top_limit + stick_top - diff_y 517 }); 518 } 519 else if (sticky_top >= offset_top + options_top - stick_top) { // stick on top 520 Sticky.stick({ 521 top: options_top - stick_top 522 }); 523 } 524 } 525 } 526 else { // stick on top 527 Sticky.stick({ 528 top: options_top - stick_top 529 }); 530 } 531 } 532 else { // starting point 533 Sticky.release({ 534 stop: true 535 }); 536 } 537 }; 538 539 let scrollAttached = false; 540 let resizeAttached = false; 541 542 const disableSticky = () => { 543 if (scrollAttached) { 544 // detach sticky from scroll 545 window.removeEventListener('scroll', runSticky, Helpers.supportsPassive); 546 547 // sticky is not attached to scroll anymore 548 scrollAttached = false; 549 } 550 }; 551 552 const initSticky = () => { 553 // check if element or it's parents are visible 554 if (elem.offsetParent === null || Helpers.getStyle(elem, 'display') === 'none') { 555 disableSticky(); 556 return; 557 } 558 559 // calculate stuff 560 calcSticky(); 561 562 // check if sticky is bigger than reffering container 563 if (sticky_height > container_height) { 564 disableSticky(); 565 return; 566 } 567 568 // run 569 runSticky(); 570 571 if (!scrollAttached) { 572 // attach sticky to scroll 573 window.addEventListener('scroll', runSticky, Helpers.supportsPassive); 574 575 // sticky is attached to scroll 576 scrollAttached = true; 577 } 578 }; 579 580 const resetSticky = () => { 581 // remove inline styles 582 elem.style.position = ''; 583 elem.style.left = ''; 584 elem.style.top = ''; 585 elem.style.bottom = ''; 586 elem.style.width = ''; 587 588 // remove sticky class 589 if (elem.classList) { 590 elem.classList.remove(STICKY_OPTIONS.stickyClass); 591 } 592 else { 593 elem.className = elem.className.replace(new RegExp('(^|\\b)' + STICKY_OPTIONS.stickyClass.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); 594 } 595 596 // reset sticky object data 597 Sticky.css = {}; 598 Sticky.position = null; 599 600 // remove spacer 601 if (Spacer.isAttached === true) { 602 Spacer.detach(); 603 } 604 }; 605 606 const reinitSticky = () => { 607 resetSticky(); 608 applyQueries(); 609 610 if (isDisabled()) { 611 disableSticky(); 612 return; 613 } 614 615 // restart sticky 616 initSticky(); 617 }; 618 619 const resizeSticky = () => { 620 // fire 'beforeResize' event 621 if (STICKY_OPTIONS.onBeforeResize) { 622 STICKY_OPTIONS.onBeforeResize.call(elem, Object.assign({}, STICKY_OPTIONS)); 623 } 624 625 // reinit sticky 626 reinitSticky(); 627 628 // fire 'resize' event 629 if (STICKY_OPTIONS.onResize) { 630 STICKY_OPTIONS.onResize.call(elem, Object.assign({}, STICKY_OPTIONS)); 631 } 632 }; 633 634 const resize_cb = !STICKY_OPTIONS.resizeDebounce ? resizeSticky : Helpers.debounce(resizeSticky, STICKY_OPTIONS.resizeDebounce); 635 636 // Method for updating options 637 const Update = (options) => { 638 setOptions(options); 639 640 // also update user settings 641 userSettings = Object.assign({}, userSettings, options || {}); 642 643 reinitSticky(); 644 }; 645 646 const Detach = () => { 647 // detach resize reinit 648 if (resizeAttached) { 649 window.removeEventListener('resize', resize_cb, Helpers.supportsPassive); 650 resizeAttached = false; 651 } 652 653 disableSticky(); 654 }; 655 656 const Destroy = () => { 657 Detach(); 658 resetSticky(); 659 }; 660 661 const Attach = () => { 662 // attach resize reinit 663 if (!resizeAttached) { 664 window.addEventListener('resize', resize_cb, Helpers.supportsPassive); 665 resizeAttached = true; 666 } 667 668 applyQueries(); 669 670 if (isDisabled()) { 671 disableSticky(); 672 return; 673 } 674 675 initSticky(); 676 }; 677 678 this.options = getOptions; 679 this.refresh = reinitSticky; 680 this.update = Update; 681 this.attach = Attach; 682 this.detach = Detach; 683 this.destroy = Destroy; 684 685 // jQuery methods 686 this.triggerMethod = (method, options) => { 687 if (typeof this[method] === 'function') { 688 this[method](options); 689 } 690 }; 691 692 this.reinit = () => { 693 deprecated('reinit', 'refresh', 'method'); 694 reinitSticky(); 695 }; 696 697 // init settings 698 setOptions(userSettings); 699 700 // start sticky 701 Attach(); 702 703 // reinit on complete page load 704 window.addEventListener('load', reinitSticky); 705 }; 706 707 // jQuery Plugin 708 if (typeof window.jQuery !== 'undefined') { 709 const $ = window.jQuery; 710 const namespace = 'hcSticky'; 711 712 $.fn.extend({ 713 hcSticky: function(args, update) { 714 // check if selected element exist 715 if (!this.length) return this; 716 717 // we need to return options 718 if (args === 'options') { 719 return $.data(this.get(0), namespace).options(); 720 } 721 722 return this.each(function() { 723 let instance = $.data(this, namespace); 724 725 if (instance) { 726 // already created, trigger method 727 instance.triggerMethod(args, update); 728 } 729 else { 730 // create new instance 731 instance = new hcSticky(this, args); 732 $.data(this, namespace, instance); 733 } 734 }); 735 } 736 });
737 } 738 739 // browser global 740 window.hcSticky = window.hcSticky || hcSticky; 741 742 return hcSticky; 743});
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.