UNPKG

lisn.js

Version:

Simply handle user gestures and actions. Includes widgets.

794 lines (725 loc) 23.6 kB
/** * @module Utils * * @categoryDescription DOM: Querying * These functions query the style, attributes or other aspects of elements, but * could lead to forced layout if not scheduled using {@link waitForMeasureTime}. * * @categoryDescription DOM: Querying (optimized) * These functions query the style, attributes or other aspects of elements in * an optimized way. Functions that could cause a forced layout use * {@link waitForMeasureTime} and so are asynchronous. Functions that can * perform the check without forcing a re-layout are synchronous. * * @categoryDescription Style: Altering * These functions transition an element from one CSS class to another, but * could lead to forced layout if not scheduled using {@link waitForMutateTime}. * If a delay is supplied, then the transition is "scheduled" and if the * opposite transition is executed before the scheduled one, the original one * is cancelled. See {@link transitionElement} for an example. * * @categoryDescription Style: Altering (optimized) * These functions transition an element from one CSS class to another in an * optimized way using {@link waitForMutateTime} and so are asynchronous. * If a delay is supplied, then the transition is "scheduled" and if the * opposite transition is executed before the scheduled one, the original one * is cancelled. See {@link transitionElement} for an example. */ import * as MC from "../globals/minification-constants.js"; import * as MH from "../globals/minification-helpers.js"; import { waitForMeasureTime, waitForMutateTime, asyncMeasurerFor, asyncMutatorFor, waitForSubsequentMutateTime } from "./dom-optimize.js"; import { isDOMElement } from "./dom-query.js"; import { isValidNum, roundNumTo } from "./math.js"; import { waitForDelay } from "./tasks.js"; import { camelToKebabCase, splitOn } from "./text.js"; /** * Removes the given `fromCls` class and adds the given `toCls` class to the * element. * * Unlike {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList/replace | DOMTokenList:replace}, * this will always add `toCls` even if `fromCls` isn't in the element's class list. * * @returns True if there was a change made (class removed or added), false * otherwise. * * @category Style: Altering */ export const transitionElementNow = (element, fromCls, toCls) => { cancelCSSTransitions(element, fromCls, toCls); // Avoid triggering MutationObserver unnecessarily. let didChange = false; if (hasClass(element, fromCls)) { didChange = true; removeClassesNow(element, fromCls); } if (!hasClass(element, toCls)) { addClassesNow(element, toCls); didChange = true; } return didChange; }; /** * Like {@link transitionElementNow} except it will {@link waitForMutateTime}, * and optionally a delay, and it finally awaits for the effective style's * transition-duration. * * If a delay is supplied, then the transition is "scheduled" and if the * opposite transition is executed before the scheduled one, this one is * cancelled. * * @example * * - {@link showElement} with delay of 100 schedules `lisn-hide` -> `lisn-show` * in 100ms * - then if {@link hideElementNow} is called, or a scheduled * {@link hideElement} completes before that timer runs out, this call to * {@link showElement} aborts * * ```javascript * hideElement(someElement, 10); * // this will be aborted in 10ms when the scheduled hideElement above * // completes * showElement(someElement, 100); * ``` * * ```javascript * // this will be aborted in 10ms when the hideElement that will be scheduled * // below completes * showElement(someElement, 100); * hideElement(someElement, 10); * ``` * * ```javascript * // this will be aborted immediately by hideElementNow that runs straight * // afterwards * showElement(someElement, 100); * hideElementNow(someElement); * ``` * * ```javascript * hideElementNow(someElement); * // this will NOT be aborted because hideElementNow has completed already * showElement(someElement, 100); * ``` * * @category Style: Altering (optimized) */ export const transitionElement = async (element, fromCls, toCls, delay = 0) => { const thisTransition = scheduleCSSTransition(element, toCls); if (delay) { await waitForDelay(delay); } await waitForMutateTime(); if (thisTransition._isCancelled()) { // it has been overridden by a later transition return false; } const didChange = transitionElementNow(element, fromCls, toCls); thisTransition._finish(); if (!didChange) { return false; } // Await for the transition duration so that caller awaiting on us knows when // it's complete. const transitionDuration = await getMaxTransitionDuration(element); if (transitionDuration) { await waitForDelay(transitionDuration); } return true; }; /** * Transitions an element from class `lisn-undisplay` (which applies `display: * none`) to `lisn-display` (no style associated with this). * * The difference between this and simply removing the `lisn-undisplay` class * is that previously scheduled transitions to `lisn-undisplay` will be * cancelled. * * @see {@link transitionElementNow} * * @category Style: Altering */ export const displayElementNow = element => transitionElementNow(element, MC.PREFIX_UNDISPLAY, MC.PREFIX_DISPLAY); /** * Like {@link displayElementNow} except it will {@link waitForMutateTime}, and * optionally a delay. * * @see {@link transitionElement} * * @category Style: Altering (optimized) */ export const displayElement = (element, delay = 0) => transitionElement(element, MC.PREFIX_UNDISPLAY, MC.PREFIX_DISPLAY, delay); /** * The opposite of {@link displayElementNow}. * * @see {@link transitionElementNow} * * @category Style: Altering */ export const undisplayElementNow = element => transitionElementNow(element, MC.PREFIX_DISPLAY, MC.PREFIX_UNDISPLAY); /** * Like {@link undisplayElementNow} except it will {@link waitForMutateTime}, * and optionally a delay. * * @see {@link transitionElement} * * @category Style: Altering (optimized) */ export const undisplayElement = (element, delay = 0) => transitionElement(element, MC.PREFIX_DISPLAY, MC.PREFIX_UNDISPLAY, delay); /** * Transitions an element from class `lisn-hide` (which makes the element * hidden) to `lisn-show` (which shows it). These classes have CSS * transitions applied so the element is faded into and out of view. * * @see {@link transitionElementNow}. * * @category Style: Altering */ export const showElementNow = element => transitionElementNow(element, MC.PREFIX_HIDE, MC.PREFIX_SHOW); /** * Like {@link showElementNow} except it will {@link waitForMutateTime}, and * optionally a delay. * * @see {@link transitionElement} * * @category Style: Altering (optimized) */ export const showElement = (element, delay = 0) => transitionElement(element, MC.PREFIX_HIDE, MC.PREFIX_SHOW, delay); /** * The opposite of {@link showElementNow}. * * @see {@link transitionElementNow} * * @category Style: Altering */ export const hideElementNow = element => transitionElementNow(element, MC.PREFIX_SHOW, MC.PREFIX_HIDE); /** * Like {@link hideElementNow} except it will {@link waitForMutateTime}, and * optionally a delay. * * @see {@link transitionElement} * * @category Style: Altering (optimized) */ export const hideElement = (element, delay = 0) => transitionElement(element, MC.PREFIX_SHOW, MC.PREFIX_HIDE, delay); /** * If {@link isElementUndisplayed}, it will {@link displayElementNow}, * otherwise it will {@link undisplayElementNow}. * * @see {@link transitionElementNow} * * @category Style: Altering */ export const toggleDisplayElementNow = element => isElementUndisplayed(element) ? displayElementNow(element) : undisplayElementNow(element); /** * Like {@link toggleDisplayElementNow} except it will {@link waitForMutateTime}, * and optionally a delay. * * @see {@link transitionElement} * * @category Style: Altering (optimized) */ export const toggleDisplayElement = (element, delay = 0) => isElementUndisplayed(element) ? displayElement(element, delay) : undisplayElement(element, delay); /** * If {@link isElementHidden}, it will {@link showElementNow}, otherwise * {@link hideElementNow}. * * @see {@link transitionElementNow} * * @category Style: Altering */ export const toggleShowElementNow = element => isElementHidden(element) ? showElementNow(element) : hideElementNow(element); /** * Like {@link toggleShowElementNow} except it will {@link waitForMutateTime}, and * optionally a delay. * * @see {@link transitionElement} * * @category Style: Altering (optimized) */ export const toggleShowElement = (element, delay = 0) => isElementHidden(element) ? showElement(element, delay) : hideElement(element, delay); /** * Returns true if the element's class list contains `lisn-hide`. * * @category DOM: Querying (optimized) */ export const isElementHidden = element => hasClass(element, MC.PREFIX_HIDE); /** * Returns true if the element's class list contains `lisn-undisplay`. * * @category DOM: Querying (optimized) */ export const isElementUndisplayed = element => hasClass(element, MC.PREFIX_UNDISPLAY); /** * Returns true if the element's class list contains the given class. * * @category DOM: Querying (optimized) */ export const hasClass = (element, className) => MH.classList(element).contains(className); /** * Returns true if the element's class list contains all of the given classes. * * @since v1.2.0 * * @category DOM: Querying (optimized) */ export const hasAllClasses = (element, ...classNames) => MH.lengthOf(classNames) > 0 && !MH.some(classNames, className => !hasClass(element, className)); /** * Returns true if the element's class list contains any of the given classes. * * @since v1.2.0 * * @category DOM: Querying (optimized) */ export const hasAnyClass = (element, ...classNames) => MH.some(classNames, className => hasClass(element, className)); /** * Adds the given classes to the element. * * @category Style: Altering */ export const addClassesNow = (element, ...classNames) => MH.classList(element).add(...classNames); /** * Like {@link addClassesNow} except it will {@link waitForMutateTime}. * * @category Style: Altering (optimized) */ export const addClasses = asyncMutatorFor(addClassesNow); /** * Removes the given classes to the element. * * @category Style: Altering */ export const removeClassesNow = (element, ...classNames) => MH.classList(element).remove(...classNames); /** * Like {@link removeClassesNow} except it will {@link waitForMutateTime}. * * @category Style: Altering (optimized) */ export const removeClasses = asyncMutatorFor(removeClassesNow); /** * Toggles the given class on the element. * * @param force See {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList/toggle | DOMTokenList:toggle} * * @category Style: Altering */ export const toggleClassNow = (element, className, force) => MH.classList(element).toggle(className, force); /** * Like {@link toggleClassNow} except it will {@link waitForMutateTime}. * * @category Style: Altering (optimized) */ export const toggleClass = asyncMutatorFor(toggleClassNow); /** * Toggles the given classes on the element. This function does not accept the * `force` parameter. * * @since v1.2.0 * * @category Style: Altering */ export const toggleClassesNow = (element, ...classNames) => { for (const cls of classNames) { toggleClassNow(element, cls); } }; /** * Like {@link toggleClassesNow} except it will {@link waitForMutateTime}. * * @since v1.2.0 * * @category Style: Altering (optimized) */ export const toggleClasses = asyncMutatorFor(toggleClassesNow); /** * Replaces the given class on the element with a new one. * * @since v1.2.0 * * @category Style: Altering */ export const replaceClassNow = (element, oldClassName, newClassName) => MH.classList(element).replace(oldClassName, newClassName); /** * Like {@link replaceClassNow} except it will {@link waitForMutateTime}. * * @since v1.2.0 * * @category Style: Altering (optimized) */ export const replaceClass = asyncMutatorFor(replaceClassNow); // For *Data: to avoid unnecessary type checking that ensures element is // HTMLElement or SVGElement, use getAttribute instead of dataset. /** * Returns the value of the given data attribute. The name of the attribute * must _not_ start with `data`. It can be in either camelCase or kebab-case, * it is converted as needed. * * @category DOM: Querying (optimized) */ export const getData = (element, name) => MH.getAttr(element, MH.prefixData(name)); /** * Returns the value of the given data attribute as a boolean. Its value is * expected to be either blank or "true" (which result in `true`), or "false" * (which results in `false`). * * The name of the attribute must _not_ start with `data`. It can be in either * camelCase or kebab-case, it is converted as needed. * * @since v1.2.0 * * @category DOM: Querying (optimized) */ export const getBooleanData = (element, name) => { const value = getData(element, name); return value !== null && value !== "false"; }; /** * @ignore * @deprecated */ export const getBoolData = getBooleanData; /** * Sets the given data attribute. * * The name of the attribute must _not_ start with `data`. It can be in either * camelCase or kebab-case, it is converted as needed. * * @category Style: Altering */ export const setDataNow = (element, name, value) => MH.setAttr(element, MH.prefixData(name), value); /** * Like {@link setDataNow} except it will {@link waitForMutateTime}. * * @category Style: Altering (optimized) */ export const setData = asyncMutatorFor(setDataNow); /** * Sets the given data attribute with value "true" (default) or "false". * * The name of the attribute must _not_ start with `data`. It can be in either * camelCase or kebab-case, it is converted as needed. * * @since v1.2.0 * * @category Style: Altering */ export const setBooleanDataNow = (element, name, value = true) => MH.setAttr(element, MH.prefixData(name), value + ""); /** * @ignore * @deprecated */ export const setBoolDataNow = setBooleanDataNow; /** * Like {@link setBooleanDataNow} except it will {@link waitForMutateTime}. * * @since v1.2.0 * * @category Style: Altering (optimized) */ export const setBooleanData = asyncMutatorFor(setBooleanDataNow); /** * @ignore * @deprecated */ export const setBoolData = setBooleanData; /** * Sets the given data attribute with value "false". * * The name of the attribute must _not_ start with `data`. It can be in either * camelCase or kebab-case, it is converted as needed. * * @since v1.2.0 * * @category Style: Altering */ export const unsetBooleanDataNow = (element, name) => MH.unsetAttr(element, MH.prefixData(name)); /** * @ignore * @deprecated */ export const unsetBoolDataNow = unsetBooleanDataNow; /** * Like {@link unsetBooleanDataNow} except it will {@link waitForMutateTime}. * * @since v1.2.0 * * @category Style: Altering (optimized) */ export const unsetBooleanData = asyncMutatorFor(unsetBooleanDataNow); /** * @ignore * @deprecated */ export const unsetBoolData = unsetBooleanData; /** * Deletes the given data attribute. * * The name of the attribute must _not_ start with `data`. It can be in either * camelCase or kebab-case, it is converted as needed. * * @category Style: Altering */ export const delDataNow = (element, name) => MH.delAttr(element, MH.prefixData(name)); /** * Like {@link delDataNow} except it will {@link waitForMutateTime}. * * @category Style: Altering (optimized) */ export const delData = asyncMutatorFor(delDataNow); /** * Returns the value of the given property from the computed style of the * element. * * @category DOM: Querying */ export const getComputedStylePropNow = (element, prop) => getComputedStyle(element).getPropertyValue(prop); /** * Like {@link getComputedStylePropNow} except it will {@link waitForMeasureTime}. * * @category DOM: Querying (optimized) */ export const getComputedStyleProp = asyncMeasurerFor(getComputedStylePropNow); /** * Returns the value of the given property from the inline style of the * element. * * @category DOM: Querying */ export const getStylePropNow = (element, prop) => { var _style; return (_style = element.style) === null || _style === void 0 ? void 0 : _style.getPropertyValue(prop); }; /** * Like {@link getStylePropNow} except it will {@link waitForMeasureTime}. * * @category DOM: Querying (optimized) */ export const getStyleProp = asyncMeasurerFor(getStylePropNow); /** * Sets the given property on the inline style of the element. * * @category DOM: Altering */ export const setStylePropNow = (element, prop, value) => { var _style2; return (_style2 = element.style) === null || _style2 === void 0 ? void 0 : _style2.setProperty(prop, value); }; /** * Like {@link setStylePropNow} except it will {@link waitForMutateTime}. * * @category DOM: Altering (optimized) */ export const setStyleProp = asyncMutatorFor(setStylePropNow); /** * Deletes the given property on the inline style of the element. * * @category DOM: Altering */ export const delStylePropNow = (element, prop) => { var _style3; return (_style3 = element.style) === null || _style3 === void 0 ? void 0 : _style3.removeProperty(prop); }; /** * Like {@link delStylePropNow} except it will {@link waitForMutateTime}. * * @category DOM: Altering (optimized) */ export const delStyleProp = asyncMutatorFor(delStylePropNow); /** * Returns the flex direction of the given element **if it has a flex layout**. * * @returns `null` if the element does not have a flex layout. * * @category DOM: Querying (optimized) * * @since v1.2.0 */ export const getFlexDirection = async element => { const displayStyle = await getComputedStyleProp(element, "display"); if (!displayStyle.includes("flex")) { return null; } return await getComputedStyleProp(element, "flex-direction"); }; /** * Returns the flex direction of the given element's parent **if it has a flex * layout**. * * @returns `null` if the element's parent does not have a flex layout. * * @category DOM: Querying (optimized) * * @since v1.2.0 */ export const getParentFlexDirection = async element => { const parent = MH.parentOf(element); return parent ? getFlexDirection(parent) : null; }; /** * Returns true if the given element has a flex layout. If direction is given, * then it also needs to match. * * @category DOM: Querying (optimized) * * @since v1.2.0 */ export const isFlex = async (element, direction) => { const flexDirection = await getFlexDirection(element); if (direction) { return direction === flexDirection; } return flexDirection !== null; }; /** * Returns true if the given element's parent has a flex layout. If direction is * given, then it also needs to match. * * @category DOM: Querying (optimized) * * @since v1.2.0 */ export const isFlexChild = async (element, direction) => { const parent = MH.parentOf(element); return parent ? isFlex(parent, direction) : false; }; /** * In milliseconds. * * @ignore * @internal */ export const getMaxTransitionDuration = async element => { const propVal = await getComputedStyleProp(element, "transition-duration"); return MH.max(...splitOn(propVal, ",", true).map(strValue => { let duration = MH.parseFloat(strValue) || 0; if (strValue === duration + "s") { duration *= 1000; } return duration; })); }; /** * @ignore * @internal */ export const disableInitialTransition = async (element, delay = 0) => { await addClasses(element, MC.PREFIX_TRANSITION_DISABLE); if (delay) { await waitForDelay(delay); } await waitForSubsequentMutateTime(); removeClassesNow(element, MC.PREFIX_TRANSITION_DISABLE); }; /** * @ignore * @internal */ export const setHasModal = () => setBooleanData(MH.getBody(), PREFIX_HAS_MODAL); /** * @ignore * @internal */ export const delHasModal = () => delData(MH.getBody(), PREFIX_HAS_MODAL); /** * @ignore * @internal */ export const copyStyle = async (fromElement, toElement, includeComputedProps) => { if (!isDOMElement(fromElement) || !isDOMElement(toElement)) { return; } await waitForMeasureTime(); const props = {}; if (includeComputedProps) { for (const prop of includeComputedProps) { props[prop] = getComputedStylePropNow(fromElement, prop); } } const style = fromElement.style; // only inline styles for (const prop in style) { const value = style.getPropertyValue(prop); if (value) { props[prop] = value; } } for (const prop in props) { setStyleProp(toElement, prop, props[prop]); } addClasses(toElement, ...MH.classList(fromElement)); }; /** * If the props keys are in camelCase they are converted to kebab-case * * If a value is null or undefined, the property is deleted. * * @ignore * @internal */ export const setNumericStyleJsVarsNow = (element, props, options = {}) => { var _options$_prefix; if (!isDOMElement(element)) { return; } const varPrefix = MH.prefixCssJsVar((_options$_prefix = options === null || options === void 0 ? void 0 : options._prefix) !== null && _options$_prefix !== void 0 ? _options$_prefix : ""); for (const prop in props) { const cssPropSuffix = camelToKebabCase(prop); const varName = `${varPrefix}${cssPropSuffix}`; let value; if (!isValidNum(props[prop])) { value = null; } else { var _options$_numDecimal; value = props[prop]; const thisNumDecimal = (_options$_numDecimal = options === null || options === void 0 ? void 0 : options._numDecimal) !== null && _options$_numDecimal !== void 0 ? _options$_numDecimal : value > 0 && value < 1 ? 2 : 0; value = roundNumTo(value, thisNumDecimal); } if (value === null) { delStylePropNow(element, varName); } else { var _options$_units; setStylePropNow(element, varName, value + ((_options$_units = options === null || options === void 0 ? void 0 : options._units) !== null && _options$_units !== void 0 ? _options$_units : "")); } } }; /** * @ignore * @internal */ export const setNumericStyleJsVars = asyncMutatorFor(setNumericStyleJsVarsNow); // ---------------------------------------- const PREFIX_HAS_MODAL = MH.prefixName("has-modal"); const scheduledCSSTransitions = MH.newWeakMap(); const cancelCSSTransitions = (element, ...toClasses) => { const scheduledTransitions = scheduledCSSTransitions.get(element); if (!scheduledTransitions) { return; } for (const toCls of toClasses) { const scheduledTransition = scheduledTransitions[toCls]; if (scheduledTransition) { scheduledTransition._cancel(); } } }; const scheduleCSSTransition = (element, toCls) => { let scheduledTransitions = scheduledCSSTransitions.get(element); if (!scheduledTransitions) { scheduledTransitions = {}; scheduledCSSTransitions.set(element, scheduledTransitions); } let isCancelled = false; scheduledTransitions[toCls] = { _cancel: () => { isCancelled = true; MH.deleteObjKey(scheduledTransitions, toCls); }, _finish: () => { MH.deleteObjKey(scheduledTransitions, toCls); }, _isCancelled: () => { return isCancelled; } }; return scheduledTransitions[toCls]; }; //# sourceMappingURL=css-alter.js.map