JAVASCRIPT   14

scrollspy

Guest on 6th July 2022 01:06:43 AM

  1. /**
  2.  * --------------------------------------------------------------------------
  3.  * Bootstrap (v4.3.1): scrollspy.js
  4.  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  5.  * --------------------------------------------------------------------------
  6.  */
  7.  
  8. import $ from 'jquery'
  9. import Util from './util'
  10.  
  11. /**
  12.  * ------------------------------------------------------------------------
  13.  * Constants
  14.  * ------------------------------------------------------------------------
  15.  */
  16.  
  17. const NAME               = 'scrollspy'
  18. const VERSION            = '4.3.1'
  19. const DATA_KEY           = 'bs.scrollspy'
  20. const EVENT_KEY          = `.${DATA_KEY}`
  21. const DATA_API_KEY       = '.data-api'
  22. const JQUERY_NO_CONFLICT = $.fn[NAME]
  23.  
  24. const Default = {
  25.   offset : 10,
  26.   method : 'auto',
  27.   target : ''
  28. }
  29.  
  30. const DefaultType = {
  31.   offset : 'number',
  32.   method : 'string',
  33.   target : '(string|element)'
  34. }
  35.  
  36. const Event = {
  37.   ACTIVATE      : `activate${EVENT_KEY}`,
  38.   SCROLL        : `scroll${EVENT_KEY}`,
  39.   LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`
  40. }
  41.  
  42. const ClassName = {
  43.   DROPDOWN_ITEM : 'dropdown-item',
  44.   DROPDOWN_MENU : 'dropdown-menu',
  45.   ACTIVE        : 'active'
  46. }
  47.  
  48. const Selector = {
  49.   DATA_SPY        : '[data-spy="scroll"]',
  50.   ACTIVE          : '.active',
  51.   NAV_LIST_GROUP  : '.nav, .list-group',
  52.   NAV_LINKS       : '.nav-link',
  53.   NAV_ITEMS       : '.nav-item',
  54.   LIST_ITEMS      : '.list-group-item',
  55.   DROPDOWN        : '.dropdown',
  56.   DROPDOWN_ITEMS  : '.dropdown-item',
  57.   DROPDOWN_TOGGLE : '.dropdown-toggle'
  58. }
  59.  
  60. const OffsetMethod = {
  61.   OFFSET   : 'offset',
  62.   POSITION : 'position'
  63. }
  64.  
  65. /**
  66.  * ------------------------------------------------------------------------
  67.  * Class Definition
  68.  * ------------------------------------------------------------------------
  69.  */
  70.  
  71. class ScrollSpy {
  72.   constructor(element, config) {
  73.     this._element       = element
  74.     this._scrollElement = element.tagName === 'BODY' ? window : element
  75.     this._config        = this._getConfig(config)
  76.     this._selector      = `${this._config.target} ${Selector.NAV_LINKS},` +
  77.                           `${this._config.target} ${Selector.LIST_ITEMS},` +
  78.                           `${this._config.target} ${Selector.DROPDOWN_ITEMS}`
  79.     this._offsets       = []
  80.     this._targets       = []
  81.     this._activeTarget  = null
  82.     this._scrollHeight  = 0
  83.  
  84.     $(this._scrollElement).on(Event.SCROLL, (event) => this._process(event))
  85.  
  86.     this.refresh()
  87.     this._process()
  88.   }
  89.  
  90.   // Getters
  91.  
  92.   static get VERSION() {
  93.     return VERSION
  94.   }
  95.  
  96.   static get Default() {
  97.     return Default
  98.   }
  99.  
  100.   // Public
  101.  
  102.   refresh() {
  103.     const autoMethod = this._scrollElement === this._scrollElement.window
  104.       ? OffsetMethod.OFFSET : OffsetMethod.POSITION
  105.  
  106.     const offsetMethod = this._config.method === 'auto'
  107.       ? autoMethod : this._config.method
  108.  
  109.     const offsetBase = offsetMethod === OffsetMethod.POSITION
  110.       ? this._getScrollTop() : 0
  111.  
  112.     this._offsets = []
  113.     this._targets = []
  114.  
  115.     this._scrollHeight = this._getScrollHeight()
  116.  
  117.     const targets = [].slice.call(document.querySelectorAll(this._selector))
  118.  
  119.     targets
  120.       .map((element) => {
  121.         let target
  122.         const targetSelector = Util.getSelectorFromElement(element)
  123.  
  124.         if (targetSelector) {
  125.           target = document.querySelector(targetSelector)
  126.         }
  127.  
  128.         if (target) {
  129.           const targetBCR = target.getBoundingClientRect()
  130.           if (targetBCR.width || targetBCR.height) {
  131.             // TODO (fat): remove sketch reliance on jQuery position/offset
  132.             return [
  133.               $(target)[offsetMethod]().top + offsetBase,
  134.               targetSelector
  135.             ]
  136.           }
  137.         }
  138.         return null
  139.       })
  140.       .filter((item) => item)
  141.       .sort((a, b) => a[0] - b[0])
  142.       .forEach((item) => {
  143.         this._offsets.push(item[0])
  144.         this._targets.push(item[1])
  145.       })
  146.   }
  147.  
  148.   dispose() {
  149.     $.removeData(this._element, DATA_KEY)
  150.     $(this._scrollElement).off(EVENT_KEY)
  151.  
  152.     this._element       = null
  153.     this._scrollElement = null
  154.     this._config        = null
  155.     this._selector      = null
  156.     this._offsets       = null
  157.     this._targets       = null
  158.     this._activeTarget  = null
  159.     this._scrollHeight  = null
  160.   }
  161.  
  162.   // Private
  163.  
  164.   _getConfig(config) {
  165.     config = {
  166.       ...Default,
  167.       ...typeof config === 'object' && config ? config : {}
  168.     }
  169.  
  170.     if (typeof config.target !== 'string') {
  171.       let id = $(config.target).attr('id')
  172.       if (!id) {
  173.         id = Util.getUID(NAME)
  174.         $(config.target).attr('id', id)
  175.       }
  176.       config.target = `#${id}`
  177.     }
  178.  
  179.     Util.typeCheckConfig(NAME, config, DefaultType)
  180.  
  181.     return config
  182.   }
  183.  
  184.   _getScrollTop() {
  185.     return this._scrollElement === window
  186.       ? this._scrollElement.pageYOffset : this._scrollElement.scrollTop
  187.   }
  188.  
  189.   _getScrollHeight() {
  190.     return this._scrollElement.scrollHeight || Math.max(
  191.       document.body.scrollHeight,
  192.       document.documentElement.scrollHeight
  193.     )
  194.   }
  195.  
  196.   _getOffsetHeight() {
  197.     return this._scrollElement === window
  198.       ? window.innerHeight : this._scrollElement.getBoundingClientRect().height
  199.   }
  200.  
  201.   _process() {
  202.     const scrollTop    = this._getScrollTop() + this._config.offset
  203.     const scrollHeight = this._getScrollHeight()
  204.     const maxScroll    = this._config.offset +
  205.       scrollHeight -
  206.       this._getOffsetHeight()
  207.  
  208.     if (this._scrollHeight !== scrollHeight) {
  209.       this.refresh()
  210.     }
  211.  
  212.     if (scrollTop >= maxScroll) {
  213.       const target = this._targets[this._targets.length - 1]
  214.  
  215.       if (this._activeTarget !== target) {
  216.         this._activate(target)
  217.       }
  218.       return
  219.     }
  220.  
  221.     if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) {
  222.       this._activeTarget = null
  223.       this._clear()
  224.       return
  225.     }
  226.  
  227.     const offsetLength = this._offsets.length
  228.     for (let i = offsetLength; i--;) {
  229.       const isActiveTarget = this._activeTarget !== this._targets[i] &&
  230.           scrollTop >= this._offsets[i] &&
  231.           (typeof this._offsets[i + 1] === 'undefined' ||
  232.               scrollTop < this._offsets[i + 1])
  233.  
  234.       if (isActiveTarget) {
  235.         this._activate(this._targets[i])
  236.       }
  237.     }
  238.   }
  239.  
  240.   _activate(target) {
  241.     this._activeTarget = target
  242.  
  243.     this._clear()
  244.  
  245.     const queries = this._selector
  246.       .split(',')
  247.       .map((selector) => `${selector}[data-target="${target}"],${selector}[href="${target}"]`)
  248.  
  249.     const $link = $([].slice.call(document.querySelectorAll(queries.join(','))))
  250.  
  251.     if ($link.hasClass(ClassName.DROPDOWN_ITEM)) {
  252.       $link.closest(Selector.DROPDOWN).find(Selector.DROPDOWN_TOGGLE).addClass(ClassName.ACTIVE)
  253.       $link.addClass(ClassName.ACTIVE)
  254.     } else {
  255.       // Set triggered link as active
  256.       $link.addClass(ClassName.ACTIVE)
  257.       // Set triggered links parents as active
  258.       // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor
  259.       $link.parents(Selector.NAV_LIST_GROUP).prev(`${Selector.NAV_LINKS}, ${Selector.LIST_ITEMS}`).addClass(ClassName.ACTIVE)
  260.       // Handle special case when .nav-link is inside .nav-item
  261.       $link.parents(Selector.NAV_LIST_GROUP).prev(Selector.NAV_ITEMS).children(Selector.NAV_LINKS).addClass(ClassName.ACTIVE)
  262.     }
  263.  
  264.     $(this._scrollElement).trigger(Event.ACTIVATE, {
  265.       relatedTarget: target
  266.     })
  267.   }
  268.  
  269.   _clear() {
  270.     [].slice.call(document.querySelectorAll(this._selector))
  271.       .filter((node) => node.classList.contains(ClassName.ACTIVE))
  272.       .forEach((node) => node.classList.remove(ClassName.ACTIVE))
  273.   }
  274.  
  275.   // Static
  276.  
  277.   static _jQueryInterface(config) {
  278.     return this.each(function () {
  279.       let data = $(this).data(DATA_KEY)
  280.       const _config = typeof config === 'object' && config
  281.  
  282.       if (!data) {
  283.         data = new ScrollSpy(this, _config)
  284.         $(this).data(DATA_KEY, data)
  285.       }
  286.  
  287.       if (typeof config === 'string') {
  288.         if (typeof data[config] === 'undefined') {
  289.           throw new TypeError(`No method named "${config}"`)
  290.         }
  291.         data[config]()
  292.       }
  293.     })
  294.   }
  295. }
  296.  
  297. /**
  298.  * ------------------------------------------------------------------------
  299.  * Data Api implementation
  300.  * ------------------------------------------------------------------------
  301.  */
  302.  
  303. $(window).on(Event.LOAD_DATA_API, () => {
  304.   const scrollSpys = [].slice.call(document.querySelectorAll(Selector.DATA_SPY))
  305.   const scrollSpysLength = scrollSpys.length
  306.  
  307.   for (let i = scrollSpysLength; i--;) {
  308.     const $spy = $(scrollSpys[i])
  309.     ScrollSpy._jQueryInterface.call($spy, $spy.data())
  310.   }
  311. })
  312.  
  313. /**
  314.  * ------------------------------------------------------------------------
  315.  * jQuery
  316.  * ------------------------------------------------------------------------
  317.  */
  318.  
  319. $.fn[NAME] = ScrollSpy._jQueryInterface
  320. $.fn[NAME].Constructor = ScrollSpy
  321. $.fn[NAME].noConflict = () => {
  322.   $.fn[NAME] = JQUERY_NO_CONFLICT
  323.   return ScrollSpy._jQueryInterface
  324. }
  325.  
  326. export default ScrollSpy

Raw Paste


Login or Register to edit or fork this paste. It's free.