UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

1,259 lines (1,105 loc) 32 kB
/** * 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);