react-table
Version:
Hooks for building lightweight, fast and extendable datagrids for React
348 lines (289 loc) • 8.07 kB
JavaScript
import { defaultColumn, emptyRenderer } from './publicUtils'
// Find the depth of the columns
export function findMaxDepth(columns, depth = 0) {
return columns.reduce((prev, curr) => {
if (curr.columns) {
return Math.max(prev, findMaxDepth(curr.columns, depth + 1))
}
return depth
}, 0)
}
// Build the visible columns, headers and flat column list
export function linkColumnStructure(columns, parent, depth = 0) {
return columns.map(column => {
column = {
...column,
parent,
depth,
}
assignColumnAccessor(column)
if (column.columns) {
column.columns = linkColumnStructure(column.columns, column, depth + 1)
}
return column
})
}
export function flattenColumns(columns) {
return flattenBy(columns, 'columns')
}
export function assignColumnAccessor(column) {
// First check for string accessor
let { id, accessor, Header } = column
if (typeof accessor === 'string') {
id = id || accessor
const accessorPath = accessor.split('.')
accessor = row => getBy(row, accessorPath)
}
if (!id && typeof Header === 'string' && Header) {
id = Header
}
if (!id && column.columns) {
console.error(column)
throw new Error('A column ID (or unique "Header" value) is required!')
}
if (!id) {
console.error(column)
throw new Error('A column ID (or string accessor) is required!')
}
Object.assign(column, {
id,
accessor,
})
return column
}
export function decorateColumn(column, userDefaultColumn) {
if (!userDefaultColumn) {
throw new Error()
}
Object.assign(column, {
// Make sure there is a fallback header, just in case
Header: emptyRenderer,
Footer: emptyRenderer,
...defaultColumn,
...userDefaultColumn,
...column,
})
Object.assign(column, {
originalWidth: column.width,
})
return column
}
// Build the header groups from the bottom up
export function makeHeaderGroups(
allColumns,
defaultColumn,
additionalHeaderProperties = () => ({})
) {
const headerGroups = []
let scanColumns = allColumns
let uid = 0
const getUID = () => uid++
while (scanColumns.length) {
// The header group we are creating
const headerGroup = {
headers: [],
}
// The parent columns we're going to scan next
const parentColumns = []
const hasParents = scanColumns.some(d => d.parent)
// Scan each column for parents
scanColumns.forEach(column => {
// What is the latest (last) parent column?
let latestParentColumn = [...parentColumns].reverse()[0]
let newParent
if (hasParents) {
// If the column has a parent, add it if necessary
if (column.parent) {
newParent = {
...column.parent,
originalId: column.parent.id,
id: `${column.parent.id}_${getUID()}`,
headers: [column],
...additionalHeaderProperties(column),
}
} else {
// If other columns have parents, we'll need to add a place holder if necessary
const originalId = `${column.id}_placeholder`
newParent = decorateColumn(
{
originalId,
id: `${column.id}_placeholder_${getUID()}`,
placeholderOf: column,
headers: [column],
...additionalHeaderProperties(column),
},
defaultColumn
)
}
// If the resulting parent columns are the same, just add
// the column and increment the header span
if (
latestParentColumn &&
latestParentColumn.originalId === newParent.originalId
) {
latestParentColumn.headers.push(column)
} else {
parentColumns.push(newParent)
}
}
headerGroup.headers.push(column)
})
headerGroups.push(headerGroup)
// Start scanning the parent columns
scanColumns = parentColumns
}
return headerGroups.reverse()
}
const pathObjCache = new Map()
export function getBy(obj, path, def) {
if (!path) {
return obj
}
const cacheKey = typeof path === 'function' ? path : JSON.stringify(path)
const pathObj =
pathObjCache.get(cacheKey) ||
(() => {
const pathObj = makePathArray(path)
pathObjCache.set(cacheKey, pathObj)
return pathObj
})()
let val
try {
val = pathObj.reduce((cursor, pathPart) => cursor[pathPart], obj)
} catch (e) {
// continue regardless of error
}
return typeof val !== 'undefined' ? val : def
}
export function getFirstDefined(...args) {
for (let i = 0; i < args.length; i += 1) {
if (typeof args[i] !== 'undefined') {
return args[i]
}
}
}
export function getElementDimensions(element) {
const rect = element.getBoundingClientRect()
const style = window.getComputedStyle(element)
const margins = {
left: parseInt(style.marginLeft),
right: parseInt(style.marginRight),
}
const padding = {
left: parseInt(style.paddingLeft),
right: parseInt(style.paddingRight),
}
return {
left: Math.ceil(rect.left),
width: Math.ceil(rect.width),
outerWidth: Math.ceil(
rect.width + margins.left + margins.right + padding.left + padding.right
),
marginLeft: margins.left,
marginRight: margins.right,
paddingLeft: padding.left,
paddingRight: padding.right,
scrollWidth: element.scrollWidth,
}
}
export function isFunction(a) {
if (typeof a === 'function') {
return a
}
}
export function flattenBy(arr, key) {
const flat = []
const recurse = arr => {
arr.forEach(d => {
if (!d[key]) {
flat.push(d)
} else {
recurse(d[key])
}
})
}
recurse(arr)
return flat
}
export function expandRows(
rows,
{ manualExpandedKey, expanded, expandSubRows = true }
) {
const expandedRows = []
const handleRow = (row, addToExpandedRows = true) => {
row.isExpanded =
(row.original && row.original[manualExpandedKey]) || expanded[row.id]
row.canExpand = row.subRows && !!row.subRows.length
if (addToExpandedRows) {
expandedRows.push(row)
}
if (row.subRows && row.subRows.length && row.isExpanded) {
row.subRows.forEach(row => handleRow(row, expandSubRows))
}
}
rows.forEach(row => handleRow(row))
return expandedRows
}
export function getFilterMethod(filter, userFilterTypes, filterTypes) {
return (
isFunction(filter) ||
userFilterTypes[filter] ||
filterTypes[filter] ||
filterTypes.text
)
}
export function shouldAutoRemoveFilter(autoRemove, value, column) {
return autoRemove ? autoRemove(value, column) : typeof value === 'undefined'
}
export function unpreparedAccessWarning() {
throw new Error(
'React-Table: You have not called prepareRow(row) one or more rows you are attempting to render.'
)
}
let passiveSupported = null
export function passiveEventSupported() {
// memoize support to avoid adding multiple test events
if (typeof passiveSupported === 'boolean') return passiveSupported
let supported = false
try {
const options = {
get passive() {
supported = true
return false
},
}
window.addEventListener('test', null, options)
window.removeEventListener('test', null, options)
} catch (err) {
supported = false
}
passiveSupported = supported
return passiveSupported
}
//
const reOpenBracket = /\[/g
const reCloseBracket = /\]/g
function makePathArray(obj) {
return (
flattenDeep(obj)
// remove all periods in parts
.map(d => String(d).replace('.', '_'))
// join parts using period
.join('.')
// replace brackets with periods
.replace(reOpenBracket, '.')
.replace(reCloseBracket, '')
// split it back out on periods
.split('.')
)
}
function flattenDeep(arr, newArr = []) {
if (!Array.isArray(arr)) {
newArr.push(arr)
} else {
for (let i = 0; i < arr.length; i += 1) {
flattenDeep(arr[i], newArr)
}
}
return newArr
}