UNPKG

ag-grid

Version:

Advanced Javascript Datagrid. Supports raw Javascript, AngularJS 1.x, AngularJS 2.0 and Web Components

667 lines (570 loc) 27.9 kB
/// <reference path='../columnController.ts' /> /// <reference path='../utils.ts' /> /// <reference path="../gridOptionsWrapper.ts" /> /// <reference path="../expressionService.ts" /> /// <reference path="../selectionRendererFactory.ts" /> /// <reference path="rowRenderer.ts" /> /// <reference path="../selectionController.ts" /> /// <reference path="../templateService.ts" /> /// <reference path="../virtualDom/vHtmlElement.ts" /> /// <reference path="../virtualDom/vWrapperElement.ts" /> module ag.grid { var _ = Utils; export class RenderedCell { private vGridCell: ag.vdom.VHtmlElement; // the outer cell private vSpanWithValue: ag.vdom.VHtmlElement; // inner cell private vCellWrapper: ag.vdom.VHtmlElement; private vParentOfValue: ag.vdom.VHtmlElement; private checkboxOnChangeListener: EventListener; private column: Column; private data: any; private node: RowNode; private rowIndex: number; private editingCell: boolean; private scope: any; private isFirstColumn: boolean = false; private gridOptionsWrapper: GridOptionsWrapper; private expressionService: ExpressionService; private selectionRendererFactory: SelectionRendererFactory; private rowRenderer: RowRenderer; private selectionController: SelectionController; private $compile: any; private templateService: TemplateService; private cellRendererMap: {[key: string]: Function}; private eCheckbox: HTMLInputElement; private columnController: ColumnController; private valueService: ValueService; private eventService: EventService; private value: any; private checkboxSelection: boolean; constructor(isFirstColumn: any, column: any, $compile: any, rowRenderer: RowRenderer, gridOptionsWrapper: GridOptionsWrapper, expressionService: ExpressionService, selectionRendererFactory: SelectionRendererFactory, selectionController: SelectionController, templateService: TemplateService, cellRendererMap: {[key: string]: any}, node: any, rowIndex: number, scope: any, columnController: ColumnController, valueService: ValueService, eventService: EventService) { this.isFirstColumn = isFirstColumn; this.column = column; this.rowRenderer = rowRenderer; this.gridOptionsWrapper = gridOptionsWrapper; this.expressionService = expressionService; this.selectionRendererFactory = selectionRendererFactory; this.selectionController = selectionController; this.cellRendererMap = cellRendererMap; this.$compile = $compile; this.templateService = templateService; this.columnController = columnController; this.valueService = valueService; this.eventService = eventService; this.checkboxSelection = this.column.colDef.checkboxSelection && !node.floating; this.node = node; this.rowIndex = rowIndex; this.scope = scope; this.data = this.getDataForRow(); this.value = this.getValue(); this.setupComponents(); } public getColumn(): Column { return this.column; } private getValue(): any { return this.valueService.getValue(this.column.colDef, this.data, this.node); } public getVGridCell(): ag.vdom.VHtmlElement { return this.vGridCell; } private getDataForRow() { if (this.node.footer) { // if footer, we always show the data return this.node.data; } else if (this.node.group) { // if header and header is expanded, we show data in footer only var footersEnabled = this.gridOptionsWrapper.isGroupIncludeFooter(); var suppressHideHeader = this.gridOptionsWrapper.isGroupSuppressBlankHeader(); if (this.node.expanded && footersEnabled && !suppressHideHeader) { return undefined; } else { return this.node.data; } } else { // otherwise it's a normal node, just return data as normal return this.node.data; } } private setupComponents() { this.vGridCell = new ag.vdom.VHtmlElement("div"); this.vGridCell.setAttribute("col", (this.column.index !== undefined && this.column.index !== null) ? this.column.index.toString() : ''); this.vGridCell.setAttribute("colId", this.column.colId); // only set tab index if cell selection is enabled if (!this.gridOptionsWrapper.isSuppressCellSelection() && !this.node.floating) { this.vGridCell.setAttribute("tabindex", "-1"); } // these are the grid styles, don't change between soft refreshes this.addClasses(); this.addCellClickedHandler(); this.addCellDoubleClickedHandler(); this.addCellContextMenuHandler(); if (!this.node.floating) { // not allowing navigation on the floating until i have time to figure it out this.addCellNavigationHandler(); } this.vGridCell.addStyles({width: this.column.actualWidth + "px"}); this.createParentOfValue(); this.populateCell(); if (this.eCheckbox) { this.setSelected(this.selectionController.isNodeSelected(this.node)); } } // called by rowRenderer when user navigates via tab key public startEditing(key?: number) { var that = this; this.editingCell = true; _.removeAllChildren(this.vGridCell.getElement()); var eInput = document.createElement('input'); eInput.type = 'text'; _.addCssClass(eInput, 'ag-cell-edit-input'); var startWithOldValue = key !== Constants.KEY_BACKSPACE && key !== Constants.KEY_DELETE; var value = this.getValue(); if (startWithOldValue && value !== null && value !== undefined) { eInput.value = value; } eInput.style.width = (this.column.actualWidth - 14) + 'px'; this.vGridCell.appendChild(eInput); eInput.focus(); eInput.select(); var blurListener = function () { that.stopEditing(eInput, blurListener); }; //stop entering if we loose focus eInput.addEventListener("blur", blurListener); //stop editing if enter pressed eInput.addEventListener('keypress', (event: any) => { var key = event.which || event.keyCode; if (key === Constants.KEY_ENTER) { this.stopEditing(eInput, blurListener); this.focusCell(true); } }); //stop editing if enter pressed eInput.addEventListener('keydown', (event: any) => { var key = event.which || event.keyCode; if (key === Constants.KEY_ESCAPE) { this.stopEditing(eInput, blurListener, true); this.focusCell(true); } }); // tab key doesn't generate keypress, so need keydown to listen for that eInput.addEventListener('keydown', function (event:any) { var key = event.which || event.keyCode; if (key == Constants.KEY_TAB) { that.stopEditing(eInput, blurListener); that.rowRenderer.startEditingNextCell(that.rowIndex, that.column, event.shiftKey); // we don't want the default tab action, so return false, this stops the event from bubbling event.preventDefault(); return false; } }); } public focusCell(forceBrowserFocus: boolean): void { this.rowRenderer.focusCell(this.vGridCell.getElement(), this.rowIndex, this.column.index, this.column.colDef, forceBrowserFocus); } private stopEditing(eInput: any, blurListener: any, reset: boolean = false) { this.editingCell = false; var newValue = eInput.value; var colDef = this.column.colDef; //If we don't remove the blur listener first, we get: //Uncaught NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is no longer a child of this node. Perhaps it was moved in a 'blur' event handler? eInput.removeEventListener('blur', blurListener); if (!reset) { var paramsForCallbacks = { node: this.node, data: this.node.data, oldValue: this.node.data[colDef.field], newValue: newValue, rowIndex: this.rowIndex, colDef: colDef, api: this.gridOptionsWrapper.getApi(), context: this.gridOptionsWrapper.getContext() }; if (colDef.newValueHandler) { colDef.newValueHandler(paramsForCallbacks); } else { this.node.data[colDef.field] = newValue; } // at this point, the value has been updated this.value = this.getValue(); paramsForCallbacks.newValue = this.value; if (typeof colDef.onCellValueChanged === 'function') { colDef.onCellValueChanged(paramsForCallbacks); } this.eventService.dispatchEvent(Events.EVENT_CELL_VALUE_CHANGED, paramsForCallbacks); } _.removeAllChildren(this.vGridCell.getElement()); if (this.checkboxSelection) { this.vGridCell.appendChild(this.vCellWrapper.getElement()); } this.refreshCell(); } public createParams(): any { var params = { node: this.node, data: this.node.data, value: this.value, rowIndex: this.rowIndex, colDef: this.column.colDef, $scope: this.scope, context: this.gridOptionsWrapper.getContext(), api: this.gridOptionsWrapper.getApi() }; return params; } public createEvent(event: any, eventSource: any): any { var agEvent = this.createParams(); agEvent.event = event; agEvent.eventSource = eventSource; return agEvent; } private addCellDoubleClickedHandler() { var that = this; var colDef = this.column.colDef; this.vGridCell.addEventListener('dblclick', function (event: any) { // always dispatch event to eventService var agEvent: any = that.createEvent(event, this); that.eventService.dispatchEvent(Events.EVENT_CELL_DOUBLE_CLICKED, agEvent); // check if colDef also wants to handle event if (typeof colDef.onCellDoubleClicked === 'function') { colDef.onCellDoubleClicked(agEvent); } if (!that.gridOptionsWrapper.isSingleClickEdit() && that.isCellEditable()) { that.startEditing(); } }); } private addCellContextMenuHandler() { var that = this; var colDef = this.column.colDef; this.vGridCell.addEventListener('contextmenu', function (event: any) { var agEvent: any = that.createEvent(event, this); that.eventService.dispatchEvent(Events.EVENT_CELL_CONTEXT_MENU, agEvent); if (colDef.onCellContextMenu) { colDef.onCellContextMenu(agEvent); } }); } public isCellEditable() { if (this.editingCell) { return false; } // never allow editing of groups if (this.node.group) { return false; } // if boolean set, then just use it var colDef = this.column.colDef; if (typeof colDef.editable === 'boolean') { return colDef.editable; } // if function, then call the function to find out if (typeof colDef.editable === 'function') { var params = this.createParams(); var editableFunc = <Function>colDef.editable; return editableFunc(params); } return false; } private addCellClickedHandler() { var colDef = this.column.colDef; var that = this; this.vGridCell.addEventListener("click", function (event: any) { // we pass false to focusCell, as we don't want the cell to focus // also get the browser focus. if we did, then the cellRenderer could // have a text field in it, for example, and as the user clicks on the // text field, the text field, the focus doesn't get to the text // field, instead to goes to the div behind, making it impossible to // select the text field. if (!that.node.floating) { that.focusCell(false); } var agEvent = that.createEvent(event, this); that.eventService.dispatchEvent(Events.EVENT_CELL_CLICKED, agEvent); if (colDef.onCellClicked) { colDef.onCellClicked(agEvent); } if (that.gridOptionsWrapper.isSingleClickEdit() && that.isCellEditable()) { that.startEditing(); } }); } private populateCell() { // populate this.putDataIntoCell(); // style this.addStylesFromCollDef(); this.addClassesFromCollDef(); this.addClassesFromRules(); } private addStylesFromCollDef() { var colDef = this.column.colDef; if (colDef.cellStyle) { var cssToUse: any; if (typeof colDef.cellStyle === 'function') { var cellStyleParams = { value: this.value, data: this.node.data, node: this.node, colDef: colDef, column: this.column, $scope: this.scope, context: this.gridOptionsWrapper.getContext(), api: this.gridOptionsWrapper.getApi() }; var cellStyleFunc = <Function>colDef.cellStyle; cssToUse = cellStyleFunc(cellStyleParams); } else { cssToUse = colDef.cellStyle; } if (cssToUse) { this.vGridCell.addStyles(cssToUse); } } } private addClassesFromCollDef() { var colDef = this.column.colDef; if (colDef.cellClass) { var classToUse: any; if (typeof colDef.cellClass === 'function') { var cellClassParams = { value: this.value, data: this.node.data, node: this.node, colDef: colDef, $scope: this.scope, context: this.gridOptionsWrapper.getContext(), api: this.gridOptionsWrapper.getApi() }; var cellClassFunc = <(cellClassParams: any) => string|string[]> colDef.cellClass; classToUse = cellClassFunc(cellClassParams); } else { classToUse = colDef.cellClass; } if (typeof classToUse === 'string') { this.vGridCell.addClass(classToUse); } else if (Array.isArray(classToUse)) { classToUse.forEach( (cssClassItem: string)=> { this.vGridCell.addClass(cssClassItem); }); } } } private addClassesFromRules() { var colDef = this.column.colDef; var classRules = colDef.cellClassRules; if (typeof classRules === 'object' && classRules !== null) { var params = { value: this.value, data: this.node.data, node: this.node, colDef: colDef, rowIndex: this.rowIndex, api: this.gridOptionsWrapper.getApi(), context: this.gridOptionsWrapper.getContext() }; var classNames = Object.keys(classRules); for (var i = 0; i < classNames.length; i++) { var className = classNames[i]; var rule = classRules[className]; var resultOfRule: any; if (typeof rule === 'string') { resultOfRule = this.expressionService.evaluate(rule, params); } else if (typeof rule === 'function') { resultOfRule = rule(params); } if (resultOfRule) { this.vGridCell.addClass(className); } else { this.vGridCell.removeClass(className); } } } } // rename this to 'add key event listener private addCellNavigationHandler() { var that = this; this.vGridCell.addEventListener('keydown', function (event: any) { if (that.editingCell) { return; } // only interested on key presses that are directly on this element, not any children elements. this // stops navigation if the user is in, for example, a text field inside the cell, and user hits // on of the keys we are looking for. if (event.target !== that.vGridCell.getElement()) { return; } var key = event.which || event.keyCode; var startNavigation = key === Constants.KEY_DOWN || key === Constants.KEY_UP || key === Constants.KEY_LEFT || key === Constants.KEY_RIGHT; if (startNavigation) { event.preventDefault(); that.rowRenderer.navigateToNextCell(key, that.rowIndex, that.column); return; } var startEdit = that.isKeycodeForStartEditing(key); if (startEdit && that.isCellEditable()) { that.startEditing(key); // if we don't prevent default, then the editor that get displayed also picks up the 'enter key' // press, and stops editing immediately, hence giving he user experience that nothing happened event.preventDefault(); return; } var selectRow = key === Constants.KEY_SPACE; if (selectRow && that.gridOptionsWrapper.isRowSelection()) { var selected = that.selectionController.isNodeSelected(that.node); if (selected) { that.selectionController.deselectNode(that.node); } else { that.selectionController.selectNode(that.node, true); } event.preventDefault(); return; } }); } private isKeycodeForStartEditing(key: number): boolean { return key === Constants.KEY_ENTER || key === Constants.KEY_BACKSPACE || key === Constants.KEY_DELETE; } public createSelectionCheckbox() { this.eCheckbox = document.createElement('input'); this.eCheckbox.type = "checkbox"; this.eCheckbox.name = "name"; this.eCheckbox.className = 'ag-selection-checkbox'; this.eCheckbox.addEventListener('click', function (event) { event.stopPropagation(); }); var that = this; this.checkboxOnChangeListener = function() { var newValue = that.eCheckbox.checked; if (newValue) { that.selectionController.selectIndex(that.rowIndex, true); } else { that.selectionController.deselectIndex(that.rowIndex); } }; this.eCheckbox.onchange = this.checkboxOnChangeListener; } public setSelected(state: boolean) { if (!this.eCheckbox) { return; } this.eCheckbox.onchange = null; if (typeof state === 'boolean') { this.eCheckbox.checked = state; this.eCheckbox.indeterminate = false; } else { // isNodeSelected returns back undefined if it's a group and the children // are a mix of selected and unselected this.eCheckbox.indeterminate = true; } this.eCheckbox.onchange = this.checkboxOnChangeListener; } private createParentOfValue() { if (this.checkboxSelection) { this.vCellWrapper = new ag.vdom.VHtmlElement('span'); this.vCellWrapper.addClass('ag-cell-wrapper'); this.vGridCell.appendChild(this.vCellWrapper); this.createSelectionCheckbox(); this.vCellWrapper.appendChild(new ag.vdom.VWrapperElement(this.eCheckbox)); // eventually we call eSpanWithValue.innerHTML = xxx, so cannot include the checkbox (above) in this span this.vSpanWithValue = new ag.vdom.VHtmlElement('span'); this.vSpanWithValue.addClass('ag-cell-value'); this.vCellWrapper.appendChild(this.vSpanWithValue); this.vParentOfValue = this.vSpanWithValue; } else { this.vGridCell.addClass('ag-cell-value'); this.vParentOfValue = this.vGridCell; } } public isVolatile() { return this.column.colDef.volatile; } public refreshCell() { _.removeAllChildren(this.vParentOfValue.getElement()); this.value = this.getValue(); this.populateCell(); if (this.checkboxSelection) { this.setSelected(this.selectionController.isNodeSelected(this.node)); } // if angular compiling, then need to also compile the cell again (angular compiling sucks, please wait...) if (this.gridOptionsWrapper.isAngularCompileRows()) { this.$compile(this.vGridCell.getElement())(this.scope); } } private putDataIntoCell() { // template gets preference, then cellRenderer, then do it ourselves var colDef = this.column.colDef; if (colDef.template) { this.vParentOfValue.setInnerHtml(colDef.template); } else if (colDef.templateUrl) { var template = this.templateService.getTemplate(colDef.templateUrl, this.refreshCell.bind(this, true)); if (template) { this.vParentOfValue.setInnerHtml(template); } } else if (colDef.floatingCellRenderer && this.node.floating) { this.useCellRenderer(colDef.floatingCellRenderer); } else if (colDef.cellRenderer) { this.useCellRenderer(colDef.cellRenderer); } else { // if we insert undefined, then it displays as the string 'undefined', ugly! if (this.value !== undefined && this.value !== null && this.value !== '') { this.vParentOfValue.setInnerHtml(this.value.toString()); } } } private useCellRenderer(cellRenderer: Function | {}) { var colDef = this.column.colDef; var rendererParams = { value: this.value, valueGetter: this.getValue, data: this.node.data, node: this.node, colDef: colDef, column: this.column, $scope: this.scope, rowIndex: this.rowIndex, api: this.gridOptionsWrapper.getApi(), context: this.gridOptionsWrapper.getContext(), refreshCell: this.refreshCell.bind(this), eGridCell: this.vGridCell }; var actualCellRenderer: Function; if (typeof cellRenderer === 'object' && cellRenderer !== null) { var cellRendererObj = <{ renderer: string }> cellRenderer; actualCellRenderer = this.cellRendererMap[cellRendererObj.renderer]; if (!actualCellRenderer) { throw 'Cell renderer ' + cellRenderer + ' not found, available are ' + Object.keys(this.cellRendererMap); } } else if (typeof cellRenderer === 'function') { actualCellRenderer = <Function>cellRenderer; } else { throw 'Cell Renderer must be String or Function'; } var resultFromRenderer = actualCellRenderer(rendererParams); if (_.isNodeOrElement(resultFromRenderer)) { // a dom node or element was returned, so add child this.vParentOfValue.appendChild(resultFromRenderer); } else { // otherwise assume it was html, so just insert this.vParentOfValue.setInnerHtml(resultFromRenderer); } } private addClasses() { this.vGridCell.addClass('ag-cell'); this.vGridCell.addClass('ag-cell-no-focus'); this.vGridCell.addClass('cell-col-' + this.column.index); if (this.node.group && this.node.footer) { this.vGridCell.addClass('ag-footer-cell'); } if (this.node.group && !this.node.footer) { this.vGridCell.addClass('ag-group-cell'); } } } }