ag-grid
Version:
Advanced Javascript Datagrid. Supports raw Javascript, AngularJS 1.x, AngularJS 2.0 and Web Components
467 lines (402 loc) • 19.1 kB
text/typescript
/// <reference path="utils.ts" />
/// <reference path="rendering/rowRenderer.ts" />
module ag.grid {
var utils = Utils;
// these constants are used for determining if groups should
// be selected or deselected when selecting groups, and the group
// then selects the children.
var SELECTED = 0;
var UNSELECTED = 1;
var MIXED = 2;
var DO_NOT_CARE = 3;
export class SelectionController {
private eParentsOfRows: HTMLElement[];
private angularGrid: Grid;
private gridOptionsWrapper: GridOptionsWrapper;
private $scope: any;
private rowRenderer: RowRenderer;
private selectedRows: any;
private selectedNodesById: any;
private rowModel: any;
private eventService: EventService;
public init(angularGrid: Grid, gridPanel: GridPanel, gridOptionsWrapper: GridOptionsWrapper,
$scope: any, rowRenderer: RowRenderer, eventService: EventService) {
this.eParentsOfRows = gridPanel.getRowsParent();
this.angularGrid = angularGrid;
this.gridOptionsWrapper = gridOptionsWrapper;
this.$scope = $scope;
this.rowRenderer = rowRenderer;
this.eventService = eventService;
this.initSelectedNodesById();
this.selectedRows = [];
}
private initSelectedNodesById() {
this.selectedNodesById = {};
}
public getSelectedNodesById() {
return this.selectedNodesById;
}
public getSelectedRows() {
return this.selectedRows;
}
public getSelectedNodes() {
var selectedNodes: any = [];
var keys = Object.keys(this.selectedNodesById);
for (var i = 0; i < keys.length; i++) {
var id = keys[i];
var selectedNode = this.selectedNodesById[id];
selectedNodes.push(selectedNode);
}
return selectedNodes;
}
// returns a list of all nodes at 'best cost' - a feature to be used
// with groups / trees. if a group has all it's children selected,
// then the group appears in the result, but not the children.
// Designed for use with 'children' as the group selection type,
// where groups don't actually appear in the selection normally.
public getBestCostNodeSelection() {
if (typeof this.rowModel.getTopLevelNodes !== 'function') {
throw 'selectAll not available when rows are on the server';
}
var topLevelNodes = this.rowModel.getTopLevelNodes();
var result: any = [];
var that = this;
// recursive function, to find the selected nodes
function traverse(nodes: any) {
for (var i = 0, l = nodes.length; i < l; i++) {
var node = nodes[i];
if (that.isNodeSelected(node)) {
result.push(node);
} else {
// if not selected, then if it's a group, and the group
// has children, continue to search for selections
if (node.group && node.children) {
traverse(node.children);
}
}
}
}
traverse(topLevelNodes);
return result;
}
public setRowModel(rowModel: any) {
this.rowModel = rowModel;
}
// this clears the selection, but doesn't clear down the css - when it is called, the
// caller then gets the grid to refresh.
public deselectAll() {
this.initSelectedNodesById();
//var keys = Object.keys(this.selectedNodesById);
//for (var i = 0; i < keys.length; i++) {
// delete this.selectedNodesById[keys[i]];
//}
this.syncSelectedRowsAndCallListener();
}
// this selects everything, but doesn't clear down the css - when it is called, the
// caller then gets the grid to refresh.
public selectAll() {
if (typeof this.rowModel.getTopLevelNodes !== 'function') {
throw 'selectAll not available when rows are on the server';
}
var selectedNodesById = this.selectedNodesById;
// if the selection is "don't include groups", then we don't include them!
var includeGroups = !this.gridOptionsWrapper.isGroupSelectsChildren();
function recursivelySelect(nodes: any) {
if (nodes) {
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node.group) {
recursivelySelect(node.children);
if (includeGroups) {
selectedNodesById[node.id] = node;
}
} else {
selectedNodesById[node.id] = node;
}
}
}
}
var topLevelNodes = this.rowModel.getTopLevelNodes();
recursivelySelect(topLevelNodes);
this.syncSelectedRowsAndCallListener();
}
public selectNode(node: any, tryMulti: any, suppressEvents?: any) {
var multiSelect = this.gridOptionsWrapper.isRowSelectionMulti() && tryMulti;
// if the node is a group, then selecting this is the same as selecting the parent,
// so to have only one flow through the below, we always select the header parent
// (which then has the side effect of selecting the child).
var nodeToSelect: any;
if (node.footer) {
nodeToSelect = node.sibling;
} else {
nodeToSelect = node;
}
// at the end, if this is true, we inform the callback
var atLeastOneItemUnselected = false;
var atLeastOneItemSelected = false;
// see if rows to be deselected
if (!multiSelect) {
atLeastOneItemUnselected = this.doWorkOfDeselectAllNodes();
}
if (this.gridOptionsWrapper.isGroupSelectsChildren() && nodeToSelect.group) {
// don't select the group, select the children instead
atLeastOneItemSelected = this.recursivelySelectAllChildren(nodeToSelect);
} else {
// see if row needs to be selected
atLeastOneItemSelected = this.doWorkOfSelectNode(nodeToSelect, suppressEvents);
}
if (atLeastOneItemUnselected || atLeastOneItemSelected) {
this.syncSelectedRowsAndCallListener(suppressEvents);
}
this.updateGroupParentsIfNeeded();
}
private recursivelySelectAllChildren(node: any, suppressEvents?: any) {
var atLeastOne = false;
if (node.children) {
for (var i = 0; i < node.children.length; i++) {
var child = node.children[i];
if (child.group) {
if (this.recursivelySelectAllChildren(child)) {
atLeastOne = true;
}
} else {
if (this.doWorkOfSelectNode(child, suppressEvents)) {
atLeastOne = true;
}
}
}
}
return atLeastOne;
}
private recursivelyDeselectAllChildren(node: any) {
if (node.children) {
for (var i = 0; i < node.children.length; i++) {
var child = node.children[i];
if (child.group) {
this.recursivelyDeselectAllChildren(child);
} else {
this.deselectRealNode(child);
}
}
}
}
// 1 - selects a node
// 2 - updates the UI
// 3 - calls callbacks
private doWorkOfSelectNode(node: any, suppressEvents: any) {
if (this.selectedNodesById[node.id]) {
return false;
}
this.selectedNodesById[node.id] = node;
this.addCssClassForNode_andInformVirtualRowListener(node);
// also color in the footer if there is one
if (node.group && node.expanded && node.sibling) {
this.addCssClassForNode_andInformVirtualRowListener(node.sibling);
}
// inform the rowSelected listener, if any
if (!suppressEvents) {
var event: any = {node: node};
this.eventService.dispatchEvent(Events.EVENT_ROW_SELECTED, event)
}
return true;
}
// 1 - selects a node
// 2 - updates the UI
// 3 - calls callbacks
// wow - what a big name for a method, exception case, it's saying what the method does
private addCssClassForNode_andInformVirtualRowListener(node: any) {
var virtualRenderedRowIndex = this.rowRenderer.getIndexOfRenderedNode(node);
if (virtualRenderedRowIndex >= 0) {
this.eParentsOfRows.forEach( function(rowContainer: HTMLElement) {
utils.querySelectorAll_addCssClass(rowContainer, '[row="' + virtualRenderedRowIndex + '"]', 'ag-row-selected');
});
// inform virtual row listener
this.angularGrid.onVirtualRowSelected(virtualRenderedRowIndex, true);
}
}
// 1 - un-selects a node
// 2 - updates the UI
// 3 - calls callbacks
private doWorkOfDeselectAllNodes(nodeToKeepSelected?: any) {
// not doing multi-select, so deselect everything other than the 'just selected' row
var atLeastOneSelectionChange: any;
var selectedNodeKeys = Object.keys(this.selectedNodesById);
for (var i = 0; i < selectedNodeKeys.length; i++) {
// skip the 'just selected' row
var key = selectedNodeKeys[i];
var nodeToDeselect = this.selectedNodesById[key];
if (nodeToDeselect === nodeToKeepSelected) {
continue;
} else {
this.deselectRealNode(nodeToDeselect);
atLeastOneSelectionChange = true;
}
}
return atLeastOneSelectionChange;
}
private deselectRealNode(node: any) {
// deselect the css
this.removeCssClassForNode(node);
// if node is a header, and if it has a sibling footer, deselect the footer also
if (node.group && node.expanded && node.sibling) { // also check that it's expanded, as sibling could be a ghost
this.removeCssClassForNode(node.sibling);
}
// remove the row
delete this.selectedNodesById[node.id];
}
private removeCssClassForNode(node: any) {
var virtualRenderedRowIndex = this.rowRenderer.getIndexOfRenderedNode(node);
if (virtualRenderedRowIndex >= 0) {
this.eParentsOfRows.forEach( function(rowContainer: HTMLElement) {
utils.querySelectorAll_removeCssClass(rowContainer, '[row="' + virtualRenderedRowIndex + '"]', 'ag-row-selected');
});
// inform virtual row listener
this.angularGrid.onVirtualRowSelected(virtualRenderedRowIndex, false);
}
}
// used by selectionRendererFactory
public deselectIndex(rowIndex: any) {
var node = this.rowModel.getVirtualRow(rowIndex);
this.deselectNode(node);
}
// used by api
public deselectNode(node: any) {
if (node) {
if (this.gridOptionsWrapper.isGroupSelectsChildren() && node.group) {
// want to deselect children, not this node, so recursively deselect
this.recursivelyDeselectAllChildren(node);
} else {
this.deselectRealNode(node);
}
}
this.syncSelectedRowsAndCallListener();
this.updateGroupParentsIfNeeded();
}
// used by selectionRendererFactory & api
public selectIndex(index: any, tryMulti: any, suppressEvents?: any) {
var node = this.rowModel.getVirtualRow(index);
this.selectNode(node, tryMulti, suppressEvents);
}
// updates the selectedRows with the selectedNodes and calls selectionChanged listener
private syncSelectedRowsAndCallListener(suppressEvents?: any) {
// update selected rows
var selectedRows = this.selectedRows;
var oldCount = selectedRows.length;
// clear selected rows
selectedRows.length = 0;
var keys = Object.keys(this.selectedNodesById);
for (var i = 0; i < keys.length; i++) {
if (this.selectedNodesById[keys[i]] !== undefined) {
var selectedNode = this.selectedNodesById[keys[i]];
selectedRows.push(selectedNode.data);
}
}
// this stop the event firing the very first the time grid is initialised. without this, the documentation
// page had a popup in the 'selection' page as soon as the page was loaded!!
var nothingChangedMustBeInitialising = oldCount === 0 && selectedRows.length === 0;
if (!nothingChangedMustBeInitialising && !suppressEvents) {
var event = {
selectedNodesById: this.selectedNodesById,
selectedRows: this.selectedRows
};
this.eventService.dispatchEvent(Events.EVENT_SELECTION_CHANGED, event);
}
var that = this;
if (this.$scope) {
setTimeout(function () {
that.$scope.$apply();
}, 0);
}
}
private recursivelyCheckIfSelected(node: any) {
var foundSelected = false;
var foundUnselected = false;
if (node.children) {
for (var i = 0; i < node.children.length; i++) {
var child = node.children[i];
var result: any;
if (child.group) {
result = this.recursivelyCheckIfSelected(child);
switch (result) {
case SELECTED:
foundSelected = true;
break;
case UNSELECTED:
foundUnselected = true;
break;
case MIXED:
foundSelected = true;
foundUnselected = true;
break;
// we can ignore the DO_NOT_CARE, as it doesn't impact, means the child
// has no children and shouldn't be considered when deciding
}
} else {
if (this.isNodeSelected(child)) {
foundSelected = true;
} else {
foundUnselected = true;
}
}
if (foundSelected && foundUnselected) {
// if mixed, then no need to go further, just return up the chain
return MIXED;
}
}
}
// got this far, so no conflicts, either all children selected, unselected, or neither
if (foundSelected) {
return SELECTED;
} else if (foundUnselected) {
return UNSELECTED;
} else {
return DO_NOT_CARE;
}
}
// used by selectionRendererFactory
// returns:
// true: if selected
// false: if unselected
// undefined: if it's a group and 'children selection' is used and 'children' are a mix of selected and unselected
public isNodeSelected(node: any) {
if (this.gridOptionsWrapper.isGroupSelectsChildren() && node.group) {
// doing child selection, we need to traverse the children
var resultOfChildren = this.recursivelyCheckIfSelected(node);
switch (resultOfChildren) {
case SELECTED:
return true;
case UNSELECTED:
return false;
default:
return undefined;
}
} else {
return this.selectedNodesById[node.id] !== undefined;
}
}
private updateGroupParentsIfNeeded() {
// we only do this if parent nodes are responsible
// for selecting their children.
if (!this.gridOptionsWrapper.isGroupSelectsChildren()) {
return;
}
var firstRow = this.rowRenderer.getFirstVirtualRenderedRow();
var lastRow = this.rowRenderer.getLastVirtualRenderedRow();
for (var rowIndex = firstRow; rowIndex <= lastRow; rowIndex++) {
// see if node is a group
var node = this.rowModel.getVirtualRow(rowIndex);
if (node.group) {
var selected = this.isNodeSelected(node);
this.angularGrid.onVirtualRowSelected(rowIndex, selected);
this.eParentsOfRows.forEach( function(rowContainer: HTMLElement) {
if (selected) {
utils.querySelectorAll_addCssClass(rowContainer, '[row="' + rowIndex + '"]', 'ag-row-selected');
} else {
utils.querySelectorAll_removeCssClass(rowContainer, '[row="' + rowIndex + '"]', 'ag-row-selected');
}
});
}
}
}
}
}