lisn.js
Version:
Simply handle user gestures and actions. Includes widgets.
1 lines • 77.4 kB
Source Map (JSON)
{"version":3,"file":"pager.cjs","names":["MC","_interopRequireWildcard","require","MH","_cssAlter","_domOptimize","_domQuery","_event","_gesture","_math","_misc","_scroll","_text","_validation","_callback","_gestureWatcher","_scrollWatcher","_sizeWatcher","_viewWatcher","_widget","_debug","_interopRequireDefault","e","__esModule","default","t","WeakMap","r","n","o","i","f","__proto__","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","_defineProperty","_toPropertyKey","value","enumerable","configurable","writable","_toPrimitive","Symbol","toPrimitive","TypeError","String","Number","Pager","Widget","element","instance","DUMMY_ID","isInstanceOf","register","registerWidget","WIDGET_NAME","config","configValidator","constructor","_Pager$get","_config$nextSwitch","_config$prevSwitch","destroyPromise","destroy","id","pages","toggles","switches","nextPrevSwitch","_next","nextSwitch","_prev","prevSwitch","pageSelector","getDefaultWidgetSelector","PREFIX_PAGE__FOR_SELECT","toggleSelector","PREFIX_TOGGLE__FOR_SELECT","switchSelector","PREFIX_SWITCH__FOR_SELECT","nextSwitchSelector","PREFIX_NEXT_SWITCH__FOR_SELECT","prevSwitchSelector","PREFIX_PREV_SWITCH__FOR_SELECT","lengthOf","push","querySelectorAll","getVisibleContentChildren","filter","matches","querySelector","numPages","usageError","page","contains","components","_pages","_toggles","_switches","_nextPrevSwitch","methods","getMethods","promiseResolve","then","isDestroyed","init","nextPage","_nextPage","prevPage","_prevPage","goToPage","pageNum","_goToPage","disablePage","_disablePage","enablePage","_enablePage","togglePage","_togglePage","isPageDisabled","_isPageDisabled","getCurrentPage","_getCurrentPage","getPreviousPage","_getPreviousPage","getCurrentPageNum","_getCurrentPageNum","getPreviousPageNum","_getPreviousPageNum","onTransition","_onTransition","getPages","getSwitches","getToggles","exports","MIN_TIME_BETWEEN_WHEEL","S_CURRENT","S_ARIA_CURRENT","ARIA_PREFIX","S_COVERED","S_NEXT","S_TOTAL_PAGES","S_VISIBLE_PAGES","S_CURRENT_PAGE","S_PAGE_NUMBER","PREFIXED_NAME","prefixName","PREFIX_ROOT","PREFIX_PAGE_CONTAINER","PREFIX_PAGE","PREFIX_STYLE","PREFIX_IS_FULLSCREEN","PREFIX_USE_PARALLAX","PREFIX_TOTAL_PAGES","PREFIX_VISIBLE_PAGES","PREFIX_CURRENT_PAGE","PREFIX_CURRENT_PAGE_IS_LAST","PREFIX_CURRENT_PAGE_IS_FIRST_ENABLED","PREFIX_CURRENT_PAGE_IS_LAST_ENABLED","PREFIX_PAGE_STATE","PREFIX_PAGE_NUMBER","VAR_CURRENT_PAGE","prefixCssJsVar","VAR_TOTAL_PAGES","VAR_VISIBLE_PAGES","VAR_VISIBLE_GAPS","VAR_TRANSLATED_PAGES","VAR_TRANSLATED_GAPS","VAR_PAGE_NUMBER","SUPPORTED_STYLES","isValidStyle","includes","initialPage","validateNumber","style","key","validateString","pageSize","peek","validateBoolean","fullscreen","parallax","horizontal","useGestures","isNullish","undefined","bool","toBoolean","validateStrList","isValidInputDevice","alignGestureDirection","preventDefault","fetchClosestScrollable","waitForMeasureTime","_getClosestScrollable","getClosestScrollable","active","setPageNumber","lastPromise","el","setData","setStyleProp","setPageState","state","setCurrentPage","pagerEl","pageNumbers","isFirstEnabled","isLastEnabled","_total","_current","setBooleanData","widget","_config$initialPage","_config$pageSize","_config$peek","_config$fullscreen","_config$parallax","_config$horizontal","_config$useGestures","_config$alignGestureD","_config$preventDefaul","logger","debug","Logger","name","formatAsString","logAtCreation","pageContainer","parentOf","toInt","pagerStyle","isCarousel","minPageSize","enablePeek","isFullscreen","isParallax","isHorizontal","orientation","S_HORIZONTAL","S_VERTICAL","scrollWatcher","ScrollWatcher","reuse","sizeWatcher","SizeWatcher","resizeThreshold","gestureWatcher","GestureWatcher","viewWatcher","ViewWatcher","rootMargin","threshold","recalculateCarouselProps","data","gap","parseFloat","getComputedStyleProp","containerSize","content","S_WIDTH","S_HEIGHT","getNumVisiblePages","addPeek","numVisiblePages","max","min","floor","debug8","currPageNum","prevPageNum","numHidden","hasPeek","visibleStart","isAtEdge","numTranslated","ceil","numVisibleGaps","fractionalNumVisiblePages","getGestureOptions","directions","devices","isBoolean","intents","S_DRAG","S_SCROLL","deltaThreshold","scrollToPager","scrollTo","scrollable","transitionOnGesture","target","swapDirection","intent","S_LEFT","S_UP","direction","addWatchers","onGesture","S_RIGHT","S_DOWN","onResize","onView","views","removeWatchers","offGesture","offResize","offView","getPageNumForEvent","event","currentTargetOf","isElement","getData","toggleClickListener","switchClickListener","nextSwitchClickListener","prevSwitchClickListener","onDisable","onEnable","onDestroy","waitForMutateTime","delDataNow","PREFIX_ORIENTATION","delStylePropNow","idx","removeClassesNow","listener","removeEventListenerFrom","S_CLICK","delAttr","addClasses","disableInitialTransition","addEventListenerTo","disabledPages","callbacks","newSet","fetchScrollOptions","asFractionOf","weCanInterrupt","lastPageNum","lastTransition","canTransition","gestureData","isDisabled","device","S_WHEEL","timeNow","scroll","callback","invoke","setAttr","targetPage","S_DISABLED","handler","add","wrapCallback"],"sources":["../../../src/ts/widgets/pager.ts"],"sourcesContent":["/**\n * @module Widgets\n */\n\nimport * as MC from \"@lisn/globals/minification-constants\";\nimport * as MH from \"@lisn/globals/minification-helpers\";\n\nimport {\n Direction,\n GestureDevice,\n CommaSeparatedStr,\n} from \"@lisn/globals/types\";\n\nimport {\n disableInitialTransition,\n addClasses,\n removeClassesNow,\n getData,\n setData,\n setBooleanData,\n delDataNow,\n getComputedStyleProp,\n setStyleProp,\n delStylePropNow,\n} from \"@lisn/utils/css-alter\";\nimport {\n waitForMeasureTime,\n waitForMutateTime,\n} from \"@lisn/utils/dom-optimize\";\nimport { getVisibleContentChildren } from \"@lisn/utils/dom-query\";\nimport { addEventListenerTo, removeEventListenerFrom } from \"@lisn/utils/event\";\nimport { isValidInputDevice } from \"@lisn/utils/gesture\";\nimport { toInt } from \"@lisn/utils/math\";\nimport { toBoolean } from \"@lisn/utils/misc\";\nimport { getClosestScrollable } from \"@lisn/utils/scroll\";\nimport { formatAsString } from \"@lisn/utils/text\";\nimport {\n validateStrList,\n validateNumber,\n validateString,\n validateBoolean,\n} from \"@lisn/utils/validation\";\n\nimport { wrapCallback } from \"@lisn/modules/callback\";\n\nimport {\n GestureWatcher,\n OnGestureOptions,\n GestureData,\n} from \"@lisn/watchers/gesture-watcher\";\nimport { ScrollWatcher, ScrollOptions } from \"@lisn/watchers/scroll-watcher\";\nimport { SizeWatcher, SizeData } from \"@lisn/watchers/size-watcher\";\nimport { ViewWatcher } from \"@lisn/watchers/view-watcher\";\n\nimport {\n Widget,\n WidgetConfigValidatorObject,\n WidgetCallback,\n WidgetHandler,\n registerWidget,\n getDefaultWidgetSelector,\n} from \"@lisn/widgets/widget\";\n\nimport debug from \"@lisn/debug/debug\";\n\n/**\n * Configures the given element as a {@link Pager} widget.\n *\n * The Pager widget sets up the elements that make up its pages to be overlayed\n * on top of each other with only one of them visible at a time. When a user\n * performs a scroll-like gesture (see {@link GestureWatcher}), the pages are\n * flicked through: gestures, whose direction is down (or left) result in the\n * next page being shown, otherwise the previous.\n *\n * The widget should have more than one page.\n *\n * Optionally, the widget can have a set of \"switch\" elements and a set of\n * \"toggle\" elements which correspond one-to-one to each page. Switches go to\n * the given page and toggles toggle the enabled/disabled state of the page.\n *\n * **IMPORTANT:** Unless the {@link PagerConfig.style} is set to \"carousel\", the\n * page elements will be positioned absolutely, and therefore the pager likely\n * needs to have an explicit height. If you enable\n * {@link PagerConfig.fullscreen}, then the element will get `height: 100vh`\n * set. Otherwise, you need to set its height in your CSS.\n *\n * **IMPORTANT:** You should not instantiate more than one {@link Pager}\n * widget on a given element. Use {@link Pager.get} to get an existing\n * instance if any. If there is already a widget instance, it will be destroyed!\n *\n * -----\n *\n * You can use the following dynamic attributes or CSS properties in your\n * stylesheet:\n *\n * The following dynamic attributes are set on the pager element:\n * - `data-lisn-orientation`: `\"horizontal\"` or `\"vertical\"`\n * - `data-lisn-use-parallax`: `\"true\"` or `\"false\"`\n * - `data-lisn-total-pages`: the number of pages\n * - `data-lisn-visible-pages`: **for carousel only** the number of visible pages;\n * can be fractional if {@link PagerConfig.peek} is enabled\n * - `data-lisn-current-page`: the current page number\n * - `data-lisn-current-page-is-last`: `\"true\"` or `\"false\"`\n * - `data-lisn-current-page-is-first-enabled`: `\"true\"` or `\"false\"`\n * - `data-lisn-current-page-is-last-enabled`: `\"true\"` or `\"false\"`\n *\n * The following dynamic CSS properties are also set on the pager element's style:\n * - `--lisn-js--total-pages`: the number of pages\n * - `--lisn-js--visible-pages`: **for carousel only** the number of visible pages\n * - `--lisn-js--current-page`: the current page number\n *\n * The following dynamic attributes are set on each page, toggle or switch element:\n * - `data-lisn-page-state`: `\"current\"`, `\"next\"`, `\"covered\"` or `\"disabled\"`\n * - `data-lisn-page-number`: the corresponding page's number\n *\n * The following analogous dynamic CSS properties are also set on each page,\n * toggle or switch element's style:\n * - `--lisn-js--page-number`: the corresponding page's number\n *\n * -----\n *\n * To use with auto-widgets (HTML API) (see\n * {@link Settings.settings.autoWidgets | settings.autoWidgets}), the following\n * CSS classes or data attributes are recognized:\n * - `lisn-pager` class or `data-lisn-pager` attribute set on the container\n * element that constitutes the pager.\n * - `lisn-pager-page` class or `data-lisn-pager-page` attribute set on\n * elements that should act as the pages.\n * - `lisn-pager-toggle` class or `data-lisn-pager-toggle` attribute set on\n * elements that should act as the toggles.\n * - `lisn-pager-switch` class or `data-lisn-pager-switch` attribute set on\n * elements that should act as the switches.\n *\n * When using auto-widgets, the elements that will be used as pages are\n * discovered in the following way:\n * 1. The top-level element that constitutes the widget is searched for any\n * elements that contain the `lisn-pager-page` class or\n * `data-lisn-pager-page` attribute. They do not have to be immediate\n * children of the root element, but they **should** all be siblings.\n * 2. If there are no such elements, all of the immediate children of the\n * widget element except any `script` or `style` elements are taken as the\n * pages.\n *\n * The toggles and switches are descendants of the top-level element that\n * constitutes the widget, that contain the\n * `lisn-pager-toggle`/`lisn-pager-switch` class or `data-lisn-pager-toggle`/\n * `data-lisn-pager-switch` attribute. They do not have to be immediate\n * children of the root element.\n *\n * See below examples for what values you can use set for the data attributes\n * in order to modify the configuration of the automatically created widget.\n *\n * @example\n * This defines a simple pager with default settings.\n *\n * ```html\n * <div class=\"lisn-pager\">\n * <div>Vertical: Page 1</div>\n * <div>Vertical: Page 2</div>\n * <div>Vertical: Page 3</div>\n * <div>Vertical: Page 4</div>\n * </div>\n * ```\n *\n * @example\n * As above but with all settings explicitly set to their default (`false`).\n *\n * ```html\n * <div data-lisn-pager=\"fullscreen=false | parallax=false | horizontal=false\">\n * <div>Vertical: Page 1</div>\n * <div>Vertical: Page 2</div>\n * <div>Vertical: Page 3</div>\n * <div>Vertical: Page 4</div>\n * </div>\n * ```\n *\n * @example\n * This defines a pager with custom settings.\n *\n * ```html\n * <div data-lisn-pager=\"fullscreen | parallax | horizontal | gestures=false\">\n * <div>Horizontal: Page 1</div>\n * <div>Horizontal: Page 2</div>\n * <div>Horizontal: Page 3</div>\n * <div>Horizontal: Page 4</div>\n * </div>\n * ```\n *\n * @example\n * This defines a pager with custom settings, as well as toggle and switch buttons.\n *\n * ```html\n * <div data-lisn-pager=\"fullscreen | parallax | horizontal | gestures=false\">\n * <div>\n * <div data-lisn-pager-page>Horizontal: Page 1</div>\n * <div data-lisn-pager-page>Horizontal: Page 2</div>\n * <div data-lisn-pager-page>Horizontal: Page 3</div>\n * <div data-lisn-pager-page>Horizontal: Page 4</div>\n * </div>\n *\n * <div>\n * <button data-lisn-pager-toggle>Toggle page 1</button>\n * <button data-lisn-pager-toggle>Toggle page 2</button>\n * <button data-lisn-pager-toggle>Toggle page 3</button>\n * <button data-lisn-pager-toggle>Toggle page 4</button>\n * </div>\n *\n * <div>\n * <button data-lisn-pager-switch>Go to page 1</button>\n * <button data-lisn-pager-switch>Go to page 2</button>\n * <button data-lisn-pager-switch>Go to page 3</button>\n * <button data-lisn-pager-switch>Go to page 4</button>\n * </div>\n * </div>\n * ```\n */\nexport class Pager extends Widget {\n /**\n * Advances the widget's page by 1. If the current page is the last one,\n * nothing is done, unless {@link PagerConfig.fullscreen} is true in which\n * case the pager's scrollable ancestor will be scrolled down/right\n * (depending on the gesture direction).\n */\n readonly nextPage: () => Promise<void>;\n\n /**\n * Reverses the widget's page by 1. If the current page is the first one,\n * nothing is done, unless {@link PagerConfig.fullscreen} is true in which\n * case the pager's scrollable ancestor will be scrolled up/left\n * (depending on the gesture direction).\n */\n readonly prevPage: () => Promise<void>;\n\n /**\n * Advances the widget to the given page. Note that page numbers start at 1.\n *\n * If this page is disabled, nothing is done.\n */\n readonly goToPage: (pageNum: number) => Promise<void>;\n\n /**\n * Disables the given page number. Note that page numbers start at 1.\n *\n * The page will be skipped during transitioning to previous/next.\n *\n * If the page is the current one, then the current page will be switched to\n * the previous one (that's not disabled), or if this is the first enabled\n * page, then it will switch to the next one that's not disabled.\n *\n * If this is the last enabled page, nothing is done.\n */\n readonly disablePage: (pageNum: number) => Promise<void>;\n\n /**\n * Re-enables the given page number. Note that page numbers start at 1.\n */\n readonly enablePage: (pageNum: number) => Promise<void>;\n\n /**\n * Re-enables the given page if it is disabled, otherwise disables it. Note\n * that page numbers start at 1.\n */\n readonly togglePage: (pageNum: number) => Promise<void>;\n\n /**\n * Returns true if the given page number is disabled. Note that page\n * numbers start at 1.\n */\n readonly isPageDisabled: (pageNum: number) => boolean;\n\n /**\n * Returns the current page.\n */\n readonly getCurrentPage: () => Element;\n\n /**\n * Returns the previous page, before the last transition.\n *\n * If there has been no change of page, returns the first page, same as\n * {@link getCurrentPageNum}.\n */\n readonly getPreviousPage: () => Element;\n\n /**\n * Returns the number of the current page. Note that numbers start at 1.\n */\n readonly getCurrentPageNum: () => number;\n\n /**\n * Returns the number of the previous page, before the last transition. Note\n * that numbers start at 1.\n *\n * If there has been no change of page, returns the number of the first page,\n * same as {@link getCurrentPageNum}.\n */\n readonly getPreviousPageNum: () => number;\n\n /**\n * The given handler will be called whenever there is a change of page. It\n * will be called after the current page number is updated internally (so\n * {@link getPreviousPageNum} and {@link getCurrentPageNum} will return the\n * updated numbers), but before the page transition styles are updated.\n *\n * If the handler returns a promise, it will be awaited upon.\n */\n readonly onTransition: (handler: WidgetHandler) => void;\n\n /**\n * Returns the page elements.\n */\n readonly getPages: () => Element[];\n\n /**\n * Returns the toggle elements if any.\n */\n readonly getToggles: () => Element[];\n\n /**\n * Returns the switch elements if any.\n */\n readonly getSwitches: () => Element[];\n\n static get(element: Element): Pager | null {\n const instance = super.get(element, DUMMY_ID);\n if (MH.isInstanceOf(instance, Pager)) {\n return instance;\n }\n return null;\n }\n\n static register() {\n registerWidget(\n WIDGET_NAME,\n (element, config) => {\n if (!Pager.get(element)) {\n return new Pager(element, config);\n }\n return null;\n },\n configValidator,\n );\n }\n\n /**\n * @throws {@link Errors.LisnUsageError | LisnUsageError}\n * If there are less than 2 pages given or found, or if any\n * page is not a descendant of the main pager element.\n */\n constructor(element: Element, config?: PagerConfig) {\n const destroyPromise = Pager.get(element)?.destroy();\n super(element, { id: DUMMY_ID });\n\n const pages = config?.pages || [];\n const toggles = config?.toggles || [];\n const switches = config?.switches || [];\n const nextPrevSwitch = {\n _next: config?.nextSwitch ?? null,\n _prev: config?.prevSwitch ?? null,\n };\n\n const pageSelector = getDefaultWidgetSelector(PREFIX_PAGE__FOR_SELECT);\n const toggleSelector = getDefaultWidgetSelector(PREFIX_TOGGLE__FOR_SELECT);\n const switchSelector = getDefaultWidgetSelector(PREFIX_SWITCH__FOR_SELECT);\n const nextSwitchSelector = getDefaultWidgetSelector(\n PREFIX_NEXT_SWITCH__FOR_SELECT,\n );\n const prevSwitchSelector = getDefaultWidgetSelector(\n PREFIX_PREV_SWITCH__FOR_SELECT,\n );\n\n if (!MH.lengthOf(pages)) {\n pages.push(...MH.querySelectorAll(element, pageSelector));\n\n if (!MH.lengthOf(pages)) {\n pages.push(\n ...getVisibleContentChildren(element).filter(\n (e) => !e.matches(switchSelector),\n ),\n );\n }\n }\n\n if (!MH.lengthOf(toggles)) {\n toggles.push(...MH.querySelectorAll(element, toggleSelector));\n }\n\n if (!MH.lengthOf(switches)) {\n switches.push(...MH.querySelectorAll(element, switchSelector));\n }\n\n if (!nextPrevSwitch._next) {\n nextPrevSwitch._next = MH.querySelector(element, nextSwitchSelector);\n }\n\n if (!nextPrevSwitch._prev) {\n nextPrevSwitch._prev = MH.querySelector(element, prevSwitchSelector);\n }\n\n const numPages = MH.lengthOf(pages);\n if (numPages < 2) {\n throw MH.usageError(\"Pager must have more than 1 page\");\n }\n\n for (const page of pages) {\n if (!element.contains(page) || page === element) {\n throw MH.usageError(\"Pager's pages must be its descendants\");\n }\n }\n\n const components = {\n _pages: pages,\n _toggles: toggles,\n _switches: switches,\n _nextPrevSwitch: nextPrevSwitch,\n };\n\n const methods = getMethods(this, components, element, config);\n\n (destroyPromise || MH.promiseResolve()).then(() => {\n if (this.isDestroyed()) {\n return;\n }\n\n init(this, element, components, config, methods);\n });\n\n this.nextPage = () => methods._nextPage();\n this.prevPage = () => methods._prevPage();\n this.goToPage = (pageNum) => methods._goToPage(pageNum);\n this.disablePage = methods._disablePage;\n this.enablePage = methods._enablePage;\n this.togglePage = methods._togglePage;\n this.isPageDisabled = methods._isPageDisabled;\n this.getCurrentPage = methods._getCurrentPage;\n this.getPreviousPage = methods._getPreviousPage;\n this.getCurrentPageNum = methods._getCurrentPageNum;\n this.getPreviousPageNum = methods._getPreviousPageNum;\n this.onTransition = methods._onTransition;\n\n this.getPages = () => [...pages];\n this.getSwitches = () => [...switches];\n this.getToggles = () => [...toggles];\n }\n}\n\n/**\n * @interface\n */\nexport type PagerConfig = {\n /**\n * The elements that will make up the pages.\n *\n * They do not have to be immediate children of the root element, but they\n * should all be siblings.\n *\n * The widget should have more than one page.\n *\n * If this is not specified, then\n * 1. The top-level element that constitutes the widget is searched for any\n * elements that contain the `lisn-pager-page` class or the\n * `data-lisn-pager-page` attribute, and these are used as pages.\n * 2. If there are no such elements, all of the immediate children of the\n * widget element (other than `script` and `style` elements) are taken as\n * the pages.\n *\n * **IMPORTANT:** Unless the {@link style} is set to \"carousel\", the page\n * elements will be positioned absolutely, and therefore the pager likely\n * needs to have an explicit height. If you enable {@link fullscreen}, then\n * the element will get `height: 100vh` set. Otherwise, you need to set its\n * height in your CSS.\n *\n * @defaultValue undefined\n */\n pages?: Element[];\n\n /**\n * If given, these elements should match one-to-one the page elements.\n *\n * Each toggle element will be assigned a click listener that toggles the\n * enabled/disabled state of the page.\n *\n * If this is not specified, then the top-level element that constitutes the\n * widget is searched for any elements that contain the `lisn-pager-toggle`\n * class or the `data-lisn-pager-toggle` attribute, and these are used as\n * toggles.\n *\n * @defaultValue undefined\n */\n toggles?: Element[];\n\n /**\n * If given, these elements should match one-to-one the page elements.\n *\n * Each toggle element will be assigned a click listener that switches the\n * pager to the page.\n *\n * If this is not specified, then the top-level element that constitutes the\n * widget is searched for any elements that contain the `lisn-pager-switch`\n * class or the `data-lisn-pager-switch` attribute, and these are used as\n * switches.\n *\n * @defaultValue undefined\n */\n switches?: Element[];\n\n /**\n * This element will be assigned a click listener that goes to the next page.\n *\n * If this is not given, then the top-level element that constitutes the\n * widget is searched for the first element that contains the\n * `lisn-pager-next-switch` class or the `data-lisn-pager-next-switch`\n * attribute, and this is used.\n *\n * @defaultValue undefined\n */\n nextSwitch?: Element;\n\n /**\n * This element will be assigned a click listener that goes to the previous\n * page.\n *\n * If this is not given, then the top-level element that constitutes the\n * widget is searched for the first element that contains the\n * `lisn-pager-prev-switch` class or the `data-lisn-pager-prev-switch`\n * attribute, and this is used.\n *\n * @defaultValue undefined\n */\n prevSwitch?: Element;\n\n /**\n * Set the initial page number.\n *\n * @defaultValue 1\n */\n initialPage?: number;\n\n /**\n * Set the style of the widget. This determines the basic CSS applied.\n *\n * When importing the stylesheets in your project, if not using the full\n * stylesheet (lisn.css) you can import either pager.css which contains all\n * three pager styles, or only `pager-<style>.css`.\n *\n * **NOTE:** The base css only includes the minimum required for positioning\n * and transitioning pages. The switches and toggles are intentionally not\n * styled for flexibility. You should style those in your CSS.\n *\n * **IMPORTANT:** Unless the {@link style} is set to \"carousel\", the page\n * elements will be positioned absolutely, and therefore the pager likely\n * needs to have an explicit height. If you enable {@link fullscreen}, then\n * the element will get `height: 100vh` set. Otherwise, you need to set its\n * height in your CSS.\n *\n * @since v1.1.0\n *\n * @defaultValue \"slider\"\n */\n style?: \"slider\" | \"carousel\" | \"tabs\";\n\n /**\n * Only relevant is {@link style} is \"carousel\".\n *\n * The *minimum* page height (or width in {@link horizontal} mode) in pixels.\n * This will determine the number of visible slides at any one width of the\n * pager. Pages will still grow to fill the size of the carousel itself.\n *\n * @since Introduced in v1.1.0.\n *\n * @defaultValue 300\n */\n pageSize?: number;\n\n /**\n * Only relevant is {@link style} is \"carousel\".\n *\n * Whether to show a bit of the upcoming and/or previous pages that are\n * hidden when not all fit.\n *\n * @since Introduced in v1.1.0.\n *\n * @defaultValue false\n */\n peek?: boolean;\n\n /**\n * If true, then:\n * - if the pager {@link style} is \"slider\", the pager's height will be set\n * to the viewport height (100vh)\n * - the pager's scrolling ancestor will be scrolled to the top of the pager\n * when 30% of it is in view\n * - scrolling beyond the first or last page will initiate scroll up/left or\n * down/right the pager's scrolling ancestor in order to allow \"leaving\"\n * the pager\n *\n * Note that except in \"carousel\" {@link style} the pager's pages will be\n * positioned absolutely, and so if you do _not_ enable this option, you will\n * need to manually set the height of the page parent element via CSS.\n *\n * @defaultValue false\n */\n fullscreen?: boolean;\n\n /**\n * Only relevant is {@link style} is \"slider\" (default) or \"carousel\".\n *\n * Use a parallax effect for switching pages.\n *\n * @defaultValue false\n */\n parallax?: boolean;\n\n /**\n * Only relevant is {@link style} is \"slider\" (default) or \"carousel\".\n *\n * Transition pages sideways instead of vertically.\n *\n * @defaultValue false\n */\n horizontal?: boolean;\n\n /**\n * Transition pages upon user scroll-like and drag-like gestures via the\n * given {@link GestureDevice}s. If set to true, then gestures using all\n * device types are supported.\n *\n * Note that drag gesture is only supported by the \"pointer\" device and also\n * the \"pointer\" device only supports drag gestures, so if you want to\n * disable drag gestures, simply pass \"wheel,key,touch\" as this option.\n *\n * @defaultValue true\n */\n useGestures?: boolean | CommaSeparatedStr<GestureDevice> | GestureDevice[];\n\n /**\n * If true, then pages will be advanced backwards/forwards regardless if the\n * gesture direction is horizontal or vertical.\n *\n * If false then, a gesture will go to the next page only if its direction is\n * down if {@link horizontal} is false/right if {@link horizontal} is true,\n * and to the previous page only if the gesture direction is up if\n * {@link horizontal} is false/left if {@link horizontal} is true.\n *\n * **IMPORTANT:**\n * If {@link fullscreen}, {@link preventDefault} and\n * {@link alignGestureDirection} are all true, then the pager's scrollable\n * parent must be scrollable in the same direction as the pager orientation,\n * otherwise automatic scroll beyond the last/first page won't work.\n *\n * @defaultValue false\n */\n alignGestureDirection?: boolean;\n\n /**\n * Whether to prevent the default action for events that would result in\n * gestures.\n *\n * **NOTE:**\n * If true (default), then all events that originate from a device given in\n * {@link useGestures} and that could result in a gesture will be prevented\n * regardless of their direction and whether {@link alignGestureDirection} is\n * true.\n *\n * @defaultValue true\n */\n preventDefault?: boolean;\n};\n\n// --------------------\n\ntype Components = {\n _pages: Element[];\n _toggles: Element[];\n _switches: Element[];\n _nextPrevSwitch: { _next: Element | null; _prev: Element | null };\n};\n\n// Swiping on some trackpads results in \"trailing\" wheel events sent for some\n// while which results in multiple pages being advanced in a short while. So we\n// limit how often pages can be advanced.\nconst MIN_TIME_BETWEEN_WHEEL = 1000;\n\nconst S_CURRENT = \"current\";\nconst S_ARIA_CURRENT = MC.ARIA_PREFIX + S_CURRENT;\nconst S_COVERED = \"covered\";\nconst S_NEXT = \"next\";\nconst S_TOTAL_PAGES = \"total-pages\";\nconst S_VISIBLE_PAGES = \"visible-pages\";\nconst S_CURRENT_PAGE = \"current-page\";\nconst S_PAGE_NUMBER = \"page-number\";\nconst WIDGET_NAME = \"pager\";\nconst PREFIXED_NAME = MH.prefixName(WIDGET_NAME);\nconst PREFIX_ROOT = `${PREFIXED_NAME}__root`;\nconst PREFIX_PAGE_CONTAINER = `${PREFIXED_NAME}__page-container`;\n\n// Use different classes for styling items to the one used for auto-discovering\n// them, so that re-creating existing widgets can correctly find the items to\n// be used by the new widget synchronously before the current one is destroyed.\nconst PREFIX_PAGE = `${PREFIXED_NAME}__page`;\nconst PREFIX_PAGE__FOR_SELECT = `${PREFIXED_NAME}-page`;\nconst PREFIX_TOGGLE__FOR_SELECT = `${PREFIXED_NAME}-toggle`;\nconst PREFIX_SWITCH__FOR_SELECT = `${PREFIXED_NAME}-switch`;\nconst PREFIX_NEXT_SWITCH__FOR_SELECT = `${PREFIXED_NAME}-next-switch`;\nconst PREFIX_PREV_SWITCH__FOR_SELECT = `${PREFIXED_NAME}-prev-switch`;\n\nconst PREFIX_STYLE = `${PREFIXED_NAME}-style`;\nconst PREFIX_IS_FULLSCREEN = MH.prefixName(\"is-fullscreen\");\nconst PREFIX_USE_PARALLAX = MH.prefixName(\"use-parallax\");\nconst PREFIX_TOTAL_PAGES = MH.prefixName(S_TOTAL_PAGES);\nconst PREFIX_VISIBLE_PAGES = MH.prefixName(S_VISIBLE_PAGES);\nconst PREFIX_CURRENT_PAGE = MH.prefixName(S_CURRENT_PAGE);\nconst PREFIX_CURRENT_PAGE_IS_LAST = `${PREFIX_CURRENT_PAGE}-is-last`;\nconst PREFIX_CURRENT_PAGE_IS_FIRST_ENABLED = `${PREFIX_CURRENT_PAGE}-is-first-enabled`;\nconst PREFIX_CURRENT_PAGE_IS_LAST_ENABLED = `${PREFIX_CURRENT_PAGE_IS_LAST}-enabled`;\nconst PREFIX_PAGE_STATE = MH.prefixName(\"page-state\");\nconst PREFIX_PAGE_NUMBER = MH.prefixName(S_PAGE_NUMBER);\nconst VAR_CURRENT_PAGE = MH.prefixCssJsVar(S_CURRENT_PAGE);\nconst VAR_TOTAL_PAGES = MH.prefixCssJsVar(S_TOTAL_PAGES);\nconst VAR_VISIBLE_PAGES = MH.prefixCssJsVar(S_VISIBLE_PAGES);\nconst VAR_VISIBLE_GAPS = MH.prefixCssJsVar(\"visible-gaps\");\nconst VAR_TRANSLATED_PAGES = MH.prefixCssJsVar(\"translated-pages\");\nconst VAR_TRANSLATED_GAPS = MH.prefixCssJsVar(\"translated-gaps\");\nconst VAR_PAGE_NUMBER = MH.prefixCssJsVar(S_PAGE_NUMBER);\n\n// Only one Pager widget per element is allowed, but Widget requires a\n// non-blank ID.\nconst DUMMY_ID = PREFIXED_NAME;\n\nconst SUPPORTED_STYLES = [\"slider\", \"carousel\", \"tabs\"] as const;\ntype PagerStyle = (typeof SUPPORTED_STYLES)[number];\n\nconst isValidStyle = (value: string): value is PagerStyle =>\n MH.includes(SUPPORTED_STYLES, value);\n\nconst configValidator: WidgetConfigValidatorObject<PagerConfig> = {\n initialPage: validateNumber,\n style: (key, value) => validateString(key, value, isValidStyle),\n pageSize: validateNumber,\n peek: validateBoolean,\n fullscreen: validateBoolean,\n parallax: validateBoolean,\n horizontal: validateBoolean,\n useGestures: (key, value) => {\n if (MH.isNullish(value)) {\n return undefined;\n }\n\n const bool = toBoolean(value);\n if (bool !== null) {\n return bool;\n }\n\n return (\n validateStrList(\n \"useGestures\",\n validateString(key, value),\n isValidInputDevice,\n ) || true\n );\n },\n alignGestureDirection: validateBoolean,\n preventDefault: validateBoolean,\n};\n\nconst fetchClosestScrollable = (element: Element) =>\n waitForMeasureTime().then(\n () => getClosestScrollable(element, { active: true }) ?? undefined,\n );\n\nconst setPageNumber = (components: Components, pageNum: number) => {\n let lastPromise: Promise<void> = MH.promiseResolve();\n for (const el of [\n components._pages[pageNum - 1],\n components._toggles[pageNum - 1],\n components._switches[pageNum - 1],\n ] as const) {\n if (el) {\n setData(el, PREFIX_PAGE_NUMBER, pageNum + \"\");\n lastPromise = setStyleProp(el, VAR_PAGE_NUMBER, pageNum + \"\");\n }\n }\n\n return lastPromise;\n};\n\nconst setPageState = async (\n components: Components,\n pageNum: number,\n state: string,\n) => {\n for (const el of [\n components._pages[pageNum - 1],\n components._toggles[pageNum - 1],\n components._switches[pageNum - 1],\n ] as const) {\n if (el) {\n await setData(el, PREFIX_PAGE_STATE, state);\n }\n }\n};\n\nconst setCurrentPage = (\n pagerEl: Element,\n pageNumbers: {\n _current: number;\n _total: number;\n },\n isPageDisabled: (pageNum: number) => boolean,\n) => {\n let isFirstEnabled = true;\n let isLastEnabled = true;\n for (let n = 1; n <= pageNumbers._total; n++) {\n if (!isPageDisabled(n)) {\n if (n < pageNumbers._current) {\n isFirstEnabled = false;\n } else if (n > pageNumbers._current) {\n isLastEnabled = false;\n }\n }\n }\n\n setStyleProp(pagerEl, VAR_CURRENT_PAGE, pageNumbers._current + \"\");\n setData(pagerEl, PREFIX_CURRENT_PAGE, pageNumbers._current + \"\");\n setBooleanData(\n pagerEl,\n PREFIX_CURRENT_PAGE_IS_LAST,\n pageNumbers._current === pageNumbers._total,\n );\n setBooleanData(pagerEl, PREFIX_CURRENT_PAGE_IS_FIRST_ENABLED, isFirstEnabled);\n return setBooleanData(\n pagerEl,\n PREFIX_CURRENT_PAGE_IS_LAST_ENABLED,\n isLastEnabled,\n );\n};\n\nconst init = (\n widget: Pager,\n element: Element,\n components: Components,\n config: PagerConfig | undefined,\n methods: ReturnType<typeof getMethods>,\n) => {\n const logger = debug\n ? new debug.Logger({\n name: `Pager-${formatAsString(element)}`,\n logAtCreation: config,\n })\n : null;\n\n const pages = components._pages;\n const toggles = components._toggles;\n const switches = components._switches;\n const nextSwitch = components._nextPrevSwitch._next;\n const prevSwitch = components._nextPrevSwitch._prev;\n const pageContainer = MH.parentOf(pages[0]);\n\n let initialPage = toInt(config?.initialPage ?? 1);\n const pagerStyle = config?.style || \"slider\";\n const isCarousel = pagerStyle === \"carousel\";\n const minPageSize = config?.pageSize ?? 300;\n const enablePeek = config?.peek ?? false;\n const isFullscreen = config?.fullscreen ?? false;\n const isParallax = config?.parallax ?? false;\n const isHorizontal = config?.horizontal ?? false;\n const orientation = isHorizontal ? MC.S_HORIZONTAL : MC.S_VERTICAL;\n const useGestures = config?.useGestures ?? true;\n const alignGestureDirection = config?.alignGestureDirection ?? false;\n const preventDefault = config?.preventDefault ?? true;\n\n const scrollWatcher = ScrollWatcher.reuse();\n const sizeWatcher = isCarousel\n ? SizeWatcher.reuse({ resizeThreshold: 10 })\n : null;\n const gestureWatcher = useGestures ? GestureWatcher.reuse() : null;\n const viewWatcher = isFullscreen\n ? ViewWatcher.reuse({ rootMargin: \"0px\", threshold: 0.3 })\n : null;\n\n const recalculateCarouselProps = async (t?: Element, data?: SizeData) => {\n if (data) {\n // there's been a resize\n const gap =\n MH.parseFloat(await getComputedStyleProp(element, \"gap\")) || 0;\n const containerSize =\n data.content[isHorizontal ? MC.S_WIDTH : MC.S_HEIGHT];\n\n const getNumVisiblePages = (addPeek = false) =>\n (numVisiblePages = MH.max(\n 1, // at least 1\n MH.min(\n MH.floor(\n (containerSize + gap - (addPeek ? 0.5 * minPageSize : 0)) /\n (minPageSize + gap),\n ),\n numPages, // and at most total number\n ),\n ));\n\n numVisiblePages = getNumVisiblePages();\n if (enablePeek && numVisiblePages < numPages) {\n // Not all pages fit now and we will add a \"peek\" from the pages on the\n // edge.\n // Re-calculate with peek added in case the resultant page size when we\n // add the \"peek\" will make it smaller than the min.\n numVisiblePages = getNumVisiblePages(true);\n }\n\n debug: logger?.debug8(\"Pager resized\", {\n gap,\n containerSize,\n numVisiblePages,\n });\n } // otherwise just a page transition\n\n const currPageNum = widget.getCurrentPageNum();\n const prevPageNum = widget.getPreviousPageNum();\n const numHidden = numPages - numVisiblePages;\n const hasPeek = enablePeek && numVisiblePages < numPages;\n\n // centre the current page as much as possible\n let visibleStart = currPageNum - (numVisiblePages - 1) / 2;\n let isAtEdge = false;\n\n if (visibleStart >= numHidden + 1) {\n visibleStart = numHidden + 1;\n isAtEdge = true;\n } else if (visibleStart <= 1) {\n visibleStart = 1;\n isAtEdge = true;\n }\n\n let numTranslated = 0;\n if (hasPeek) {\n numTranslated = MH.max(0, visibleStart - 1 - (isAtEdge ? 0.5 : 0.25));\n } else {\n numTranslated =\n (prevPageNum > currPageNum ? MH.floor : MH.ceil)(visibleStart) - 1;\n }\n\n const numVisibleGaps = !hasPeek\n ? numVisiblePages - 1\n : isAtEdge || numVisiblePages % 2 === 0\n ? numVisiblePages\n : numVisiblePages + 1;\n\n const fractionalNumVisiblePages = hasPeek\n ? numVisiblePages + 0.5\n : numVisiblePages;\n\n debug: logger?.debug8(\"Carousel calculations\", {\n currPageNum,\n prevPageNum,\n visibleStart,\n isAtEdge,\n numVisiblePages,\n numVisibleGaps,\n numTranslated,\n });\n\n setData(element, PREFIX_VISIBLE_PAGES, fractionalNumVisiblePages + \"\");\n setStyleProp(element, VAR_VISIBLE_PAGES, fractionalNumVisiblePages + \"\");\n setStyleProp(element, VAR_VISIBLE_GAPS, numVisibleGaps + \"\");\n setStyleProp(element, VAR_TRANSLATED_PAGES, numTranslated + \"\");\n setStyleProp(element, VAR_TRANSLATED_GAPS, MH.floor(numTranslated) + \"\");\n };\n\n const getGestureOptions = (\n directions: Direction | Direction[] | undefined,\n ): OnGestureOptions => {\n return {\n devices: MH.isBoolean(useGestures) // i.e. true; if it's false, then gestureWatcher is null\n ? undefined // all devices\n : useGestures,\n intents: [MC.S_DRAG, MC.S_SCROLL],\n directions,\n deltaThreshold: 25,\n preventDefault,\n };\n };\n\n const scrollToPager = async () => {\n scrollWatcher.scrollTo(element, {\n scrollable: await fetchClosestScrollable(element),\n });\n };\n\n const transitionOnGesture = (target: EventTarget, data: GestureData) => {\n const swapDirection = data.intent === MC.S_DRAG;\n\n if (MH.includes([MC.S_LEFT, MC.S_UP], data.direction)) {\n (swapDirection ? methods._nextPage : methods._prevPage)(data);\n } else {\n (swapDirection ? methods._prevPage : methods._nextPage)(data);\n }\n };\n\n const addWatchers = () => {\n gestureWatcher?.onGesture(\n element,\n transitionOnGesture,\n getGestureOptions(\n alignGestureDirection\n ? isHorizontal\n ? [MC.S_LEFT, MC.S_RIGHT]\n : [MC.S_UP, MC.S_DOWN]\n : undefined, // all directions\n ),\n );\n\n sizeWatcher?.onResize(recalculateCarouselProps, { target: element });\n viewWatcher?.onView(element, scrollToPager, { views: \"at\" });\n };\n\n const removeWatchers = () => {\n gestureWatcher?.offGesture(element, transitionOnGesture);\n sizeWatcher?.offResize(recalculateCarouselProps, element);\n viewWatcher?.offView(element, scrollToPager);\n };\n\n const getPageNumForEvent = (event: Event) => {\n const target = MH.currentTargetOf(event);\n return MH.isElement(target)\n ? toInt(getData(target, PREFIX_PAGE_NUMBER))\n : 0;\n };\n\n const toggleClickListener = (event: Event) => {\n const pageNum = getPageNumForEvent(event);\n methods._togglePage(pageNum);\n };\n\n const switchClickListener = (event: Event) => {\n const pageNum = getPageNumForEvent(event);\n methods._goToPage(pageNum);\n };\n\n const nextSwitchClickListener = () => methods._nextPage();\n const prevSwitchClickListener = () => methods._prevPage();\n\n // SETUP ------------------------------\n\n widget.onDisable(removeWatchers);\n widget.onEnable(addWatchers);\n\n widget.onDestroy(async () => {\n await waitForMutateTime();\n delDataNow(element, MC.PREFIX_ORIENTATION);\n delDataNow(element, PREFIX_STYLE);\n delDataNow(element, PREFIX_IS_FULLSCREEN);\n delDataNow(element, PREFIX_USE_PARALLAX);\n delDataNow(element, PREFIX_CURRENT_PAGE);\n delDataNow(element, PREFIX_CURRENT_PAGE_IS_LAST);\n delDataNow(element, PREFIX_CURRENT_PAGE_IS_FIRST_ENABLED);\n delDataNow(element, PREFIX_CURRENT_PAGE_IS_LAST_ENABLED);\n delDataNow(element, PREFIX_TOTAL_PAGES);\n delDataNow(element, PREFIX_VISIBLE_PAGES);\n delStylePropNow(element, VAR_CURRENT_PAGE);\n delStylePropNow(element, VAR_TOTAL_PAGES);\n delStylePropNow(element, VAR_VISIBLE_PAGES);\n delStylePropNow(element, VAR_VISIBLE_GAPS);\n delStylePropNow(element, VAR_TRANSLATED_PAGES);\n delStylePropNow(element, VAR_TRANSLATED_GAPS);\n\n for (let idx = 0; idx < MH.lengthOf(pages); idx++) {\n removeClassesNow(pages[idx], PREFIX_PAGE);\n\n for (const [el, listener] of [\n [pages[idx], null],\n [toggles[idx], toggleClickListener],\n [switches[idx], switchClickListener],\n ] as const) {\n if (el) {\n delDataNow(el, PREFIX_PAGE_STATE);\n delDataNow(el, PREFIX_PAGE_NUMBER);\n delStylePropNow(el, VAR_PAGE_NUMBER);\n if (listener) {\n removeEventListenerFrom(el, MC.S_CLICK, listener);\n }\n }\n }\n\n MH.delAttr(pages[idx], S_ARIA_CURRENT);\n }\n\n if (nextSwitch) {\n removeEventListenerFrom(nextSwitch, MC.S_CLICK, nextSwitchClickListener);\n }\n\n if (prevSwitch) {\n removeEventListenerFrom(prevSwitch, MC.S_CLICK, prevSwitchClickListener);\n }\n\n removeClassesNow(element, PREFIX_ROOT);\n if (pageContainer) {\n removeClassesNow(pageContainer, PREFIX_PAGE_CONTAINER);\n }\n });\n\n if (isCarousel) {\n widget.onTransition(() => recalculateCarouselProps());\n }\n\n addWatchers();\n addClasses(element, PREFIX_ROOT);\n if (pageContainer) {\n addClasses(pageContainer, PREFIX_PAGE_CONTAINER);\n }\n\n const numPages = MH.lengthOf(pages);\n let numVisiblePages = numPages;\n\n setData(element, MC.PREFIX_ORIENTATION, orientation);\n setData(element, PREFIX_STYLE, pagerStyle);\n setBooleanData(element, PREFIX_IS_FULLSCREEN, isFullscreen);\n setBooleanData(element, PREFIX_USE_PARALLAX, isParallax);\n setData(element, PREFIX_TOTAL_PAGES, numPages + \"\");\n setStyleProp(element, VAR_TOTAL_PAGES, (numPages || 1) + \"\");\n\n for (const page of pages) {\n disableInitialTransition(page);\n addClasses(page, PREFIX_PAGE);\n }\n\n for (let idx = 0; idx < numPages; idx++) {\n setPageNumber(components, idx + 1);\n setPageState(components, idx + 1, S_NEXT);\n\n for (const [el, listener] of [\n [toggles[idx], toggleClickListener],\n [switches[idx], switchClickListener],\n ] as const) {\n if (el) {\n addEventListenerTo(el, MC.S_CLICK, listener);\n }\n }\n }\n\n if (nextSwitch) {\n addEventListenerTo(nextSwitch, MC.S_CLICK, nextSwitchClickListener);\n }\n\n if (prevSwitch) {\n addEventListenerTo(prevSwitch, MC.S_CLICK, prevSwitchClickListener);\n }\n\n if (initialPage < 1 || initialPage > numPages) {\n initialPage = 1;\n }\n methods._goToPage(initialPage);\n};\n\nconst getMethods = (\n widget: Pager,\n components: Components,\n element: Element,\n config: PagerConfig | undefined,\n) => {\n const pages = components._pages;\n const scrollWatcher = ScrollWatcher.reuse();\n const isFullscreen = config?.fullscreen;\n const disabledPages: Record<number, boolean> = {};\n const callbacks = MH.newSet<WidgetCallback>();\n\n const fetchScrollOptions = async (): Promise<ScrollOptions> => ({\n scrollable: await fetchClosestScrollable(element),\n // default amount is already 100%\n asFractionOf: \"visible\",\n weCanInterrupt: true,\n });\n\n let currPageNum = -1;\n let lastPageNum = -1;\n let lastTransition = 0;\n\n const canTransition = (gestureData?: GestureData) => {\n if (widget.isDisabled()) {\n return false;\n }\n\n if (gestureData?.device !== MC.S_WHEEL) {\n return true;\n }\n\n const timeNow = MH.timeNow();\n if (timeNow - lastTransition > MIN_TIME_BETWEEN_WHEEL) {\n lastTransition = timeNow;\n return true;\n }\n\n return false;\n };\n\n const goToPage = async (pageNum: number, gestureData?: GestureData) => {\n pageNum = toInt(pageNum, -1);\n if (pageNum === currPageNum || !canTransition(gestureData)) {\n return;\n }\n\n const numPages = MH.lengthOf(pages);\n\n if (\n (currPageNum === 1 && pageNum === 0) ||\n (currPageNum === numPages && pageNum === numPages + 1)\n ) {\n // next/prev page beyond last/first\n if (isFullscreen) {\n scrollWatcher.scroll(\n pageNum\n ? gestureData?.direction === MC.S_RIGHT\n ? MC.S_RIGHT\n : MC.S_DOWN\n : gestureData?.direction === MC.S_LEFT\n ? MC.S_LEFT\n : MC.S_UP,\n await fetchScrollOptions(),\n ); // no need to await\n }\n\n return;\n }\n\n if (isPageDisabled(pageNum) || pageNum < 1 || pageNum > numPages) {\n // invalid or disabled\n return;\n }\n\n lastPageNum = currPageNum > 0 ? currPageNum : pageNum;\n currPageNum = pageNum;\n\n for (const callback of callbacks) {\n await callback.invoke(widget);\n }\n\n MH.delAttr(pages[lastPageNum - 1], S_ARIA_CURRENT);\n for (\n let n = lastPageNum;\n n !== currPageNum;\n currPageNum < lastPageNum ? n-- : n++\n ) {\n if (!isPageDisabled(n)) {\n setPageState(\n components,\n n,\n currPageNum < lastPageNum ? S_NEXT : S_COVERED,\n );\n }\n }\n\n setCurrentPage(\n element,\n { _current: currPageNum, _total: numPages },\n isPageDisabled,\n );\n MH.setAttr(pages[currPageNum - 1], S_ARIA_CURRENT);\n await setPageState(components, currPageNum, S_CURRENT);\n };\n\n const nextPage = async (gestureData?: GestureData) => {\n let targetPage = currPageNum + 1;\n while (isPageDisabled(targetPage)) {\n targetPage++;\n }\n\n return goToPage(targetPage, gestureData);\n };\n\n const prevPage = async (gestureData?: GestureData) => {\n let targetPage = currPageNum - 1;\n while (isPageDisabled(targetPage)) {\n targetPage--;\n }\n\n return goToPage(targetPage, gestureData);\n };\n\n const isPageDisabled = (pageNum: number) => disabledPages[pageNum] === true;\n\n const disablePage = async (pageNum: number) => {\n pageNum = toInt(pageNum);\n if (widget.isDisabled() || pageNum < 1 || pageNum > MH.lengthOf(pages)) {\n return;\n }\n\n // set immediately for toggle to work without awaiting on it\n disabledPages[pageNum] = true;\n\n if (pageNum === currPageNum) {\n await prevPage();\n\n if (pageNum === currPageNum) {\n // was the first enabled one\n await nextPage();\n\n if (pageNum === currPageNum) {\n // was the only enabled one\n disabledPages[pageNum] = false;\n return;\n }\n }\n }\n\n setCurrentPage(\n element,\n { _current: currPageNum, _total: MH.lengthOf(pages) },\n isPageDisabled,\n );\n await setPageState(components, pageNum, MC.S_DISABLED);\n };\n\n const enablePage = async (pageNum: number) => {\n pageNum = toInt(pageNum);\n if (widget.isDisabled() || !isPageDisabled(pageNum)) {\n return;\n }\n\n // set immediately for toggle to work without awaiting on it\n disabledPages[pageNum] = false;\n\n setCurrentPage(\n element,\n { _current: currPageNum, _total: MH.lengthOf(pages) },\n isPageDisabled,\n );\n await setPageState(\n components,\n pageNum,\n pageNum < currPageNum ? S_COVERED : S_NEXT,\n );\n };\n\n const togglePage = (pageNum: number) =>\n isPageDisabled(pageNum) ? enablePage(pageNum) : disablePage(pageNum);\n\n const onTransition = (handler: WidgetHandler) =>\n callbacks.add(wrapCallback(handler));\n\n return {\n _nextPage: nextPage,\n _prevPage: prevPage,\n _goToPage: goToPage,\n _disablePage: disablePage,\n _enablePage: enablePage,\n _togglePage: togglePage,\n _isPageDisabled: isPageDisabled,\n _getCurrentPage: () => pages[currPageNum - 1],\n _getPreviousPage: () => pages[lastPageNum - 1],\n _getCurrentPageNum: () => (MH.lengthOf(pages) > 0 ? currPageNum : 0),\n _getPreviousPageNum: () => (MH.lengthOf(pages) > 0 ? lastPageNum : 0),\n _onTransition: onTransition,\n