UNPKG

simple-data-table

Version:

🔨 Lightweight and simple data table with no dependencies

334 lines (273 loc) • 9.71 kB
class SimpleDataTable { constructor($el, options = {}) { this.$el = $el; this.addButtonLabel = options.addButtonLabel || '✚'; this.readonly = options.readonly || false; this.defaultColumnPrefix = options.defaultColumnPrefix || 'column'; this.defaultColumnNumber = options.defaultColumnNumber || null; this.defaultHighlightedCellClass = options.defaultHighlightedCellClass || 'highlighted-cell'; this._headers = []; this.data = []; this._events = {}; this._sorted = { columnIndex: -1, descending: false, }; this._sortComparingFn = (a, b) => a.toString() .localeCompare(b.toString()); } _renderTHead($table) { const $thead = document.createElement('thead'); const $row = document.createElement('tr'); this._headers.forEach((name, index) => { const $cell = this._createEmptyHeaderCell(); let label = name; if (this._sorted.columnIndex === index) { label += ` ${this._sorted.descending ? '\u2191' : '\u2193'}`; } $cell.textContent = label; $cell.addEventListener('click', () => { this._sorted.descending = (this._sorted.columnIndex === index) && !this._sorted.descending; this.sortByColumn(index); }); $row.appendChild($cell); }); $thead.appendChild($row); $table.appendChild($thead); } _renderTBody($table) { const $tbody = document.createElement('tbody'); this.data.forEach((item, rowIndex) => { const $row = document.createElement('tr'); Object.entries(item).forEach(([key, value]) => { const $cell = this._createCellWithInput(key, value, rowIndex); $row.appendChild($cell); }); const $cell = this.readonly ? this._createEmptyCell() : this._createCellWithRemoveRowButton(); $row.appendChild($cell); $tbody.appendChild($row); }); $table.appendChild($tbody); } render() { if (!this.$el) { throw new Error('this.$el is not defined'); } SimpleDataTable.clearElement(this.$el); const $wrapper = document.createElement('div'); $wrapper.classList.add('simple-data-table'); const $table = document.createElement('table'); if (this._headers.length > 0) { this._renderTHead($table); } this._renderTBody($table); $wrapper.appendChild($table); if (!this.readonly) { const $addButton = this._createAddButton(); $wrapper.appendChild($addButton); } this.$el.appendChild($wrapper); return this; } getRowsCount() { return this.$el.querySelectorAll('tr').length; } findCellsByContent(...content) { const indexes = []; const $rows = this.$el.querySelectorAll('tr'); $rows.forEach((row, rowIndex) => { const cells = row.querySelectorAll('td'); cells.forEach((cell, cellIndex) => { const $cellInput = cell.querySelector('input'); const cellContent = $cellInput ? $cellInput.value : cell.textContent; content.forEach((item) => { if (cellContent === item) { indexes.push({ rowIndex, cellIndex, }); } }); }); }); return indexes; } getCell(rowIndex, cellIndex) { const $rows = this.$el.querySelectorAll('tr'); const $row = $rows[rowIndex]; if (!$row) { return null; } const $cells = $row.querySelectorAll('td'); const $cell = $cells[cellIndex]; if (!$cell) { return null; } return $cell; } highlightCell(rowIndex, cellIndex) { const $cell = this.getCell(rowIndex, cellIndex); $cell.classList.add(this.defaultHighlightedCellClass); } clearHighlightedCells() { const $cells = this.$el.querySelectorAll('td'); $cells.forEach(($cell) => { $cell.classList.remove(this.defaultHighlightedCellClass); }); } setInputCellContent(rowIndex, cellIndex, content) { const $cell = this.getCell(rowIndex, cellIndex); const $input = $cell.querySelector('input'); $input.value = content; } _createEmptyCell() { return document.createElement('td'); } _createEmptyHeaderCell() { return document.createElement('th'); } _createCellWithRemoveRowButton() { const $cell = this._createEmptyCell(); const $removeButton = document.createElement('button'); $removeButton.classList.add('remove-row'); $removeButton.textContent = '✖︎'; $removeButton.addEventListener('click', () => { const $tr = $cell.parentNode; this._removeRow($tr); }); $cell.appendChild($removeButton); return $cell; } _removeRow($tr) { const $siblings = Array.from($tr.parentNode.children); const index = $siblings.indexOf($tr); this.data.splice(index, 1); $tr.remove(); this.emit(SimpleDataTable.EVENTS.ROW_REMOVED, this.data); } _createAddButton() { const $addButton = document.createElement('button'); $addButton.classList.add('add-row'); $addButton.textContent = this.addButtonLabel; $addButton.addEventListener('click', this._createEmptyRow.bind(this)); return $addButton; } _createCellWithInput(name, value, rowIndex) { const $cell = document.createElement('td'); const $input = document.createElement('input'); $input.value = value; $input.name = name; if (this.readonly) { $input.disabled = true; } $input.addEventListener('change', () => { this.data[rowIndex][name] = $input.value; this.emit(SimpleDataTable.EVENTS.UPDATE, this.data); }); $cell.appendChild($input); return $cell; } _createEmptyRow() { const $tbody = this.$el.querySelector('tbody'); const rowsCount = $tbody.querySelectorAll('tr').length; const $row = document.createElement('tr'); const columnNames = this._fetchColumnNames(); const record = {}; columnNames.forEach((cellName) => { const $cell = this._createCellWithInput(cellName, '', rowsCount); $row.appendChild($cell); record[cellName] = ''; }); this.data.push(record); $row.appendChild(this._createCellWithRemoveRowButton()); $tbody.appendChild($row); this.emit(SimpleDataTable.EVENTS.ROW_ADDED); } _fetchColumnNames() { const $tbody = this.$el.querySelector('tbody'); const $firstRecord = $tbody.querySelector('tr'); if (!$firstRecord) { const size = this.defaultColumnNumber ? this.defaultColumnNumber : this._headers ? this._headers.length : this.data[0] && this.data[0].length; if (!size) { return []; } return Array(size) .fill(this.defaultColumnPrefix) .map((name, index) => `${name}${index + 1}`); } const $elements = Array.from($firstRecord.children); return $elements .map(($cell) => $cell.querySelector('input')) .filter(($element) => $element) .map(($input) => $input.name); } /** * @param {string[]} items * @returns */ setHeaders(items) { this._headers = items; return this; } load(data) { this.data = Array.from(data); return this; } emit(name, payload) { if (!this._events[name]) { return; } this._events[name].forEach((cb) => cb(payload)); return this; } on(name, handler) { if (!this._events[name]) { this._events[name] = []; } this._events[name].push(handler); return this; } sortByColumn(index) { this._sorted.columnIndex = index; const order = this._sorted.descending ? 1 : -1; this.data.sort((firstRow, secondRow) => this._sortComparingFn( Object.values(firstRow)[index], Object.values(secondRow)[index] ) * order ); this.render(); this.emit(SimpleDataTable.EVENTS.DATA_SORTED); } setSortComparingFn(fn) { this._sortComparingFn = fn; } static clearElement($element) { while ($element.firstElementChild) { $element.firstElementChild.remove(); } } } SimpleDataTable.EVENTS = { UPDATE: 'SimpleDataTable.EVENTS.UPDATE', ROW_ADDED: 'SimpleDataTable.EVENTS.ROW_ADDED', ROW_REMOVED: 'SimpleDataTable.EVENTS.ROW_REMOVED', DATA_SORTED: 'SimpleDataTable.EVENTS.DATA_SORTED', }; // Exports if (typeof module === 'object' && module.exports) { module.exports = { SimpleDataTable }; } else if (typeof define === 'function' && define.amd) { define(() => ({ SimpleDataTable })); } else { window.SimpleDataTable = SimpleDataTable; }