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.

391 lines
13 KiB

  1. /*!
  2. * Bootstrap scrollspy.js v5.1.3 (https://getbootstrap.com/)
  3. * Copyright 2011-2021 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('./dom/event-handler.js'), require('./dom/manipulator.js'), require('./dom/selector-engine.js'), require('./base-component.js')) :
  8. typeof define === 'function' && define.amd ? define(['./dom/event-handler', './dom/manipulator', './dom/selector-engine', './base-component'], factory) :
  9. (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.ScrollSpy = factory(global.EventHandler, global.Manipulator, global.SelectorEngine, global.Base));
  10. })(this, (function (EventHandler, Manipulator, SelectorEngine, BaseComponent) { 'use strict';
  11. const _interopDefaultLegacy = e => e && typeof e === 'object' && 'default' in e ? e : { default: e };
  12. const EventHandler__default = /*#__PURE__*/_interopDefaultLegacy(EventHandler);
  13. const Manipulator__default = /*#__PURE__*/_interopDefaultLegacy(Manipulator);
  14. const SelectorEngine__default = /*#__PURE__*/_interopDefaultLegacy(SelectorEngine);
  15. const BaseComponent__default = /*#__PURE__*/_interopDefaultLegacy(BaseComponent);
  16. /**
  17. * --------------------------------------------------------------------------
  18. * Bootstrap (v5.1.3): util/index.js
  19. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
  20. * --------------------------------------------------------------------------
  21. */
  22. const toType = obj => {
  23. if (obj === null || obj === undefined) {
  24. return `${obj}`;
  25. }
  26. return {}.toString.call(obj).match(/\s([a-z]+)/i)[1].toLowerCase();
  27. };
  28. const getSelector = element => {
  29. let selector = element.getAttribute('data-bs-target');
  30. if (!selector || selector === '#') {
  31. let hrefAttr = element.getAttribute('href'); // The only valid content that could double as a selector are IDs or classes,
  32. // so everything starting with `#` or `.`. If a "real" URL is used as the selector,
  33. // `document.querySelector` will rightfully complain it is invalid.
  34. // See https://github.com/twbs/bootstrap/issues/32273
  35. if (!hrefAttr || !hrefAttr.includes('#') && !hrefAttr.startsWith('.')) {
  36. return null;
  37. } // Just in case some CMS puts out a full URL with the anchor appended
  38. if (hrefAttr.includes('#') && !hrefAttr.startsWith('#')) {
  39. hrefAttr = `#${hrefAttr.split('#')[1]}`;
  40. }
  41. selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : null;
  42. }
  43. return selector;
  44. };
  45. const getSelectorFromElement = element => {
  46. const selector = getSelector(element);
  47. if (selector) {
  48. return document.querySelector(selector) ? selector : null;
  49. }
  50. return null;
  51. };
  52. const isElement = obj => {
  53. if (!obj || typeof obj !== 'object') {
  54. return false;
  55. }
  56. if (typeof obj.jquery !== 'undefined') {
  57. obj = obj[0];
  58. }
  59. return typeof obj.nodeType !== 'undefined';
  60. };
  61. const getElement = obj => {
  62. if (isElement(obj)) {
  63. // it's a jQuery object or a node element
  64. return obj.jquery ? obj[0] : obj;
  65. }
  66. if (typeof obj === 'string' && obj.length > 0) {
  67. return document.querySelector(obj);
  68. }
  69. return null;
  70. };
  71. const typeCheckConfig = (componentName, config, configTypes) => {
  72. Object.keys(configTypes).forEach(property => {
  73. const expectedTypes = configTypes[property];
  74. const value = config[property];
  75. const valueType = value && isElement(value) ? 'element' : toType(value);
  76. if (!new RegExp(expectedTypes).test(valueType)) {
  77. throw new TypeError(`${componentName.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`);
  78. }
  79. });
  80. };
  81. const getjQuery = () => {
  82. const {
  83. jQuery
  84. } = window;
  85. if (jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {
  86. return jQuery;
  87. }
  88. return null;
  89. };
  90. const DOMContentLoadedCallbacks = [];
  91. const onDOMContentLoaded = callback => {
  92. if (document.readyState === 'loading') {
  93. // add listener on the first call when the document is in loading state
  94. if (!DOMContentLoadedCallbacks.length) {
  95. document.addEventListener('DOMContentLoaded', () => {
  96. DOMContentLoadedCallbacks.forEach(callback => callback());
  97. });
  98. }
  99. DOMContentLoadedCallbacks.push(callback);
  100. } else {
  101. callback();
  102. }
  103. };
  104. const defineJQueryPlugin = plugin => {
  105. onDOMContentLoaded(() => {
  106. const $ = getjQuery();
  107. /* istanbul ignore if */
  108. if ($) {
  109. const name = plugin.NAME;
  110. const JQUERY_NO_CONFLICT = $.fn[name];
  111. $.fn[name] = plugin.jQueryInterface;
  112. $.fn[name].Constructor = plugin;
  113. $.fn[name].noConflict = () => {
  114. $.fn[name] = JQUERY_NO_CONFLICT;
  115. return plugin.jQueryInterface;
  116. };
  117. }
  118. });
  119. };
  120. /**
  121. * --------------------------------------------------------------------------
  122. * Bootstrap (v5.1.3): scrollspy.js
  123. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
  124. * --------------------------------------------------------------------------
  125. */
  126. /**
  127. * ------------------------------------------------------------------------
  128. * Constants
  129. * ------------------------------------------------------------------------
  130. */
  131. const NAME = 'scrollspy';
  132. const DATA_KEY = 'bs.scrollspy';
  133. const EVENT_KEY = `.${DATA_KEY}`;
  134. const DATA_API_KEY = '.data-api';
  135. const Default = {
  136. offset: 10,
  137. method: 'auto',
  138. target: ''
  139. };
  140. const DefaultType = {
  141. offset: 'number',
  142. method: 'string',
  143. target: '(string|element)'
  144. };
  145. const EVENT_ACTIVATE = `activate${EVENT_KEY}`;
  146. const EVENT_SCROLL = `scroll${EVENT_KEY}`;
  147. const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`;
  148. const CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item';
  149. const CLASS_NAME_ACTIVE = 'active';
  150. const SELECTOR_DATA_SPY = '[data-bs-spy="scroll"]';
  151. const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group';
  152. const SELECTOR_NAV_LINKS = '.nav-link';
  153. const SELECTOR_NAV_ITEMS = '.nav-item';
  154. const SELECTOR_LIST_ITEMS = '.list-group-item';
  155. const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}, .${CLASS_NAME_DROPDOWN_ITEM}`;
  156. const SELECTOR_DROPDOWN = '.dropdown';
  157. const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle';
  158. const METHOD_OFFSET = 'offset';
  159. const METHOD_POSITION = 'position';
  160. /**
  161. * ------------------------------------------------------------------------
  162. * Class Definition
  163. * ------------------------------------------------------------------------
  164. */
  165. class ScrollSpy extends BaseComponent__default.default {
  166. constructor(element, config) {
  167. super(element);
  168. this._scrollElement = this._element.tagName === 'BODY' ? window : this._element;
  169. this._config = this._getConfig(config);
  170. this._offsets = [];
  171. this._targets = [];
  172. this._activeTarget = null;
  173. this._scrollHeight = 0;
  174. EventHandler__default.default.on(this._scrollElement, EVENT_SCROLL, () => this._process());
  175. this.refresh();
  176. this._process();
  177. } // Getters
  178. static get Default() {
  179. return Default;
  180. }
  181. static get NAME() {
  182. return NAME;
  183. } // Public
  184. refresh() {
  185. const autoMethod = this._scrollElement === this._scrollElement.window ? METHOD_OFFSET : METHOD_POSITION;
  186. const offsetMethod = this._config.method === 'auto' ? autoMethod : this._config.method;
  187. const offsetBase = offsetMethod === METHOD_POSITION ? this._getScrollTop() : 0;
  188. this._offsets = [];
  189. this._targets = [];
  190. this._scrollHeight = this._getScrollHeight();
  191. const targets = SelectorEngine__default.default.find(SELECTOR_LINK_ITEMS, this._config.target);
  192. targets.map(element => {
  193. const targetSelector = getSelectorFromElement(element);
  194. const target = targetSelector ? SelectorEngine__default.default.findOne(targetSelector) : null;
  195. if (target) {
  196. const targetBCR = target.getBoundingClientRect();
  197. if (targetBCR.width || targetBCR.height) {
  198. return [Manipulator__default.default[offsetMethod](target).top + offsetBase, targetSelector];
  199. }
  200. }
  201. return null;
  202. }).filter(item => item).sort((a, b) => a[0] - b[0]).forEach(item => {
  203. this._offsets.push(item[0]);
  204. this._targets.push(item[1]);
  205. });
  206. }
  207. dispose() {
  208. EventHandler__default.default.off(this._scrollElement, EVENT_KEY);
  209. super.dispose();
  210. } // Private
  211. _getConfig(config) {
  212. config = { ...Default,
  213. ...Manipulator__default.default.getDataAttributes(this._element),
  214. ...(typeof config === 'object' && config ? config : {})
  215. };
  216. config.target = getElement(config.target) || document.documentElement;
  217. typeCheckConfig(NAME, config, DefaultType);
  218. return config;
  219. }
  220. _getScrollTop() {
  221. return this._scrollElement === window ? this._scrollElement.pageYOffset : this._scrollElement.scrollTop;
  222. }
  223. _getScrollHeight() {
  224. return this._scrollElement.scrollHeight || Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
  225. }
  226. _getOffsetHeight() {
  227. return this._scrollElement === window ? window.innerHeight : this._scrollElement.getBoundingClientRect().height;
  228. }
  229. _process() {
  230. const scrollTop = this._getScrollTop() + this._config.offset;
  231. const scrollHeight = this._getScrollHeight();
  232. const maxScroll = this._config.offset + scrollHeight - this._getOffsetHeight();
  233. if (this._scrollHeight !== scrollHeight) {
  234. this.refresh();
  235. }
  236. if (scrollTop >= maxScroll) {
  237. const target = this._targets[this._targets.length - 1];
  238. if (this._activeTarget !== target) {
  239. this._activate(target);
  240. }
  241. return;
  242. }
  243. if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) {
  244. this._activeTarget = null;
  245. this._clear();
  246. return;
  247. }
  248. for (let i = this._offsets.length; i--;) {
  249. const isActiveTarget = this._activeTarget !== this._targets[i] && scrollTop >= this._offsets[i] && (typeof this._offsets[i + 1] === 'undefined' || scrollTop < this._offsets[i + 1]);
  250. if (isActiveTarget) {
  251. this._activate(this._targets[i]);
  252. }
  253. }
  254. }
  255. _activate(target) {
  256. this._activeTarget = target;
  257. this._clear();
  258. const queries = SELECTOR_LINK_ITEMS.split(',').map(selector => `${selector}[data-bs-target="${target}"],${selector}[href="${target}"]`);
  259. const link = SelectorEngine__default.default.findOne(queries.join(','), this._config.target);
  260. link.classList.add(CLASS_NAME_ACTIVE);
  261. if (link.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {
  262. SelectorEngine__default.default.findOne(SELECTOR_DROPDOWN_TOGGLE, link.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE);
  263. } else {
  264. SelectorEngine__default.default.parents(link, SELECTOR_NAV_LIST_GROUP).forEach(listGroup => {
  265. // Set triggered links parents as active
  266. // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor
  267. SelectorEngine__default.default.prev(listGroup, `${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`).forEach(item => item.classList.add(CLASS_NAME_ACTIVE)); // Handle special case when .nav-link is inside .nav-item
  268. SelectorEngine__default.default.prev(listGroup, SELECTOR_NAV_ITEMS).forEach(navItem => {
  269. SelectorEngine__default.default.children(navItem, SELECTOR_NAV_LINKS).forEach(item => item.classList.add(CLASS_NAME_ACTIVE));
  270. });
  271. });
  272. }
  273. EventHandler__default.default.trigger(this._scrollElement, EVENT_ACTIVATE, {
  274. relatedTarget: target
  275. });
  276. }
  277. _clear() {
  278. SelectorEngine__default.default.find(SELECTOR_LINK_ITEMS, this._config.target).filter(node => node.classList.contains(CLASS_NAME_ACTIVE)).forEach(node => node.classList.remove(CLASS_NAME_ACTIVE));
  279. } // Static
  280. static jQueryInterface(config) {
  281. return this.each(function () {
  282. const data = ScrollSpy.getOrCreateInstance(this, config);
  283. if (typeof config !== 'string') {
  284. return;
  285. }
  286. if (typeof data[config] === 'undefined') {
  287. throw new TypeError(`No method named "${config}"`);
  288. }
  289. data[config]();
  290. });
  291. }
  292. }
  293. /**
  294. * ------------------------------------------------------------------------
  295. * Data Api implementation
  296. * ------------------------------------------------------------------------
  297. */
  298. EventHandler__default.default.on(window, EVENT_LOAD_DATA_API, () => {
  299. SelectorEngine__default.default.find(SELECTOR_DATA_SPY).forEach(spy => new ScrollSpy(spy));
  300. });
  301. /**
  302. * ------------------------------------------------------------------------
  303. * jQuery
  304. * ------------------------------------------------------------------------
  305. * add .ScrollSpy to jQuery only if jQuery is present
  306. */
  307. defineJQueryPlugin(ScrollSpy);
  308. return ScrollSpy;
  309. }));
  310. //# sourceMappingURL=scrollspy.js.map