/* *
 *
 *  (c) 2009-2026 Highsoft AS
 *  Author: Øystein Moseng
 *
 *  Utility functions for accessibility module.
 *
 *  Integration of this software requires a license.
 *  - For commercial use, see www.highcharts.com/license
 *  - For non-commercial, see www.highcharts.com/license-eula
 *
 *
 * */
'use strict';
import H from '../../Core/Globals.js';
import { css } from '../../Shared/Utilities.js';
const { doc, win } = H;
/* *
 *
 *  Constants
 *
 * */
const simulatedEventTarget = win.EventTarget && new win.EventTarget() || 'none';
/* *
 *
 *  Functions
 *
 * */
/**
 * @private
 * @param {Highcharts.HTMLDOMElement} el
 * @param {string} className
 * @return {void}
 */
function addClass(el, className) {
    if (el.classList) {
        el.classList.add(className);
    }
    else if (el.className.indexOf(className) < 0) {
        // Note: Dumb check for class name exists, should be fine for practical
        // use cases, but will return false positives if the element has a class
        // that contains the className.
        el.className += ' ' + className;
    }
}
/**
 * @private
 * @param {Highcharts.HTMLDOMElement} el
 * @param {string} className
 * @return {void}
 */
function removeClass(el, className) {
    if (el.classList) {
        el.classList.remove(className);
    }
    else {
        // Note: Dumb logic that will break if the element has a class name that
        // consists of className plus something else.
        el.className = el.className.replace(new RegExp(className, 'g'), '');
    }
}
/**
 * Utility function to clone a mouse event for re-dispatching.
 * @private
 */
function cloneMouseEvent(e) {
    if (typeof win.MouseEvent === 'function') {
        return new win.MouseEvent(e.type, e);
    }
    // No MouseEvent support, try using initMouseEvent
    if (doc?.createEvent) {
        const evt = doc.createEvent('MouseEvent');
        if (evt.initMouseEvent) {
            evt.initMouseEvent(e.type, e.bubbles, // #10561, #12161
            e.cancelable, e.view || win, e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget);
            return evt;
        }
    }
    return getFakeMouseEvent(e.type);
}
/**
 * Utility function to clone a touch event for re-dispatching.
 * @private
 */
function cloneTouchEvent(e) {
    const touchListToTouchArray = (l) => {
        const touchArray = [];
        for (let i = 0; i < l.length; ++i) {
            const item = l.item(i);
            if (item) {
                touchArray.push(item);
            }
        }
        return touchArray;
    };
    if (typeof win.TouchEvent === 'function') {
        const newEvent = new win.TouchEvent(e.type, {
            touches: touchListToTouchArray(e.touches),
            targetTouches: touchListToTouchArray(e.targetTouches),
            changedTouches: touchListToTouchArray(e.changedTouches),
            ctrlKey: e.ctrlKey,
            shiftKey: e.shiftKey,
            altKey: e.altKey,
            metaKey: e.metaKey,
            bubbles: e.bubbles,
            cancelable: e.cancelable,
            composed: e.composed,
            detail: e.detail,
            view: e.view
        });
        if (e.defaultPrevented) {
            newEvent.preventDefault();
        }
        return newEvent;
    }
    const fakeEvt = cloneMouseEvent(e);
    fakeEvt.touches = e.touches;
    fakeEvt.changedTouches = e.changedTouches;
    fakeEvt.targetTouches = e.targetTouches;
    return fakeEvt;
}
/**
 * @private
 */
function escapeStringForHTML(str) {
    return str
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#x27;')
        .replace(/\//g, '&#x2F;');
}
/**
 * Get an element by ID
 * @private
 */
function getElement(id) {
    return doc.getElementById(id);
}
/**
 * Get a fake mouse event of a given type. If relatedTarget is not given,
 * it will point to simulatedEventTarget, as an indicator that the event
 * is fake.
 * @private
 */
function getFakeMouseEvent(type, position, relatedTarget) {
    const pos = position || {
        x: 0,
        y: 0
    };
    if (typeof win.MouseEvent === 'function') {
        return new win.MouseEvent(type, {
            bubbles: true,
            cancelable: true,
            composed: true,
            button: 0,
            buttons: 1,
            relatedTarget: relatedTarget || simulatedEventTarget,
            view: win,
            detail: type === 'click' ? 1 : 0,
            screenX: pos.x,
            screenY: pos.y,
            clientX: pos.x,
            clientY: pos.y
        });
    }
    // No MouseEvent support, try using initMouseEvent
    if (doc?.createEvent) {
        const evt = doc.createEvent('MouseEvent');
        if (evt.initMouseEvent) {
            evt.initMouseEvent(type, true, // Bubble
            true, // Cancel
            win, // View
            type === 'click' ? 1 : 0, // Detail
            // Coords
            pos.x, pos.y, pos.x, pos.y, 
            // Pressed keys
            false, false, false, false, 0, // Button
            null // Related target
            );
            return evt;
        }
    }
    return { type: type };
}
/**
 * Get an appropriate heading level for an element. Corresponds to the
 * heading level below the previous heading in the DOM.
 *
 * Note: Only detects previous headings in the DOM that are siblings,
 * ancestors, or previous siblings of ancestors. Headings that are nested below
 * siblings of ancestors (cousins et.al) are not picked up. This is because it
 * is ambiguous whether or not the nesting is for layout purposes or indicates a
 * separate section.
 *
 * @private
 * @param {Highcharts.HTMLDOMElement} [element]
 * @return {string} The heading tag name (h1, h2 etc).
 * If no nearest heading is found, "p" is returned.
 */
function getHeadingTagNameForElement(element) {
    const getIncreasedHeadingLevel = (tagName) => {
        const headingLevel = parseInt(tagName.slice(1), 10), newLevel = Math.min(6, headingLevel + 1);
        return 'h' + newLevel;
    };
    const isHeading = (tagName) => /^H[1-6]$/i.test(tagName);
    const getPreviousSiblingsHeading = (el) => {
        let sibling = el;
        while (sibling = sibling.previousSibling) { // eslint-disable-line
            const tagName = sibling.tagName || '';
            if (isHeading(tagName)) {
                return tagName;
            }
        }
        return '';
    };
    const getHeadingRecursive = (el) => {
        const prevSiblingsHeading = getPreviousSiblingsHeading(el);
        if (prevSiblingsHeading) {
            return getIncreasedHeadingLevel(prevSiblingsHeading);
        }
        // No previous siblings are headings, try parent node
        const parent = el.parentElement;
        if (!parent) {
            return 'h6';
        }
        const parentTagName = parent.tagName;
        if (isHeading(parentTagName)) {
            return getIncreasedHeadingLevel(parentTagName);
        }
        return getHeadingRecursive(parent);
    };
    return getHeadingRecursive(element);
}
/**
 * Remove an element from the DOM.
 * @private
 * @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} [element]
 * @return {void}
 */
function removeElement(element) {
    if (element && element.parentNode) {
        element.parentNode.removeChild(element);
    }
}
/**
 * Remove all child nodes from an element.
 * @private
 * @param {Highcharts.HTMLDOMElement|Highcharts.SVGDOMElement} [element]
 * @return {void}
 */
function removeChildNodes(element) {
    while (element.lastChild) {
        element.removeChild(element.lastChild);
    }
}
/**
 * Utility function. Reverses child nodes of a DOM element.
 * @private
 */
function reverseChildNodes(node) {
    let i = node.childNodes.length;
    while (i--) {
        node.appendChild(node.childNodes[i]);
    }
}
/**
 * Used for aria-label attributes, painting on a canvas will fail if the
 * text contains tags.
 * @private
 */
function stripHTMLTagsFromString(str, isForExport = false) {
    return (typeof str === 'string') ?
        (isForExport ?
            str.replace(/<\/?[^>]+(>|$)/g, '') :
            str.replace(/<\/?(?!\s)[^>]+(>|$)/g, '')) : str;
}
/**
 * Utility function for hiding an element visually, but still keeping it
 * available to screen reader users.
 * @private
 */
function visuallyHideElement(element) {
    css(element, {
        position: 'absolute',
        width: '1px',
        height: '1px',
        overflow: 'hidden',
        whiteSpace: 'nowrap',
        clip: 'rect(1px, 1px, 1px, 1px)',
        marginTop: '-3px',
        '-ms-filter': 'progid:DXImageTransform.Microsoft.Alpha(Opacity=1)',
        filter: 'alpha(opacity=1)',
        opacity: 0.01
    });
}
/* *
 *
 *  Default Export
 *
 * */
const HTMLUtilities = {
    addClass,
    cloneMouseEvent,
    cloneTouchEvent,
    escapeStringForHTML,
    getElement,
    getFakeMouseEvent,
    getHeadingTagNameForElement,
    removeChildNodes,
    removeClass,
    removeElement,
    reverseChildNodes,
    simulatedEventTarget,
    stripHTMLTagsFromString,
    visuallyHideElement
};
export default HTMLUtilities;
