simple-datatables
Version:
A lightweight, dependency-free JavaScript HTML table plugin.
415 lines (358 loc) • 13.5 kB
text/typescript
import {readDataCell, readHeaderCell} from "./read_data"
import {DataTable} from "./datatable"
import {
cellType,
columnSettingsType,
columnsStateType,
dataRowType,
elementNodeType,
headerCellType,
inputCellType,
inputHeaderCellType
} from "./types"
import {readColumnSettings} from "./column_settings"
import {cellToText} from "./helpers"
export class Columns {
dt: DataTable
settings: columnSettingsType[]
_state: columnsStateType
constructor(dt: DataTable) {
this.dt = dt
this.init()
}
init() {
[this.settings, this._state] = readColumnSettings(this.dt.options.columns, this.dt.options.type, this.dt.options.format)
}
get(column: number) {
if (column < 0 || column >= this.size()) {
return null
}
return {...this.settings[column]}
}
size() {
return this.settings.length
}
/**
* Swap two columns
*/
swap(columns: [number, number]) {
if (columns.length === 2) {
// Get the current column indexes
const cols = this.dt.data.headings.map((_node: headerCellType, index: number) => index)
const x = columns[0]
const y = columns[1]
const b = cols[y]
cols[y] = cols[x]
cols[x] = b
return this.order(cols)
}
}
/**
* Reorder the columns
*/
order(columns: number[]) {
this.dt.data.headings = columns.map((index: number) => this.dt.data.headings[index])
this.dt.data.data.forEach(
(row: dataRowType) => (row.cells = columns.map((index: number) => row.cells[index]))
)
this.settings = columns.map(
(index: number) => this.settings[index]
)
// Update
this.dt.update()
}
/**
* Hide columns
*/
hide(columns: number | number[]) {
if (!Array.isArray(columns)) {
columns = [columns]
}
if (!columns.length) {
return
}
columns.forEach((index: number) => {
if (!this.settings[index]) {
this.settings[index] = {
type: "string"
}
}
const column = this.settings[index]
column.hidden = true
})
this.dt.update()
}
/**
* Show columns
*/
show(columns: number | number[]) {
if (!Array.isArray(columns)) {
columns = [columns]
}
if (!columns.length) {
return
}
columns.forEach((index: number) => {
if (!this.settings[index]) {
this.settings[index] = {
type: "string",
sortable: true
}
}
const column = this.settings[index]
delete column.hidden
})
this.dt.update()
}
/**
* Check column(s) visibility
*/
visible(columns: number | number[] | undefined) {
if (columns === undefined) {
columns = [...Array(this.dt.data.headings.length).keys()]
}
if (Array.isArray(columns)) {
return columns.map(index => !this.settings[index]?.hidden)
}
return !this.settings[columns]?.hidden
}
/**
* Add a new column
*/
add(data: {data: inputCellType[], heading: inputHeaderCellType} & columnSettingsType) {
const newColumnSelector = this.dt.data.headings.length
this.dt.data.headings = this.dt.data.headings.concat([readHeaderCell(data.heading)])
this.dt.data.data.forEach((row: dataRowType, index: number) => {
row.cells = row.cells.concat([readDataCell(data.data[index], data)])
})
this.settings[newColumnSelector] = {
type: data.type || "string",
sortable: true,
searchable: true
}
if (data.type || data.format || data.sortable || data.render || data.filter) {
const column = this.settings[newColumnSelector]
if (data.render) {
column.render = data.render
}
if (data.format) {
column.format = data.format
}
if (data.cellClass) {
column.cellClass = data.cellClass
}
if (data.headerClass) {
column.headerClass = data.headerClass
}
if (data.locale) {
column.locale = data.locale
}
if (data.sortable === false) {
column.sortable = false
} else {
if (data.numeric) {
column.numeric = data.numeric
}
if (data.caseFirst) {
column.caseFirst = data.caseFirst
}
}
if (data.searchable === false) {
column.searchable = false
} else {
if (data.sensitivity) {
column.sensitivity = data.sensitivity
}
}
if (column.searchable || column.sortable) {
if (data.ignorePunctuation) {
column.ignorePunctuation = data.ignorePunctuation
}
}
if (data.hidden) {
column.hidden = true
}
if (data.filter) {
column.filter = data.filter
}
if (data.sortSequence) {
column.sortSequence = data.sortSequence
}
}
this.dt.update(true)
}
/**
* Remove column(s)
*/
remove(columns: number | number[]) {
if (!Array.isArray(columns)) {
columns = [columns]
}
this.dt.data.headings = this.dt.data.headings.filter((_heading: headerCellType, index: number) => !columns.includes(index))
this.dt.data.data.forEach(
(row: dataRowType) => (row.cells = row.cells.filter((_cell: cellType, index: number) => !columns.includes(index)))
)
this.dt.update(true)
}
/**
* Filter by column
*/
filter(column: number, init = false) {
if (!this.settings[column]?.filter?.length) {
// There is no filter to apply.
return
}
const currentFilter = this._state.filters[column]
let newFilterState
if (currentFilter) {
let returnNext = false
newFilterState = this.settings[column].filter.find((filter: (string | number | boolean | elementNodeType[] | object | ((arg: (string | number | boolean | elementNodeType[] | object)) => boolean))) => {
if (returnNext) {
return true
}
if (filter === currentFilter) {
returnNext = true
}
return false
})
} else {
const filter = this.settings[column].filter
newFilterState = filter ? filter[0] : undefined
}
if (newFilterState) {
this._state.filters[column] = newFilterState
} else if (currentFilter) {
this._state.filters[column] = undefined
}
this.dt._currentPage = 1
this.dt.update()
if (!init) {
this.dt.emit("datatable.filter", column, newFilterState)
}
}
/**
* Sort by column
*/
sort(index: number, dir: ("asc" | "desc" | undefined) = undefined, init = false) {
const column = this.settings[index]
if (!init) {
this.dt.emit("datatable.sorting", index, dir)
}
if (!dir) {
const currentDir = this._state.sort && this._state.sort.column === index ? this._state.sort?.dir : false
const sortSequence = column?.sortSequence || ["asc", "desc"]
if (!currentDir) {
dir = sortSequence.length ? sortSequence[0] : "asc"
} else {
const currentDirIndex = sortSequence.indexOf(currentDir)
if (currentDirIndex === -1) {
dir = sortSequence[0] || "asc"
} else if (currentDirIndex === sortSequence.length -1) {
dir = sortSequence[0]
} else {
dir = sortSequence[currentDirIndex + 1]
}
}
}
const collator = ["string", "html"].includes(column.type) ?
new Intl.Collator(column.locale || this.dt.options.locale, {
usage: "sort",
numeric: column.numeric || this.dt.options.numeric,
caseFirst: column.caseFirst || this.dt.options.caseFirst,
ignorePunctuation: column.ignorePunctuation|| this.dt.options.ignorePunctuation
}) :
false
this.dt.data.data.sort((row1: dataRowType, row2: dataRowType) => {
const cell1 = row1.cells[index]
const cell2 = row2.cells[index]
let order1 = cell1.order ?? cellToText(cell1)
let order2 = cell2.order ?? cellToText(cell2)
if (dir === "desc") {
const temp = order1
order1 = order2
order2 = temp
}
if (collator && (typeof order1 !== "number") && (typeof order2 !== "number")) {
return collator.compare(String(order1), String(order2))
}
if (order1 < order2) {
return -1
} else if (order1 > order2) {
return 1
}
return 0
})
this._state.sort = {column: index,
dir}
if (this.dt._searchQueries.length) {
this.dt.multiSearch(this.dt._searchQueries)
this.dt.emit("datatable.sort", index, dir)
} else if (!init) {
this.dt._currentPage = 1
this.dt.update()
this.dt.emit("datatable.sort", index, dir)
}
}
/**
* Measure the actual width of cell content by rendering the entire table with all contents.
* Note: Destroys current DOM and therefore requires subsequent dt.update()
*/
_measureWidths() {
const activeHeadings = this.dt.data.headings.filter((heading: headerCellType, index: number) => !this.settings[index]?.hidden)
if ((this.dt.options.scrollY.length || this.dt.options.fixedColumns) && activeHeadings?.length) {
this._state.widths = []
const renderOptions: {noPaging?: true, noColumnWidths?: true, unhideHeader?: true, renderHeader?: true} = {
noPaging: true
}
// If we have headings we need only set the widths on them
// otherwise we need a temp header and the widths need applying to all cells
if (this.dt.options.header || this.dt.options.footer) {
if (this.dt.options.scrollY.length) {
renderOptions.unhideHeader = true
}
if (this.dt.headerDOM) {
// Remove headerDOM for accurate measurements
this.dt.headerDOM.parentElement.removeChild(this.dt.headerDOM)
}
// Reset widths
renderOptions.noColumnWidths = true
this.dt._renderTable(renderOptions)
const activeDOMHeadings : HTMLTableCellElement[] = Array.from(this.dt.dom.querySelector("thead, tfoot")?.firstElementChild?.querySelectorAll("th") || [])
let domCounter = 0
const absoluteColumnWidths = this.dt.data.headings.map((_heading: headerCellType, index: number) => {
if (this.settings[index]?.hidden) {
return 0
}
const width = activeDOMHeadings[domCounter].offsetWidth
domCounter += 1
return width
})
const totalOffsetWidth = absoluteColumnWidths.reduce(
(total, cellWidth) => total + cellWidth,
0
)
this._state.widths = absoluteColumnWidths.map(cellWidth => cellWidth / totalOffsetWidth * 100)
} else {
renderOptions.renderHeader = true
this.dt._renderTable(renderOptions)
const activeDOMHeadings: HTMLTableCellElement[] = Array.from(this.dt.dom.querySelector("thead, tfoot")?.firstElementChild?.querySelectorAll("th") || [])
let domCounter = 0
const absoluteColumnWidths = this.dt.data.headings.map((_heading: headerCellType, index: number) => {
if (this.settings[index]?.hidden) {
return 0
}
const width = activeDOMHeadings[domCounter].offsetWidth
domCounter += 1
return width
})
const totalOffsetWidth = absoluteColumnWidths.reduce(
(total, cellWidth) => total + cellWidth,
0
)
this._state.widths = absoluteColumnWidths.map(cellWidth => cellWidth / totalOffsetWidth * 100)
}
// render table without options for measurements
this.dt._renderTable()
}
}
}