diff options
Diffstat (limited to 'vendor/twbs/bootstrap/js/dist/scrollspy.js')
-rw-r--r-- | vendor/twbs/bootstrap/js/dist/scrollspy.js | 441 |
1 files changed, 179 insertions, 262 deletions
diff --git a/vendor/twbs/bootstrap/js/dist/scrollspy.js b/vendor/twbs/bootstrap/js/dist/scrollspy.js index 261c2658a..1cb5ee113 100644 --- a/vendor/twbs/bootstrap/js/dist/scrollspy.js +++ b/vendor/twbs/bootstrap/js/dist/scrollspy.js @@ -1,215 +1,80 @@ /*! - * Bootstrap scrollspy.js v5.1.3 (https://getbootstrap.com/) - * Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Bootstrap scrollspy.js v5.2.0 (https://getbootstrap.com/) + * Copyright 2011-2022 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ (function (global, factory) { - 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')) : - typeof define === 'function' && define.amd ? define(['./dom/event-handler', './dom/manipulator', './dom/selector-engine', './base-component'], factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.ScrollSpy = factory(global.EventHandler, global.Manipulator, global.SelectorEngine, global.Base)); -})(this, (function (EventHandler, Manipulator, SelectorEngine, BaseComponent) { 'use strict'; + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./util/index'), require('./dom/event-handler'), require('./dom/selector-engine'), require('./base-component')) : + typeof define === 'function' && define.amd ? define(['./util/index', './dom/event-handler', './dom/selector-engine', './base-component'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Scrollspy = factory(global.Index, global.EventHandler, global.SelectorEngine, global.BaseComponent)); +})(this, (function (index, EventHandler, SelectorEngine, BaseComponent) { 'use strict'; const _interopDefaultLegacy = e => e && typeof e === 'object' && 'default' in e ? e : { default: e }; const EventHandler__default = /*#__PURE__*/_interopDefaultLegacy(EventHandler); - const Manipulator__default = /*#__PURE__*/_interopDefaultLegacy(Manipulator); const SelectorEngine__default = /*#__PURE__*/_interopDefaultLegacy(SelectorEngine); const BaseComponent__default = /*#__PURE__*/_interopDefaultLegacy(BaseComponent); /** * -------------------------------------------------------------------------- - * Bootstrap (v5.1.3): util/index.js + * Bootstrap (v5.2.0): scrollspy.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ - - const toType = obj => { - if (obj === null || obj === undefined) { - return `${obj}`; - } - - return {}.toString.call(obj).match(/\s([a-z]+)/i)[1].toLowerCase(); - }; - - const getSelector = element => { - let selector = element.getAttribute('data-bs-target'); - - if (!selector || selector === '#') { - let hrefAttr = element.getAttribute('href'); // The only valid content that could double as a selector are IDs or classes, - // so everything starting with `#` or `.`. If a "real" URL is used as the selector, - // `document.querySelector` will rightfully complain it is invalid. - // See https://github.com/twbs/bootstrap/issues/32273 - - if (!hrefAttr || !hrefAttr.includes('#') && !hrefAttr.startsWith('.')) { - return null; - } // Just in case some CMS puts out a full URL with the anchor appended - - - if (hrefAttr.includes('#') && !hrefAttr.startsWith('#')) { - hrefAttr = `#${hrefAttr.split('#')[1]}`; - } - - selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : null; - } - - return selector; - }; - - const getSelectorFromElement = element => { - const selector = getSelector(element); - - if (selector) { - return document.querySelector(selector) ? selector : null; - } - - return null; - }; - - const isElement = obj => { - if (!obj || typeof obj !== 'object') { - return false; - } - - if (typeof obj.jquery !== 'undefined') { - obj = obj[0]; - } - - return typeof obj.nodeType !== 'undefined'; - }; - - const getElement = obj => { - if (isElement(obj)) { - // it's a jQuery object or a node element - return obj.jquery ? obj[0] : obj; - } - - if (typeof obj === 'string' && obj.length > 0) { - return document.querySelector(obj); - } - - return null; - }; - - const typeCheckConfig = (componentName, config, configTypes) => { - Object.keys(configTypes).forEach(property => { - const expectedTypes = configTypes[property]; - const value = config[property]; - const valueType = value && isElement(value) ? 'element' : toType(value); - - if (!new RegExp(expectedTypes).test(valueType)) { - throw new TypeError(`${componentName.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`); - } - }); - }; - - const getjQuery = () => { - const { - jQuery - } = window; - - if (jQuery && !document.body.hasAttribute('data-bs-no-jquery')) { - return jQuery; - } - - return null; - }; - - const DOMContentLoadedCallbacks = []; - - const onDOMContentLoaded = callback => { - if (document.readyState === 'loading') { - // add listener on the first call when the document is in loading state - if (!DOMContentLoadedCallbacks.length) { - document.addEventListener('DOMContentLoaded', () => { - DOMContentLoadedCallbacks.forEach(callback => callback()); - }); - } - - DOMContentLoadedCallbacks.push(callback); - } else { - callback(); - } - }; - - const defineJQueryPlugin = plugin => { - onDOMContentLoaded(() => { - const $ = getjQuery(); - /* istanbul ignore if */ - - if ($) { - const name = plugin.NAME; - const JQUERY_NO_CONFLICT = $.fn[name]; - $.fn[name] = plugin.jQueryInterface; - $.fn[name].Constructor = plugin; - - $.fn[name].noConflict = () => { - $.fn[name] = JQUERY_NO_CONFLICT; - return plugin.jQueryInterface; - }; - } - }); - }; - /** - * -------------------------------------------------------------------------- - * Bootstrap (v5.1.3): scrollspy.js - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - * -------------------------------------------------------------------------- - */ - /** - * ------------------------------------------------------------------------ * Constants - * ------------------------------------------------------------------------ */ const NAME = 'scrollspy'; const DATA_KEY = 'bs.scrollspy'; const EVENT_KEY = `.${DATA_KEY}`; const DATA_API_KEY = '.data-api'; - const Default = { - offset: 10, - method: 'auto', - target: '' - }; - const DefaultType = { - offset: 'number', - method: 'string', - target: '(string|element)' - }; const EVENT_ACTIVATE = `activate${EVENT_KEY}`; - const EVENT_SCROLL = `scroll${EVENT_KEY}`; + const EVENT_CLICK = `click${EVENT_KEY}`; const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`; const CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item'; const CLASS_NAME_ACTIVE = 'active'; const SELECTOR_DATA_SPY = '[data-bs-spy="scroll"]'; + const SELECTOR_TARGET_LINKS = '[href]'; const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'; const SELECTOR_NAV_LINKS = '.nav-link'; const SELECTOR_NAV_ITEMS = '.nav-item'; const SELECTOR_LIST_ITEMS = '.list-group-item'; - const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}, .${CLASS_NAME_DROPDOWN_ITEM}`; + const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`; const SELECTOR_DROPDOWN = '.dropdown'; const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'; - const METHOD_OFFSET = 'offset'; - const METHOD_POSITION = 'position'; + const Default = { + offset: null, + // TODO: v6 @deprecated, keep it for backwards compatibility reasons + rootMargin: '0px 0px -25%', + smoothScroll: false, + target: null + }; + const DefaultType = { + offset: '(number|null)', + // TODO v6 @deprecated, keep it for backwards compatibility reasons + rootMargin: 'string', + smoothScroll: 'boolean', + target: 'element' + }; /** - * ------------------------------------------------------------------------ - * Class Definition - * ------------------------------------------------------------------------ + * Class definition */ class ScrollSpy extends BaseComponent__default.default { constructor(element, config) { - super(element); - this._scrollElement = this._element.tagName === 'BODY' ? window : this._element; - this._config = this._getConfig(config); - this._offsets = []; - this._targets = []; - this._activeTarget = null; - this._scrollHeight = 0; - EventHandler__default.default.on(this._scrollElement, EVENT_SCROLL, () => this._process()); - this.refresh(); + super(element, config); // this._element is the observablesContainer and config.target the menu links wrapper - this._process(); + this._targetLinks = new Map(); + this._observableSections = new Map(); + this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element; + this._activeTarget = null; + this._observer = null; + this._previousScrollData = { + visibleEntryTop: 0, + parentScrollTop: 0 + }; + this.refresh(); // initialize } // Getters @@ -217,135 +82,190 @@ return Default; } + static get DefaultType() { + return DefaultType; + } + static get NAME() { return NAME; } // Public refresh() { - const autoMethod = this._scrollElement === this._scrollElement.window ? METHOD_OFFSET : METHOD_POSITION; - const offsetMethod = this._config.method === 'auto' ? autoMethod : this._config.method; - const offsetBase = offsetMethod === METHOD_POSITION ? this._getScrollTop() : 0; - this._offsets = []; - this._targets = []; - this._scrollHeight = this._getScrollHeight(); - const targets = SelectorEngine__default.default.find(SELECTOR_LINK_ITEMS, this._config.target); - targets.map(element => { - const targetSelector = getSelectorFromElement(element); - const target = targetSelector ? SelectorEngine__default.default.findOne(targetSelector) : null; - - if (target) { - const targetBCR = target.getBoundingClientRect(); - - if (targetBCR.width || targetBCR.height) { - return [Manipulator__default.default[offsetMethod](target).top + offsetBase, targetSelector]; - } - } + this._initializeTargetsAndObservables(); - return null; - }).filter(item => item).sort((a, b) => a[0] - b[0]).forEach(item => { - this._offsets.push(item[0]); + this._maybeEnableSmoothScroll(); - this._targets.push(item[1]); - }); + if (this._observer) { + this._observer.disconnect(); + } else { + this._observer = this._getNewObserver(); + } + + for (const section of this._observableSections.values()) { + this._observer.observe(section); + } } dispose() { - EventHandler__default.default.off(this._scrollElement, EVENT_KEY); + this._observer.disconnect(); + super.dispose(); } // Private - _getConfig(config) { - config = { ...Default, - ...Manipulator__default.default.getDataAttributes(this._element), - ...(typeof config === 'object' && config ? config : {}) - }; - config.target = getElement(config.target) || document.documentElement; - typeCheckConfig(NAME, config, DefaultType); + _configAfterMerge(config) { + // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case + config.target = index.getElement(config.target) || document.body; return config; } - _getScrollTop() { - return this._scrollElement === window ? this._scrollElement.pageYOffset : this._scrollElement.scrollTop; - } + _maybeEnableSmoothScroll() { + if (!this._config.smoothScroll) { + return; + } // unregister any previous listeners - _getScrollHeight() { - return this._scrollElement.scrollHeight || Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); - } - _getOffsetHeight() { - return this._scrollElement === window ? window.innerHeight : this._scrollElement.getBoundingClientRect().height; + EventHandler__default.default.off(this._config.target, EVENT_CLICK); + EventHandler__default.default.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => { + const observableSection = this._observableSections.get(event.target.hash); + + if (observableSection) { + event.preventDefault(); + const root = this._rootElement || window; + const height = observableSection.offsetTop - this._element.offsetTop; + + if (root.scrollTo) { + root.scrollTo({ + top: height, + behavior: 'smooth' + }); + return; + } // Chrome 60 doesn't support `scrollTo` + + + root.scrollTop = height; + } + }); } - _process() { - const scrollTop = this._getScrollTop() + this._config.offset; + _getNewObserver() { + const options = { + root: this._rootElement, + threshold: [0.1, 0.5, 1], + rootMargin: this._getRootMargin() + }; + return new IntersectionObserver(entries => this._observerCallback(entries), options); + } // The logic of selection + - const scrollHeight = this._getScrollHeight(); + _observerCallback(entries) { + const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`); - const maxScroll = this._config.offset + scrollHeight - this._getOffsetHeight(); + const activate = entry => { + this._previousScrollData.visibleEntryTop = entry.target.offsetTop; - if (this._scrollHeight !== scrollHeight) { - this.refresh(); - } + this._process(targetElement(entry)); + }; + + const parentScrollTop = (this._rootElement || document.documentElement).scrollTop; + const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop; + this._previousScrollData.parentScrollTop = parentScrollTop; + + for (const entry of entries) { + if (!entry.isIntersecting) { + this._activeTarget = null; - if (scrollTop >= maxScroll) { - const target = this._targets[this._targets.length - 1]; + this._clearActiveClass(targetElement(entry)); - if (this._activeTarget !== target) { - this._activate(target); + continue; } - return; - } + const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop; // if we are scrolling down, pick the bigger offsetTop - if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) { - this._activeTarget = null; + if (userScrollsDown && entryIsLowerThanPrevious) { + activate(entry); // if parent isn't scrolled, let's keep the first visible item, breaking the iteration - this._clear(); + if (!parentScrollTop) { + return; + } - return; - } + continue; + } // if we are scrolling up, pick the smallest offsetTop - for (let i = this._offsets.length; i--;) { - const isActiveTarget = this._activeTarget !== this._targets[i] && scrollTop >= this._offsets[i] && (typeof this._offsets[i + 1] === 'undefined' || scrollTop < this._offsets[i + 1]); - if (isActiveTarget) { - this._activate(this._targets[i]); + if (!userScrollsDown && !entryIsLowerThanPrevious) { + activate(entry); } } + } // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only + + + _getRootMargin() { + return this._config.offset ? `${this._config.offset}px 0px -30%` : this._config.rootMargin; } - _activate(target) { - this._activeTarget = target; + _initializeTargetsAndObservables() { + this._targetLinks = new Map(); + this._observableSections = new Map(); + const targetLinks = SelectorEngine__default.default.find(SELECTOR_TARGET_LINKS, this._config.target); - this._clear(); + for (const anchor of targetLinks) { + // ensure that the anchor has an id and is not disabled + if (!anchor.hash || index.isDisabled(anchor)) { + continue; + } - const queries = SELECTOR_LINK_ITEMS.split(',').map(selector => `${selector}[data-bs-target="${target}"],${selector}[href="${target}"]`); - const link = SelectorEngine__default.default.findOne(queries.join(','), this._config.target); - link.classList.add(CLASS_NAME_ACTIVE); + const observableSection = SelectorEngine__default.default.findOne(anchor.hash, this._element); // ensure that the observableSection exists & is visible - if (link.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) { - SelectorEngine__default.default.findOne(SELECTOR_DROPDOWN_TOGGLE, link.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE); - } else { - SelectorEngine__default.default.parents(link, SELECTOR_NAV_LIST_GROUP).forEach(listGroup => { - // Set triggered links parents as active - // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor - 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 - - SelectorEngine__default.default.prev(listGroup, SELECTOR_NAV_ITEMS).forEach(navItem => { - SelectorEngine__default.default.children(navItem, SELECTOR_NAV_LINKS).forEach(item => item.classList.add(CLASS_NAME_ACTIVE)); - }); - }); + if (index.isVisible(observableSection)) { + this._targetLinks.set(anchor.hash, anchor); + + this._observableSections.set(anchor.hash, observableSection); + } + } + } + + _process(target) { + if (this._activeTarget === target) { + return; } - EventHandler__default.default.trigger(this._scrollElement, EVENT_ACTIVATE, { + this._clearActiveClass(this._config.target); + + this._activeTarget = target; + target.classList.add(CLASS_NAME_ACTIVE); + + this._activateParents(target); + + EventHandler__default.default.trigger(this._element, EVENT_ACTIVATE, { relatedTarget: target }); } - _clear() { - 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)); + _activateParents(target) { + // Activate dropdown parents + if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) { + SelectorEngine__default.default.findOne(SELECTOR_DROPDOWN_TOGGLE, target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE); + return; + } + + for (const listGroup of SelectorEngine__default.default.parents(target, SELECTOR_NAV_LIST_GROUP)) { + // Set triggered links parents as active + // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor + for (const item of SelectorEngine__default.default.prev(listGroup, SELECTOR_LINK_ITEMS)) { + item.classList.add(CLASS_NAME_ACTIVE); + } + } + } + + _clearActiveClass(parent) { + parent.classList.remove(CLASS_NAME_ACTIVE); + const activeNodes = SelectorEngine__default.default.find(`${SELECTOR_TARGET_LINKS}.${CLASS_NAME_ACTIVE}`, parent); + + for (const node of activeNodes) { + node.classList.remove(CLASS_NAME_ACTIVE); + } } // Static @@ -357,7 +277,7 @@ return; } - if (typeof data[config] === 'undefined') { + if (data[config] === undefined || config.startsWith('_') || config === 'constructor') { throw new TypeError(`No method named "${config}"`); } @@ -367,23 +287,20 @@ } /** - * ------------------------------------------------------------------------ - * Data Api implementation - * ------------------------------------------------------------------------ + * Data API implementation */ EventHandler__default.default.on(window, EVENT_LOAD_DATA_API, () => { - SelectorEngine__default.default.find(SELECTOR_DATA_SPY).forEach(spy => new ScrollSpy(spy)); + for (const spy of SelectorEngine__default.default.find(SELECTOR_DATA_SPY)) { + ScrollSpy.getOrCreateInstance(spy); + } }); /** - * ------------------------------------------------------------------------ * jQuery - * ------------------------------------------------------------------------ - * add .ScrollSpy to jQuery only if jQuery is present */ - defineJQueryPlugin(ScrollSpy); + index.defineJQueryPlugin(ScrollSpy); return ScrollSpy; |