highcharts
Version:
JavaScript charting framework
320 lines (319 loc) • 9.19 kB
JavaScript
/* *
*
* (c) 2009-2025 Øystein Moseng
*
* Utility functions for accessibility module.
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
;
import H from '../../Core/Globals.js';
const { doc, win } = H;
import U from '../../Core/Utilities.js';
const { css } = U;
/* *
*
* Constants
*
* */
const simulatedEventTarget = win.EventTarget && new win.EventTarget() || 'none';
/* *
*
* Functions
*
* */
/* eslint-disable valid-jsdoc */
/**
* @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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\//g, '/');
}
/**
* 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;