Layout von Websiten mit Bootstrap und Foundation
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

scrollspy.js 8.8KB

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