UNPKG

awesome-data-view

Version:
493 lines (434 loc) 15.4 kB
import { getEventListeners } from "events"; import hexToRGBA from "./helpers/hexToRGB"; import { NotFoundIllustration } from "./helpers/icons"; import PaleColor from "./helpers/PaleColor"; const defaultPadding = "10px"; type theme = { table: { background?: string; color?: string; border?: string; fontFamily?: string; borderCollapse?: string; borderSpacing?: string; [key: string]: any; }; header: { background: string; color: string; padding: string; border: string; [key: string]: any; }; cell: { background: string; color: string; padding: string; border: string; [key: string]: any; }; }; const defaultTheme: theme = { table: { background: "transparent", color: "inherit", border: "none", borderCollapse: "separate", borderSpacing: "0px", fontFamily: "inherit", }, header: { background: "transparent", color: "inherit", padding: defaultPadding, border: "0.5px solid #000", }, cell: { background: "transparent", color: "inherit", padding: defaultPadding, border: "0.5px solid #000", }, }; const lightTheme: theme = { table: { background: "white", color: "black", border: "none", fontFamily: "inherit", borderCollapse: "separate", borderSpacing: "1px", }, header: { background: "#ffffff", color: "#000", padding: defaultPadding, border: "0.5px solid #000", }, cell: { background: "#ffffff", color: "#000", padding: defaultPadding, border: "0.5px solid #000", }, }; const darkTheme: theme = { table: { background: "#ffffff", color: "black", border: "none", fontFamily: "inherit", borderCollapse: "separate", borderSpacing: "1px", }, header: { background: "#111111", color: "#f2f2f2", padding: defaultPadding, border: "0.5px solid #000", }, cell: { background: "#111111", color: "#f2f2f2", padding: defaultPadding, border: "0.5px solid #f2f2f2", }, }; export default class TableRenderer { public table: HTMLTableElement; protected tbody: HTMLElement; protected container: HTMLElement; public theme: theme; protected headers: Array<string | HTMLElement>; protected _options = { expandable: true, shouldGraySomeRows: true, shouldFitContainer: true, }; protected listeners: { onClickRow?: Function; onClickCell?: Function; onClickHeader?: Function; } = {}; protected constructor() { this.theme = defaultTheme; } onClickRow(fn: Function) { this.listeners.onClickRow = fn; return this; } onClickCell(fn: Function) { this.listeners.onClickCell = fn; return this; } onClickHeader(fn: Function) { this.listeners.onClickHeader = fn; return this; } static container(container: HTMLElement) { let instance = new TableRenderer(); instance.container = container; return instance; } lightTheme() { this.theme = lightTheme; return this; } darkTheme() { this.theme = darkTheme; return this; } customTheme(theme: theme) { if (theme.cell) { Object.keys(theme.cell).forEach((attribute) => { this.theme.cell[attribute] = theme.cell[attribute]; }); } if (theme.table) { Object.keys(theme.table).forEach((attribute) => { this.theme.table[attribute] = theme.table[attribute]; }); } if (theme.header) { Object.keys(theme.header).forEach((attribute) => { this.theme.header[attribute] = theme.header[attribute]; }); } return this; } render(data: object | Array<object>, headers: Array<string | HTMLElement>) { let div = document.createElement("div"); if (this._options.shouldFitContainer) { let { width, height } = this.container.getBoundingClientRect(); if (width !== 0) { div.style.width = this.container.getBoundingClientRect().width + "px"; } div.style.overflow = "auto"; div.style.position = "relative"; } this.table = document.createElement("table"); // this.table.style.borderCollapse = 'collapse' Object.keys(this.theme.table).forEach((style) => { this.table.style[style] = this.theme.table[style]; }); this.headers = headers; if (!headers) { this.headers = this.getHeadersFromData(data); } this.renderHeaders(this.headers); this.tbody = document.createElement("tbody"); this.table.appendChild(this.tbody); this.renderData(data, this.headers); div.appendChild(this.table); this.container.appendChild(div); return this; } protected getHeadersFromData(data: object | Array<object>) { if (data instanceof Array) { return data.length !== 0 ? Object.keys(data[0]) : []; } return this.getHeadersFromData(data[Object.keys(data)[0]]); } protected renderData( data: object | Array<object>, headers: Array<string | HTMLElement> ) { if (data instanceof Array) { data.forEach((datum, i) => { this.renderRow(datum, i, i % 2 === 0, i === data.length - 1); }); } else { Object.keys(data).forEach((name) => { this.renderRowHeader(name, headers.length); this.renderData(data[name], headers); }); } } protected renderHeaders(headers: Array<string | HTMLElement> = []) { let tr = document.createElement("tr"); tr.style.position = "sticky"; tr.style.top = "0"; let onClickHeader = this.listeners.onClickHeader; if (this._options.expandable) { let th = document.createElement("th"); Object.keys(this.theme.header).forEach((attribute) => { th.style[attribute] = this.theme.header[attribute]; }); tr.appendChild(th); } headers.forEach((header) => { let th = document.createElement("th"); th.style.textTransform = "capitalize"; Object.keys(this.theme.header).forEach((attribute) => { th.style[attribute] = this.theme.header[attribute]; }); if (header instanceof HTMLElement) { th.appendChild(header); } else { th.innerText = header; } th.addEventListener("click", () => { if (onClickHeader) onClickHeader(th, header, headers); }); // th.style.position = 'sticky'; // th.style.top = '0' tr.appendChild(th); }); this.table.appendChild(tr); } protected renderRowHeader(header: string | HTMLElement, colspan: number) { let tr = document.createElement("tr"); let td = document.createElement("td"); td.style.fontStyle = "italic"; td.style.fontWeight = "600"; td.colSpan = colspan; if (this._options.expandable) { td.colSpan += 1; } if (header instanceof HTMLElement) { td.appendChild(header); } else { td.innerText = header; } Object.keys(this.theme.cell).forEach((attribute) => { td.style[attribute] = this.theme.cell[attribute]; }); tr.appendChild(td); this.tbody.appendChild(tr); } protected renderRow( row: object, index: number, shouldReduceOpacity = false, isLastIndex = false, ) { let tr = document.createElement("tr"); tr.setAttribute('data-index', index.toString()) let onClickCell = this.listeners.onClickCell; let onClickRow = this.listeners.onClickRow; let addTd = (value) => { let td = document.createElement("td"); Object.keys(this.theme.cell).forEach((attribute) => { td.style[attribute] = this.theme.cell[attribute]; }); if (shouldReduceOpacity) { const currentColor = PaleColor( hexToRGBA(this.theme.cell.background, 1), 0.05 ); td.style.setProperty("background", currentColor); } if (typeof value === "object" && value?.raw) { value = value.raw; } if (/http(s)?:\/\/(\w+)/i.test(value)) { value = `<a href="${value}" style="color: #abc4ff;" target="_blank">${value}</a>` } if (!value) { value = 'NA' } if (value instanceof HTMLElement) { td.appendChild(value.cloneNode(true)); } else if (value instanceof Function) { let output = value(row, index, shouldReduceOpacity, isLastIndex); if (output instanceof HTMLElement) { td.appendChild(output); } else { td.innerHTML = output ?? 'NA'; } } else { td.innerHTML = value ?? 'NA'; } return td; }; if (onClickRow) { onClickRow(tr, row, this); } if (this._options.expandable) { let td = document.createElement("td"); Object.keys(this.theme.cell).forEach((attribute) => { td.style[attribute] = this.theme.cell[attribute]; }); if (shouldReduceOpacity) { const currentColor = PaleColor( hexToRGBA(this.theme.cell.background, 1), 0.05 ); td.style.setProperty("background", currentColor); } let button = document.createElement("button"); let openIcon = `<svg style="width: 16px; color: #5bfd80" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus"><path d="M5 12h14"/><path d="M12 5v14"/></svg>`; let closeIcon = `<svg style="width: 16px; color: #f85a5a" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-minus"><path d="M5 12h14"/></svg>`; let isOpen = false; let tableRow: HTMLElement | null = null; let createTable = () => { tableRow = document.createElement("tr"); let td = document.createElement("td"); td.colSpan = this.headers.length + 1; let table = document.createElement("table"); table.style.width = "100%"; Object.keys(row).forEach((attribute) => { let tr = document.createElement("tr"); tr.appendChild(addTd(attribute)); tr.appendChild(addTd(row[attribute])); table.appendChild(tr); }); td.appendChild(table); tableRow.appendChild(td); tr.insertAdjacentElement("afterend", tableRow); }; button.addEventListener("click", () => { isOpen = !isOpen; if (isOpen) { button.innerHTML = closeIcon; createTable(); } else { tableRow?.remove(); button.innerHTML = openIcon; } }); button.innerHTML = openIcon; td.appendChild(button); tr.appendChild(td); } Object.keys(row).forEach((attribute) => { let td = document.createElement("td"); Object.keys(this.theme.cell).forEach((attribute) => { td.style[attribute] = this.theme.cell[attribute]; }); if (shouldReduceOpacity) { const currentColor = PaleColor( hexToRGBA(this.theme.cell.background, 1), 0.05 ); td.style.setProperty("background", currentColor); } if (isLastIndex) { td.style.borderBottom = "none"; } let v = row[attribute] ?? 'NA'; let title = v; let display = v; if (typeof v === "object" && v) { if (v.display) { display = v.display; } if (v.raw) { title = v.raw; if (/http(s)?:\/\/(\w+)/i.test(display)) { display = `<a href="${v.raw}" style="color: #abc4ff;" target="_blank">${display}</a>` } } } if (typeof title === "string") { td.title = title; } if (display instanceof HTMLElement) { td.appendChild(display.cloneNode(true)); } else if (display instanceof Function) { let output = display(row, index, shouldReduceOpacity, isLastIndex); if (output instanceof HTMLElement) { td.appendChild(output); } else { td.innerHTML = output ?? 'NA'; } } else { td.innerHTML = display ?? 'NA'; } td.addEventListener("click", () => { if (onClickCell) onClickCell(td, row[attribute], row, attribute); }); tr.appendChild(td); }); this.tbody.appendChild(tr); } showEmptyIndicator() { let tr = document.createElement("tr"); let td = document.createElement("td"); Object.keys(this.theme.cell).forEach((attribute) => { td.style[attribute] = this.theme.cell[attribute]; }); td.colSpan = this.headers.length; if (this._options.expandable) { td.colSpan += 1; } let tableRect = this.table.getBoundingClientRect(); td.style.height = tableRect.height > 0 ? tableRect.height + "px" : "100%"; td.style.width = tableRect.width > 0 ? tableRect.width + "px" : "100%"; td.style.padding = "10px"; td.innerHTML = `${NotFoundIllustration}`; tr.appendChild(td); this.tbody.appendChild(tr); } updateRows(rows: object | Array<object>) { this.tbody.innerHTML = ""; if (rows instanceof Array && rows.length === 0) { this.showEmptyIndicator(); return; } this.renderData(rows, this.headers); } }