/**
* A collection of utility functions that make it easier to work with
* table data.
* @module Utils
*/
import SortDirection from './SortDirection';
import DataType from './DataType';
/**
* Gets the value of a cell given the row data. If column.value is
* a function, it gets called, otherwise it is interpreted as a
* key to rowData. If column.value is not defined, column.id is
* used as a key to rowData.
*
* @param {Object} column The column definition
* @param {Object} rowData The data for the row
* @param {Number} rowNumber The number of the row
* @param {Object[]} tableData The array of data for the whole table
* @param {Object[]} columns The column definitions for the whole table
* @return {Any} The value for this cell
*/
export function getCellData(column, rowData, rowNumber, tableData, columns) {
const { value, id } = column;
// call value as a function
if (typeof value === 'function') {
return value(rowData, rowNumber, tableData, columns);
// interpret value as a key
} else if (value != null) {
return rowData[value];
}
// otherwise, use the ID as a key
return rowData[id];
}
/**
* Gets the sort value of a cell given the cell data and row data. If
* no sortValue function is provided on the column, the cellData is
* returned.
*
* @param {Object} cellData The cell data
* @param {Object} column The column definition
* @param {Object} rowData The data for the row
* @return {Any} The sort value for this cell
*/
export function getSortValueFromCellData(cellData, column, rowData) {
const { sortValue } = column;
if (sortValue) {
return sortValue(cellData, rowData);
}
return cellData;
}
/**
* Gets the sort value for a cell by first computing the cell data. If
* no sortValue function is provided on the column, the cellData is
* returned.
*
* @param {Object} column The column definition
* @param {Object} rowData The data for the row
* @param {Number} rowNumber The number of the row
* @param {Object[]} tableData The array of data for the whole table
* @param {Object[]} columns The column definitions for the whole table
* @return {Any} The sort value for this cell
*/
export function getSortValue(column, rowData, rowNumber, tableData, columns) {
const cellData = getCellData(column, rowData, rowNumber, tableData, columns);
return getSortValueFromCellData(cellData, column, rowData);
}
/**
* Gets a column from the column definitions based on its ID
*
* @param {Object[]} columns The column definitions for the whole table
* @param {String} columnId the `id` of the column
* @return {Object} The column definition
*/
export function getColumnById(columns, columnId) {
return columns.find(column => column.id === columnId);
}
/**
* Gets the comparator function to use based on the type of data
* the column represents. These comparator functions expect the
* data to be presented as { index, sortValue }. sortValue is used
* to determine the order and index is used to break ties to maintain
* a stable sort.
*
* @param {String} type the DataType the column represents
* @return {Function} the comparator for stable sort
*/
export function getSortComparator(type) {
let comparator;
switch (type) {
case DataType.Number:
case DataType.NumberOrdinal:
comparator = function numberComparator(a, b) {
const aSortValue = a.sortValue;
const bSortValue = b.sortValue;
if (aSortValue == null && bSortValue == null) {
// compare index to maintain stable sort
return a.index - b.index;
} else if (aSortValue == null) {
return 1;
} else if (bSortValue == null) {
return -1;
}
const difference = parseFloat(aSortValue) - parseFloat(bSortValue);
if (difference === 0) {
// compare index to maintain stable sort
return a.index - b.index;
}
return difference;
};
break;
case DataType.String:
comparator = function stringComparator(a, b) {
const aSortValue = a.sortValue;
const bSortValue = b.sortValue;
if (aSortValue == null && bSortValue == null) {
// compare index to maintain stable sort
return a.index - b.index;
} else if (aSortValue == null) {
return -1;
} else if (bSortValue == null) {
return 1;
}
// compute the result here, then check if equal to maintain stable sort
const result = String(aSortValue).toLowerCase()
.localeCompare(String(bSortValue).toLowerCase());
if (result === 0) {
// compare index to maintain stable sort
return a.index - b.index;
}
return result;
};
break;
default:
comparator = function defaultComparator(a, b) {
const aSortValue = a.sortValue;
const bSortValue = b.sortValue;
if (aSortValue == null && bSortValue == null) {
// compare index to maintain stable sort
return a.index - b.index;
} else if (aSortValue == null) {
return -1;
} else if (bSortValue == null) {
return 1;
}
if (aSortValue === bSortValue) {
// compare index to maintain stable sort
return a.index - b.index;
}
return aSortValue < bSortValue ? -1 : 1;
};
break;
}
return comparator;
}
/**
* Sorts the data based on sort value and column type. Uses a stable sort
* by keeping track of the original position to break ties.
*
* @param {Object[]} data the array of data for the whole table
* @param {String} columnId the column ID of the column to sort by
* @param {Boolean} sortDirection The direction to sort in
* @param {Object[]} columns The column definitions for the whole table
* @return {Object[]} The sorted data
*/
export function sortData(data, columnId, sortDirection, columns) {
const column = getColumnById(columns, columnId);
if (!column) {
if (process.env.NODE_ENV !== 'production') {
console.warn('No column found by ID', columnId, columns);
}
return data;
}
// read the type from `sortType` property if defined, otherwise use `type`
const sortType = column.sortType == null ? column.type : column.sortType;
const comparator = getSortComparator(sortType);
const sortedData = data.map((rowData, index) => ({
rowData,
index,
sortValue: getSortValue(column, rowData, index, data, columns),
}))
.sort(comparator)
.map(sortItem => sortItem.rowData);
if (sortDirection === SortDirection.Descending) {
sortedData.reverse();
}
return sortedData;
}
/**
* Renders a cell's contents based on the renderer function. If no
* renderer is provided, it just returns the raw cell data. In such
* cases, the user should take care that cellData can be rendered
* directly.
*
* @param {Any} cellData The data for the cell
* @param {Object} column The column definition
* @param {Object} rowData The data for the row
* @param {Number} rowNumber The number of the row
* @param {Object[]} tableData The array of data for the whole table
* @param {Object[]} columns The column definitions for the whole table
* @return {Renderable} The contents of the cell
*/
export function renderCell(cellData, column, rowData, rowNumber, tableData, columns) {
const { renderer } = column;
// if renderer is provided, call it
if (renderer != null) {
return renderer(cellData, column, rowData, rowNumber, tableData, columns);
}
// otherwise, render the raw cell data
return cellData;
}
/**
* Checks an array of column definitions to see if there are any issues.
* Checks if
*
* - multiple columns have the same ID
*
* Typically only used in development.
*
* @param {Object[]} columns The column definitions for the whole table
* @returns {void}
*/
export function validateColumns(columns) {
if (!columns) {
return;
}
// check IDs
const ids = {};
columns.forEach((column, i) => {
const { id } = column;
if (!ids[id]) {
ids[id] = [i];
} else {
ids[id].push(i);
}
});
Object.keys(ids).forEach(id => {
if (ids[id].length > 1) {
console.warn(`Column ID '${id}' used in multiple columns ${ids[id].join(', ')}`, ids[id].map(index => columns[index]));
}
});
}