@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
1,259 lines (1,105 loc) • 32 kB
JavaScript
/**
* Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
* Node module: @schukai/monster
*
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
*
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
* For more information about purchasing a commercial license, please contact Volker Schukai.
*
* SPDX-License-Identifier: AGPL-3.0
*/
import {
assembleMethodSymbol,
CustomElement,
registerCustomElement,
} from "../../dom/customelement.mjs";
import { findElementWithSelectorUpwards, getWindow } from "../../dom/util.mjs";
import { DeadMansSwitch } from "../../util/deadmansswitch.mjs";
import { ThemeStyleSheet } from "../stylesheet/theme.mjs";
import { ATTRIBUTE_DATASOURCE_SELECTOR } from "./constants.mjs";
import { Datasource } from "./datasource.mjs";
import { Observer } from "../../types/observer.mjs";
import { ATTRIBUTE_ROLE } from "../../dom/constants.mjs";
import { findTargetElementFromEvent } from "../../dom/events.mjs";
import { PaginationStyleSheet } from "./stylesheet/pagination.mjs";
import { DisplayStyleSheet } from "../stylesheet/display.mjs";
import { isString } from "../../types/is.mjs";
import { Pathfinder } from "../../data/pathfinder.mjs";
import { instanceSymbol } from "../../constants.mjs";
import { Formatter } from "../../text/formatter.mjs";
import "../form/select.mjs";
import { addAttributeToken } from "../../dom/attributes.mjs";
import { ATTRIBUTE_ERRORMESSAGE } from "../../dom/constants.mjs";
import "./datasource/dom.mjs";
import "./datasource/rest.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
export { Pagination };
/**
* @private
* @type {symbol}
*/
const paginationElementSymbol = Symbol.for("paginationElement");
/**
* @private
* @type {symbol}
*/
const datasourceLinkedElementSymbol = Symbol("datasourceLinkedElement");
/**
* @private
* @type {symbol}
*/
const resizeObserverSymbol = Symbol("resizeObserver");
/**
* @private
* @type {symbol}
*/
const sizeDataSymbol = Symbol("sizeData");
/**
* @private
* @type {symbol}
*/
const debounceSizeSymbol = Symbol("debounceSize");
/**
* @private
* @type {symbol}
*/
const layoutUpdateSymbol = Symbol("layoutUpdate");
/**
* @private
* @type {symbol}
*/
const layoutApplySymbol = Symbol("layoutApply");
/**
* @private
* @type {symbol}
*/
const labelStateSymbol = Symbol("labelState");
const layoutModeSymbol = Symbol("layoutMode");
const lastNavClickTimeSymbol = Symbol("lastNavClickTime");
const lastNavClickTargetSymbol = Symbol("lastNavClickTarget");
const minNavDoubleClickDelayMs = 200;
const compactPrevIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0m3.5 7.5a.5.5 0 0 1 0 1H5.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L5.707 7.5z"/></svg>`;
const compactNextIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0M4.5 7.5a.5.5 0 0 0 0 1h5.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5z"/></svg>`;
/**
* A Pagination component
*
* @fragments /fragments/components/datatable/pagination
*
* @example /examples/components/datatable/pagination-simple Pagination
*
* @copyright Volker Schukai
* @summary The Pagination component is used to show the current page and the total number of pages.
*/
class Pagination extends CustomElement {
/**
*/
constructor() {
super();
this[datasourceLinkedElementSymbol] = null;
}
/**
* This method is called by the `instanceof` operator.
* @return {symbol}
*/
static get [instanceSymbol]() {
return Symbol.for("@schukai/monster/components/pagination");
}
/**
* To set the options via the HTML tag, the attribute `data-monster-options` must be used.
* @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
*
* The individual configuration values can be found in the table.
*
* @property {Object} templates Template definitions
* @property {string} templates.main Main template
* @property {Object} datasource Datasource configuration
* @property {string} datasource.selector Datasource selector
* @property {Object} labels Label definitions
* @property {string} labels.page Page label
* @property {string} labels.description Description label
* @property {string} labels.previous Previous label
* @property {string} labels.next Next label
* @property {string} labels.of Of label
* @property {string} href Href
* @property {number} currentPage Current page
* @property {number} pages Pages
* @property {number} objectsPerPage Objects per page
* @property {Object} mapping Mapping
* @property {string} mapping.pages Pages mapping
* @property {string} mapping.objectsPerPage Objects per page mapping
* @property {string} mapping.currentPage Current page mapping
*/
get defaults() {
return Object.assign(
{},
super.defaults,
{
templates: {
main: getTemplate(),
},
datasource: {
selector: null,
},
labels: getTranslations(),
callbacks: {
click: null,
},
href: "page-${page}",
pages: null,
objectsPerPage: 20,
currentPage: null,
mapping: {
pages: "sys.pagination.pages",
objectsPerPage: "sys.pagination.objectsPerPage",
currentPage: "sys.pagination.currentPage",
},
/* @private */
pagination: {
items: [],
},
},
initOptionsFromArguments.call(this),
);
}
/**
* Sets the pagination state directly, without requiring a datasource.
* This is useful for controlling the component programmatically.
*
* @param {object} state - The state object for the pagination.
* @param {number} state.currentPage - The current active page number.
* @param {number} state.totalPages - The total number of available pages.
* @return {void}
*/
setPaginationState({ currentPage, totalPages }) {
if (typeof currentPage !== "number" || typeof totalPages !== "number") {
console.error(
"setPaginationState requires currentPage and totalPages to be numbers.",
);
return;
}
// 1. Update the component's internal options with the new values.
this.setOption("currentPage", currentPage);
this.setOption("pages", totalPages);
// 2. Call the existing buildPagination function to recalculate the links.
const pagination = buildPagination.call(
this,
this.getOption("currentPage"),
this.getOption("pages"),
);
// 3. Preserve the responsive visibility of page numbers.
if (
!isAdaptivePagination.call(this) &&
this?.[sizeDataSymbol]?.showNumbers !== true
) {
pagination.items = [];
}
// 4. Set the 'pagination' option, which will trigger the component to re-render.
this.setOption("pagination", pagination);
if (isAdaptivePagination.call(this)) {
const list = this.shadowRoot?.querySelector(".pagination-list");
if (list) {
list.setAttribute("data-monster-adaptive-ready", "false");
}
schedulePaginationLayoutUpdate.call(this);
} else {
const list = this.shadowRoot?.querySelector(".pagination-list");
if (list) {
list.setAttribute("data-monster-adaptive-ready", "true");
}
}
}
/**
*
* @return {string}
*/
static getTag() {
return "monster-pagination";
}
/**
* @return {void}
*/
disconnectedCallback() {
super.disconnectedCallback();
if (this?.[resizeObserverSymbol] instanceof ResizeObserver) {
this[resizeObserverSymbol].disconnect();
}
if (this?.[debounceSizeSymbol] instanceof DeadMansSwitch) {
try {
this[debounceSizeSymbol].defuse();
delete this[debounceSizeSymbol];
} catch (e) {
// already fired
}
}
}
/**
* @return {void}
*/
connectedCallback() {
super.connectedCallback();
const parentNode = this.parentNode;
if (!parentNode) {
return;
}
try {
handleDataSourceChanges.call(this);
} catch (e) {
addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e?.message || `${e}`);
}
setTimeout(() => {
const parentParentNode = parentNode?.parentNode || parentNode;
this[resizeObserverSymbol] = new ResizeObserver(() => {
if (this[layoutApplySymbol]) {
return;
}
if (this[debounceSizeSymbol] instanceof DeadMansSwitch) {
try {
this[debounceSizeSymbol].touch();
return;
} catch (e) {
delete this[debounceSizeSymbol];
}
}
this[debounceSizeSymbol] = new DeadMansSwitch(250, () => {
queueMicrotask(() => {
if (isAdaptivePagination.call(this)) {
schedulePaginationLayoutUpdate.call(this);
return;
}
const parentWidth = parentParentNode.offsetWidth;
const ownWidth = this.clientWidth;
if (this[sizeDataSymbol]?.last?.parentWidth === parentWidth) {
return;
}
this[sizeDataSymbol].last = {
parentWidth: parentWidth,
};
this[sizeDataSymbol].showNumbers = ownWidth <= parentWidth;
handleDataSourceChanges.call(this);
});
});
});
if (isAdaptivePagination.call(this)) {
this[sizeDataSymbol] = {
last: {
parentWidth: 0,
},
showNumbers: true,
};
schedulePaginationLayoutUpdate.call(this);
} else {
const parentWidth = parentParentNode.offsetWidth;
const ownWidth = this.offsetWidth;
this[sizeDataSymbol] = {
last: {
parentWidth: 0,
},
showNumbers: ownWidth < parentWidth,
};
const list = this.shadowRoot?.querySelector(".pagination-list");
if (list) {
list.setAttribute("data-monster-adaptive-ready", "true");
}
}
this[resizeObserverSymbol].observe(parentParentNode);
}, 0);
}
/**
* @return {void}
*/
[assembleMethodSymbol]() {
super[assembleMethodSymbol]();
initControlReferences.call(this);
initEventHandler.call(this);
const selector = this.getOption("datasource.selector", "");
if (isString(selector)) {
const element = findElementWithSelectorUpwards(this, selector);
if (element === null) {
throw new Error("the selector must match exactly one element");
}
if (!(element instanceof Datasource)) {
throw new TypeError("the element must be a datasource");
}
this[datasourceLinkedElementSymbol] = element;
element.datasource.attachObserver(
new Observer(handleDataSourceChanges.bind(this)),
);
element.attachObserver(new Observer(handleDataSourceChanges.bind(this)));
handleDataSourceChanges.call(this);
}
}
/**
* @private
* @return {CSSStyleSheet}
*/
static getControlCSSStyleSheet() {
return PaginationStyleSheet;
}
/**
* @return {CSSStyleSheet[]}
*/
static getCSSStyleSheet() {
return [this.getControlCSSStyleSheet(), DisplayStyleSheet, ThemeStyleSheet];
}
}
function getTranslations() {
const locale = getLocaleOfDocument();
switch (locale.language) {
case "de":
return {
page: "${page}",
description: "Seite ${page}",
previous: "Vorherige",
next: "Nächste",
of: "von",
summary: "Seite ${current} von ${max}",
};
case "fr":
return {
page: "${page}",
description: "Page ${page}",
previous: "Précédent",
next: "Suivant",
of: "de",
summary: "Page ${current} sur ${max}",
};
case "sp":
return {
page: "${page}",
description: "Página ${page}",
previous: "Anterior",
next: "Siguiente",
of: "de",
summary: "Página ${current} de ${max}",
};
case "it":
return {
page: "${page}",
description: "Pagina ${page}",
previous: "Precedente",
next: "Successivo",
of: "di",
summary: "Pagina ${current} di ${max}",
};
case "pl":
return {
page: "${page}",
description: "Strona ${page}",
previous: "Poprzednia",
next: "Następna",
of: "z",
summary: "Strona ${current} z ${max}",
};
case "no":
return {
page: "${page}",
description: "Side ${page}",
previous: "Forrige",
next: "Neste",
of: "av",
summary: "Side ${current} av ${max}",
};
case "dk":
return {
page: "${page}",
description: "Side ${page}",
previous: "Forrige",
next: "Næste",
of: "af",
summary: "Side ${current} af ${max}",
};
case "sw":
return {
page: "${page}",
description: "Sida ${page}",
previous: "Föregående",
next: "Nästa",
of: "av",
summary: "Sida ${current} av ${max}",
};
default:
case "en":
return {
page: "${page}",
description: "Page ${page}",
previous: "Previous",
next: "Next",
of: "of",
summary: "${current} of ${max}",
};
}
}
/**
* @private
* @return {Select}
* @throws {Error} no shadow-root is defined
*/
function initControlReferences() {
if (!this.shadowRoot) {
throw new Error("no shadow-root is defined");
}
this[paginationElementSymbol] = this.shadowRoot.querySelector(
"[data-monster-role=pagination]",
);
}
/**
* @private
*/
/**
* @private
*/
function initEventHandler() {
const self = this;
self[paginationElementSymbol].addEventListener("click", function (event) {
const prevTarget = findTargetElementFromEvent(
event,
ATTRIBUTE_ROLE,
"pagination-prev",
);
const nextTarget = findTargetElementFromEvent(
event,
ATTRIBUTE_ROLE,
"pagination-next",
);
const itemTarget = findTargetElementFromEvent(
event,
ATTRIBUTE_ROLE,
"pagination-item",
);
const element = itemTarget || prevTarget || nextTarget;
if (
!(element instanceof HTMLElement) ||
!element.hasAttribute("data-page-no")
) {
return;
}
if (
(event.detail === 1 || event.detail === 0) &&
(prevTarget || nextTarget)
) {
self[lastNavClickTimeSymbol] = event.timeStamp;
self[lastNavClickTargetSymbol] = prevTarget ? "prev" : "next";
}
const page = element.getAttribute("data-page-no");
if (!page || page === "…" || page === "null" || page === "undefined") {
return;
}
event.preventDefault();
const datasource = self[datasourceLinkedElementSymbol];
const clickCallback = self.getOption("callbacks.click");
if (datasource && typeof datasource.setParameters === "function") {
datasource.setParameters({ page });
if (typeof datasource.reload === "function") {
datasource.reload();
}
} else if (typeof clickCallback === "function") {
clickCallback(parseInt(page, 10), event);
}
});
self[paginationElementSymbol].addEventListener("dblclick", function (event) {
const prevTarget = findTargetElementFromEvent(
event,
ATTRIBUTE_ROLE,
"pagination-prev",
);
const nextTarget = findTargetElementFromEvent(
event,
ATTRIBUTE_ROLE,
"pagination-next",
);
if (!prevTarget && !nextTarget) {
return;
}
event.preventDefault();
const lastTarget = self[lastNavClickTargetSymbol];
const lastTime = self[lastNavClickTimeSymbol];
const currentTarget = prevTarget ? "prev" : "next";
if (
lastTarget !== currentTarget ||
!Number.isFinite(lastTime) ||
event.timeStamp - lastTime < minNavDoubleClickDelayMs
) {
return;
}
const maxPage = parseInt(self.getOption("pages"), 10);
if (!Number.isFinite(maxPage) || maxPage <= 0) {
return;
}
const targetPage = prevTarget ? 1 : maxPage;
const datasource = self[datasourceLinkedElementSymbol];
const clickCallback = self.getOption("callbacks.click");
if (datasource && typeof datasource.setParameters === "function") {
datasource.setParameters({ page: targetPage });
if (typeof datasource.reload === "function") {
datasource.reload();
}
} else if (typeof clickCallback === "function") {
clickCallback(targetPage, event);
}
});
}
/**
* This attribute can be used to pass a URL to this select.
*
* ```
* <monster-form data-monster-datasource="restapi:....."></monster-form>
* ```
*
* @private
* @return {object}
* @throws {TypeError} incorrect arguments passed for the datasource
* @throws {Error} the datasource could not be initialized
*/
function initOptionsFromArguments() {
const options = {};
const selector = this.getAttribute(ATTRIBUTE_DATASOURCE_SELECTOR);
if (selector) {
options.datasource = { selector: selector };
}
return options;
}
/**
* @private
*/
function handleDataSourceChanges() {
let pagination;
if (!this[datasourceLinkedElementSymbol]) {
return;
}
const mapping = this.getOption("mapping");
const pf = new Pathfinder(this[datasourceLinkedElementSymbol].data);
for (const key in mapping) {
const path = mapping[key];
if (pf.exists(path)) {
const value = pf.getVia(path);
this.setOption(key, value);
}
const o = this[datasourceLinkedElementSymbol].getOption(path);
if (o !== undefined && o !== null) {
this.setOption(key, o);
}
}
pagination = buildPagination.call(
this,
this.getOption("currentPage"),
this.getOption("pages"),
);
if (this?.[sizeDataSymbol]?.showNumbers !== true) {
pagination.items = [];
}
this.setOption("pagination", pagination);
if (isAdaptivePagination.call(this)) {
schedulePaginationLayoutUpdate.call(this);
} else {
const list = this.shadowRoot?.querySelector(".pagination-list");
if (list) {
list.setAttribute("data-monster-adaptive-ready", "true");
}
}
}
/**
* @private
* @param current
* @param max
* @return {object}
*/
function buildPagination(current, max) {
const parsedCurrent = Number.parseInt(current, 10);
const parsedMax = Number.parseInt(max, 10);
max = Number.isFinite(parsedMax) && parsedMax >= 1 ? parsedMax : 1;
current = Number.isFinite(parsedCurrent) ? parsedCurrent : 1;
if (current < 1) {
current = 1;
} else if (current > max) {
current = max;
}
let prev = current === 1 ? null : current - 1;
let next = current === max ? null : current + 1;
const itemList = [1];
if (current > 4) itemList.push(-1);
const r = 2;
const r1 = current - r;
const r2 = current + r;
for (let i = r1 > 2 ? r1 : 2; i <= Math.min(max, r2); i++) itemList.push(i);
if (r2 + 1 < max) itemList.push(-1);
if (r2 < max) itemList.push(max);
let prevClass = "";
if (prev === null) {
prevClass = " disabled";
}
let nextClass = "";
if (next === null) {
nextClass = " disabled";
}
const items = itemList.map((item) => {
let p = `${item}`;
if (item === -1) {
item = null;
p = "…";
}
const c = `${current}`;
const obj = {
pageNo: item, // as integer
page: p, // as string
current: p === c,
class: (p === c ? "current" : "").trim(),
};
if (item === null) {
obj.class += " disabled".trim();
}
const formatter = new Formatter(obj);
obj.description = formatter.format(this.getOption("labels.description"));
obj.label = formatter.format(this.getOption("labels.page"));
obj.href =
item === null
? "#"
: p === c
? "#"
: p === "1"
? "#"
: `#${formatter.format(this.getOption("href"))}`;
return obj;
});
const nextNo = next;
next = `${next}`;
const nextHref =
next === "null"
? "#"
: `#${new Formatter({ page: next }).format(this.getOption("href"))}`;
const prevNo = prev;
prev = `${prev}`;
const prevHref =
prev === "null"
? "#"
: `#${new Formatter({ page: prev }).format(this.getOption("href"))}`;
const ofLabel = this.getOption("labels.of") || "of";
const summaryTemplate = this.getOption("labels.summary");
const summaryContext = {
current: String(current),
max: String(max),
of: String(ofLabel),
};
const summary = isString(summaryTemplate)
? new Formatter(summaryContext).format(summaryTemplate)
: `${summaryContext.current} ${summaryContext.of} ${summaryContext.max}`;
return {
current,
nextNo,
next,
nextClass,
nextHref,
prevNo,
prev,
prevClass,
prevHref,
summary,
items,
};
}
/**
* @private
*/
function schedulePaginationLayoutUpdate() {
if (!isAdaptivePagination.call(this)) {
return;
}
if (this[layoutUpdateSymbol] || this[layoutApplySymbol]) {
return;
}
this[layoutUpdateSymbol] = true;
requestAnimationFrame(() => {
this[layoutUpdateSymbol] = false;
applyPaginationLayout.call(this);
});
}
/**
* @private
*/
function applyPaginationLayout() {
if (!this.isConnected || !this.shadowRoot) {
return;
}
if (!isAdaptivePagination.call(this)) {
return;
}
const parentNode = this.parentNode;
const parentParentNode = parentNode?.parentNode || parentNode;
const availableWidth = isSelectPagination.call(this)
? getSelectAvailableWidth.call(this, parentNode)
: isEmbeddedPagination.call(this)
? getEmbeddedAvailableWidth.call(this, parentNode)
: this.getBoundingClientRect().width ||
parentParentNode?.offsetWidth ||
0;
if (!availableWidth) {
return;
}
this[layoutApplySymbol] = true;
try {
ensureLabelState.call(this);
const list = this.shadowRoot.querySelector(".pagination-list");
if (!list) {
return;
}
list.classList.add("pagination-no-wrap");
list.setAttribute("data-monster-adaptive-ready", "false");
const prevLink = this.shadowRoot.querySelector(
"a[data-monster-role=pagination-prev]",
);
const nextLink = this.shadowRoot.querySelector(
"a[data-monster-role=pagination-next]",
);
const widthFull = applyPaginationMode.call(
this,
list,
prevLink,
nextLink,
"full",
);
const widthNoNumbers = applyPaginationMode.call(
this,
list,
prevLink,
nextLink,
"no-numbers",
);
const widthCompactSummary = applyPaginationMode.call(
this,
list,
prevLink,
nextLink,
"compact-summary",
);
const widthCompact = applyPaginationMode.call(
this,
list,
prevLink,
nextLink,
"compact",
);
const nextMode = choosePaginationMode.call(this, {
availableWidth,
widthFull,
widthNoNumbers,
widthCompactSummary,
widthCompact,
});
applyPaginationMode.call(this, list, prevLink, nextLink, nextMode);
list.setAttribute("data-monster-adaptive-ready", "true");
} finally {
this[layoutApplySymbol] = false;
}
}
/**
* @private
* @param {number} parentWidth
* @return {boolean}
*/
function paginationFits(parentWidth) {
const list = this.shadowRoot.querySelector(".pagination-list");
if (!list) {
return true;
}
return list.scrollWidth <= parentWidth;
}
/**
* @private
* @param {HTMLElement} list
* @param {boolean} visible
*/
function setNumberItemsVisible(list, visible) {
const numberItems = list.querySelectorAll(
"[data-monster-role=pagination-item]",
);
for (const item of numberItems) {
const container = item.parentElement;
if (!container) continue;
container.style.display = visible ? "" : "none";
}
}
/**
* @private
* @param {HTMLElement} list
* @param {boolean} visible
*/
function setSummaryVisible(list, visible) {
const summaryItem = list.querySelector(
"[data-monster-role=pagination-summary]",
);
if (!summaryItem) {
return;
}
summaryItem.style.display = visible ? "flex" : "none";
}
/**
* @private
*/
function ensureLabelState() {
if (this[labelStateSymbol]) {
return;
}
const prevLink = this.shadowRoot?.querySelector(
"a[data-monster-role=pagination-prev]",
);
const nextLink = this.shadowRoot?.querySelector(
"a[data-monster-role=pagination-next]",
);
this[labelStateSymbol] = {
previous: prevLink?.innerHTML || this.getOption("labels.previous"),
next: nextLink?.innerHTML || this.getOption("labels.next"),
};
}
/**
* @private
* @param {boolean} compact
*/
function setCompactLabels(compact, prevLink, nextLink) {
const state = this[labelStateSymbol];
if (!state) return;
if (compact) {
if (prevLink) prevLink.innerHTML = compactPrevIcon;
if (nextLink) nextLink.innerHTML = compactNextIcon;
} else {
if (prevLink) prevLink.innerHTML = state.previous;
if (nextLink) nextLink.innerHTML = state.next;
}
}
/**
* @private
* @return {boolean}
*/
function isEmbeddedPagination() {
return this?.tagName?.toLowerCase?.() === "monster-embedded-pagination";
}
/**
* @private
* @return {boolean}
*/
function isSelectPagination() {
const root = this.getRootNode?.();
const host = root?.host;
if (host?.tagName?.toLowerCase?.() === "monster-select") {
return true;
}
return (
host?.constructor?.[instanceSymbol] ===
Symbol.for("@schukai/monster/components/form/select@@instance")
);
}
/**
* @private
* @return {boolean}
*/
function isAdaptivePagination() {
return isEmbeddedPagination.call(this) || isSelectPagination.call(this);
}
/**
* @private
* @param {HTMLElement|null} parentNode
* @return {number}
*/
function getSelectAvailableWidth(parentNode) {
if (!parentNode) {
return this.getBoundingClientRect().width;
}
const rect = parentNode.getBoundingClientRect();
const styles = getComputedStyle(parentNode);
const paddingLeft = parseFloat(styles.paddingLeft || "0");
const paddingRight = parseFloat(styles.paddingRight || "0");
const parentWidth = Math.max(0, rect.width - paddingLeft - paddingRight);
const root = this.getRootNode?.();
const host = root?.host;
const hostWidth = host?.getBoundingClientRect?.().width || 0;
if (hostWidth > 0) {
if (parentWidth > 0) {
return Math.min(parentWidth, hostWidth);
}
return hostWidth;
}
return parentWidth || this.getBoundingClientRect().width;
}
/**
* @private
* @param {HTMLElement|null} parentNode
* @return {number}
*/
function getEmbeddedAvailableWidth(parentNode) {
if (!parentNode) {
return this.getBoundingClientRect().width;
}
const rect = parentNode.getBoundingClientRect();
if (!rect.width) {
return this.getBoundingClientRect().width;
}
const styles = getComputedStyle(parentNode);
const gapValue = styles.columnGap || styles.gap || "0";
const gap = parseFloat(gapValue) || 0;
const siblings = Array.from(parentNode.children).filter((el) => el !== this);
let siblingsWidth = 0;
for (const sibling of siblings) {
const siblingStyles = getComputedStyle(sibling);
if (siblingStyles.display === "none") {
continue;
}
siblingsWidth += sibling.getBoundingClientRect().width;
}
const gaps = siblings.length > 0 ? siblings.length : 0;
return Math.max(0, rect.width - siblingsWidth - gap * gaps);
}
/**
* @private
* @param {HTMLElement} list
* @param {HTMLElement|null} prevLink
* @param {HTMLElement|null} nextLink
* @param {"full"|"no-numbers"|"compact-summary"|"compact"} mode
* @return {number}
*/
function applyPaginationMode(list, prevLink, nextLink, mode) {
switch (mode) {
case "compact":
list.classList.add("pagination-numbers-hidden");
list.classList.add("pagination-compact");
setNumberItemsVisible(list, false);
setSummaryVisible(list, true);
setCompactLabels.call(this, true, prevLink, nextLink);
break;
case "compact-summary":
list.classList.add("pagination-numbers-hidden");
list.classList.add("pagination-compact");
setNumberItemsVisible(list, false);
setSummaryVisible(list, true);
setCompactLabels.call(this, true, prevLink, nextLink);
break;
case "no-numbers":
list.classList.add("pagination-numbers-hidden");
list.classList.remove("pagination-compact");
setNumberItemsVisible(list, false);
setSummaryVisible(list, true);
setCompactLabels.call(this, false, prevLink, nextLink);
break;
default:
list.classList.remove("pagination-numbers-hidden");
list.classList.remove("pagination-compact");
setNumberItemsVisible(list, true);
setSummaryVisible(list, false);
setCompactLabels.call(this, false, prevLink, nextLink);
break;
}
return list.scrollWidth;
}
/**
* @private
* @param {object} params
* @return {"full"|"no-numbers"|"compact-summary"|"compact"}
*/
function choosePaginationMode({
availableWidth,
widthFull,
widthNoNumbers,
widthCompactSummary,
widthCompact,
}) {
const hysteresisUp = 24;
const hysteresisDown = 8;
const currentMode = this[layoutModeSymbol] || "full";
if (currentMode === "full") {
if (widthFull > availableWidth - hysteresisDown) {
if (widthNoNumbers <= availableWidth - hysteresisDown) {
this[layoutModeSymbol] = "no-numbers";
return "no-numbers";
}
if (widthCompactSummary <= availableWidth - hysteresisDown) {
this[layoutModeSymbol] = "compact-summary";
return "compact-summary";
}
this[layoutModeSymbol] = "compact";
return "compact";
}
this[layoutModeSymbol] = "full";
return "full";
}
if (currentMode === "no-numbers") {
if (widthNoNumbers > availableWidth - hysteresisDown) {
if (widthCompactSummary <= availableWidth - hysteresisDown) {
this[layoutModeSymbol] = "compact-summary";
return "compact-summary";
}
this[layoutModeSymbol] = "compact";
return "compact";
}
if (widthFull + hysteresisUp <= availableWidth) {
this[layoutModeSymbol] = "full";
return "full";
}
this[layoutModeSymbol] = "no-numbers";
return "no-numbers";
}
if (currentMode === "compact-summary") {
if (widthNoNumbers + hysteresisUp <= availableWidth) {
this[layoutModeSymbol] = "no-numbers";
return "no-numbers";
}
if (widthCompactSummary > availableWidth - hysteresisDown) {
this[layoutModeSymbol] = "compact";
return "compact";
}
this[layoutModeSymbol] = "compact-summary";
return "compact-summary";
}
if (widthCompactSummary + hysteresisUp <= availableWidth) {
this[layoutModeSymbol] = "compact-summary";
return "compact-summary";
}
this[layoutModeSymbol] = "compact";
return "compact";
}
/**
* @private
* @return {string}
*/
function getTemplate() {
// language=HTML
return `
<template id="items">
<li part="item"><a part="link"
data-monster-attributes="class path:items.class,
href path:items.href,
aria-label path:items.description,
disabled path:items.disabled:?disabled:undefined,
data-page-no path:items.pageNo,
aria-current path:items.current"
data-monster-role="pagination-item"
data-monster-replace="path:items.label"></a></li>
</template>
<div data-monster-role="control" part="control">
<nav data-monster-role="pagination" role="navigation" aria-label="pagination" part="nav">
<ul class="pagination-list" data-monster-insert="items path:pagination.items"
data-monster-adaptive-ready="false"
data-monster-select-this="true" part="list">
<li part="prev" data-monster-role="pagination-prev"><a
data-monster-role="pagination-prev"
part="prev-link"
data-monster-attributes="
class path:pagination.prevClass | prefix: previous,
data-page-no path:pagination.prevNo,
href path:pagination.prevHref"
data-monster-replace="path:labels.previous">Previous</a></li>
<li part="summary" data-monster-role="pagination-summary">
<span data-monster-replace="path:pagination.summary"></span>
</li>
<li part="next" data-monster-role="pagination-next"><a
data-monster-role="pagination-next"
part="next-link"
data-monster-attributes="class path:pagination.nextClass | prefix: next,
data-page-no path:pagination.nextNo,
href path:pagination.nextHref"
data-monster-replace="path:labels.next">Next</a></li>
</ul>
</nav>
</div>
`;
}
registerCustomElement(Pagination);