lisn.js
Version:
Simply handle user gestures and actions. Includes widgets.
1,014 lines (982 loc) • 42.1 kB
JavaScript
"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