221 lines
7.4 KiB
JavaScript
221 lines
7.4 KiB
JavaScript
|
/* *
|
||
|
*
|
||
|
* (c) 2009-2024 Ø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.
|
||
|
*
|
||
|
* License: www.highcharts.com/license
|
||
|
*
|
||
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||
|
*
|
||
|
* */
|
||
|
'use strict';
|
||
|
import H from '../Core/Globals.js';
|
||
|
const { doc } = H;
|
||
|
import U from '../Core/Utilities.js';
|
||
|
const { attr, css, merge } = U;
|
||
|
import EventProvider from './Utils/EventProvider.js';
|
||
|
import ChartUtilities from './Utils/ChartUtilities.js';
|
||
|
const { fireEventOnWrappedOrUnwrappedElement } = ChartUtilities;
|
||
|
import HTMLUtilities from './Utils/HTMLUtilities.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 target The new target definition
|
||
|
* @param 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 legend = this.chart.legend;
|
||
|
const groupDiv = legend.group && legend.group.div;
|
||
|
const noTooltipOnGroup = stringHasNoTooltip(groupDiv && groupDiv.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',
|
||
|
'-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 rectEl = posElement.getBoundingClientRect(), chartPos = pointer.getChartPosition();
|
||
|
return {
|
||
|
x: (rectEl.left - chartPos.left) / chartPos.scaleX,
|
||
|
y: (rectEl.top - 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;
|