/* *
 *
 *  (c) 2009-2026 Highsoft AS
 *  Author: Øystein Moseng
 *
 *  Proxy elements are used to shadow SVG elements in HTML for assistive
 *  technology, such as screen readers or voice input software.
 *
 *  The ProxyElement class represents such an element, and deals with
 *  overlay positioning and mirroring events for the target.
 *
 *  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';
const { doc, win } = H;
import EventProvider from './Utils/EventProvider.js';
import ChartUtilities from './Utils/ChartUtilities.js';
const { fireEventOnWrappedOrUnwrappedElement } = ChartUtilities;
import HTMLUtilities from './Utils/HTMLUtilities.js';
import { attr, css, merge } from '../Shared/Utilities.js';
const { cloneMouseEvent, cloneTouchEvent, getFakeMouseEvent, removeElement } = HTMLUtilities;
/* *
 *
 *  Class
 *
 * */
/**
 * Represents a proxy element that overlays a target and relays events
 * to its target.
 *
 * @private
 * @class
 */
class ProxyElement {
    /* *
     *
     *  Constructor
     *
     * */
    constructor(chart, target, proxyElementType = 'button', wrapperElementType, attributes) {
        this.chart = chart;
        this.target = target;
        this.eventProvider = new EventProvider();
        const innerEl = this.innerElement =
            doc.createElement(proxyElementType), wrapperEl = this.element = wrapperElementType ?
            doc.createElement(wrapperElementType) : innerEl;
        if (!chart.styledMode) {
            this.hideElementVisually(innerEl);
        }
        if (wrapperElementType) {
            if (wrapperElementType === 'li' && !chart.styledMode) {
                wrapperEl.style.listStyle = 'none';
            }
            wrapperEl.appendChild(innerEl);
            this.element = wrapperEl;
        }
        this.updateTarget(target, attributes);
    }
    /* *
     *
     *  Functions
     *
     * */
    /**
     * Fake a click event on the target.
     */
    click() {
        const pos = this.getTargetPosition();
        pos.x += pos.width / 2;
        pos.y += pos.height / 2;
        const fakeEventObject = getFakeMouseEvent('click', pos);
        fireEventOnWrappedOrUnwrappedElement(this.target.click, fakeEventObject);
    }
    /**
     * Update the target to be proxied. The position and events are updated to
     * match the new target.
     * @param {object} target The new target definition
     * @param {object} attributes New HTML attributes to apply to the proxy. Set an
     * attribute to null to remove.
     */
    updateTarget(target, attributes) {
        this.target = target;
        this.updateCSSClassName();
        const attrs = attributes || {};
        Object.keys(attrs).forEach((a) => {
            if (attrs[a] === null) {
                delete attrs[a];
            }
        });
        const targetAriaLabel = this.getTargetAttr(target.click, 'aria-label');
        attr(this.innerElement, merge(targetAriaLabel ? {
            'aria-label': targetAriaLabel
        } : {}, attrs));
        this.eventProvider.removeAddedEvents();
        this.addProxyEventsToElement(this.innerElement, target.click);
        this.refreshPosition();
    }
    /**
     * Refresh the position of the proxy element to match the current target
     */
    refreshPosition() {
        const bBox = this.getTargetPosition();
        css(this.innerElement, {
            width: (bBox.width || 1) + 'px',
            height: (bBox.height || 1) + 'px',
            left: (Math.round(bBox.x) || 0) + 'px',
            top: (Math.round(bBox.y) || 0) + 'px'
        });
    }
    /**
     * Remove button from DOM, and clear events.
     */
    remove() {
        this.eventProvider.removeAddedEvents();
        removeElement(this.element);
    }
    // -------------------------- private ------------------------------------
    /**
     * Update the CSS class name to match target
     */
    updateCSSClassName() {
        const stringHasNoTooltip = (s) => (s.indexOf('highcharts-no-tooltip') > -1);
        const noTooltipOnGroup = stringHasNoTooltip(this.chart.legend?.group?.div?.className || '');
        const targetClassName = this.getTargetAttr(this.target.click, 'class') || '';
        const noTooltipOnTarget = stringHasNoTooltip(targetClassName);
        this.innerElement.className = noTooltipOnGroup || noTooltipOnTarget ?
            'highcharts-a11y-proxy-element highcharts-no-tooltip' :
            'highcharts-a11y-proxy-element';
    }
    /**
     * Mirror events for a proxy element to a target
     */
    addProxyEventsToElement(element, target) {
        [
            'click', 'touchstart', 'touchend', 'touchcancel', 'touchmove',
            'mouseover', 'mouseenter', 'mouseleave', 'mouseout'
        ].forEach((evtType) => {
            const isTouchEvent = evtType.indexOf('touch') === 0;
            this.eventProvider.addEvent(element, evtType, (e) => {
                const clonedEvent = isTouchEvent ?
                    cloneTouchEvent(e) :
                    cloneMouseEvent(e);
                if (target) {
                    fireEventOnWrappedOrUnwrappedElement(target, clonedEvent);
                }
                e.stopPropagation();
                // #9682, #15318: Touch scrolling didn't work when touching
                // proxy
                if (!isTouchEvent) {
                    e.preventDefault();
                }
            }, { passive: false });
        });
    }
    /**
     * Set visually hidden style on a proxy element
     */
    hideElementVisually(el) {
        css(el, {
            borderWidth: 0,
            backgroundColor: 'transparent',
            cursor: 'pointer',
            outline: 'none',
            opacity: 0.001,
            filter: 'alpha(opacity=1)',
            zIndex: 999,
            overflow: 'hidden',
            padding: 0,
            margin: 0,
            display: 'block',
            position: 'absolute',
            'pointer-events': 'none',
            '-ms-filter': 'progid:DXImageTransform.Microsoft.Alpha(Opacity=1)'
        });
    }
    /**
     * Get the position relative to chart container for the target
     */
    getTargetPosition() {
        const clickTarget = this.target.click;
        // We accept both DOM elements and wrapped elements as click targets.
        const clickTargetElement = clickTarget.element ?
            clickTarget.element :
            clickTarget;
        const posElement = this.target.visual || clickTargetElement;
        const chartDiv = this.chart.renderTo, pointer = this.chart.pointer;
        if (chartDiv && posElement?.getBoundingClientRect && pointer) {
            const scrollTop = win.scrollY ||
                doc.documentElement.scrollTop, rectEl = posElement.getBoundingClientRect(), chartPos = pointer.getChartPosition();
            return {
                x: (rectEl.left - chartPos.left) / chartPos.scaleX,
                // #21994, Add scroll position as "getBoundingClientRect"
                // returns the position from the viewport, not the document top.
                y: ((rectEl.top + scrollTop) - chartPos.top) / chartPos.scaleY,
                width: rectEl.right / chartPos.scaleX -
                    rectEl.left / chartPos.scaleX,
                height: rectEl.bottom / chartPos.scaleY -
                    rectEl.top / chartPos.scaleY
            };
        }
        return { x: 0, y: 0, width: 1, height: 1 };
    }
    /**
     * Get an attribute value of a target
     */
    getTargetAttr(target, key) {
        if (target.element) {
            return target.element.getAttribute(key);
        }
        return target.getAttribute(key);
    }
}
/* *
 *
 *  Default Export
 *
 * */
export default ProxyElement;
