UNPKG

lisn.js

Version:

Simply handle user gestures and actions. Includes widgets.

1,233 lines (1,193 loc) 64.1 kB
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /** * @module Widgets */ import * as MC from "../globals/minification-constants.js"; import * as MH from "../globals/minification-helpers.js"; import { settings } from "../globals/settings.js"; import { disableInitialTransition, hasClass, addClasses, addClassesNow, removeClasses, removeClassesNow, getData, getBooleanData, setData, setDataNow, setBooleanData, setBooleanDataNow, unsetBooleanData, unsetBooleanDataNow, delData, delDataNow, setHasModal, delHasModal, getStyleProp, setStyleProp, delStyleProp, getComputedStyleProp, getMaxTransitionDuration } from "../utils/css-alter.js"; import { wrapElement, wrapElementNow, moveElement, moveElementNow, replaceElementNow, getOrAssignID } from "../utils/dom-alter.js"; import { waitForInteractive } from "../utils/dom-events.js"; import { waitForMeasureTime, waitForMutateTime } from "../utils/dom-optimize.js"; import { isInlineTag } from "../utils/dom-query.js"; import { addEventListenerTo, removeEventListenerFrom } from "../utils/event.js"; import { logError } from "../utils/log.js"; import { keyWithMaxVal } from "../utils/math.js"; import { toBoolean, toArrayIfSingle } from "../utils/misc.js"; import { waitForDelay } from "../utils/tasks.js"; import { isValidPosition, isValidTwoFoldPosition } from "../utils/position.js"; import { fetchViewportSize } from "../utils/size.js"; import { validateStrList, validateBoolean, validateBooleanOrString, validateString } from "../utils/validation.js"; import { wrapCallback } from "../modules/callback.js"; import { SizeWatcher } from "../watchers/size-watcher.js"; import { ViewWatcher } from "../watchers/view-watcher.js"; import { Widget, registerWidget, getWidgetConfig, getDefaultWidgetSelector } from "./widget.js"; /* ******************** * Base Openable * ********************/ /** * Enables automatic setting up of an {@link Openable} widget from an * elements matching its content element selector (`[data-lisn-<name>]` or * `.lisn-<name>`). * * The name you specify here should generally be the same name you pass in * {@link OpenableProperties.name | options.name} to the * {@link Openable.constructor} but it does not need to be the same. * * @param name The name of the openable. Should be in kebab-case. * @param newOpenable Called for every element matching the selector. * @param configValidator A validator object, or a function that returns such * an object, for all options supported by the widget. * * @see {@link registerWidget} */ export const registerOpenable = (name, newOpenable, configValidator) => { registerWidget(name, (element, config) => { if (MH.isHTMLElement(element)) { if (!Openable.get(element)) { return newOpenable(element, config); } } else { logError(MH.usageError("Openable widget supports only HTMLElement")); } return null; }, configValidator); }; /** * {@link Openable} is an abstract base class. You should not directly * instantiate it but can inherit it to create your own custom openable widget. * * **IMPORTANT:** You should not instantiate more than one {@link Openable} * widget, regardless of type, on a given element. Use {@link Openable.get} to * get an existing instance if any. If there is already an {@link Openable} * widget of any type on this element, it will be destroyed! * * @see {@link registerOpenable} */ export class Openable extends Widget { /** * Retrieve an existing widget by its content element or any of its triggers. * * If the element is already part of a configured {@link Openable} widget, * the widget instance is returned. Otherwise `null`. * * Note that trigger elements are not guaranteed to be unique among openable * widgets as the same element can be a trigger for multiple such widgets. If * the element you pass is a trigger, then the last openable widget that was * created for it will be returned. */ static get(element) { var _instances$get; // We manage the instances here since we also map associated elements and // not just the main content element that created the widget. return (_instances$get = instances.get(element)) !== null && _instances$get !== void 0 ? _instances$get : null; } constructor(element, properties) { super(element); /** * Opens the widget unless it is disabled. */ _defineProperty(this, "open", void 0); /** * Closes the widget. */ _defineProperty(this, "close", void 0); /** * Closes the widget if it is open, or opens it if it is closed (unless * it is disabled). */ _defineProperty(this, "toggle", void 0); /** * The given handler will be called when the widget is open. * * If it returns a promise, it will be awaited upon. */ _defineProperty(this, "onOpen", void 0); /** * The given handler will be called when the widget is closed. * * If it returns a promise, it will be awaited upon. */ _defineProperty(this, "onClose", void 0); /** * Returns true if the widget is currently open. */ _defineProperty(this, "isOpen", void 0); /** * Returns the root element created by us that wraps the original content * element passed to the constructor. It is located in the content element's * original place. */ _defineProperty(this, "getRoot", void 0); /** * Returns the element that was found to be the container. It is the closest * ancestor that has a `lisn-collapsible-container` class, or if no such * ancestor then the immediate parent of the content element. */ _defineProperty(this, "getContainer", void 0); /** * Returns the trigger elements, if any. Note that these may be wrappers * around the original triggers passed. */ _defineProperty(this, "getTriggers", void 0); /** * Returns the trigger elements along with their configuration. */ _defineProperty(this, "getTriggerConfigs", void 0); const { isModal, isOffcanvas } = properties; const openCallbacks = MH.newSet(); const closeCallbacks = MH.newSet(); let isOpen = false; // ---------- const open = async () => { if (this.isDisabled() || isOpen) { return; } isOpen = true; for (const callback of openCallbacks) { await callback.invoke(this); } if (isModal) { setHasModal(); } await setBooleanData(root, PREFIX_IS_OPEN); }; // ---------- const close = async () => { if (this.isDisabled() || !isOpen) { return; } isOpen = false; for (const callback of closeCallbacks) { await callback.invoke(this); } if (isModal) { delHasModal(); } if (isOffcanvas) { scrollWrapperToTop(); // no need to await } await unsetBooleanData(root, PREFIX_IS_OPEN); }; // ---------- const scrollWrapperToTop = async () => { // Wait a bit before scrolling since the hiding of the element is animated. // Assume no more than 1s animation time. await waitForDelay(1000); await waitForMeasureTime(); MH.elScrollTo(outerWrapper, { top: 0, left: 0 }); }; // -------------------- this.open = open; this.close = close; this[MC.S_TOGGLE] = () => isOpen ? close() : open(); this.onOpen = handler => openCallbacks.add(wrapCallback(handler)); this.onClose = handler => closeCallbacks.add(wrapCallback(handler)); this.isOpen = () => isOpen; this.getRoot = () => root; this.getContainer = () => container; this.getTriggers = () => [...triggers.keys()]; this.getTriggerConfigs = () => MH.newMap([...triggers.entries()]); this.onDestroy(() => { openCallbacks.clear(); closeCallbacks.clear(); }); const { root, container, triggers, outerWrapper } = setupElements(this, element, properties); } } /** * Per-trigger based configuration. Can either be given as an object as the * value of the {@link OpenableProperties.triggers} map, or it can be set as a * string configuration in the `data-lisn-<name>-trigger` data attribute. See * {@link getWidgetConfig} for the syntax. * * @example * ```html * <div data-lisn-collapsible-trigger="auto-close * | icon=right * | icon-closed=arrow-down * | icon-open=x" * ></div> * ``` * * @interface */ /** * @interface */ /* ******************** * Collapsible * ********************/ /** * Configures the given element as a {@link Collapsible} widget. * * The Collapsible widget sets up the given element to be collapsed and * expanded upon activation. Activation can be done manually by calling * {@link open} or when clicking on any of the given * {@link CollapsibleConfig.triggers | triggers}. * * **NOTE:** The Collapsible widget always wraps each trigger element in * another element in order to allow positioning the icon, if any. * * **IMPORTANT:** You should not instantiate more than one {@link Openable} * widget, regardless of type, on a given element. Use {@link Openable.get} to * get an existing instance if any. If there is already an {@link Openable} * widget of any type on this element, it will be destroyed! * * ----- * * You can use the following dynamic attributes or CSS properties in your * stylesheet: * * The following dynamic attributes are set on the root element that is created * by LISN and has a class `lisn-collapsible__root`: * - `data-lisn-is-open`: `"true"` or `"false"` * - `data-lisn-reverse`: `"true"` or `"false"` * - `data-lisn-orientation`: `"horizontal"` or `"vertical"` * * The following dynamic attributes are set on each trigger: * - `data-lisn-opens-on-hover: `"true"` or `"false"` * * ----- * * To use with auto-widgets (HTML API) (see * {@link Settings.settings.autoWidgets | settings.autoWidgets}), the following * CSS classes or data attributes are recognized: * - `lisn-collapsible` class or `data-lisn-collapsible` attribute set on the * element that holds the content of the collapsible * - `lisn-collapsible-trigger` class or `data-lisn-collapsible-trigger` * attribute set on elements that should act as the triggers. * If using a data attribute, you can configure the trigger via the value * with a similar syntax to the configuration of the openable widget. For * example: * - Set the attribute to `"hover"` in order to have this trigger open the * collapsible on hover _in addition to click_. * - Set the attribute to `"hover|auto-close"` in order to have this trigger * open the collapsible on hover but and override * {@link CollapsibleConfig.autoClose} with true. * * When using auto-widgets, the elements that will be used as triggers are * discovered in the following way: * 1. If the content element has a `data-lisn-collapsible-content-id` attribute, * then it must be a unique (for the current page) ID. In this case, the * trigger elements will be any element in the document that has a * `lisn-collapsible-trigger` class or `data-lisn-collapsible-trigger` * attribute and the same `data-lisn-collapsible-content-id` attribute. * 2. Otherwise, the closest ancestor that has a `lisn-collapsible-container` * class, or if no such ancestor then the immediate parent of the content * element, is searched for any elements that have a * `lisn-collapsible-trigger` class or `data-lisn-collapsible-trigger` * attribute and that do _not_ have a `data-lisn-collapsible-content-id` * attribute, and that are _not_ children of the content element. * * See below examples for what values you can use set for the data attributes * in order to modify the configuration of the automatically created widget. * * @example * This defines a simple collapsible with one trigger. * * ```html * <div> * <div class="lisn-collapsible-trigger">Expand</div> * <div class="lisn-collapsible"> * Some long content here... * </div> * </div> * ``` * * @example * This defines a collapsible that is partially visible when collapsed, and * where the trigger is in a different parent to the content. * * ```html * <div> * <div data-lisn-collapsible-content-id="readmore" * data-lisn-collapsible="peek"> * <p> * Lorem ipsum odor amet, consectetuer adipiscing elit. Etiam duis * viverra faucibus facilisis luctus. Nunc tellus turpis facilisi dapibus * aliquet turpis. Diam potenti egestas dolor auctor nostra vestibulum. * Tempus auctor quis turpis; pulvinar ante ultrices. Netus morbi * imperdiet volutpat litora tellus turpis a. Sociosqu interdum sodales * sapien nulla aptent pellentesque praesent. Senectus magnis * pellentesque; dis porta justo habitant. * </p> * * <p> * Imperdiet placerat habitant tristique turpis habitasse ligula pretium * vehicula. Mauris molestie lectus leo aliquam condimentum elit fermentum * tempus nisi. Eget mi vestibulum quisque enim himenaeos. Odio nascetur * vel congue vivamus eleifend ut nascetur. Ultrices quisque non dictumst * risus libero varius tincidunt vel. Suscipit netus maecenas imperdiet * elementum donec maximus suspendisse luctus. Eu velit semper urna sem * ullamcorper nisl turpis hendrerit. Gravida commodo nisl malesuada nibh * ultricies scelerisque hendrerit tempus vehicula. Risus eleifend eros * aliquam turpis elit ridiculus est class. * </p> * </div> * </div> * * <div> * <div data-lisn-collapsible-content-id="readmore" * class="lisn-collapsible-trigger"> * Read more * </div> * </div> * ``` * * @example * As above, but with all other possible configuration settings set explicitly. * * ```html * <div> * <div data-lisn-collapsible-content-id="readmore" * data-lisn-collapsible="peek=50px * | horizontal=false * | reverse=false * | auto-close * | icon=right * | icon-closed=arrow-up" * | icon-open=arrow-down"> * <p> * Lorem ipsum odor amet, consectetuer adipiscing elit. Etiam duis * viverra faucibus facilisis luctus. Nunc tellus turpis facilisi dapibus * aliquet turpis. Diam potenti egestas dolor auctor nostra vestibulum. * Tempus auctor quis turpis; pulvinar ante ultrices. Netus morbi * imperdiet volutpat litora tellus turpis a. Sociosqu interdum sodales * sapien nulla aptent pellentesque praesent. Senectus magnis * pellentesque; dis porta justo habitant. * </p> * * <p> * Imperdiet placerat habitant tristique turpis habitasse ligula pretium * vehicula. Mauris molestie lectus leo aliquam condimentum elit fermentum * tempus nisi. Eget mi vestibulum quisque enim himenaeos. Odio nascetur * vel congue vivamus eleifend ut nascetur. Ultrices quisque non dictumst * risus libero varius tincidunt vel. Suscipit netus maecenas imperdiet * elementum donec maximus suspendisse luctus. Eu velit semper urna sem * ullamcorper nisl turpis hendrerit. Gravida commodo nisl malesuada nibh * ultricies scelerisque hendrerit tempus vehicula. Risus eleifend eros * aliquam turpis elit ridiculus est class. * </p> * </div> * </div> * * <div> * <div data-lisn-collapsible-content-id="readmore" * class="lisn-collapsible-trigger"> * Read more * </div> * </div> * ``` */ export class Collapsible extends Openable { static register() { registerOpenable(WIDGET_NAME_COLLAPSIBLE, (el, config) => new Collapsible(el, config), collapsibleConfigValidator); } constructor(element, config) { var _config$autoClose, _config$reverse; const isHorizontal = config === null || config === void 0 ? void 0 : config.horizontal; const orientation = isHorizontal ? MC.S_HORIZONTAL : MC.S_VERTICAL; const onSetup = () => { // The triggers here are wrappers around the original which will be // replaced by the original on destroy, so no need to clean up this. for (const [trigger, triggerConfig] of this.getTriggerConfigs().entries()) { insertCollapsibleIcon(trigger, triggerConfig, this, config); setDataNow(trigger, MC.PREFIX_ORIENTATION, orientation); } }; super(element, { name: WIDGET_NAME_COLLAPSIBLE, id: config === null || config === void 0 ? void 0 : config.id, className: config === null || config === void 0 ? void 0 : config.className, autoClose: (_config$autoClose = config === null || config === void 0 ? void 0 : config.autoClose) !== null && _config$autoClose !== void 0 ? _config$autoClose : false, isModal: false, isOffcanvas: false, closeButton: false, triggers: config === null || config === void 0 ? void 0 : config.triggers, wrapTriggers: true, onSetup }); const root = this.getRoot(); const wrapper = MH.childrenOf(root)[0]; setData(root, MC.PREFIX_ORIENTATION, orientation); setBooleanData(root, PREFIX_REVERSE, (_config$reverse = config === null || config === void 0 ? void 0 : config.reverse) !== null && _config$reverse !== void 0 ? _config$reverse : false); // -------------------- Transitions disableInitialTransition(element, 100); disableInitialTransition(root, 100); disableInitialTransition(wrapper, 100); let disableTransitionTimer = null; const tempEnableTransition = async () => { await removeClasses(root, MC.PREFIX_TRANSITION_DISABLE); await removeClasses(wrapper, MC.PREFIX_TRANSITION_DISABLE); if (disableTransitionTimer) { MH.clearTimer(disableTransitionTimer); } const transitionDuration = await getMaxTransitionDuration(root); disableTransitionTimer = MH.setTimer(() => { if (this.isOpen()) { addClasses(root, MC.PREFIX_TRANSITION_DISABLE); addClasses(wrapper, MC.PREFIX_TRANSITION_DISABLE); disableTransitionTimer = null; } }, transitionDuration); }; // Disable transitions except during open/close, so that resizing the // window for example doesn't result in lagging width/height transition. this.onOpen(tempEnableTransition); this.onClose(tempEnableTransition); // -------------------- Peek const peek = config === null || config === void 0 ? void 0 : config.peek; if (peek) { (async () => { let peekSize = null; if (MH.isString(peek)) { peekSize = peek; } else { peekSize = await getStyleProp(element, VAR_PEEK_SIZE); } addClasses(root, PREFIX_PEEK); if (peekSize) { setStyleProp(root, VAR_PEEK_SIZE, peekSize); } })(); } // -------------------- Width in horizontal mode if (isHorizontal) { const updateWidth = async () => { const width = await getComputedStyleProp(root, MC.S_WIDTH); await setStyleProp(element, VAR_JS_COLLAPSIBLE_WIDTH, width); }; MH.setTimer(updateWidth); // Save its current width so that if it contains text, it does not // "collapse" and end up super tall. this.onClose(updateWidth); this.onOpen(async () => { // Update the content width before opening. await updateWidth(); // Delete the fixed width property soon after opening to allow it to // resize again while it's open. waitForDelay(2000).then(() => { if (this.isOpen()) { delStyleProp(element, VAR_JS_COLLAPSIBLE_WIDTH); } }); }); } } } /** * @interface */ /* ******************** * Popup * ********************/ /** * Configures the given element as a {@link Popup} widget. * * The Popup widget sets up the given element to be hidden and open in a * floating popup upon activation. Activation can be done manually by calling * {@link open} or when clicking on any of the given * {@link PopupConfig.triggers | triggers}. * * **IMPORTANT:** The popup is positioned absolutely in its container and the * position is relative to the container. The container gets `width: * fit-content` by default but you can override this in your CSS. The popup * also gets a configurable `min-width` set. * * **IMPORTANT:** You should not instantiate more than one {@link Openable} * widget, regardless of type, on a given element. Use {@link Openable.get} to * get an existing instance if any. If there is already an {@link Openable} * widget of any type on this element, it will be destroyed! * * ----- * * You can use the following dynamic attributes or CSS properties in your * stylesheet: * * The following dynamic attributes are set on the root element that is created * by LISN and has a class `lisn-popup__root`: * - `data-lisn-is-open`: `"true"` or `"false"` * - `data-lisn-place`: the actual position (top, bottom, left, top-left, etc) * * The following dynamic attributes are set on each trigger: * - `data-lisn-opens-on-hover: `"true"` or `"false"` * * ----- * * To use with auto-widgets (HTML API) (see * {@link Settings.settings.autoWidgets | settings.autoWidgets}), the following * CSS classes or data attributes are recognized: * - `lisn-popup` class or `data-lisn-popup` attribute set on the element that * holds the content of the popup * - `lisn-popup-trigger` class or `data-lisn-popup-trigger` * attribute set on elements that should act as the triggers. * If using a data attribute, you can configure the trigger via the value * with a similar syntax to the configuration of the openable widget. For * example: * - Set the attribute to `"hover"` in order to have this trigger open the * popup on hover _in addition to click_. * - Set the attribute to `"hover|auto-close=false"` in order to have this * trigger open the popup on hover but and override * {@link PopupConfig.autoClose} with true. * * When using auto-widgets, the elements that will be used as triggers are * discovered in the following way: * 1. If the content element has a `data-lisn-popup-content-id` attribute, then * it must be a unique (for the current page) ID. In this case, the trigger * elements will be any element in the document that has a * `lisn-popup-trigger` class or `data-lisn-popup-trigger` attribute and the * same `data-lisn-popup-content-id` attribute. * 2. Otherwise, the closest ancestor that has a `lisn-popup-container` class, * or if no such ancestor then the immediate parent of the content element, * is searched for any elements that have a `lisn-popup-trigger` class or * `data-lisn-popup-trigger` attribute and that do _not_ have a * `data-lisn-popup-content-id` attribute, and that are _not_ children of * the content element. * * See below examples for what values you can use set for the data attributes * in order to modify the configuration of the automatically created widget. * * @example * This defines a simple popup with one trigger. * * ```html * <div> * <div class="lisn-popup-trigger">Open</div> * <div class="lisn-popup"> * Some content here... * </div> * </div> * ``` * * @example * This defines a popup that has a close button, and where the trigger is in a * different parent to the content. * * ```html * <div> * <div data-lisn-popup-content-id="popup" * data-lisn-popup="close-button"> * Lorem ipsum odor amet, consectetuer adipiscing elit. Etiam duis viverra * faucibus facilisis luctus. Nunc tellus turpis facilisi dapibus aliquet * turpis. Diam potenti egestas dolor auctor nostra vestibulum. Tempus * auctor quis turpis; pulvinar ante ultrices. Netus morbi imperdiet * volutpat litora tellus turpis a. Sociosqu interdum sodales sapien nulla * aptent pellentesque praesent. Senectus magnis pellentesque; dis porta * justo habitant. * </div> * </div> * * <div> * <div data-lisn-popup-content-id="popup" class="lisn-popup-trigger"> * Open * </div> * </div> * ``` * * @example * As above, but with all possible configuration settings set explicitly. * * ```html * <div> * <div data-lisn-popup-content-id="popup" class="lisn-popup-trigger"> * Open * </div> * </div> * * <div> * <div data-lisn-popup-content-id="popup" * data-lisn-popup="close-button | position=bottom | auto-close=false"> * Lorem ipsum odor amet, consectetuer adipiscing elit. Etiam duis viverra * faucibus facilisis luctus. Nunc tellus turpis facilisi dapibus aliquet * turpis. Diam potenti egestas dolor auctor nostra vestibulum. Tempus * auctor quis turpis; pulvinar ante ultrices. Netus morbi imperdiet * volutpat litora tellus turpis a. Sociosqu interdum sodales sapien nulla * aptent pellentesque praesent. Senectus magnis pellentesque; dis porta * justo habitant. * </div> * </div> * ``` */ export class Popup extends Openable { static register() { registerOpenable(WIDGET_NAME_POPUP, (el, config) => new Popup(el, config), popupConfigValidator); } constructor(element, config) { var _config$autoClose2, _config$closeButton; super(element, { name: WIDGET_NAME_POPUP, id: config === null || config === void 0 ? void 0 : config.id, className: config === null || config === void 0 ? void 0 : config.className, autoClose: (_config$autoClose2 = config === null || config === void 0 ? void 0 : config.autoClose) !== null && _config$autoClose2 !== void 0 ? _config$autoClose2 : true, isModal: false, isOffcanvas: false, closeButton: (_config$closeButton = config === null || config === void 0 ? void 0 : config.closeButton) !== null && _config$closeButton !== void 0 ? _config$closeButton : false, triggers: config === null || config === void 0 ? void 0 : config.triggers }); const root = this.getRoot(); const container = this.getContainer(); const position = (config === null || config === void 0 ? void 0 : config.position) || S_AUTO; if (position !== S_AUTO) { setData(root, MC.PREFIX_PLACE, position); } if (container && position === S_AUTO) { // Automatic position this.onOpen(async () => { const [contentSize, containerView] = await MH.promiseAll([SizeWatcher.reuse().fetchCurrentSize(element), ViewWatcher.reuse().fetchCurrentView(container)]); const placement = await fetchPopupPlacement(contentSize, containerView); if (placement) { await setData(root, MC.PREFIX_PLACE, placement); } }); } } } /** * @interface */ /* ******************** * Modal * ********************/ /** * Configures the given element as a {@link Modal} widget. * * The Modal widget sets up the given element to be hidden and open in a fixed * full-screen modal popup upon activation. Activation can be done manually by * calling {@link open} or when clicking on any of the given * {@link ModalConfig.triggers | triggers}. * * **IMPORTANT:** You should not instantiate more than one {@link Openable} * widget, regardless of type, on a given element. Use {@link Openable.get} to * get an existing instance if any. If there is already an {@link Openable} * widget of any type on this element, it will be destroyed! * * ----- * * You can use the following dynamic attributes or CSS properties in your * stylesheet: * * The following dynamic attributes are set on the root element that is created * by LISN and has a class `lisn-modal__root`: * - `data-lisn-is-open`: `"true"` or `"false"` * * The following dynamic attributes are set on each trigger: * - `data-lisn-opens-on-hover: `"true"` or `"false"` * * ----- * * To use with auto-widgets (HTML API) (see * {@link Settings.settings.autoWidgets | settings.autoWidgets}), the following * CSS classes or data attributes are recognized: * - `lisn-modal` class or `data-lisn-modal` attribute set on the element that * holds the content of the modal * - `lisn-modal-trigger` class or `data-lisn-modal-trigger` * attribute set on elements that should act as the triggers. * If using a data attribute, you can configure the trigger via the value * with a similar syntax to the configuration of the openable widget. For * example: * - Set the attribute to `"hover"` in order to have this trigger open the * modal on hover _in addition to click_. * - Set the attribute to `"hover|auto-close=false"` in order to have this * trigger open the modal on hover but and override * {@link ModalConfig.autoClose} with true. * * When using auto-widgets, the elements that will be used as triggers are * discovered in the following way: * 1. If the content element has a `data-lisn-modal-content-id` attribute, then * it must be a unique (for the current page) ID. In this case, the trigger * elements will be any element in the document that has a * `lisn-modal-trigger` class or `data-lisn-modal-trigger` attribute and the * same `data-lisn-modal-content-id` attribute. * 2. Otherwise, the closest ancestor that has a `lisn-modal-container` class, * or if no such ancestor then the immediate parent of the content element, * is searched for any elements that have a `lisn-modal-trigger` class or * `data-lisn-modal-trigger` attribute and that do _not_ have a * `data-lisn-modal-content-id` attribute, and that are _not_ children of * the content element. * * See below examples for what values you can use set for the data attributes * in order to modify the configuration of the automatically created widget. * * @example * This defines a simple modal with one trigger. * * ```html * <div> * <div class="lisn-modal-trigger">Open</div> * <div class="lisn-modal"> * Some content here... * </div> * </div> * ``` * * @example * This defines a modal that doesn't automatically close on click outside or * Escape and, and that has several triggers in a different parent to the * content. * * ```html * <div> * <div data-lisn-modal-content-id="modal" * data-lisn-modal="auto-close=false"> * Lorem ipsum odor amet, consectetuer adipiscing elit. Etiam duis viverra * faucibus facilisis luctus. Nunc tellus turpis facilisi dapibus aliquet * turpis. Diam potenti egestas dolor auctor nostra vestibulum. Tempus * auctor quis turpis; pulvinar ante ultrices. Netus morbi imperdiet * volutpat litora tellus turpis a. Sociosqu interdum sodales sapien nulla * aptent pellentesque praesent. Senectus magnis pellentesque; dis porta * justo habitant. * </div> * </div> * * <div> * <div data-lisn-modal-content-id="modal" class="lisn-modal-trigger"> * Open * </div> * </div> * * <div> * <div data-lisn-modal-content-id="modal" class="lisn-modal-trigger"> * Another trigger * </div> * </div> * ``` * * @example * As above, but with all possible configuration settings set explicitly. * * ```html * <div> * <div data-lisn-modal-content-id="modal" * data-lisn-modal="auto-close=false | close-button=true"> * Lorem ipsum odor amet, consectetuer adipiscing elit. Etiam duis viverra * faucibus facilisis luctus. Nunc tellus turpis facilisi dapibus aliquet * turpis. Diam potenti egestas dolor auctor nostra vestibulum. Tempus * auctor quis turpis; pulvinar ante ultrices. Netus morbi imperdiet * volutpat litora tellus turpis a. Sociosqu interdum sodales sapien nulla * aptent pellentesque praesent. Senectus magnis pellentesque; dis porta * justo habitant. * </div> * </div> * * <div> * <div data-lisn-modal-content-id="modal" class="lisn-modal-trigger"> * Open * </div> * </div> * * <div> * <div data-lisn-modal-content-id="modal" class="lisn-modal-trigger"> * Another trigger * </div> * </div> * ``` */ export class Modal extends Openable { static register() { registerOpenable(WIDGET_NAME_MODAL, (el, config) => new Modal(el, config), modalConfigValidator); } constructor(element, config) { var _config$autoClose3, _config$closeButton2; super(element, { name: WIDGET_NAME_MODAL, id: config === null || config === void 0 ? void 0 : config.id, className: config === null || config === void 0 ? void 0 : config.className, autoClose: (_config$autoClose3 = config === null || config === void 0 ? void 0 : config.autoClose) !== null && _config$autoClose3 !== void 0 ? _config$autoClose3 : true, isModal: true, isOffcanvas: true, closeButton: (_config$closeButton2 = config === null || config === void 0 ? void 0 : config.closeButton) !== null && _config$closeButton2 !== void 0 ? _config$closeButton2 : true, triggers: config === null || config === void 0 ? void 0 : config.triggers }); } } /** * @interface */ /* ******************** * Offcanvas * ********************/ /** * Configures the given element as a {@link Offcanvas} widget. * * The Offcanvas widget sets up the given element to be hidden and open in a * fixed overlay (non full-screen) upon activation. Activation can be done * manually by calling {@link open} or when clicking on any of the given * {@link OffcanvasConfig.triggers | triggers}. * * **IMPORTANT:** You should not instantiate more than one {@link Openable} * widget, regardless of type, on a given element. Use {@link Openable.get} to * get an existing instance if any. If there is already an {@link Openable} * widget of any type on this element, it will be destroyed! * * ----- * * You can use the following dynamic attributes or CSS properties in your * stylesheet: * * The following dynamic attributes are set on the root element that is created * by LISN and has a class `lisn-offcanvas__root`: * - `data-lisn-is-open`: `"true"` or `"false"` * - `data-lisn-place`: the actual position `"top"`, `"bottom"`, `"left"` or * `"right"` * * The following dynamic attributes are set on each trigger: * - `data-lisn-opens-on-hover: `"true"` or `"false"` * * ----- * * To use with auto-widgets (HTML API) (see * {@link Settings.settings.autoWidgets | settings.autoWidgets}), the following * CSS classes or data attributes are recognized: * - `lisn-offcanvas` class or `data-lisn-offcanvas` attribute set on the * element that holds the content of the offcanvas * - `lisn-offcanvas-trigger` class or `data-lisn-offcanvas-trigger` * attribute set on elements that should act as the triggers. * If using a data attribute, you can configure the trigger via the value * with a similar syntax to the configuration of the openable widget. For * example: * - Set the attribute to `"hover"` in order to have this trigger open the * offcanvas on hover _in addition to click_. * - Set the attribute to `"hover|auto-close=false"` in order to have this * trigger open the offcanvas on hover but and override * {@link OffcanvasConfig.autoClose} with true. * * When using auto-widgets, the elements that will be used as triggers are * discovered in the following way: * 1. If the content element has a `data-lisn-offcanvas-content-id` attribute, * then it must be a unique (for the current page) ID. In this case, the * trigger elements will be any element in the document that has a * `lisn-offcanvas-trigger` class or `data-lisn-offcanvas-trigger` attribute * and the same `data-lisn-offcanvas-content-id` attribute. * 2. Otherwise, the closest ancestor that has a `lisn-offcanvas-container` * class, or if no such ancestor then the immediate parent of the content * element, is searched for any elements that have a * `lisn-offcanvas-trigger` class or `data-lisn-offcanvas-trigger` attribute * and that do _not_ have a `data-lisn-offcanvas-content-id` * attribute, and that are _not_ children of the content element. * * See below examples for what values you can use set for the data attributes * in order to modify the configuration of the automatically created widget. * * @example * This defines a simple offcanvas with one trigger. * * ```html * <div> * <div class="lisn-offcanvas-trigger">Open</div> * <div class="lisn-offcanvas"> * Some content here... * </div> * </div> * ``` * * @example * This defines a offcanvas that doesn't automatically close on click outside * or Escape and, and that has several triggers in a different parent to the * content. * * ```html * <div> * <div data-lisn-offcanvas-content-id="offcanvas" * data-lisn-offcanvas="auto-close=false"> * Lorem ipsum odor amet, consectetuer adipiscing elit. Etiam duis viverra * faucibus facilisis luctus. Nunc tellus turpis facilisi dapibus aliquet * turpis. Diam potenti egestas dolor auctor nostra vestibulum. Tempus * auctor quis turpis; pulvinar ante ultrices. Netus morbi imperdiet * volutpat litora tellus turpis a. Sociosqu interdum sodales sapien nulla * aptent pellentesque praesent. Senectus magnis pellentesque; dis porta * justo habitant. * </div> * </div> * * <div> * <div data-lisn-offcanvas-content-id="offcanvas" class="lisn-offcanvas-trigger"> * Open * </div> * </div> * * <div> * <div data-lisn-offcanvas-content-id="offcanvas" class="lisn-offcanvas-trigger"> * Another trigger * </div> * </div> * ``` * * @example * As above, but with all possible configuration settings set explicitly. * * ```html * <div> * <div data-lisn-offcanvas-content-id="offcanvas" * data-lisn-offcanvas="position=top | auto-close=false | close-button=true"> * Lorem ipsum odor amet, consectetuer adipiscing elit. Etiam duis viverra * faucibus facilisis luctus. Nunc tellus turpis facilisi dapibus aliquet * turpis. Diam potenti egestas dolor auctor nostra vestibulum. Tempus * auctor quis turpis; pulvinar ante ultrices. Netus morbi imperdiet * volutpat litora tellus turpis a. Sociosqu interdum sodales sapien nulla * aptent pellentesque praesent. Senectus magnis pellentesque; dis porta * justo habitant. * </div> * </div> * * <div> * <div data-lisn-offcanvas-content-id="offcanvas" class="lisn-offcanvas-trigger"> * Open * </div> * </div> * * <div> * <div data-lisn-offcanvas-content-id="offcanvas" class="lisn-offcanvas-trigger"> * Another trigger * </div> * </div> * ``` */ export class Offcanvas extends Openable { static register() { registerOpenable(WIDGET_NAME_OFFCANVAS, (el, config) => new Offcanvas(el, config), offcanvasConfigValidator); } constructor(element, config) { var _config$autoClose4, _config$closeButton3; super(element, { name: WIDGET_NAME_OFFCANVAS, id: config === null || config === void 0 ? void 0 : config.id, className: config === null || config === void 0 ? void 0 : config.className, autoClose: (_config$autoClose4 = config === null || config === void 0 ? void 0 : config.autoClose) !== null && _config$autoClose4 !== void 0 ? _config$autoClose4 : true, isModal: false, isOffcanvas: true, closeButton: (_config$closeButton3 = config === null || config === void 0 ? void 0 : config.closeButton) !== null && _config$closeButton3 !== void 0 ? _config$closeButton3 : true, triggers: config === null || config === void 0 ? void 0 : config.triggers }); const position = (config === null || config === void 0 ? void 0 : config.position) || MC.S_RIGHT; setData(this.getRoot(), MC.PREFIX_PLACE, position); } } /** * @interface */ // ------------------------------ const instances = MH.newWeakMap(); const WIDGET_NAME_COLLAPSIBLE = "collapsible"; const WIDGET_NAME_POPUP = "popup"; const WIDGET_NAME_MODAL = "modal"; const WIDGET_NAME_OFFCANVAS = "offcanvas"; const PREFIX_CLOSE_BTN = MH.prefixName("close-button"); const PREFIX_IS_OPEN = MH.prefixName("is-open"); const PREFIX_REVERSE = MH.prefixName(MC.S_REVERSE); const PREFIX_PEEK = MH.prefixName("peek"); const PREFIX_OPENS_ON_HOVER = MH.prefixName("opens-on-hover"); const PREFIX_LINE = MH.prefixName("line"); const PREFIX_ICON_POSITION = MH.prefixName("icon-position"); const PREFIX_TRIGGER_ICON = MH.prefixName("trigger-icon"); const PREFIX_ICON_WRAPPER = MH.prefixName("icon-wrapper"); const S_AUTO = "auto"; const S_ARIA_EXPANDED = MC.ARIA_PREFIX + "expanded"; const S_ARIA_MODAL = MC.ARIA_PREFIX + "modal"; const VAR_PEEK_SIZE = MH.prefixCssVar("peek-size"); const VAR_JS_COLLAPSIBLE_WIDTH = MH.prefixCssJsVar("collapsible-width"); const MIN_CLICK_TIME_AFTER_HOVER_OPEN = 1000; const S_ARROW_UP = `${MC.S_ARROW}-${MC.S_UP}`; const S_ARROW_DOWN = `${MC.S_ARROW}-${MC.S_DOWN}`; const S_ARROW_LEFT = `${MC.S_ARROW}-${MC.S_LEFT}`; const S_ARROW_RIGHT = `${MC.S_ARROW}-${MC.S_RIGHT}`; const ARROW_TYPES = [S_ARROW_UP, S_ARROW_DOWN, S_ARROW_LEFT, S_ARROW_RIGHT]; const ICON_CLOSED_TYPES = ["plus", ...ARROW_TYPES]; const ICON_OPEN_TYPES = ["minus", "x", ...ARROW_TYPES]; const isValidIconClosed = value => MH.includes(ICON_CLOSED_TYPES, value); const isValidIconOpen = value => MH.includes(ICON_OPEN_TYPES, value); const triggerConfigValidator = { id: validateString, className: (key, value) => validateStrList(key, toArrayIfSingle(value)), autoClose: validateBoolean, icon: (key, value) => value && toBoolean(value) === false ? false : validateString(key, value, isValidPosition), iconClosed: (key, value) => validateString(key, value, isValidIconClosed), iconOpen: (key, value) => validateString(key, value, isValidIconOpen), hover: validateBoolean }; const collapsibleConfigValidator = { id: validateString, className: (key, value) => validateStrList(key, toArrayIfSingle(value)), horizontal: validateBoolean, reverse: validateBoolean, peek: validateBooleanOrString, autoClose: validateBoolean, icon: (key, value) => toBoolean(value) === false ? false : validateString(key, value, isValidPosition), iconClosed: (key, value) => validateString(key, value, isValidIconClosed), iconOpen: (key, value) => validateString(key, value, isValidIconOpen) }; const popupConfigValidator = { id: validateString, className: (key, value) => validateStrList(key, toArrayIfSingle(value)), closeButton: validateBoolean, position: (key, value) => validateString(key, value, v => v === S_AUTO || isValidPosition(v) || isValidTwoFoldPosition(v)), autoClose: validateBoolean }; const modalConfigValidator = { id: validateString, className: (key, value) => validateStrList(key, toArrayIfSingle(value)), closeButton: validateBoolean, autoClose: validateBoolean }; const offcanvasConfigValidator = { id: validateString, className: (key, value) => validateStrList(key, toArrayIfSingle(value)), closeButton: validateBoolean, position: (key, value) => validateString(key, value, isValidPosition), autoClose: validateBoolean }; const getPrefixedNames = name => { const pref = MH.prefixName(name); return { _root: `${pref}__root`, _overlay: `${pref}__overlay`, // only used for modal/offcanvas _innerWrapper: `${pref}__inner-wrapper`, _outerWrapper: `${pref}__outer-wrapper`, _content: `${pref}__content`, _container: `${pref}__container`, _trigger: `${pref}__trigger`, // Use different classes for styling to the ones used for auto-discovering // elements, so that re-creating existing widgets can correctly find the // elements to be used by the new widget synchronously before the current // one is destroyed. _containerForSelect: `${pref}-container`, _triggerForSelect: `${pref}-trigger`, _contentId: `${pref}-content-id` }; }; const findContainer = (content, cls) => { var _currWidget$getRoot; const currWidget = instances.get(content); // If there's an existing widget that we're about to destroy, the content // element will be wrapped in several elements and won't be restored until // the next mutate time. In that case, to correctly determine the container // element, use the current widget's root element, which is located in the // content element's original place. let childRef = (_currWidget$getRoot = currWidget === null || currWidget === void 0 ? void 0 : currWidget.getRoot()) !== null && _currWidget$getRoot !== void 0 ? _currWidget$getRoot : content; if (!MH.parentOf(childRef)) { // The current widget is not yet initialized (i.e. we are re-creating it // immediately after it was constructed) childRef = content; } // Find the content container let container = childRef.closest(`.${cls}`); if (!container) { container = MH.parentOf(childRef); } return container; }; const findTriggers = (content, prefixedNames) => { const container = findContainer(content, prefixedNames._containerForSelect); // jsdom does not like the below selector when suffixed by [data-*] or :not()... // const triggerSelector = `:is(.${prefixedNames._triggerForSelect},[data-${prefixedNames._triggerForSelect}])`; // So use this: const getTriggerSelector = suffix => `.${prefixedNames._triggerForSelect}${suffix},` + `[data-${prefixedNames._triggerForSelect}]${suffix}`; const contentId = getData(content, prefixedNames._contentId); let triggers = []; if (contentId) { triggers = [...MH.docQuerySelectorAll(getTriggerSelector(`[data-${prefixedNames._contentId}="${contentId}"]`))]; } else if (container) { triggers = [...MH.arrayFrom(MH.querySelectorAll(container, getTriggerSelector(`:not([data-${prefixedNames._contentId}])`))).filter(t => !content.contains(t))]; } return triggers; }; const getTriggersFrom = (content, inputTriggers, wrapTriggers, prefixedNames) => { const triggerMap = MH.newMap(); inputTriggers = inputTriggers || findTriggers(content, prefixedNames); const addTrigger = (trigger, triggerConfig) => { if (wrapTriggers) { const wrapper = MH.createElement(isInlineTag(MH.tagName(trigger)) ? "span" : "div"); wrapElement(trigger, { wrapper, ignoreMove: true }); // no need to await trigger = wrapper; } triggerMap.set(trigger, triggerConfig); }; if (MH.isArray(inputTriggers)) { for (const trigger of inputTriggers) { addTrigger(trigger, getWidgetConfig(getData(trigger, prefixedNames._triggerForSelect), triggerConfigValidator)); } } else if (MH.isInstanceOf(inputTriggers, Map)) { for (const [trigger, triggerConfig] of inputTriggers.entries()) { addTrigger(trigger, getWidgetConfig(triggerConfig, triggerConfigValidator)); } } return triggerMap; }; const setupElements = (widget, content, properties) => { var _properties$wrapTrigg; const prefixedNames = getPrefixedNames(properties.name); const container = findContainer(content, prefixedNames._containerForSelect); const wrapTriggers = (_properties$wrapTrigg = properties.wrapTriggers) !== null && _properties$wrapTrigg !== void 0 ? _properties$wrapTrigg : false; const triggers = getTriggersFrom(content, properties.triggers, wrapTriggers, prefixedNames); // Create two wrappers const innerWrapper = MH.createElement("div"); addClasses(innerWrapper, prefixedNames._innerWrapper); const outerWrapper = wrapElementNow(innerWrapper); // Setup the root element. // For off-canvas types we need another wrapper to serve as the root and we // need a placeholder element to save the content's original position so it // can be restored on destroy. // Otherwise use outerWrapper for root and insert the root where the content // was. let root; let placeholder; if (properties.isOffcanvas) { addClasses(outerWrapper, prefixedNames._outerWrapper); root = wrapElementNow(outerWrapper); placeholder = MH.createElement("div"); const overlay = MH.createElement("div"); addClasses(overlay, prefixedNames._overlay); moveElement(overlay, { to: root }); } else { // Otherwise use the outer wrapper as the root root = placeholder = outerWrapper; } if (properties.id) { root.id = properties.id; } if (properties.className) { addClassesNow(root, ...toArrayIfSingle(properties.className)); } unsetBooleanData(root, PREFIX_IS_OPEN); const domID = getOrAssignID(root, properties.name); if (properties.isModal) { MH.setAttr(root, MC.S_ROLE, "dialog"); MH.setAttr(root, S_ARIA_MODAL); } addClasses(root, prefixedNames._root); disableInitialTransition(root); // Add a close button? if (properties.closeButton) { const closeBtn = MH.createButton("Close"); addClasses(closeBtn, PREFIX_CLOSE_BTN); // If autoClose is true, it will be closed on click anyway, because the // close button is outside the content. addEventListenerTo(closeBtn, MC.S_CLICK, () => { widget.close(); }); moveElement(closeBtn, { to: properties.isOffcanvas ? root : innerWrapper }); } // Transfer the relevant classes/data attrs from content to root element, so // that our CSS can work without :has. // This won't cause fo