/** * @param options.$el The jQuery element to render to. Must be in the DOM. * @param options.items An array of items to display in the table * @param options.search Whether to create a search widget * @param options.rowHeader Renderer to call for each row in the table * @param options.rowHeight Table row height * @param height: Height in pixels of table. '564px', * @param options.collapseBreakpoint: 500 * @param options.showHeader: true * @param options.select: true * @param options.responsive: true * @param options.fixedWidth: Fixed table with when responsive is false. '320px' * @param options.columns An array of column descriptors. Each column can have the properties: * visible, name, field, renderer */ morpheus.Table = function (options) { options = morpheus.Table.createOptions(options); this.options = options; if (!options.width) { options.width = options.$el.attr('class'); } var _this = this; var height = options.height; var $gridDiv = $('
'); this.$gridDiv = $gridDiv; $gridDiv.appendTo(options.$el); // all columns (including those that are currently not visible */ var columns = options.columns; this.columns = columns; var visibleColumns = columns.filter(function (c) { return c.visible; }); var grid = new morpheus.Grid({ gridOptions: { select: options.select, rowHeight: options.rowHeight, autoEdit: false, editable: false, autoHeight: options.height === 'auto', enableTextSelectionOnCells: true, }, $el: $gridDiv, items: options.items, columns: visibleColumns }); this.grid = grid; this.searchFunction = null; var searchFilter = { isEmpty: function () { return _this.searchFunction == null; }, init: function () { }, accept: function (item) { return _this.searchFunction(item); } }; // add empty search filter this.grid .getFilter().add(searchFilter); var $header = $('
'); this.$header = $header; var $right = $header.find('.pull-right'); if (options.search) { var tableSearch = new morpheus.TableSearchUI({ $el: $header.find('[name=top]'), $right: $right }); tableSearch.setTable(this); this.tableSearch = tableSearch; } if (options.columnPicker && visibleColumns.length !== this.columns.length) { var select = []; select .push(''); var $select = $(select.join('')); $select.appendTo($right); $select.selectpicker({ iconBase: 'fa', tickIcon: 'fa-check', style: 'btn-default btn-xs' }); $select.on('change', function () { var oldColumns = grid.getColumns().map(function (c) { return c.id; }); var selectedColumnIndices = $select.val(); visibleColumns = []; for (var i = 0; i < selectedColumnIndices.length; i++) { visibleColumns.push(sortedColumns[parseInt(selectedColumnIndices[i])]); } var newColumns = visibleColumns.map(function (c) { return c.id; }); grid.setColumns(visibleColumns); if (newColumns.length > oldColumns.length) { var set = new morpheus.Set(); for (var i = 0; i < newColumns.length; i++) { set.add(newColumns[i]); } for (var i = 0; i < oldColumns.length; i++) { set.remove(oldColumns[i]); } var added = set.values(); grid.setSortColumns([{ columnId: added[0], sortAsc: true }]); } // if column added, sort by added column _this.resize(); _this.redraw(); }); } $header.prependTo(options.$el); var collapsed = false; var lastWidth = -1; var resize = function () { if (!_this.options.responsive) { return; } var gridWidth = options.$el.width(); if (gridWidth === lastWidth) { return; } lastWidth = gridWidth; $gridDiv.css('width', gridWidth + 'px'); // if (options.responsiveHeight) { // var verticalPosition = _this.$gridDiv[0].getBoundingClientRect().top // + window.pageYOffset; // $gridDiv.css('height', // (document.body.clientHeight - verticalPosition) + 'px'); // } if (!collapsed && gridWidth < options.collapseBreakpoint && visibleColumns.length > 1) { collapsed = true; $gridDiv.addClass('slick-stacked'); _this.grid.grid.getOptions().rowHeight = (options.collapsedRowHeight ? options.collapsedRowHeight : options.rowHeight) * visibleColumns.length; // collapse _this.grid.grid .setColumns([{ id: 0, tooltip: function (item, value) { var html = []; for (var i = 0; i < visibleColumns.length; i++) { var text = visibleColumns[i].tooltip(item, visibleColumns[i] .getter(item)); if (text != null && text !== '') { html.push(text); } } return html.join('
'); }, collapsed: true, getter: function (item) { return item; }, formatter: function (row, cell, value, columnDef, dataContext) { var html = []; html .push('
'); if (options.rowHeader) { // e.g. render checkbox html.push(options.rowHeader(dataContext)); html.push('
'); } for (var i = 0; i < visibleColumns.length; i++) { if (i > 0) { html.push('
'); } var c = visibleColumns[i]; html.push(c.name); html.push(':'); var s = c.renderer(dataContext, c .getter(dataContext)); html.push(s); } html.push('
'); return html.join(''); }, sortable: false, name: '' }]); $gridDiv.find('.slick-header').hide(); _this.grid.grid.resizeCanvas(); _this.grid.grid.invalidate(); } else if (collapsed && gridWidth >= options.collapseBreakpoint) { $gridDiv.removeClass('slick-stacked'); collapsed = false; if (options.showHeader) { $gridDiv.find('.slick-header').show(); } _this.grid.grid.getOptions().rowHeight = options.rowHeight; _this.grid.grid.setColumns(visibleColumns); _this.grid.grid.resizeCanvas(); if (options.select) { _this.grid.grid.setSelectedRows(_this.grid.grid .getSelectedRows()); } _this.grid.grid.invalidate(); } else { _this.grid.grid.resizeCanvas(); _this.grid.grid.invalidate(); } _this.grid.maybeAutoResizeColumns(); }; if (!options.showHeader) { $gridDiv.find('.slick-header').hide(); } if (options.responsive) { $(window).on('resize orientationchange', resize); $gridDiv.on('remove', function () { $(window).off('resize', resize); }); resize(); } this.resize = resize; if (visibleColumns.length > 1 && options.items != null && options.items.length > 0) { this.setItems(options.items); } if (!$gridDiv.is(':visible')) { // find 1st parent that is not visible var $parent = $gridDiv; var observer = new MutationObserver(function (mutations) { if (window.getComputedStyle($parent[0]).display !== 'none') { observer.disconnect(); resize(); } }); while ($parent.length > 0) { if (window.getComputedStyle($parent[0]).display === 'none') { break; } $parent = $parent.parent(); } if ($parent.length > 0) { observer.observe($parent[0], { attributes: true, childList: false, characterData: false }); } } } ; morpheus.Table.defaultRenderer = function (item, value) { if (value == null) { return ''; } else if (_.isNumber(value)) { return morpheus.Util.nf(value); } else if (morpheus.Util.isArray(value)) { var s = []; for (var i = 0, length = value.length; i < length; i++) { if (i > 0) { s.push(', '); } var val = value[i]; s.push(value[i]); } return s.join(''); } else { return '' + value; } }; morpheus.Table.prototype = { toText: function () { var text = []; var items = this.getItems(); var columns = this.columns.filter(function (c) { return c.visible; }); for (var j = 0; j < columns.length; j++) { if (j > 0) { text.push('\t'); } text.push(columns[j].name); } text.push('\n'); for (var i = 0; i < items.length; i++) { var item = items[i]; for (var j = 0; j < columns.length; j++) { if (j > 0) { text.push('\t'); } var value = columns[j].getter(item); text.push(morpheus.Util.toString(value)); } text.push('\n'); } return text.join(''); }, setHeight: function (height) { this.options.height = height; if (height === 'auto') { this.$gridDiv.css('height', ''); this.grid.grid.getOptions().autoHeight = true; this.grid.grid.setOptions(this.grid.grid.getOptions()); } else { this.$gridDiv.css('height', height); } this.grid.grid.resizeCanvas(); if (height === 'auto') { var height = this.getItems().length * this.options.rowHeight + this.options.rowHeight; this.$gridDiv.find('.slick-viewport').css('height', height + 'px'); } this.grid.grid.invalidate(); }, setSearchVisible: function (visible) { this.$header.find('[name=search]').css('display', visible ? '' : 'none'); }, autocomplete: function (tokens, response) { var matches = []; var token = tokens != null && tokens.length > 0 ? tokens[tokens.selectionStartIndex] : ''; token = $.trim(token); var columns = this.columns.filter(function (c) { return (c.searchable && c.visible) || c.alwaysSearch; }); var ncolumns = columns.length; var showField = ncolumns > 1; if (token === '') { if (ncolumns <= 1) { return response(matches); } for (var i = 0; i < ncolumns; i++) { var field = columns[i].name; matches.push({ value: field + ':', label: '' + field + ':', show: true }); // show column names } matches .sort(function (a, b) { return (a.value === b.value ? 0 : (a.value < b.value ? -1 : 1)); }); return response(matches); } var field = null; var semi = token.indexOf(':'); var regex = new RegExp('^' + morpheus.Util.escapeRegex(token), 'i'); if (semi > 0) { // field search? if (token.charCodeAt(semi - 1) !== 92) { // \: var possibleField = $.trim(token.substring(0, semi)); if (possibleField.length > 0 && possibleField[0] === '"' && possibleField[token.length - 1] === '"') { possibleField = possibleField.substring(1, possibleField.length - 1); } var columnNameToColumn = new morpheus.Map(); var columnNames = columns.map(function (c) { return c.name; }); for (var i = 0; i < columnNames.length; i++) { columnNameToColumn.set(columnNames[i], columns[i]); } var c = columnNameToColumn.get(possibleField); if (c !== undefined) { token = $.trim(token.substring(semi + 1)); columns = [c]; ncolumns = 1; } } } else if (ncolumns > 1) { var regex = new RegExp('^' + morpheus.Util.escapeRegex(token), 'i'); for (var j = 0; j < ncolumns; j++) { var field = columns[j].name; if (regex.test(field)) { matches.push({ value: field + ':', label: '' + field + ':', show: true }); } } } var set = new morpheus.Set(); var regex = new RegExp('^' + morpheus.Util.escapeRegex(token), 'i'); var items = this.getItems(); var dataTypes = []; // filter numeric columns var filteredColumns = []; columns.forEach(function (c) { var dataType = null; for (var i = 0, nitems = items.length; i < nitems; i++) { var value = c.getter(items[i]); if (value != null) { dataType = morpheus.Util.getDataType(value); break; } } if (dataType === 'string' || dataType === '[string]') { dataTypes.push(dataType); filteredColumns.push(c); } }); columns = filteredColumns; ncolumns = columns.length; var maxSize = matches.length + 10; for (var i = 0, nitems = items.length; i < nitems; i++) { var item = items[i]; for (var j = 0; j < ncolumns; j++) { var field = columns[j].name; var value = columns[j].getter(item); var dataType = dataTypes[j]; if (dataType === '[string]') { var nvalues = value == null ? 0 : value.length; for (var k = 0; k < nvalues; k++) { var val = value[k]; if (regex.test(val) && !set.has(val)) { set.add(val); matches .push({ value: showField ? (field + ':' + val) : val, label: showField ? ('' + field + ':' + '' + val + '') : ('' + val + '') }); } if (matches.length === maxSize) { return response(matches); } } } else { if (regex.test(value) && !set.has(value)) { set.add(value); matches .push({ value: showField ? (field + ':' + value) : value, label: showField ? ('' + field + ':' + '' + value + '') : ('' + value + '') }); if (matches.length === maxSize) { return response(matches); } } } } } return response(matches); }, searchWithPredicates: function (predicates) { if (predicates == null || predicates.length === 0) { this.searchFunction = null; this.grid .setFilter(this.grid .getFilter()); return; } var columns = this.columns.filter(function (c) { return (c.searchable && c.visible) || c.alwaysSearch; }); var columnNameToColumn = new morpheus.Map(); var columnNames = columns.map(function (c) { return c.name; }); for (var i = 0; i < columnNames.length; i++) { columnNameToColumn.set(columnNames[i], columns[i]); } var filteredPredicates = []; var npredicates = predicates.length; for (var i = 0; i < npredicates; i++) { var predicate = predicates[i]; var filterColumnName = predicate.getField(); if (filterColumnName != null) { var column = columnNameToColumn.get(filterColumnName); if (column) { predicate.column = column; filteredPredicates.push(predicate); } } else { filteredPredicates.push(predicate); } } predicates = filteredPredicates; npredicates = predicates.length; var f = function (item) { for (var p = 0; p < npredicates; p++) { var predicate = predicates[p]; var searchColumns; if (predicate.column) { searchColumns = [predicate.column]; } else { searchColumns = columns; } for (var j = 0, ncolumns = searchColumns.length; j < ncolumns; j++) { var value = searchColumns[j].getter(item); if (morpheus.Util.isArray(value)) { var nvalues = value.length; for (var i = 0; i < nvalues; i++) { if (predicate.accept(value[i])) { return true; } } } else { var predicate = predicates[p]; if (predicate.accept(value)) { return true; } } } } return false; }; this.searchFunction = f; this.grid .setFilter(this.grid .getFilter()); }, search: function (text) { if (text === '') { this.searchFunction = null; this.grid .setFilter(this.grid .getFilter()); } else { var tokens = morpheus.Util.getAutocompleteTokens(text); var columns = this.columns.filter(function (c) { return (c.searchable && c.visible) || c.alwaysSearch; }); var columnNames = columns.map(function (c) { return c.name; }); var predicates = morpheus.Util.createSearchPredicates({ tokens: tokens, fields: columnNames }); this.searchWithPredicates(predicates); } }, getSelectedRows: function () { return this.grid.getSelectedRows(); }, getSelectedItems: function () { return this.grid.getSelectedItems(); }, getSelectedItem: function () { return this.grid.getSelectedItem(); }, setSelectedRows: function (rows) { this.grid.setSelectedRows(rows); }, getItems: function (items) { return this.grid.getItems(); }, getAllItemCount: function () { return this.grid.getAllItemCount(); }, getFilteredItemCount: function () { return this.grid.getFilteredItemCount(); }, setFilter: function (f) { this.grid.setFilter(f); }, getFilter: function () { return this.grid.getFilter(); }, setItems: function (items) { this.grid.setItems(items); this.grid.redraw(); // TODO update height? }, redraw: function () { this.grid.redraw(); }, /** * @param evtStr * selectionChanged */ on: function (evtStr, handler) { this.grid.on(evtStr, handler); return this; }, off: function (evtStr, handler) { this.grid.off(evtStr, handler); return this; }, trigger: function (evtStr) { this.grid.trigger(evtStr); } }; morpheus.Table.createOptions = function (options) { options = $.extend(true, {}, { items: [], height: '564px', collapseBreakpoint: 400, showHeader: true, select: true, rowHeader: null, responsive: true, fixedWidth: '320px', columnPicker: true }, options); if (!options.columns) { options.columns = [{ name: '' }]; } var columns = []; options.columns.forEach(function (c, i) { var column = $.extend(true, {}, { id: i, tooltip: function (dataContext, value) { return morpheus.Table.defaultRenderer(dataContext, value); }, formatter: function (row, cell, value, columnDef, dataContext) { var html = []; html.push('
'); if (options.rowHeader && cell === 0) { html.push(options.rowHeader(dataContext)); } html.push(column.renderer(dataContext, value)); html.push('
'); return html.join(''); }, comparator: function (a, b) { var aNaN = (a == null || _.isNumber(a) && isNaN(a)); var bNaN = (b == null || _.isNumber(b) && isNaN(b)); if (aNaN && bNaN) { return 0; } if (aNaN) { return 1; } if (bNaN) { return -1; } if (a.toLowerCase) { a = a.toLowerCase(); } if (b.toLowerCase) { b = b.toLowerCase(); } return (a === b ? 0 : (a < b ? -1 : 1)); }, sortable: true, searchable: true, width: null, name: c.name, renderer: morpheus.Table.defaultRenderer }, c); if (column.visible === undefined) { column.visible = true; } if (!column.getter) { column.getter = column.field == null ? function (item) { return item; } : function (item) { return item[c.field]; }; } columns.push(column); }); options.columns = columns; if (options.columns.length === 1) { options.tableClass = 'slick-table-compact'; } else { options.tableClass = 'slick-bordered-table'; } if (!options.rowHeight) { // options.rowHeight = options.tableClass === 'slick-table-compact' ? 18 // : 20; options.rowHeight = 22; } return options; }; morpheus.TableSearchUI = function (options) { var _this = this; var $search = $(''); $search.appendTo(options.$el); this.$search = $search; this.$searchResults = $(''); this.$showAll = $('
Show' + ' all
'); this.$searchResults.appendTo(options.$right); this.$showAll.appendTo(options.$right); this.$showAll.on('click', function (e) { e.preventDefault(); $search.val(''); _this.table.search(''); _this.table.trigger('showAll', {table: _this.table}); }); $search.on('keyup', _.debounce(function () { _this.table.search($.trim($(this).val())); }, 100)); morpheus.Util.autosuggest({ $el: $search, suggestWhenEmpty: true, filter: function (tokens, response) { _this.table.autocomplete(tokens, response); }, select: function () { _this.table.search($.trim($search.val())); } }); }; morpheus.TableSearchUI.prototype = { updateSearchLabel: function () { var text = 'Showing: ' + morpheus.Util.intFormat(this.table.getFilteredItemCount()) + ' / ' + morpheus.Util.intFormat(this.table.getAllItemCount()); this.$searchResults.html(text); }, setTable: function (table) { this.table = table; var _this = this; table.on('filter', function () { _this.updateSearchLabel(); }); } };