scrollspy.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. import Util from './util'
  2. /**
  3. * --------------------------------------------------------------------------
  4. * Bootstrap (v4.0.0-alpha.4): scrollspy.js
  5. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  6. * --------------------------------------------------------------------------
  7. */
  8. const ScrollSpy = (($) => {
  9. /**
  10. * ------------------------------------------------------------------------
  11. * Constants
  12. * ------------------------------------------------------------------------
  13. */
  14. const NAME = 'scrollspy'
  15. const VERSION = '4.0.0-alpha.4'
  16. const DATA_KEY = 'bs.scrollspy'
  17. const EVENT_KEY = `.${DATA_KEY}`
  18. const DATA_API_KEY = '.data-api'
  19. const JQUERY_NO_CONFLICT = $.fn[NAME]
  20. const Default = {
  21. offset : 10,
  22. method : 'auto',
  23. target : ''
  24. }
  25. const DefaultType = {
  26. offset : 'number',
  27. method : 'string',
  28. target : '(string|element)'
  29. }
  30. const Event = {
  31. ACTIVATE : `activate${EVENT_KEY}`,
  32. SCROLL : `scroll${EVENT_KEY}`,
  33. LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`
  34. }
  35. const ClassName = {
  36. DROPDOWN_ITEM : 'dropdown-item',
  37. DROPDOWN_MENU : 'dropdown-menu',
  38. NAV_LINK : 'nav-link',
  39. NAV : 'nav',
  40. ACTIVE : 'active'
  41. }
  42. const Selector = {
  43. DATA_SPY : '[data-spy="scroll"]',
  44. ACTIVE : '.active',
  45. LIST_ITEM : '.list-item',
  46. LI : 'li',
  47. LI_DROPDOWN : 'li.dropdown',
  48. NAV_LINKS : '.nav-link',
  49. DROPDOWN : '.dropdown',
  50. DROPDOWN_ITEMS : '.dropdown-item',
  51. DROPDOWN_TOGGLE : '.dropdown-toggle'
  52. }
  53. const OffsetMethod = {
  54. OFFSET : 'offset',
  55. POSITION : 'position'
  56. }
  57. /**
  58. * ------------------------------------------------------------------------
  59. * Class Definition
  60. * ------------------------------------------------------------------------
  61. */
  62. class ScrollSpy {
  63. constructor(element, config) {
  64. this._element = element
  65. this._scrollElement = element.tagName === 'BODY' ? window : element
  66. this._config = this._getConfig(config)
  67. this._selector = `${this._config.target} ${Selector.NAV_LINKS},`
  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, $.proxy(this._process, this))
  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. let autoMethod = this._scrollElement !== this._scrollElement.window ?
  87. OffsetMethod.POSITION : OffsetMethod.OFFSET
  88. let offsetMethod = this._config.method === 'auto' ?
  89. autoMethod : this._config.method
  90. let offsetBase = offsetMethod === OffsetMethod.POSITION ?
  91. this._getScrollTop() : 0
  92. this._offsets = []
  93. this._targets = []
  94. this._scrollHeight = this._getScrollHeight()
  95. let targets = $.makeArray($(this._selector))
  96. targets
  97. .map((element) => {
  98. let target
  99. let targetSelector = Util.getSelectorFromElement(element)
  100. if (targetSelector) {
  101. target = $(targetSelector)[0]
  102. }
  103. if (target && (target.offsetWidth || target.offsetHeight)) {
  104. // todo (fat): remove sketch reliance on jQuery position/offset
  105. return [
  106. $(target)[offsetMethod]().top + offsetBase,
  107. targetSelector
  108. ]
  109. }
  110. return null
  111. })
  112. .filter((item) => item)
  113. .sort((a, b) => a[0] - b[0])
  114. .forEach((item) => {
  115. this._offsets.push(item[0])
  116. this._targets.push(item[1])
  117. })
  118. }
  119. dispose() {
  120. $.removeData(this._element, DATA_KEY)
  121. $(this._scrollElement).off(EVENT_KEY)
  122. this._element = null
  123. this._scrollElement = null
  124. this._config = null
  125. this._selector = null
  126. this._offsets = null
  127. this._targets = null
  128. this._activeTarget = null
  129. this._scrollHeight = null
  130. }
  131. // private
  132. _getConfig(config) {
  133. config = $.extend({}, Default, config)
  134. if (typeof config.target !== 'string') {
  135. let id = $(config.target).attr('id')
  136. if (!id) {
  137. id = Util.getUID(NAME)
  138. $(config.target).attr('id', id)
  139. }
  140. config.target = `#${id}`
  141. }
  142. Util.typeCheckConfig(NAME, config, DefaultType)
  143. return config
  144. }
  145. _getScrollTop() {
  146. return this._scrollElement === window ?
  147. this._scrollElement.scrollY : this._scrollElement.scrollTop
  148. }
  149. _getScrollHeight() {
  150. return this._scrollElement.scrollHeight || Math.max(
  151. document.body.scrollHeight,
  152. document.documentElement.scrollHeight
  153. )
  154. }
  155. _process() {
  156. let scrollTop = this._getScrollTop() + this._config.offset
  157. let scrollHeight = this._getScrollHeight()
  158. let maxScroll = this._config.offset
  159. + scrollHeight
  160. - this._scrollElement.offsetHeight
  161. if (this._scrollHeight !== scrollHeight) {
  162. this.refresh()
  163. }
  164. if (scrollTop >= maxScroll) {
  165. let target = this._targets[this._targets.length - 1]
  166. if (this._activeTarget !== target) {
  167. this._activate(target)
  168. }
  169. }
  170. if (this._activeTarget && scrollTop < this._offsets[0]) {
  171. this._activeTarget = null
  172. this._clear()
  173. return
  174. }
  175. for (let i = this._offsets.length; i--;) {
  176. let isActiveTarget = this._activeTarget !== this._targets[i]
  177. && scrollTop >= this._offsets[i]
  178. && (this._offsets[i + 1] === undefined ||
  179. scrollTop < this._offsets[i + 1])
  180. if (isActiveTarget) {
  181. this._activate(this._targets[i])
  182. }
  183. }
  184. }
  185. _activate(target) {
  186. this._activeTarget = target
  187. this._clear()
  188. let queries = this._selector.split(',')
  189. queries = queries.map((selector) => {
  190. return `${selector}[data-target="${target}"],` +
  191. `${selector}[href="${target}"]`
  192. })
  193. let $link = $(queries.join(','))
  194. if ($link.hasClass(ClassName.DROPDOWN_ITEM)) {
  195. $link.closest(Selector.DROPDOWN).find(Selector.DROPDOWN_TOGGLE).addClass(ClassName.ACTIVE)
  196. $link.addClass(ClassName.ACTIVE)
  197. } else {
  198. // todo (fat) this is kinda sus...
  199. // recursively add actives to tested nav-links
  200. $link.parents(Selector.LI).find(Selector.NAV_LINKS).addClass(ClassName.ACTIVE)
  201. }
  202. $(this._scrollElement).trigger(Event.ACTIVATE, {
  203. relatedTarget: target
  204. })
  205. }
  206. _clear() {
  207. $(this._selector).filter(Selector.ACTIVE).removeClass(ClassName.ACTIVE)
  208. }
  209. // static
  210. static _jQueryInterface(config) {
  211. return this.each(function () {
  212. let data = $(this).data(DATA_KEY)
  213. let _config = typeof config === 'object' && config || null
  214. if (!data) {
  215. data = new ScrollSpy(this, _config)
  216. $(this).data(DATA_KEY, data)
  217. }
  218. if (typeof config === 'string') {
  219. if (data[config] === undefined) {
  220. throw new Error(`No method named "${config}"`)
  221. }
  222. data[config]()
  223. }
  224. })
  225. }
  226. }
  227. /**
  228. * ------------------------------------------------------------------------
  229. * Data Api implementation
  230. * ------------------------------------------------------------------------
  231. */
  232. $(window).on(Event.LOAD_DATA_API, () => {
  233. let scrollSpys = $.makeArray($(Selector.DATA_SPY))
  234. for (let i = scrollSpys.length; i--;) {
  235. let $spy = $(scrollSpys[i])
  236. ScrollSpy._jQueryInterface.call($spy, $spy.data())
  237. }
  238. })
  239. /**
  240. * ------------------------------------------------------------------------
  241. * jQuery
  242. * ------------------------------------------------------------------------
  243. */
  244. $.fn[NAME] = ScrollSpy._jQueryInterface
  245. $.fn[NAME].Constructor = ScrollSpy
  246. $.fn[NAME].noConflict = function () {
  247. $.fn[NAME] = JQUERY_NO_CONFLICT
  248. return ScrollSpy._jQueryInterface
  249. }
  250. return ScrollSpy
  251. })(jQuery)
  252. export default ScrollSpy