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.

275 lines
9.6 KiB

5 months ago
  1. /*!
  2. * Bootstrap scrollspy.js v5.3.3 (https://getbootstrap.com/)
  3. * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
  4. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
  5. */
  6. (function (global, factory) {
  7. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/index.js')) :
  8. typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/index'], factory) :
  9. (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Scrollspy = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Index));
  10. })(this, (function (BaseComponent, EventHandler, SelectorEngine, index_js) { 'use strict';
  11. /**
  12. * --------------------------------------------------------------------------
  13. * Bootstrap scrollspy.js
  14. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
  15. * --------------------------------------------------------------------------
  16. */
  17. /**
  18. * Constants
  19. */
  20. const NAME = 'scrollspy';
  21. const DATA_KEY = 'bs.scrollspy';
  22. const EVENT_KEY = `.${DATA_KEY}`;
  23. const DATA_API_KEY = '.data-api';
  24. const EVENT_ACTIVATE = `activate${EVENT_KEY}`;
  25. const EVENT_CLICK = `click${EVENT_KEY}`;
  26. const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`;
  27. const CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item';
  28. const CLASS_NAME_ACTIVE = 'active';
  29. const SELECTOR_DATA_SPY = '[data-bs-spy="scroll"]';
  30. const SELECTOR_TARGET_LINKS = '[href]';
  31. const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group';
  32. const SELECTOR_NAV_LINKS = '.nav-link';
  33. const SELECTOR_NAV_ITEMS = '.nav-item';
  34. const SELECTOR_LIST_ITEMS = '.list-group-item';
  35. const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`;
  36. const SELECTOR_DROPDOWN = '.dropdown';
  37. const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle';
  38. const Default = {
  39. offset: null,
  40. // TODO: v6 @deprecated, keep it for backwards compatibility reasons
  41. rootMargin: '0px 0px -25%',
  42. smoothScroll: false,
  43. target: null,
  44. threshold: [0.1, 0.5, 1]
  45. };
  46. const DefaultType = {
  47. offset: '(number|null)',
  48. // TODO v6 @deprecated, keep it for backwards compatibility reasons
  49. rootMargin: 'string',
  50. smoothScroll: 'boolean',
  51. target: 'element',
  52. threshold: 'array'
  53. };
  54. /**
  55. * Class definition
  56. */
  57. class ScrollSpy extends BaseComponent {
  58. constructor(element, config) {
  59. super(element, config);
  60. // this._element is the observablesContainer and config.target the menu links wrapper
  61. this._targetLinks = new Map();
  62. this._observableSections = new Map();
  63. this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element;
  64. this._activeTarget = null;
  65. this._observer = null;
  66. this._previousScrollData = {
  67. visibleEntryTop: 0,
  68. parentScrollTop: 0
  69. };
  70. this.refresh(); // initialize
  71. }
  72. // Getters
  73. static get Default() {
  74. return Default;
  75. }
  76. static get DefaultType() {
  77. return DefaultType;
  78. }
  79. static get NAME() {
  80. return NAME;
  81. }
  82. // Public
  83. refresh() {
  84. this._initializeTargetsAndObservables();
  85. this._maybeEnableSmoothScroll();
  86. if (this._observer) {
  87. this._observer.disconnect();
  88. } else {
  89. this._observer = this._getNewObserver();
  90. }
  91. for (const section of this._observableSections.values()) {
  92. this._observer.observe(section);
  93. }
  94. }
  95. dispose() {
  96. this._observer.disconnect();
  97. super.dispose();
  98. }
  99. // Private
  100. _configAfterMerge(config) {
  101. // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case
  102. config.target = index_js.getElement(config.target) || document.body;
  103. // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only
  104. config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin;
  105. if (typeof config.threshold === 'string') {
  106. config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value));
  107. }
  108. return config;
  109. }
  110. _maybeEnableSmoothScroll() {
  111. if (!this._config.smoothScroll) {
  112. return;
  113. }
  114. // unregister any previous listeners
  115. EventHandler.off(this._config.target, EVENT_CLICK);
  116. EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {
  117. const observableSection = this._observableSections.get(event.target.hash);
  118. if (observableSection) {
  119. event.preventDefault();
  120. const root = this._rootElement || window;
  121. const height = observableSection.offsetTop - this._element.offsetTop;
  122. if (root.scrollTo) {
  123. root.scrollTo({
  124. top: height,
  125. behavior: 'smooth'
  126. });
  127. return;
  128. }
  129. // Chrome 60 doesn't support `scrollTo`
  130. root.scrollTop = height;
  131. }
  132. });
  133. }
  134. _getNewObserver() {
  135. const options = {
  136. root: this._rootElement,
  137. threshold: this._config.threshold,
  138. rootMargin: this._config.rootMargin
  139. };
  140. return new IntersectionObserver(entries => this._observerCallback(entries), options);
  141. }
  142. // The logic of selection
  143. _observerCallback(entries) {
  144. const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`);
  145. const activate = entry => {
  146. this._previousScrollData.visibleEntryTop = entry.target.offsetTop;
  147. this._process(targetElement(entry));
  148. };
  149. const parentScrollTop = (this._rootElement || document.documentElement).scrollTop;
  150. const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop;
  151. this._previousScrollData.parentScrollTop = parentScrollTop;
  152. for (const entry of entries) {
  153. if (!entry.isIntersecting) {
  154. this._activeTarget = null;
  155. this._clearActiveClass(targetElement(entry));
  156. continue;
  157. }
  158. const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop;
  159. // if we are scrolling down, pick the bigger offsetTop
  160. if (userScrollsDown && entryIsLowerThanPrevious) {
  161. activate(entry);
  162. // if parent isn't scrolled, let's keep the first visible item, breaking the iteration
  163. if (!parentScrollTop) {
  164. return;
  165. }
  166. continue;
  167. }
  168. // if we are scrolling up, pick the smallest offsetTop
  169. if (!userScrollsDown && !entryIsLowerThanPrevious) {
  170. activate(entry);
  171. }
  172. }
  173. }
  174. _initializeTargetsAndObservables() {
  175. this._targetLinks = new Map();
  176. this._observableSections = new Map();
  177. const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target);
  178. for (const anchor of targetLinks) {
  179. // ensure that the anchor has an id and is not disabled
  180. if (!anchor.hash || index_js.isDisabled(anchor)) {
  181. continue;
  182. }
  183. const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element);
  184. // ensure that the observableSection exists & is visible
  185. if (index_js.isVisible(observableSection)) {
  186. this._targetLinks.set(decodeURI(anchor.hash), anchor);
  187. this._observableSections.set(anchor.hash, observableSection);
  188. }
  189. }
  190. }
  191. _process(target) {
  192. if (this._activeTarget === target) {
  193. return;
  194. }
  195. this._clearActiveClass(this._config.target);
  196. this._activeTarget = target;
  197. target.classList.add(CLASS_NAME_ACTIVE);
  198. this._activateParents(target);
  199. EventHandler.trigger(this._element, EVENT_ACTIVATE, {
  200. relatedTarget: target
  201. });
  202. }
  203. _activateParents(target) {
  204. // Activate dropdown parents
  205. if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {
  206. SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE, target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE);
  207. return;
  208. }
  209. for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {
  210. // Set triggered links parents as active
  211. // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor
  212. for (const item of SelectorEngine.prev(listGroup, SELECTOR_LINK_ITEMS)) {
  213. item.classList.add(CLASS_NAME_ACTIVE);
  214. }
  215. }
  216. }
  217. _clearActiveClass(parent) {
  218. parent.classList.remove(CLASS_NAME_ACTIVE);
  219. const activeNodes = SelectorEngine.find(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE}`, parent);
  220. for (const node of activeNodes) {
  221. node.classList.remove(CLASS_NAME_ACTIVE);
  222. }
  223. }
  224. // Static
  225. static jQueryInterface(config) {
  226. return this.each(function () {
  227. const data = ScrollSpy.getOrCreateInstance(this, config);
  228. if (typeof config !== 'string') {
  229. return;
  230. }
  231. if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
  232. throw new TypeError(`No method named "${config}"`);
  233. }
  234. data[config]();
  235. });
  236. }
  237. }
  238. /**
  239. * Data API implementation
  240. */
  241. EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
  242. for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) {
  243. ScrollSpy.getOrCreateInstance(spy);
  244. }
  245. });
  246. /**
  247. * jQuery
  248. */
  249. index_js.defineJQueryPlugin(ScrollSpy);
  250. return ScrollSpy;
  251. }));
  252. //# sourceMappingURL=scrollspy.js.map