UNPKG

lisn.js

Version:

Simply handle user gestures and actions. Includes widgets.

1 lines 124 kB
{"version":3,"file":"openable.cjs","names":["MC","_interopRequireWildcard","require","MH","_settings","_cssAlter","_domAlter","_domEvents","_domOptimize","_event","_log","_math","_misc","_tasks","_position","_size","_validation","_callback","_sizeWatcher","_viewWatcher","_widget","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","_defineProperty","_toPropertyKey","value","enumerable","configurable","writable","_toPrimitive","Symbol","toPrimitive","TypeError","String","Number","registerOpenable","name","newOpenable","configValidator","registerWidget","element","config","isHTMLElement","Openable","logError","usageError","exports","Widget","_instances$get","instances","constructor","isModal","isOffcanvas","openCallbacks","newSet","closeCallbacks","isOpen","open","isDisabled","callback","invoke","setHasModal","setBooleanData","root","PREFIX_IS_OPEN","close","delHasModal","scrollWrapperToTop","unsetBooleanData","waitForDelay","waitForMeasureTime","elScrollTo","outerWrapper","top","left","S_TOGGLE","onOpen","handler","add","wrapCallback","onClose","getRoot","getContainer","container","getTriggers","triggers","keys","getTriggerConfigs","newMap","entries","onDestroy","clear","init","Collapsible","register","WIDGET_NAME_COLLAPSIBLE","collapsibleConfigValidator","_config$autoClose","_config$reverse","isHorizontal","horizontal","orientation","S_HORIZONTAL","S_VERTICAL","onSetup","trigger","triggerConfig","insertCollapsibleIcon","setDataNow","PREFIX_ORIENTATION","id","className","autoClose","closeButton","wrapTriggers","wrapper","childrenOf","setData","PREFIX_REVERSE","reverse","disableInitialTransition","disableTransitionTimer","tempEnableTransition","removeClasses","PREFIX_TRANSITION_DISABLE","clearTimer","transitionDuration","getMaxTransitionDuration","setTimer","addClasses","peek","peekSize","isString","getStyleProp","VAR_PEEK_SIZE","PREFIX_PEEK","setStyleProp","updateWidth","width","getComputedStyleProp","S_WIDTH","VAR_JS_COLLAPSIBLE_WIDTH","then","delStyleProp","Popup","WIDGET_NAME_POPUP","popupConfigValidator","_config$autoClose2","_config$closeButton","position","S_AUTO","PREFIX_PLACE","contentSize","containerView","promiseAll","SizeWatcher","reuse","fetchCurrentSize","ViewWatcher","fetchCurrentView","placement","fetchPopupPlacement","Modal","WIDGET_NAME_MODAL","modalConfigValidator","_config$autoClose3","_config$closeButton2","Offcanvas","WIDGET_NAME_OFFCANVAS","offcanvasConfigValidator","_config$autoClose4","_config$closeButton3","S_RIGHT","newWeakMap","PREFIX_CLOSE_BTN","prefixName","S_REVERSE","PREFIX_OPENS_ON_HOVER","PREFIX_LINE","PREFIX_ICON_POSITION","PREFIX_TRIGGER_ICON","PREFIX_ICON_WRAPPER","S_ARIA_EXPANDED","ARIA_PREFIX","S_ARIA_MODAL","prefixCssVar","prefixCssJsVar","MIN_CLICK_TIME_AFTER_HOVER_OPEN","S_ARROW_UP","S_ARROW","S_UP","S_ARROW_DOWN","S_DOWN","S_ARROW_LEFT","S_LEFT","S_ARROW_RIGHT","ARROW_TYPES","ICON_CLOSED_TYPES","ICON_OPEN_TYPES","isValidIconClosed","includes","isValidIconOpen","triggerConfigValidator","validateString","key","validateStrList","toArrayIfSingle","validateBoolean","icon","toBoolean","isValidPosition","iconClosed","iconOpen","hover","validateBooleanOrString","v","isValidTwoFoldPosition","getPrefixedNames","pref","_root","_overlay","_innerWrapper","_outerWrapper","_content","_container","_trigger","_containerForSelect","_triggerForSelect","_contentId","findContainer","content","cls","_currWidget$getRoot","currWidget","childRef","parentOf","closest","findTriggers","prefixedNames","getTriggerSelector","suffix","contentId","getData","docQuerySelectorAll","querySelectorAll","filter","contains","getTriggersFrom","inputTriggers","triggerMap","addTrigger","createWrapperFor","wrapElement","ignoreMove","isArray","getWidgetConfig","isInstanceOf","Map","widget","_config$wrapTriggers","innerWrapper","createElement","wrapElementNow","placeholder","overlay","moveElement","to","addClassesNow","domID","getOrAssignID","setAttr","S_ROLE","closeBtn","createButton","addEventListenerTo","S_CLICK","settings","lightThemeClassName","darkThemeClassName","hasClass","elements","delData","unsetAttr","waitForMutateTime","replaceElementNow","moveElementNow","removeClassesNow","delAttr","S_ARIA_CONTROLS","delDataNow","el","deleteKey","waitForInteractive","destroy","isDestroyed","getBody","setBooleanDataNow","S_HOVER","unsetBooleanDataNow","setupListeners","doc","getDoc","hoverTimeOpened","isPointerOver","activeTrigger","isTrigger","getDefaultWidgetSelector","shouldPreventDefault","_triggers$get$prevent","_triggers$get","preventDefault","usesHover","_triggers$get2","usesAutoClose","_ref","_triggers$get3","toggleTrigger","event","openIt","currentTargetOf","isElement","timeSince","S_POINTERENTER","setIsPointerOver","S_POINTERLEAVE","pointerLeft","unsetIsPointerOver","isTouchPointerEvent","pointerEntered","timeNow","closeIfEscape","closeIfClickOutside","target","targetOf","maybeSetupAutoCloseListeners","remove","addOrRemove","removeEventListenerFrom","setupOrCleanup","widgetConfig","_triggerConfig$icon","_triggerConfig$iconCl","_triggerConfig$iconOp","iconPosition","l","line","getBooleanData","containerPosition","relative","containerTop","S_TOP","containerBottom","S_BOTTOM","containerLeft","containerRight","containerHMiddle","hMiddle","containerVMiddle","vMiddle","vpSize","fetchViewportSize","popupWidth","border","popupHeight","S_HEIGHT","placementVars","bottom","right","keyWithMaxVal","undefined","finalPlacement","alignmentVars","middle","min","alignment"],"sources":["../../../src/ts/widgets/openable.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 { settings } from \"@lisn/globals/settings\";\n\nimport { XYDirection, Position } from \"@lisn/globals/types\";\n\nimport {\n disableInitialTransition,\n hasClass,\n addClasses,\n addClassesNow,\n removeClasses,\n removeClassesNow,\n getData,\n getBooleanData,\n setData,\n setDataNow,\n setBooleanData,\n setBooleanDataNow,\n unsetBooleanData,\n unsetBooleanDataNow,\n delData,\n delDataNow,\n setHasModal,\n delHasModal,\n getStyleProp,\n setStyleProp,\n delStyleProp,\n getComputedStyleProp,\n getMaxTransitionDuration,\n} from \"@lisn/utils/css-alter\";\nimport {\n wrapElement,\n wrapElementNow,\n moveElement,\n moveElementNow,\n replaceElementNow,\n getOrAssignID,\n createWrapperFor,\n} from \"@lisn/utils/dom-alter\";\nimport { waitForInteractive } from \"@lisn/utils/dom-events\";\nimport {\n waitForMeasureTime,\n waitForMutateTime,\n} from \"@lisn/utils/dom-optimize\";\nimport { addEventListenerTo, removeEventListenerFrom } from \"@lisn/utils/event\";\nimport { logError } from \"@lisn/utils/log\";\nimport { keyWithMaxVal } from \"@lisn/utils/math\";\nimport { toBoolean, toArrayIfSingle } from \"@lisn/utils/misc\";\nimport { waitForDelay } from \"@lisn/utils/tasks\";\nimport { isValidPosition, isValidTwoFoldPosition } from \"@lisn/utils/position\";\nimport { fetchViewportSize } from \"@lisn/utils/size\";\nimport {\n validateStrList,\n validateBoolean,\n validateBooleanOrString,\n validateString,\n} from \"@lisn/utils/validation\";\n\nimport { wrapCallback } from \"@lisn/modules/callback\";\n\nimport { SizeWatcher, SizeData } from \"@lisn/watchers/size-watcher\";\nimport { ViewWatcher, ViewData } from \"@lisn/watchers/view-watcher\";\n\nimport {\n Widget,\n WidgetHandler,\n WidgetCallback,\n WidgetConfigValidator,\n WidgetConfigValidatorObject,\n registerWidget,\n getWidgetConfig,\n getDefaultWidgetSelector,\n} from \"@lisn/widgets/widget\";\n\n/* ********************\n * Base Openable\n * ********************/\n\nexport type OpenableCreateFn<Config extends Record<string, unknown>> = (\n element: HTMLElement,\n config?: Config,\n) => Openable;\n\n/**\n * Enables automatic setting up of an {@link Openable} widget from an\n * elements matching its content element selector (`[data-lisn-<name>]` or\n * `.lisn-<name>`).\n *\n * The name you specify here should generally be the same name you pass in\n * {@link OpenableConfig.name | options.name} to the {@link Openable.constructor}\n * but it does not need to be the same.\n *\n * @param name The name of the openable. Should be in kebab-case.\n * @param newOpenable Called for every element matching the selector.\n * @param configValidator A validator object, or a function that returns such\n * an object, for all options supported by the widget.\n *\n * @see {@link registerWidget}\n */\nexport const registerOpenable = <Config extends Record<string, unknown>>(\n name: string,\n newOpenable: OpenableCreateFn<Config>,\n configValidator?: null | WidgetConfigValidator<Config>,\n) => {\n registerWidget(\n name,\n (element, config) => {\n if (MH.isHTMLElement(element)) {\n if (!Openable.get(element)) {\n return newOpenable(element, config);\n }\n } else {\n logError(MH.usageError(\"Openable widget supports only HTMLElement\"));\n }\n\n return null;\n },\n configValidator,\n );\n};\n\n/**\n * {@link Openable} is an abstract base class. You should not directly\n * instantiate it but can inherit it to create your own custom openable widget.\n *\n * **IMPORTANT:** You should not instantiate more than one {@link Openable}\n * widget, regardless of type, on a given element. Use {@link Openable.get} to\n * get an existing instance if any. If there is already an {@link Openable}\n * widget of any type on this element, it will be destroyed!\n *\n * @see {@link registerOpenable}\n */\nexport abstract class Openable extends Widget {\n /**\n * Opens the widget unless it is disabled.\n */\n readonly open: () => Promise<void>;\n\n /**\n * Closes the widget.\n */\n readonly close: () => Promise<void>;\n\n /**\n * Closes the widget if it is open, or opens it if it is closed (unless\n * it is disabled).\n */\n readonly toggle: () => Promise<void>;\n\n /**\n * The given handler will be called when the widget is open.\n *\n * If it returns a promise, it will be awaited upon.\n */\n readonly onOpen: (handler: WidgetHandler) => void;\n\n /**\n * The given handler will be called when the widget is closed.\n *\n * If it returns a promise, it will be awaited upon.\n */\n readonly onClose: (handler: WidgetHandler) => void;\n\n /**\n * Returns true if the widget is currently open.\n */\n readonly isOpen: () => boolean;\n\n /**\n * Returns the root element created by us that wraps the original content\n * element passed to the constructor. It is located in the content element's\n * original place.\n */\n readonly getRoot: () => HTMLElement;\n\n /**\n * Returns the element that was found to be the container. It is the closest\n * ancestor that has a `lisn-collapsible-container` class, or if no such\n * ancestor then the immediate parent of the content element.\n */\n readonly getContainer: () => HTMLElement | null;\n\n /**\n * Returns the trigger elements, if any. Note that these may be wrappers\n * around the original triggers passed.\n */\n readonly getTriggers: () => Element[];\n\n /**\n * Returns the trigger elements along with their configuration.\n */\n readonly getTriggerConfigs: () => Map<Element, OpenableTriggerConfig>;\n\n /**\n * Retrieve an existing widget by its content element or any of its triggers.\n *\n * If the element is already part of a configured {@link Openable} widget,\n * the widget instance is returned. Otherwise `null`.\n *\n * Note that trigger elements are not guaranteed to be unique among openable\n * widgets as the same element can be a trigger for multiple such widgets. If\n * the element you pass is a trigger, then the last openable widget that was\n * created for it will be returned.\n */\n static get(element: Element): Openable | null {\n // We manage the instances here since we also map associated elements and\n // not just the main content element that created the widget.\n return instances.get(element) ?? null;\n }\n\n constructor(element: HTMLElement, config: OpenableConfig) {\n super(element);\n\n const { isModal, isOffcanvas } = config;\n\n const openCallbacks = MH.newSet<WidgetCallback>();\n const closeCallbacks = MH.newSet<WidgetCallback>();\n\n let isOpen = false;\n\n // ----------\n\n const open = async () => {\n if (this.isDisabled() || isOpen) {\n return;\n }\n\n isOpen = true;\n\n for (const callback of openCallbacks) {\n await callback.invoke(this);\n }\n\n if (isModal) {\n setHasModal();\n }\n\n await setBooleanData(root, PREFIX_IS_OPEN);\n };\n\n // ----------\n\n const close = async () => {\n if (this.isDisabled() || !isOpen) {\n return;\n }\n\n isOpen = false;\n\n for (const callback of closeCallbacks) {\n await callback.invoke(this);\n }\n\n if (isModal) {\n delHasModal();\n }\n\n if (isOffcanvas) {\n scrollWrapperToTop(); // no need to await\n }\n\n await unsetBooleanData(root, PREFIX_IS_OPEN);\n };\n\n // ----------\n\n const scrollWrapperToTop = async () => {\n // Wait a bit before scrolling since the hiding of the element is animated.\n // Assume no more than 1s animation time.\n await waitForDelay(1000);\n await waitForMeasureTime();\n MH.elScrollTo(outerWrapper, {\n top: 0,\n left: 0,\n });\n };\n\n // --------------------\n\n this.open = open;\n this.close = close;\n this[MC.S_TOGGLE] = () => (isOpen ? close() : open());\n this.onOpen = (handler) => openCallbacks.add(wrapCallback(handler));\n this.onClose = (handler) => closeCallbacks.add(wrapCallback(handler));\n this.isOpen = () => isOpen;\n this.getRoot = () => root;\n this.getContainer = () => container;\n this.getTriggers = () => [...triggers.keys()];\n this.getTriggerConfigs = () => MH.newMap([...triggers.entries()]);\n\n this.onDestroy(() => {\n openCallbacks.clear();\n closeCallbacks.clear();\n });\n\n const { root, container, triggers, outerWrapper } = init(\n this,\n element,\n config,\n );\n }\n}\n\n/**\n * Per-trigger based configuration. Can either be given as an object as the\n * value of the {@link OpenableConfig.triggers} map, or it can be set as a\n * string configuration in the `data-lisn-<name>-trigger` data attribute. See\n * {@link getWidgetConfig} for the syntax.\n *\n * @example\n * ```html\n * <div data-lisn-collapsible-trigger=\"auto-close\n * | icon=right\n * | icon-closed=arrow-down\n * | icon-open=x\"\n * ></div>\n * ```\n *\n * @interface\n */\nexport type OpenableTriggerConfig = {\n /**\n * The DOM ID to set on the trigger. Will result in the trigger element, which\n * could be a wrapper around the original element (as in the case of {@link\n * Collapsible} you passed, getting this ID.\n *\n * **IMPORTANT:** If the trigger element already has an ID and is not being\n * wrapped, then this will override the ID and it _won't_ be restored on destroy.\n *\n * @defaultValue undefined\n */\n id?: string;\n\n /**\n * Class name(s) for the trigger. Will result in the trigger element, which\n * could be a wrapper around the original element you passed, getting these\n * classes.\n *\n * @defaultValue undefined\n */\n className?: string[] | string;\n\n /**\n * Override the widget's {@link OpenableConfig.autoClose} for this trigger.\n *\n * @defaultValue undefined // Widget default\n */\n autoClose?: boolean;\n\n /**\n * Open the openable when this trigger is hovered.\n *\n * If the device is touch and {@link OpenableConfig.autoClose} is enabled,\n * the widget will be closed shortly after the pointer leaves both the\n * trigger and the root element.\n *\n * @defaultValue false\n */\n hover?: boolean;\n\n /**\n * Whether to prevent default click action.\n *\n * @defaultValue true\n */\n preventDefault?: boolean;\n\n /**\n * Override the widget's {@link CollapsibleConfig.icon} for this trigger.\n *\n * Currently only relevant for {@link Collapsible}s.\n *\n * @defaultValue undefined // Widget default\n */\n icon?: false | Position;\n\n /**\n * Override the widget's {@link CollapsibleConfig.iconClosed} for this\n * trigger.\n *\n * Currently only relevant for {@link Collapsible}s.\n *\n * @defaultValue undefined // Widget default\n */\n iconClosed?: \"plus\" | `arrow-${XYDirection}`;\n\n /**\n * Override the widget's {@link CollapsibleConfig.iconOpen} for this\n * trigger.\n *\n * Currently only relevant for {@link Collapsible}s.\n *\n * @defaultValue undefined // Widget default\n */\n iconOpen?: \"minus\" | \"x\" | `arrow-${XYDirection}`;\n};\n\n/**\n * @interface\n */\nexport type OpenableConfig = {\n /**\n * The name of the _type_ of the openable. Will set the class prefix to\n * `lisn-<name>`.\n */\n name: string;\n\n /**\n * The DOM ID to set on the openable. Will result in the top-level root\n * element that's created by us getting this ID.\n *\n * @defaultValue undefined\n */\n id?: string;\n\n /**\n * Class name(s) or a list of class names to set on the openable. Will result\n * in the top-level root element that's created by us getting these classes.\n *\n * @defaultValue undefined\n */\n className?: string[] | string;\n\n /**\n * Whether to auto-close the widget on clicking outside the content element\n * or on pressing Escape key. Furthermore, if any trigger opens the widget on\n * {@link OpenableTriggerConfig.hover}, the widget will be closed when the\n * pointer leaves both the trigger and the root.\n *\n * This is true by default for {@link Popup}, {@link Modal} and {@link Offcanvas}.\n */\n autoClose: boolean;\n\n /**\n * If true, then while the widget is open, the `document.body` will be set to\n * `overflow: hidden`.\n *\n * This is true for {@link Modal}.\n */\n isModal: boolean;\n\n /**\n * If true, then the content element is assumed to be possibly scrollable and\n * will be scrolled back to its top after the widget is closed.\n *\n * This is true for {@link Modal} and {@link Offcanvas}.\n */\n isOffcanvas: boolean;\n\n /**\n * Add a close button at the top right.\n *\n * This is true by default for {@link Modal} and {@link Offcanvas}.\n */\n closeButton: boolean;\n\n /**\n * The elements that open the widget when clicked on. You can also pass a map\n * whose keys are the elements and values are {@link OpenableTriggerConfig}\n * objects.\n *\n * If not given, then the elements that will be used as triggers are\n * discovered in the following way (`<name>` is what is given as\n * {@link name}):\n * 1. If the content element has a `data-lisn-<name>-content-id` attribute,\n * then it must be a unique (for the current page) ID. In this case, the\n * trigger elements will be any element in the document that has a\n * `lisn-<name>-trigger` class or `data-lisn-<name>-trigger` attribute\n * and the same `data-lisn-<name>-content-id` attribute.\n * 2. Otherwise, the closest ancestor that has a `lisn-<name>-container`\n * class, or if no such ancestor then the immediate parent of the content\n * element, is searched for any elements that have a `lisn-<name>-trigger`\n * class or `data-lisn-<name>-trigger` attribute and that do _not_ have a\n * `data-lisn-<name>-content-id` attribute, and that are _not_ children of\n * the content element.\n *\n * @defaultValue undefined\n */\n triggers?: Element[] | Map<Element, OpenableTriggerConfig | null>;\n\n /**\n * Whether to wrap each trigger in a new element.\n *\n * @defaultValue false\n */\n wrapTriggers?: boolean;\n\n /**\n * Hook to run once the widget is fully setup (which happens asynchronously).\n *\n * It is called during \"mutate time\". See {@link waitForMutateTime}.\n *\n * @defaultValue undefined\n */\n onSetup?: () => void;\n};\n\n/**\n * @interface\n * @ignore\n * @deprecated\n *\n * Deprecated alias for {@link OpenableConfig}\n */\nexport type OpenableProperties = OpenableConfig;\n\n/* ********************\n * Collapsible\n * ********************/\n\n/**\n * Configures the given element as a {@link Collapsible} widget.\n *\n * The Collapsible widget sets up the given element to be collapsed and\n * expanded upon activation. Activation can be done manually by calling\n * {@link open} or when clicking on any of the given\n * {@link CollapsibleConfig.triggers | triggers}.\n *\n * **NOTE:** The Collapsible widget always wraps each trigger element in\n * another element in order to allow positioning the icon, if any.\n *\n * **IMPORTANT:** You should not instantiate more than one {@link Openable}\n * widget, regardless of type, on a given element. Use {@link Openable.get} to\n * get an existing instance if any. If there is already an {@link Openable}\n * widget of any type on this element, 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 root element that is created\n * by LISN and has a class `lisn-collapsible__root`:\n * - `data-lisn-is-open`: `\"true\"` or `\"false\"`\n * - `data-lisn-reverse`: `\"true\"` or `\"false\"`\n * - `data-lisn-orientation`: `\"horizontal\"` or `\"vertical\"`\n *\n * The following dynamic attributes are set on each trigger:\n * - `data-lisn-opens-on-hover: `\"true\"` or `\"false\"`\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-collapsible` class or `data-lisn-collapsible` attribute set on the\n * element that holds the content of the collapsible\n * - `lisn-collapsible-trigger` class or `data-lisn-collapsible-trigger`\n * attribute set on elements that should act as the triggers.\n * If using a data attribute, you can configure the trigger via the value\n * with a similar syntax to the configuration of the openable widget. For\n * example:\n * - Set the attribute to `\"hover\"` in order to have this trigger open the\n * collapsible on hover _in addition to click_.\n * - Set the attribute to `\"hover|auto-close\"` in order to have this trigger\n * open the collapsible on hover but and override\n * {@link CollapsibleConfig.autoClose} with true.\n *\n * When using auto-widgets, the elements that will be used as triggers are\n * discovered in the following way:\n * 1. If the content element has a `data-lisn-collapsible-content-id` attribute,\n * then it must be a unique (for the current page) ID. In this case, the\n * trigger elements will be any element in the document that has a\n * `lisn-collapsible-trigger` class or `data-lisn-collapsible-trigger`\n * attribute and the same `data-lisn-collapsible-content-id` attribute.\n * 2. Otherwise, the closest ancestor that has a `lisn-collapsible-container`\n * class, or if no such ancestor then the immediate parent of the content\n * element, is searched for any elements that have a\n * `lisn-collapsible-trigger` class or `data-lisn-collapsible-trigger`\n * attribute and that do _not_ have a `data-lisn-collapsible-content-id`\n * attribute, and that are _not_ children of the content 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 collapsible with one trigger.\n *\n * ```html\n * <div>\n * <div class=\"lisn-collapsible-trigger\">Expand</div>\n * <div class=\"lisn-collapsible\">\n * Some long content here...\n * </div>\n * </div>\n * ```\n *\n * @example\n * This defines a collapsible that is partially visible when collapsed, and\n * where the trigger is in a different parent to the content.\n *\n * ```html\n * <div>\n * <div data-lisn-collapsible-content-id=\"readmore\"\n * data-lisn-collapsible=\"peek\">\n * <p>\n * Lorem ipsum odor amet, consectetuer adipiscing elit. Etiam duis\n * viverra faucibus facilisis luctus. Nunc tellus turpis facilisi dapibus\n * aliquet turpis. Diam potenti egestas dolor auctor nostra vestibulum.\n * Tempus auctor quis turpis; pulvinar ante ultrices. Netus morbi\n * imperdiet volutpat litora tellus turpis a. Sociosqu interdum sodales\n * sapien nulla aptent pellentesque praesent. Senectus magnis\n * pellentesque; dis porta justo habitant.\n * </p>\n *\n * <p>\n * Imperdiet placerat habitant tristique turpis habitasse ligula pretium\n * vehicula. Mauris molestie lectus leo aliquam condimentum elit fermentum\n * tempus nisi. Eget mi vestibulum quisque enim himenaeos. Odio nascetur\n * vel congue vivamus eleifend ut nascetur. Ultrices quisque non dictumst\n * risus libero varius tincidunt vel. Suscipit netus maecenas imperdiet\n * elementum donec maximus suspendisse luctus. Eu velit semper urna sem\n * ullamcorper nisl turpis hendrerit. Gravida commodo nisl malesuada nibh\n * ultricies scelerisque hendrerit tempus vehicula. Risus eleifend eros\n * aliquam turpis elit ridiculus est class.\n * </p>\n * </div>\n * </div>\n *\n * <div>\n * <div data-lisn-collapsible-content-id=\"readmore\"\n * class=\"lisn-collapsible-trigger\">\n * Read more\n * </div>\n * </div>\n * ```\n *\n * @example\n * As above, but with all other possible configuration settings set explicitly.\n *\n * ```html\n * <div>\n * <div data-lisn-collapsible-content-id=\"readmore\"\n * data-lisn-collapsible=\"peek=50px\n * | horizontal=false\n * | reverse=false\n * | auto-close\n * | icon=right\n * | icon-closed=arrow-up\"\n * | icon-open=arrow-down\">\n * <p>\n * Lorem ipsum odor amet, consectetuer adipiscing elit. Etiam duis\n * viverra faucibus facilisis luctus. Nunc tellus turpis facilisi dapibus\n * aliquet turpis. Diam potenti egestas dolor auctor nostra vestibulum.\n * Tempus auctor quis turpis; pulvinar ante ultrices. Netus morbi\n * imperdiet volutpat litora tellus turpis a. Sociosqu interdum sodales\n * sapien nulla aptent pellentesque praesent. Senectus magnis\n * pellentesque; dis porta justo habitant.\n * </p>\n *\n * <p>\n * Imperdiet placerat habitant tristique turpis habitasse ligula pretium\n * vehicula. Mauris molestie lectus leo aliquam condimentum elit fermentum\n * tempus nisi. Eget mi vestibulum quisque enim himenaeos. Odio nascetur\n * vel congue vivamus eleifend ut nascetur. Ultrices quisque non dictumst\n * risus libero varius tincidunt vel. Suscipit netus maecenas imperdiet\n * elementum donec maximus suspendisse luctus. Eu velit semper urna sem\n * ullamcorper nisl turpis hendrerit. Gravida commodo nisl malesuada nibh\n * ultricies scelerisque hendrerit tempus vehicula. Risus eleifend eros\n * aliquam turpis elit ridiculus est class.\n * </p>\n * </div>\n * </div>\n *\n * <div>\n * <div data-lisn-collapsible-content-id=\"readmore\"\n * class=\"lisn-collapsible-trigger\">\n * Read more\n * </div>\n * </div>\n * ```\n */\nexport class Collapsible extends Openable {\n static register() {\n registerOpenable(\n WIDGET_NAME_COLLAPSIBLE,\n (element, config) => new Collapsible(element, config),\n collapsibleConfigValidator,\n );\n }\n\n constructor(element: HTMLElement, config?: CollapsibleConfig) {\n const isHorizontal = config?.horizontal;\n const orientation = isHorizontal ? MC.S_HORIZONTAL : MC.S_VERTICAL;\n\n const onSetup = () => {\n // The triggers here are wrappers around the original which will be\n // replaced by the original on destroy, so no need to clean up this.\n for (const [\n trigger,\n triggerConfig,\n ] of this.getTriggerConfigs().entries()) {\n insertCollapsibleIcon(trigger, triggerConfig, this, config);\n setDataNow(trigger, MC.PREFIX_ORIENTATION, orientation);\n }\n };\n\n super(element, {\n name: WIDGET_NAME_COLLAPSIBLE,\n id: config?.id,\n className: config?.className,\n autoClose: config?.autoClose ?? false,\n isModal: false,\n isOffcanvas: false,\n closeButton: false,\n triggers: config?.triggers,\n wrapTriggers: true,\n onSetup,\n });\n\n const root = this.getRoot();\n const wrapper = MH.childrenOf(root)[0];\n\n setData(root, MC.PREFIX_ORIENTATION, orientation);\n setBooleanData(root, PREFIX_REVERSE, config?.reverse ?? false);\n\n // -------------------- Transitions\n disableInitialTransition(element, 100);\n disableInitialTransition(root, 100);\n disableInitialTransition(wrapper, 100);\n\n let disableTransitionTimer: ReturnType<typeof setTimeout> | null = null;\n const tempEnableTransition = async () => {\n await removeClasses(root, MC.PREFIX_TRANSITION_DISABLE);\n await removeClasses(wrapper, MC.PREFIX_TRANSITION_DISABLE);\n\n if (disableTransitionTimer) {\n MH.clearTimer(disableTransitionTimer);\n }\n\n const transitionDuration = await getMaxTransitionDuration(root);\n disableTransitionTimer = MH.setTimer(() => {\n if (this.isOpen()) {\n addClasses(root, MC.PREFIX_TRANSITION_DISABLE);\n addClasses(wrapper, MC.PREFIX_TRANSITION_DISABLE);\n disableTransitionTimer = null;\n }\n }, transitionDuration);\n };\n\n // Disable transitions except during open/close, so that resizing the\n // window for example doesn't result in lagging width/height transition.\n this.onOpen(tempEnableTransition);\n this.onClose(tempEnableTransition);\n\n // -------------------- Peek\n const peek = config?.peek;\n if (peek) {\n (async () => {\n let peekSize: string | null = null;\n if (MH.isString(peek)) {\n peekSize = peek;\n } else {\n peekSize = await getStyleProp(element, VAR_PEEK_SIZE);\n }\n\n addClasses(root, PREFIX_PEEK);\n if (peekSize) {\n setStyleProp(root, VAR_PEEK_SIZE, peekSize);\n }\n })();\n }\n\n // -------------------- Width in horizontal mode\n if (isHorizontal) {\n const updateWidth = async () => {\n const width = await getComputedStyleProp(root, MC.S_WIDTH);\n await setStyleProp(element, VAR_JS_COLLAPSIBLE_WIDTH, width);\n };\n\n MH.setTimer(updateWidth);\n\n // Save its current width so that if it contains text, it does not\n // \"collapse\" and end up super tall.\n this.onClose(updateWidth);\n\n this.onOpen(async () => {\n // Update the content width before opening.\n await updateWidth();\n\n // Delete the fixed width property soon after opening to allow it to\n // resize again while it's open.\n waitForDelay(2000).then(() => {\n if (this.isOpen()) {\n delStyleProp(element, VAR_JS_COLLAPSIBLE_WIDTH);\n }\n });\n });\n }\n }\n}\n\n/**\n * @interface\n */\nexport type CollapsibleConfig = {\n /**\n * The DOM ID to set on the collapsible. Will result in the top-level root\n * element that's created by us getting this ID.\n *\n * Note, this does not replace or affect the\n * `data-lisn-collapsible-content-id` attribute used to link triggers to the\n * collapsible.\n *\n * @defaultValue undefined\n */\n id?: string;\n\n /**\n * Class name(s) or a list of class names to set on the collapsible. Will\n * result in the top-level root element that's created by us getting these\n * classes.\n *\n * @defaultValue undefined\n */\n className?: string[] | string;\n\n /**\n * The elements that open the widget when clicked on. You can also pass a map\n * whose keys are the elements and values are {@link OpenableTriggerConfig}\n * objects.\n *\n * If not given, then the elements that will be used as triggers are\n * discovered in the following way:\n * 1. If the content element has a `data-lisn-collapsible-content-id`\n * attribute, then it must be a unique (for the current page) ID. In this\n * case, the trigger elements will be any element in the document that\n * has a `lisn-collapsible-trigger` class or\n * `data-lisn-collapsible-trigger` attribute and the same\n * `data-lisn-collapsible-content-id` attribute.\n * 2. Otherwise, the closest ancestor that has a `lisn-collapsible-container`\n * class, or if no such ancestor then the immediate parent of the content\n * element, is searched for any elements that have a\n * `lisn-collapsible-trigger` class or `data-lisn-collapsible-trigger`\n * attribute and that do _not_ have a `data-lisn-collapsible-content-id`\n * attribute, and that are _not_ children of the content element.\n *\n * @defaultValue undefined\n */\n triggers?: Element[] | Map<Element, OpenableTriggerConfig | null>;\n\n /**\n * Open sideways (to the right) instead of downwards (default).\n *\n * **IMPORTANT:** In horizontal mode the width of the content element should\n * not be set (or be `auto`), but you can use `min-width` or `max-width` in\n * your CSS if needed.\n *\n * @defaultValue false\n */\n horizontal?: boolean;\n\n /**\n * Open to the left if horizontal or upwards if vertical.\n *\n * @defaultValue false\n */\n reverse?: boolean;\n\n /**\n * If not false, part of the content will be visible when the collapsible is\n * closed. The value can be any valid CSS width specification.\n *\n * If you set this to `true`, then the size of the peek window will be\n * dictated by CSS. By default the size is 100px, but you can change this by\n * setting `--lisn-peek-size` CSS property on the content element or any of\n * its ancestors.\n *\n * Otherwise, if the value is a string, it must be a CSS length including units.\n *\n * @defaultValue false\n */\n peek?: boolean | string;\n\n /**\n * Automatically close the collapsible when clicking outside it or pressing\n * Escape. Furthermore, if any trigger opens the widget on\n * {@link OpenableTriggerConfig.hover}, the widget will be closed when the\n * pointer leaves both the trigger and the root.\n *\n * @defaultValue false\n */\n autoClose?: boolean;\n\n /**\n * Add an icon to each trigger.\n *\n * If set to something other than `false`, then by default the icon in the\n * closed state is a plus (+) and in the open state it's a minus (-), but\n * this can be configured with {@link iconClosed} and {@link iconOpen}.\n *\n * @defaultValue false\n */\n icon?: false | Position;\n\n /**\n * Set the type of icon used on the trigger(s) in the closed state.\n *\n * Note that {@link icon} must be set to something other than `false`.\n *\n * @defaultValue \"plus\"\n */\n iconClosed?: \"plus\" | `arrow-${XYDirection}`;\n\n /**\n * Set the type of icon used on the trigger(s) in the open state.\n *\n * Note that {@link icon} must be set to something other than `false`.\n *\n * @defaultValue \"minus\";\n */\n iconOpen?: \"minus\" | \"x\" | `arrow-${XYDirection}`;\n};\n\n/* ********************\n * Popup\n * ********************/\n\n/**\n * Configures the given element as a {@link Popup} widget.\n *\n * The Popup widget sets up the given element to be hidden and open in a\n * floating popup upon activation. Activation can be done manually by calling\n * {@link open} or when clicking on any of the given\n * {@link PopupConfig.triggers | triggers}.\n *\n * **IMPORTANT:** The popup is positioned absolutely in its container and the\n * position is relative to the container. The container gets `width:\n * fit-content` by default but you can override this in your CSS. The popup\n * also gets a configurable `min-width` set.\n *\n * **IMPORTANT:** You should not instantiate more than one {@link Openable}\n * widget, regardless of type, on a given element. Use {@link Openable.get} to\n * get an existing instance if any. If there is already an {@link Openable}\n * widget of any type on this element, 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 root element that is created\n * by LISN and has a class `lisn-popup__root`:\n * - `data-lisn-is-open`: `\"true\"` or `\"false\"`\n * - `data-lisn-place`: the actual position (top, bottom, left, top-left, etc)\n *\n * The following dynamic attributes are set on each trigger:\n * - `data-lisn-opens-on-hover: `\"true\"` or `\"false\"`\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-popup` class or `data-lisn-popup` attribute set on the element that\n * holds the content of the popup\n * - `lisn-popup-trigger` class or `data-lisn-popup-trigger`\n * attribute set on elements that should act as the triggers.\n * If using a data attribute, you can configure the trigger via the value\n * with a similar syntax to the configuration of the openable widget. For\n * example:\n * - Set the attribute to `\"hover\"` in order to have this trigger open the\n * popup on hover _in addition to click_.\n * - Set the attribute to `\"hover|auto-close=false\"` in order to have this\n * trigger open the popup on hover but and override\n * {@link PopupConfig.autoClose} with true.\n *\n * When using auto-widgets, the elements that will be used as triggers are\n * discovered in the following way:\n * 1. If the content element has a `data-lisn-popup-content-id` attribute, then\n * it must be a unique (for the current page) ID. In this case, the trigger\n * elements will be any element in the document that has a\n * `lisn-popup-trigger` class or `data-lisn-popup-trigger` attribute and the\n * same `data-lisn-popup-content-id` attribute.\n * 2. Otherwise, the closest ancestor that has a `lisn-popup-container` class,\n * or if no such ancestor then the immediate parent of the content element,\n * is searched for any elements that have a `lisn-popup-trigger` class or\n * `data-lisn-popup-trigger` attribute and that do _not_ have a\n * `data-lisn-popup-content-id` attribute, and that are _not_ children of\n * the content 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 popup with one trigger.\n *\n * ```html\n * <div>\n * <div class=\"lisn-popup-trigger\">Open</div>\n * <div class=\"lisn-popup\">\n * Some content here...\n * </div>\n * </div>\n * ```\n *\n * @example\n * This defines a popup that has a close button, and where the trigger is in a\n * different parent to the content.\n *\n * ```html\n * <div>\n * <div data-lisn-popup-content-id=\"popup\"\n * data-lisn-popup=\"close-button\">\n * Lorem ipsum odor amet, consectetuer adipiscing elit. Etiam duis viverra\n * faucibus facilisis luctus. Nunc tellus turpis facilisi dapibus aliquet\n * turpis. Diam potenti egestas dolor auctor nostra vestibulum. Tempus\n * auctor quis turpis; pulvinar ante ultrices. Netus morbi imperdiet\n * volutpat litora tellus turpis a. Sociosqu interdum sodales sapien nulla\n * aptent pellentesque praesent. Senectus magnis pellentesque; dis porta\n * justo habitant.\n * </div>\n * </div>\n *\n * <div>\n * <div data-lisn-popup-content-id=\"popup\" class=\"lisn-popup-trigger\">\n * Open\n * </div>\n * </div>\n * ```\n *\n * @example\n * As above, but with all possible configuration settings set explicitly.\n *\n * ```html\n * <div>\n * <div data-lisn-popup-content-id=\"popup\" class=\"lisn-popup-trigger\">\n * Open\n * </div>\n * </div>\n *\n * <div>\n * <div data-lisn-popup-content-id=\"popup\"\n * data-lisn-popup=\"close-button | position=bottom | auto-close=false\">\n * Lorem ipsum odor amet, consectetuer adipiscing elit. Etiam duis viverra\n * faucibus facilisis luctus. Nunc tellus turpis facilisi dapibus aliquet\n * turpis. Diam potenti egestas dolor auctor nostra vestibulum. Tempus\n * auctor quis turpis; pulvinar ante ultrices. Netus morbi imperdiet\n * volutpat litora tellus turpis a. Sociosqu interdum sodales sapien nulla\n * aptent pellentesque praesent. Senectus magnis pellentesque; dis porta\n * justo habitant.\n * </div>\n * </div>\n * ```\n */\nexport class Popup extends Openable {\n static register() {\n registerOpenable(\n WIDGET_NAME_POPUP,\n (element, config) => new Popup(element, config),\n popupConfigValidator,\n );\n }\n\n constructor(element: HTMLElement, config?: PopupConfig) {\n super(element, {\n name: WIDGET_NAME_POPUP,\n id: config?.id,\n className: config?.className,\n autoClose: config?.autoClose ?? true,\n isModal: false,\n isOffcanvas: false,\n closeButton: config?.closeButton ?? false,\n triggers: config?.triggers,\n });\n\n const root = this.getRoot();\n const container = this.getContainer();\n\n const position = config?.position || S_AUTO;\n if (position !== S_AUTO) {\n setData(root, MC.PREFIX_PLACE, position);\n }\n\n if (container && position === S_AUTO) {\n // Automatic position\n this.onOpen(async () => {\n const [contentSize, containerView] = await MH.promiseAll([\n SizeWatcher.reuse().fetchCurrentSize(element),\n ViewWatcher.reuse().fetchCurrentView(container),\n ]);\n\n const placement = await fetchPopupPlacement(contentSize, containerView);\n if (placement) {\n await setData(root, MC.PREFIX_PLACE, placement);\n }\n });\n }\n }\n}\n\n/**\n * @interface\n */\nexport type PopupConfig = {\n /**\n * The DOM ID to set on the popup. Will result in the top-level root element\n * that's created by us getting this ID.\n *\n * Note, this does not replace or affect the `data-lisn-popup-content-id`\n * attribute used to link triggers to the popup.\n *\n * @defaultValue undefined\n */\n id?: string;\n\n /**\n * Class name(s) or a list of class names to set on the popup. Will result in\n * the top-level root element that's created by us getting these classes.\n *\n * @defaultValue undefined\n */\n className?: string[] | string;\n\n /**\n * The elements that open the widget when clicked on. You can also pass a map\n * whose keys are the elements and values are {@link OpenableTriggerConfig}\n * objects.\n *\n * If not given, then the elements that will be used as triggers are\n * discovered in the following way:\n * 1. If the content element has a `data-lisn-popup-content-id` attribute,\n * then it must be a unique (for the current page) ID. In this case, the\n * trigger elements will be any element in the document that has a\n * `lisn-popup-trigger` class or `data-lisn-popup-trigger` attribute and\n * the same `data-lisn-popup-content-id` attribute.\n * 2. Otherwise, the closest ancestor that has a `lisn-popup-container` class,\n * or if no such ancestor then the immediate parent of the content\n * element, is searched for any elements that have a `lisn-popup-trigger`\n * class or `data-lisn-popup-trigger` attribute and that do _not_ have a\n * `data-lisn-popup-content-id` attribute, and that are _not_ children of\n * the content element.\n *\n * @defaultValue undefined\n */\n triggers?: Element[] | Map<Element, OpenableTriggerConfig | null>;\n\n /**\n * Add a close button at the top right.\n *\n * @defaultValue false\n */\n closeButton?: boolean;\n\n /**\n * Specify the popup position _relative to its container_. Supported\n * positions include `\"top\"`, `\"bottom\"`, `\"left\"`, `\"right\" `(which result\n * on the popup being placed on top, bottom, etc, but center-aligned), or\n * `\"top-left\"`, `\"left-top\"`, etc, as well as `\"auto\"`. If set to `\"auto\"`,\n * then popup position will be based on the container position within the\n * viewport at the time it's open.\n *\n * @defaultValue \"auto\"\n */\n position?: Position | `${Position}-${Position}` | \"auto\";\n\n /**\n * Automatically close the popup when clicking outside it or pressing Escape.\n * Furthermore, if any trigger opens the widget on\n * {@link OpenableTriggerConfig.hover}, the widget will be closed when the\n * pointer leaves both the trigger and the root.\n *\n * @defaultValue true\n */\n autoClose?: boolean;\n};\n\n/* ********************\n * Modal\n * ********************/\n\n/**\n * Configures the given element as a {@link Modal} widget.\n *\n * The Modal widget sets up the given element to be hidden and open in a fixed\n * full-screen modal popup upon activation. Activation can be done manually by\n * calling {@link open} or when clicking on any of the given\n * {@link ModalConfig.triggers | triggers}.\n *\n * **IMPORTANT:** You should not instantiate more than one {@link Openable}\n * widget, regardless of type, on a given element. Use {@link Openable.get} to\n * get an existing instance if any. If there is already an {@link Openable}\n * widget of any type on this element, 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 root element that is created\n * by LISN and has a class `lisn-modal__root`:\n * - `data-lisn-is-open`: `\"true\"` or `\"false\"`\n *\n * The following dynamic attributes are set on each trigger:\n * - `data-lisn-opens-on-hover: `\"true\"` or `\"false\"`\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-modal` class or `data-lisn-modal` attribute set on the element that\n * holds the content of the modal\n * - `lisn-modal-trigger` class or `data-lisn-modal-trigger`\n * attribute set on elements that should act as the triggers.\n * If using a data attribute, you can configure the trigger via the value\n * with a similar syntax to the configuration of the openable widget. For\n * example:\n * - Set the attribute to `\"hover\"` in order to have this trigger open the\n * modal on hover _in addition to click_.\n * - Set the attribute to `\"hover|auto-close=false\"` in order to have this\n * trigger open the modal on hover but and override\n * {@link ModalConfig.autoClose} with true.\n *\n * When using auto-widgets, the elements that will be used as triggers are\n * discovered in the following way:\n * 1. If the content element has a `data-lisn-modal-content-id` attribute, then\n * it must be a unique (for the current page) ID. In this case, the trigger\n * elements will be any element in the document that has a\n * `lisn-modal-trigger` class or `data-lisn-modal-trigger` attribute and the\n * same `data-lisn-modal-content-id` attribute.\n * 2. Otherwise, the closest ancestor that has a `lisn-modal-container` class,\n * or if no such ancestor then the immediate parent of the content element,\n * is searched for any elements that have a `lisn-modal-trigger` class or\n * `data-lisn-modal-trigger` attribute and that do _not_ have a\n * `data-lisn-modal-content-id` attribute, and that are _not_ children of\n * the content 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 modal with one trigger.\n *\n * ```html\n * <div>\n * <div class=\"lisn-modal-trigger\">Open</div>\n * <div class=\"lisn-modal\">\n * Some content here...\n * </div>\n * </div>\n * ```\n *\n * @example\n * This defines a modal that doesn't automatically close on click outside or\n * Escape and, and that has several triggers in a different parent to the\n * content.\n *\n * ```html\n * <div>\n * <div data-lisn-modal-content-id=\"modal\"\n * data-lisn-modal=\"auto-close=false\">\n * Lorem ipsum odor amet, consectetuer adipiscing elit. Etiam duis viverra\n * faucibus facilisis luctus. Nunc tellus turpis facilisi dapibus aliquet\n * turpis. Diam potenti egestas dolor auctor nostra vestibulum. Tempus\n * auctor quis turpis; pulvinar ante ultrices. Netus morbi imperdiet\n * volutpat litora tellus turpis a. Sociosqu interdum sodales sapien nulla\n * aptent pellentesque praesent. Senectus magnis pellentesque; dis porta\n * justo habitant.\n * </div>\n * </div>\n *\n * <div>\n * <div data-lisn-modal-content-id=\"modal\" class=\"lisn-modal-trigger\">\n * Open\n * </div>