261 lines
8.2 KiB
JavaScript
261 lines
8.2 KiB
JavaScript
/*!
|
|
* Bootstrap Native Scrollspy v5.1.10 (https://thednp.github.io/bootstrap.native/)
|
|
* Copyright 2026 © thednp
|
|
* Licensed under MIT (https://github.com/thednp/bootstrap.native/blob/master/LICENSE)
|
|
*/
|
|
"use strict";
|
|
|
|
import { Data, ObjectKeys, addClass, closest, createCustomEvent, dispatchEvent, getAttribute, getBoundingClientRect, getDocument, getDocumentElement, getElementById, getElementsByTagName, getInstance, hasClass, isElement, isString, mouseclickEvent, normalizeOptions, querySelector, removeClass } from "@thednp/shorty";
|
|
import PositionObserver from "@thednp/position-observer";
|
|
import { addListener, removeListener } from "@thednp/event-listener";
|
|
//#region src/strings/activeClass.ts
|
|
/**
|
|
* Global namespace for most components active class.
|
|
*/
|
|
const activeClass = "active";
|
|
//#endregion
|
|
//#region src/strings/scrollspyString.ts
|
|
/** @type {string} */
|
|
const scrollspyString = "scrollspy";
|
|
//#endregion
|
|
//#region src/strings/scrollspyComponent.ts
|
|
/** @type {string} */
|
|
const scrollspyComponent = "ScrollSpy";
|
|
//#endregion
|
|
//#region src/util/isDisabled.ts
|
|
/**
|
|
* Check if interactive element is disabled.
|
|
* @param target either a `<button>` or an `<a>`
|
|
* @returns whether the target is disabled
|
|
*/
|
|
const isDisabled = (target) => {
|
|
return hasClass(target, "disabled") || getAttribute(target, "disabled") === "true";
|
|
};
|
|
//#endregion
|
|
//#region src/version.ts
|
|
const Version = "5.1.10";
|
|
//#endregion
|
|
//#region src/components/base-component.ts
|
|
/** Returns a new `BaseComponent` instance. */
|
|
var BaseComponent = class {
|
|
/**
|
|
* @param target `Element` or selector string
|
|
* @param config component instance options
|
|
*/
|
|
constructor(target, config) {
|
|
let element;
|
|
try {
|
|
if (isElement(target)) element = target;
|
|
else if (isString(target)) {
|
|
element = querySelector(target);
|
|
if (!element) throw Error(`"${target}" is not a valid selector.`);
|
|
} else throw Error(`your target is not an instance of HTMLElement.`);
|
|
} catch (e) {
|
|
throw Error(`${this.name} Error: ${e.message}`);
|
|
}
|
|
const prevInstance = Data.get(element, this.name);
|
|
if (prevInstance) prevInstance._toggleEventListeners();
|
|
this.element = element;
|
|
this.options = this.defaults && ObjectKeys(this.defaults).length ? normalizeOptions(element, this.defaults, config || {}, "bs") : {};
|
|
Data.set(element, this.name, this);
|
|
}
|
|
get version() {
|
|
return Version;
|
|
}
|
|
get name() {
|
|
return "BaseComponent";
|
|
}
|
|
get defaults() {
|
|
return {};
|
|
}
|
|
/** just to have something to extend from */
|
|
_toggleEventListeners = () => {};
|
|
/** Removes component from target element. */
|
|
dispose() {
|
|
Data.remove(this.element, this.name);
|
|
ObjectKeys(this).forEach((prop) => {
|
|
delete this[prop];
|
|
});
|
|
}
|
|
};
|
|
//#endregion
|
|
//#region src/components/scrollspy.ts
|
|
const scrollspySelector = "[data-bs-spy=\"scroll\"]";
|
|
const scrollSpyAnchorSelector = "[href]";
|
|
const scrollspyDefaults = {
|
|
offset: 10,
|
|
target: void 0
|
|
};
|
|
/**
|
|
* Static method which returns an existing `ScrollSpy` instance associated
|
|
* to a target `Element`.
|
|
*/
|
|
const getScrollSpyInstance = (element) => getInstance(element, scrollspyComponent);
|
|
/**
|
|
* A `ScrollSpy` initialization callback.
|
|
*/
|
|
const scrollspyInitCallback = (element) => new ScrollSpy(element);
|
|
const activateScrollSpy = createCustomEvent(`activate.bs.${scrollspyString}`);
|
|
/**
|
|
* Update the state of all items.
|
|
*
|
|
* @param self the `ScrollSpy` instance
|
|
*/
|
|
const updateSpyTargets = (self) => {
|
|
const { target, _itemsLength, _observables } = self;
|
|
const links = getElementsByTagName("A", target);
|
|
const doc = getDocument(target);
|
|
if (!links.length || _itemsLength === _observables.size) return;
|
|
_observables.clear();
|
|
Array.from(links).forEach((link) => {
|
|
const hash = getAttribute(link, "href")?.slice(1);
|
|
const targetItem = hash?.length ? doc.getElementById(hash) : null;
|
|
if (targetItem && !isDisabled(link)) self._observables.set(targetItem, link);
|
|
});
|
|
self._itemsLength = self._observables.size;
|
|
};
|
|
/**
|
|
* Clear all items of the target.
|
|
*
|
|
* @param target a single item
|
|
*/
|
|
const clear = (target) => {
|
|
Array.from(getElementsByTagName("A", target)).forEach((item) => {
|
|
if (hasClass(item, "active")) removeClass(item, activeClass);
|
|
});
|
|
};
|
|
/**
|
|
* Activates a new item.
|
|
*
|
|
* @param self the `ScrollSpy` instance
|
|
* @param item a single item
|
|
*/
|
|
const activate = (self, item) => {
|
|
const { target, element } = self;
|
|
clear(target);
|
|
self._activeItem = item;
|
|
addClass(item, activeClass);
|
|
let parentItem = item;
|
|
while (parentItem !== target) {
|
|
parentItem = parentItem.parentElement;
|
|
if ([
|
|
"nav",
|
|
"dropdown-menu",
|
|
"list-group"
|
|
].some((c) => hasClass(parentItem, c))) {
|
|
const parentLink = parentItem.previousElementSibling;
|
|
if (parentLink && !hasClass(parentLink, "active")) addClass(parentLink, activeClass);
|
|
}
|
|
}
|
|
activateScrollSpy.relatedTarget = item;
|
|
dispatchEvent(element, activateScrollSpy);
|
|
};
|
|
const getOffset = (self, target) => {
|
|
const { scrollTarget, element, options } = self;
|
|
return (scrollTarget !== element ? getBoundingClientRect(target).top + scrollTarget.scrollTop : target.offsetTop) - (options.offset || 10);
|
|
};
|
|
/** Returns a new `ScrollSpy` instance. */
|
|
var ScrollSpy = class extends BaseComponent {
|
|
static selector = scrollspySelector;
|
|
static init = scrollspyInitCallback;
|
|
static getInstance = getScrollSpyInstance;
|
|
/**
|
|
* @param target the target element
|
|
* @param config the instance options
|
|
*/
|
|
constructor(target, config) {
|
|
super(target, config);
|
|
const { element, options } = this;
|
|
const spyTarget = querySelector(options.target, getDocument(element));
|
|
if (!spyTarget) return;
|
|
this.target = spyTarget;
|
|
this.scrollTarget = element.clientHeight < element.scrollHeight ? element : getDocumentElement(element);
|
|
this._observables = /* @__PURE__ */ new Map();
|
|
this.refresh();
|
|
this._observer = new PositionObserver(() => {
|
|
requestAnimationFrame(() => this.refresh?.());
|
|
}, { root: this.scrollTarget });
|
|
this._toggleEventListeners(true);
|
|
}
|
|
/**
|
|
* Returns component name string.
|
|
*/
|
|
get name() {
|
|
return scrollspyComponent;
|
|
}
|
|
/**
|
|
* Returns component default options.
|
|
*/
|
|
get defaults() {
|
|
return scrollspyDefaults;
|
|
}
|
|
/** Updates all items. */
|
|
refresh = () => {
|
|
const { target, scrollTarget } = this;
|
|
if (!target || target.offsetHeight === 0) return;
|
|
updateSpyTargets(this);
|
|
const { _itemsLength, _observables, _activeItem } = this;
|
|
if (!_itemsLength) return;
|
|
const entries = _observables.entries().toArray();
|
|
const { scrollTop, scrollHeight, clientHeight } = scrollTarget;
|
|
if (scrollTop >= scrollHeight - clientHeight) {
|
|
const newActiveItem = entries[_itemsLength - 1]?.[1];
|
|
if (_activeItem !== newActiveItem) activate(this, newActiveItem);
|
|
return;
|
|
}
|
|
const firstOffset = entries[0]?.[0] ? getOffset(this, entries[0][0]) : null;
|
|
if (firstOffset !== null && scrollTop < firstOffset && firstOffset > 0) {
|
|
this._activeItem = null;
|
|
clear(target);
|
|
return;
|
|
}
|
|
for (let i = 0; i < _itemsLength; i += 1) {
|
|
const [targetItem, item] = entries[i];
|
|
const offsetTop = getOffset(this, targetItem);
|
|
const nextTarget = entries[i + 1]?.[0];
|
|
const nextOffsetTop = nextTarget ? getOffset(this, nextTarget) : null;
|
|
if (_activeItem !== item && scrollTop >= offsetTop && (nextOffsetTop === null || scrollTop < nextOffsetTop)) {
|
|
activate(this, item);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
/**
|
|
* This method provides an event handle
|
|
* for scrollspy
|
|
* @param e the event listener object
|
|
*/
|
|
_scrollTo = (e) => {
|
|
const item = closest(e.target, scrollSpyAnchorSelector);
|
|
const hash = item && getAttribute(item, "href")?.slice(1);
|
|
const targetItem = hash && getElementById(hash, this.target);
|
|
if (targetItem) {
|
|
this.scrollTarget.scrollTo({
|
|
top: targetItem.offsetTop,
|
|
behavior: "smooth"
|
|
});
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
/**
|
|
* Toggles on/off the component observer.
|
|
*
|
|
* @param self the ScrollSpy instance
|
|
* @param add when `true`, listener is added
|
|
*/
|
|
_toggleEventListeners = (add) => {
|
|
const { target, _observables, _observer, _scrollTo } = this;
|
|
(add ? addListener : removeListener)(target, mouseclickEvent, _scrollTo);
|
|
if (add) _observables?.forEach((_, targetItem) => _observer.observe(targetItem));
|
|
else _observer.disconnect();
|
|
};
|
|
/** Removes `ScrollSpy` from the target element. */
|
|
dispose() {
|
|
this._toggleEventListeners();
|
|
super.dispose();
|
|
}
|
|
};
|
|
//#endregion
|
|
export { ScrollSpy as default };
|
|
|
|
//# sourceMappingURL=scrollspy.mjs.map
|