UNPKG

lisn.js

Version:

Simply handle user gestures and actions. Includes widgets.

1,014 lines (982 loc) 42.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SameHeight = 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 _domQuery = require("../utils/dom-query.cjs"); var _log = require("../utils/log.cjs"); var _math = require("../utils/math.cjs"); var _text = require("../utils/text.cjs"); var _validation = require("../utils/validation.cjs"); var _sizeWatcher = require("../watchers/size-watcher.cjs"); var _widget = require("./widget.cjs"); var _debug = _interopRequireDefault(require("../debug/debug.cjs")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } 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 */ // This widget finds optimal widths of flexbox children so that their heights // are equal or as close as possible to each other. It takes into account // whether they contain text (and possibly other elements, but not images) or // images. // // NOTE: // - We assume that a given flexbox child is either a "text container" and // contains only text and other non-image elements (such as buttons), or is // an "image container" and contains only images. // - We also assume that all the text inside a text container is the same // font size as the font size of the text container. // // ~~~~~~ BACKGROUND: analysis for one text container and one image container ~~~~~~ // // A text box has a fixed area, its height decreasing as width increases. // Whereas an image has a fixed aspect ratio, its height increasing as width // increases. // // We want to find an optimal configuration at which the text container (which // can include other elements apart from text) and image heights are equal, or // if not possible, at which they are as close as possible to each other while // satisfying as best as possible these "guidelines" (constraints that are not // enforced), based on visual appeal: // - minGap, minimum gap between each item // - maxWidthR, maximum ratio between the width of the widest child and the // narrowest child // - maxFreeR, maximum free space in the container as a percentage of its // total width // // Then we set flex-basis as the optimal width (making sure this is disabled // when the flex direction is column). This allows for fluid width if the user // to configure shrink or wrap on the flexbox using CSS. // // ~~~~~~ FORMULAE: text and image width as a function of their height ~~~~~~ // // For a given height, h, the widths of the text and image are: // // txtArea // txtW(h) = ————————————— // h - txtExtraH // // imgW(h) = imgAspectR * h // // where txtExtraH comes from buttons and other non-text elements inside the // text container, whose height is treated as fixed (not changing with width). // // ~~~~~~ PLOT: total width as a function of height ~~~~~~ // // The sum of the widths of image and text varies with their height, h, as: // // w(h) = txtW(h) + imgW(h) // // txtArea // = ————————————— + imgAspectR * h // h - txtExtraH // // // w(h) // ^ // | | . // | . . // | . . // flexW + . . // | . . // | . . // | - // | // |———|———|—————|———————————> h // h1 h0 h2 // // // ~~~~~~ FORMULAE: height at which total width is minimum ~~~~~~ // // The minimum of the function w(h) is at h = h0 // // ⌈ txtArea ⌉ // h0 = sqrt| —————————— | + txtExtraH // ⌊ imgAspectR ⌋ // // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // The widths of image and text container at height = h0 are: // // txtW(h0) = sqrt( txtArea * imgAspectR ) // // imgW(h0) = sqrt( txtArea * imgAspectR ) + imgAspectR * txtExtraH // = txtW(h0) + imgAspectR * txtExtraH // // - NOTE: at if txtExtraH is 0 (i.e. the container has only text), then // their widths are equal at h0; otherwise the image is wider // // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // There are zero, one or two values of h at which w(h) equals the flexbox // width, flexW. Labelled h1 and h2 above. // // ~~~~~~ FORMULAE: height at which total width is equal to flexbox width ~~~~~~ // // The heights at which the sum of the widths, w(h) equals exactly flexW are: // // -b ± sqrt( b^2 - 4ac ) // h2/1 = —————————————————————— // 2a // // where: // a = imgAspectR // b = - ( (imgAspectR * txtExtraH) + flexW ) // c = txtArea + (txtExtraH * flexW) // // If h1 and h2 are real, then h1 <= h0 <= h2, as shown in plot above. // // ~~~~~~ SCENARIOS: free space or overflow in the flexbox ~~~~~~ // // Whether there is a solution to the above equation, i.e. whether h1 and h2 // are real, depends on which scenario we have: // // 1. If flexW = w(h0), then h1 = h2 = h0 // 2. If flexW < w(h0), then there is no exact solution, i.e. it's impossible // to fit the text and image inside the flexbox and have them equal heights; // there is overflow even at h0 // 3. If flexW > w(h0) (as in the graph above), then at h0 there is free space // in the flexbox and we can choose any height between h1 and h2 // // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // The widths h0, h1 and h2 represent the following visual configuration: // - h0: intermediate height, maximum free space in the container; // - h1: minimum height (i.e. wide text and small image), no free space in // the container; // - h2: maximum height (i.e. narrow text and large image), no free space in // the container; // // ~~~~~~ THEREFORE: approach ~~~~~~ // // 1. If flexW = w(h0), i.e. h1 = h2 = h0: // => we choose h0 as the height // 2. If flexW < w(h0), i.e. it's impossible to fit the text and image inside // the flexbox and have them equal heights: // => we still choose h0 as the height as that gives the least amount of // overflow; user-defined CSS can control whether the items will be // shrunk, the flexbox will wrap or overflow // 3. If flexW > w(h0), i.e. at h0 there is free space in the flexbox: // => choose a height between h1 and h2 that best fits with the guidelines // maxWidthR and maxFreeR // // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // In scenario 3 we can look at the guidelines, maxWidthR and maxFreeR. // // ~~~~~~ GUIDELINE: maxWidthR ~~~~~~ // // ~~~~~~ FORMULAE: height at which text and image width are equal ~~~~~~ // // The width of the text and image container are equal at height hR0: // // txtExtraH + sqrt( txtExtraH^2 + 4 * (h0 - txtExtraH)^2 ) // hR0 = —————————————————————————————————————————————————————————— // 2 // // ~~~~~~ FORMULAE: height at which text to image width is maxWidthR ~~~~~~ // // For heights < hR0, i.e. text becomes wider than the image, at some point the // ratio of text width to image width becomes maxWidthR. This happens at hR1. // // ⌈ 4 * (h0 - txtExtraH)^2 ⌉ // txtExtraH + sqrt| txtExtraH^2 + —————————————————————— | // ⌊ maxWidthR ⌋ // hR1 = —————————————————————————————————————————————————————————— // 2 // // ~~~~~~ FORMULAE: height at which image to text width is maxWidthR ~~~~~~ // // For heights > hR0, i.e. text becomes narrower than the image, at some point // the ratio of image width to text width becomes maxWidthR. This happens at hR2. // // txtExtraH + sqrt( txtExtraH^2 + 4 * maxWidthR * (h0 - txtExtraH)^2 ) // hR2 = —————————————————————————————————————————————————————————————————————— // 2 // // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // NOTE: // - hR1 <= hR0 <= hR2 && hR0 <= h0 // - hR0, hR1 and hR2 are the first (larger) roots of the quadratic equation // with coefficients: // a = imgAspectR * R // b = - imgAspectR * txtExtraH * R // c = - textArea // where R = 1 gives hR0, R = maxWidthR gives hR1 and R = 1 / maxWidthR gives hR2 // - The smaller roots of the equation should be negative, so we ignore them // // ~~~~~~ GUIDELINE: maxFreeR ~~~~~~ // // ~~~~~~ FORMULAE: free space in flexbox relative to its width ~~~~~~ // // The percentage of free space in the container is: // // flexW - w(h) // freeR = ———————————— // flexW // // // txtArea // flexW - ————————————— - imgAspectR * h // h - txtExtraH // = ————————————————————————————————————————— // flexW // // ~~~~~~ FORMULAE: height at which relative free space is maxFreeR ~~~~~~ // // This would be equal to maxFreeR at hF1 and hF2: // // -b ± sqrt( b^2 - 4ac ) // hF2/1 = —————————————————————— // 2a // // where: // a = imgAspectR // b = - ( (imgAspectR * txtExtraH) + ( flexW * (1 - maxFreeR) ) ) // c = txtArea + ( txtExtraH * flexW * (1 - maxFreeR) ) // // If hF1 and hF2 are real, then h1 < hF1 <= h0 <= hF2 < h2. // // ~~~~~~ THEREFORE: choosing a height in scenario 3 ~~~~~~ // // So in scenario 3 we can choose any height h between // // max(h1, hR1, hF1) and min(h2, hR2, hF2) // // Note, it's possible that max(h1, hR1, hF1) is greater than min(h2, hR2, hF2), // e.g. if hF1 > hR2 or hR1 > hF2. // // This will make the text and image equal height, fitting in the flexbox, and // if possible, satisfying both maxFreeR and maxWidthR. // // Here we choose the smallest height possible, which would result in the // larger ratio between text width and image width, but it will satisfy the // constraints maxFreeR and maxWidthR, so that is ok. // // ~~~~~~ GENERALISING: for more than one text and/or image container ~~~~~~ // // We can generalise the above in order to find an approximate solution for the // case of multiple text or image containers (an exact solution would require // solving a polynomial of degree equal to the number of elements). // // If we imaging putting all text in one container and all images in another // container we are back at the above exact solutions for a single text and // image container. // // We can solve for the following parameters: // - txtArea: total text area // = sum_i(txtArea_i) // // - txtExtraH: weighted average extra height // = sum_i(txtExtraH_i * txtArea_i) / txtArea // // - imgAspectR: total image aspect ratio (for horizontally laid out image // containers) // = sum_i(imgAspectR_i) // // ~~~~~~ CASE 1: only images containers ~~~~~~ // If we have only image containers, we solve for the optimal height as follows: // // flexW = imgAspectR * h // // flexW // => hIdeal = —————————— // imgAspectR // // ~~~~~~ CASE 2: only text containers ~~~~~~ // If we have only text containers, we solve for the optimal height as follows: // // txtArea // flexW = —————————————————— // hIdeal - txtExtraH // // txtArea // => hIdeal = ——————— + txtExtraH // flexW // // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Once we've found the optimal height h, we calculate the individual widths of // the flexbox children as: // // txtArea_i // txtW_i(h) = ————————————— // h - txtExtraH_i // // imgW_i(h) = imgAspectR_i * h // // ~~~~~~ IMPLEMENTATION ~~~~~~ // // We go through the flexbox children and determine whether a child is a "text // container" or an "image container". // // For image containers, we measure the width and height and calculate the // aspect ratio using these. // // For text containers, we measure their width and height. We calculate the // text area by measuring the size of all children of the text container that // are deemed to contain only text (or if the entire text container is deemed // to contain only text, then we take its size). Then we sum the areas of // all such text-only boxes. // // To determine the extra height in the text container, we take the total // height of all text-only boxes inside it, and we subtract that from its // measured height. // // NOTE: // - This does not work if the flexbox children are set to align stretch, // because in such cases there would be free vertical space in the container // that shouldn't be counted. // - If the flexbox children or any of their descendants have paddings and // margins, then this calculation would only work if the paddings/margins // inside text containers are absolute and only on top and bottom, and // paddings/margins inside image containers are in percentages and only on // descendants of the image container. Otherwise the image aspect ratio and the // extra text height would not be constant, and there may be extra width in // the text container. It is very tricky to take all of this into account. So // we ignore such cases and assume constant image aspect ratio and constant // text area and text container extra height. // // We use resize observers to get the size of relevant elements and // re-calculate as needed. /** * Configures the given element as a {@link SameHeight} widget. * * The SameHeight widget sets up the given element as a flexbox and sets the * flex basis of its components so that their heights are as close as possible * to each other. It tracks their size (see {@link SizeWatcher}) and * continually updates the basis as needed. * * When calculating the best flex basis that would result in equal heights, * SameHeight determines whether a flex child is mostly text or mostly images * since the height of these scales in opposite manner with their width. * Therefore, the components of the widget should contain either mostly text or * mostly images. * * The widget should have more than one item and the items must be immediate * children of the container element. * * SameHeight tries to automatically determine if an item is mostly text or * mostly images based on the total display text content, but you can override * this in two ways: * 1. By passing a map of elements as {@link SameHeightConfig.items | items} * instead of an array, and setting the value for each to either `"text"` or * `"image"` * 2. By setting the `data-lisn-same-height-item` attribute to `"text"` or * `"image"` on the children. **NOTE** however that when auto-discovering the * items (i.e. when you have not explicitly passed a list/map of items), if * you set the `data-lisn-same-height-item` attribute on _any_ child you must * also add this attribute to all other children that are to be used by the * widget. Other children (that don't have this attribute) will be ignored * and assumed to be either zero-size or position absolute/fixed. * * **IMPORTANT:** You should not instantiate more than one {@link SameHeight} * widget on a given element. Use {@link SameHeight.get} to get an existing * instance if any. If there is already a widget instance, it will be destroyed! * * **IMPORTANT:** The element you pass will be set to `display: flex` and its * children will get `box-sizing: border-box` and continually updated * `flex-basis` style. You can add additional CSS to the element or its * children if you wish. For example you may wish to set `flex-wrap: wrap` on * the element and a `min-width` on the children. * * ----- * * To use with auto-widgets (HTML API) (see {@link settings.autoWidgets}), the * following CSS classes or data attributes are recognized: * - `lisn-same-height` class or `data-lisn-same-height` attribute set on the * container element that constitutes the widget * * When using auto-widgets, the elements that will be used as items are * discovered in the following way: * 1. The immediate children of the top-level element that constitutes the * widget that have the `lisn-same-height-item` class or * `data-lisn-same-height-item` attribute are taken. * 2. If none of the root's children have this class or attribute, then all of * the immediate children of the widget element except any `script` or * `style` elements are taken as the items. * * See below examples for what values you can use set for the data attribute * in order to modify the configuration of the automatically created widget. * * @example * This defines a simple SameHeight widget with one text and one image child. * * ```html * <div class="lisn-same-height"> * <div> * <p> * Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do * eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad * minim veniam, quis nostrud exercitation ullamco laboris nisi ut * aliquip ex ea commodo consequat. Duis aute irure dolor in * reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla * pariatur. Excepteur sint occaecat cupidatat non proident, sunt in * culpa qui officia deserunt mollit anim id est laborum. * </p> * </div> * * <div> * <img * src="https://www.wikipedia.org/portal/wikipedia.org/assets/img/Wikipedia-logo-v2@1.5x.png" * /> * </div> * </div> * ``` * * @example * This defines a SameHeight widget with the flexbox children specified * explicitly (and one ignored), as well as having all custom settings. * * ```html * <div data-lisn-same-height="diff-tolerance=20 * | resize-threshold=10 * | debounce-window=50 * | min-gap=50 * | max-free-r=0.2 * | max-width-r=3.2"> * <div>Example ignored child</div> * * <div data-lisn-same-height-item><!-- Will be detected as text anyway --> * <p> * Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do * eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad * minim veniam, quis nostrud exercitation ullamco laboris nisi ut * aliquip ex ea commodo consequat. Duis aute irure dolor in * reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla * pariatur. Excepteur sint occaecat cupidatat non proident, sunt in * culpa qui officia deserunt mollit anim id est laborum. * </p> * </div> * * <!-- Explicitly set to image type, though it will be detected as such --> * <div data-lisn-same-height-item="image"> * <img * src="https://www.wikipedia.org/portal/wikipedia.org/assets/img/Wikipedia-logo-v2@1.5x.png" * /> * </div> * * <!-- Explicitly set to text type, because it will NOT be detected as such (text too short). --> * <div data-lisn-same-height-item="text"> * <p> * Lorem ipsum dolor sit amet, consectetur adipiscing elit. * </p> * </div> * </div> * ``` */ class SameHeight extends _widget.Widget { /** * If the element is already configured as a SameHeight widget, the widget * instance is returned. Otherwise null. */ static get(containerElement) { const instance = super.get(containerElement, DUMMY_ID); if (MH.isInstanceOf(instance, SameHeight)) { return instance; } return null; } static register() { (0, _widget.registerWidget)(WIDGET_NAME, (element, config) => { if (MH.isHTMLElement(element)) { if (!SameHeight.get(element)) { return new SameHeight(element, config); } } else { (0, _log.logError)(MH.usageError("Only HTMLElement is supported for SameHeight widget")); } return null; }, configValidator); } constructor(containerElement, config) { var _SameHeight$get; const destroyPromise = (_SameHeight$get = SameHeight.get(containerElement)) === null || _SameHeight$get === void 0 ? void 0 : _SameHeight$get.destroy(); super(containerElement, { id: DUMMY_ID }); /** * Switches the flexbox to vertical (column) mode. * * You can alternatively do this by setting the * `data-lisn-orientation="vertical"` attribute on the element at any time. * * You can do this for example as part of a trigger: * * @example * ```html * <div class="lisn-same-height" * data-lisn-on-layout="max-mobile-wide:set-attribute=data-lisn-orientation#vertical"> * <!-- ... children --> * </div> * ``` */ _defineProperty(this, "toColumn", void 0); /** * Switches the flexbox back to horizontal (row) mode, which is the default. * * You can alternatively do this by deleting the * `data-lisn-orientation` attribute on the element, or setting it to * `"horizontal"` at any time. */ _defineProperty(this, "toRow", void 0); /** * Returns the elements used as the flex children. */ _defineProperty(this, "getItems", void 0); /** * Returns a map of the elements used as the flex children with their type. */ _defineProperty(this, "getItemConfigs", void 0); const items = getItemsFrom(containerElement, config === null || config === void 0 ? void 0 : config.items); if (MH.sizeOf(items) < 2) { throw MH.usageError("SameHeight must have more than 1 item"); } for (const item of items.keys()) { if (MH.parentOf(item) !== containerElement) { throw MH.usageError("SameHeight's items must be its immediate children"); } } fetchConfig(containerElement, config).then(fullConfig => { (destroyPromise || MH.promiseResolve()).then(() => { if (this.isDestroyed()) { return; } init(this, containerElement, items, fullConfig); }); }); this.toColumn = () => (0, _cssAlter.setData)(containerElement, MC.PREFIX_ORIENTATION, MC.S_VERTICAL); this.toRow = () => (0, _cssAlter.delData)(containerElement, MC.PREFIX_ORIENTATION); this.getItems = () => [...items.keys()]; this.getItemConfigs = () => MH.newMap([...items.entries()]); } } /** * @interface */ // ------------------------------ exports.SameHeight = SameHeight; const WIDGET_NAME = "same-height"; const PREFIXED_NAME = MH.prefixName(WIDGET_NAME); const PREFIX_ROOT = `${PREFIXED_NAME}__root`; // Use different classes for styling items to the one used for auto-discovering // them, so that re-creating existing widgets can correctly find the items to // be used by the new widget synchronously before the current one is destroyed. const PREFIX_ITEM = `${PREFIXED_NAME}__item`; const PREFIX_ITEM__FOR_SELECT = `${PREFIXED_NAME}-item`; const S_TEXT = "text"; const S_IMAGE = "image"; // Only one SameHeight widget per element is allowed, but Widget requires a // non-blank ID. const DUMMY_ID = PREFIXED_NAME; // We consider elements that have text content of at least <MIN_CHARS_FOR_TEXT> // characters to be text. const MIN_CHARS_FOR_TEXT = 100; const configValidator = { diffTolerance: _validation.validateNumber, resizeThreshold: _validation.validateNumber, [MC.S_DEBOUNCE_WINDOW]: _validation.validateNumber, minGap: _validation.validateNumber, maxFreeR: _validation.validateNumber, maxWidthR: _validation.validateNumber }; const isText = element => (0, _cssAlter.getData)(element, PREFIX_ITEM__FOR_SELECT) === S_TEXT || (0, _cssAlter.getData)(element, PREFIX_ITEM__FOR_SELECT) !== S_IMAGE && MH.isHTMLElement(element) && MH.lengthOf(element.innerText) >= MIN_CHARS_FOR_TEXT; const areImagesLoaded = element => { for (const img of element.querySelectorAll("img")) { // Don't rely on img.complete since sometimes this returns false even // though the image is loaded and has a size. Just check the size. if (img.naturalWidth === 0 || img.width === 0 || img.naturalHeight === 0 || img.height === 0) { return false; } } return true; }; const fetchConfig = async (containerElement, userConfig) => { var _userConfig$minGap, _userConfig$maxFreeR, _userConfig$maxWidthR, _userConfig$diffToler, _userConfig$resizeThr, _userConfig$debounceW; const colGapStr = await (0, _cssAlter.getComputedStyleProp)(containerElement, "column-gap"); const minGap = getNumValue(MH.strReplace(colGapStr, /px$/, ""), _settings.settings.sameHeightMinGap); return { _minGap: (0, _math.toNumWithBounds)((_userConfig$minGap = userConfig === null || userConfig === void 0 ? void 0 : userConfig.minGap) !== null && _userConfig$minGap !== void 0 ? _userConfig$minGap : minGap, { min: 0 }, 10), _maxFreeR: (0, _math.toNumWithBounds)((_userConfig$maxFreeR = userConfig === null || userConfig === void 0 ? void 0 : userConfig.maxFreeR) !== null && _userConfig$maxFreeR !== void 0 ? _userConfig$maxFreeR : _settings.settings.sameHeightMaxFreeR, { min: 0, max: 0.9 }, -1), _maxWidthR: (0, _math.toNumWithBounds)((_userConfig$maxWidthR = userConfig === null || userConfig === void 0 ? void 0 : userConfig.maxWidthR) !== null && _userConfig$maxWidthR !== void 0 ? _userConfig$maxWidthR : _settings.settings.sameHeightMaxWidthR, { min: 1 }, -1), _diffTolerance: (_userConfig$diffToler = userConfig === null || userConfig === void 0 ? void 0 : userConfig.diffTolerance) !== null && _userConfig$diffToler !== void 0 ? _userConfig$diffToler : _settings.settings.sameHeightDiffTolerance, _resizeThreshold: (_userConfig$resizeThr = userConfig === null || userConfig === void 0 ? void 0 : userConfig.resizeThreshold) !== null && _userConfig$resizeThr !== void 0 ? _userConfig$resizeThr : _settings.settings.sameHeightResizeThreshold, _debounceWindow: (_userConfig$debounceW = userConfig === null || userConfig === void 0 ? void 0 : userConfig.debounceWindow) !== null && _userConfig$debounceW !== void 0 ? _userConfig$debounceW : _settings.settings.sameHeightDebounceWindow }; }; const getNumValue = (strValue, defaultValue) => { const num = strValue ? MH.parseFloat(strValue) : NaN; return MH.isNaN(num) ? defaultValue : num; }; const findItems = containerElement => { const items = [...MH.querySelectorAll(containerElement, (0, _widget.getDefaultWidgetSelector)(PREFIX_ITEM__FOR_SELECT))]; if (!MH.lengthOf(items)) { items.push(...(0, _domQuery.getVisibleContentChildren)(containerElement)); } return items; }; const getItemsFrom = (containerElement, inputItems) => { const itemMap = MH.newMap(); inputItems = inputItems || findItems(containerElement); const addItem = (item, itemType) => { itemType = itemType || (isText(item) ? S_TEXT : S_IMAGE); itemMap.set(item, itemType); }; if (MH.isArray(inputItems)) { for (const item of inputItems) { addItem(item); } } else if (MH.isInstanceOf(inputItems, Map)) { for (const [item, itemType] of inputItems.entries()) { addItem(item, itemType); } } return itemMap; }; const init = (widget, containerElement, items, config) => { const logger = _debug.default ? new _debug.default.Logger({ name: `SameHeight-${(0, _text.formatAsString)(containerElement)}` }) : null; const diffTolerance = config._diffTolerance; const debounceWindow = config._debounceWindow; const sizeWatcher = _sizeWatcher.SizeWatcher.reuse({ [MC.S_DEBOUNCE_WINDOW]: debounceWindow, resizeThreshold: config._resizeThreshold }); const allItems = MH.newMap(); let callCounter = 0; let isFirstTime = true; let lastOptimalHeight = 0; let hasScheduledReset = false; let counterTimeout = null; // ---------- const resizeHandler = (element, sizeData) => { // Since the SizeWatcher calls us once for every element, we batch the // re-calculations so they are done once in every cycle. // Allow the queue of ResizeObserverEntry in the SizeWatcher to be // emptied, and therefore to ensure we have the latest size for all // elements. if (!hasScheduledReset) { debug: logger === null || logger === void 0 || logger.debug7("Scheduling calculations", callCounter); hasScheduledReset = true; MH.setTimer(() => { hasScheduledReset = false; if (callCounter > 1) { debug: logger === null || logger === void 0 || logger.debug7("Already re-calculated once, skipping"); callCounter = 0; return; } callCounter++; if (counterTimeout) { MH.clearTimer(counterTimeout); } const measurements = calculateMeasurements(containerElement, allItems, isFirstTime, logger); const height = measurements ? getOptimalHeight(measurements, config, logger) : null; if (height && MH.abs(lastOptimalHeight - height) > diffTolerance) { // Re-set widths again. We may be called again in the next cycle if // the change in size exceeds the resizeThreshold. lastOptimalHeight = height; isFirstTime = false; setWidths(height); // no need to await // If we are _not_ called again in the next cycle (just after // debounceWindow), then reset the counter. It means the resultant // change in size did not exceed the SizeWatcher threshold. counterTimeout = MH.setTimer(() => { callCounter = 0; }, debounceWindow + 50); } else { // Done, until the next time elements are resized callCounter = 0; } }, 0); } // Save the size of the item const properties = allItems.get(element); if (!properties) { (0, _log.logError)(MH.bugError("Got SizeWatcher call for unknown element")); return; } properties._width = sizeData.border[MC.S_WIDTH] || sizeData.content[MC.S_WIDTH]; properties._height = sizeData.border[MC.S_HEIGHT] || sizeData.content[MC.S_HEIGHT]; debug: logger === null || logger === void 0 || logger.debug7("Got size", element, properties); }; // ---------- const observeAll = () => { isFirstTime = true; for (const element of allItems.keys()) { sizeWatcher.onResize(resizeHandler, { target: element }); } }; // ---------- const unobserveAll = () => { for (const element of allItems.keys()) { sizeWatcher.offResize(resizeHandler, element); } }; // ---------- const setWidths = height => { for (const [element, properties] of allItems.entries()) { if (MH.parentOf(element) === containerElement) { const width = getWidthAtH(element, properties, height); debug: logger === null || logger === void 0 || logger.debug9("Setting width property", element, properties, width); (0, _cssAlter.setNumericStyleJsVars)(element, { sameHeightW: width }, { _units: "px" }); } } }; // SETUP ------------------------------ widget.onDisable(unobserveAll); widget.onEnable(observeAll); widget.onDestroy(async () => { for (const element of allItems.keys()) { if (MH.parentOf(element) === containerElement) { // delete the property and attribute await (0, _cssAlter.setNumericStyleJsVars)(element, { sameHeightW: NaN }); await (0, _cssAlter.removeClasses)(element, PREFIX_ITEM); } } allItems.clear(); await (0, _cssAlter.removeClasses)(containerElement, PREFIX_ROOT); }); // Find all relevant items: the container, its direct children and the // top-level text only elements. const getProperties = itemType => { return { _type: itemType, _width: NaN, _height: NaN, _aspectR: NaN, _area: NaN, _extraH: NaN, _components: [] }; }; allItems.set(containerElement, getProperties("")); for (const [item, itemType] of items.entries()) { (0, _cssAlter.addClasses)(item, PREFIX_ITEM); const properties = getProperties(itemType); allItems.set(item, properties); if (itemType === S_TEXT) { properties._components = getTextComponents(item); for (const child of properties._components) { allItems.set(child, getProperties("")); } } } (0, _cssAlter.addClasses)(containerElement, PREFIX_ROOT); observeAll(); }; /** * Find the top-level text-only elements that are descendants of the given one. */ const getTextComponents = element => { const components = []; for (const child of (0, _domQuery.getVisibleContentChildren)(element)) { if (isText(child)) { components.push(child); } else { components.push(...getTextComponents(child)); } } return components; }; const calculateMeasurements = (containerElement, allItems, isFirstTime, logger) => { if ((0, _cssAlter.getData)(containerElement, MC.PREFIX_ORIENTATION) === MC.S_VERTICAL) { debug: logger === null || logger === void 0 || logger.debug8("In vertical mode"); return null; } debug: logger === null || logger === void 0 || logger.debug7("Calculating measurements"); // initial values let tArea = NaN, tExtraH = 0, imgAR = NaN, flexW = NaN, nItems = 0; for (const [element, properties] of allItems.entries()) { const width = properties._width; const height = properties._height; if (element === containerElement) { flexW = width; nItems = MH.lengthOf((0, _domQuery.getVisibleContentChildren)(element)); // } else if (properties._type === S_TEXT) { let thisTxtArea = 0, thisTxtExtraH = 0; const components = properties._components; if (MH.lengthOf(components)) { for (const component of properties._components) { const cmpProps = allItems.get(component); if (cmpProps) { thisTxtArea += cmpProps._width * cmpProps._height; } else { (0, _log.logError)(MH.bugError("Text component not observed")); } } thisTxtExtraH = height - thisTxtArea / width; } else { thisTxtArea = width * height; } properties._area = thisTxtArea; properties._extraH = thisTxtExtraH; tArea = (tArea || 0) + thisTxtArea; tExtraH += thisTxtExtraH; // } else if (properties._type === S_IMAGE) { if (isFirstTime && !areImagesLoaded(element)) { debug: logger === null || logger === void 0 || logger.debug8("Images not loaded"); return null; } const thisAspectR = width / height; imgAR = (imgAR || 0) + thisAspectR; properties._aspectR = thisAspectR; // } else { // skip grandchildren (text components), here continue; } debug: logger === null || logger === void 0 || logger.debug8("Examined", properties, { tArea, tExtraH, imgAR, flexW }); } return { _tArea: tArea, _tExtraH: tExtraH, _imgAR: imgAR, _flexW: flexW, _nItems: nItems }; }; const getWidthAtH = (element, properties, targetHeight) => properties._type === S_TEXT ? properties._area / (targetHeight - (properties._extraH || 0)) : properties._aspectR * targetHeight; const getOptimalHeight = (measurements, config, logger) => { const tArea = measurements._tArea; const tExtraH = measurements._tExtraH; const imgAR = measurements._imgAR; const flexW = measurements._flexW - (measurements._nItems - 1) * config._minGap; const maxFreeR = config._maxFreeR; const maxWidthR = config._maxWidthR; debug: logger === null || logger === void 0 || logger.debug8("Getting optimal height", measurements, config); // CASE 1: No text items if (MH.isNaN(tArea)) { debug: logger === null || logger === void 0 || logger.debug8("No text items"); if (!imgAR) { debug: logger === null || logger === void 0 || logger.debug8("Images not loaded"); return NaN; } return flexW / imgAR; } // CASE 2: No images if (MH.isNaN(imgAR)) { debug: logger === null || logger === void 0 || logger.debug8("No images"); return tArea / flexW + tExtraH; } if (!imgAR || !tArea) { debug: logger === null || logger === void 0 || logger.debug8("Expected both images and text, but no imgAR or tArea"); return NaN; } const h0 = MH.sqrt(tArea / imgAR) + tExtraH; // heights satisfying w(h) === flexW const [h2, h1] = (0, _math.quadraticRoots)(imgAR, -(imgAR * tExtraH + flexW), tArea + tExtraH * flexW); // heights satisfying maxWidthR let hR0 = NaN, hR1 = NaN, hR2 = NaN; if (maxWidthR > 0) { hR0 = (0, _math.quadraticRoots)(imgAR, -imgAR * tExtraH, -tArea)[0]; hR1 = (0, _math.quadraticRoots)(imgAR * maxWidthR, -imgAR * tExtraH * maxWidthR, -tArea)[0]; hR2 = (0, _math.quadraticRoots)(imgAR / maxWidthR, -imgAR * tExtraH / maxWidthR, -tArea)[0]; } // heights satisfying maxFreeR let hF2 = NaN, hF1 = NaN; if (maxFreeR >= 0) { [hF2, hF1] = (0, _math.quadraticRoots)(imgAR, -(imgAR * tExtraH + flexW * (1 - maxFreeR)), tArea + tExtraH * flexW * (1 - maxFreeR)); } // limits on constraints const hConstr1 = MH.max(...MH.filter([h1, hR1, hF1], v => (0, _math.isValidNum)(v))); const hConstr2 = MH.min(...MH.filter([h2, hR2, hF2], v => (0, _math.isValidNum)(v))); // text and image widths at h0 const tw0 = tArea / (h0 - tExtraH); const iw0 = h0 * imgAR; // free space at h0 const freeSpace0 = flexW - tw0 - iw0; debug: logger === null || logger === void 0 || logger.debug8("Optimal height calculations", config, measurements, { h0, h1, h2, hR0, hR1, hR2, hF1, hF2, hConstr1, hConstr2, tw0, iw0, freeSpace0 }); // ~~~~ Some sanity checks // If any of then is NaN, the comparison would be false, so we don't need to // check. // Also, we round the difference to 0.1 pixels to account for rounding // errors during calculations. if (!h0 || h0 <= 0) { debug: logger === null || logger === void 0 || logger.debug1("Invalid calculation: Invalid h0"); } else if ((0, _math.isValidNum)(h1) !== (0, _math.isValidNum)(h2)) { debug: logger === null || logger === void 0 || logger.debug1("Invalid calculation: One and only one of h1 or h2 is real"); } else if ((0, _math.isValidNum)(hR1) !== (0, _math.isValidNum)(hR2)) { debug: logger === null || logger === void 0 || logger.debug1("Invalid calculation: One and only one of hR1 or hR2 is real"); } else if ((0, _math.isValidNum)(hF1) !== (0, _math.isValidNum)(hF2)) { debug: logger === null || logger === void 0 || logger.debug1("Invalid calculation: One and only one of hF1 or hF2 is real"); } else if (h1 - h0 > 0.1) { debug: logger === null || logger === void 0 || logger.debug1("Invalid calculation: h1 > h0"); } else if (h0 - h2 > 0.1) { debug: logger === null || logger === void 0 || logger.debug1("Invalid calculation: h0 > h2"); } else if (hR0 - h0 > 0.1) { debug: logger === null || logger === void 0 || logger.debug1("Invalid calculation: hR0 > h0"); } else if (hR1 - hR0 > 0.1) { debug: logger === null || logger === void 0 || logger.debug1("Invalid calculation: hR1 > hR0"); } else if (hR0 - hR2 > 0.1) { debug: logger === null || logger === void 0 || logger.debug1("Invalid calculation: hR0 > hR2"); } else if (hF1 - hF2 > 0.1) { debug: logger === null || logger === void 0 || logger.debug1("Invalid calculation: hF1 > hF2"); } else if (h1 - hF1 > 0.1) { debug: logger === null || logger === void 0 || logger.debug1("Invalid calculation: h1 > hF1"); } else if (hF2 - h2 > 0.1) { debug: logger === null || logger === void 0 || logger.debug1("Invalid calculation: hF2 > h2"); } else { // Choose a height if (freeSpace0 <= 0) { // scenario 1 or 2 return h0; } else { // scenario 3 return MH.min(hConstr1, hConstr2); } } (0, _log.logError)(MH.bugError("Invalid SameHeight calculations"), measurements, config); return NaN; // sanity checks failed }; //# sourceMappingURL=same-height.cjs.map