react-table/src/plugin-hooks/useSortBy.js
2019-12-13 00:22:30 -07:00

344 lines
8.8 KiB
JavaScript
Executable File

import React from 'react'
import {
actions,
ensurePluginOrder,
defaultColumn,
makePropGetter,
useConsumeHookGetter,
getFirstDefined,
defaultOrderByFn,
isFunction,
useGetLatest,
useMountedLayoutEffect,
} from '../utils'
import * as sortTypes from '../sortTypes'
// Actions
actions.resetSortBy = 'resetSortBy'
actions.toggleSortBy = 'toggleSortBy'
actions.clearSortBy = 'clearSortBy'
defaultColumn.sortType = 'alphanumeric'
defaultColumn.sortDescFirst = false
export const useSortBy = hooks => {
hooks.getSortByToggleProps = [defaultGetSortByToggleProps]
hooks.stateReducers.push(reducer)
hooks.useInstance.push(useInstance)
}
useSortBy.pluginName = 'useSortBy'
const defaultGetSortByToggleProps = (props, instance, column) => {
const { isMultiSortEvent = e => e.shiftKey } = instance
return [
props,
{
onClick: column.canSort
? e => {
e.persist()
column.toggleSortBy(
undefined,
!instance.disableMultiSort && isMultiSortEvent(e)
)
}
: undefined,
style: {
cursor: column.canSort ? 'pointer' : undefined,
},
title: column.canSort ? 'Toggle SortBy' : undefined,
},
]
}
// Reducer
function reducer(state, action, previousState, instance) {
if (action.type === actions.init) {
return {
sortBy: [],
...state,
}
}
if (action.type === actions.resetSortBy) {
return {
...state,
sortBy: [],
}
}
if (action.type === actions.clearSortBy) {
const { sortBy } = state
const newSortBy = sortBy.filter(d => d.id !== action.columnId)
return {
...state,
sortBy: newSortBy,
}
}
if (action.type === actions.toggleSortBy) {
const { columnId, desc, multi } = action
const {
flatColumns,
disableMultiSort,
disableSortRemove,
disableMultiRemove,
maxMultiSortColCount = Number.MAX_SAFE_INTEGER,
} = instance
const { sortBy } = state
// Find the column for this columnId
const column = flatColumns.find(d => d.id === columnId)
const { sortDescFirst } = column
// Find any existing sortBy for this column
const existingSortBy = sortBy.find(d => d.id === columnId)
const existingIndex = sortBy.findIndex(d => d.id === columnId)
const hasDescDefined = typeof desc !== 'undefined' && desc !== null
let newSortBy = []
// What should we do with this sort action?
let sortAction
if (!disableMultiSort && multi) {
if (existingSortBy) {
sortAction = 'toggle'
} else {
sortAction = 'add'
}
} else {
// Normal mode
if (existingIndex !== sortBy.length - 1) {
sortAction = 'replace'
} else if (existingSortBy) {
sortAction = 'toggle'
} else {
sortAction = 'replace'
}
}
// Handle toggle states that will remove the sortBy
if (
sortAction === 'toggle' && // Must be toggling
!disableSortRemove && // If disableSortRemove, disable in general
!hasDescDefined && // Must not be setting desc
(multi ? !disableMultiRemove : true) && // If multi, don't allow if disableMultiRemove
((existingSortBy && // Finally, detect if it should indeed be removed
existingSortBy.desc &&
!sortDescFirst) ||
(!existingSortBy.desc && sortDescFirst))
) {
sortAction = 'remove'
}
if (sortAction === 'replace') {
newSortBy = [
{
id: columnId,
desc: hasDescDefined ? desc : sortDescFirst,
},
]
} else if (sortAction === 'add') {
newSortBy = [
...sortBy,
{
id: columnId,
desc: hasDescDefined ? desc : sortDescFirst,
},
]
// Take latest n columns
newSortBy.splice(0, newSortBy.length - maxMultiSortColCount)
} else if (sortAction === 'toggle') {
// This flips (or sets) the
newSortBy = sortBy.map(d => {
if (d.id === columnId) {
return {
...d,
desc: hasDescDefined ? desc : !existingSortBy.desc,
}
}
return d
})
} else if (sortAction === 'remove') {
newSortBy = sortBy.filter(d => d.id !== columnId)
}
return {
...state,
sortBy: newSortBy,
}
}
}
function useInstance(instance) {
const {
data,
rows,
flatColumns,
orderByFn = defaultOrderByFn,
sortTypes: userSortTypes,
manualSortBy,
defaultCanSort,
disableSortBy,
flatHeaders,
state: { sortBy },
dispatch,
plugins,
autoResetSortBy = true,
} = instance
ensurePluginOrder(plugins, ['useFilters'], 'useSortBy', [])
// Updates sorting based on a columnId, desc flag and multi flag
const toggleSortBy = (columnId, desc, multi) => {
dispatch({ type: actions.toggleSortBy, columnId, desc, multi })
}
// use reference to avoid memory leak in #1608
const getInstance = useGetLatest(instance)
const getSortByTogglePropsHooks = useConsumeHookGetter(
getInstance().hooks,
'getSortByToggleProps'
)
// Add the getSortByToggleProps method to columns and headers
flatHeaders.forEach(column => {
const {
accessor,
canSort: defaultColumnCanSort,
disableSortBy: columnDisableSortBy,
id,
} = column
const canSort = accessor
? getFirstDefined(
columnDisableSortBy === true ? false : undefined,
disableSortBy === true ? false : undefined,
true
)
: getFirstDefined(defaultCanSort, defaultColumnCanSort, false)
column.canSort = canSort
if (column.canSort) {
column.toggleSortBy = (desc, multi) =>
toggleSortBy(column.id, desc, multi)
column.clearSortBy = () => {
dispatch({ type: actions.clearSortBy, columnId: column.id })
}
}
column.getSortByToggleProps = makePropGetter(
getSortByTogglePropsHooks(),
getInstance(),
column
)
const columnSort = sortBy.find(d => d.id === id)
column.isSorted = !!columnSort
column.sortedIndex = sortBy.findIndex(d => d.id === id)
column.isSortedDesc = column.isSorted ? columnSort.desc : undefined
})
const sortedRows = React.useMemo(() => {
if (manualSortBy || !sortBy.length) {
return rows
}
// Filter out sortBys that correspond to non existing columns
const availableSortBy = sortBy.filter(sort =>
flatColumns.find(col => col.id === sort.id)
)
const sortData = rows => {
// Use the orderByFn to compose multiple sortBy's together.
// This will also perform a stable sorting using the row index
// if needed.
const sortedData = orderByFn(
rows,
availableSortBy.map(sort => {
// Support custom sorting methods for each column
const column = flatColumns.find(d => d.id === sort.id)
if (!column) {
throw new Error(
`React-Table: Could not find a column with id: ${sort.id} while sorting`
)
}
const { sortType } = column
// Look up sortBy functions in this order:
// column function
// column string lookup on user sortType
// column string lookup on built-in sortType
// default function
// default string lookup on user sortType
// default string lookup on built-in sortType
const sortMethod =
isFunction(sortType) ||
(userSortTypes || {})[sortType] ||
sortTypes[sortType]
if (!sortMethod) {
throw new Error(
`React-Table: Could not find a valid sortType of '${sortType}' for column '${sort.id}'.`
)
}
// Return the correct sortFn.
// This function should always return in ascending order
return (a, b) => sortMethod(a, b, sort.id)
}),
// Map the directions
availableSortBy.map(sort => {
// Detect and use the sortInverted option
const column = flatColumns.find(d => d.id === sort.id)
if (column && column.sortInverted) {
return sort.desc
}
return !sort.desc
})
)
// If there are sub-rows, sort them
sortedData.forEach(row => {
if (!row.subRows || row.subRows.length <= 1) {
return
}
row.subRows = sortData(row.subRows)
})
return sortedData
}
return sortData(rows)
}, [manualSortBy, sortBy, rows, flatColumns, orderByFn, userSortTypes])
const getAutoResetSortBy = useGetLatest(autoResetSortBy)
useMountedLayoutEffect(() => {
if (getAutoResetSortBy()) {
dispatch({ type: actions.resetSortBy })
}
}, [manualSortBy ? null : data])
Object.assign(instance, {
preSortedRows: rows,
sortedRows,
rows: sortedRows,
toggleSortBy,
})
}