/* globals $, Clusterize */ import { getHeaderHTML, getBodyHTML, getRowHTML, buildCSSRule, removeCSSRule, getDefault } from './utils'; import DataManager from './datamanager'; import CellManager from './cellmanager'; import './style.scss'; const DEFAULT_OPTIONS = { events: null, data: { columns: [], rows: [] }, editing: null, addSerialNoColumn: true, addCheckboxColumn: true, enableClusterize: true, enableLogs: false, takeAvailableSpace: false }; export default class DataTable { constructor(wrapper, options) { this.wrapper = $(wrapper); if (this.wrapper.length === 0) { throw new Error('Invalid argument given for `wrapper`'); } this.options = Object.assign({}, DEFAULT_OPTIONS, options); // custom user events this.events = this.options.events; // map of checked rows this.checkMap = []; // make dom, make style, bind events this.make(); this.datamanager = new DataManager(this.options); this.cellmanager = new CellManager(this); if (this.options.data) { this.refresh(this.options.data); } } make() { if (this.wrapper.find('.data-table').length === 0) { this.makeDom(); this.makeStyle(); this.bindEvents(); } } makeDom() { this.wrapper.html(`
`); this.header = this.wrapper.find('.data-table-header'); this.bodyScrollable = this.wrapper.find('.body-scrollable'); // this.body = this.wrapper.find('.data-table-body'); this.footer = this.wrapper.find('.data-table-footer'); this.$borders = this.wrapper.find('.data-table-borders'); } refresh(data) { this.datamanager.init(data); this.render(); } appendRows(rows) { this.datamanager.appendRows(rows); this.render(); } render() { this.renderHeader(); this.renderBody(); this.setDimensions(); } renderHeader() { const columns = this.datamanager.getColumns(); this.header.html(getHeaderHTML(columns)); } renderBody() { if (this.options.enableClusterize) { this.renderBodyWithClusterize(); } else { this.renderBodyHTML(); } } renderBodyHTML() { const rows = this.datamanager.getRows(); this.bodyScrollable.html(` ${getBodyHTML(rows)}
`); } renderBodyWithClusterize() { const self = this; // empty body this.bodyScrollable.html(` ${getBodyHTML([])}
`); this.start = 0; this.pageLength = 1000; this.end = this.start + this.pageLength; // only append ${this.pageLength} rows in the beginning, // defer remaining const rows = this.datamanager.getRows(this.start, this.end); const initialData = this.getDataForClusterize(rows); this.clusterize = new Clusterize({ rows: initialData, scrollElem: this.bodyScrollable.get(0), contentElem: this.bodyScrollable.find('tbody').get(0), callbacks: { clusterChanged() { self.highlightCheckedRows(); } } }); this.log('dataAppended', this.pageLength); this.appendRemainingData(); } appendRemainingData() { let dataAppended = this.pageLength; const promises = []; const rowCount = this.datamanager.getRowCount(); while (dataAppended + this.pageLength < rowCount) { this.start = this.end; this.end = this.start + this.pageLength; promises.push(this.appendNextPagePromise(this.start, this.end)); dataAppended += this.pageLength; } if (rowCount % this.pageLength > 0) { // last page this.start = this.end; this.end = this.start + this.pageLength; promises.push(this.appendNextPagePromise(this.start, this.end)); } return promises.reduce( (prev, cur) => prev.then(cur), Promise.resolve() ); } appendNextPagePromise(start, end) { return new Promise(resolve => { setTimeout(() => { const rows = this.datamanager.getRows(start, end); const data = this.getDataForClusterize(rows); this.clusterize.append(data); this.log('dataAppended', rows.length); resolve(); }, 0); }); } getDataForClusterize(rows) { return rows.map((row) => getRowHTML(row, { rowIndex: row[0].rowIndex })); } refreshRows() { this.renderBody(); this.setDimensions(); } bindEvents() { this.bindResizeColumn(); this.bindSortColumn(); this.bindCheckbox(); } setDimensions() { const self = this; if (!this.options.takeAvailableSpace) { // setting width as 0 will ensure that the // header doesn't take the available space this.header.css({ width: 0 }); } this.header.css({ margin: 0 }); // cache minWidth for each column this.minWidthMap = getDefault(this.minWidthMap, []); this.header.find('.data-table-col').each(function () { const col = $(this); const width = parseInt(col.find('.content').css('width'), 10); const colIndex = col.attr('data-col-index'); if (!self.minWidthMap[colIndex]) { // only set this once self.minWidthMap[colIndex] = width; } }); // set initial width as naturally calculated by table's first row this.bodyScrollable.find('.data-table-row[data-row-index="0"] .data-table-col').each(function () { const $cell = $(this); let width = parseInt($cell.find('.content').css('width'), 10); const height = parseInt($cell.find('.content').css('height'), 10); const { colIndex } = self.getCellAttr($cell); const minWidth = self.getColumnMinWidth(colIndex); if (width < minWidth) { width = minWidth; } self.setColumnWidth(colIndex, width); self.setDefaultCellHeight(height); }); this.setBodyWidth(); this.setStyle('.data-table .body-scrollable', { 'margin-top': this.header.height() + 'px' }); // center align Sr. No column if (this.options.addSerialNoColumn) { const index = this.getSerialColumnIndex(); this.setStyle(`.data-table [data-col-index="${index}"]`, { 'text-align': 'center' }); } this.bodyScrollable.find('.table').css('margin', 0); } bindResizeColumn() { const self = this; let isDragging = false; let $currCell, startWidth, startX; this.header.on('mousedown', '.data-table-col', function (e) { $currCell = $(this); const colIndex = $currCell.attr('data-col-index'); const col = self.getColumn(colIndex); if (col && col.resizable === false) { return; } isDragging = true; startWidth = $currCell.find('.content').width(); startX = e.pageX; }); $('body').on('mouseup', function (e) { if (!$currCell) return; isDragging = false; const colIndex = $currCell.attr('data-col-index'); if ($currCell) { const width = parseInt($currCell.find('.content').css('width'), 10); self.setColumnWidth(colIndex, width); self.setBodyWidth(); $currCell = null; } }); $('body').on('mousemove', function (e) { if (!isDragging) return; const finalWidth = startWidth + (e.pageX - startX); const colIndex = $currCell.attr('data-col-index'); if (self.getColumnMinWidth(colIndex) > finalWidth) { // don't resize past minWidth return; } self.setColumnHeaderWidth(colIndex, finalWidth); }); } bindSortColumn() { const self = this; this.header.on('click', '.data-table-col .content span', function () { const $cell = $(this).closest('.data-table-col'); let sortOrder = getDefault($cell.attr('data-sort-order'), 'none'); const colIndex = $cell.attr('data-col-index'); const col = self.getColumn(colIndex); if (col && col.sortable === false) { return; } // reset sort indicator self.header.find('.sort-indicator').text(''); self.header.find('.data-table-col').attr('data-sort-order', 'none'); if (sortOrder === 'none') { $cell.attr('data-sort-order', 'asc'); $cell.find('.sort-indicator').text('▲'); } else if (sortOrder === 'asc') { $cell.attr('data-sort-order', 'desc'); $cell.find('.sort-indicator').text('▼'); } else if (sortOrder === 'desc') { $cell.attr('data-sort-order', 'none'); $cell.find('.sort-indicator').text(''); } // sortWith this action sortOrder = $cell.attr('data-sort-order'); if (self.events && self.events.onSort) { self.events.onSort(colIndex, sortOrder); } else { self.sortRows(colIndex, sortOrder); self.refreshRows(); } }); } sortRows(colIndex, sortOrder) { this.datamanager.sortRows(colIndex, sortOrder); } bindCheckbox() { if (!this.options.addCheckboxColumn) return; const self = this; this.wrapper.on('click', '.data-table-col[data-col-index="0"] [type="checkbox"]', function () { const $checkbox = $(this); const $cell = $checkbox.closest('.data-table-col'); const { rowIndex, isHeader } = self.getCellAttr($cell); const checked = $checkbox.is(':checked'); if (isHeader) { self.checkAll(checked); } else { self.checkRow(rowIndex, checked); } }); } getCheckedRows() { return this.checkMap .map((c, rowIndex) => { if (c) { return rowIndex; } return null; }) .filter(c => { return c !== null || c !== undefined; }); } highlightCheckedRows() { this.getCheckedRows() .map(rowIndex => this.checkRow(rowIndex, true)); } checkRow(rowIndex, toggle) { const value = toggle ? 1 : 0; // update internal map this.checkMap[rowIndex] = value; // set checkbox value explicitly this.bodyScrollable .find(`.data-table-col[data-row-index="${rowIndex}"][data-col-index="0"] [type="checkbox"]`) .prop('checked', toggle); // highlight row this.highlightRow(rowIndex, toggle); } checkAll(toggle) { const value = toggle ? 1 : 0; // update internal map if (toggle) { this.checkMap = Array.from(Array(this.getTotalRows())).map(c => value); } else { this.checkMap = []; } // set checkbox value this.bodyScrollable .find('.data-table-col[data-col-index="0"] [type="checkbox"]') .prop('checked', toggle); // highlight all this.highlightAll(toggle); } highlightRow(rowIndex, toggle = true) { const $row = this.bodyScrollable .find(`.data-table-row[data-row-index="${rowIndex}"]`); if (toggle) { $row.addClass('row-highlight'); } else { $row.removeClass('row-highlight'); } } highlightAll(toggle = true) { this.bodyScrollable .find('.data-table-row') .toggleClass('row-highlight', toggle); } setColumnWidth(colIndex, width) { // set width for content this.setStyle(`[data-col-index="${colIndex}"] .content`, { width: width + 'px' }); // set width for edit cell this.setStyle(`[data-col-index="${colIndex}"] .edit-cell`, { width: width + 'px' }); } setColumnHeaderWidth(colIndex, width) { this.setStyle(`[data-col-index="${colIndex}"][data-is-header] .content`, { width: width + 'px' }); } setDefaultCellHeight(height) { this.setStyle('.data-table-col .content', { height: height + 'px' }); } setRowHeight(rowIndex, height) { this.setStyle(`[data-row-index="${rowIndex}"] .content`, { height: height + 'px' }); } setColumnWidths() { const availableWidth = this.wrapper.width(); const headerWidth = this.header.width(); if (headerWidth > availableWidth) { // don't resize, horizontal scroll takes place return; } const columns = this.datamanager.getColumns(); const deltaWidth = (availableWidth - headerWidth) / this.datamanager.getColumnCount(); columns.map(col => { const width = this.getColumnHeaderElement(col.colIndex).width(); let finalWidth = width + deltaWidth - 16; if (this.options.addSerialNoColumn && col.colIndex === 0) { return; } this.setColumnHeaderWidth(col.colIndex, finalWidth); this.setColumnWidth(col.colIndex, finalWidth); }); this.setBodyWidth(); } setBodyWidth() { this.bodyScrollable.css( 'width', parseInt(this.header.css('width'), 10) ); } setStyle(rule, styleMap) { let styles = this.$style.text(); styles = buildCSSRule(rule, styleMap, styles); this.$style.html(styles); } removeStyle(rule) { let styles = this.$style.text(); styles = removeCSSRule(rule, styles); this.$style.html(styles); } makeStyle() { this.$style = $('') .prependTo(this.wrapper); } getColumn(colIndex) { return this.datamanager.getColumn(colIndex); } getRow(rowIndex) { return this.datamanager.getRow(rowIndex); } getCell(colIndex, rowIndex) { return this.datamanager.getCell(colIndex, rowIndex); } getColumnHeaderElement(colIndex) { colIndex = +colIndex; if (colIndex < 0) return null; return this.wrapper.find( `.data-table-col[data-is-header][data-col-index="${colIndex}"]` ); } getColumnMinWidth(colIndex) { colIndex = +colIndex; return this.minWidthMap && this.minWidthMap[colIndex]; } getCellAttr($cell) { return $cell.data(); } getTotalRows() { return this.datamanager.getRowCount(); } getFirstColumnIndex() { if (this.options.addCheckboxColumn && this.options.addSerialNoColumn) { return 2; } if (this.options.addCheckboxColumn || this.options.addSerialNoColumn) { return 1; } return 0; } getSerialColumnIndex() { const columns = this.datamanager.getColumns(); return columns.findIndex(column => column.content.includes('Sr. No')); } log() { if (this.options.enableLogs) { console.log.apply(console, arguments); } } }