lisn.js
Version:
Simply handle user gestures and actions. Includes widgets.
1,196 lines (1,164 loc) • 66.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.registerOpenable = exports.Popup = exports.Openable = exports.Offcanvas = exports.Modal = exports.Collapsible = void 0;
var MC = _interopRequireWildcard(require("../globals/minification-constants.cjs"));
var MH = _interopRequireWildcard(require("../globals/minification-helpers.cjs"));
var _settings = require("../globals/settings.cjs");
var _cssAlter = require("../utils/css-alter.cjs");
var _domAlter = require("../utils/dom-alter.cjs");
var _domEvents = require("../utils/dom-events.cjs");
var _domOptimize = require("../utils/dom-optimize.cjs");
var _event = require("../utils/event.cjs");
var _log = require("../utils/log.cjs");
var _math = require("../utils/math.cjs");
var _misc = require("../utils/misc.cjs");
var _tasks = require("../utils/tasks.cjs");
var _position = require("../utils/position.cjs");
var _size = require("../utils/size.cjs");
var _validation = require("../utils/validation.cjs");
var _callback = require("../modules/callback.cjs");
var _sizeWatcher = require("../watchers/size-watcher.cjs");
var _viewWatcher = require("../watchers/view-watcher.cjs");
var _widget = require("./widget.cjs");
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
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
*/
/* ********************
* 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 OpenableConfig.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}
*/
const registerOpenable = (name, newOpenable, configValidator) => {
(0, _widget.registerWidget)(name, (element, config) => {
if (MH.isHTMLElement(element)) {
if (!Openable.get(element)) {
return newOpenable(element, config);
}
} else {
(0, _log.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}
*/
exports.registerOpenable = registerOpenable;
class Openable extends _widget.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, config) {
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
} = config;
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) {
(0, _cssAlter.setHasModal)();
}
await (0, _cssAlter.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) {
(0, _cssAlter.delHasModal)();
}
if (isOffcanvas) {
scrollWrapperToTop(); // no need to await
}
await (0, _cssAlter.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 (0, _tasks.waitForDelay)(1000);
await (0, _domOptimize.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((0, _callback.wrapCallback)(handler));
this.onClose = handler => closeCallbacks.add((0, _callback.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
} = init(this, element, config);
}
}
/**
* Per-trigger based configuration. Can either be given as an object as the
* value of the {@link OpenableConfig.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
*/
/**
* @interface
* @ignore
* @deprecated
*
* Deprecated alias for {@link OpenableConfig}
*/
exports.Openable = Openable;
/* ********************
* 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>
* ```
*/
class Collapsible extends Openable {
static register() {
registerOpenable(WIDGET_NAME_COLLAPSIBLE, (element, config) => new Collapsible(element, 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);
(0, _cssAlter.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];
(0, _cssAlter.setData)(root, MC.PREFIX_ORIENTATION, orientation);
(0, _cssAlter.setBooleanData)(root, PREFIX_REVERSE, (_config$reverse = config === null || config === void 0 ? void 0 : config.reverse) !== null && _config$reverse !== void 0 ? _config$reverse : false);
// -------------------- Transitions
(0, _cssAlter.disableInitialTransition)(element, 100);
(0, _cssAlter.disableInitialTransition)(root, 100);
(0, _cssAlter.disableInitialTransition)(wrapper, 100);
let disableTransitionTimer = null;
const tempEnableTransition = async () => {
await (0, _cssAlter.removeClasses)(root, MC.PREFIX_TRANSITION_DISABLE);
await (0, _cssAlter.removeClasses)(wrapper, MC.PREFIX_TRANSITION_DISABLE);
if (disableTransitionTimer) {
MH.clearTimer(disableTransitionTimer);
}
const transitionDuration = await (0, _cssAlter.getMaxTransitionDuration)(root);
disableTransitionTimer = MH.setTimer(() => {
if (this.isOpen()) {
(0, _cssAlter.addClasses)(root, MC.PREFIX_TRANSITION_DISABLE);
(0, _cssAlter.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 (0, _cssAlter.getStyleProp)(element, VAR_PEEK_SIZE);
}
(0, _cssAlter.addClasses)(root, PREFIX_PEEK);
if (peekSize) {
(0, _cssAlter.setStyleProp)(root, VAR_PEEK_SIZE, peekSize);
}
})();
}
// -------------------- Width in horizontal mode
if (isHorizontal) {
const updateWidth = async () => {
const width = await (0, _cssAlter.getComputedStyleProp)(root, MC.S_WIDTH);
await (0, _cssAlter.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.
(0, _tasks.waitForDelay)(2000).then(() => {
if (this.isOpen()) {
(0, _cssAlter.delStyleProp)(element, VAR_JS_COLLAPSIBLE_WIDTH);
}
});
});
}
}
}
/**
* @interface
*/
exports.Collapsible = Collapsible;
/* ********************
* 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>
* ```
*/
class Popup extends Openable {
static register() {
registerOpenable(WIDGET_NAME_POPUP, (element, config) => new Popup(element, 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) {
(0, _cssAlter.setData)(root, MC.PREFIX_PLACE, position);
}
if (container && position === S_AUTO) {
// Automatic position
this.onOpen(async () => {
const [contentSize, containerView] = await MH.promiseAll([_sizeWatcher.SizeWatcher.reuse().fetchCurrentSize(element), _viewWatcher.ViewWatcher.reuse().fetchCurrentView(container)]);
const placement = await fetchPopupPlacement(contentSize, containerView);
if (placement) {
await (0, _cssAlter.setData)(root, MC.PREFIX_PLACE, placement);
}
});
}
}
}
/**
* @interface
*/
exports.Popup = Popup;
/* ********************
* 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>
* ```
*/
class Modal extends Openable {
static register() {
registerOpenable(WIDGET_NAME_MODAL, (element, config) => new Modal(element, 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
*/
exports.Modal = Modal;
/* ********************
* 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>
* ```
*/
class Offcanvas extends Openable {
static register() {
registerOpenable(WIDGET_NAME_OFFCANVAS, (element, config) => new Offcanvas(element, 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;
(0, _cssAlter.setData)(this.getRoot(), MC.PREFIX_PLACE, position);
}
}
/**
* @interface
*/
// ------------------------------
exports.Offcanvas = Offcanvas;
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: _validation.validateString,
className: (key, value) => (0, _validation.validateStrList)(key, (0, _misc.toArrayIfSingle)(value)),
autoClose: _validation.validateBoolean,
icon: (key, value) => value && (0, _misc.toBoolean)(value) === false ? false : (0, _validation.validateString)(key, value, _position.isValidPosition),
iconClosed: (key, value) => (0, _validation.validateString)(key, value, isValidIconClosed),
iconOpen: (key, value) => (0, _validation.validateString)(key, value, isValidIconOpen),
hover: _validation.validateBoolean
};
const collapsibleConfigValidator = {
id: _validation.validateString,
className: (key, value) => (0, _validation.validateStrList)(key, (0, _misc.toArrayIfSingle)(value)),
horizontal: _validation.validateBoolean,
reverse: _validation.validateBoolean,
peek: _validation.validateBooleanOrString,
autoClose: _validation.validateBoolean,
icon: (key, value) => (0, _misc.toBoolean)(value) === false ? false : (0, _validation.validateString)(key, value, _position.isValidPosition),
iconClosed: (key, value) => (0, _validation.validateString)(key, value, isValidIconClosed),
iconOpen: (key, value) => (0, _validation.validateString)(key, value, isValidIconOpen)
};
const popupConfigValidator = {
id: _validation.validateString,
className: (key, value) => (0, _validation.validateStrList)(key, (0, _misc.toArrayIfSingle)(value)),
closeButton: _validation.validateBoolean,
position: (key, value) => (0, _validation.validateString)(key, value, v => v === S_AUTO || (0, _position.isValidPosition)(v) || (0, _position.isValidTwoFoldPosition)(v)),
autoClose: _validation.validateBoolean
};
const modalConfigValidator = {
id: _validation.validateString,
className: (key, value) => (0, _validation.validateStrList)(key, (0, _misc.toArrayIfSingle)(value)),
closeButton: _validation.validateBoolean,
autoClose: _validation.validateBoolean
};
const offcanvasConfigValidator = {
id: _validation.validateString,
className: (key, value) => (0, _validation.validateStrList)(key, (0, _misc.toArrayIfSingle)(value)),
closeButton: _validation.validateBoolean,
position: (key, value) => (0, _validation.validateString)(key, value, _position.isValidPosition),
autoClose: _validation.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 = (0, _cssAlter.getData)(content, prefixedNames._contentId);
let triggers = [];
if (contentId) {
triggers = [...MH.docQuerySelectorAll(getTriggerSelector(`[data-${prefixedNames._contentId}="${contentId}"]`))];
} else if (container) {
triggers = [...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 !== null && inputTriggers !== void 0 ? inputTriggers : findTriggers(content, prefixedNames);
const addTrigger = (trigger, triggerConfig) => {
if (wrapTriggers) {
const wrapper = (0, _domAlter.createWrapperFor)(trigger);
(0, _domAlter.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, (0, _widget.getWidgetConfig)((0, _cssAlter.getData)(trigger, prefixedNames._triggerForSelect), triggerConfigValidator));
}
} else if (MH.isInstanceOf(inputTriggers, Map)) {
for (const [trigger, triggerConfig] of inputTriggers.entries()) {
addTrigger(trigger, (0, _widget.getWidgetConfig)(triggerConfig, triggerConfigValidator));
}
}
return triggerMap;
};
const init = (widget, content, config) => {
var _config$wrapTriggers;
const prefixedNames = getPrefixedNames(config.name);
const container = findContainer(content, prefixedNames._containerForSelect);
const wrapTriggers = (_config$wrapTriggers = config.wrapTriggers) !== null && _config$wrapTriggers !== void 0 ? _config$wrapTriggers : false;
const triggers = getTriggersFrom(content, config.triggers, wrapTriggers, prefixedNames);
// Create two wrappers
const innerWrapper = MH.createElement("div");
(0, _cssAlter.addClasses)(innerWrapper, prefixedNames._innerWrapper);
const outerWrapper = (0, _domAlter.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