vis-util
Version:
utilitie collection for visjs
1,542 lines (1,513 loc) • 120 kB
JavaScript
/**
* vis-util
* https://github.com/visjs/vis-util
*
* utilitie collection for visjs
*
* @version 6.0.0
* @date 2025-07-12T18:02:43.836Z
*
* @copyright (c) 2011-2017 Almende B.V, http://almende.com
* @copyright (c) 2017-2019 visjs contributors, https://github.com/visjs
*
* @license
* vis.js is dual licensed under both
*
* 1. The Apache 2.0 License
* http://www.apache.org/licenses/LICENSE-2.0
*
* and
*
* 2. The MIT License
* http://opensource.org/licenses/MIT
*
* vis.js may be distributed under either license.
*/
import Emitter from 'component-emitter';
import RealHammer from '@egjs/hammerjs';
/**
* Use this symbol to delete properies in deepObjectAssign.
*/
const DELETE = Symbol("DELETE");
/**
* Pure version of deepObjectAssign, it doesn't modify any of it's arguments.
* @param base - The base object that fullfils the whole interface T.
* @param updates - Updates that may change or delete props.
* @returns A brand new instance with all the supplied objects deeply merged.
*/
function pureDeepObjectAssign(base, ...updates) {
return deepObjectAssign({}, base, ...updates);
}
/**
* Deep version of object assign with additional deleting by the DELETE symbol.
* @param values - Objects to be deeply merged.
* @returns The first object from values.
*/
function deepObjectAssign(...values) {
const merged = deepObjectAssignNonentry(...values);
stripDelete(merged);
return merged;
}
/**
* Deep version of object assign with additional deleting by the DELETE symbol.
* @remarks
* This doesn't strip the DELETE symbols so they may end up in the final object.
* @param values - Objects to be deeply merged.
* @returns The first object from values.
*/
function deepObjectAssignNonentry(...values) {
if (values.length < 2) {
return values[0];
}
else if (values.length > 2) {
return deepObjectAssignNonentry(deepObjectAssign(values[0], values[1]), ...values.slice(2));
}
const a = values[0];
const b = values[1];
if (a instanceof Date && b instanceof Date) {
a.setTime(b.getTime());
return a;
}
for (const prop of Reflect.ownKeys(b)) {
if (!Object.prototype.propertyIsEnumerable.call(b, prop)) ;
else if (b[prop] === DELETE) {
delete a[prop];
}
else if (a[prop] !== null &&
b[prop] !== null &&
typeof a[prop] === "object" &&
typeof b[prop] === "object" &&
!Array.isArray(a[prop]) &&
!Array.isArray(b[prop])) {
a[prop] = deepObjectAssignNonentry(a[prop], b[prop]);
}
else {
a[prop] = clone(b[prop]);
}
}
return a;
}
/**
* Deep clone given object or array. In case of primitive simply return.
* @param a - Anything.
* @returns Deep cloned object/array or unchanged a.
*/
function clone(a) {
if (Array.isArray(a)) {
return a.map((value) => clone(value));
}
else if (typeof a === "object" && a !== null) {
if (a instanceof Date) {
return new Date(a.getTime());
}
return deepObjectAssignNonentry({}, a);
}
else {
return a;
}
}
/**
* Strip DELETE from given object.
* @param a - Object which may contain DELETE but won't after this is executed.
*/
function stripDelete(a) {
for (const prop of Object.keys(a)) {
if (a[prop] === DELETE) {
delete a[prop];
}
else if (typeof a[prop] === "object" && a[prop] !== null) {
stripDelete(a[prop]);
}
}
}
/**
* Seedable, fast and reasonably good (not crypto but more than okay for our
* needs) random number generator.
* @remarks
* Adapted from {@link https://web.archive.org/web/20110429100736/http://baagoe.com:80/en/RandomMusings/javascript}.
* Original algorithm created by Johannes Baagøe \<baagoe\@baagoe.com\> in 2010.
*/
/**
* Create a seeded pseudo random generator based on Alea by Johannes Baagøe.
* @param seed - All supplied arguments will be used as a seed. In case nothing
* is supplied the current time will be used to seed the generator.
* @returns A ready to use seeded generator.
*/
function Alea(...seed) {
return AleaImplementation(seed.length ? seed : [Date.now()]);
}
/**
* An implementation of [[Alea]] without user input validation.
* @param seed - The data that will be used to seed the generator.
* @returns A ready to use seeded generator.
*/
function AleaImplementation(seed) {
let [s0, s1, s2] = mashSeed(seed);
let c = 1;
const random = () => {
const t = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32
s0 = s1;
s1 = s2;
return (s2 = t - (c = t | 0));
};
random.uint32 = () => random() * 0x100000000; // 2^32
random.fract53 = () => random() + ((random() * 0x200000) | 0) * 1.1102230246251565e-16; // 2^-53
random.algorithm = "Alea";
random.seed = seed;
random.version = "0.9";
return random;
}
/**
* Turn arbitrary data into values [[AleaImplementation]] can use to generate
* random numbers.
* @param seed - Arbitrary data that will be used as the seed.
* @returns Three numbers to use as initial values for [[AleaImplementation]].
*/
function mashSeed(...seed) {
const mash = Mash();
let s0 = mash(" ");
let s1 = mash(" ");
let s2 = mash(" ");
for (let i = 0; i < seed.length; i++) {
s0 -= mash(seed[i]);
if (s0 < 0) {
s0 += 1;
}
s1 -= mash(seed[i]);
if (s1 < 0) {
s1 += 1;
}
s2 -= mash(seed[i]);
if (s2 < 0) {
s2 += 1;
}
}
return [s0, s1, s2];
}
/**
* Create a new mash function.
* @returns A nonpure function that takes arbitrary [[Mashable]] data and turns
* them into numbers.
*/
function Mash() {
let n = 0xefc8249d;
return function (data) {
const string = data.toString();
for (let i = 0; i < string.length; i++) {
n += string.charCodeAt(i);
let h = 0.02519603282416938 * n;
n = h >>> 0;
h -= n;
h *= n;
n = h >>> 0;
h -= n;
n += h * 0x100000000; // 2^32
}
return (n >>> 0) * 2.3283064365386963e-10; // 2^-32
};
}
/**
* Setup a mock hammer.js object, for unit testing.
*
* Inspiration: https://github.com/uber/deck.gl/pull/658
* @returns {{on: noop, off: noop, destroy: noop, emit: noop, get: get}}
*/
function hammerMock() {
const noop = () => {};
return {
on: noop,
off: noop,
destroy: noop,
emit: noop,
get() {
return {
set: noop,
};
},
};
}
const Hammer$1 =
typeof window !== "undefined"
? window.Hammer || RealHammer
: function () {
// hammer.js is only available in a browser, not in node.js. Replacing it with a mock object.
return hammerMock();
};
/**
* Turn an element into an clickToUse element.
* When not active, the element has a transparent overlay. When the overlay is
* clicked, the mode is changed to active.
* When active, the element is displayed with a blue border around it, and
* the interactive contents of the element can be used. When clicked outside
* the element, the elements mode is changed to inactive.
* @param {Element} container
* @class Activator
*/
function Activator$1(container) {
this._cleanupQueue = [];
this.active = false;
this._dom = {
container,
overlay: document.createElement("div"),
};
this._dom.overlay.classList.add("vis-overlay");
this._dom.container.appendChild(this._dom.overlay);
this._cleanupQueue.push(() => {
this._dom.overlay.parentNode.removeChild(this._dom.overlay);
});
const hammer = Hammer$1(this._dom.overlay);
hammer.on("tap", this._onTapOverlay.bind(this));
this._cleanupQueue.push(() => {
hammer.destroy();
// FIXME: cleaning up hammer instances doesn't work (Timeline not removed
// from memory)
});
// block all touch events (except tap)
const events = [
"tap",
"doubletap",
"press",
"pinch",
"pan",
"panstart",
"panmove",
"panend",
];
events.forEach((event) => {
hammer.on(event, (event) => {
event.srcEvent.stopPropagation();
});
});
// attach a click event to the window, in order to deactivate when clicking outside the timeline
if (document && document.body) {
this._onClick = (event) => {
if (!_hasParent(event.target, container)) {
this.deactivate();
}
};
document.body.addEventListener("click", this._onClick);
this._cleanupQueue.push(() => {
document.body.removeEventListener("click", this._onClick);
});
}
// prepare escape key listener for deactivating when active
this._escListener = (event) => {
if (
"key" in event
? event.key === "Escape"
: event.keyCode === 27 /* the keyCode is for IE11 */
) {
this.deactivate();
}
};
}
// turn into an event emitter
Emitter(Activator$1.prototype);
// The currently active activator
Activator$1.current = null;
/**
* Destroy the activator. Cleans up all created DOM and event listeners
*/
Activator$1.prototype.destroy = function () {
this.deactivate();
for (const callback of this._cleanupQueue.splice(0).reverse()) {
callback();
}
};
/**
* Activate the element
* Overlay is hidden, element is decorated with a blue shadow border
*/
Activator$1.prototype.activate = function () {
// we allow only one active activator at a time
if (Activator$1.current) {
Activator$1.current.deactivate();
}
Activator$1.current = this;
this.active = true;
this._dom.overlay.style.display = "none";
this._dom.container.classList.add("vis-active");
this.emit("change");
this.emit("activate");
// ugly hack: bind ESC after emitting the events, as the Network rebinds all
// keyboard events on a 'change' event
document.body.addEventListener("keydown", this._escListener);
};
/**
* Deactivate the element
* Overlay is displayed on top of the element
*/
Activator$1.prototype.deactivate = function () {
this.active = false;
this._dom.overlay.style.display = "block";
this._dom.container.classList.remove("vis-active");
document.body.removeEventListener("keydown", this._escListener);
this.emit("change");
this.emit("deactivate");
};
/**
* Handle a tap event: activate the container
* @param {Event} event The event
* @private
*/
Activator$1.prototype._onTapOverlay = function (event) {
// activate the container
this.activate();
event.srcEvent.stopPropagation();
};
/**
* Test whether the element has the requested parent element somewhere in
* its chain of parent nodes.
* @param {HTMLElement} element
* @param {HTMLElement} parent
* @returns {boolean} Returns true when the parent is found somewhere in the
* chain of parent nodes.
* @private
*/
function _hasParent(element, parent) {
while (element) {
if (element === parent) {
return true;
}
element = element.parentNode;
}
return false;
}
// utility functions
// parse ASP.Net Date pattern,
// for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
// code from http://momentjs.com/
const ASPDateRegex = /^\/?Date\((-?\d+)/i;
// Color REs
const fullHexRE = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i;
const shortHexRE = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
const rgbRE = /^rgb\( *(1?\d{1,2}|2[0-4]\d|25[0-5]) *, *(1?\d{1,2}|2[0-4]\d|25[0-5]) *, *(1?\d{1,2}|2[0-4]\d|25[0-5]) *\)$/i;
const rgbaRE = /^rgba\( *(1?\d{1,2}|2[0-4]\d|25[0-5]) *, *(1?\d{1,2}|2[0-4]\d|25[0-5]) *, *(1?\d{1,2}|2[0-4]\d|25[0-5]) *, *([01]|0?\.\d+) *\)$/i;
/**
* Test whether given object is a number.
* @param value - Input value of unknown type.
* @returns True if number, false otherwise.
*/
function isNumber(value) {
return value instanceof Number || typeof value === "number";
}
/**
* Remove everything in the DOM object.
* @param DOMobject - Node whose child nodes will be recursively deleted.
*/
function recursiveDOMDelete(DOMobject) {
if (DOMobject) {
while (DOMobject.hasChildNodes() === true) {
const child = DOMobject.firstChild;
if (child) {
recursiveDOMDelete(child);
DOMobject.removeChild(child);
}
}
}
}
/**
* Test whether given object is a string.
* @param value - Input value of unknown type.
* @returns True if string, false otherwise.
*/
function isString(value) {
return value instanceof String || typeof value === "string";
}
/**
* Test whether given object is a object (not primitive or null).
* @param value - Input value of unknown type.
* @returns True if not null object, false otherwise.
*/
function isObject(value) {
return typeof value === "object" && value !== null;
}
/**
* Test whether given object is a Date, or a String containing a Date.
* @param value - Input value of unknown type.
* @returns True if Date instance or string date representation, false otherwise.
*/
function isDate(value) {
if (value instanceof Date) {
return true;
}
else if (isString(value)) {
// test whether this string contains a date
const match = ASPDateRegex.exec(value);
if (match) {
return true;
}
else if (!isNaN(Date.parse(value))) {
return true;
}
}
return false;
}
/**
* Copy property from b to a if property present in a.
* If property in b explicitly set to null, delete it if `allowDeletion` set.
*
* Internal helper routine, should not be exported. Not added to `exports` for that reason.
* @param a - Target object.
* @param b - Source object.
* @param prop - Name of property to copy from b to a.
* @param allowDeletion - If true, delete property in a if explicitly set to null in b.
*/
function copyOrDelete(a, b, prop, allowDeletion) {
let doDeletion = false;
if (allowDeletion === true) {
doDeletion = b[prop] === null && a[prop] !== undefined;
}
if (doDeletion) {
delete a[prop];
}
else {
a[prop] = b[prop]; // Remember, this is a reference copy!
}
}
/**
* Fill an object with a possibly partially defined other object.
*
* Only copies values for the properties already present in a.
* That means an object is not created on a property if only the b object has it.
* @param a - The object that will have it's properties updated.
* @param b - The object with property updates.
* @param allowDeletion - If true, delete properties in a that are explicitly set to null in b.
*/
function fillIfDefined(a, b, allowDeletion = false) {
// NOTE: iteration of properties of a
// NOTE: prototype properties iterated over as well
for (const prop in a) {
if (b[prop] !== undefined) {
if (b[prop] === null || typeof b[prop] !== "object") {
// Note: typeof null === 'object'
copyOrDelete(a, b, prop, allowDeletion);
}
else {
const aProp = a[prop];
const bProp = b[prop];
if (isObject(aProp) && isObject(bProp)) {
fillIfDefined(aProp, bProp, allowDeletion);
}
}
}
}
}
/**
* Copy the values of all of the enumerable own properties from one or more source objects to a
* target object. Returns the target object.
* @param target - The target object to copy to.
* @param source - The source object from which to copy properties.
* @returns The target object.
*/
const extend = Object.assign;
/**
* Extend object a with selected properties of object b or a series of objects.
* @remarks
* Only properties with defined values are copied.
* @param props - Properties to be copied to a.
* @param a - The target.
* @param others - The sources.
* @returns Argument a.
*/
function selectiveExtend(props, a, ...others) {
if (!Array.isArray(props)) {
throw new Error("Array with property names expected as first argument");
}
for (const other of others) {
for (let p = 0; p < props.length; p++) {
const prop = props[p];
if (other && Object.prototype.hasOwnProperty.call(other, prop)) {
a[prop] = other[prop];
}
}
}
return a;
}
/**
* Extend object a with selected properties of object b.
* Only properties with defined values are copied.
* @remarks
* Previous version of this routine implied that multiple source objects could
* be used; however, the implementation was **wrong**. Since multiple (\>1)
* sources weren't used anywhere in the `vis.js` code, this has been removed
* @param props - Names of first-level properties to copy over.
* @param a - Target object.
* @param b - Source object.
* @param allowDeletion - If true, delete property in a if explicitly set to null in b.
* @returns Argument a.
*/
function selectiveDeepExtend(props, a, b, allowDeletion = false) {
// TODO: add support for Arrays to deepExtend
if (Array.isArray(b)) {
throw new TypeError("Arrays are not supported by deepExtend");
}
for (let p = 0; p < props.length; p++) {
const prop = props[p];
if (Object.prototype.hasOwnProperty.call(b, prop)) {
if (b[prop] && b[prop].constructor === Object) {
if (a[prop] === undefined) {
a[prop] = {};
}
if (a[prop].constructor === Object) {
deepExtend(a[prop], b[prop], false, allowDeletion);
}
else {
copyOrDelete(a, b, prop, allowDeletion);
}
}
else if (Array.isArray(b[prop])) {
throw new TypeError("Arrays are not supported by deepExtend");
}
else {
copyOrDelete(a, b, prop, allowDeletion);
}
}
}
return a;
}
/**
* Extend object `a` with properties of object `b`, ignoring properties which
* are explicitly specified to be excluded.
* @remarks
* The properties of `b` are considered for copying. Properties which are
* themselves objects are are also extended. Only properties with defined
* values are copied.
* @param propsToExclude - Names of properties which should *not* be copied.
* @param a - Object to extend.
* @param b - Object to take properties from for extension.
* @param allowDeletion - If true, delete properties in a that are explicitly
* set to null in b.
* @returns Argument a.
*/
function selectiveNotDeepExtend(propsToExclude, a, b, allowDeletion = false) {
// TODO: add support for Arrays to deepExtend
// NOTE: array properties have an else-below; apparently, there is a problem here.
if (Array.isArray(b)) {
throw new TypeError("Arrays are not supported by deepExtend");
}
for (const prop in b) {
if (!Object.prototype.hasOwnProperty.call(b, prop)) {
continue;
} // Handle local properties only
if (propsToExclude.includes(prop)) {
continue;
} // In exclusion list, skip
if (b[prop] && b[prop].constructor === Object) {
if (a[prop] === undefined) {
a[prop] = {};
}
if (a[prop].constructor === Object) {
deepExtend(a[prop], b[prop]); // NOTE: allowDeletion not propagated!
}
else {
copyOrDelete(a, b, prop, allowDeletion);
}
}
else if (Array.isArray(b[prop])) {
a[prop] = [];
for (let i = 0; i < b[prop].length; i++) {
a[prop].push(b[prop][i]);
}
}
else {
copyOrDelete(a, b, prop, allowDeletion);
}
}
return a;
}
/**
* Deep extend an object a with the properties of object b.
* @param a - Target object.
* @param b - Source object.
* @param protoExtend - If true, the prototype values will also be extended.
* (That is the options objects that inherit from others will also get the
* inherited options).
* @param allowDeletion - If true, the values of fields that are null will be deleted.
* @returns Argument a.
*/
function deepExtend(a, b, protoExtend = false, allowDeletion = false) {
for (const prop in b) {
if (Object.prototype.hasOwnProperty.call(b, prop) || protoExtend === true) {
if (typeof b[prop] === "object" &&
b[prop] !== null &&
Object.getPrototypeOf(b[prop]) === Object.prototype) {
if (a[prop] === undefined) {
a[prop] = deepExtend({}, b[prop], protoExtend); // NOTE: allowDeletion not propagated!
}
else if (typeof a[prop] === "object" &&
a[prop] !== null &&
Object.getPrototypeOf(a[prop]) === Object.prototype) {
deepExtend(a[prop], b[prop], protoExtend); // NOTE: allowDeletion not propagated!
}
else {
copyOrDelete(a, b, prop, allowDeletion);
}
}
else if (Array.isArray(b[prop])) {
a[prop] = b[prop].slice();
}
else {
copyOrDelete(a, b, prop, allowDeletion);
}
}
}
return a;
}
/**
* Test whether all elements in two arrays are equal.
* @param a - First array.
* @param b - Second array.
* @returns True if both arrays have the same length and same elements (1 = '1').
*/
function equalArray(a, b) {
if (a.length !== b.length) {
return false;
}
for (let i = 0, len = a.length; i < len; i++) {
if (a[i] != b[i]) {
return false;
}
}
return true;
}
/**
* Get the type of an object, for example exports.getType([]) returns 'Array'.
* @param object - Input value of unknown type.
* @returns Detected type.
*/
function getType(object) {
const type = typeof object;
if (type === "object") {
if (object === null) {
return "null";
}
if (object instanceof Boolean) {
return "Boolean";
}
if (object instanceof Number) {
return "Number";
}
if (object instanceof String) {
return "String";
}
if (Array.isArray(object)) {
return "Array";
}
if (object instanceof Date) {
return "Date";
}
return "Object";
}
if (type === "number") {
return "Number";
}
if (type === "boolean") {
return "Boolean";
}
if (type === "string") {
return "String";
}
if (type === undefined) {
return "undefined";
}
return type;
}
/**
* Used to extend an array and copy it. This is used to propagate paths recursively.
* @param arr - First part.
* @param newValue - The value to be aadded into the array.
* @returns A new array with all items from arr and newValue (which is last).
*/
function copyAndExtendArray(arr, newValue) {
return [...arr, newValue];
}
/**
* Used to extend an array and copy it. This is used to propagate paths recursively.
* @param arr - The array to be copied.
* @returns Shallow copy of arr.
*/
function copyArray(arr) {
return arr.slice();
}
/**
* Retrieve the absolute left value of a DOM element.
* @param elem - A dom element, for example a div.
* @returns The absolute left position of this element in the browser page.
*/
function getAbsoluteLeft(elem) {
return elem.getBoundingClientRect().left;
}
/**
* Retrieve the absolute right value of a DOM element.
* @param elem - A dom element, for example a div.
* @returns The absolute right position of this element in the browser page.
*/
function getAbsoluteRight(elem) {
return elem.getBoundingClientRect().right;
}
/**
* Retrieve the absolute top value of a DOM element.
* @param elem - A dom element, for example a div.
* @returns The absolute top position of this element in the browser page.
*/
function getAbsoluteTop(elem) {
return elem.getBoundingClientRect().top;
}
/**
* Add a className to the given elements style.
* @param elem - The element to which the classes will be added.
* @param classNames - Space separated list of classes.
*/
function addClassName(elem, classNames) {
let classes = elem.className.split(" ");
const newClasses = classNames.split(" ");
classes = classes.concat(newClasses.filter(function (className) {
return !classes.includes(className);
}));
elem.className = classes.join(" ");
}
/**
* Remove a className from the given elements style.
* @param elem - The element from which the classes will be removed.
* @param classNames - Space separated list of classes.
*/
function removeClassName(elem, classNames) {
let classes = elem.className.split(" ");
const oldClasses = classNames.split(" ");
classes = classes.filter(function (className) {
return !oldClasses.includes(className);
});
elem.className = classes.join(" ");
}
/**
* For each method for both arrays and objects.
* In case of an array, the built-in Array.forEach() is applied (**No, it's not!**).
* In case of an Object, the method loops over all properties of the object.
* @param object - An Object or Array to be iterated over.
* @param callback - Array.forEach-like callback.
*/
function forEach(object, callback) {
if (Array.isArray(object)) {
// array
const len = object.length;
for (let i = 0; i < len; i++) {
callback(object[i], i, object);
}
}
else {
// object
for (const key in object) {
if (Object.prototype.hasOwnProperty.call(object, key)) {
callback(object[key], key, object);
}
}
}
}
/**
* Convert an object into an array: all objects properties are put into the array. The resulting array is unordered.
* @param o - Object that contains the properties and methods.
* @returns An array of unordered values.
*/
const toArray = Object.values;
/**
* Update a property in an object.
* @param object - The object whose property will be updated.
* @param key - Name of the property to be updated.
* @param value - The new value to be assigned.
* @returns Whether the value was updated (true) or already strictly the same in the original object (false).
*/
function updateProperty(object, key, value) {
if (object[key] !== value) {
object[key] = value;
return true;
}
else {
return false;
}
}
/**
* Throttle the given function to be only executed once per animation frame.
* @param fn - The original function.
* @returns The throttled function.
*/
function throttle(fn) {
let scheduled = false;
return () => {
if (!scheduled) {
scheduled = true;
requestAnimationFrame(() => {
scheduled = false;
fn();
});
}
};
}
/**
* Cancels the event's default action if it is cancelable, without stopping further propagation of the event.
* @param event - The event whose default action should be prevented.
*/
function preventDefault(event) {
if (!event) {
event = window.event;
}
if (!event) ;
else if (event.preventDefault) {
event.preventDefault(); // non-IE browsers
}
else {
// @TODO: IE types? Does anyone care?
event.returnValue = false; // IE browsers
}
}
/**
* Get HTML element which is the target of the event.
* @param event - The event.
* @returns The element or null if not obtainable.
*/
function getTarget(event = window.event) {
// code from http://www.quirksmode.org/js/events_properties.html
// @TODO: EventTarget can be almost anything, is it okay to return only Elements?
let target = null;
if (!event) ;
else if (event.target) {
target = event.target;
}
else if (event.srcElement) {
target = event.srcElement;
}
if (!(target instanceof Element)) {
return null;
}
if (target.nodeType != null && target.nodeType == 3) {
// defeat Safari bug
target = target.parentNode;
if (!(target instanceof Element)) {
return null;
}
}
return target;
}
/**
* Check if given element contains given parent somewhere in the DOM tree.
* @param element - The element to be tested.
* @param parent - The ancestor (not necessarily parent) of the element.
* @returns True if parent is an ancestor of the element, false otherwise.
*/
function hasParent(element, parent) {
let elem = element;
while (elem) {
if (elem === parent) {
return true;
}
else if (elem.parentNode) {
elem = elem.parentNode;
}
else {
return false;
}
}
return false;
}
const option = {
/**
* Convert a value into a boolean.
* @param value - Value to be converted intoboolean, a function will be executed as `(() => unknown)`.
* @param defaultValue - If the value or the return value of the function == null then this will be returned.
* @returns Corresponding boolean value, if none then the default value, if none then null.
*/
asBoolean(value, defaultValue) {
if (typeof value == "function") {
value = value();
}
if (value != null) {
return value != false;
}
return defaultValue || null;
},
/**
* Convert a value into a number.
* @param value - Value to be converted intonumber, a function will be executed as `(() => unknown)`.
* @param defaultValue - If the value or the return value of the function == null then this will be returned.
* @returns Corresponding **boxed** number value, if none then the default value, if none then null.
*/
asNumber(value, defaultValue) {
if (typeof value == "function") {
value = value();
}
if (value != null) {
return Number(value) || defaultValue || null;
}
return defaultValue || null;
},
/**
* Convert a value into a string.
* @param value - Value to be converted intostring, a function will be executed as `(() => unknown)`.
* @param defaultValue - If the value or the return value of the function == null then this will be returned.
* @returns Corresponding **boxed** string value, if none then the default value, if none then null.
*/
asString(value, defaultValue) {
if (typeof value == "function") {
value = value();
}
if (value != null) {
return String(value);
}
return defaultValue || null;
},
/**
* Convert a value into a size.
* @param value - Value to be converted intosize, a function will be executed as `(() => unknown)`.
* @param defaultValue - If the value or the return value of the function == null then this will be returned.
* @returns Corresponding string value (number + 'px'), if none then the default value, if none then null.
*/
asSize(value, defaultValue) {
if (typeof value == "function") {
value = value();
}
if (isString(value)) {
return value;
}
else if (isNumber(value)) {
return value + "px";
}
else {
return defaultValue || null;
}
},
/**
* Convert a value into a DOM Element.
* @param value - Value to be converted into DOM Element, a function will be executed as `(() => unknown)`.
* @param defaultValue - If the value or the return value of the function == null then this will be returned.
* @returns The DOM Element, if none then the default value, if none then null.
*/
asElement(value, defaultValue) {
if (typeof value == "function") {
value = value();
}
return value || defaultValue || null;
},
};
/**
* Convert hex color string into RGB color object.
* @remarks
* {@link http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb}
* @param hex - Hex color string (3 or 6 digits, with or without #).
* @returns RGB color object.
*/
function hexToRGB(hex) {
let result;
switch (hex.length) {
case 3:
case 4:
result = shortHexRE.exec(hex);
return result
? {
r: parseInt(result[1] + result[1], 16),
g: parseInt(result[2] + result[2], 16),
b: parseInt(result[3] + result[3], 16),
}
: null;
case 6:
case 7:
result = fullHexRE.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
default:
return null;
}
}
/**
* This function takes string color in hex or RGB format and adds the opacity, RGBA is passed through unchanged.
* @param color - The color string (hex, RGB, RGBA).
* @param opacity - The new opacity.
* @returns RGBA string, for example 'rgba(255, 0, 127, 0.3)'.
*/
function overrideOpacity(color, opacity) {
if (color.includes("rgba")) {
return color;
}
else if (color.includes("rgb")) {
const rgb = color
.substr(color.indexOf("(") + 1)
.replace(")", "")
.split(",");
return "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + "," + opacity + ")";
}
else {
const rgb = hexToRGB(color);
if (rgb == null) {
return color;
}
else {
return "rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + "," + opacity + ")";
}
}
}
/**
* Convert RGB \<0, 255\> into hex color string.
* @param red - Red channel.
* @param green - Green channel.
* @param blue - Blue channel.
* @returns Hex color string (for example: '#0acdc0').
*/
function RGBToHex(red, green, blue) {
return ("#" + ((1 << 24) + (red << 16) + (green << 8) + blue).toString(16).slice(1));
}
/**
* Parse a color property into an object with border, background, and highlight colors.
* @param inputColor - Shorthand color string or input color object.
* @param defaultColor - Full color object to fill in missing values in inputColor.
* @returns Color object.
*/
function parseColor(inputColor, defaultColor) {
if (isString(inputColor)) {
let colorStr = inputColor;
if (isValidRGB(colorStr)) {
const rgb = colorStr
.substr(4)
.substr(0, colorStr.length - 5)
.split(",")
.map(function (value) {
return parseInt(value);
});
colorStr = RGBToHex(rgb[0], rgb[1], rgb[2]);
}
if (isValidHex(colorStr) === true) {
const hsv = hexToHSV(colorStr);
const lighterColorHSV = {
h: hsv.h,
s: hsv.s * 0.8,
v: Math.min(1, hsv.v * 1.02),
};
const darkerColorHSV = {
h: hsv.h,
s: Math.min(1, hsv.s * 1.25),
v: hsv.v * 0.8,
};
const darkerColorHex = HSVToHex(darkerColorHSV.h, darkerColorHSV.s, darkerColorHSV.v);
const lighterColorHex = HSVToHex(lighterColorHSV.h, lighterColorHSV.s, lighterColorHSV.v);
return {
background: colorStr,
border: darkerColorHex,
highlight: {
background: lighterColorHex,
border: darkerColorHex,
},
hover: {
background: lighterColorHex,
border: darkerColorHex,
},
};
}
else {
return {
background: colorStr,
border: colorStr,
highlight: {
background: colorStr,
border: colorStr,
},
hover: {
background: colorStr,
border: colorStr,
},
};
}
}
else {
if (defaultColor) {
const color = {
background: inputColor.background || defaultColor.background,
border: inputColor.border || defaultColor.border,
highlight: isString(inputColor.highlight)
? {
border: inputColor.highlight,
background: inputColor.highlight,
}
: {
background: (inputColor.highlight && inputColor.highlight.background) ||
defaultColor.highlight.background,
border: (inputColor.highlight && inputColor.highlight.border) ||
defaultColor.highlight.border,
},
hover: isString(inputColor.hover)
? {
border: inputColor.hover,
background: inputColor.hover,
}
: {
border: (inputColor.hover && inputColor.hover.border) ||
defaultColor.hover.border,
background: (inputColor.hover && inputColor.hover.background) ||
defaultColor.hover.background,
},
};
return color;
}
else {
const color = {
background: inputColor.background || undefined,
border: inputColor.border || undefined,
highlight: isString(inputColor.highlight)
? {
border: inputColor.highlight,
background: inputColor.highlight,
}
: {
background: (inputColor.highlight && inputColor.highlight.background) ||
undefined,
border: (inputColor.highlight && inputColor.highlight.border) ||
undefined,
},
hover: isString(inputColor.hover)
? {
border: inputColor.hover,
background: inputColor.hover,
}
: {
border: (inputColor.hover && inputColor.hover.border) || undefined,
background: (inputColor.hover && inputColor.hover.background) || undefined,
},
};
return color;
}
}
}
/**
* Convert RGB \<0, 255\> into HSV object.
* @remarks
* {@link http://www.javascripter.net/faq/rgb2hsv.htm}
* @param red - Red channel.
* @param green - Green channel.
* @param blue - Blue channel.
* @returns HSV color object.
*/
function RGBToHSV(red, green, blue) {
red = red / 255;
green = green / 255;
blue = blue / 255;
const minRGB = Math.min(red, Math.min(green, blue));
const maxRGB = Math.max(red, Math.max(green, blue));
// Black-gray-white
if (minRGB === maxRGB) {
return { h: 0, s: 0, v: minRGB };
}
// Colors other than black-gray-white:
const d = red === minRGB ? green - blue : blue === minRGB ? red - green : blue - red;
const h = red === minRGB ? 3 : blue === minRGB ? 1 : 5;
const hue = (60 * (h - d / (maxRGB - minRGB))) / 360;
const saturation = (maxRGB - minRGB) / maxRGB;
const value = maxRGB;
return { h: hue, s: saturation, v: value };
}
/**
* Split a string with css styles into an object with key/values.
* @param cssText - CSS source code to split into key/value object.
* @returns Key/value object corresponding to {@link cssText}.
*/
function splitCSSText(cssText) {
const tmpEllement = document.createElement("div");
const styles = {};
tmpEllement.style.cssText = cssText;
for (let i = 0; i < tmpEllement.style.length; ++i) {
styles[tmpEllement.style[i]] = tmpEllement.style.getPropertyValue(tmpEllement.style[i]);
}
return styles;
}
/**
* Append a string with css styles to an element.
* @param element - The element that will receive new styles.
* @param cssText - The styles to be appended.
*/
function addCssText(element, cssText) {
const cssStyle = splitCSSText(cssText);
for (const [key, value] of Object.entries(cssStyle)) {
element.style.setProperty(key, value);
}
}
/**
* Remove a string with css styles from an element.
* @param element - The element from which styles should be removed.
* @param cssText - The styles to be removed.
*/
function removeCssText(element, cssText) {
const cssStyle = splitCSSText(cssText);
for (const key of Object.keys(cssStyle)) {
element.style.removeProperty(key);
}
}
/**
* Convert HSV \<0, 1\> into RGB color object.
* @remarks
* {@link https://gist.github.com/mjijackson/5311256}
* @param h - Hue.
* @param s - Saturation.
* @param v - Value.
* @returns RGB color object.
*/
function HSVToRGB(h, s, v) {
let r;
let g;
let b;
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0:
((r = v), (g = t), (b = p));
break;
case 1:
((r = q), (g = v), (b = p));
break;
case 2:
((r = p), (g = v), (b = t));
break;
case 3:
((r = p), (g = q), (b = v));
break;
case 4:
((r = t), (g = p), (b = v));
break;
case 5:
((r = v), (g = p), (b = q));
break;
}
return {
r: Math.floor(r * 255),
g: Math.floor(g * 255),
b: Math.floor(b * 255),
};
}
/**
* Convert HSV \<0, 1\> into hex color string.
* @param h - Hue.
* @param s - Saturation.
* @param v - Value.
* @returns Hex color string.
*/
function HSVToHex(h, s, v) {
const rgb = HSVToRGB(h, s, v);
return RGBToHex(rgb.r, rgb.g, rgb.b);
}
/**
* Convert hex color string into HSV \<0, 1\>.
* @param hex - Hex color string.
* @returns HSV color object.
*/
function hexToHSV(hex) {
const rgb = hexToRGB(hex);
if (!rgb) {
throw new TypeError(`'${hex}' is not a valid color.`);
}
return RGBToHSV(rgb.r, rgb.g, rgb.b);
}
/**
* Validate hex color string.
* @param hex - Unknown string that may contain a color.
* @returns True if the string is valid, false otherwise.
*/
function isValidHex(hex) {
const isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
return isOk;
}
/**
* Validate RGB color string.
* @param rgb - Unknown string that may contain a color.
* @returns True if the string is valid, false otherwise.
*/
function isValidRGB(rgb) {
return rgbRE.test(rgb);
}
/**
* Validate RGBA color string.
* @param rgba - Unknown string that may contain a color.
* @returns True if the string is valid, false otherwise.
*/
function isValidRGBA(rgba) {
return rgbaRE.test(rgba);
}
/**
* This recursively redirects the prototype of JSON objects to the referenceObject.
* This is used for default options.
* @param fields - Names of properties to be bridged.
* @param referenceObject - The original object.
* @returns A new object inheriting from the referenceObject.
*/
function selectiveBridgeObject(fields, referenceObject) {
if (referenceObject !== null && typeof referenceObject === "object") {
// !!! typeof null === 'object'
const objectTo = Object.create(referenceObject);
for (let i = 0; i < fields.length; i++) {
if (Object.prototype.hasOwnProperty.call(referenceObject, fields[i])) {
if (typeof referenceObject[fields[i]] == "object") {
objectTo[fields[i]] = bridgeObject(referenceObject[fields[i]]);
}
}
}
return objectTo;
}
else {
return null;
}
}
/**
* This recursively redirects the prototype of JSON objects to the referenceObject.
* This is used for default options.
* @param referenceObject - The original object.
* @returns The Element if the referenceObject is an Element, or a new object inheriting from the referenceObject.
*/
function bridgeObject(referenceObject) {
if (referenceObject === null || typeof referenceObject !== "object") {
return null;
}
if (referenceObject instanceof Element) {
// Avoid bridging DOM objects
return referenceObject;
}
const objectTo = Object.create(referenceObject);
for (const i in referenceObject) {
if (Object.prototype.hasOwnProperty.call(referenceObject, i)) {
if (typeof referenceObject[i] == "object") {
objectTo[i] = bridgeObject(referenceObject[i]);
}
}
}
return objectTo;
}
/**
* This method provides a stable sort implementation, very fast for presorted data.
* @param a - The array to be sorted (in-place).
* @param compare - An order comparator.
* @returns The argument a.
*/
function insertSort(a, compare) {
for (let i = 0; i < a.length; i++) {
const k = a[i];
let j;
for (j = i; j > 0 && compare(k, a[j - 1]) < 0; j--) {
a[j] = a[j - 1];
}
a[j] = k;
}
return a;
}
/**
* This is used to set the options of subobjects in the options object.
*
* A requirement of these subobjects is that they have an 'enabled' element
* which is optional for the user but mandatory for the program.
*
* The added value here of the merge is that option 'enabled' is set as required.
* @param mergeTarget - Either this.options or the options used for the groups.
* @param options - Options.
* @param option - Option key in the options argument.
* @param globalOptions - Global options, passed in to determine value of option 'enabled'.
*/
function mergeOptions(mergeTarget, options, option, globalOptions = {}) {
// Local helpers
const isPresent = function (obj) {
return obj !== null && obj !== undefined;
};
const isObject = function (obj) {
return obj !== null && typeof obj === "object";
};
// https://stackoverflow.com/a/34491287/1223531
const isEmpty = function (obj) {
for (const x in obj) {
if (Object.prototype.hasOwnProperty.call(obj, x)) {
return false;
}
}
return true;
};
// Guards
if (!isObject(mergeTarget)) {
throw new Error("Parameter mergeTarget must be an object");
}
if (!isObject(options)) {
throw new Error("Parameter options must be an object");
}
if (!isPresent(option)) {
throw new Error("Parameter option must have a value");
}
if (!isObject(globalOptions)) {
throw new Error("Parameter globalOptions must be an object");
}
//
// Actual merge routine, separated from main logic
// Only a single level of options is merged. Deeper levels are ref'd. This may actually be an issue.
//
const doMerge = function (target, options, option) {
if (!isObject(target[option])) {
target[option] = {};
}
const src = options[option];
const dst = target[option];
for (const prop in src) {
if (Object.prototype.hasOwnProperty.call(src, prop)) {
dst[prop] = src[prop];
}
}
};
// Local initialization
const srcOption = options[option];
const globalPassed = isObject(globalOptions) && !isEmpty(globalOptions);
const globalOption = globalPassed ? globalOptions[option] : undefined;
const globalEnabled = globalOption ? globalOption.enabled : undefined;
/////////////////////////////////////////
// Main routine
/////////////////////////////////////////
if (srcOption === undefined) {
return; // Nothing to do
}
if (typeof srcOption === "boolean") {
if (!isObject(mergeTarget[option])) {
mergeTarget[option] = {};
}
mergeTarget[option].enabled = srcOption;
return;
}
if (srcOption === null && !isObject(mergeTarget[option])) {
// If possible, explicit copy from globals
if (isPresent(globalOption)) {
mergeTarget[option] = Object.create(globalOption);
}
else {
return; // Nothing to do
}
}
if (!isObject(srcOption)) {
return;
}
//
// Ensure that 'enabled' is properly set. It is required internally
// Note that the value from options will always overwrite the existing value
//
let enabled = true; // default value
if (srcOption.enabled !== undefined) {
enabled = srcOption.enabled;
}
else {
// Take from globals, if present
if (globalEnabled !== undefined) {
enabled = globalOption.enabled;
}
}
doMerge(mergeTarget, options, option);
mergeTarget[option].enabled = enabled;
}
/**
* This function does a binary search for a visible item in a sorted list. If we find a visible item, the code that uses
* this function will then iterate in both directions over this sorted list to find all visible items.
* @param orderedItems - Items ordered by start.
* @param comparator - -1 is lower, 0 is equal, 1 is higher.
* @param field - Property name on an item (That is item[field]).
* @param field2 - Second property name on an item (That i