"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getInlineStartProperty = exports.getReferenceElement = exports.getLanguageDirection = exports.clearTimeouts = exports.preventedEvents = exports.trimLeft = exports.innerDimensions = exports.getTextWidth = exports.canUseDOM = exports.toCamel = exports.getBreakpoint = exports.getVerticalBreakpoint = exports.formatBreakpointMods = exports.setBreakpointCssVars = exports.pluralize = exports.getNextIndex = exports.findTabbableElements = exports.keyHandler = exports.fillTemplate = exports.sideElementIsOutOfView = exports.isElementInView = exports.debounce = exports.getUniqueId = exports.capitalize = void 0;
const constants_1 = require("./constants");
/**
 * @param {string} input - String to capitalize first letter
 */
function capitalize(input) {
    return input[0].toUpperCase() + input.substring(1);
}
exports.capitalize = capitalize;
/**
 * @param {string} prefix - String to prefix ID with
 */
function getUniqueId(prefix = 'pf') {
    const uid = new Date().getTime() + Math.random().toString(36).slice(2);
    return `${prefix}-${uid}`;
}
exports.getUniqueId = getUniqueId;
/**
 * @param { any } this - "This" reference
 * @param { Function } func - Function to debounce
 * @param { number } wait - Debounce amount
 */
function debounce(func, wait) {
    let timeout;
    return (...args) => {
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(this, args), wait);
    };
}
exports.debounce = debounce;
/** This function returns whether or not an element is within the viewable area of a container. If partial is true,
 * then this function will return true even if only part of the element is in view.
 *
 * @param {HTMLElement} container  The container to check if the element is in view of.
 * @param {HTMLElement} element    The element to check if it is view
 * @param {boolean} partial   true if partial view is allowed
 * @param {boolean} strict    true if strict mode is set, never consider the container width and element width
 *
 * @returns { boolean } True if the component is in View.
 */
function isElementInView(container, element, partial, strict = false) {
    if (!container || !element) {
        return false;
    }
    const containerBounds = container.getBoundingClientRect();
    const elementBounds = element.getBoundingClientRect();
    const containerBoundsLeft = Math.ceil(containerBounds.left);
    const containerBoundsRight = Math.floor(containerBounds.right);
    const elementBoundsLeft = Math.ceil(elementBounds.left);
    const elementBoundsRight = Math.floor(elementBounds.right);
    // Check if in view
    const isTotallyInView = elementBoundsLeft >= containerBoundsLeft && elementBoundsRight <= containerBoundsRight;
    const isPartiallyInView = (partial || (!strict && containerBounds.width < elementBounds.width)) &&
        ((elementBoundsLeft < containerBoundsLeft && elementBoundsRight > containerBoundsLeft) ||
            (elementBoundsRight > containerBoundsRight && elementBoundsLeft < containerBoundsRight));
    // Return outcome
    return isTotallyInView || isPartiallyInView;
}
exports.isElementInView = isElementInView;
/** This function returns the side the element is out of view on (right, left or both)
 *
 * @param {HTMLElement} container    The container to check if the element is in view of.
 * @param {HTMLElement} element      The element to check if it is view
 *
 * @returns {string} right if the element is of the right, left if element is off the left or both if it is off on both sides.
 */
function sideElementIsOutOfView(container, element) {
    const containerBounds = container.getBoundingClientRect();
    const elementBounds = element.getBoundingClientRect();
    const containerBoundsLeft = Math.floor(containerBounds.left);
    const containerBoundsRight = Math.floor(containerBounds.right);
    const elementBoundsLeft = Math.floor(elementBounds.left);
    const elementBoundsRight = Math.floor(elementBounds.right);
    // Check if in view
    const isOffLeft = elementBoundsLeft < containerBoundsLeft;
    const isOffRight = elementBoundsRight > containerBoundsRight;
    let side = constants_1.SIDE.NONE;
    if (isOffRight && isOffLeft) {
        side = constants_1.SIDE.BOTH;
    }
    else if (isOffRight) {
        side = constants_1.SIDE.RIGHT;
    }
    else if (isOffLeft) {
        side = constants_1.SIDE.LEFT;
    }
    // Return outcome
    return side;
}
exports.sideElementIsOutOfView = sideElementIsOutOfView;
/** Interpolates a parameterized templateString using values from a templateVars object.
 * The templateVars object should have keys and values which match the templateString's parameters.
 * Example:
 *    const templateString: 'My name is ${firstName} ${lastName}';
 *    const templateVars: {
 *      firstName: 'Jon'
 *      lastName: 'Dough'
 *    };
 *    const result = fillTemplate(templateString, templateVars);
 *    // "My name is Jon Dough"
 *
 * @param {string} templateString  The string passed by the consumer
 * @param {object} templateVars The variables passed to the string
 *
 * @returns {string} The template string literal result
 */
function fillTemplate(templateString, templateVars) {
    return templateString.replace(/\${(.*?)}/g, (_, match) => templateVars[match] || '');
}
exports.fillTemplate = fillTemplate;
/**
 * This function allows for keyboard navigation through dropdowns. The custom argument is optional.
 *
 * @param {number} index The index of the element you're on
 * @param {number} innerIndex Inner index number
 * @param {string} position The orientation of the dropdown
 * @param {string[]} refsCollection Array of refs to the items in the dropdown
 * @param {object[]} kids Array of items in the dropdown
 * @param {boolean} [custom] Allows for handling of flexible content
 */
function keyHandler(index, innerIndex, position, refsCollection, kids, custom = false) {
    if (!Array.isArray(kids)) {
        return;
    }
    const isMultiDimensional = refsCollection.filter((ref) => ref)[0].constructor === Array;
    let nextIndex = index;
    let nextInnerIndex = innerIndex;
    if (position === 'up') {
        if (index === 0) {
            // loop back to end
            nextIndex = kids.length - 1;
        }
        else {
            nextIndex = index - 1;
        }
    }
    else if (position === 'down') {
        if (index === kids.length - 1) {
            // loop back to beginning
            nextIndex = 0;
        }
        else {
            nextIndex = index + 1;
        }
    }
    else if (position === 'left') {
        if (innerIndex === 0) {
            nextInnerIndex = refsCollection[index].length - 1;
        }
        else {
            nextInnerIndex = innerIndex - 1;
        }
    }
    else if (position === 'right') {
        if (innerIndex === refsCollection[index].length - 1) {
            nextInnerIndex = 0;
        }
        else {
            nextInnerIndex = innerIndex + 1;
        }
    }
    if (refsCollection[nextIndex] === null ||
        refsCollection[nextIndex] === undefined ||
        (isMultiDimensional &&
            (refsCollection[nextIndex][nextInnerIndex] === null || refsCollection[nextIndex][nextInnerIndex] === undefined))) {
        keyHandler(nextIndex, nextInnerIndex, position, refsCollection, kids, custom);
    }
    else if (custom) {
        if (refsCollection[nextIndex].focus) {
            refsCollection[nextIndex].focus();
        }
        const element = refsCollection[nextIndex];
        element.focus();
    }
    else if (position !== 'tab') {
        if (isMultiDimensional) {
            refsCollection[nextIndex][nextInnerIndex].focus();
        }
        else {
            refsCollection[nextIndex].focus();
        }
    }
}
exports.keyHandler = keyHandler;
/** This function returns a list of tabbable items in a container
 *
 *  @param {any} containerRef to the container
 *  @param {string} tababbleSelectors CSS selector string of tabbable items
 */
function findTabbableElements(containerRef, tababbleSelectors) {
    const tabbable = containerRef.current.querySelectorAll(tababbleSelectors);
    const list = Array.prototype.filter.call(tabbable, function (item) {
        return item.tabIndex >= '0';
    });
    return list;
}
exports.findTabbableElements = findTabbableElements;
/** This function is a helper for keyboard navigation through dropdowns.
 *
 * @param {number} index The index of the element you're on
 * @param {string} position The orientation of the dropdown
 * @param {string[]} collection Array of refs to the items in the dropdown
 */
function getNextIndex(index, position, collection) {
    let nextIndex;
    if (position === 'up') {
        if (index === 0) {
            // loop back to end
            nextIndex = collection.length - 1;
        }
        else {
            nextIndex = index - 1;
        }
    }
    else if (index === collection.length - 1) {
        // loop back to beginning
        nextIndex = 0;
    }
    else {
        nextIndex = index + 1;
    }
    if (collection[nextIndex] === undefined || collection[nextIndex][0] === null) {
        return getNextIndex(nextIndex, position, collection);
    }
    else {
        return nextIndex;
    }
}
exports.getNextIndex = getNextIndex;
/** This function is a helper for pluralizing strings.
 *
 * @param {number} i The quantity of the string you want to pluralize
 * @param {string} singular The singular version of the string
 * @param {string} plural The change to the string that should occur if the quantity is not equal to 1.
 *                 Defaults to adding an 's'.
 */
function pluralize(i, singular, plural) {
    if (!plural) {
        plural = `${singular}s`;
    }
    return `${i || 0} ${i === 1 ? singular : plural}`;
}
exports.pluralize = pluralize;
/**
 * This function is a helper for turning arrays of breakpointMod objects for flex and grid into style object
 *
 * @param {object} mods The modifiers object
 * @param {string} css-variable The appropriate css variable for the component
 */
const setBreakpointCssVars = (mods, cssVar) => Object.entries(mods || {}).reduce((acc, [breakpoint, value]) => breakpoint === 'default' ? Object.assign(Object.assign({}, acc), { [cssVar]: value }) : Object.assign(Object.assign({}, acc), { [`${cssVar}-on-${breakpoint}`]: value }), {});
exports.setBreakpointCssVars = setBreakpointCssVars;
/**
 * This function is a helper for turning arrays of breakpointMod objects for data toolbar and flex into classes
 *
 * @param {object} mods The modifiers object
 * @param {any} styles The appropriate styles object for the component
 */
const formatBreakpointMods = (mods, styles, stylePrefix = '', breakpoint, vertical) => {
    if (!mods) {
        return '';
    }
    if (breakpoint && !vertical) {
        if (breakpoint in mods) {
            return styles.modifiers[(0, exports.toCamel)(`${stylePrefix}${mods[breakpoint]}`)];
        }
        // the current breakpoint is not specified in mods, so we try to find the next nearest
        const breakpointsOrder = ['2xl', 'xl', 'lg', 'md', 'sm', 'default'];
        const breakpointsIndex = breakpointsOrder.indexOf(breakpoint);
        for (let i = breakpointsIndex; i < breakpointsOrder.length; i++) {
            if (breakpointsOrder[i] in mods) {
                return styles.modifiers[(0, exports.toCamel)(`${stylePrefix}${mods[breakpointsOrder[i]]}`)];
            }
        }
        return '';
    }
    return Object.entries(mods || {})
        .map(([breakpoint, mod]) => `${stylePrefix}${mod}${breakpoint !== 'default' ? `-on-${breakpoint}` : ''}${vertical && breakpoint !== 'default' ? '-height' : ''}`)
        .map(exports.toCamel)
        .map((mod) => mod.replace(/-?(\dxl)/gi, (_res, group) => `_${group}`))
        .map((modifierKey) => styles.modifiers[modifierKey])
        .filter(Boolean)
        .join(' ');
};
exports.formatBreakpointMods = formatBreakpointMods;
/**
 * Return the breakpoint for the given height
 *
 * @param {number | null} height The height to check
 * @returns {'default' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'} The breakpoint
 */
const getVerticalBreakpoint = (height) => {
    if (height === null) {
        return null;
    }
    if (height >= constants_1.globalHeightBreakpoints['2xl']) {
        return '2xl';
    }
    if (height >= constants_1.globalHeightBreakpoints.xl) {
        return 'xl';
    }
    if (height >= constants_1.globalHeightBreakpoints.lg) {
        return 'lg';
    }
    if (height >= constants_1.globalHeightBreakpoints.md) {
        return 'md';
    }
    if (height >= constants_1.globalHeightBreakpoints.sm) {
        return 'sm';
    }
    return 'default';
};
exports.getVerticalBreakpoint = getVerticalBreakpoint;
/**
 * Return the breakpoint for the given width
 *
 * @param {number | null} width The width to check
 * @returns {'default' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'} The breakpoint
 */
const getBreakpoint = (width) => {
    if (width === null) {
        return null;
    }
    if (width >= constants_1.globalWidthBreakpoints['2xl']) {
        return '2xl';
    }
    if (width >= constants_1.globalWidthBreakpoints.xl) {
        return 'xl';
    }
    if (width >= constants_1.globalWidthBreakpoints.lg) {
        return 'lg';
    }
    if (width >= constants_1.globalWidthBreakpoints.md) {
        return 'md';
    }
    if (width >= constants_1.globalWidthBreakpoints.sm) {
        return 'sm';
    }
    return 'default';
};
exports.getBreakpoint = getBreakpoint;
const camelize = (s) => s.toUpperCase().replace('-', '').replace('_', '');
/**
 *
 * @param {string} s string to make camelCased
 */
const toCamel = (s) => s.replace(/([-_][a-z])/gi, camelize);
exports.toCamel = toCamel;
/**
 * Copied from exenv
 */
exports.canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
/**
 * Calculate the width of the text
 * Example:
 * getTextWidth('my text', node)
 *
 * @param {string} text The text to calculate the width for
 * @param {HTMLElement} node The HTML element
 */
const getTextWidth = (text, node) => {
    const computedStyle = getComputedStyle(node);
    // Firefox returns the empty string for .font, so this function creates the .font property manually
    const getFontFromComputedStyle = () => {
        let computedFont = '';
        // Firefox uses percentages for font-stretch, but Canvas does not accept percentages
        // so convert to keywords, as listed at:
        // https://developer.mozilla.org/en-US/docs/Web/CSS/font-stretch
        const fontStretchLookupTable = {
            '50%': 'ultra-condensed',
            '62.5%': 'extra-condensed',
            '75%': 'condensed',
            '87.5%': 'semi-condensed',
            '100%': 'normal',
            '112.5%': 'semi-expanded',
            '125%': 'expanded',
            '150%': 'extra-expanded',
            '200%': 'ultra-expanded'
        };
        // If the retrieved font-stretch percentage isn't found in the lookup table, use
        // 'normal' as a last resort.
        let fontStretch;
        if (computedStyle.fontStretch in fontStretchLookupTable) {
            fontStretch = fontStretchLookupTable[computedStyle.fontStretch];
        }
        else {
            fontStretch = 'normal';
        }
        computedFont =
            computedStyle.fontStyle +
                ' ' +
                computedStyle.fontVariant +
                ' ' +
                computedStyle.fontWeight +
                ' ' +
                fontStretch +
                ' ' +
                computedStyle.fontSize +
                '/' +
                computedStyle.lineHeight +
                ' ' +
                computedStyle.fontFamily;
        return computedFont;
    };
    const canvas = document.createElement('canvas');
    const context = canvas.getContext('2d');
    context.font = computedStyle.font || getFontFromComputedStyle();
    return context.measureText(text).width;
};
exports.getTextWidth = getTextWidth;
/**
 * Get the inner dimensions of an element
 *
 * @param {HTMLElement} node HTML element to calculate the inner dimensions for
 */
const innerDimensions = (node) => {
    const computedStyle = getComputedStyle(node);
    let width = node.clientWidth; // width with padding
    let height = node.clientHeight; // height with padding
    height -= parseFloat(computedStyle.paddingTop) + parseFloat(computedStyle.paddingBottom);
    width -= parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight);
    return { height, width };
};
exports.innerDimensions = innerDimensions;
/**
 * This function is a helper for truncating text content on the left, leaving the right side of the content in view
 *
 * @param {HTMLElement} node HTML element
 * @param {string} value The original text value
 */
const trimLeft = (node, value) => {
    const availableWidth = (0, exports.innerDimensions)(node).width;
    let newValue = value;
    if ((0, exports.getTextWidth)(value, node) > availableWidth) {
        // we have text overflow, trim the text to the left and add ... in the front until it fits
        while ((0, exports.getTextWidth)(`...${newValue}`, node) > availableWidth) {
            newValue = newValue.substring(1);
        }
        // replace text with our truncated text
        if (node.value) {
            node.value = `...${newValue}`;
        }
        else {
            node.innerText = `...${newValue}`;
        }
    }
    else {
        if (node.value) {
            node.value = value;
        }
        else {
            node.innerText = value;
        }
    }
};
exports.trimLeft = trimLeft;
/**
 * @param {string[]} events - Operations to prevent when disabled
 */
const preventedEvents = (events) => events.reduce((handlers, eventToPrevent) => (Object.assign(Object.assign({}, handlers), { [eventToPrevent]: (event) => {
        event.preventDefault();
    } })), {});
exports.preventedEvents = preventedEvents;
/**
 * @param {React.RefObject<any>[]} timeoutRefs - Timeout refs to clear
 */
const clearTimeouts = (timeoutRefs) => {
    timeoutRefs.forEach((ref) => {
        if (ref.current) {
            clearTimeout(ref.current);
        }
    });
};
exports.clearTimeouts = clearTimeouts;
/**
 * Helper function to get the language direction of a given element, useful for figuring out if left-to-right
 * or right-to-left specific logic should be applied.
 *
 * @param {HTMLElement} targetElement  - Element the helper will get the language direction of
 * @param {'ltr' | 'rtl'} defaultDirection - Language direction to assume if one can't be determined, defaults to 'ltr'
 * @returns {'ltr' | 'rtl'} - The language direction of the target element
 */
const getLanguageDirection = (targetElement, defaultDirection = 'ltr') => {
    if (!targetElement) {
        return defaultDirection;
    }
    const computedDirection = getComputedStyle(targetElement).getPropertyValue('direction');
    if (['ltr', 'rtl'].includes(computedDirection)) {
        return computedDirection;
    }
    return defaultDirection;
};
exports.getLanguageDirection = getLanguageDirection;
/**
 * Gets a reference element based on a ref property, which can typically be 1 of several types.
 *
 * @param {HTMLElement | (() => HTMLElement) | React.RefObject<any>} refProp The ref property to get a reference element from.
 * @returns The reference element if one is found.
 */
const getReferenceElement = (refProp) => {
    if (refProp instanceof HTMLElement) {
        return refProp;
    }
    if (typeof refProp === 'function') {
        return refProp();
    }
    return refProp === null || refProp === void 0 ? void 0 : refProp.current;
};
exports.getReferenceElement = getReferenceElement;
/**
 * Gets the [client|offset|scroll]Left property of an element, and determines whether it needs to be
 * adjusted for an RTL direction.
 *
 * @param {HTMLElement} targetElement  - Element to get the inline-start property of.
 * @param {HTMLElement} ancestorElement - Ancestor element to base the inline-start calculation off of when the direction is RTL.
 * @param {'client' | 'offset' | 'scroll'} inlineType - The inline-start property type to base calculations on.
 * @returns {number} - The value of the inline-start property.
 */
const getInlineStartProperty = (targetElement, ancestorElement, inlineType = 'offset') => {
    if (!targetElement) {
        return;
    }
    const inlineProperty = `${inlineType}Left`;
    const isRTL = (0, exports.getLanguageDirection)(targetElement) === 'rtl';
    if (!isRTL) {
        return targetElement[inlineProperty];
    }
    const widthProperty = `${inlineType}Width`;
    return ancestorElement[widthProperty] - (targetElement[inlineProperty] + targetElement[widthProperty]);
};
exports.getInlineStartProperty = getInlineStartProperty;
//# sourceMappingURL=util.js.map