UNPKG

simple-datatables

Version:

A lightweight, dependency-free JavaScript HTML table plugin.

285 lines (268 loc) 9.9 kB
import {nodeToObj, stringToObj} from "diff-dom" import {parseDate} from "./date" import {namedNodeMapToObject, objToText} from "./helpers" import { cellType, columnSettingsType, DataOption, dataRowType, headerCellType, inputCellType, inputHeaderCellType, inputRowType, nodeType } from "./types" export const readDataCell = (cell: inputCellType, columnSettings : columnSettingsType) : cellType => { if (cell?.constructor === Object && Object.prototype.hasOwnProperty.call(cell, "data") && !Object.keys(cell).find(key => !(["text", "order", "data", "attributes"].includes(key)))) { return (cell as cellType) } const cellData : cellType = { data: cell } switch (columnSettings.type) { case "string": if (!(typeof cell === "string")) { cellData.text = String(cellData.data) cellData.order = cellData.text } break case "date": if (columnSettings.format) { cellData.order = parseDate(String(cellData.data), columnSettings.format) } break case "number": cellData.text = String(cellData.data as number) cellData.data = parseFloat(cellData.data as string) cellData.order = cellData.data break case "html": { const node = Array.isArray(cellData.data) ? {nodeName: "TD", childNodes: (cellData.data as nodeType[])} : // If it is an array, we assume it is an array of nodeType stringToObj(`<td>${String(cellData.data)}</td>`) cellData.data = node.childNodes || [] const text = objToText(node) cellData.text = text cellData.order = text break } case "boolean": if (typeof cellData.data === "string") { cellData.data = cellData.data.toLowerCase().trim() } cellData.data = !["false", false, null, undefined, 0].includes(cellData.data as (string | number | boolean | null | undefined)) cellData.order = cellData.data ? 1 : 0 cellData.text = String(cellData.data) break case "other": cellData.text = "" cellData.order = 0 break default: cellData.text = JSON.stringify(cellData.data) break } return cellData } const readDOMDataCell = (cell: HTMLElement, columnSettings : columnSettingsType) : cellType => { let cellData : cellType switch (columnSettings.type) { case "string": cellData = { data: cell.innerText } break case "date": { const data = cell.innerText cellData = { data, order: parseDate(data, columnSettings.format) } break } case "number": { const data = parseFloat(cell.innerText) cellData = { data, order: data, text: cell.innerText } break } case "boolean": { const data = !["false", "0", "null", "undefined"].includes(cell.innerText.toLowerCase().trim()) cellData = { data, text: data ? "1" : "0", order: data ? 1 : 0 } break } default: { // "html", "other" const node = nodeToObj(cell, {valueDiffing: false}) cellData = { data: node.childNodes || [], text: cell.innerText, order: cell.innerText } break } } // Save cell attributes to reference when rendering cellData.attributes = namedNodeMapToObject(cell.attributes) return cellData } export const readHeaderCell = (cell: inputHeaderCellType) : headerCellType => { if ( cell instanceof Object && cell.constructor === Object && cell.hasOwnProperty("data") && (typeof cell.text === "string" || typeof cell.data === "string") ) { return cell } const cellData : headerCellType = { data: cell } if (typeof cell === "string") { if (cell.length) { const node = stringToObj(`<th>${cell}</th>`) if (node.childNodes && (node.childNodes.length !== 1 || node.childNodes[0].nodeName !== "#text")) { cellData.data = node.childNodes cellData.type = "html" const text = objToText(node) cellData.text = text } } } else if ([null, undefined].includes(cell)) { cellData.text = "" } else { cellData.text = JSON.stringify(cell) } return cellData } export const readDOMHeaderCell = (cell: HTMLElement) : headerCellType => { const node = nodeToObj(cell, {valueDiffing: false}) let cellData: headerCellType if (node.childNodes && (node.childNodes.length !== 1 || node.childNodes[0].nodeName !== "#text")) { cellData = { data: node.childNodes, type: "html", text: objToText(node) } } else { cellData = { data: cell.innerText, type: "string" } } // Save header cell attributes to reference when rendering cellData.attributes = node.attributes return cellData } export const readTableData = (dataOption: DataOption, dom: (HTMLTableElement | undefined)=undefined, columnSettings, defaultType, defaultFormat) => { const data = { data: [] as dataRowType[], headings: [] as headerCellType[] } if (dataOption.headings) { data.headings = dataOption.headings.map((heading: inputHeaderCellType) => readHeaderCell(heading)) } else if (dom?.tHead) { data.headings = Array.from(dom.tHead.querySelectorAll("th")).map((th, index) => { const heading = readDOMHeaderCell(th) if (!columnSettings[index]) { columnSettings[index] = { type: defaultType, format: defaultFormat, searchable: true, sortable: true } } const settings = columnSettings[index] if (th.dataset.sortable?.trim().toLowerCase() === "false" || th.dataset.sort?.trim().toLowerCase() === "false") { settings.sortable = false } if (th.dataset.searchable?.trim().toLowerCase() === "false") { settings.searchable = false } if (th.dataset.hidden?.trim().toLowerCase() === "true" || th.getAttribute("hidden")?.trim().toLowerCase() === "true") { settings.hidden = true } if (["number", "string", "html", "date", "boolean", "other"].includes(th.dataset.type)) { settings.type = th.dataset.type if (settings.type === "date" && th.dataset.format) { settings.format = th.dataset.format } } return heading }) } else if (dataOption.data?.length) { const firstRow = dataOption.data[0] const firstRowCells = Array.isArray(firstRow) ? firstRow : firstRow.cells data.headings = firstRowCells.map((_cell: inputCellType) => readHeaderCell("")) } else if (dom?.tBodies.length) { data.headings = Array.from(dom.tBodies[0].rows[0].cells).map((_cell: HTMLElement) => readHeaderCell("")) } for (let i=0; i<data.headings.length; i++) { // Make sure that there are settings for all columns if (!columnSettings[i]) { columnSettings[i] = { type: defaultType, format: defaultFormat, sortable: true, searchable: true } } } if (dataOption.data) { const headings = data.headings.map((heading: headerCellType) => heading.data ? String(heading.data) : heading.text) data.data = dataOption.data.map((row: inputRowType | inputCellType[]) => { let attributes: { [key: string]: string } let cells: inputCellType[] if (Array.isArray(row)) { attributes = {} cells = row } else if (row.hasOwnProperty("cells") && Object.keys(row).every(key => ["cells", "attributes"].includes(key))) { attributes = row.attributes cells = row.cells } else { attributes = {} cells = [] Object.entries(row).forEach(([heading, cell]) => { const index = headings.indexOf(heading) if (index > -1) { cells[index] = cell } }) } return { attributes, cells: cells.map((cell: inputCellType, index: number) => readDataCell(cell, columnSettings[index])) } as dataRowType }) } else if (dom?.tBodies?.length) { data.data = Array.from(dom.tBodies[0].rows).map( row => ({ attributes: namedNodeMapToObject(row.attributes), cells: Array.from(row.cells).map( (cell, index) => { const cellData = cell.dataset.content ? readDataCell(cell.dataset.content, columnSettings[index]) : readDOMDataCell(cell, columnSettings[index]) if (cell.dataset.order) { cellData.order = isNaN(parseFloat(cell.dataset.order)) ? cell.dataset.order : parseFloat(cell.dataset.order) } return cellData } ) } as dataRowType) ) } if (data.data.length && data.data[0].cells.length !== data.headings.length) { throw new Error( "Data heading length mismatch." ) } return data }