Core.UI = Core.UI || {}; Core.UI.Table = class extends Core.Singleton { /** * Get class name. * @return {string} */ static getClassName() { return 'Core.UI.Table'; } /** * Initialize table. * @param {string} tableId * @param {object} parameters */ init(tableId, parameters) { this._parameters = parameters; this._tableId = tableId; this.$table = (parameters.$container || $('body')).find(`table.${tableId}:not(.dataTable)`); if (this.$table.doesntExist()) { console.error(`Table ${tableId} doesnt exist`); return false; } else if (this.$table.children('tbody').doesntExist()) { console.error(`Table ${tableId} is empty`); return false; } this.tableId = tableId; this.instance = parameters.instance || this.instance || Page.activePage; this.prepareTable(); this.loadedIds = []; this.populateIds = []; this.populatingCount = 0; this.rowsBoundOn = null; this.bindRequestedOn = null; this.rowLockCounts = {}; this.$emptyTableRow = null; this.queueSize = this.$table.data('queue-size') || 5; let $parent = this.$table.parent(); let scrollY = this.$table.parent().innerHeight(); if (this.$table.parent().innerHeight() > window.innerHeight) { scrollY = window.innerHeight - (this.$table.parent().innerHeight() - window.innerHeight); } if ($parent.parents('.ui.modal').exists()) { $parent = $parent.parent(); scrollY = $parent.innerHeight() - this.$table.children('thead').outerHeight() - this.$table.children('tfoot').outerHeight() - (14 * 3.5); } let dataTableParameters = Object.assign( { scrollY: Math.min( Math.max( Math.abs(scrollY), 600 ), 600 ), scrollCollapse: true, scrollX: true, paging: false, searching: false, search: { smart: false }, info: false, deferRender: true, destroy: true, autoWidth: false, dom: `<"toolbar ${tableId}">frtip` }, this.$table.data('datatables-parameters') ); if (dataTableParameters.paging) { dataTableParameters.scrollY = null; } try { this.table = this.$table.DataTable(dataTableParameters); } catch (e) { console.debug(e); return false; } this.table.on('order.dt', () => this.bindRequestedOn = new Date()); this.table.on('draw.dt', () => this.bindRequestedOn = new Date()); this.table.on('page.dt', () => this.populate()); this.table.on('search.dt', () => this.populate()); this.updateBody(); this.hook(); if (dataTableParameters.hideHeader) { this.getContainer().addClass('no-head'); } if (!dataTableParameters.scrollY) { this.getContainer().addClass('no-scroll'); } this.$table.find('img').one( 'load', () => this.table.columns.adjust() ); this.table.draw(); this.table.columns.adjust(); if (dataTableParameters.paging === false) { this.populate(); } if (this.instance) { setTimeout( () => this.instance.bindTable( $(this.table.table().container()) ), 500 ); } return true; } /** * If the object becomes stale, re-initialize it. * @param {string} * @param {object} parameters */ reinit(tableId, parameters) { if (this.$table.doesntExist() || this.$table.notVisible()) { return this.init(tableId, parameters); } return true; } /** * Monitor if rows need to be populated or bound. */ hook() { setTimeout( () => { if (this.$table.doesntExist()) { return; } if (this.populatingCount < this.queueSize && this.populateIds.length !== 0) { for (let i = 0; i < (this.queueSize - this.populatingCount); i++) { if (this.populateIds.length === 0) { break; } this.populatingCount++; this.loadRow( this.populateIds.shift(), () => this.populatingCount-- ); } } if (!this.rowsBoundOn || this.rowsBoundOn < this.bindRequestedOn) { this.rowsBoundOn = new Date(); this.bindRows(); } this.hook(); }, 250 ); } /** * Get count of table columns. */ getColumnCount() { return this.$table .children('thead') .children('tr') .children('th') .length; } /** * Since the colspan attribute breaks DataTables, restructure * any rows that use it. */ prepareTable() { let columnCount = this.getColumnCount(); this.$table.find('tbody > tr').each(function () { let $tr = $(this); if ($tr.find('td').first().hasAttr('colspan')) { let text = $tr.find('td').first().html(); $tr.find('td').remove(); let style = ''; for (let i = 0; i < columnCount; i++) { $tr.append(` ${text} `); text = ''; style = ' style="display: none;"'; } } if ($tr.hasClass('empty-table-row')) { $tr.children('td').attr('colspan', columnCount); } }); this.$table.find('tfoot > tr > *[colspan="999"]').each(function () { $(this).attr('colspan', columnCount); }); } /** * Bind all row events. * @param {Array} ids */ bindRows(ids) { let me = this; Core.UI.Bind.get(this.$table, this.instance); me.table.rows({ page: 'current' }).every(function () { let $tr = $(this.node()); if ($tr.hasClass('preloaded')) { return; } if ($tr.hasClass('empty-table-row')) { return; } if (ids && !ids.includes($tr.data('id'))) { return; } if (me.instance) { me.instance.bindRow($tr, me.tableId); } }); } /** * Get footer element. * @return {jQuery} */ getFooter() { return $(this.table.table().footer()); } /** * Get an array with all the row ids. */ getRowIds() { let ids = []; this.table.rows().every(function () { let id = $(this.node()).data('id'); if (!id) { return; } ids.push(id); }); return ids; } /** * Select all rows. */ selectAll(withDataParameters) { this.table.rows().every(function () { let $tr = $(this.node()); $tr.find('td > .ui.selected.checkbox > input[type="checkbox"]:not(:checked)').each(function () { if (withDataParameters) { for (let name in withDataParameters) { if ($(this).parent().data(name) != withDataParameters[name]) { return; } } } $(this).trigger('click'); }); }); } /** * Clear selection. */ clearSelection() { this.table.rows().every(function () { let $tr = $(this.node()); $tr.find('td > .ui.selected.checkbox > input[type="checkbox"]:checked').each(function () { $(this).trigger('click'); }); }); } /** * Iterate through table and get row ids that are selected. */ getSelectedIds() { let ids = []; this.table.rows().every(function () { let $tr = $(this.node()); $tr.find('td > .ui.selected.checkbox > input[type="checkbox"]:checked').each(function () { ids.push( $(this).parents('tr[data-id]').data('id') ); }); }); return ids; } /** * Determine if this table is empty. * @return {boolean} */ isEmpty() { let $rows = this.table.rows('[data-id]').eq(0); if (!$rows) { return true; } return $rows.length === 0; } /** * Determine if this table is not empty. * @return {boolean} */ notEmpty() { return this.isEmpty() === false; } /** * Determine if the table is populated. */ populated() { let preloadedRowCount = 0; this.table.rows({ page: 'current' }).every(function () { let $row = $(this.node()); if ($row.hasClass('preloaded')) { preloadedRowCount++; } }); return preloadedRowCount === 0; } /** * Get table toolbar. * @return {JQuery} */ getToolbar() { return $(this.table.table().container()) .children(`.${this.tableId}.toolbar`); } /** * Show/hide empty row based on if there are rows in the table. */ updateBody() { if (this.notEmpty() && !this.$emptyTableRow) { this.$emptyTableRow = $(this.table.row('.empty-table-row').node()); this.table.row('.empty-table-row').remove(); this.table.draw(false); } else if (this.isEmpty() && this.$emptyTableRow && !this.table.row('.empty-table-row').node()) { this.table.row.add(this.$emptyTableRow); this.$emptyTableRow = null; this.table.draw(false); } if (!Boolean(this.$table.data('preload-page-only'))) { let preloadedCount = 0; let populatedCount = 0; this.table.rows().every(function () { let $tr = $(this.node()); if ($tr.data('id')) { if ($tr.hasClass('preloaded')) { preloadedCount++; } else { populatedCount++; } } }); if (preloadedCount) { if (!this.$table.data('populating')) { this.$table.data('populating', 1); if (Boolean(this.$table.data('preload-first'))) { this.showDimmer(); } if (this.getToolbar().find('.loading-table.progress').doesntExist()) { this.getToolbar().append(`
Populating table...
`); } } } if ((preloadedCount + populatedCount) > 0) { let $populatedProgressBar = this.getToolbar().find('.loading-table.progress'); if (preloadedCount > 0) { $populatedProgressBar.progress( 'set percent', populatedCount / (preloadedCount + populatedCount) * 100 ); } else if (this.$table.data('populating')) { this.$table.data('populating', ''); this.hideDimmer(); this.getToolbar().find('.loading-table.progress').parent().remove(); this.table.draw(false); this.table.columns.adjust(); if (this.instance) { this.instance.bindTable( $(this.table.table().container()), this.tableId ); } } } } if (!Boolean(this.$table.data('preload-first')) || this.populated()) { this.table.draw(false); } } /** * Get container object. * @return {jQuery} */ getContainer() { return $(this.table.table().container()) .find('.dataTables_scroll'); } /** * Dim table. */ showDimmer() { this.$table.data( 'populating-dimmer-id', this.getContainer().fade('show') ); } /** * Remove table dimmer. */ hideDimmer() { this.getContainer() .fade( 'hide', this.$table.data('populating-dimmer-id') ); this.$table.data( 'populating-dimmer-id', '' ); } /** * Add a row to the table by id. Then update the table by row id. * @param {string} rowId */ addRow(rowId, callback, uri) { let html = ``; for (let i = 0; i < this.table.columns()[0].length; i++) { html += ''; } html += ''; this.table.row.add($(html)); this.updateBody(); this.loadRow(rowId, callback, null, null, uri); } /** * Delete row and update table. This is called recursively in case there * are multiple rows with the same id. * @param {string} rowId * @param {Boolean} draw */ deleteRow(rowId, draw) { let row = this.table.row(this.getRow(rowId)); if (!row.length) { if (draw !== false) { row.draw(false); } this.updateBody(); return; } row.remove(); this.deleteRow(rowId, draw); } /** * Get row from id. * @param {string} rowId */ getRow(rowId) { let row = this.table.row(`[data-id="${rowId}"]`); let node = row.node(); return $(node); } /** * Redraw row. * @param {string} rowId */ redrawRow(rowId) { this.table.row(`[data-id="${rowId}"]`).draw(false); } /** * Queue all rows that are only preloaded. */ populate() { if (this.getToolbar().notVisible()) { return; } let me = this; let page = (Boolean(this.$table.data('preload-page-only')) ? 'current' : 'all'); this.table.rows({ page: page }).every(function () { let $row = $(this.node()); if ($row.hasClass('preloaded') && !$row.hasClass('loading')) { if (me.populateIds.includes($row.data('id')) === false) { me.populateRow($row.data('id')); } } }); } /** * Trigger row to be populated. * @param {string} rowId */ populateRow(rowId) { if (this.populateIds.includes(rowId)) { return; } this.populateIds.push(rowId); } /** * Load row. * @param {string} rowId * @param {function()} callback * @param {boolean} dontLock * @param {object} rowData * @param {string} uri */ loadRow(rowId, callback, dontLock, rowData, uri) { if (!this.instance) { return; } let $row = this.getRow(rowId); $row.addClass('loading'); if (!dontLock) { this.lockRow(rowId); } Core.API.call({ url: uri ?? this.instance.getURI(), data: { func: 'apiGetTableRow', table_id: this.tableId, row_id: rowId, row_data: rowData, configuration: Page.getParameters() }, callback: (data) => { if (!data.row_id) { data.row_id = rowId; } this.deleteRow(rowId, false); if (data.html) { this.table.row.add($(data.html)); this.unlockRow(rowId, true); } if ($.isFunction(callback)) { callback(); } this.updateBody(); }, fadeParameters: { hide: true } }); } /** * Lock row cells. * @param {string} rowId */ lockRow(rowId) { let me = this; if (!me.rowLockCounts.hasOwnProperty(rowId)) { me.rowLockCounts[rowId] = 0; } me.rowLockCounts[rowId]++; me.getRow(rowId).children('td').each(function () { let $td = $(this); $td.addClass('disabled'); // @todo what if readonly property was set before this? $td.find('input').prop('readonly', 'readonly'); }); } /** * Unlock row cells. * @param {string} rowId * @param {Boolean} force */ unlockRow(rowId, force) { let me = this; me.rowLockCounts[rowId]--; if (force) { me.rowLockCounts[rowId] = 0; } if (me.rowLockCounts[rowId] > 0) { return; } me.getRow(rowId).children('td').each(function () { let $td = $(this); $td.removeClass('disabled'); // @todo remove only if necessary $td.find('input').removeProp('readonly'); }); if (this.populated()) { this.table.draw(false); } } /** * Reload all table rows individually. */ reloadRows() { let me = this; this.table.rows({ page: 'all' }).every(function() { let $row = $(this.node()); if (!$row.data('id')) { return; } me.populateRow($row.data('id')); }); } /** * Reload table. * @param {object} data */ reload(data, callback) { if (!data) { data = {}; } Core.API.call({ url: this.instance.getURI(), data: { func: 'apiGetTable', table_id: this.tableId, parameters: $.extend(Page.getParameters(), data) }, callback: (data) => { if (!data.html) { return; } this.table.clear().destroy(); this.$table.replaceWith(data.html); this.init(this._tableId, this._parameters); this.table.columns.adjust(); if ($.isFunction(callback)) { callback(); } }, $context: this.$table, fadeParameters: { hide: true } }); } };