blob: b392b44f16ce1e2be9ccf8b2e2da9925fbe53a72 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014-2018 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
******************************************************************************/
scout.Table = function() {
scout.Table.parent.call(this);
this.autoResizeColumns = false;
this.columnAddable = false;
this.columnLayoutDirty = false;
this.columns = [];
this.contextColumn = null;
this.checkable = false;
this.checkableStyle = scout.Table.CheckableStyle.CHECKBOX;
this.dropType = 0;
this.dropMaximumSize = scout.dragAndDrop.DEFAULT_DROP_MAXIMUM_SIZE;
this.enabled = true;
this.groupingStyle = scout.Table.GroupingStyle.BOTTOM;
this.header = null;
this.headerEnabled = true;
this.headerVisible = true;
this.headerMenusEnabled = true;
this.hasReloadHandler = false;
this.hierarchical = false;
this.hierarchicalStyle = scout.Table.HierarchicalStyle.DEFAULT;
this.keyStrokes = [];
this.keyboardNavigation = true;
this.menus = [];
this.menuBar = null;
this.contextMenu = null;
this.multiCheck = true;
this.multiSelect = true;
this.multilineText = false;
this.scrollToSelection = false;
this.scrollTop = 0;
this.selectedRows = [];
this.sortEnabled = true;
this.tableControls = [];
this.tableStatusVisible = false;
this.footer = null;
this.footerVisible = false;
this.filters = [];
this.rows = [];
this.rootRows = [];
this.visibleRows = [];
this.estimatedRowCount = 0;
this.maxRowCount = 0;
this.truncatedCellTooltipEnabled = null;
this.visibleRowsMap = {}; // visible rows by id
this.rowLevelPadding;
this.rowsMap = {}; // rows by id
this.rowHeight = 0;
this.rowWidth = 0;
this.rowBorderWidth; // read-only, set by _calculateRowBorderWidth(), also used in TableLayout.js
this.rowBorderLeftWidth = 0; // read-only, set by _calculateRowBorderWidth(), also used in TableHeader.js
this.rowBorderRightWidth = 0; // read-only, set by _calculateRowBorderWidth(), also used in TableHeader.js
this.rowIconVisible = false;
this.rowIconColumnWidth = scout.Column.NARROW_MIN_WIDTH;
this.staticMenus = [];
this.selectionHandler = new scout.TableSelectionHandler(this);
this.tooltips = [];
this._filterMap = {};
this._filteredRows = [];
this.tableNodeColumn = null;
this._maxLevel = 0;
this._aggregateRows = [];
this._animationRowLimit = 25;
this._blockLoadThreshold = 25;
this.updateBuffer = new scout.TableUpdateBuffer(this);
// Initial value must be > 0 to make prefSize work (if it is 0, no filler will be generated).
// If rows have a variable height, prefSize is only correct for 10 rows.
// Layout will adjust this value depending on the view port size.
this.viewRangeSize = 10;
this.viewRangeDirty = false;
this.viewRangeRendered = new scout.Range(0, 0);
this.virtual = true;
this._doubleClickSupport = new scout.DoubleClickSupport();
this._permanentHeadSortColumns = [];
this._permanentTailSortColumns = [];
this._filterMenusHandler = this._filterMenus.bind(this);
this._popupOpenHandler = this._onDesktopPopupOpen.bind(this);
this._desktopPropertyChangeHandler = this._onDesktopPropertyChange.bind(this);
this._addWidgetProperties(['tableControls', 'menus', 'keyStrokes', 'staticMenus']);
this.$data = null;
this.$emptyData = null;
};
scout.inherits(scout.Table, scout.Widget);
// TODO [7.0] cgu create StringColumn.js incl. defaultValues from defaultValues.json
scout.Table.HierarchicalStyle = {
DEFAULT: 'default',
STRUCTURED: 'structured'
};
scout.Table.GroupingStyle = {
/**
* Aggregate row is rendered on top of the row-group.
*/
TOP: 'top',
/**
* Aggregate row is rendered on the bottom of the row-group (default).
*/
BOTTOM: 'bottom'
};
scout.Table.CheckableStyle = {
/**
* When row is checked a boolean column with a checkbox is inserted into the table.
*/
CHECKBOX: 'checkbox',
/**
* When a row is checked the table-row is marked as checked. By default a background
* color is set on the table-row when the row is checked.
*/
TABLE_ROW: 'tableRow',
/**
* Like the CHECKBOX Style but a click anywhere on the row triggers the check.
*/
CHECKBOX_TABLE_ROW: 'checkbox_table_row'
};
/**
* This enum defines the reload reasons for a table reload operation
*/
scout.Table.ReloadReason = {
/**
* No specificv reason, just reload data using the current search settings, the current row limits and the current
* filter (Default)
*/
UNSPECIFIED: 'unspecified',
/**
* Some search parameters changed or the search was reset and the search was triggered
*/
SEARCH: 'search',
/**
* The user requested loading more data than his soft limit, up to the application specific hard limit
*/
OVERRIDE_ROW_LIMIT: 'overrideRowLimit',
/**
* The user requested loading no more data than his soft limit;
*/
RESET_ROW_LIMIT: 'resetRowLimit',
/**
* The column structure of the table was changed
*/
ORGANIZE_COLUMNS: 'organizeColumns',
/**
* Any call to IPage#dataChanged
*/
DATA_CHANGED_TRIGGER: 'dataChangedTrigger'
};
scout.Table.SELECTION_CLASSES = 'select-middle select-top select-bottom select-single selected';
scout.Table.prototype._init = function(model) {
scout.Table.parent.prototype._init.call(this, model);
this.resolveConsts([{
property: 'hierarchicalStyle',
constType: scout.Table.HierarchicalStyle
}]);
this._initColumns();
this.rows.forEach(function(row, i) {
this.rows[i] = this._initRow(row);
}, this);
this.setFilters(this.filters);
this._updateRowStructure({
updateTree: true
});
this.menuBar = scout.create('MenuBar', {
parent: this,
menuOrder: new scout.MenuItemsOrder(this.session, 'Table'),
menuFilter: this._filterMenusHandler
});
this.menuBar.bottom();
this._setSelectedRows(this.selectedRows);
this._setKeyStrokes(this.keyStrokes);
this._setMenus(this.menus);
this._setTableControls(this.tableControls);
this._setTableStatus(this.tableStatus);
this._calculateValuesForBackgroundEffect();
this._group();
};
scout.Table.prototype._initRow = function(row) {
if (!(row instanceof scout.TableRow)) {
row.parent = this;
row = scout.create('TableRow', row);
}
this.rowsMap[row.id] = row;
this.trigger('rowInit', {
row: row
});
return row;
};
scout.Table.prototype._initColumns = function() {
this.columns = this.columns.map(function(colModel, index) {
var column = colModel;
column.session = this.session;
if (column instanceof scout.Column) {
column._setTable(this);
} else {
column.table = this;
column = scout.create(column);
}
if (column.index < 0) {
column.index = index;
}
if (column.checkable) {
// set checkable column if this column is the checkable one
this.checkableColumn = column;
}
return column;
}, this);
// Add gui only checkbox column at the beginning
this._setCheckable(this.checkable);
// Add gui only row icon column at the beginning
if (this.rowIconVisible) {
this._insertRowIconColumn();
}
this._calculateTableNodeColumn();
// Sync head and tail sort columns
this._setHeadAndTailSortColumns();
this.columnLayoutDirty = true;
};
scout.Table.prototype._destroy = function() {
this._destroyColumns();
scout.Table.parent.prototype._destroy.call(this);
};
scout.Table.prototype._destroyColumns = function() {
this.columns.forEach(function(column) {
column.destroy();
});
this.checkableColumn = null;
this.columns = [];
};
scout.Table.prototype._calculateTableNodeColumn = function() {
var candidateColumns = this.visibleColumns().filter(function(column) {
return column.nodeColumnCandidate;
});
var tableNodeColumn = scout.arrays.first(candidateColumns);
if (this.tableNodeColumn && this.tableNodeColumn !== tableNodeColumn) {
// restore
this.tableNodeColumn.minWidth = this.tableNodeColumn._initialMinWidth;
}
this.tableNodeColumn = tableNodeColumn;
if (this.tableNodeColumn) {
this.tableNodeColumn._initialMinWidth = this.tableNodeColumn.minWidth;
this.tableNodeColumn.minWidth = this.rowLevelPadding * this._maxLevel + this.tableNodeColumn.tableNodeLevel0CellPadding + 8;
if (this.tableNodeColumn.minWidth > this.tableNodeColumn.width) {
if (this.rendered) {
this.resizeColumn(this.tableNodeColumn, this.tableNodeColumn.minWidth);
} else {
this.tableNodeColumn.width = this.tableNodeColumn.minWidth;
}
}
}
};
/**
* @override
*/
scout.Table.prototype._createLoadingSupport = function() {
return new scout.LoadingSupport({
widget: this,
$container: function() {
return this.$data;
}.bind(this)
});
};
/**
* @override
*/
scout.Table.prototype._createKeyStrokeContext = function() {
return new scout.KeyStrokeContext();
};
/**
* @override
*/
scout.Table.prototype._initKeyStrokeContext = function() {
scout.Table.parent.prototype._initKeyStrokeContext.call(this);
this._initTableKeyStrokeContext();
};
scout.Table.prototype._initTableKeyStrokeContext = function() {
this.keyStrokeContext.registerKeyStroke([
new scout.TableNavigationUpKeyStroke(this),
new scout.TableNavigationDownKeyStroke(this),
new scout.TableNavigationPageUpKeyStroke(this),
new scout.TableNavigationPageDownKeyStroke(this),
new scout.TableNavigationHomeKeyStroke(this),
new scout.TableNavigationEndKeyStroke(this),
new scout.TableNavigationCollapseKeyStroke(this),
new scout.TableNavigationExpandKeyStroke(this),
new scout.TableFocusFilterFieldKeyStroke(this),
new scout.TableStartCellEditKeyStroke(this),
new scout.TableSelectAllKeyStroke(this),
new scout.TableRefreshKeyStroke(this),
new scout.TableToggleRowKeyStroke(this),
new scout.TableCopyKeyStroke(this),
new scout.ContextMenuKeyStroke(this, this.showContextMenu, this),
new scout.AppLinkKeyStroke(this, this.handleAppLinkAction)
]);
// Prevent default action and do not propagate ↓ or ↑ keys if ctrl- or alt-modifier is not pressed.
// Otherwise, an '↑-event' on the first row, or an '↓-event' on the last row will bubble up (because not consumed by table navigation keystrokes) and cause a superior table to move its selection.
// Use case: - outline page table with search form that contains a table field;
// - shift + '↑-event'/'↓-event' are not consumed by a single selection table, and would propagate otherwise;
// - preventDefault because of smartfield, so that the cursor is not moved on first or last row;
this.keyStrokeContext.registerStopPropagationInterceptor(function(event) {
if (!event.ctrlKey && !event.altKey && scout.isOneOf(event.which, scout.keys.UP, scout.keys.DOWN)) {
event.stopPropagation();
event.preventDefault();
}
});
};
scout.Table.prototype._insertBooleanColumn = function() {
// don't add checkbox column when we're in checkableStyle mode
if (this.checkableStyle === scout.Table.CheckableStyle.TABLE_ROW) {
return;
}
var column = scout.create('BooleanColumn', {
session: this.session,
fixedWidth: true,
fixedPosition: true,
guiOnly: true,
nodeColumnCandidate: false,
headerMenuEnabled: false,
showSeparator: false,
width: scout.Column.NARROW_MIN_WIDTH,
table: this
});
scout.arrays.insert(this.columns, column, 0);
this.checkableColumn = column;
};
scout.Table.prototype._insertRowIconColumn = function() {
var position = 0,
column = scout.create('IconColumn', {
session: this.session,
fixedWidth: true,
fixedPosition: true,
guiOnly: true,
nodeColumnCandidate: false,
headerMenuEnabled: false,
showSeparator: false,
width: this.rowIconColumnWidth,
table: this
});
if (this.columns[0] === this.checkableColumn) {
position = 1;
}
scout.arrays.insert(this.columns, column, position);
this.rowIconColumn = column;
};
scout.Table.prototype.handleAppLinkAction = function(event) {
var $appLink = $(event.target);
var column = this._columnAtX($appLink.offset().left);
this._triggerAppLinkAction(column, $appLink.data('ref'));
};
scout.Table.prototype._render = function() {
this.$container = this.$parent.appendDiv('table');
this.htmlComp = scout.HtmlComponent.install(this.$container, this.session);
this.htmlComp.setLayout(new scout.TableLayout(this));
this.htmlComp.pixelBasedSizing = false;
if (this.uiCssClass) {
this.$container.addClass(this.uiCssClass);
}
this.$data = this.$container.appendDiv('table-data');
this.$data.on('mousedown', '.table-row', this._onRowMouseDown.bind(this))
.on('mouseup', '.table-row', this._onRowMouseUp.bind(this))
.on('dblclick', '.table-row', this._onRowDoubleClick.bind(this))
.on('contextmenu', function(event) {
event.preventDefault();
});
this._installScrollbars({
axis: 'both'
});
this._installImageListeners();
this._installCellTooltipSupport();
this.menuBar.render();
// layout bugfix for IE9 (and maybe other browsers)
if (scout.device.tableAdditionalDivRequired) {
// determine @table-cell-padding-left and @table-cell-padding-right (actually the sum)
var test = this.$data.appendDiv('table-cell');
test.text('&nbsp;');
this.cellHorizontalPadding = test.cssPxValue('padding-left') + test.cssPxValue('padding-right');
test.remove();
}
this._calculateRowBorderWidth();
this._updateRowWidth();
this._updateRowHeight();
this._renderViewport();
if (this.scrollToSelection) {
this.revealSelection();
}
this.session.desktop.on('popupOpen', this._popupOpenHandler);
this.session.desktop.on('propertyChange', this._desktopPropertyChangeHandler);
};
scout.Table.prototype._renderProperties = function() {
scout.Table.parent.prototype._renderProperties.call(this);
this._renderTableHeader();
this._renderFooterVisible();
this._renderDropType();
this._renderCheckableStyle();
this._renderHierarchicalStyle();
};
scout.Table.prototype._setCssClass = function(cssClass) {
scout.Table.parent.prototype._setCssClass.call(this, cssClass);
// calculate row level padding
var paddingClasses = ['table-row-level-padding'];
if (this.cssClass) {
paddingClasses.push(this.cssClass);
}
this.setRowLevelPadding(scout.styles.getSize(paddingClasses.reduce(function(acc, cssClass) {
return acc + ' ' + cssClass;
}, ''), 'width', 'width', 15));
};
scout.Table.prototype._remove = function() {
this.session.desktop.off('propertyChange', this._desktopPropertyChangeHandler);
this.session.desktop.off('popupOpen', this._popupOpenHandler);
this._uninstallDragAndDropHandler();
// TODO [7.0] cgu do not delete header, implement according to footer
this.header = null;
this._destroyCellEditorPopup();
this._removeAggregateRows();
this._uninstallImageListeners();
this._uninstallCellTooltipSupport();
this._removeRows();
this.$fillBefore = null;
this.$fillAfter = null;
this.$data = null;
this.$emptyData = null;
scout.Table.parent.prototype._remove.call(this);
};
scout.Table.prototype.setRowLevelPadding = function(rowLevelPadding) {
this.setProperty('rowLevelPadding', rowLevelPadding);
};
scout.Table.prototype._renderRowLevelPadding = function() {
this._rerenderViewport();
};
scout.Table.prototype.setTableControls = function(controls) {
this.setProperty('tableControls', controls);
};
scout.Table.prototype._renderTableControls = function() {
if (this.footer) {
this.footer._renderControls();
}
};
scout.Table.prototype._setTableControls = function(controls) {
var i;
for (i = 0; i < this.tableControls.length; i++) {
this.keyStrokeContext.unregisterKeyStroke(this.tableControls[i]);
}
this._setProperty('tableControls', controls);
for (i = 0; i < this.tableControls.length; i++) {
this.keyStrokeContext.registerKeyStroke(this.tableControls[i]);
}
this._updateFooterVisibility();
this.tableControls.forEach(function(control) {
control.tableFooter = this.footer;
}, this);
};
/**
* When an IMG has been loaded we must update the stored height in the model-row.
* Note: we don't change the width of the row or table.
*/
scout.Table.prototype._onImageLoadOrError = function(event) {
var $target = $(event.target);
if ($target.data('measure') === 'in-progress') {
// Ignore events created by autoOptimizeWidth measurement (see ColumnOptimalWidthMeasurer)
// Using event.stopPropagation() is not possible because the image load event does not bubble
return;
}
var $row = $target.closest('.table-row');
var row = $row.data('row');
if (!row) {
return; // row was removed while loading the image
}
var oldRowHeight = row.height;
row.height = $row.outerHeight(true);
if (oldRowHeight !== row.height) {
this.invalidateLayoutTree();
}
};
scout.Table.prototype._onRowMouseDown = function(event) {
this._doubleClickSupport.mousedown(event);
this._$mouseDownRow = $(event.currentTarget);
this._mouseDownRowId = this._$mouseDownRow.data('row').id;
this._mouseDownColumn = this._columnAtX(event.pageX);
this._$mouseDownRow.window().one('mouseup', function() {
this._$mouseDownRow = null;
this._mouseDownRowId = null;
this._mouseDownColumn = null;
}.bind(this));
this.setContextColumn(this._columnAtX(event.pageX));
this.selectionHandler.onMouseDown(event);
var isRightClick = event.which === 3;
var row = this._$mouseDownRow.data('row');
var $target = $(event.target);
// For checkableStyle TABLE_ROW & CHECKBOX_TABLE_ROW only: check row if left click OR clicked row was not checked yet
if (scout.isOneOf(this.checkableStyle, scout.Table.CheckableStyle.TABLE_ROW, scout.Table.CheckableStyle.CHECKBOX_TABLE_ROW) &&
(!isRightClick || !row.checked) &&
!$(event.target).is('.table-row-control') &&
// Click on BooleanColumns should not trigger a row check. The only exception is if the BooleanColumn is the checkableColumn of this table (handled in BooleanColumn.js)
!($target.hasClass('checkable') || $target.parent().hasClass('checkable'))) {
this.checkRow(row, !row.checked);
}
if (isRightClick) {
this.showContextMenu({
pageX: event.pageX,
pageY: event.pageY
});
return false;
}
};
scout.Table.prototype._onRowMouseUp = function(event) {
var $row, $mouseUpRow, column, $appLink, row,
mouseButton = event.which,
$target = $(event.target);
if (this._doubleClickSupport.doubleClicked()) {
// Don't execute on double click events
return;
}
$mouseUpRow = $(event.currentTarget);
this.selectionHandler.onMouseUp(event, $mouseUpRow);
if (!this._$mouseDownRow || this._mouseDownRowId !== $mouseUpRow.data('row').id) {
// Don't accept if mouse up happens on another row than mouse down, or mousedown didn't happen on a row at all
return;
}
$row = $mouseUpRow;
column = this._columnAtX(event.pageX);
if (column !== this._mouseDownColumn) {
// Don't execute click / appLinks when the mouse gets pressed and moved outside of a cell
return;
}
row = $row.data('row');
// handle expansion
if ($target.hasClass('table-row-control') ||
$target.parent().hasClass('table-row-control')) {
if (row.expanded) {
this.collapseRow(row);
} else {
this.expandRow(row);
}
return;
}
if (mouseButton === 1) {
column.onMouseUp(event, $row);
$appLink = this._find$AppLink(event);
}
if ($appLink) {
this._triggerAppLinkAction(column, $appLink.data('ref'));
} else {
this._triggerRowClick(row, mouseButton);
}
};
scout.Table.prototype._onRowDoubleClick = function(event) {
var $row = $(event.currentTarget),
column = this._columnAtX(event.pageX);
this.doRowAction($row.data('row'), column);
};
scout.Table.prototype.showContextMenu = function(options) {
this.session.onRequestsDone(this._showContextMenu.bind(this, options));
};
scout.Table.prototype._showContextMenu = function(options) {
options = options || {};
if (!this.rendered) { // check needed because function is called asynchronously
return;
}
if (this.selectedRows.length === 0) {
return;
}
var menuItems = this._filterMenus(this.menus, scout.MenuDestinations.CONTEXT_MENU, true, false, ['Header']);
if (menuItems.length === 0) {
return;
}
var pageX = scout.nvl(options.pageX, null);
var pageY = scout.nvl(options.pageY, null);
if (pageX === null || pageY === null) {
var rowToDisplay = this.isRowSelectedAndVisible(this.selectionHandler.lastActionRow) ? this.selectionHandler.lastActionRow : this.getLastSelectedAndVisibleRow();
if (rowToDisplay !== null) {
var $rowToDisplay = rowToDisplay.$row;
var offset = $rowToDisplay.offset();
var dataOffsetBounds = scout.graphics.offsetBounds(this.$data);
offset.left += this.$data.scrollLeft();
pageX = offset.left + 10;
pageY = offset.top + $rowToDisplay.outerHeight() / 2;
pageY = Math.min(Math.max(pageY, dataOffsetBounds.y + 1), dataOffsetBounds.bottom() - 1);
} else {
pageX = this.$data.offset().left + 10;
pageY = this.$data.offset().top + 10;
}
}
// Prevent firing of 'onClose'-handler during contextMenu.open()
// (Can lead to null-access when adding a new handler to this.contextMenu)
if (this.contextMenu) {
this.contextMenu.close();
}
this.contextMenu = scout.create('ContextMenuPopup', {
parent: this,
menuItems: menuItems,
location: {
x: pageX,
y: pageY
},
$anchor: this.$data,
menuFilter: this._filterMenusHandler
});
this.contextMenu.open();
};
scout.Table.prototype.isRowSelectedAndVisible = function(row) {
if (!this.isRowSelected(row) || !row.$row) {
return false;
}
return scout.graphics.offsetBounds(row.$row).intersects(scout.graphics.offsetBounds(this.$data));
};
scout.Table.prototype.getLastSelectedAndVisibleRow = function() {
for (var i = this.viewRangeRendered.to; i >= this.viewRangeRendered.from; i--) {
if (this.isRowSelectedAndVisible(this.rows[i])) {
return this.rows[i];
}
}
return null;
};
scout.Table.prototype.onColumnVisibilityChanged = function(column) {
this.columnLayoutDirty = true;
if (this.rendered) {
this._updateRowWidth();
this._redraw();
this.invalidateLayoutTree();
}
this.trigger('columnStructureChanged');
};
/**
* @override
*/
scout.Table.prototype._onScroll = function() {
var scrollTop = this.$data[0].scrollTop;
var scrollLeft = this.$data[0].scrollLeft;
if (this.scrollTop !== scrollTop) {
this._renderViewport();
}
this.scrollTop = scrollTop;
this.scrollLeft = scrollLeft;
};
scout.Table.prototype._renderTableStatus = function() {
this.trigger('statusChanged');
};
scout.Table.prototype.setContextColumn = function(contextColumn) {
this.setProperty('contextColumn', contextColumn);
};
scout.Table.prototype._hasVisibleTableControls = function() {
return this.tableControls.some(function(control) {
return control.visible;
});
};
scout.Table.prototype.hasAggregateTableControl = function() {
return this.tableControls.some(function(control) {
return control instanceof scout.AggregateTableControl;
});
};
scout.Table.prototype._createHeader = function() {
return scout.create('TableHeader', {
parent: this,
table: this,
enabled: this.headerEnabled,
headerMenusEnabled: this.headerMenusEnabled
});
};
scout.Table.prototype._createFooter = function() {
return scout.create('TableFooter', {
parent: this,
table: this
});
};
scout.Table.prototype._installCellTooltipSupport = function() {
scout.tooltips.install(this.$data, {
parent: this,
selector: '.table-cell',
text: this._cellTooltipText.bind(this),
arrowPosition: 50,
arrowPositionUnit: '%',
nativeTooltip: !scout.device.isCustomEllipsisTooltipPossible()
});
};
scout.Table.prototype._uninstallCellTooltipSupport = function() {
scout.tooltips.uninstall(this.$data);
};
scout.Table.prototype._cellTooltipText = function($cell) {
var cell, tooltipText,
$row = $cell.parent(),
column = this.columnFor$Cell($cell, $row),
row = $row.data('row');
if (row) {
cell = this.cell(column, row);
tooltipText = cell.tooltipText;
}
if (tooltipText) {
return tooltipText;
} else if (this._isTruncatedCellTooltipEnabled(column) && $cell.isContentTruncated()) {
return scout.strings.plainText($cell.html(), {
trim: true
});
}
};
scout.Table.prototype.setTruncatedCellTooltipEnabled = function(truncatedCellTooltipEnabled) {
this.setProperty('truncatedCellTooltipEnabled', truncatedCellTooltipEnabled);
};
/**
* Decides if a cell tooltip should be shown for a truncated cell.
*/
scout.Table.prototype._isTruncatedCellTooltipEnabled = function(column) {
if (this.truncatedCellTooltipEnabled === null) {
// Show cell tooltip only if it is not possible to resize the column.
return !this.headerVisible || !this.headerEnabled || column.fixedWidth;
}
return this.truncatedCellTooltipEnabled;
};
scout.Table.prototype.reload = function(reloadReason) {
if (!this.hasReloadHandler) {
return;
}
this._removeRows();
this._renderFiller();
this._triggerReload(reloadReason);
};
/**
* @override
*/
scout.Table.prototype.setLoading = function(loading) {
if (!loading && this.updateBuffer.isBuffering()) {
// Don't abort loading while buffering, the buffer will do it at the end
return;
}
scout.Table.parent.prototype.setLoading.call(this, loading);
};
scout.Table.prototype.exportToClipboard = function() {
this._triggerClipboardExport();
};
/**
* JS implementation of AbstractTable.execCopy(rows)
*/
scout.Table.prototype._exportToClipboard = function() {
scout.clipboard.copyText({
parent: this,
text: this._selectedRowsToText()
});
};
scout.Table.prototype._selectedRowsToText = function() {
var columns = this.visibleColumns();
return this.selectedRows.map(function(row) {
return columns.map(function(column) {
var cell = column.cell(row);
var text;
if (column instanceof scout.BooleanColumn) {
text = (cell.value ? 'X' : '');
} else if (cell.htmlEnabled) {
text = scout.strings.plainText(cell.text);
} else {
text = cell.text;
}
// unwrap
return scout.strings.nvl(text)
.replace(/\r/g, '')
.replace(/[\n\t]/g, ' ')
.replace(/[ ]+/g, ' ');
}).join('\t');
}).join('\n');
};
scout.Table.prototype.setMultiSelect = function(multiSelect) {
this.setProperty('multiSelect', multiSelect);
};
scout.Table.prototype.toggleSelection = function() {
if (this.selectedRows.length === this.visibleRows.length) {
this.deselectAll();
} else {
this.selectAll();
}
};
scout.Table.prototype.selectAll = function() {
this.selectRows(this.visibleRows);
};
scout.Table.prototype.deselectAll = function() {
this.selectRows([]);
};
scout.Table.prototype.checkAll = function(checked, options) {
var opts = $.extend(options, {
checked: checked
});
this.checkRows(this.visibleRows, opts);
};
scout.Table.prototype.uncheckAll = function(options) {
this.checkAll(false, options);
};
scout.Table.prototype.updateScrollbars = function() {
scout.scrollbars.update(this.$data);
};
scout.Table.prototype._sort = function(animateAggregateRows) {
var sortColumns = this._sortColumns();
// Initialize comparators
if (!this._isSortingPossible(sortColumns)) {
return false;
}
this.clearAggregateRows(animateAggregateRows);
if (!sortColumns.length) {
// no sort column defined.
return true;
}
// add all visible columns as fallback sorting to guarantee same sorting as in Java.
sortColumns = scout.arrays.union(sortColumns, this.columns);
this._sortImpl(sortColumns);
this._triggerRowOrderChanged();
if (this.rendered) {
this._renderRowOrderChanges();
}
// Do it after row order has been rendered, because renderRowOrderChangeds rerenders the whole viewport which would destroy the animation
this._group(animateAggregateRows);
// Sort was possible -> return true
return true;
};
/**
* @returns whether or not sorting is possible. Asks each column to answer this question by calling Column#isSortingPossible.
*/
scout.Table.prototype._isSortingPossible = function(sortColumns) {
return sortColumns.every(function(column) {
return column.isSortingPossible();
});
};
scout.Table.prototype._sortColumns = function() {
var sortColumns = [];
for (var c = 0; c < this.columns.length; c++) {
var column = this.columns[c];
var sortIndex = column.sortIndex;
if (sortIndex >= 0) {
sortColumns[sortIndex] = column;
}
}
return sortColumns;
};
scout.Table.prototype._sortImpl = function(sortColumns) {
var sortFunction = function(row1, row2) {
for (var s = 0; s < sortColumns.length; s++) {
var column = sortColumns[s];
var result = column.compare(row1, row2);
if (column.sortActive && !column.sortAscending) {
// only consider sortAscending flag when sort is active
// columns with !sortActive are always sorted ascending (sortAscending represents last state for those, thus not considered)
result = -result;
}
if (result !== 0) {
return result;
}
}
return 0;
}.bind(this);
if (this.hierarchical) {
// sort tree and set flat row array afterwards.
this._sortHierarchical(sortFunction);
var sortedFlatRows = [];
this.visitRows(function(row) {
sortedFlatRows.push(row);
}.bind(this));
this.rows = sortedFlatRows;
} else {
// sort the flat rows and set the rootRows afterwards.
this.rows.sort(sortFunction);
this.rootRows = this.rows;
}
this._updateRowStructure({
filteredRows: true,
applyFilters: false,
visibleRows: true
});
};
/**
* Pre-order (top-down) traversal of all rows in this table (if hierarchical).
*/
scout.Table.prototype.visitRows = function(visitFunc, rows, level) {
level = scout.nvl(level, 0);
rows = rows || this.rootRows;
rows.forEach(function(row) {
visitFunc(row, level);
this.visitRows(visitFunc, row.childRows, level + 1);
}, this);
};
scout.Table.prototype._sortHierarchical = function(sortFunc, rows) {
rows = rows || this.rootRows;
rows.sort(sortFunc);
rows.forEach(function(row) {
this._sortHierarchical(sortFunc, row.childRows);
}, this);
};
scout.Table.prototype._renderRowOrderChanges = function() {
var animate,
$rows = this.$rows(),
oldRowPositions = {};
// store old position
// animate only if every row is rendered, otherwise some rows would be animated and some not
if ($rows.length === this.visibleRows.length) {
$rows.each(function(index, elem) {
var rowWasInserted = false,
$row = $(elem),
row = $row.data('row');
// Prevent the order animation for newly inserted rows (to not confuse the user)
if (this._insertedRows) {
for (var i = 0; i < this._insertedRows.length; i++) {
if (this._insertedRows[i].id === row.id) {
rowWasInserted = true;
break;
}
}
}
if (!rowWasInserted) {
animate = true;
oldRowPositions[row.id] = $row.offset().top;
}
}.bind(this));
}
this._rerenderViewport();
// If aggregate rows are being removed by animation, rerenderViewport does not delete them -> reorder
// This may happen if grouping gets deactivated and another column will get the new first sort column
this._order$AggregateRows();
// for less than animationRowLimit rows: move to old position and then animate
if (animate) {
$rows = this.$rows();
$rows.each(function(index, elem) {
var $row = $(elem),
row = $row.data('row'),
oldTop = oldRowPositions[row.id];
if (oldTop !== undefined) {
$row.css('top', oldTop - $row.offset().top).animate({
top: 0
}, {
progress: this._triggerRowOrderChanged.bind(this, row, true)
});
}
}.bind(this));
}
};
scout.Table.prototype.setSortEnabled = function(sortEnabled) {
this.setProperty('sortEnabled', sortEnabled);
};
/**
* @param multiSort true to add the column to list of sorted columns. False to use this column exclusively as sort column (reset other columns)
* @param remove true to remove the column from the sort columns
*/
scout.Table.prototype.sort = function(column, direction, multiSort, remove) {
var data, sorted, animateAggregateRows;
multiSort = scout.nvl(multiSort, false);
remove = scout.nvl(remove, false);
// Animate if sort removes aggregate rows
animateAggregateRows = !multiSort;
if (remove) {
this._removeSortColumn(column);
} else {
this._addSortColumn(column, direction, multiSort);
}
if (this.header) {
this.header.onSortingChanged();
}
sorted = this._sort(animateAggregateRows);
data = {
column: column,
sortAscending: column.sortAscending
};
if (remove) {
data.sortingRemoved = true;
}
if (multiSort) {
data.multiSort = true;
}
if (!sorted) {
// Delegate sorting to server when it is not possible on client side
data.sortingRequested = true;
// hint to animate the aggregate after the row order changed event
this._animateAggregateRows = animateAggregateRows;
}
this.trigger('sort', data);
};
scout.Table.prototype._addSortColumn = function(column, direction, multiSort) {
var groupColCount, sortColCount;
direction = scout.nvl(direction, column.sortAscending ? 'asc' : 'desc');
multiSort = scout.nvl(multiSort, true);
this._updateSortIndexForColumn(column, multiSort);
// Reset grouped flag if column should be sorted exclusively
if (!multiSort) {
groupColCount = this._groupedColumns().length;
sortColCount = this._sortColumns().length;
if (sortColCount === 1 && groupColCount === 1) {
// special case: if it is the only sort column and also grouped, do not remove grouped property.
} else {
column.grouped = false;
}
}
column.sortAscending = direction === 'asc' ? true : false;
column.sortActive = true;
};
/**
* Intended to be called for new sort columns.
* Sets the sortIndex of the given column and its siblings.
*/
scout.Table.prototype._updateSortIndexForColumn = function(column, multiSort) {
var deviation,
sortIndex = -1;
if (multiSort) {
// if not already sorted set the appropriate sort index (check for sortIndex necessary if called by _onColumnHeadersUpdated)
if (!column.sortActive || column.sortIndex === -1) {
sortIndex = Math.max(-1, scout.arrays.max(this.columns.map(function(c) {
return (c.sortIndex === undefined || c.initialAlwaysIncludeSortAtEnd) ? -1 : c.sortIndex;
})));
column.sortIndex = sortIndex + 1;
// increase sortIndex for all permanent tail columns (a column has been added in front of them)
this._permanentTailSortColumns.forEach(function(c) {
c.sortIndex++;
});
}
} else {
// do not update sort index for permanent head/tail sort columns, their order is fixed (see ColumnSet.java)
if (!(column.initialAlwaysIncludeSortAtBegin || column.initialAlwaysIncludeSortAtEnd)) {
column.sortIndex = this._permanentHeadSortColumns.length;
}
// remove sort index for siblings (ignore permanent head/tail columns, only if not multi sort)
scout.arrays.eachSibling(this.columns, column, function(siblingColumn) {
if (siblingColumn.sortActive) {
this._removeSortColumnInternal(siblingColumn);
}
}.bind(this));
// set correct sort index for all permanent tail sort columns
deviation = (column.initialAlwaysIncludeSortAtBegin || column.initialAlwaysIncludeSortAtEnd) ? 0 : 1;
this._permanentTailSortColumns.forEach(function(c, index) {
c.sortIndex = this._permanentHeadSortColumns.length + deviation + index;
}, this);
}
};
scout.Table.prototype._removeSortColumn = function(column) {
if (column.initialAlwaysIncludeSortAtBegin || column.initialAlwaysIncludeSortAtEnd) {
return;
}
// Adjust sibling columns with higher index
scout.arrays.eachSibling(this.columns, column, function(siblingColumn) {
if (siblingColumn.sortIndex > column.sortIndex) {
siblingColumn.sortIndex = siblingColumn.sortIndex - 1;
}
});
this._removeSortColumnInternal(column);
};
scout.Table.prototype._removeSortColumnInternal = function(column) {
if (column.initialAlwaysIncludeSortAtBegin || column.initialAlwaysIncludeSortAtEnd) {
return;
}
column.sortActive = false;
column.grouped = false;
column.sortIndex = -1;
};
scout.Table.prototype.isGroupingPossible = function(column) {
var possible = true;
if (this.hierarchical) {
return false;
}
if (!this.sortEnabled) {
// grouping without sorting is not possible
return false;
}
if (this._permanentHeadSortColumns && this._permanentHeadSortColumns.length === 0) {
// no permanent head sort columns. grouping ok.
return true;
}
if (column.initialAlwaysIncludeSortAtBegin) {
possible = true;
scout.arrays.eachSibling(this._permanentHeadSortColumns, column, function(c) {
if (c.sortIndex < column.sortIndex) {
possible = possible && c.grouped;
}
});
return possible;
}
if (column.initialAlwaysIncludeSortAtEnd) {
// it is a tail sort column. Grouping does not make sense.
return false;
}
// column itself is not a head or tail sort column. Therefore, all head sort columns must be grouped.
this._permanentHeadSortColumns.forEach(function(c) {
possible = possible && c.grouped;
});
return possible;
};
scout.Table.prototype.isAggregationPossible = function(column) {
if (!(column instanceof scout.NumberColumn)) {
return false;
}
if (column.grouped) {
// Aggregation is not possible if column is grouped
return false;
}
if (!column.allowedAggregationFunctions || column.allowedAggregationFunctions.length <= 1) {
// Aggregation is not possible if no aggregation functions are allowed or only exactly one aggregation is pre-defined.
return false;
}
// Aggregation is possible if it is grouped by another column or aggregation control is available
return this.isGrouped() || this.hasAggregateTableControl();
};
scout.Table.prototype.changeAggregation = function(column, func) {
this.changeAggregations([column], [func]);
};
scout.Table.prototype.changeAggregations = function(columns, functions) {
columns.forEach(function(column, i) {
var func = functions[i];
if (column.aggregationFunction !== func) {
column.setAggregationFunction(func);
this._triggerAggregationFunctionChanged(column);
}
}, this);
this._group();
};
scout.Table.prototype._addGroupColumn = function(column, direction, multiGroup) {
var sortIndex = -1;
if (!this.isGroupingPossible(column)) {
return;
}
direction = scout.nvl(direction, column.sortAscending ? 'asc' : 'desc');
multiGroup = scout.nvl(multiGroup, true);
if (!(column.initialAlwaysIncludeSortAtBegin || column.initialAlwaysIncludeSortAtEnd)) {
// do not update sort index for permanent head/tail sort columns, their order is fixed (see ColumnSet.java)
if (multiGroup) {
sortIndex = Math.max(-1, scout.arrays.max(this.columns.map(function(c) {
return (c.sortIndex === undefined || c.initialAlwaysIncludeSortAtEnd || !c.grouped) ? -1 : c.sortIndex;
})));
if (!column.sortActive) {
// column was not yet present: insert at determined position
// and move all subsequent nodes by one.
// add just after all other grouping columns in column set.
column.sortIndex = sortIndex + 1;
scout.arrays.eachSibling(this.columns, column, function(siblingColumn) {
if (siblingColumn.sortActive && !(siblingColumn.initialAlwaysIncludeSortAtBegin || siblingColumn.initialAlwaysIncludeSortAtEnd) && siblingColumn.sortIndex > sortIndex) {
siblingColumn.sortIndex++;
}
});
// increase sortIndex for all permanent tail columns (a column has been added in front of them)
this._permanentTailSortColumns.forEach(function(c) {
c.sortIndex++;
});
} else {
// column already sorted, update position:
// move all sort columns between the newly determined sortindex and the old sortindex by one.
scout.arrays.eachSibling(this.columns, column, function(siblingColumn) {
if (siblingColumn.sortActive && !(siblingColumn.initialAlwaysIncludeSortAtBegin || siblingColumn.initialAlwaysIncludeSortAtEnd) &&
(siblingColumn.sortIndex > sortIndex) &&
(siblingColumn.sortIndex < column.sortIndex)) {
siblingColumn.sortIndex++;
}
});
column.sortIndex = sortIndex + 1;
}
} else {
// no multigroup:
sortIndex = this._permanentHeadSortColumns.length;
if (column.sortActive) {
// column already sorted, update position:
// move all sort columns between the newly determined sortindex and the old sortindex by one.
scout.arrays.eachSibling(this.columns, column, function(siblingColumn) {
if (siblingColumn.sortActive && !(siblingColumn.initialAlwaysIncludeSortAtBegin || siblingColumn.initialAlwaysIncludeSortAtEnd) &&
(siblingColumn.sortIndex >= sortIndex) &&
(siblingColumn.sortIndex < column.sortIndex)) {
siblingColumn.sortIndex++;
}
});
column.sortIndex = sortIndex;
} else { //not sorted yet
scout.arrays.eachSibling(this.columns, column, function(siblingColumn) {
if (siblingColumn.sortActive && !(siblingColumn.initialAlwaysIncludeSortAtBegin || siblingColumn.initialAlwaysIncludeSortAtEnd) && siblingColumn.sortIndex >= sortIndex) {
siblingColumn.sortIndex++;
}
});
column.sortIndex = sortIndex;
// increase sortIndex for all permanent tail columns (a column has been added in front of them)
this._permanentTailSortColumns.forEach(function(c) {
c.sortIndex++;
});
}
// remove all other grouped properties:
scout.arrays.eachSibling(this.columns, column, function(siblingColumn) {
if (siblingColumn.sortActive && !(siblingColumn.initialAlwaysIncludeSortAtBegin || siblingColumn.initialAlwaysIncludeSortAtEnd) && siblingColumn.sortIndex >= sortIndex) {
siblingColumn.grouped = false;
}
});
}
column.sortAscending = direction === 'asc' ? true : false;
column.sortActive = true;
} else {
if (column.initialAlwaysIncludeSortAtBegin) {
// do not change order or direction. just set grouped to true.
column.grouped = true;
}
}
column.grouped = true;
};
scout.Table.prototype._removeGroupColumn = function(column) {
column.grouped = false;
if (column.initialAlwaysIncludeSortAtBegin) {
// head sort case: remove all groupings after this column.
this.columns.forEach(function(c) {
if (c.sortIndex >= column.sortIndex) {
c.grouped = false;
}
});
}
this._removeSortColumn(column);
};
scout.Table.prototype._buildRowDiv = function(row) {
var rowWidth = this.rowWidth;
var rowClass = 'table-row';
if (row.cssClass) {
rowClass += ' ' + row.cssClass;
}
if (!row.enabled) {
rowClass += ' disabled';
}
if (row.checked && this.checkableStyle === scout.Table.CheckableStyle.TABLE_ROW) {
rowClass += ' checked';
}
// if a row is not filterAccepted it must be visible since any of its child rows are filter accepted.
if (!row.filterAccepted) {
rowClass += ' filter-not-accepted';
}
if (scout.arrays.empty(row.childRows)) {
rowClass += ' leaf';
}
var i, column,
rowDiv = '<div class="' + rowClass + '" style="width: ' + rowWidth + 'px"' + scout.device.unselectableAttribute.string + '>';
for (i = 0; i < this.columns.length; i++) {
column = this.columns[i];
if (column.isVisible()) {
rowDiv += column.buildCellForRow(row);
}
}
rowDiv += '</div>';
return rowDiv;
};
scout.Table.prototype._calculateRowBorderWidth = function() {
// var $tableRowDummy = this.$data.appendDiv('table-row');
// this.rowBorderLeftWidth = $tableRowDummy.cssBorderLeftWidth();
// this.rowBorderRightWidth = $tableRowDummy.cssBorderRightWidth();
// this.rowBorderWidth = this.rowBorderLeftWidth + this.rowBorderRightWidth;
// $tableRowDummy.remove();
return 1;
};
scout.Table.prototype._updateRowWidth = function() {
this.rowWidth = this.visibleColumns().reduce(function(sum, column) {
return sum + column.width;
}, this.rowBorderWidth);
};
scout.Table.prototype._updateRowHeight = function() {
// var $emptyRow = this.$data.appendDiv('table-row');
// var $emptyAggrRow = this.$data.appendDiv('table-aggregate-row');
//
// $emptyRow.appendDiv('table-cell').html('&nbsp;');
// $emptyAggrRow.appendDiv('table-cell').html('&nbsp;');
// this.rowHeight = $emptyRow.outerHeight(true);
// this.aggregateRowHeight = $emptyAggrRow.outerHeight(true);
// $emptyRow.remove();
// $emptyAggrRow.remove();
this.rowHeight = 30;
this.aggregateRowHeight = 30;
};
/**
* Updates the row heights for every visible row and aggregate row and clears the height of the others
*/
scout.Table.prototype._updateRowHeights = function() {
this.rows.forEach(function(row) {
if (!row.$row) {
row.height = null;
} else {
row.height = row.$row.outerHeight(true);
}
});
this._aggregateRows.forEach(function(aggregateRow) {
if (!aggregateRow.$row) {
aggregateRow.height = null;
} else {
aggregateRow.height = aggregateRow.$row.outerHeight(true);
}
});
};
scout.Table.prototype._renderRowsInRange = function(range) {
var $rows,
rowString = '',
numRowsRendered = 0,
prepend = false;
var rows = this.visibleRows;
if (rows.length === 0) {
return;
}
var maxRange = new scout.Range(0, this.rows.length);
range = maxRange.intersect(range);
if (this.viewRangeRendered.size() > 0 && !range.intersect(this.viewRangeRendered).equals(new scout.Range(0, 0))) {
throw new Error('New range must not intersect with existing.');
}
if (range.to <= this.viewRangeRendered.from) {
prepend = true;
}
var newRange = this.viewRangeRendered.union(range);
if (newRange.length === 2) {
throw new Error('Can only prepend or append rows to the existing range. Existing: ' + this.viewRangeRendered + '. New: ' + newRange);
}
this.viewRangeRendered = newRange[0];
this._removeEmptyData();
// Build $rows (as string instead of jQuery objects due to efficiency reasons)
for (var r = range.from; r < range.to; r++) {
var row = rows[r];
rowString += this._buildRowDiv(row);
numRowsRendered++;
}
// append block of rows
$rows = this.$data.makeElement(rowString);
if (prepend) {
if (this.$fillBefore) {
$rows = $rows.insertAfter(this.$fillBefore);
} else {
$rows = $rows.prependTo(this.$data);
}
} else {
if (this.$fillAfter) {
$rows = $rows.insertBefore(this.$fillAfter);
} else {
$rows = $rows.appendTo(this.$data);
}
}
$rows.each(function(index, rowObject) {
var $row = $(rowObject);
var row = rows[range.from + index];
scout.Table.linkRowToDiv(row, $row);
this._installRow(row);
}.bind(this));
if ($.log.isTraceEnabled()) {
$.log.trace(numRowsRendered + ' new rows rendered from ' + range);
$.log.trace(this._rowsRenderedInfo());
}
};
scout.Table.prototype._rowsRenderedInfo = function() {
var numRenderedRows = this.$rows().length,
renderedRowsRange = '(' + this.viewRangeRendered + ')',
text = numRenderedRows + ' rows rendered ' + renderedRowsRange;
return text;
};
/**
* Moves the row to the top.
*/
scout.Table.prototype.moveRowToTop = function(row) {
var rowIndex = this.rows.indexOf(row);
this.moveRow(rowIndex, 0);
};
/**
* Moves the row to the bottom.
*/
scout.Table.prototype.moveRowToBottom = function(row) {
var rowIndex = this.rows.indexOf(row);
this.moveRow(rowIndex, this.rows.length - 1);
};
/**
* Moves the row one up, disregarding filtered rows.
*/
scout.Table.prototype.moveRowUp = function(row) {
var rowIndex = this.rows.indexOf(row),
targetIndex = rowIndex - 1;
if (this.hierarchical) {
// find index with same parent
var siblings = this.rows.filter(function(candidate) {
return row.parentRow === candidate.parentRow;
}, this),
rowIndexSiblings = siblings.indexOf(row),
sibling = siblings[rowIndexSiblings - 1];
if (sibling) {
targetIndex = this.rows.indexOf(sibling);
} else {
targetIndex = 0;
}
}
this.moveRow(rowIndex, targetIndex);
};
/**
* Moves the row one down, disregarding filtered rows.
*/
scout.Table.prototype.moveRowDown = function(row) {
var rowIndex = this.rows.indexOf(row),
targetIndex = rowIndex + 1;
if (this.hierarchical) {
// find index with same parent
var siblings = this.rows.filter(function(candidate) {
return row.parentRow === candidate.parentRow;
}, this),
rowIndexSiblings = siblings.indexOf(row),
sibling = siblings[rowIndexSiblings + 1];
if (sibling) {
targetIndex = this.rows.indexOf(sibling);
} else {
targetIndex = this.rows.length;
}
}
this.moveRow(rowIndex, targetIndex);
};
/**
* Moves the row one up with respected to filtered rows. Row must be one of the filtered rows.
* @deprecated use moveVisibleRowUp instead
*/
scout.Table.prototype.moveFilteredRowUp = function(row) {
this.moveVisibleRowUp(row);
};
scout.Table.prototype.moveVisibleRowUp = function(row) {
var rowIndex = this.rows.indexOf(row),
visibleIndex = this.visibleRows.indexOf(row),
sibling,
targetIndex;
if (this.hierarchical) {
var siblings = this.visibleRows.filter(function(candidate) {
return row.parentRow === candidate.parentRow;
}, this);
sibling = siblings[siblings.indexOf(row) - 1];
if (sibling) {
targetIndex = this.rows.indexOf(sibling);
} else {
// no previous sibling
return;
}
} else {
sibling = this.visibleRows[visibleIndex - 1];
if (!sibling) {
// no previous sibling
return;
}
targetIndex = this.rows.indexOf(sibling);
}
this.moveRow(rowIndex, targetIndex);
};
/**
* Moves the row one down with respected to filtered rows. Row must be one of the filtered rows.
* @deprecated use moveVisibleRowDown instead
*/
scout.Table.prototype.moveFilteredRowDown = function(row) {
this.moveVisibleRowDown(row);
};
scout.Table.prototype.moveVisibleRowDown = function(row) {
var rowIndex = this.rows.indexOf(row),
visibleIndex = this.visibleRows.indexOf(row),
sibling,
targetIndex;
if (this.hierarchical) {
var siblings = this.visibleRows.filter(function(candidate) {
return row.parentRow === candidate.parentRow;
}, this);
sibling = siblings[siblings.indexOf(row) + 1];
if (sibling) {
targetIndex = this.rows.indexOf(sibling);
} else {
// no following sibling
return;
}
} else {
sibling = this.visibleRows[visibleIndex + 1];
if (!sibling) {
// no following sibling
return;
}
targetIndex = this.rows.indexOf(sibling);
}
this.moveRow(rowIndex, targetIndex);
};
scout.Table.prototype.moveRow = function(sourceIndex, targetIndex) {
var rowCount = this.rows.length;
sourceIndex = Math.max(sourceIndex, 0);
sourceIndex = Math.min(sourceIndex, rowCount - 1);
targetIndex = Math.max(targetIndex, 0);
targetIndex = Math.min(targetIndex, rowCount - 1);
if (sourceIndex === targetIndex) {
return;
}
scout.arrays.move(this.rows, sourceIndex, targetIndex);
this.updateRowOrder(this.rows);
};
scout.Table.prototype._removeRowsInRange = function(range) {
var row, i,
numRowsRemoved = 0,
rows = this.visibleRows;
var maxRange = new scout.Range(0, rows.length);
range = maxRange.intersect(range);
var newRange = this.viewRangeRendered.subtract(range);
if (newRange.length === 2) {
throw new Error('Can only remove rows at the beginning or end of the existing range. ' + this.viewRangeRendered + '. New: ' + newRange);
}
this.viewRangeRendered = newRange[0];
for (i = range.from; i < range.to; i++) {
row = rows[i];
this._removeRow(row);
numRowsRemoved++;
}
if ($.log.isTraceEnabled()) {
$.log.trace(numRowsRemoved + ' rows removed from ' + range + '.');
$.log.trace(this._rowsRenderedInfo());
}
};
scout.Table.prototype.removeAllRows = function() {
if (this.rendered) {
this.$rows().each(function(i, elem) {
var $row = $(elem),
row = $row.data('row');
if ($row.hasClass('hiding')) {
// Do not remove rows which are removed using an animation
// row.$row may already point to a new row -> don't call removeRow to not accidentally remove the new row
return;
}
this._removeRow(row);
}.bind(this));
}
this.viewRangeRendered = new scout.Range(0, 0);
};
/**
*
* @param rows if undefined, all rows are removed
*/
scout.Table.prototype._removeRows = function(rows) {
if (!rows) {
this.removeAllRows();
return;
}
rows = scout.arrays.ensure(rows);
rows.forEach(function(row) {
var rowIndex = this.visibleRows.indexOf(row);
if (rowIndex === -1) {
// row is not visible
return;
}
var rowRendered = !!row.$row;
var rowInViewRange = this.viewRangeRendered.contains(rowIndex);
// Note: these checks can only be done, when table is rendered. When the table is detached it can
// still add rows, but these new rows are not rendered while the table is detached. Thus this check would fail,
// when a row that has been added in detached state is removed again while table is still detached.
if (this.rendered) {
// if row is not rendered but its row-index is inside the view range -> inconsistency
if (!rowRendered && rowInViewRange) {
throw new Error('Inconsistency found while removing row. Row is undefined but inside rendered view range. RowIndex: ' + rowIndex);
}
// if row is rendered but its row-index is not inside the view range -> inconsistency
if (rowRendered && !rowInViewRange) {
throw new Error('Inconsistency found while removing row. Row is rendered but not inside rendered view range. RowIndex: ' + rowIndex);
}
}
this._removeRow(row);
// Adjust view range if row is inside or before range
if (rowInViewRange || rowIndex < this.viewRangeRendered.from) {
if (rowIndex < this.viewRangeRendered.from) {
this.viewRangeRendered.from--;
this.viewRangeRendered.to--;
} else if (rowIndex <= this.viewRangeRendered.to) {
this.viewRangeRendered.to--;
}
}
}.bind(this));
};
/**
* Just removes the row, does NOT adjust this.viewRangeRendered
*/
scout.Table.prototype._removeRow = function(row) {
var $row = row.$row;
if (!$row) {
return;
}
this._destroyTooltipsForRow(row);
this._removeCellEditorForRow(row);
// Do not remove rows which are removed using an animation
if (!$row.hasClass('hiding')) {
$row.remove();
row.$row = null;
}
};
/**
* Animates the rendering of a row by setting it to invisible before doing a slideDown animation. The row needs to already be rendered.
*/
scout.Table.prototype._showRow = function(row) {
var $row = row.$row;
if (!$row) {
return;
}
if ($row.is('.showing')) {
return;
}
$row.hide(); // intentionally don't use setVisible(false) here
$row.addClass('showing');
$row.removeClass('hiding');
$row.stop().slideDown({
duration: 250,
complete: function() {
$row.removeClass('showing');
this.updateScrollbars();
}.bind(this)
});
};
/**
* Animates the removal of a row by doing a slideUp animation. The row will be removed after the animation finishes.
*/
scout.Table.prototype._hideRow = function(row) {
var $row = row.$row;
if (!$row) {
return;
}
if ($row.is('.hiding')) {
return;
}
$row.addClass('hiding');
$row.removeClass('showing');
$row.stop().slideUp({
duration: 250,
complete: function() {
if (!row.$row) {
// ignore already removed rows
return;
}
$row.remove();
if ($row[0] === row.$row[0]) {
// Only set to null if row still is linked to to original $row
// If row got rendered again while the animation is still running, row.$row points to the new $row
row.$row = null;
}
this.updateScrollbars();
}.bind(this)
});
};
/**
* This method should be used after a row is added to the DOM (new rows, updated rows). The 'row'
* is expected to be linked with the corresponding '$row' (row.$row and $row.data('row')).
*/
scout.Table.prototype._installRow = function(row) {
row.height = row.$row.outerHeight(true);
if (row.hasError) {
this._showCellErrorForRow(row);
}
// Reopen editor popup (closed when row was removed)
if (this.cellEditorPopup && !this.cellEditorPopup.rendered && this.cellEditorPopup.row.id === row.id) {
var editorField = this.cellEditorPopup.cell.field;
this.startCellEdit(this.cellEditorPopup.column, row, editorField);
}
};
scout.Table.prototype._calcRowLevelPadding = function(row) {
if (!row) {
return -this.rowLevelPadding;
}
return this._calcRowLevelPadding(row.parentRow) + this.rowLevelPadding;
};
scout.Table.prototype._showCellErrorForRow = function(row) {
var $cells = this.$cellsForRow(row.$row),
that = this;
$cells.each(function(index) {
var $cell = $(this);
var cell = that.cellByCellIndex(index, row);
if (cell.errorStatus) {
that._showCellError(row, $cell, cell.errorStatus);
}
});
};
scout.Table.prototype._showCellError = function(row, $cell, errorStatus) {
var tooltip, opts,
text = errorStatus.message;
opts = {
parent: this,
text: text,
autoRemove: false,
$anchor: $cell,
table: this
};
tooltip = scout.create('TableTooltip', opts);
tooltip.render();
// link to be able to remove it when row gets deleted
tooltip.row = row;
this.tooltips.push(tooltip);
};
/**
* @returns the column at position x (e.g. from event.pageX)
*/
scout.Table.prototype._columnAtX = function(x) {
var columnOffsetRight = 0,
columnOffsetLeft = this.$data.offset().left + this.rowBorderLeftWidth,
scrollLeft = this.$data.scrollLeft();
if (x < columnOffsetLeft) {
// Clicked left of first column (on selection border) --> return first column
return this.columns[0];
}
columnOffsetLeft -= scrollLeft;
var visibleColumns = this.visibleColumns();
var column = scout.arrays.find(visibleColumns, function(column) {
columnOffsetRight = columnOffsetLeft + column.width;
if (x >= columnOffsetLeft && x < columnOffsetRight) {
return true;
}
columnOffsetLeft = columnOffsetRight;
});
if (!column) {
// No column found (clicked right of last column, on selection border) --> return last column
column = visibleColumns[visibleColumns.length - 1];
}
return column;
};
scout.Table.prototype._find$AppLink = function(event) {
// bubble up from target to delegateTarget
var $elem = $(event.target);
var $stop = $(event.delegateTarget);
while ($elem.length > 0) {
if ($elem.hasClass('app-link')) {
return $elem;
}
if ($elem[0] === $stop[0]) {
return null;
}
$elem = $elem.parent();
}
return null;
};
scout.Table.prototype._filterMenus = function(menus, destination, onlyVisible, enableDisableKeyStroke, notAllowedTypes) {
return scout.menus.filterAccordingToSelection('Table', this.selectedRows.length, menus, destination, onlyVisible, enableDisableKeyStroke, notAllowedTypes);
};
scout.Table.prototype.setStaticMenus = function(staticMenus) {
this.setProperty('staticMenus', staticMenus);
this._updateMenuBar();
};
scout.Table.prototype._removeMenus = function() {
// menubar takes care about removal
};
scout.Table.prototype.notifyRowSelectionFinished = function() {
if (this._triggerRowsSelectedPending) {
this._triggerRowsSelected();
this._triggerRowsSelectedPending = false;
}
this.session.onRequestsDone(this._updateMenuBar.bind(this));
};
scout.Table.prototype._triggerRowClick = function(row, mouseButton, column) {
var event = {
row: row,
mouseButton: mouseButton
};
this.trigger('rowClick', event);
};
scout.Table.prototype._triggerRowAction = function(row, column) {
this.trigger('rowAction', {
row: row,
column: column
});
};
/**
* This functions starts the cell editor for the given row and column. Prepare must wait until
* a pending completeCellEdit operation is resolved.
*/
scout.Table.prototype.prepareCellEdit = function(column, row, openFieldPopupOnCellEdit) {
var promise = $.resolvedPromise();
if (this.cellEditorPopup) {
promise = this.cellEditorPopup.waitForCompleteCellEdit();
}
promise.then(this.prepareCellEditInternal.bind(this, column, row, openFieldPopupOnCellEdit));
};
/**
* @param openFieldPopupOnCellEdit when this parameter is set to true, the CellEditorPopup sets an
* additional property 'cellEditor' on the editor-field. The field instance may use this property
* to decide whether or not it should open a popup immediately after it is rendered. This is used
* for Smart- and DateFields.
*/
scout.Table.prototype.prepareCellEditInternal = function(column, row, openFieldPopupOnCellEdit) {
var event = new scout.Event({
column: column,
row: row
});
this.openFieldPopupOnCellEdit = scout.nvl(openFieldPopupOnCellEdit, false);
this.trigger('prepareCellEdit', event);
if (!event.defaultPrevented) {
var field = column.createEditor(row);
this.startCellEdit(column, row, field);
}
};
/**
* @returns returns a cell for the given column and row. Row Icon column and cell icon column don't not have cells --> generate one.
*/
scout.Table.prototype.cell = function(column, row) {
if (column === this.rowIconColumn) {
return scout.create('Cell', {
iconId: row.iconId,
cssClass: scout.strings.join(' ', 'row-icon-cell', row.cssClass)
});
}
if (column === this.checkableColumn) {
return scout.create('Cell', {
value: row.checked,
editable: true,
cssClass: row.cssClass
});
}
return row.cells[column.index];
};
scout.Table.prototype.cellByCellIndex = function(cellIndex, row) {
return this.cell(this.columns[cellIndex], row);
};
scout.Table.prototype.cellValue = function(column, row) {
var cell = this.cell(column, row);
if (!cell) {
return cell;
}
if (cell.value !== undefined) {
return cell.value;
}
return '';
};
scout.Table.prototype.cellText = function(column, row) {
var cell = this.cell(column, row);
if (!cell) {
return '';
}
return cell.text || '';
};
/**
*
* @returns the next editable position in the table, starting from the cell at (currentColumn / currentRow).
* A position is an object containing row and column (cell has no reference to a row or column due to memory reasons).
*/
scout.Table.prototype.nextEditableCellPos = function(currentColumn, currentRow, reverse) {
var pos, startColumnIndex, rowIndex, startRowIndex, predicate,
colIndex = this.columns.indexOf(currentColumn);
startColumnIndex = colIndex + 1;
if (reverse) {
startColumnIndex = colIndex - 1;
}
pos = this.nextEditableCellPosForRow(startColumnIndex, currentRow, reverse);
if (pos) {
return pos;
}
predicate = function(row) {
if (!row.$row) {
return false;
}
startColumnIndex = 0;
if (reverse) {
startColumnIndex = this.columns.length - 1;
}
pos = this.nextEditableCellPosForRow(startColumnIndex, row, reverse);
if (pos) {
return true;
}
}.bind(this);
rowIndex = this.rows.indexOf(currentRow);
startRowIndex = rowIndex + 1;
if (reverse) {
startRowIndex = rowIndex - 1;
}
scout.arrays.findFrom(this.rows, startRowIndex, predicate, reverse);
return pos;
};
scout.Table.prototype.nextEditableCellPosForRow = function(startColumnIndex, row, reverse) {
var cell, column, predicate;
predicate = function(column) {
if (column.guiOnly) {
// does not support tabbing
return false;
}
cell = this.cell(column, row);
return this.enabled && row.enabled && cell.editable;
}.bind(this);
column = scout.arrays.findFrom(this.columns, startColumnIndex, predicate, reverse);
if (column) {
return {
column: column,
row: row
};
}
};
scout.Table.prototype.clearAggregateRows = function(animate) {
// Remove "hasAggregateRow" markers from real rows
this._aggregateRows.forEach(function(aggregateRow) {
if (aggregateRow.prevRow) {
aggregateRow.prevRow.aggregateRowAfter = null;
}
if (aggregateRow.nextRow) {
aggregateRow.nextRow.aggregateRowBefore = null;
}
}, this);
if (this.rendered) {
this._removeAggregateRows(animate);
this._renderSelection(); // fix selection borders
}
this._aggregateRows = [];
};
/**
* Executes the aggregate function with the given funcName for each visible column, but only if the Column
* has that function, which is currently only the case for NumberColumns.
*
* @param states is a reference to an Array containing the results for each column.
* @param row (optional) if set, an additional cell-value parameter is passed to the aggregate function
*/
scout.Table.prototype._forEachVisibleColumn = function(funcName, states, row) {
var value;
this.visibleColumns().forEach(function(column, i) {
if (column[funcName]) {
if (row) {
value = column.cellValueOrTextForCalculation(row);
}
states[i] = column[funcName](states[i], value);
} else {
states[i] = undefined;
}
});
};
scout.Table.prototype._group = function(animate) {
var rows, nextRow, newGroup, firstRow, lastRow,
groupColumns = this._groupedColumns(),
onTop = this.groupingStyle === scout.Table.GroupingStyle.TOP,
states = [];
this.clearAggregateRows();
if (!groupColumns.length) {
return;
}
rows = this.visibleRows;
this._forEachVisibleColumn('aggrStart', states);
rows.forEach(function(row, r) {
if (!firstRow) {
firstRow = row;
}
this._forEachVisibleColumn('aggrStep', states, row);
// test if sum should be shown, if yes: reset sum-array
nextRow = rows[r + 1];
// test if group is finished
newGroup = (r === rows.length - 1) || this._isNewGroup(groupColumns, row, nextRow);
// if group is finished: add group row
if (newGroup) {
// finish aggregation
this._forEachVisibleColumn('aggrFinish', states);
// append sum row
this._addAggregateRow(states,
onTop ? lastRow : row,
onTop ? firstRow : nextRow);
// reset after group
this._forEachVisibleColumn('aggrStart', states);
firstRow = null;
lastRow = row;
}
}.bind(this));
if (this.rendered) {
this._renderAggregateRows(animate);
this._renderSelection(); // fix selection borders
}
};
scout.Table.prototype._isNewGroup = function(groupedColumns, row, nextRow) {
var i, col, newRow = false,
hasCellTextForGroupingFunction;
if (!nextRow) {
return true; //row is last row
}
for (i = 0; i < groupedColumns.length; i++) {
col = groupedColumns[i];
hasCellTextForGroupingFunction = col && col.cellTextForGrouping && typeof col.cellTextForGrouping === 'function';
newRow = newRow || (hasCellTextForGroupingFunction && col.cellTextForGrouping(row) !== col.cellTextForGrouping(nextRow)); // NOSONAR
newRow = newRow || (!hasCellTextForGroupingFunction && this.cellText(col, row) !== this.cellText(col, nextRow));
if (newRow) {
return true;
}
}
return false;
};
scout.Table.prototype._groupedColumns = function() {
return this.columns.filter(function(col) {
return col.grouped;
});
};
/**
* Inserts a new aggregation row between 'prevRow' and 'nextRow'.
*
* @param contents cells of the new aggregate row
* @param prevRow row _before_ the new aggregate row
* @param nextRow row _after_ the new aggregate row
*/
scout.Table.prototype._addAggregateRow = function(contents, prevRow, nextRow) {
var aggregateRow = {
contents: contents.slice(),
prevRow: prevRow,
nextRow: nextRow
};
this._aggregateRows.push(aggregateRow);
if (prevRow) {
prevRow.aggregateRowAfter = aggregateRow;
}
if (nextRow) {
nextRow.aggregateRowBefore = aggregateRow;
}
};
scout.Table.prototype._removeAggregateRows = function(animate) {
if (this._aggregateRows.length === 0) {
return;
}
animate = scout.nvl(animate, false);
if (!animate) {
this._aggregateRows.forEach(function(aggregateRow) {
this._removeRow(aggregateRow);
}, this);
this.updateScrollbars();
} else {
this._aggregateRows.forEach(function(aggregateRow, i) {
this._hideRow(aggregateRow);
}, this);
}
};
scout.Table.prototype._renderAggregateRows = function(animate) {
var onTop = this.groupingStyle === scout.Table.GroupingStyle.TOP,
insertFunc = onTop ? 'insertBefore' : 'insertAfter';
animate = scout.nvl(animate, false);
this._aggregateRows.forEach(function(aggregateRow, r) {
var refRow, $cell, $aggregateRow;
if (aggregateRow.$row) {
// already rendered, no need to update again (necessary for subsequent renderAggregateRows calls (e.g. in insertRows -> renderRows)
return;
}
refRow = (onTop ? aggregateRow.nextRow : aggregateRow.prevRow);
if (!refRow || !refRow.$row) {
return;
}
$aggregateRow = this.$container.makeDiv('table-aggregate-row')
.data('aggregateRow', aggregateRow);
this.visibleColumns().forEach(function(column) {
$cell = $(column.buildCellForAggregateRow(aggregateRow));
$cell.appendTo($aggregateRow);
});
$aggregateRow[insertFunc](refRow.$row).width(this.rowWidth);
aggregateRow.height = $aggregateRow.outerHeight(true);
aggregateRow.$row = $aggregateRow;
if (animate) {
this._showRow(aggregateRow);
}
}, this);
};
scout.Table.prototype.groupColumn = function(column, multiGroup, direction, remove) {
var data, sorted;
multiGroup = scout.nvl(multiGroup, false);
remove = scout.nvl(remove, false);
if (remove) {
this._removeGroupColumn(column);
}
if (!this.isGroupingPossible(column)) {
return;
}
if (!remove) {
this._addGroupColumn(column, direction, multiGroup);
}
if (this.header) {
this.header.onSortingChanged();
}
sorted = this._sort(true);
data = {
column: column,
groupAscending: column.sortAscending
};
if (remove) {
data.groupingRemoved = true;
}
if (multiGroup) {
data.multiGroup = true;
}
if (!sorted) {
// Delegate sorting to server when it is not possible on client side
data.groupingRequested = true;
// hint to animate the aggregate after the row order changed event
this._animateAggregateRows = true;
}
this.trigger('group', data);
};
scout.Table.prototype.removeColumnGrouping = function(column) {
if (column) {
this.groupColumn(column, false, 'asc', true);
}
};
/**
* @returns {boolean} true if at least one column has grouped=true
*/
scout.Table.prototype.isGrouped = function() {
return this.columns.some(function(column) {
return column.grouped;
});
};
scout.Table.prototype.setColumnBackgroundEffect = function(column, effect) {
column.setBackgroundEffect(effect);
};
/**
* Updates the background effect of every column, if column.backgroundEffect is set.
* Meaning: Recalculates the min / max values and renders the background effect again.
*/
scout.Table.prototype._updateBackgroundEffect = function() {
this.columns.forEach(function(column) {
if (!column.backgroundEffect) {
return;
}
column.updateBackgroundEffect();
}, this);
};
/**
* Recalculates the values necessary for the background effect of every column, if column.backgroundEffect is set
*/
scout.Table.prototype._calculateValuesForBackgroundEffect = function() {
this.columns.forEach(function(column) {
if (!column.backgroundEffect) {
return;
}
column.calculateMinMaxValues();
}, this);
};
scout.Table.prototype._markAutoOptimizeWidthColumnsAsDirty = function() {
this.columns.forEach(function(column) {
column.autoOptimizeWidthRequired = true;
});
};
scout.Table.prototype._markAutoOptimizeWidthColumnsAsDirtyIfNeeded = function(autoOptimizeWidthColumns, oldRow, newRow) {
var i,
marked = false;
for (i = autoOptimizeWidthColumns.length - 1; i >= 0; i--) {
var column = autoOptimizeWidthColumns[i];
if (this.cellValue(column, oldRow) !== this.cellValue(column, newRow)) {
column.autoOptimizeWidthRequired = true;
// Remove column from list since it is now marked and does not have to be processed next time
autoOptimizeWidthColumns.splice(i, 1);
marked = true;
}
}
return marked;
};
scout.Table.prototype.setMultiCheck = function(multiCheck) {
this.setProperty('multiCheck', multiCheck);
};
scout.Table.prototype.checkedRows = function() {
return this.rows.filter(function(row) {
return row.checked;
});
};
scout.Table.prototype.checkRow = function(row, checked, options) {
var opts = $.extend(options, {
checked: checked
});
this.checkRows([row], opts);
};
scout.Table.prototype.checkRows = function(rows, options) {
var opts = $.extend({
checked: true,
checkOnlyEnabled: true
}, options);
var checkedRows = [];
// use enabled computed because when the parent of the table is disabled, it should not be allowed to check rows
if (!this.checkable || (!this.enabledComputed && opts.checkOnlyEnabled)) {
return;
}
rows = scout.arrays.ensure(rows);
rows.forEach(function(row) {
if ((!row.enabled && opts.checkOnlyEnabled) || row.checked === opts.checked) {
return;
}
if (!this.multiCheck && opts.checked) {
for (var i = 0; i < this.rows.length; i++) {
if (this.rows[i].checked) {
this.rows[i].checked = false;
checkedRows.push(this.rows[i]);
}
}
}
row.checked = opts.checked;
checkedRows.push(row);
}, this);
if (this.rendered) {
checkedRows.forEach(function(row) {
this._renderRowChecked(row);
}, this);
}
this._triggerRowsChecked(checkedRows);
};
scout.Table.prototype.uncheckRow = function(row, options) {
this.uncheckRows([row], options);
};
scout.Table.prototype.uncheckRows = function(rows, options) {
var opts = $.extend({
checked: false
}, options);
this.checkRows(rows, opts);
};
scout.Table.prototype.isTableNodeColumn = function(column) {
return this.hierarchical && this.tableNodeColumn === column;
};
scout.Table.prototype.collapseRow = function(row) {
this.collapseRows(scout.arrays.ensure(row));
};
scout.Table.prototype.collapseAll = function() {
this.expandRowsInternal(this.rootRows, false, true);
};
scout.Table.prototype.expandAll = function() {
this.expandRowsInternal(this.rootRows, true, true);
};
scout.Table.prototype.collapseRows = function(rows, recursive) {
this.expandRowsInternal(rows, false, recursive);
};
scout.Table.prototype.expandRow = function(row, recursive) {
this.expandRows(scout.arrays.ensure(row));
};
scout.Table.prototype.expandRows = function(rows, recursive) {
this.expandRowsInternal(rows, true, recursive);
};
scout.Table.prototype.expandRowsInternal = function(rows, expanded, recursive) {
var changedRows = [],
rowsForAnimation = [];
rows = rows || this.rootRows;
expanded = scout.nvl(expanded, true);
recursive = scout.nvl(recursive, false);
if (recursive) {
// collect rows
this.visitRows(function(row) {
var changed = row.expanded !== expanded;
if (changed) {
row.expanded = expanded;
changedRows.push(row);
if (row.$row) {
rowsForAnimation.push(row);
}
}
}.bind(this), rows);
} else {
changedRows = rows.filter(function(row) {
var changed = row.expanded !== expanded;
if (changed && row.$row) {
rowsForAnimation.push(row);
}
row.expanded = expanded;
return changed;
});
}
if (changedRows.length === 0) {
return;
}
this._updateRowStructure({
visibleRows: true
});
this._triggerRowsExpanded(changedRows);
if (this.rendered) {
this._renderRowDelta();
rowsForAnimation.forEach(function(row) {
row.animateExpansion();
});
if (rows[0].$row) {
scout.scrollbars.ensureExpansionVisible({
element: rows[0],
$element: rows[0].$row,
$scrollable: this.get$Scrollable(),
isExpanded: function(element) {
return element.expanded;
},
getChildren: function(parent) {
return parent.childRows;
},
defaultChildHeight: this.rowHeight
});
}
}
};
scout.Table.prototype.doRowAction = function(row, column) {
if (this.selectedRows.length !== 1 || this.selectedRows[0] !== row) {
// Only allow row action if the selected row was double clicked because the handler of the event expects a selected row.
// This may happen if the user modifies the selection using ctrl or shift while double clicking.
return;
}
column = column || this.columns[0];
if (column && column.guiOnly) {
column = scout.arrays.find(this.columns, function(col) {
return !col.guiOnly;
});
}
if (!row || !column) {
return;
}
this._triggerRowAction(row, column);
};
scout.Table.prototype.insertRow = function(row) {
this.insertRows([row]);
};
scout.Table.prototype.insertRows = function(rows) {
var wasEmpty = this.rows.length === 0;
// Update model
rows.forEach(function(row, i) {
row = this._initRow(row);
row.status = scout.TableRow.Status.INSERTED;
rows[i] = row;
// Always insert new rows at the end, if the order is wrong a rowOrderChanged event will follow
this.rows.push(row);
}, this);
var filterAcceptedRows = rows.filter(function(row) {
this._applyFiltersForRow(row);
return row.filterAccepted;
}, this);
this._updateRowStructure({
updateTree: true,
filteredRows: true,
applyFilters: false,
visibleRows: true
});
if (filterAcceptedRows.length > 0) {
this._triggerFilter();
}
this._calculateValuesForBackgroundEffect();
this._markAutoOptimizeWidthColumnsAsDirty();
// this event should be triggered before the rowOrderChanged event (triggered by the _sort function).
this._triggerRowsInserted(rows);
this._sortAfterInsert(wasEmpty);
// Update HTML
if (this.rendered) {
if (this.hierarchical) {
this._renderRowOrderChanges();
}
// Remember inserted rows for future events like rowOrderChanged
if (!this._insertedRows) {
this._insertedRows = rows;
setTimeout(function() {
this._insertedRows = null;
}.bind(this), 0);
} else {
scout.arrays.pushAll(this._insertedRows, rows);
}
this.viewRangeDirty = true;
this._renderViewport();
this.invalidateLayoutTree();
}
};
scout.Table.prototype._sortAfterInsert = function(wasEmpty) {
this._sort();
};
scout.Table.prototype.deleteRow = function(row) {
this.deleteRows([row]);
};
scout.Table.prototype.deleteRows = function(rows) {
var invalidate,
filterChanged,
removedRows = [];
this.visitRows(function(row) {
if (!this.rowsMap[row.id]) {
return;
}
removedRows.push(row);
// Update HTML
if (this.rendered) {
// Cancel cell editing if cell editor belongs to a cell of the deleted row
if (this.cellEditorPopup && this.cellEditorPopup.row.id === row.id) {
this.cellEditorPopup.cancelEdit();
}
this._removeRows(row);
invalidate = true;
}
// Update model
scout.arrays.remove(this.rows, row);
scout.arrays.remove(this.visibleRows, row);
if (this._filterCount() > 0 && scout.arrays.remove(this._filteredRows, row)) {
filterChanged = true;
}
delete this.rowsMap[row.id];
if (this.selectionHandler.lastActionRow === row) {
this.selectionHandler.clearLastSelectedRowMarker();
}
}.bind(this), rows);
this.deselectRows(removedRows);
this._updateRowStructure({
updateTree: true,
filteredRows: true,
applyFilters: false,
visibleRows: true
});
if (filterChanged) {
this._triggerFilter();
}
this._group();
this._updateBackgroundEffect();
this._markAutoOptimizeWidthColumnsAsDirty();
this._triggerRowsDeleted(rows);
if (invalidate) {
this._renderViewport();
// Update markers and filler because row may be removed by removeRows. RenderViewport doesn't do it if view range is already correctly rendered.
this._renderRangeMarkers();
this._renderFiller();
this._renderEmptyData();
this.invalidateLayoutTree();
}
};
scout.Table.prototype.deleteAllRows = function() {
var filterChanged = this._filterCount() > 0 && this._filteredRows.length > 0,
rows = this.rows;
// Update HTML
if (this.rendered) {
// Cancel cell editing
if (this.cellEditorPopup) {
this.cellEditorPopup.cancelEdit();
}
this.selectionHandler.clearLastSelectedRowMarker();
this._removeRows();
}
// Update model
this.rows = [];
this.rowsMap = {};
this._filteredRows = [];
this.deselectAll();
this._updateRowStructure({
updateTree: true,
filteredRows: true,
applyFilters: false,
visibleRows: true
});
if (filterChanged) {
this._triggerFilter();
}
this._markAutoOptimizeWidthColumnsAsDirty();
this._group();
this._updateBackgroundEffect();
this._triggerAllRowsDeleted(rows);
// Update HTML
if (this.rendered) {
this._renderFiller();
this._renderViewport();
this._renderEmptyData();
this.invalidateLayoutTree();
}
};
scout.Table.prototype.updateRow = function(row) {
this.updateRows([row]);
};
scout.Table.prototype.updateRows = function(rows) {
if (this.updateBuffer.isBuffering()) {
this.updateBuffer.buffer(rows);
return;
}
var filterChanged, autoOptimizeWidthColumnsDirty;
var autoOptimizeWidthColumns = this.columns.filter(function(column) {
return column.autoOptimizeWidth && !column.autoOptimizeWidthRequired;
});
var rowsToIndex = {};
this.rows.forEach(function(row, index) {
rowsToIndex[row.id] = index;
}, this);
var oldRowsMap = {};
var structureChanged = false;
rows = rows.map(function(row) {
var parentRowId = row.parentRow,
oldRow = this.rowsMap[row.id];
// collect old rows
oldRowsMap[row.id] = oldRow;
if (!oldRow) {
throw new Error('Update event received for non existing row. RowId: ' + row.id);
}
// check structure changes
if (row.parentRow && !scout.objects.isNullOrUndefined(row.parentRow.id)) {
parentRowId = row.parentRow.id;
}
structureChanged = structureChanged || row._parentRowId !== parentRowId;
row = this._initRow(row);
// Check if cell values have changed
if (row.status === scout.TableRow.Status.NON_CHANGED) {
row.cells.some(function(cell, i) {
var oldCell = oldRow.cells[i];
if (!oldCell || oldCell.value !== cell.value) {
row.status = scout.TableRow.Status.UPDATED;
return true; // break "some()" loop
}
});
}
// selection
if (this.selectionHandler.lastActionRow === oldRow) {
this.selectionHandler.lastActionRow = row;
}
scout.arrays.replace(this.selectedRows, oldRow, row);
// replace row use index lookup for performance reasons
this.rows[rowsToIndex[row.id]] = row;
// filter
row.filterAccepted = oldRow.filterAccepted;
if (this._filterCount() > 0) {
filterChanged = this._applyFiltersForRow(row) || filterChanged;
}
// Check if cell content changed and if yes mark auto optimize width column as dirty
autoOptimizeWidthColumnsDirty = this._markAutoOptimizeWidthColumnsAsDirtyIfNeeded(autoOptimizeWidthColumns, oldRow, row);
return row;
}, this);
this._updateRowStructure({
updateTree: true,
filteredRows: true,
applyFilters: false,
visibleRows: true
});
this._triggerRowsUpdated(rows);
if (this.rendered) {
// render row and replace div in DOM
rows.forEach(function(row) {
var oldRow = oldRowsMap[row.id],
$updatedRow;
if (!oldRow.$row) {
return;
}
$updatedRow = $(this._buildRowDiv(row));
$updatedRow.copyCssClasses(oldRow.$row, scout.Table.SELECTION_CLASSES + ' first last');
oldRow.$row.replaceWith($updatedRow);
scout.Table.linkRowToDiv(row, $updatedRow);
this._destroyTooltipsForRow(row);
this._removeCellEditorForRow(row);
this._installRow(row);
}, this);
if (structureChanged) {
this._renderRowOrderChanges();
}
}
if (filterChanged) {
this._triggerFilter();
this._renderRowDelta();
}
this._sortAfterUpdate();
this._updateBackgroundEffect();
this.invalidateLayoutTree(); // this will also update the scroll-bars
};
scout.Table.prototype._sortAfterUpdate = function() {
this._sort();
};
scout.Table.prototype.isHierarchical = function() {
return this.hierarchical;
};
/**
* The given rows must be rows of this table in desired order.
* @param {scout.TableRow[]} rows
*/
scout.Table.prototype.updateRowOrder = function(rows) {
rows = scout.arrays.ensure(rows);
if (rows.length !== this.rows.length) {
throw new Error('Row order may not be updated because lengths of the arrays differ.');
}
// update model (make a copy so that original array stays untouched)
this.rows = rows.slice();
this._updateRowStructure({
updateTree: true,
filteredRows: true,
applyFilters: false,
visibleRows: true
});
this.clearAggregateRows(this._animateAggregateRows);
if (this.rendered) {
this._renderRowOrderChanges();
}
this._triggerRowOrderChanged();
this._group(this._animateAggregateRows);
this._animateAggregateRows = false;
};
scout.Table.prototype._destroyTooltipsForRow = function(row) {
for (var i = this.tooltips.length - 1; i >= 0; i--) {
if (this.tooltips[i].row.id === row.id) {
this.tooltips[i].destroy();
this.tooltips.splice(i, 1);
}
}
};
scout.Table.prototype._removeCellEditorForRow = function(row) {
if (this.cellEditorPopup && this.cellEditorPopup.rendered && this.cellEditorPopup.row.id === row.id) {
this.cellEditorPopup.remove();
}
};
scout.Table.prototype.startCellEdit = function(column, row, field) {
if (!this.rendered) {
this._postRenderActions.push(this.startCellEdit.bind(this, column, row, field));
return;
}
this.trigger('startCellEdit', {
column: column,
row: row,
field: field
});
this.ensureRowRendered(row);
var popup = column.startCellEdit(row, field);
this.cellEditorPopup = popup;
return popup;
};
/**
* @param saveEditorValue when this parameter is set to true, the value of the editor field is set as
* new value on the edited cell. In remote case this parameter is always false, because the cell
* value is updated by an updateRow event instead.
*/
scout.Table.prototype.endCellEdit = function(field, saveEditorValue) {
if (!this.rendered) {
this._postRenderActions.push(this.endCellEdit.bind(this, field, saveEditorValue));
return;
}
// the cellEditorPopup could already be removed by scrolling (out of view range) or be removed by update rows
if (this.cellEditorPopup) {
var context = this.cellEditorPopup;
// Remove the cell-editor popup prior destroying the field, so that the 'cell-editor-popup's focus context is
// uninstalled first and the focus can be restored onto the last focused element of the surrounding focus context.
// Otherwise, if the currently focused field is removed from DOM, the $entryPoint would be focused first, which can
// be avoided if removing the popup first.
this._destroyCellEditorPopup();
// Must store context in a local variable and call setCellValue _after_ cellEditorPopup is set to null
// because in updateRows we check if the popup is still there and start cell editing mode again.
saveEditorValue = scout.nvl(saveEditorValue, false);
if (saveEditorValue) {
this.setCellValue(context.column, context.row, field.value);
}
}
field.destroy();
};
scout.Table.prototype.completeCellEdit = function() {
var field = this.cellEditorPopup.cell.field;
var event = new scout.Event({
field: field,
row: this.cellEditorPopup.row,
column: this.cellEditorPopup.column,
cell: this.cellEditorPopup.celll
});
this.trigger('completeCellEdit', event);
if (!event.defaultPrevented) {
return this.endCellEdit(field, true);
}
};
scout.Table.prototype.cancelCellEdit = function() {
var field = this.cellEditorPopup.cell.field;
var event = new scout.Event({
field: field,
row: this.cellEditorPopup.row,
column: this.cellEditorPopup.column,
cell: this.cellEditorPopup.celll
});
this.trigger('cancelCellEdit', event);
if (!event.defaultPrevented) {
this.endCellEdit(field);
}
};
scout.Table.prototype.scrollTo = function(row, options) {
if (this.viewRangeRendered.size() === 0) {
// Cannot scroll to a row no row is rendered
return;
}
this.ensureRowRendered(row);
scout.scrollbars.scrollTo(this.$data, row.$row, options);
};
scout.Table.prototype.scrollPageUp = function() {
var newScrollTop = Math.max(0, this.$data[0].scrollTop - this.$data.height());
this.setScrollTop(newScrollTop);
};
scout.Table.prototype.scrollPageDown = function() {
var newScrollTop = Math.min(this.$data[0].scrollHeight, this.$data[0].scrollTop + this.$data.height());
this.setScrollTop(newScrollTop);
};
/**
* @override
*/
scout.Table.prototype.setScrollTop = function(scrollTop) {
this.setProperty('scrollTop', scrollTop);
// call _renderViewport to make sure rows are rendered immediately. The browser fires the scroll event handled by onDataScroll delayed
if (this.rendered) {
this._renderViewport();
}
};
/**
* @override
*/
scout.Table.prototype._renderScrollTop = function() {
if (this.rendering) {
// Not necessary to do it while rendering since it will be done by the layout
return;
}
scout.scrollbars.scrollTop(this.$data, this.scrollTop);
};
/**
* @override
*/
scout.Table.prototype.get$Scrollable = function() {
return this.$data;
};
scout.Table.prototype.setScrollToSelection = function(scrollToSelection) {
this.setProperty('scrollToSelection', scrollToSelection);
};
scout.Table.prototype.revealSelection = function() {
if (!this.rendered) {
// Execute delayed because table may be not layouted yet
this.session.layoutValidator.schedulePostValidateFunction(this.revealSelection.bind(this));
return;
}
if (this.selectedRows.length > 0) {
this.scrollTo(this.selectedRows[0]);
}
};
scout.Table.prototype.revealChecked = function() {
var firstCheckedRow = scout.arrays.find(this.rows, function(row) {
return row.checked === true;
});
if (firstCheckedRow) {
this.scrollTo(firstCheckedRow);
}
};
scout.Table.prototype._rowById = function(id) {
return this.rowsMap[id];
};
scout.Table.prototype._rowsByIds = function(ids) {
return ids.map(this._rowById.bind(this));
};
scout.Table.prototype._rowsToIds = function(rows) {
return rows.map(function(row) {
return row.id;
});
};
/**
* render borders and selection of row. default select if no argument or false is passed in deselect
* model has to be updated before calling this method.
*/
scout.Table.prototype._renderSelection = function(rows) {
rows = scout.arrays.ensure(rows || this.selectedRows);
// helper function adds/removes a class for a row only if necessary, return true if classes have been changed
var addOrRemoveClassIfNeededFunc = function($row, condition, classname) {
var hasClass = $row.hasClass(classname);
if (condition && !hasClass) {
$row.addClass(classname);
return true;
} else if (!condition && hasClass) {
$row.removeClass(classname);
return true;
}
return false;
};
for (var i = 0; i < rows.length; i++) { // traditional for loop, elements might be added during loop
var row = rows[i];
if (!row.$row) {
continue;
}
var thisRowSelected = this.selectedRows.indexOf(row) !== -1,
visibleRows = this.visibleRows,
previousIndex = visibleRows.indexOf(row) - 1,
previousRowSelected = previousIndex >= 0 && this.selectedRows.indexOf(visibleRows[previousIndex]) !== -1,
followingIndex = visibleRows.indexOf(row) + 1,
followingRowSelected = followingIndex < visibleRows.length && this.selectedRows.indexOf(visibleRows[followingIndex]) !== -1;
// Don't collapse selection borders if two consecutively selected (real) rows are separated by an aggregation row
if (thisRowSelected && previousRowSelected && row.aggregateRowBefore) {
previousRowSelected = false;
}
if (thisRowSelected && followingRowSelected && row.aggregateRowAfter) {
followingRowSelected = false;
}
// Note: We deliberately use the '+' operator on booleans here! That way, _all_ methods are executed (boolean
// operators might stop in between) and the variable classChanged contains a number > 1 (which is truthy) when
// at least one method call returned true.
var classChanged = 0 +
addOrRemoveClassIfNeededFunc(row.$row, thisRowSelected, 'selected') +
addOrRemoveClassIfNeededFunc(row.$row, thisRowSelected && !previousRowSelected && followingRowSelected, 'select-top') +
addOrRemoveClassIfNeededFunc(row.$row, thisRowSelected && previousRowSelected && !followingRowSelected, 'select-bottom') +
addOrRemoveClassIfNeededFunc(row.$row, thisRowSelected && !previousRowSelected && !followingRowSelected, 'select-single') +
addOrRemoveClassIfNeededFunc(row.$row, thisRowSelected && previousRowSelected && followingRowSelected, 'select-middle');
if (classChanged && previousRowSelected && rows.indexOf(visibleRows[previousIndex]) === -1) {
rows.push(visibleRows[previousIndex]);
}
if (classChanged && followingRowSelected && rows.indexOf(visibleRows[followingIndex]) === -1) {
rows.push(visibleRows[followingIndex]);
}
}
// Make sure the cell editor popup is correctly layouted because selection changes the cell bounds
if (this.cellEditorPopup && this.cellEditorPopup.rendered && this.selectedRows.indexOf(this.cellEditorPopup.row) > -1) {
this.cellEditorPopup.position();
this.cellEditorPopup.pack();
}
};
scout.Table.prototype._removeSelection = function() {
this.selectedRows.forEach(function(row) {
if (!row.$row) {
return;
}
row.$row.select(false);
row.$row.toggleClass(scout.Table.SELECTION_CLASSES, false);
}, this);
};
scout.Table.prototype.addRowToSelection = function(row, ongoingSelection) {
if (this.selectedRows.indexOf(row) > -1) {
return;
}
ongoingSelection = ongoingSelection !== undefined ? ongoingSelection : true;
this.selectedRows.push(row);
if (row.$row && this.rendered) {
row.$row.select(true);
this._renderSelection(row);
if (this.scrollToSelection) {
this.revealSelection();
}
}
this._triggerRowsSelectedPending = true;
if (!ongoingSelection) {
this.notifyRowSelectionFinished();
}
};
scout.Table.prototype.removeRowFromSelection = function(row, ongoingSelection) {
ongoingSelection = ongoingSelection !== undefined ? ongoingSelection : true;
if (scout.arrays.remove(this.selectedRows, row)) {
if (this.rendered) {
this._renderSelection(row);
}
if (!ongoingSelection) {
this._triggerRowsSelected();
} else {
this._triggerRowsSelectedPending = true;
}
}
};
scout.Table.prototype.selectRow = function(row, debounceSend) {
this.selectRows(row, debounceSend);
};
scout.Table.prototype.selectRows = function(rows, debounceSend) {
// Exclude rows that are currently not visible because of a filter (they cannot be selected)
rows = scout.arrays.ensure(rows).filter(function(row) {
return !!this.visibleRowsMap[row.id];
}, this);
var selectedEqualRows = scout.arrays.equalsIgnoreOrder(rows, this.selectedRows);
// TODO [7.0] cgu: maybe make sure selectedRows are in correct order, this would make logic in AbstractTableNavigationKeyStroke or renderSelection easier
// but requires some effort (remember rowIndex, keep array in order after sort, ... see java Table)
if (selectedEqualRows) {
return;
}
if (this.rendered) {
this._removeSelection();
}
if (!this.multiSelect && rows.length > 1) {
rows = [rows[0]];
}
this.selectedRows = rows; // (Note: direct assignment is safe because the initial filtering created a copy of the original array)
this._triggerRowsSelected(debounceSend);
this._updateMenuBar();
if (this.rendered) {
this._renderSelection();
if (this.scrollToSelection) {
this.revealSelection();
}
}
};
scout.Table.prototype.deselectRow = function(row) {
this.deselectRows(row);
};
scout.Table.prototype.deselectRows = function(rows) {
rows = scout.arrays.ensure(rows);
var selectedRows = this.selectedRows.slice(); // copy
if (scout.arrays.removeAll(selectedRows, rows)) {
this.selectRows(selectedRows);
}
};
scout.Table.prototype.isRowSelected = function(row) {
return this.selectedRows.indexOf(row) > -1;
};
scout.Table.prototype._filterCount = function() {
return Object.keys(this._filterMap).length;
};
scout.Table.prototype.filteredRows = function() {
return this._filteredRows;
};
scout.Table.prototype.$rows = function(includeAggrRows) {
var selector = '.table-row';
if (includeAggrRows) {
selector += ', .table-aggregate-row';
}
return this.$data.find(selector);
};
scout.Table.prototype.$aggregateRows = function() {
return this.$data.find('.table-aggregate-row');
};
/**
* @returns {scout.TableRow} the first selected row of this table or null when no row is selected
*/
scout.Table.prototype.selectedRow = function() {
if (this.selectedRows.length > 0) {
return this.selectedRows[0];
}
return null;
};
scout.Table.prototype.$selectedRows = function() {
if (!this.$data) {
return $();
}
return this.$data.find('.selected');
};
scout.Table.prototype.$cellsForColIndex = function(colIndex, includeAggrRows) {
var selector = '.table-row > div:nth-of-type(' + colIndex + ')';
if (includeAggrRows) {
selector += ', .table-aggregate-row > div:nth-of-type(' + colIndex + ')';
}
return this.$data.find(selector);
};
scout.Table.prototype.$cellsForColIndexWidthFix = function(colIndex, includeAggrRows) {
var selector = '.table-row > div:nth-of-type(' + colIndex + ') > .width-fix ';
if (includeAggrRows) {
selector += ', .table-aggregate-row > div:nth-of-type(' + colIndex + ') > .width-fix';
}
return this.$data.find(selector);
};
scout.Table.prototype.$cellsForRow = function($row) {
return $row.children('.table-cell');
};
scout.Table.prototype.$cell = function(column, $row) {
var columnIndex = column;
if (typeof column !== 'number') {
columnIndex = this.visibleColumns().indexOf(column);
}
return $row.children('.table-cell').eq(columnIndex);
};
scout.Table.prototype.columnById = function(columnId) {
return scout.arrays.find(this.columns, function(column) {
return column.id === columnId;
});
};
/**
* @param {$} $cell the $cell to get the column for
* @param {$} [$row] the $row which contains the $cell. If not passed it will be determined automatically
* @returns {scout.Column} the column for the given $cell
*/
scout.Table.prototype.columnFor$Cell = function($cell, $row) {
$row = $row || $cell.closest('.table-row');
var cellIndex = this.$cellsForRow($row).index($cell);
return this.visibleColumns()[cellIndex];
};
scout.Table.prototype.columnsByIds = function(columnIds) {
return columnIds.map(this.columnById.bind(this));
};
scout.Table.prototype.getVisibleRows = function() {
return this.visibleRows;
};
scout.Table.prototype._updateRowStructure = function(options) {
var updateTree = scout.nvl(options.updateTree, false),
updateFilteredRows = scout.nvl(options.filteredRows, updateTree),
applyFilters = scout.nvl(options.applyFilters, updateFilteredRows),
updateVisibleRows = scout.nvl(options.visibleRows, updateFilteredRows);
if (updateTree) {
this._rebuildTreeStructure();
}
if (updateFilteredRows) {
this._updateFilteredRows(applyFilters);
}
if (updateVisibleRows) {
this._updateVisibleRows();
}
};
scout.Table.prototype._rebuildTreeStructure = function() {
var hierarchical = false;
this.rows.forEach(function(row) {
row.childRows = [];
hierarchical = hierarchical || !scout.objects.isNullOrUndefined(row.parentRow);
}, this);
if (!hierarchical) {
this.rootRows = this.rows;
this.hierarchical = hierarchical;
return;
}
this.hierarchical = hierarchical;
this.rootRows = [];
this.rows.forEach(function(row) {
var parentRow;
if (scout.objects.isNullOrUndefined(row.parentRow)) {
// root row
row.parentRow = null;
row._parentRowId = null;
this.rootRows.push(row);
return;
}
if (!scout.objects.isNullOrUndefined(row.parentRow.id)) {
parentRow = this.rowsMap[row.parentRow.id];
} else {
// expect id
parentRow = this.rowsMap[row.parentRow];
}
if (parentRow) {
row.parentRow = parentRow;
row._parentRowId = parentRow.id;
parentRow.childRows.push(row);
} else {
// do not allow unresolvable parent rows.
throw new Error('Parent row of ' + row + ' can not be resolved.');
}
}, this);
// traverse row tree to have minimal order of rows.
this._maxLevel = 0;
this.rows = [];
this.visitRows(function(row, level) {
row._hierarchyLevel = level;
this._maxLevel = Math.max(level, this._maxLevel);
this.rows.push(row);
}.bind(this));
this._calculateTableNodeColumn();
};
scout.Table.prototype._updateFilteredRows = function(applyFilters, changed) {
changed = !!changed;
applyFilters = scout.nvl(applyFilters, true);
this._filteredRows = this.rows.filter(function(row) {
if (applyFilters) {
changed = this._applyFiltersForRow(row) || changed;
}
return row.filterAccepted;
}, this);
if (changed) {
this._triggerFilter();
}
};
scout.Table.prototype._updateVisibleRows = function() {
this.visibleRows = this._computeVisibleRows();
// rebuild the rows by id map of visible rows
this.visibleRowsMap = this.visibleRows.reduce(function(map, row) {
map[row.id] = row;
return map;
}, {});
if (this.initialized) {
// deselect not visible rows
this.deselectRows(this.selectedRows.filter(function(selectedRow) {
return !this.visibleRowsMap[selectedRow.id];
}, this));
}
};
scout.Table.prototype._computeVisibleRows = function(rows) {
var visibleRows = [];
rows = rows || this.rootRows;
rows.forEach(function(row) {
var visibleChildRows = this._computeVisibleRows(row.childRows);
if (row.filterAccepted) {
visibleRows.push(row);
} else if (visibleChildRows.length > 0) {
visibleRows.push(row);
}
row._expandable = visibleChildRows.length > 0;
if (row.expanded) {
visibleRows = visibleRows.concat(visibleChildRows);
}
}, this);
return visibleRows;
};
scout.Table.prototype.visibleChildRows = function(row) {
return row.childRows.filter(function(child) {
return !!this.visibleRowsMap[child.id];
}, this);
};
scout.Table.prototype._renderRowDelta = function() {
if (!this.rendered) {
return;
}
var renderedRows = [];
this.$rows().each(function(i, elem) {
var $row = $(elem),
row = $row.data('row');
if (this.visibleRows.indexOf(row) < 0) {
// remove animated
this._hideRow(row);
} else {
renderedRows.push(row);
}
}.bind(this));
this._rerenderViewport();
// Rows removed by an animation are still there, new rows were appended -> reset correct row order
this._order$Rows().insertAfter(this.$fillBefore);
// Also make sure aggregate rows are at the correct position (_renderAggregateRows does nothing because they are already rendered)
this._order$AggregateRows();
this.$rows().each(function(i, elem) {
var $row = $(elem),
row = $row.data('row');
if ($row.hasClass('hiding')) {
// Do not remove rows which are removed using an animation
// row.$row may already point to a new row -> don't call removeRow to not accidentally remove the new row
return;
}
if (renderedRows.indexOf(row) < 0) {
this._showRow(row);
}
}.bind(this));
this._renderScrollTop();
this._renderEmptyData();
};
scout.Table.prototype.filter = function() {
this._updateRowStructure({
filteredRows: true
});
this._renderRowDelta();
this._group();
this.revealSelection();
};
/**
* Sorts the given $rows according to the row index
*/
scout.Table.prototype._order$Rows = function($rows) {
// Find rows using jquery because
// this.filteredRows() may be empty but there may be $rows which are getting removed by animation
$rows = $rows || this.$rows();
return $rows.sort(function(elem1, elem2) {
var $row1 = $(elem1),
$row2 = $(elem2),
row1 = $row1.data('row'),
row2 = $row2.data('row');
return this.rows.indexOf(row1) - this.rows.indexOf(row2);
}.bind(this));
};
scout.Table.prototype._order$AggregateRows = function($rows) {
// Find aggregate rows using jquery because
// this._aggregateRows may be empty but there may be $aggregateRows which are getting removed by animation
$rows = $rows || this.$aggregateRows();
$rows.each(function(i, elem) {
var $aggrRow = $(elem),
aggregateRow = $aggrRow.data('aggregateRow');
if (!aggregateRow || !aggregateRow.prevRow) {
return;
}
$aggrRow.insertAfter(aggregateRow.prevRow.$row);
});
};
scout.Table.prototype._rowAcceptedByFilters = function(row) {
for (var key in this._filterMap) { // NOSONAR
var filter = this._filterMap[key];
if (!filter.accept(row)) {
return false;
}
}
return true;
};
/**
* @returns {Boolean} true if row state has changed, false if not
*/
scout.Table.prototype._applyFiltersForRow = function(row) {
if (this._rowAcceptedByFilters(row)) {
if (!row.filterAccepted) {
row.filterAccepted = true;
return true;
}
} else {
if (row.filterAccepted) {
row.filterAccepted = false;
return true;
}
}
return false;
};
/**
* @returns {String[]} labels of the currently active TableUserFilters
*/
scout.Table.prototype.filteredBy = function() {
var filteredBy = [];
for (var key in this._filterMap) { // NOSONAR
var filter = this._filterMap[key];
if (filter instanceof scout.TableUserFilter) {
filteredBy.push(filter.createLabel());
}
}
return filteredBy;
};
scout.Table.prototype.resetUserFilter = function() {
var filter;
for (var key in this._filterMap) { // NOSONAR
filter = this._filterMap[key];
if (filter instanceof scout.TableUserFilter) {
this.removeFilterByKey(key);
}
}
// reset rows
this.filter();
this._triggerFilterReset();
};
scout.Table.prototype.resizeToFit = function(column, maxWidth) {
if (column.fixedWidth) {
return;
}
var returnValue = column.calculateOptimalWidth();
if (scout.objects.isPlainObject(returnValue)) {
// Function returned a promise -> delay resizing
returnValue.always(this._resizeToFit.bind(this, column, maxWidth));
} else {
this._resizeToFit(column, maxWidth, returnValue);
}
};
scout.Table.prototype._resizeToFit = function(column, maxWidth, calculatedSize) {
if (calculatedSize === -1) {
// Calculation has been aborted -> don't resize
return;
}
if (maxWidth && maxWidth > 0 && calculatedSize > maxWidth) {
calculatedSize = maxWidth;
}
if (scout.device.isInternetExplorer() && calculatedSize !== column.minWidth) {
calculatedSize++;
}
if (column.width !== calculatedSize) {
this.resizeColumn(column, calculatedSize);
}
column.autoOptimizeWidthRequired = false;
this._triggerColumnResizedToFit();
};
/**
* @param filter object with createKey() and accept()
*/
scout.Table.prototype.addFilter = function(filter) {
var key = filter.createKey();
if (!key) {
throw new Error('key has to be defined');
}
this._filterMap[key] = filter;
this.trigger('filterAdded', {
filter: filter
});
};
scout.Table.prototype.removeFilter = function(filter) {
this.removeFilterByKey(filter.createKey());
};
scout.Table.prototype.removeFilterByKey = function(key) {
if (!key) {
throw new Error('key has to be defined');
}
var filter = this._filterMap[key];
if (!filter) {
return;
}
delete this._filterMap[key];
this.trigger('filterRemoved', {
filter: filter
});
};
scout.Table.prototype.getFilter = function(key) {
if (!key) {
throw new Error('key has to be defined');
}
return this._filterMap[key];
};
/**
* Resizes the given column to the new size.
*
* @param column
* column to resize
* @param width
* new column size
*/
scout.Table.prototype.resizeColumn = function(column, width) {
if (column.fixedWidth) {
return;
}
width = Math.floor(width);
column.width = width;
var visibleColumnIndex = this.visibleColumns().indexOf(column);
if (visibleColumnIndex !== -1) {
var colNum = visibleColumnIndex + 1;
this.$cellsForColIndex(colNum, true)
.css('min-width', width)
.css('max-width', width);
if (scout.device.tableAdditionalDivRequired) {
this.$cellsForColIndexWidthFix(colNum, true)
.css('max-width', (width - this.cellHorizontalPadding - 2 /* unknown IE9 extra space */ ));
// same calculation in scout.Column.prototype.buildCellForRow;
}
this._updateRowWidth();
this.$rows(true)
.css('width', this.rowWidth);
// If resized column contains cells with wrapped text, view port needs to be updated
// Remove row height for non rendered rows because it may have changed due to resizing (wrap text)
this._updateRowHeights();
this._renderFiller();
this._renderViewport();
this.updateScrollbars();
this._renderEmptyData();
}
this._triggerColumnResized(column);
};
scout.Table.prototype.moveColumn = function(column, visibleOldPos, visibleNewPos, dragged) {
// If there are fixed columns, don't allow moving the column onto the other side of the fixed columns
visibleNewPos = this._considerFixedPositionColumns(visibleOldPos, visibleNewPos);
// Translate position of 'visible columns' array to position in 'all columns' array
var visibleColumns = this.visibleColumns();
var newColumn = visibleColumns[visibleNewPos];
var newPos = this.columns.indexOf(newColumn);
scout.arrays.remove(this.columns, column);
scout.arrays.insert(this.columns, column, newPos);
visibleColumns = this.visibleColumns();
visibleNewPos = visibleColumns.indexOf(column); // we must re-evaluate visible columns
this._calculateTableNodeColumn();
this._triggerColumnMoved(column, visibleOldPos, visibleNewPos, dragged);
// move aggregated rows
this._aggregateRows.forEach(function(aggregateRow) {
scout.arrays.move(aggregateRow.contents, visibleOldPos, visibleNewPos);
});
// move cells
if (this.rendered) {
this._rerenderViewport();
}
};
/**
* Ensures the given newPos does not pass a fixed column boundary (necessary when moving columns)
*/
scout.Table.prototype._considerFixedPositionColumns = function(visibleOldPos, visibleNewPos) {
var fixedColumnIndex = -1;
if (visibleNewPos > visibleOldPos) {
// move to right
fixedColumnIndex = scout.arrays.findIndexFrom(this.visibleColumns(), visibleOldPos, function(col) {
return col.fixedPosition;
});
if (fixedColumnIndex > -1) {
visibleNewPos = Math.min(visibleNewPos, fixedColumnIndex - 1);
}
} else {
// move to left
fixedColumnIndex = scout.arrays.findIndexFromReverse(this.visibleColumns(), visibleOldPos, function(col) {
return col.fixedPosition;
});
if (fixedColumnIndex > -1) {
visibleNewPos = Math.max(visibleNewPos, fixedColumnIndex + 1);
}
}
return visibleNewPos;
};
scout.Table.prototype._renderColumnOrderChanges = function(oldColumnOrder) {
var column, i, j, $orderedCells, $cell, $cells, that = this,
$row;
if (this.header) {
this.header.onOrderChanged(oldColumnOrder);
}
// move cells
this.$rows(true).each(function() {
$row = $(this);
$orderedCells = $();
$cells = $row.children();
for (i = 0; i < that.columns.length; i++) {
column = that.columns[i];
//Find $cell for given column
for (j = 0; j < oldColumnOrder.length; j++) {
if (oldColumnOrder[j] === column) {
$cell = $cells[j];
break;
}
}
$orderedCells.push($cell);
}
$row.prepend($orderedCells);
});
};
scout.Table.prototype._triggerRowsInserted = function(rows) {
this.trigger('rowsInserted', {
rows: rows
});
};
scout.Table.prototype._triggerRowsDeleted = function(rows) {
this.trigger('rowsDeleted', {
rows: rows
});
};
scout.Table.prototype._triggerRowsUpdated = function(rows) {
this.trigger('rowsUpdated', {
rows: rows
});
};
scout.Table.prototype._triggerAllRowsDeleted = function(rows) {
this.trigger('allRowsDeleted', {
rows: rows
});
};
scout.Table.prototype._triggerRowsSelected = function(debounce) {
this.trigger('rowsSelected', {
debounce: debounce
});
};
scout.Table.prototype._triggerRowsChecked = function(rows) {
this.trigger('rowsChecked', {
rows: rows
});
};
scout.Table.prototype._triggerRowsExpanded = function(rows) {
this.trigger('rowsExpanded', {
rows: rows
});
};
scout.Table.prototype._triggerFilter = function() {
this.trigger('filter');
};
scout.Table.prototype._triggerFilterReset = function() {
this.trigger('filterReset');
};
scout.Table.prototype._triggerAppLinkAction = function(column, ref) {
this.trigger('appLinkAction', {
column: column,
ref: ref
});
};
scout.Table.prototype._triggerReload = function(reloadReason) {
this.trigger('reload', {
reloadReason: reloadReason
});
};
scout.Table.prototype._triggerClipboardExport = function() {
var event = new scout.Event();
this.trigger('clipboardExport', event);
if (!event.defaultPrevented) {
this._exportToClipboard();
}
};
scout.Table.prototype._triggerRowOrderChanged = function(row, animating) {
var event = {
row: row,
animating: animating
};
this.trigger('rowOrderChanged', event);
};
scout.Table.prototype._triggerColumnResized = function(column) {
var event = {
column: column
};
this.trigger('columnResized', event);
};
scout.Table.prototype._triggerColumnResizedToFit = function(column) {
var event = {
column: column
};
this.trigger('columnResizedToFit', event);
};
scout.Table.prototype._triggerColumnMoved = function(column, oldPos, newPos, dragged) {
var event = {
column: column,
oldPos: oldPos,
newPos: newPos,
dragged: dragged
};
this.trigger('columnMoved', event);
};
scout.Table.prototype._triggerAggregationFunctionChanged = function(column) {
var event = {
column: column
};
this.trigger('aggregationFunctionChanged', event);
};
scout.Table.prototype.setHeaderVisible = function(visible) {
this.setProperty('headerVisible', visible);
};
scout.Table.prototype._renderHeaderVisible = function() {
this._renderTableHeader();
};
scout.Table.prototype.setHeaderEnabled = function(headerEnabled) {
this.setProperty('headerEnabled', headerEnabled);
};
scout.Table.prototype._renderHeaderEnabled = function() {
// Rebuild the table header when this property changes
this._removeTableHeader();
this._renderTableHeader();
};
scout.Table.prototype.setHeaderMenusEnabled = function(headerMenusEnabled) {
this.setProperty('headerMenusEnabled', headerMenusEnabled);
if (this.header) {
this.header.setHeaderMenusEnabled(this.headerMenusEnabled);
}
};
scout.Table.prototype.hasPermanentHeadOrTailSortColumns = function() {
return this._permanentHeadSortColumns.length !== 0 || this._permanentTailSortColumns.length !== 0;
};
scout.Table.prototype._setHeadAndTailSortColumns = function() {
// find all sort columns (head and tail sort columns should always be included)
var sortColumns = this.columns.filter(function(c) {
return c.sortIndex >= 0;
});
sortColumns.sort(function(a, b) {
return a.sortIndex - b.sortIndex;
});
this._permanentHeadSortColumns = [];
this._permanentTailSortColumns = [];
sortColumns.forEach(function(c) {
if (c.initialAlwaysIncludeSortAtBegin) {
this._permanentHeadSortColumns.push(c);
} else if (c.initialAlwaysIncludeSortAtEnd) {
this._permanentTailSortColumns.push(c);
}
}, this);
};
scout.Table.prototype.setRowIconVisible = function(rowIconVisible) {
this.setProperty('rowIconVisible', rowIconVisible);
};
scout.Table.prototype._setRowIconVisible = function(rowIconVisible) {
this._setProperty('rowIconVisible', rowIconVisible);
var column = this.rowIconColumn;
if (this.rowIconVisible && !column) {
this._insertRowIconColumn();
this._calculateTableNodeColumn();
this.trigger('columnStructureChanged');
} else if (!this.rowIconVisible && column) {
scout.arrays.remove(this.columns, column);
this.rowIconColumn = null;
this._calculateTableNodeColumn();
this.trigger('columnStructureChanged');
}
};
scout.Table.prototype.setRowIconColumnWidth = function(width) {
this.setProperty('rowIconColumnWidth', width);
};
scout.Table.prototype._setRowIconColumnWidth = function(width) {
this._setProperty('rowIconColumnWidth', width);
var column = this.rowIconColumn;
if (column) {
column.width = width;
}
};
scout.Table.prototype._setSelectedRows = function(selectedRows) {
if (typeof selectedRows[0] === 'string') {
selectedRows = this._rowsByIds(selectedRows);
}
this._setProperty('selectedRows', selectedRows);
};
scout.Table.prototype.setMenus = function(menus) {
this.setProperty('menus', menus);
};
scout.Table.prototype._setMenus = function(menus, oldMenus) {
this.updateKeyStrokes(menus, oldMenus);
this._setProperty('menus', menus);
this._updateMenuBar();
if (this.header) {
this.header.updateMenuBar();
}
};
scout.Table.prototype._updateMenuBar = function() {
var notAllowedTypes = ['Header'];
var menuItems = this._filterMenus(this.menus, scout.MenuDestinations.MENU_BAR, false, true, notAllowedTypes);
menuItems = this.staticMenus.concat(menuItems);
this.menuBar.setMenuItems(menuItems);
if (this.contextMenu) {
var contextMenuItems = this._filterMenus(this.menus, scout.MenuDestinations.CONTEXT_MENU, true, false, ['Header']);
this.contextMenu.updateMenuItems(contextMenuItems);
}
};
scout.Table.prototype._setKeyStrokes = function(keyStrokes) {
this.updateKeyStrokes(keyStrokes, this.keyStrokes);
this._setProperty('keyStrokes', keyStrokes);
};
scout.Table.prototype.setFilters = function(filters) {
var filter;
for (var key in this._filterMap) { // NOSONAR
filter = this._filterMap[key];
if (filter instanceof scout.TableUserFilter) {
this.removeFilterByKey(key);
}
}
if (filters) {
filters.forEach(function(filter) {
filter = this._ensureFilter(filter);
this.addFilter(filter);
}, this);
}
};
scout.Table.prototype._ensureFilter = function(filter) {
if (filter instanceof scout.TableUserFilter) {
return filter;
}
if (filter.column) {
filter.column = this.columnById(filter.column);
}
filter.table = this;
filter.session = this.session;
return scout.create(filter);
};
scout.Table.prototype.setTableStatus = function(status) {
this.setProperty('tableStatus', status);
};
scout.Table.prototype._setTableStatus = function(status) {
status = scout.Status.ensure(status);
this._setProperty('tableStatus', status);
};
scout.Table.prototype.setTableStatusVisible = function(visible) {
this.setProperty('tableStatusVisible', visible);
this._updateFooterVisibility();
};
scout.Table.prototype._updateFooterVisibility = function() {
this.setFooterVisible(this.tableStatusVisible || this._hasVisibleTableControls());
};
scout.Table.prototype.setHierarchicalStyle = function(style) {
this.setProperty('hierarchicalStyle', style);
};
scout.Table.prototype._renderHierarchicalStyle = function() {
this.$container.toggleClass('structured', scout.Table.HierarchicalStyle.STRUCTURED === this.hierarchicalStyle);
};
scout.Table.prototype.setFooterVisible = function(visible) {
this._setProperty('footerVisible', visible);
if (visible && !this.footer) {
this.footer = this._createFooter();
}
// relink table controls to new footer
this.tableControls.forEach(function(control) {
control.tableFooter = this.footer;
}, this);
if (this.rendered) {
this._renderFooterVisible();
}
if (!visible && this.footer) {
this.footer.destroy();
this.footer = null;
}
};
/**
* Renders the background effect of every column, if column.backgroundEffect is set
*/
scout.Table.prototype._renderBackgroundEffect = function() {
this.columns.forEach(function(column) {
if (!column.backgroundEffect) {
return;
}
column._renderBackgroundEffect();
}, this);
};
scout.Table.prototype._renderRowChecked = function(row) {
if (!this.checkable) {
return;
}
if (!row.$row) {
return;
}
var $styleElem;
if (this.checkableStyle === scout.Table.CheckableStyle.TABLE_ROW) {
$styleElem = row.$row;
} else {
if (!this.checkableColumn) {
throw new Error('checkableColumn not set');
}
$styleElem = this.checkableColumn.$checkBox(row.$row);
}
$styleElem.toggleClass('checked', row.checked);
};
scout.Table.prototype.setCheckable = function(checkable) {
this.setProperty('checkable', checkable);
};
scout.Table.prototype._setCheckable = function(checkable) {
this._setProperty('checkable', checkable);
this._updateCheckableColumn();
};
scout.Table.prototype._updateCheckableColumn = function() {
var column = this.checkableColumn;
var showCheckBoxes = this.checkable && scout.isOneOf(this.checkableStyle, scout.Table.CheckableStyle.CHECKBOX, scout.Table.CheckableStyle.CHECKBOX_TABLE_ROW);
if (showCheckBoxes && !column) {
this._insertBooleanColumn();
this._calculateTableNodeColumn();
this.trigger('columnStructureChanged');
} else if (!showCheckBoxes && column && column.guiOnly) {
scout.arrays.remove(this.columns, column);
this.checkableColumn = null;
this._calculateTableNodeColumn();
this.trigger('columnStructureChanged');
}
};
scout.Table.prototype._renderCheckable = function() {
this.columnLayoutDirty = true;
this._updateRowWidth();
this._redraw();
this.invalidateLayoutTree();
};
scout.Table.prototype.setCheckableStyle = function(checkableStyle) {
this.setProperty('checkableStyle', checkableStyle);
};
scout.Table.prototype._setCheckableStyle = function(checkableStyle) {
this._setProperty('checkableStyle', checkableStyle);
this._updateCheckableColumn();
};
scout.Table.prototype._renderCheckableStyle = function() {
this.$container.toggleClass('checkable', scout.isOneOf(this.checkableStyle, scout.Table.CheckableStyle.TABLE_ROW, scout.Table.CheckableStyle.CHECKBOX_TABLE_ROW));
this.$container.toggleClass('table-row-check', this.checkableStyle === scout.Table.CheckableStyle.TABLE_ROW);
if (this.rendered) {
this._redraw();
}
};
scout.Table.prototype._renderRowIconVisible = function() {
this.columnLayoutDirty = true;
this._updateRowWidth();
this._redraw();
this.invalidateLayoutTree();
};
scout.Table.prototype._renderRowIconColumnWidth = function() {
if (!this.rowIconVisible) {
return;
}
this._renderRowIconVisible();
};
scout.Table.prototype.setGroupingStyle = function(groupingStyle) {
this.setProperty('groupingStyle', groupingStyle);
};
scout.Table.prototype._setGroupingStyle = function(groupingStyle) {
this._setProperty('groupingStyle', groupingStyle);
this._group();
};
scout.Table.prototype._renderGroupingStyle = function() {
this._rerenderViewport();
};
scout.Table.prototype._redraw = function() {
this._rerenderHeaderColumns();
this._rerenderViewport();
};
scout.Table.prototype._rerenderHeaderColumns = function() {
if (this.header) {
this.header.rerenderColumns();
this.invalidateLayoutTree();
}
};
scout.Table.prototype._renderTableHeader = function() {
var changed = false;
if (this.headerVisible && !this.header) {
this.header = this._createHeader();
this.header.render();
this._renderEmptyData();
changed = true;
} else if (!this.headerVisible && this.header) {
this._removeTableHeader();
this._removeEmptyData();
changed = true;
}
this.$container.toggleClass('header-invisible', !this.header);
if (changed) {
this.invalidateLayoutTree();
}
};
scout.Table.prototype._removeTableHeader = function() {
if (this.header) {
this.header.destroy();
this.header = null;
}
};
/**
* @param width optional width of emptyData, if omitted the width is set to the header's scrollWidth.
*/
scout.Table.prototype._renderEmptyData = function() {
if (!this.header || this.visibleRows.length > 0) {
return;
}
if (!this.$emptyData) {
this.$emptyData = this.$data.appendDiv().html('&nbsp;');
}
this.$emptyData
.css('min-width', this.rowWidth)
.css('max-width', this.rowWidth);
this.updateScrollbars();
};
scout.Table.prototype._removeEmptyData = function() {
if (this.header && this.visibleRows.length === 0) {
return;
}
if (this.$emptyData) {
this.$emptyData.remove();
this.$emptyData = null;
this.updateScrollbars();
}
};
scout.Table.prototype._renderFooterVisible = function() {
if (!this.footer) {
return;
}
if (this.footerVisible) {
this._renderFooter();
} else {
this._removeFooter();
}
this.invalidateLayoutTree();
};
scout.Table.prototype._renderFooter = function() {
if (this.footer.rendered) {
return;
}
this.footer.render();
};
scout.Table.prototype._removeFooter = function() {
if (!this.footer.rendered) {
return;
}
this.footer.remove();
};
/**
* @override Widget.js
*/
scout.Table.prototype._renderEnabled = function() {
scout.Table.parent.prototype._renderEnabled.call(this);
var enabled = this.enabled;
this.$data.setEnabled(enabled);
this.$container.setTabbable(enabled);
if (this.rendered) {
// Enable/disable all checkboxes
this.$rows().each(function() {
var $row = $(this),
row = $row.data('row');
$row.find('input').setEnabled(enabled && row.enabled);
});
}
};
/**
* @override Widget.js
*/
scout.Table.prototype._renderDisabledStyle = function() {
scout.Table.parent.prototype._renderDisabledStyle.call(this);
this._renderDisabledStyleInternal(this.$data);
};
scout.Table.prototype.setAutoResizeColumns = function(autoResizeColumns) {
this.setProperty('autoResizeColumns', autoResizeColumns);
};
scout.Table.prototype._renderAutoResizeColumns = function() {
if (this.autoResizeColumns) {
this.columnLayoutDirty = true;
this.invalidateLayoutTree();
}
};
scout.Table.prototype.setMultilineText = function(multilineText) {
this.setProperty('multilineText', multilineText);
};
scout.Table.prototype._renderMultilineText = function() {
this._markAutoOptimizeWidthColumnsAsDirty();
this._redraw();
this.invalidateLayoutTree();
};
scout.Table.prototype._renderDropType = function() {
if (this.dropType) {
this._installDragAndDropHandler();
} else {
this._uninstallDragAndDropHandler();
}
};
scout.Table.prototype._installDragAndDropHandler = function(event) {
if (this.dragAndDropHandler) {
return;
}
this.dragAndDropHandler = scout.dragAndDrop.handler(this, {
supportedScoutTypes: scout.dragAndDrop.SCOUT_TYPES.FILE_TRANSFER,
dropType: function() {
return this.dropType;
}.bind(this),
dropMaximumSize: function() {
return this.dropMaximumSize;
}.bind(this),
additionalDropProperties: function(event) {
var $target = $(event.currentTarget);
var properties = {
rowId: ''
};
if ($target.hasClass('table-row')) {
var row = $target.data('row');
properties.rowId = row.id;
}
return properties;
}.bind(this)
});
this.dragAndDropHandler.install(this.$container, '.table-data,.table-row');
};
scout.Table.prototype._uninstallDragAndDropHandler = function(event) {
if (!this.dragAndDropHandler) {
return;
}
this.dragAndDropHandler.uninstall();
this.dragAndDropHandler = null;
};
/**
* This listener is used to invalidate table layout when an image icon has been loaded (which happens async in the browser).
*/
scout.Table.prototype._installImageListeners = function() {
this._imageLoadListener = this._onImageLoadOrError.bind(this);
// Image events don't bubble -> use capture phase instead
this.$data[0].addEventListener('load', this._imageLoadListener, true);
this.$data[0].addEventListener('error', this._imageLoadListener, true);
};
scout.Table.prototype._uninstallImageListeners = function() {
this.$data[0].removeEventListener('load', this._imageLoadListener, true);
this.$data[0].removeEventListener('error', this._imageLoadListener, true);
};
/**
* Calculates the optimal view range size (number of rows to be rendered).
* It uses the default row height to estimate how many rows fit in the view port.
* The view range size is this value * 2.
*/
scout.Table.prototype.calculateViewRangeSize = function() {
// Make sure row height is up to date (row height may be different after zooming)
this._updateRowHeight();
if (this.rowHeight === 0) {
throw new Error('Cannot calculate view range with rowHeight = 0');
}
return Math.ceil(this.$data.outerHeight() / this.rowHeight) * 2;
};
scout.Table.prototype.setViewRangeSize = function(viewRangeSize) {
if (this.viewRangeSize === viewRangeSize) {
return;
}
this._setProperty('viewRangeSize', viewRangeSize);
if (this.rendered) {
this._renderViewport();
}
};
scout.Table.prototype._calculateCurrentViewRange = function() {
var rowIndex,
scrollTop = this.$data[0].scrollTop,
maxScrollTop = this.$data[0].scrollHeight - this.$data[0].clientHeight;
if (maxScrollTop === 0) {
// no scrollbars visible
rowIndex = 0;
} else {
rowIndex = this._rowIndexAtScrollTop(scrollTop);
}
return this._calculateViewRangeForRowIndex(rowIndex);
};
/**
* Returns the index of the row which is at position scrollTop.
*/
scout.Table.prototype._rowIndexAtScrollTop = function(scrollTop) {
var height = 0,
index = -1;
this.visibleRows.some(function(row, i) {
height += this._heightForRow(row);
if (scrollTop < height) {
index = i;
return true;
}
}.bind(this));
return index;
};
scout.Table.prototype._heightForRow = function(row) {
var height = 0,
aggregateRow = row.aggregateRowAfter;
if (row.height) {
height = row.height;
} else {
height = this.rowHeight;
}
// Add height of aggregate row as well
if (aggregateRow) {
if (aggregateRow.height) {
height += aggregateRow.height;
} else {
height += this.aggregateRowHeight;
}
}
return height;
};
/**
* Returns a range of size this.viewRangeSize. Start of range is rowIndex - viewRangeSize / 4.
* -> 1/4 of the rows are before the viewport 2/4 in the viewport 1/4 after the viewport,
* assuming viewRangeSize is 2*number of possible rows in the viewport (see calculateViewRangeSize).
*/
scout.Table.prototype._calculateViewRangeForRowIndex = function(rowIndex) {
// regular / non-virtual scrolling? -> all rows are already rendered in the DOM
if (!this.virtual) {
return new scout.Range(0, this.visibleRows.length);
}
var viewRange = new scout.Range(),
quarterRange = Math.floor(this.viewRangeSize / 4),
diff;
viewRange.from = Math.max(rowIndex - quarterRange, 0);
viewRange.to = Math.min(viewRange.from + this.viewRangeSize, this.visibleRows.length);
// Try to use the whole viewRangeSize (extend from if necessary)
diff = this.viewRangeSize - viewRange.size();
if (diff > 0) {
viewRange.from = Math.max(viewRange.to - this.viewRangeSize, 0);
}
return viewRange;
};
/**
* Calculates and renders the rows which should be visible in the current viewport based on scroll top.
*/
scout.Table.prototype._renderViewport = function() {
if (this._renderViewportBlocked) {
return;
}
if (this.visibleColumns().length === 0) {
return;
}
if (!this.$container.isEveryParentVisible()) {
// If the table is invisible, the height of the rows cannot be determined.
// In that case, the table won't be layouted either -> as soon as it will be layouted, renderViewport will be called again
return;
}
var viewRange = this._calculateCurrentViewRange();
this._renderViewRange(viewRange);
};
scout.Table.prototype._rerenderViewport = function() {
this._removeRows();
this._removeAggregateRows();
this._renderFiller();
this._renderViewport();
};
scout.Table.prototype._renderViewRangeForRowIndex = function(rowIndex) {
var viewRange = this._calculateViewRangeForRowIndex(rowIndex);
this._renderViewRange(viewRange);
};
/**
* Renders the rows visible in the viewport and removes the other rows
*/
scout.Table.prototype._renderViewRange = function(viewRange) {
if (viewRange.from === this.viewRangeRendered.from && viewRange.to === this.viewRangeRendered.to && !this.viewRangeDirty) {
// Range already rendered -> do nothing
return;
}
this._removeRangeMarkers();
var rangesToRender = viewRange.subtract(this.viewRangeRendered);
var rangesToRemove = this.viewRangeRendered.subtract(viewRange);
rangesToRemove.forEach(function(range) {
this._removeRowsInRange(range);
}.bind(this));
rangesToRender.forEach(function(range) {
this._renderRowsInRange(range);
}.bind(this));
// check if at least last and first row in range got correctly rendered
if (this.viewRangeRendered.size() > 0) {
var rows = this.visibleRows;
var firstRow = rows[this.viewRangeRendered.from];
var lastRow = rows[this.viewRangeRendered.to - 1];
if (!firstRow.$row || !lastRow.$row) {
throw new Error('Rows not rendered as expected. ' + this.viewRangeRendered + '. First: ' + firstRow.$row + '. Last: ' + lastRow.$row);
}
}
this._renderRangeMarkers();
this._removeAggregateRows();
this._renderAggregateRows();
this._renderFiller();
this._renderEmptyData();
this._renderBackgroundEffect();
this._renderSelection();
this.viewRangeDirty = false;
};
scout.Table.prototype._removeRangeMarkers = function() {
this._modifyRangeMarkers('removeClass');
};
scout.Table.prototype._renderRangeMarkers = function() {
this._modifyRangeMarkers('addClass');
};
scout.Table.prototype._modifyRangeMarkers = function(funcName) {
if (this.viewRangeRendered.size() === 0) {
return;
}
var visibleRows = this.visibleRows;
modifyRangeMarker(visibleRows[this.viewRangeRendered.from], 'first');
modifyRangeMarker(visibleRows[this.viewRangeRendered.to - 1], 'last');
function modifyRangeMarker(row, cssClass) {
if (row && row.$row) {
row.$row[funcName](cssClass);
}
}
};
scout.Table.prototype.ensureRowRendered = function(row) {
if (!row.$row) {
var rowIndex = this.visibleRows.indexOf(row);
this._renderViewRangeForRowIndex(rowIndex);
}
};
scout.Table.prototype._renderFiller = function() {
if (!this.$fillBefore) {
this.$fillBefore = this.$data.prependDiv('table-data-fill');
this._applyFillerStyle(this.$fillBefore);
}
var fillBeforeHeight = this._calculateFillerHeight(new scout.Range(0, this.viewRangeRendered.from));
this.$fillBefore.cssHeight(fillBeforeHeight);
this.$fillBefore.cssWidth(this.rowWidth);
$.log.isTraceEnabled() && $.log.trace('FillBefore height: ' + fillBeforeHeight);
if (!this.$fillAfter) {
this.$fillAfter = this.$data.appendDiv('table-data-fill');
this._applyFillerStyle(this.$fillAfter);
}
var fillAfterHeight = this._calculateFillerHeight(new scout.Range(this.viewRangeRendered.to, this.visibleRows.length));
this.$fillAfter.cssHeight(fillAfterHeight);
this.$fillAfter.cssWidth(this.rowWidth);
$.log.isTraceEnabled() && $.log.trace('FillAfter height: ' + fillAfterHeight);
};
scout.Table.prototype._applyFillerStyle = function($filler) {
var lineColor = $filler.css('background-color');
// In order to get a 1px border we need to get the right value in percentage for the linear gradient
var lineWidth = ((1 - (1 / this.rowHeight)) * 100).toFixed(2) + '%';
$filler.css({
background: 'linear-gradient(to bottom, transparent, transparent ' + lineWidth + ', ' + lineColor + ' ' + lineWidth + ', ' + lineColor + ')',
backgroundSize: '100% ' + this.rowHeight + 'px',
backgroundColor: 'transparent'
});
};
scout.Table.prototype._calculateFillerHeight = function(range) {
var totalHeight = 0;
for (var i = range.from; i < range.to; i++) {
var row = this.visibleRows[i];
totalHeight += this._heightForRow(row);
}
return totalHeight;
};
scout.Table.prototype.containsAggregatedNumberColumn = function() {
if (!this.initialized) {
return false;
}
return this.visibleColumns().some(function(column) {
return column instanceof scout.NumberColumn && column.aggregationFunction !== 'none';
});
};
/**
* Rebuilds the header.<br>
* Does not modify the rows, it expects a deleteAll and insert operation to follow which will do the job.
*/
scout.Table.prototype.updateColumnStructure = function(columns) {
this._destroyColumns();
this.columns = columns;
this._initColumns();
if (this.rendered) {
this._updateRowWidth();
this.$rows(true).css('width', this.rowWidth);
this._rerenderHeaderColumns();
this._renderEmptyData();
}
this.trigger('columnStructureChanged');
};
scout.Table.prototype.updateColumnOrder = function(columns) {
var i, column, currentPosition, oldColumnOrder;
if (columns.length !== this.columns.length) {
throw new Error('Column order may not be updated because lengths of the arrays differ.');
}
oldColumnOrder = this.columns.slice();
for (i = 0; i < columns.length; i++) {
column = columns[i];
currentPosition = this.columns.indexOf(column);
if (currentPosition < 0) {
throw new Error('Column with id ' + column.id + 'not found.');
}
if (currentPosition !== i) {
// Update model
scout.arrays.remove(this.columns, column);
scout.arrays.insert(this.columns, column, i);
}
}
if (this.rendered) {
this._renderColumnOrderChanges(oldColumnOrder);
}
};
/**
* @param columns array of columns which were updated.
*/
scout.Table.prototype.updateColumnHeaders = function(columns) {
var column, oldColumnState;
// Update model columns
for (var i = 0; i < columns.length; i++) {
column = this.columnById(columns[i].id);
oldColumnState = $.extend(oldColumnState, column);
column.text = columns[i].text;
column.headerTooltipText = columns[i].headerTooltipText;
column.headerCssClass = columns[i].headerCssClass;
column.headerHtmlEnabled = columns[i].headerHtmlEnabled;
column.headerBackgroundColor = columns[i].headerBackgroundColor;
column.headerForegroundColor = columns[i].headerForegroundColor;
column.headerFont = columns[i].headerFont;
column.headerIconId = columns[i].headerIconId;
column.sortActive = columns[i].sortActive;
column.sortAscending = columns[i].sortAscending;
column.grouped = columns[i].grouped;
if (!column.sortActive && column.sortIndex !== -1) {
// Adjust indices of other sort columns (if a sort column in the middle got removed, there won't necessarily be an event for the other columns)
this._removeSortColumn(column);
} else if (column.sortActive && column.sortIndex === -1) {
// Necessary if there is a tail sort column (there won't be an event for the tail sort column if another sort column was added before)
this._addSortColumn(column);
} else {
column.sortIndex = columns[i].sortIndex;
}
if (this.rendered && this.header) {
this.header.updateHeader(column, oldColumnState);
}
}
};
scout.Table.prototype.focusCell = function(column, row) {
if (!this.rendered) {
this._postRenderActions.push(this.focusCell.bind(this, column, row));
return;
}
var cell = this.cell(column, row);
if (this.enabled && row.enabled && cell.editable) {
this.prepareCellEdit(column, row, false);
}
};
scout.Table.prototype._destroyCellEditorPopup = function() {
// When a cell editor popup is open and table is detached, we close the popup immediately
// and don't wait for the model event 'endCellEdit'. By doing this we can avoid problems
// with invalid focus contexts.
if (this.cellEditorPopup) {
this.cellEditorPopup.destroy();
this.cellEditorPopup = null;
}
};
scout.Table.prototype.setVirtual = function(virtual) {
this._setProperty('virtual', virtual);
};
scout.Table.prototype.setCellValue = function(column, row, value) {
column.setCellValue(row, value);
};
scout.Table.prototype.visibleColumns = function(includeGuiColumns) {
includeGuiColumns = scout.nvl(includeGuiColumns, true);
return this.columns.filter(function(column) {
return column.isVisible() && (includeGuiColumns || !column.guiOnly);
}, this);
};
// same as on scout.Tree.prototype._onDesktopPopupOpen
scout.Table.prototype._onDesktopPopupOpen = function(event) {
var popup = event.popup;
if (!this.enabled) {
return;
}
// Set table style to focused if a context menu or a menu bar popup opens, so that it looks as it still has the focus
if (this.has(popup) && popup instanceof scout.ContextMenuPopup) {
this.$container.addClass('focused');
popup.one('destroy', function() {
if (this.rendered) {
this.$container.removeClass('focused');
}
}.bind(this));
}
};
scout.Table.prototype._onDesktopPropertyChange = function(event) {
// The height of the menuBar changes by css when switching to or from the dense mode
if (event.propertyName === 'dense') {
this.menuBar.invalidateLayoutTree();
}
};
scout.Table.prototype.markRowsAsNonChanged = function(rows) {
scout.arrays.ensure(rows || this.rows).forEach(function(row) {
row.status = scout.TableRow.Status.NON_CHANGED;
});
};
/* --- STATIC HELPERS ------------------------------------------------------------- */
/**
* @memberOf scout.Table
*/
scout.Table.parseHorizontalAlignment = function(alignment) {
if (alignment > 0) {
return 'right';
}
if (alignment === 0) {
return 'center';
}
return 'left';
};
scout.Table.linkRowToDiv = function(row, $row) {
if (row) {
row.$row = $row;
}
if ($row) {
$row.data('row', row);
}
};