scrollspy.js 9.3 KB

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