scrollspy.js 14 KB

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