@highcharts/dashboards
Version:
Highcharts Dashboards framework
452 lines (451 loc) • 13.8 kB
JavaScript
/* *
*
* (c) 2009-2025 Highsoft AS
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* Authors:
* - Sebastian Bochan
* - Wojciech Chmiel
* - Gøran Slettemark
* - Sophie Bremer
*
* */
;
/* *
*
* Imports
*
* */
import D from './Globals.js';
const { doc, supportsPassiveEvents } = D;
import U from '../Core/Utilities.js';
const { error: coreError, isClass, isDOMElement, isObject, objectEach, uniqueKey: coreUniqueKey } = U;
/* *
*
* Functions
*
* */
/**
* Add an event listener.
*
* @function Highcharts.addEvent<T>
*
* @param {D.Class<T>|T} el
* The element or object to add a listener to. It can be a
* {@link HTMLDOMElement}, an {@link SVGElement} or any other object.
*
* @param {string} type
* The event type.
*
* @param {Dashboards.EventCallbackFunction<T>|Function} fn
* The function callback to execute when the event is fired.
*
* @param {Dashboards.EventOptionsObject} [options]
* Options for adding the event.
*
* @return {Function}
* A callback function to remove the added event.
*/
function addEvent(el, type, fn, options = {}) {
/* eslint-enable valid-jsdoc */
// Add hcEvents to either the prototype (in case we're running addEvent on a
// class) or the instance. If hasOwnProperty('hcEvents') is false, it is
// inherited down the prototype chain, in which case we need to set the
// property on this instance (which may itself be a prototype).
const owner = typeof el === 'function' && el.prototype || el;
if (!Object.hasOwnProperty.call(owner, 'hcEvents')) {
owner.hcEvents = {};
}
const events = owner.hcEvents;
// Handle DOM events
// If the browser supports passive events, add it to improve performance
// on touch events (#11353).
const addEventListener = el.addEventListener;
if (addEventListener) {
addEventListener.call(el, type, fn, supportsPassiveEvents ? {
passive: options.passive === void 0 ?
type.indexOf('touch') !== -1 : options.passive,
capture: false
} : false);
}
if (!events[type]) {
events[type] = [];
}
const eventObject = {
fn,
order: typeof options.order === 'number' ? options.order : Infinity
};
events[type].push(eventObject);
// Order the calls
events[type].sort((a, b) => a.order - b.order);
// Return a function that can be called to remove this event.
return function () {
removeEvent(el, type, fn);
};
}
/**
* Utility function to deep merge two or more objects and return a third object.
* If the first argument is true, the contents of the second object is copied
* into the first object. The merge function can also be used with a single
* object argument to create a deep copy of an object.
*
* @function Highcharts.merge<T>
*
* @param {boolean} extend
* Whether to extend the left-side object (a) or return a whole new
* object.
*
* @param {T|undefined} a
* The first object to extend. When only this is given, the function
* returns a deep copy.
*
* @param {...Array<object|undefined>} [n]
* An object to merge into the previous one.
*
* @return {T}
* The merged object. If the first argument is true, the return is the
* same as the second argument.
*/ /**
* Utility function to deep merge two or more objects and return a third object.
* The merge function can also be used with a single object argument to create a
* deep copy of an object.
*
* @function Highcharts.merge<T>
*
* @param {T|undefined} a
* The first object to extend. When only this is given, the function
* returns a deep copy.
*
* @param {...Array<object|undefined>} [n]
* An object to merge into the previous one.
*
* @return {T}
* The merged object. If the first argument is true, the return is the
* same as the second argument.
*/
function merge(a, ...n) {
let copyDepth = 0, obj = {};
// Descriptive error stack:
const copyDepthError = new Error('Recursive copy depth > 100'), doCopy = (copy, original) => {
// An object is replacing a primitive
if (typeof copy !== 'object') {
copy = {};
}
if (++copyDepth > 100) {
throw copyDepthError;
}
objectEach(original, (value, key) => {
// Prototype pollution (#14883)
if (key === '__proto__' || key === 'constructor') {
return;
}
// Copy the contents of objects, but not arrays or DOM nodes
if (isObject(value, true) &&
!isClass(value) &&
!isDOMElement(value)) {
copy[key] = doCopy(copy[key] || {}, value);
// Primitives and arrays are copied over directly
}
else {
copy[key] = original[key];
}
});
--copyDepth;
return copy;
};
// If first argument is true, copy into the existing object. Used in
// setOptions.
if (a === true) {
obj = n.shift();
}
else {
n.unshift(a);
}
// For each argument, extend the return
for (let i = 0, iEnd = n.length; i < iEnd; ++i) {
obj = doCopy(obj, n[i]);
}
return obj;
}
/**
* Returns a deep copy of an argument. It differs from `merge` in that it copies
* also arrays.
*
* @param value
* The value to clone.
*
* @param excludedKeys
* An array of keys to exclude from the clone.
*/
function deepClone(value, excludedKeys) {
if (Array.isArray(value)) {
return value.map((v) => deepClone(v, excludedKeys));
}
if (value && typeof value === 'object') {
const clone = {};
const keys = Object.keys(value);
for (const key of keys) {
if (excludedKeys && excludedKeys.includes(key)) {
clone[key] = value[key];
}
else {
clone[key] = deepClone(value[key], excludedKeys);
}
}
return clone;
}
return value;
}
/**
* Creates a session-dependent unique key string for reference purposes.
*
* @function Dashboards.uniqueKey
*
* @return {string}
* Unique key string
*/
function uniqueKey() {
return `dashboard-${coreUniqueKey().replace('highcharts-', '')}`;
}
/**
* Provide error messages for debugging, with links to online explanation. This
* function can be overridden to provide custom error handling.
*
* @sample highcharts/chart/highcharts-error/
* Custom error handler
*
* @function Dashboards.error
*
* @param {number|string} code
* The error code. See
* [errors.xml](https://github.com/highcharts/highcharts/blob/master/errors/errors.xml)
* for available codes. If it is a string, the error message is printed
* directly in the console.
*
* @param {boolean} [stop=false]
* Whether to throw an error or just log a warning in the console.
*
* @return {void}
*/
function error(code, stop) {
// TODO- replace with proper error handling
if (code === 16) {
console.warn(// eslint-disable-line no-console
'Dashboard error: Dashboards library loaded more than once.' +
'This may cause undefined behavior.');
return;
}
coreError(code, stop);
}
/**
* Utility function to extend an object with the members of another.
*
* @function Dashboards.extend<T>
*
* @param {T|undefined} a
* The object to be extended.
*
* @param {Partial<T>} b
* The object to add to the first one.
*
* @return {T}
* Object a, the original object.
*/
function extend(a, b) {
/* eslint-enable valid-jsdoc */
let n;
if (!a) {
a = {};
}
for (n in b) { // eslint-disable-line guard-for-in
a[n] = b[n];
}
return a;
}
/**
* Fire an event that was registered with addEvent.
*
* @function Highcharts.fireEvent<T>
*
* @param {T} el
* The object to fire the event on. It can be a {@link HTMLDOMElement},
* an {@link SVGElement} or any other object.
*
* @param {string} type
* The type of event.
*
* @param {Dashboards.Dictionary<*>|Event} [eventArguments]
* Custom event arguments that are passed on as an argument to the event
* handler.
*
* @param {Dashboards.EventCallbackFunction<T>|Function} [defaultFunction]
* The default function to execute if the other listeners haven't
* returned false.
*
* @return {void}
*/
function fireEvent(el, type, eventArguments, defaultFunction) {
/* eslint-enable valid-jsdoc */
eventArguments = eventArguments || {};
if (doc?.createEvent &&
(el.dispatchEvent ||
(el.fireEvent &&
// Enable firing events on Highcharts instance.
el !== D))) {
const e = doc.createEvent('Events');
e.initEvent(type, true, true);
eventArguments = extend(e, eventArguments);
if (el.dispatchEvent) {
el.dispatchEvent(eventArguments);
}
else {
el.fireEvent(type, eventArguments);
}
}
else if (el.hcEvents) {
if (!eventArguments.target) {
// We're running a custom event
extend(eventArguments, {
// Attach a simple preventDefault function to skip
// default handler if called. The built-in
// defaultPrevented property is not overwritable (#5112)
preventDefault: function () {
eventArguments.defaultPrevented = true;
},
// Setting target to native events fails with clicking
// the zoom-out button in Chrome.
target: el,
// If the type is not set, we're running a custom event
// (#2297). If it is set, we're running a browser event.
type: type
});
}
const events = [];
let object = el;
let multilevel = false;
// Recurse up the inheritance chain and collect hcEvents set as own
// objects on the prototypes.
while (object.hcEvents) {
if (Object.hasOwnProperty.call(object, 'hcEvents') &&
object.hcEvents[type]) {
if (events.length) {
multilevel = true;
}
events.unshift.apply(events, object.hcEvents[type]);
}
object = Object.getPrototypeOf(object);
}
// For performance reasons, only sort the event handlers in case we are
// dealing with multiple levels in the prototype chain. Otherwise, the
// events are already sorted in the addEvent function.
if (multilevel) {
// Order the calls
events.sort((a, b) => a.order - b.order);
}
// Call the collected event handlers
events.forEach((obj) => {
// If the event handler returns false, prevent the default handler
// from executing
if (obj.fn.call(el, eventArguments) === false) {
eventArguments.preventDefault();
}
});
}
// Run the default if not prevented
if (defaultFunction && !eventArguments.defaultPrevented) {
defaultFunction.call(el, eventArguments);
}
}
/**
* Remove an event that was added with {@link Highcharts#addEvent}.
*
* @function Dashboards.removeEvent<T>
*
* @param {Dashboards.Class<T>|T} el
* The element to remove events on.
*
* @param {string} [type]
* The type of events to remove. If undefined, all events are removed
* from the element.
*
* @param {Dashboards.EventCallbackFunction<T>} [fn]
* The specific callback to remove. If undefined, all events that match
* the element and optionally the type are removed.
*
* @return {void}
*/
function removeEvent(el, type, fn) {
/* eslint-enable valid-jsdoc */
/**
* @private
*/
function removeOneEvent(type, fn) {
const removeEventListener = el.removeEventListener;
if (removeEventListener) {
removeEventListener.call(el, type, fn, false);
}
}
/**
* @private
*/
function removeAllEvents(eventCollection) {
let types, len;
if (!el.nodeName) {
return; // Break on non-DOM events
}
if (type) {
types = {};
types[type] = true;
}
else {
types = eventCollection;
}
objectEach(types, function (_val, n) {
if (eventCollection[n]) {
len = eventCollection[n].length;
while (len--) {
removeOneEvent(n, eventCollection[n][len].fn);
}
}
});
}
const owner = typeof el === 'function' && el.prototype || el;
if (Object.hasOwnProperty.call(owner, 'hcEvents')) {
const events = owner.hcEvents;
if (type) {
const typeEvents = (events[type] || []);
if (fn) {
events[type] = typeEvents.filter(function (obj) {
return fn !== obj.fn;
});
removeOneEvent(type, fn);
}
else {
removeAllEvents(events);
events[type] = [];
}
}
else {
removeAllEvents(events);
delete owner.hcEvents;
}
}
}
/* *
*
* Default Export
*
* */
const Utilities = {
addEvent,
deepClone,
error,
fireEvent,
merge,
removeEvent,
uniqueKey
};
export default Utilities;