0
0
mirror of https://github.com/twbs/bootstrap.git synced 2024-12-12 00:08:59 +01:00
Bootstrap/js/dist/scrollspy.js

392 lines
13 KiB
JavaScript
Raw Normal View History

2018-11-13 07:41:12 +01:00
/*!
2021-10-05 17:50:18 +02:00
* Bootstrap scrollspy.js v5.1.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
2020-06-16 20:50:01 +02:00
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
2018-11-13 07:41:12 +01:00
*/
2018-07-24 02:51:14 +02:00
(function (global, factory) {
2021-08-04 17:41:51 +02:00
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));
2021-10-05 17:50:18 +02:00
})(this, (function (EventHandler, Manipulator, SelectorEngine, BaseComponent) { 'use strict';
2018-07-24 02:51:14 +02:00
2021-10-05 17:50:18 +02:00
const _interopDefaultLegacy = e => e && typeof e === 'object' && 'default' in e ? e : { default: e };
2020-09-14 17:12:06 +02:00
2021-10-05 17:50:18 +02:00
const EventHandler__default = /*#__PURE__*/_interopDefaultLegacy(EventHandler);
const Manipulator__default = /*#__PURE__*/_interopDefaultLegacy(Manipulator);
const SelectorEngine__default = /*#__PURE__*/_interopDefaultLegacy(SelectorEngine);
const BaseComponent__default = /*#__PURE__*/_interopDefaultLegacy(BaseComponent);
2019-03-01 17:31:34 +01:00
/**
* --------------------------------------------------------------------------
2021-10-05 17:50:18 +02:00
* Bootstrap (v5.1.2): util/index.js
2020-06-16 20:50:01 +02:00
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
2019-03-01 17:31:34 +01:00
* --------------------------------------------------------------------------
*/
2021-03-23 17:26:54 +01:00
const toType = obj => {
2020-03-28 11:29:08 +01:00
if (obj === null || obj === undefined) {
2021-03-23 17:26:54 +01:00
return `${obj}`;
2020-03-28 11:29:08 +01:00
}
2019-03-01 17:31:34 +01:00
return {}.toString.call(obj).match(/\s([a-z]+)/i)[1].toLowerCase();
};
2021-03-23 17:26:54 +01:00
const getSelector = element => {
let selector = element.getAttribute('data-bs-target');
2019-03-01 17:31:34 +01:00
if (!selector || selector === '#') {
2021-03-23 17:26:54 +01:00
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]}`;
}
2019-08-27 15:03:21 +02:00
selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : null;
2019-03-01 17:31:34 +01:00
}
2019-08-27 15:03:21 +02:00
return selector;
};
2021-03-23 17:26:54 +01:00
const getSelectorFromElement = element => {
const selector = getSelector(element);
2019-08-27 15:03:21 +02:00
if (selector) {
2019-03-01 17:31:34 +01:00
return document.querySelector(selector) ? selector : null;
}
2019-08-27 15:03:21 +02:00
return null;
2019-03-01 17:31:34 +01:00
};
const isElement = obj => {
if (!obj || typeof obj !== 'object') {
return false;
}
if (typeof obj.jquery !== 'undefined') {
obj = obj[0];
}
return typeof obj.nodeType !== 'undefined';
};
2019-03-01 17:31:34 +01:00
2021-08-04 17:41:51 +02:00
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;
};
2021-03-23 17:26:54 +01:00
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);
2019-03-01 17:31:34 +01:00
if (!new RegExp(expectedTypes).test(valueType)) {
throw new TypeError(`${componentName.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`);
2019-03-01 17:31:34 +01:00
}
});
};
2021-03-23 17:26:54 +01:00
const getjQuery = () => {
const {
jQuery
} = window;
2019-08-27 15:03:21 +02:00
2020-11-23 14:17:16 +01:00
if (jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {
2019-08-27 15:03:21 +02:00
return jQuery;
}
return null;
};
const DOMContentLoadedCallbacks = [];
2021-03-23 17:26:54 +01:00
const onDOMContentLoaded = callback => {
2020-11-11 18:07:37 +01:00
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);
2020-11-11 18:07:37 +01:00
} else {
callback();
}
};
const defineJQueryPlugin = plugin => {
2021-03-23 17:26:54 +01:00
onDOMContentLoaded(() => {
const $ = getjQuery();
/* istanbul ignore if */
2020-12-03 15:18:59 +01:00
if ($) {
const name = plugin.NAME;
2021-03-23 17:26:54 +01:00
const JQUERY_NO_CONFLICT = $.fn[name];
$.fn[name] = plugin.jQueryInterface;
$.fn[name].Constructor = plugin;
2020-12-03 15:18:59 +01:00
2021-03-23 17:26:54 +01:00
$.fn[name].noConflict = () => {
$.fn[name] = JQUERY_NO_CONFLICT;
return plugin.jQueryInterface;
};
2020-12-03 15:18:59 +01:00
}
});
};
2020-12-03 15:18:59 +01:00
2021-03-23 17:26:54 +01:00
/**
* --------------------------------------------------------------------------
2021-10-05 17:50:18 +02:00
* Bootstrap (v5.1.2): scrollspy.js
2021-03-23 17:26:54 +01:00
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
2020-12-03 15:18:59 +01:00
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
2021-03-23 17:26:54 +01:00
const NAME = 'scrollspy';
const DATA_KEY = 'bs.scrollspy';
const EVENT_KEY = `.${DATA_KEY}`;
const DATA_API_KEY = '.data-api';
const Default = {
2018-11-13 07:41:12 +01:00
offset: 10,
method: 'auto',
target: ''
};
2021-03-23 17:26:54 +01:00
const DefaultType = {
2018-11-13 07:41:12 +01:00
offset: 'number',
method: 'string',
target: '(string|element)'
};
2021-03-23 17:26:54 +01:00
const EVENT_ACTIVATE = `activate${EVENT_KEY}`;
const EVENT_SCROLL = `scroll${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_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';
2021-08-04 17:41:51 +02:00
const SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}, .${CLASS_NAME_DROPDOWN_ITEM}`;
2021-03-23 17:26:54 +01:00
const SELECTOR_DROPDOWN = '.dropdown';
const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle';
const METHOD_OFFSET = 'offset';
const METHOD_POSITION = 'position';
2019-10-08 08:39:10 +02:00
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
*/
2018-11-13 07:41:12 +01:00
2021-10-05 17:50:18 +02:00
class ScrollSpy extends BaseComponent__default.default {
2021-03-23 17:26:54 +01:00
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;
2021-10-05 17:50:18 +02:00
EventHandler__default.default.on(this._scrollElement, EVENT_SCROLL, () => this._process());
2021-03-23 17:26:54 +01:00
this.refresh();
2019-03-01 17:31:34 +01:00
2021-03-23 17:26:54 +01:00
this._process();
2018-11-13 07:41:12 +01:00
} // Getters
2015-05-11 21:05:35 +02:00
2021-03-23 17:26:54 +01:00
static get Default() {
return Default;
}
static get NAME() {
return NAME;
2021-03-23 17:26:54 +01:00
} // Public
2015-05-11 21:05:35 +02:00
2017-09-30 23:28:03 +02:00
2021-03-23 17:26:54 +01:00
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;
2018-11-13 07:41:12 +01:00
this._offsets = [];
this._targets = [];
this._scrollHeight = this._getScrollHeight();
2021-10-05 17:50:18 +02:00
const targets = SelectorEngine__default.default.find(SELECTOR_LINK_ITEMS, this._config.target);
2021-03-23 17:26:54 +01:00
targets.map(element => {
const targetSelector = getSelectorFromElement(element);
2021-10-05 17:50:18 +02:00
const target = targetSelector ? SelectorEngine__default.default.findOne(targetSelector) : null;
2018-11-13 07:41:12 +01:00
if (target) {
2021-03-23 17:26:54 +01:00
const targetBCR = target.getBoundingClientRect();
2015-05-11 21:05:35 +02:00
2018-11-13 07:41:12 +01:00
if (targetBCR.width || targetBCR.height) {
2021-10-05 17:50:18 +02:00
return [Manipulator__default.default[offsetMethod](target).top + offsetBase, targetSelector];
2017-09-30 23:28:03 +02:00
}
2018-11-13 07:41:12 +01:00
}
2015-05-11 21:05:35 +02:00
2018-11-13 07:41:12 +01:00
return null;
2021-03-23 17:26:54 +01:00
}).filter(item => item).sort((a, b) => a[0] - b[0]).forEach(item => {
this._offsets.push(item[0]);
2016-10-10 02:26:51 +02:00
2021-03-23 17:26:54 +01:00
this._targets.push(item[1]);
});
}
2020-12-03 15:18:59 +01:00
2021-03-23 17:26:54 +01:00
dispose() {
2021-10-05 17:50:18 +02:00
EventHandler__default.default.off(this._scrollElement, EVENT_KEY);
super.dispose();
2019-01-04 17:29:45 +01:00
} // Private
2018-11-13 07:41:12 +01:00
2021-03-23 17:26:54 +01:00
_getConfig(config) {
config = { ...Default,
2021-10-05 17:50:18 +02:00
...Manipulator__default.default.getDataAttributes(this._element),
2021-03-23 17:26:54 +01:00
...(typeof config === 'object' && config ? config : {})
};
2021-08-04 17:41:51 +02:00
config.target = getElement(config.target) || document.documentElement;
2019-03-01 17:31:34 +01:00
typeCheckConfig(NAME, config, DefaultType);
2018-11-13 07:41:12 +01:00
return config;
2021-03-23 17:26:54 +01:00
}
2015-05-11 21:05:35 +02:00
2021-03-23 17:26:54 +01:00
_getScrollTop() {
2018-11-13 07:41:12 +01:00
return this._scrollElement === window ? this._scrollElement.pageYOffset : this._scrollElement.scrollTop;
2021-03-23 17:26:54 +01:00
}
2015-05-11 21:05:35 +02:00
2021-03-23 17:26:54 +01:00
_getScrollHeight() {
2018-11-13 07:41:12 +01:00
return this._scrollElement.scrollHeight || Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
2021-03-23 17:26:54 +01:00
}
2015-05-11 21:05:35 +02:00
2021-03-23 17:26:54 +01:00
_getOffsetHeight() {
2018-11-13 07:41:12 +01:00
return this._scrollElement === window ? window.innerHeight : this._scrollElement.getBoundingClientRect().height;
2021-03-23 17:26:54 +01:00
}
2015-05-11 21:05:35 +02:00
2021-03-23 17:26:54 +01:00
_process() {
const scrollTop = this._getScrollTop() + this._config.offset;
2015-05-11 21:05:35 +02:00
2021-03-23 17:26:54 +01:00
const scrollHeight = this._getScrollHeight();
2017-09-30 23:28:03 +02:00
2021-03-23 17:26:54 +01:00
const maxScroll = this._config.offset + scrollHeight - this._getOffsetHeight();
2017-09-30 23:28:03 +02:00
2018-11-13 07:41:12 +01:00
if (this._scrollHeight !== scrollHeight) {
this.refresh();
}
2017-09-30 23:28:03 +02:00
2018-11-13 07:41:12 +01:00
if (scrollTop >= maxScroll) {
2021-03-23 17:26:54 +01:00
const target = this._targets[this._targets.length - 1];
2015-05-11 21:05:35 +02:00
2018-11-13 07:41:12 +01:00
if (this._activeTarget !== target) {
this._activate(target);
2018-07-24 02:51:14 +02:00
}
2015-05-11 21:05:35 +02:00
2018-11-13 07:41:12 +01:00
return;
}
2018-07-24 02:51:14 +02:00
2018-11-13 07:41:12 +01:00
if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) {
this._activeTarget = null;
2017-09-30 23:28:03 +02:00
2018-11-13 07:41:12 +01:00
this._clear();
2018-06-22 07:55:23 +02:00
2018-11-13 07:41:12 +01:00
return;
}
2015-05-11 21:05:35 +02:00
2021-03-23 17:26:54 +01:00
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]);
2017-09-30 23:28:03 +02:00
2018-11-13 07:41:12 +01:00
if (isActiveTarget) {
this._activate(this._targets[i]);
}
}
2021-03-23 17:26:54 +01:00
}
2017-09-30 23:28:03 +02:00
2021-03-23 17:26:54 +01:00
_activate(target) {
2018-11-13 07:41:12 +01:00
this._activeTarget = target;
2017-09-30 23:28:03 +02:00
2018-11-13 07:41:12 +01:00
this._clear();
2017-09-30 23:28:03 +02:00
2021-08-04 17:41:51 +02:00
const queries = SELECTOR_LINK_ITEMS.split(',').map(selector => `${selector}[data-bs-target="${target}"],${selector}[href="${target}"]`);
2021-10-05 17:50:18 +02:00
const link = SelectorEngine__default.default.findOne(queries.join(','), this._config.target);
2021-08-04 17:41:51 +02:00
link.classList.add(CLASS_NAME_ACTIVE);
2017-09-30 23:28:03 +02:00
2020-03-28 11:29:08 +01:00
if (link.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {
2021-10-05 17:50:18 +02:00
SelectorEngine__default.default.findOne(SELECTOR_DROPDOWN_TOGGLE, link.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE);
2018-11-13 07:41:12 +01:00
} else {
2021-10-05 17:50:18 +02:00
SelectorEngine__default.default.parents(link, SELECTOR_NAV_LIST_GROUP).forEach(listGroup => {
2019-03-01 17:31:34 +01:00
// Set triggered links parents as active
// With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor
2021-10-05 17:50:18 +02:00
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
2021-03-23 17:26:54 +01:00
2021-10-05 17:50:18 +02:00
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));
2019-03-01 17:31:34 +01:00
});
});
2018-11-13 07:41:12 +01:00
}
2016-10-10 02:26:51 +02:00
2021-10-05 17:50:18 +02:00
EventHandler__default.default.trigger(this._scrollElement, EVENT_ACTIVATE, {
2018-11-13 07:41:12 +01:00
relatedTarget: target
});
2021-03-23 17:26:54 +01:00
}
2016-10-10 02:26:51 +02:00
2021-03-23 17:26:54 +01:00
_clear() {
2021-10-05 17:50:18 +02:00
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));
2019-01-04 17:29:45 +01:00
} // Static
2017-09-30 23:28:03 +02:00
2021-03-23 17:26:54 +01:00
static jQueryInterface(config) {
2018-11-13 07:41:12 +01:00
return this.each(function () {
const data = ScrollSpy.getOrCreateInstance(this, config);
2017-09-30 23:28:03 +02:00
if (typeof config !== 'string') {
return;
2018-11-13 07:41:12 +01:00
}
2018-07-24 02:51:14 +02:00
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`);
2017-09-30 23:28:03 +02:00
}
data[config]();
2018-11-13 07:41:12 +01:00
});
2021-03-23 17:26:54 +01:00
}
2015-05-11 21:05:35 +02:00
2021-03-23 17:26:54 +01:00
}
2018-11-13 07:41:12 +01:00
/**
* ------------------------------------------------------------------------
* Data Api implementation
* ------------------------------------------------------------------------
*/
2015-05-11 21:05:35 +02:00
2017-09-30 23:28:03 +02:00
2021-10-05 17:50:18 +02:00
EventHandler__default.default.on(window, EVENT_LOAD_DATA_API, () => {
SelectorEngine__default.default.find(SELECTOR_DATA_SPY).forEach(spy => new ScrollSpy(spy));
2018-11-13 07:41:12 +01:00
});
/**
* ------------------------------------------------------------------------
* jQuery
* ------------------------------------------------------------------------
2020-11-11 18:07:37 +01:00
* add .ScrollSpy to jQuery only if jQuery is present
2018-11-13 07:41:12 +01:00
*/
2015-05-11 21:05:35 +02:00
defineJQueryPlugin(ScrollSpy);
2015-05-11 21:05:35 +02:00
return ScrollSpy;
2018-07-24 02:51:14 +02:00
2021-10-05 17:50:18 +02:00
}));
2018-07-24 02:51:14 +02:00
//# sourceMappingURL=scrollspy.js.map