/**
 * @license
 * Copyright Tyler Technologies, Inc. 
 * License: Apache-2.0
 */
/**
 * Highlights text in the given label by converting it to HTML and using a `<span>` tag to show the highlighted text within the original label.
 * @param label The full text.
 * @param highlightText The text to highlight.
 */
export function highlightTextHTML(label, highlightText) {
    const text = label.toLowerCase();
    const startIndex = text.indexOf(highlightText.toLowerCase());
    if (startIndex !== -1) {
        const endIndex = startIndex + highlightText.length;
        const wrapperSpan = document.createElement('span');
        const highlightSpan = document.createElement('span');
        highlightSpan.style.fontWeight = 'bold';
        highlightSpan.textContent = label.substring(startIndex, endIndex);
        wrapperSpan.appendChild(document.createTextNode(label.substring(0, startIndex)));
        wrapperSpan.appendChild(highlightSpan);
        wrapperSpan.appendChild(document.createTextNode(label.substring(endIndex)));
        return wrapperSpan;
    }
    return undefined;
}
/**
 * Awaits user interaction on an element in the form of `pointerenter` or `focusin` to let a listener know
 * when the user has attempted to interact with the provided element.
 *
 * The listeners are only called once, and the other is removed after one of the listeners is called.
 * @param element The element to listen to.
 * @param capture Whether to use capturing listeners or not.
 * @returns A `Promise` that will be resolved when either of the listeners has executed.
 */
export function createUserInteractionListener(element, { capture = true, pointerenter = true, focusin = true } = {}) {
    let destroyFn;
    const destroy = () => {
        if (typeof destroyFn === 'function') {
            destroyFn();
        }
    };
    const userInteraction = new Promise(resolve => {
        const listenerOpts = { once: true, capture };
        const handlePointerenter = (evt) => {
            if (focusin) {
                element.removeEventListener('focusin', handleFocusin, listenerOpts);
            }
            resolve(evt);
        };
        const handleFocusin = (evt) => {
            if (pointerenter) {
                element.removeEventListener('pointerenter', handlePointerenter, listenerOpts);
            }
            resolve(evt);
        };
        destroyFn = () => {
            if (pointerenter) {
                element.removeEventListener('pointerenter', handlePointerenter, listenerOpts);
            }
            if (focusin) {
                element.removeEventListener('focusin', handleFocusin, listenerOpts);
            }
        };
        if (pointerenter) {
            element.addEventListener('pointerenter', handlePointerenter, listenerOpts);
        }
        if (focusin) {
            element.addEventListener('focusin', handleFocusin, listenerOpts);
        }
    });
    return { userInteraction, destroy };
}
/**
 * Converts a percent value to pixels.
 * @param amount A percent value.
 * @param containerSize The size of the parent element along the relevant axis.
 * @returns A pixel value.
 */
export function percentToPixels(amount, containerSize) {
    if (containerSize === 0) {
        return 0;
    }
    return amount / 100 * containerSize;
}
/**
 * Converts a pixel value to a percentage.
 * @param amount A pixel value.
 * @param containerSize The size of the parent element along the relevant axis.
 * @returns A percent value.
 */
export function pixelsToPercent(amount, containerSize) {
    if (containerSize === 0) {
        return 0;
    }
    return amount * 100 / containerSize;
}
/**
 * Scales a value from one range to another.
 * @param value The original number value.
 * @param fromMin The lower bound of the input range.
 * @param fromMax The upper bound of the input range.
 * @param toMin The lower bound of the output range (defaults to 0).
 * @param toMax The lower bound of the output range (defaults to 100).
 * @returns A value mapped to the output range.
 */
export function scaleValue(value, fromMin, fromMax, toMin = 0, toMax = 100) {
    const range = fromMax - fromMin;
    const adjustedValue = value - fromMin;
    if (!range || !adjustedValue) {
        return toMin;
    }
    return adjustedValue * toMax / range + toMin;
}
/**
 * Returns the min of a set of numbers where some values may be undefined.
 * @param args `number` or `undefined` values to compare.
 * @returns The min value or `Number.POSITIVE_INFINITY` if all values are `undefined`.
 */
export function safeMin(...args) {
    return Math.min(...args.map(arg => arg !== null && arg !== void 0 ? arg : Number.POSITIVE_INFINITY));
}
/**
 * Returns the max of a set of numbers where some values may be undefined.
 * @param args `number` or `undefined` values to compare.
 * @returns The max value or `Number.NEGATIVE_INFINITY` if all values are `undefined`.
 */
export function safeMax(...args) {
    return Math.max(...args.map(arg => arg !== null && arg !== void 0 ? arg : Number.NEGATIVE_INFINITY));
}
/**
 * Determines if two elements are overlapping.
 * @param elA {Element | null}
 * @param elB {Element | null}
 * @returns
 */
export function elementsOverlapping(elA, elB) {
    if (!(elA && elB)) {
        return false;
    }
    const a = elA.getBoundingClientRect();
    const b = elB.getBoundingClientRect();
    return !(a.top > b.bottom || a.right < b.left || a.bottom < b.top || a.left > b.right);
}
/**
 * Determines if a pointer event is over an element.
 * @param event {PointerEvent} The pointer event to test.
 * @param element {HTMElement} The element to test against.
 * @returns
 */
export function isPointerOverElement({ x, y }, element) {
    if (!element) {
        return false;
    }
    const { top, left, bottom, right } = element.getBoundingClientRect();
    return x >= left && x <= right && y >= top && y <= bottom;
}
/**
 * Attempts to locate a target element based on a heuristic.
 *
 * We use the following heuristic for locating the target element:
 *  - If an id is set, we use that value to query the DOM for the target element
 *  - If id is set to `:host`, we use the host element from within a shadow tree (only if the root node is a ShadowRoot instance)
 *  - If an id is set but the querySelector returns null, we use the parent element
 *  - If an id is not set, we use the parent element
 * @param value {string | null} - A selector string to query the DOM for the target element
 */
export function locateTargetHeuristic(element, id) {
    let targetEl = null;
    if (id) {
        targetEl = locateElementById(element, id);
    }
    if (!targetEl) {
        return element.parentElement;
    }
    return targetEl;
}
/**
 * Attempts to locate an element by id within its root node.
 * @param element The element to search from.
 * @param id The id of the element to locate.
 * @returns The element if found, otherwise `null`.
 */
export function locateElementById(element, id) {
    const rootNode = element.getRootNode();
    // Special case handling for a `:host` selector to easily target a host element
    // from within a shadow tree, given that this is a very common scenario
    if (id === ':host' && rootNode instanceof ShadowRoot) {
        return rootNode.host;
    }
    return rootNode.querySelector(`#${id}`);
}
/**
 * Replaces an existing element with a new element, while optionally moving the children of the old element into the new element.
 * @param oldElement The element to replace.
 * @param newElement The element to replace with.
 * @param preserveChildren Whether or not to preserve the children of the old element in the new element.
 * @returns The new element.
 */
export function replaceElement(oldElement, newElement, preserveChildren = true) {
    if (preserveChildren) {
        newElement.append(...oldElement.childNodes);
    }
    oldElement.insertAdjacentElement('beforebegin', newElement);
    oldElement.remove();
    return newElement;
}
/**
 * Coerces a string separated by `separator` into an array of strings.
 * @param value The string to coerce.
 * @params [separator=','] The separator to use when splitting the string.
 * @returns An array of strings.
 */
export function coerceStringToArray(value, separator = ',') {
    return value.split(separator).map(p => p.trim());
}
/**
 * Wraps an array of elements in a new element.
 * @param elements The elements to wrap.
 * @param wrapper The new wrapper element.
 */
export function wrapElements(elements, wrapper, exclude) {
    if (!elements.length) {
        return;
    }
    const parentNode = elements[0].parentNode;
    if (!parentNode) {
        return;
    }
    parentNode.insertBefore(wrapper, elements[0]);
    elements.forEach(el => {
        if ((exclude === null || exclude === void 0 ? void 0 : exclude.length) && exclude.some(ex => el.matches(ex))) {
            return;
        }
        wrapper.append(el);
    });
}
/**
 * Unwraps an element by moving its children to its parent and removing the element.
 * @param wrapper The element to unwrap.
 */
export function unwrapElements(wrapper) {
    const parentNode = wrapper.parentNode;
    if (!parentNode) {
        return;
    }
    while (wrapper.firstChild) {
        parentNode.insertBefore(wrapper.firstChild, wrapper);
    }
    wrapper.remove();
}
/**
 * Rounds a value to the nearest pixel based on the device pixel ratio.
 * @param {number} value The value to round.
 * @returns {number} The rounded value.
 */
export function roundByDPR(value) {
    const dpr = window.devicePixelRatio || 1;
    return Math.round(value * dpr) / dpr;
}
