/** * vanilla-match-height v2.1.0 by @mitera * Simone Miterangelis * License: MIT */ interface HTMLElement { matchHeight(settings?: Settings): MatchHeight; } interface MatchHeight { _remains: Item[]; wrapEl: HTMLElement; settings: Settings; _bind(): void; _init(): void; _unbind(): void; _throttle(fn: Function, threshold: number): () => void; _applyAll(): void; _validateProperty(value?: string | null): RegExpMatchArray | null; _applyDataApi(property: string): void; _remove(): void; _apply(): void; _resetStyle($that: HTMLElement, property: string): void; _update(elements: HTMLElement[]): void; _process(): void; } interface Settings { elements?: string | null; byRow?: boolean | null; target?: HTMLElement | null; attributeName?: string | null; attributeValue?: string | null; property: string; remove?: HTMLElement | null; events?: boolean | null; throttle?: number | null; beforeUpdate?: any | null; afterUpdate?: any | null; } type Item = { el: HTMLElement; top: number; height: number; } (function(){ /** * Extends the HTMLElement prototype with a method named `matchHeight`. * * This method adjusts the height of the element to match the height of the tallest element in a collection of associated elements. * * The elements to "match height" with are typically determined by the context in which the method is invoked, * such as sibling elements or elements in the same container. * * It ensures consistent heights for elements in flexible layouts, improving visual alignment and design structure. * * Note: This method does not remove or reset inline height styles. Use caution to avoid conflicts with pre-existing styles. */ HTMLElement.prototype.matchHeight = function(settings?: Settings) { // @ts-ignore return new MatchHeight(this, settings); } /** * Constructs a new MatchHeight instance. * * @param {HTMLElement} wrapEl - The parent element within which child elements will have their heights matched. * @param {Settings} settings - An object containing configuration options for the MatchHeight instance, such as elements to select, row-type matching, and event handling. * @return {void} This function does not return a value; it initializes a new instance of the MatchHeight class. */ function MatchHeight(this: MatchHeight, wrapEl: HTMLElement, settings: Settings) { this.wrapEl = wrapEl; this._remains = []; // Default settings let default_settings: Settings = { elements: null, byRow: true, target: null, attributeName: null, attributeValue: null, property: 'height', remove: null, events: true, throttle: 80, beforeUpdate: null, afterUpdate: null } this.settings = {...default_settings, ...settings} as Settings; if (!this._validateProperty(this.settings.property)) { this.settings.property = 'height'; } if (this.settings.events) { const update = this._applyAll.bind(this); if (document.readyState !== 'loading') { this._applyAll(); } else { document.addEventListener( 'DOMContentLoaded', update, { once: true } ); } if (this.settings.throttle && this.settings.throttle > 0) { this._bind = this._throttle(update, this.settings.throttle); } this._init(); } } /** * Initializes the MatchHeight instance by setting up necessary * configurations or bindings to ensure the height matching functionality. * This method is typically used internally during the setup process. * * @private */ MatchHeight.prototype._init = function() { window.addEventListener("resize", this._bind); window.addEventListener("orientationchange", this._bind); } /** * Unbinds all MatchHeight event listeners from the target elements. * * This method removes all associated events such as resize listeners or scroll listeners * that were previously bound by the MatchHeight functionality. It ensures that the * target elements are no longer affected by the MatchHeight behavior, preventing * further updates or recalculations. * * Use this method when you no longer need MatchHeight to handle the height equalization * of the elements, or before completely removing the associated DOM elements from the page. */ MatchHeight.prototype._unbind = function() { window.removeEventListener("resize", this._bind); window.removeEventListener("orientationchange", this._bind); } /** * A utility function designed to limit the rate at which a given function is executed. * This method ensures that the specified function is triggered at most once within * a defined time period determined by the provided delay duration. * * @param {Function} callback - The function to be throttled. It will only execute * once per delay interval regardless of how often it is invoked. * @param {number} delay - The time interval, specified in milliseconds, during * which repeated function calls will be ignored after the initial execution. * @returns {Function} - A new function that wraps the original callback and enforces * the throttle behavior, allowing controlled execution within the given delay. */ MatchHeight.prototype._throttle = function(fn: Function, threshold: number) { let last: number, deferTimer: any; return function () { const now = Date.now(); if (last && now < last + threshold) { clearTimeout(deferTimer); deferTimer = setTimeout(function () { last = now; fn(); }, threshold); } else { last = now; fn(); } }; } /** * Applies the height matching functionality to all elements managed by the MatchHeight instance. * * This method calculates and sets the maximum height for each group of elements * that share the same context, ensuring a uniform height across the group. * * The function iterates through each group of elements, computes the tallest height * among them, and applies that height to all elements within the group. * * It is typically invoked to synchronize the heights of elements * after initialization or when elements are updated dynamically. */ MatchHeight.prototype._applyAll = function() { if (this.settings && this.settings.beforeUpdate) { this.settings.beforeUpdate(); } this._apply(); if (this.settings.attributeName && this._validateProperty(this.settings.attributeName)) { this._applyDataApi(this.settings.attributeName); } this._applyDataApi('data-match-height'); this._applyDataApi('data-mh'); if (this.settings && this.settings.afterUpdate) { this.settings.afterUpdate(); } } /** * Validates the given property to ensure it exists and is valid for use * in the MatchHeight functionality. This method checks if the property is * allowed and conforms to expected types or values for processing height * matching. * * @private * @param {string} property - The property to validate. * @returns {boolean} True if the property is valid, otherwise false. */ MatchHeight.prototype._validateProperty = function(value: string) { return String(value) .toLowerCase() .match( /^([a-z-]{2,})$/ ); } /** * Applies the match height functionality to all elements * found in the DOM that use the `data-match-height` attribute. * The method selects such elements and applies the match * height logic to ensure consistent height across groups. * * This function is intended to be triggered automatically * and processes elements with matching data attributes, * grouping them by their attribute values and applying * equal height adjustments. * * @param {String} attributeName */ MatchHeight.prototype._applyDataApi = function(attributeName: string) { let elements: HTMLElement[] = Array.from(this.wrapEl.querySelectorAll('[' + attributeName + ']')); elements.forEach( ( item ) => { this._resetStyle(item, this.settings.property); } ); const groups: Map = new Map(); elements.forEach((el) => { const groupId = el.getAttribute(attributeName); if (groupId) { if (!groups.has(groupId)) { groups.set(groupId, []); } groups.get(groupId)!.push(el); } }); // Apply once per unique group instead of once per element groups.forEach((elements) => { this._update(elements); }); } /** * Removes the match height functionality from the elements. * * This function iterates through the selected elements and resets * their inline styles that were applied to set equal heights. * It ensures that the DOM elements revert to their original height styles. * * @private */ MatchHeight.prototype._remove = function() { let $elements: HTMLElement[] = [] let opts = this.settings; if (opts.elements) { $elements = Array.from(this.wrapEl.querySelectorAll(opts.elements)); } else { if (opts.attributeName && opts.attributeValue) { $elements = Array.from(this.wrapEl.querySelectorAll('[' + opts.attributeName + '="' + opts.attributeValue + '"]')); } } $elements.forEach((item) => { item.style.setProperty(opts.property, ''); if (item.getAttribute('style') === '') item.removeAttribute('style'); }); } /** * Applies the match height functionality to the set of elements. * This method calculates the maximum height among the group of elements * and adjusts each element's height to match the maximum. It ensures that all * selected elements within the context of the MatchHeight instance appear with * consistent height. * * This method takes into account any existing styles, box model properties, * such as padding and borders, to ensure accurate height calculations if needed. * * It is typically used internally to enforce height equality in the associated * elements. */ MatchHeight.prototype._apply = function() { let opts = this.settings; let elements: HTMLElement[] = [] if (opts.elements && opts.elements.trim() != '') { elements = Array.from(this.wrapEl.querySelectorAll(opts.elements)); } else { if (opts.attributeName && this._validateProperty(opts.attributeName) && opts.attributeValue && opts.attributeValue.trim() != '') { elements = Array.from(this.wrapEl.querySelectorAll('[' + opts.attributeName + '="' + opts.attributeValue + '"]')); } } this._update(elements); } /** * Updates the height of elements in the MatchHeight instance. * This function recalculates and applies the matching height * logic to the target elements based on their current visibility, * size, and any applied options. It ensures that the elements * are properly aligned and maintain consistent heights. * * This method is usually called internally when the heights need * to be refreshed, for example, after a window resize or content * change. * * The height update takes into account any groupings, overrides, * or custom configurations provided to the MatchHeight instance. */ MatchHeight.prototype._update = function(elements: HTMLElement[]) { if ( elements.length === 0 ) return; this._remains = Array.prototype.map.call( elements, ( el: HTMLElement ): Item => { return { el, top: 0, height: 0 }; } ) as Item[]; // remove all height before this._remains.forEach( ( item: Item ) => { this._resetStyle(item.el, this.settings.property); } ); this._process(); } /** * Processes the elements provided to the MatchHeight instance and determines * the height adjustment required to make all the elements have the same height. * This method calculates the maximum height among a group of elements and * applies it uniformly to maintain consistency in their appearance. * * This method is typically used when dynamic height adjustments * are required to ensure a uniform layout for elements with varying heights. * Internal logic includes checking constraints, grouping elements, and * calculating corresponding height adjustments while avoiding unnecessary DOM changes. * * @private */ MatchHeight.prototype._process = function() { this._remains.forEach( ( item:Item ) => { const bb = item.el.getBoundingClientRect(); item.top = this.settings.byRow ? bb.top : 0; item.height = bb.height; } ); this._remains.sort( ( a:Item, b:Item ) => a.top - b.top); const errorThreshold = 1; const processingTop = this._remains[0].top; const processingTargets = this._remains.filter((item: { top: number; }) => Math.abs(item.top - processingTop) <= errorThreshold); let maxHeightInRow = 0; if (this.settings.target) maxHeightInRow = this.settings.target.getBoundingClientRect().height; else maxHeightInRow = Math.max(...processingTargets.map((item: Item) => item.height)); processingTargets.forEach((item: Item) => { const styles = window.getComputedStyle(item.el); const isBorderBox = styles.boxSizing === 'border-box'; if (isBorderBox) { if (this.settings.property) item.el.style.setProperty(this.settings.property, `${maxHeightInRow}px`); } else { const paddingAndBorder = (parseFloat(styles.paddingTop) || 0) + (parseFloat(styles.paddingBottom) || 0) + (parseFloat(styles.borderTopWidth) || 0) + (parseFloat(styles.borderBottomWidth) || 0); if (this.settings.property) item.el.style.setProperty(this.settings.property, `${maxHeightInRow - paddingAndBorder}px`); } if (this.settings.remove) { if (this.settings.remove instanceof NodeList) { Array.from(this.settings.remove).forEach((el) => { if (item.el === el && this.settings.property && el instanceof HTMLElement) { this._resetStyle(el, this.settings.property); } }); } else if (item.el === this.settings.remove && this.settings.property) { this._resetStyle(item.el, this.settings.property); } } }); this._remains.splice(0, processingTargets.length); if (0 < this._remains.length) { this._process() } } /** * Resets the inline styles applied to the elements previously adjusted * for matching height. This method ensures that the height-related * adjustments such as min-height, height, or padding are removed, * reverting the elements to their original styles. * * @param {HTMLElement} $that * @param {String} property */ MatchHeight.prototype._resetStyle = function($that: HTMLElement, property: string) { if (this._validateProperty(property)) { $that.style.setProperty(property, ''); if ($that.getAttribute('style') === '') $that.removeAttribute('style'); } } })();