highcharts
Version:
JavaScript charting framework
385 lines (384 loc) • 10.9 kB
JavaScript
/* *
*
* (c) 2020-2025 Highsoft AS
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* Authors:
* - Sophie Bremer
*
* */
'use strict';
/* *
*
* Class
*
* */
/**
* This class manages state cursors pointing on {@link Data.DataTable}. It
* creates a relation between states of the user interface and the table cells,
* columns, or rows.
*
* @class
* @name Data.DataCursor
*/
class DataCursor {
/* *
*
* Constructor
*
* */
constructor(stateMap = {}) {
this.emittingRegister = [];
this.listenerMap = {};
this.stateMap = stateMap;
}
/* *
*
* Functions
*
* */
/**
* This function registers a listener for a specific state and table.
*
* @example
* ```TypeScript
* dataCursor.addListener(myTable.id, 'hover', (e: DataCursor.Event) => {
* if (e.cursor.type === 'position') {
* console.log(`Hover over row #${e.cursor.row}.`);
* }
* });
* ```
*
* @function #addListener
*
* @param {Data.DataCursor.TableId} tableId
* The ID of the table to listen to.
*
* @param {Data.DataCursor.State} state
* The state on the table to listen to.
*
* @param {Data.DataCursor.Listener} listener
* The listener to register.
*
* @return {Data.DataCursor}
* Returns the DataCursor instance for a call chain.
*/
addListener(tableId, state, listener) {
const listenerMap = this.listenerMap[tableId] = (this.listenerMap[tableId] ||
{});
const listeners = listenerMap[state] = (listenerMap[state] ||
[]);
listeners.push(listener);
return this;
}
/**
* @private
*/
buildEmittingTag(e) {
return (e.cursor.type === 'position' ?
[
e.table.id,
e.cursor.column,
e.cursor.row,
e.cursor.state,
e.cursor.type
] :
[
e.table.id,
e.cursor.columns,
e.cursor.firstRow,
e.cursor.lastRow,
e.cursor.state,
e.cursor.type
]).join('\0');
}
/**
* This function emits a state cursor related to a table. It will provide
* lasting state cursors of the table to listeners.
*
* @example
* ```ts
* dataCursor.emit(myTable, {
* type: 'position',
* column: 'city',
* row: 4,
* state: 'hover',
* });
* ```
*
* @param {Data.DataTable} table
* The related table of the cursor.
*
* @param {Data.DataCursor.Type} cursor
* The state cursor to emit.
*
* @param {Event} [event]
* Optional event information from a related source.
*
* @param {boolean} [lasting]
* Whether this state cursor should be kept until it is cleared with
* {@link DataCursor#remitCursor}.
*
* @return {Data.DataCursor}
* Returns the DataCursor instance for a call chain.
*/
emitCursor(table, cursor, event, lasting) {
const tableId = table.id, state = cursor.state, listeners = (this.listenerMap[tableId] &&
this.listenerMap[tableId][state]);
if (listeners) {
const stateMap = this.stateMap[tableId] = (this.stateMap[tableId] ?? {});
const cursors = stateMap[cursor.state] || [];
if (lasting) {
if (!cursors.length) {
stateMap[cursor.state] = cursors;
}
if (DataCursor.getIndex(cursor, cursors) === -1) {
cursors.push(cursor);
}
}
const e = {
cursor,
cursors,
table
};
if (event) {
e.event = event;
}
const emittingRegister = this.emittingRegister, emittingTag = this.buildEmittingTag(e);
if (emittingRegister.indexOf(emittingTag) >= 0) {
// Break call stack loops
return this;
}
try {
this.emittingRegister.push(emittingTag);
for (let i = 0, iEnd = listeners.length; i < iEnd; ++i) {
listeners[i].call(this, e);
}
}
finally {
const index = this.emittingRegister.indexOf(emittingTag);
if (index >= 0) {
this.emittingRegister.splice(index, 1);
}
}
}
return this;
}
/**
* Removes a lasting state cursor.
*
* @function #remitCursor
*
* @param {string} tableId
* ID of the related cursor table.
*
* @param {Data.DataCursor.Type} cursor
* Copy or reference of the cursor.
*
* @return {Data.DataCursor}
* Returns the DataCursor instance for a call chain.
*/
remitCursor(tableId, cursor) {
const cursors = (this.stateMap[tableId] &&
this.stateMap[tableId][cursor.state]);
if (cursors) {
const index = DataCursor.getIndex(cursor, cursors);
if (index >= 0) {
cursors.splice(index, 1);
}
}
return this;
}
/**
* This function removes a listener.
*
* @function #addListener
*
* @param {Data.DataCursor.TableId} tableId
* The ID of the table the listener is connected to.
*
* @param {Data.DataCursor.State} state
* The state on the table the listener is listening to.
*
* @param {Data.DataCursor.Listener} listener
* The listener to deregister.
*
* @return {Data.DataCursor}
* Returns the DataCursor instance for a call chain.
*/
removeListener(tableId, state, listener) {
const listeners = (this.listenerMap[tableId] &&
this.listenerMap[tableId][state]);
if (listeners) {
const index = listeners.indexOf(listener);
if (index >= 0) {
listeners.splice(index, 1);
}
}
return this;
}
}
/* *
*
* Static Properties
*
* */
/**
* Semantic version string of the DataCursor class.
* @internal
*/
DataCursor.version = '1.0.0';
/* *
*
* Class Namespace
*
* */
/**
* @class Data.DataCursor
*/
(function (DataCursor) {
/* *
*
* Declarations
*
* */
/* *
*
* Functions
*
* */
/**
* Finds the index of an cursor in an array.
* @private
*/
function getIndex(needle, cursors) {
if (needle.type === 'position') {
for (let cursor, i = 0, iEnd = cursors.length; i < iEnd; ++i) {
cursor = cursors[i];
if (cursor.type === 'position' &&
cursor.state === needle.state &&
cursor.column === needle.column &&
cursor.row === needle.row) {
return i;
}
}
}
else {
const columnNeedle = JSON.stringify(needle.columns);
for (let cursor, i = 0, iEnd = cursors.length; i < iEnd; ++i) {
cursor = cursors[i];
if (cursor.type === 'range' &&
cursor.state === needle.state &&
cursor.firstRow === needle.firstRow &&
cursor.lastRow === needle.lastRow &&
JSON.stringify(cursor.columns) === columnNeedle) {
return i;
}
}
}
return -1;
}
DataCursor.getIndex = getIndex;
/**
* Checks whether two cursor share the same properties.
* @private
*/
function isEqual(cursorA, cursorB) {
if (cursorA.type === 'position' && cursorB.type === 'position') {
return (cursorA.column === cursorB.column &&
cursorA.row === cursorB.row &&
cursorA.state === cursorB.state);
}
if (cursorA.type === 'range' && cursorB.type === 'range') {
return (cursorA.firstRow === cursorB.firstRow &&
cursorA.lastRow === cursorB.lastRow &&
(JSON.stringify(cursorA.columns) ===
JSON.stringify(cursorB.columns)));
}
return false;
}
DataCursor.isEqual = isEqual;
/**
* Checks whether a cursor is in a range.
* @private
*/
function isInRange(needle, range) {
if (range.type === 'position') {
range = toRange(range);
}
if (needle.type === 'position') {
needle = toRange(needle, range);
}
const needleColumns = needle.columns;
const rangeColumns = range.columns;
return (needle.firstRow >= range.firstRow &&
needle.lastRow <= range.lastRow &&
(!needleColumns ||
!rangeColumns ||
needleColumns.every((column) => rangeColumns.indexOf(column) >= 0)));
}
DataCursor.isInRange = isInRange;
/**
* @private
*/
function toPositions(cursor) {
if (cursor.type === 'position') {
return [cursor];
}
const columns = (cursor.columns || []);
const positions = [];
const state = cursor.state;
for (let row = cursor.firstRow, rowEnd = cursor.lastRow; row < rowEnd; ++row) {
if (!columns.length) {
positions.push({
type: 'position',
row,
state
});
continue;
}
for (let column = 0, columnEnd = columns.length; column < columnEnd; ++column) {
positions.push({
type: 'position',
column: columns[column],
row,
state
});
}
}
return positions;
}
DataCursor.toPositions = toPositions;
/**
* @private
*/
function toRange(cursor, defaultRange) {
if (cursor.type === 'range') {
return cursor;
}
const range = {
type: 'range',
firstRow: (cursor.row ??
(defaultRange && defaultRange.firstRow) ??
0),
lastRow: (cursor.row ??
(defaultRange && defaultRange.lastRow) ??
Number.MAX_VALUE),
state: cursor.state
};
if (typeof cursor.column !== 'undefined') {
range.columns = [cursor.column];
}
return range;
}
DataCursor.toRange = toRange;
})(DataCursor || (DataCursor = {}));
/* *
*
* Default Export
*
* */
export default DataCursor;