UNPKG

ag-grid

Version:

Advanced Data Grid / Data Table supporting Javascript / React / AngularJS / Web Components

1,254 lines (1,054 loc) 69.7 kB
import {_} from "../utils"; import {Column} from "../entities/column"; import {CellChangedEvent, RowNode} from "../entities/rowNode"; import {Constants} from "../constants"; import { CellClickedEvent, CellContextMenuEvent, CellDoubleClickedEvent, CellEditingStartedEvent, CellEditingStoppedEvent, CellEvent, CellMouseDownEvent, CellMouseOutEvent, CellMouseOverEvent, Events, FlashCellsEvent } from "../events"; import {GridCell, GridCellDef} from "../entities/gridCell"; import {ICellEditorComp, ICellEditorParams} from "./cellEditors/iCellEditor"; import {Component} from "../widgets/component"; import {ICellRendererComp, ICellRendererParams} from "./cellRenderers/iCellRenderer"; import {CheckboxSelectionComponent} from "./checkboxSelectionComponent"; import {NewValueParams, SuppressKeyboardEventParams} from "../entities/colDef"; import {Beans} from "./beans"; import {RowComp} from "./rowComp"; import {RowDragComp} from "./rowDragComp"; export class CellComp extends Component { public static DOM_DATA_KEY_CELL_COMP = 'cellComp'; private eCellWrapper: HTMLElement; private eParentOfValue: HTMLElement; private beans: Beans; private column: Column; private rowNode: RowNode; private eParentRow: HTMLElement; private gridCell: GridCell; private rangeCount: number; private usingWrapper: boolean; private includeSelectionComponent: boolean; private includeRowDraggingComponent: boolean; private cellFocused: boolean; private editingCell = false; private cellEditorInPopup: boolean; private hideEditorPopup: Function; private lastIPadMouseClickEvent: number; // true if we are using a cell renderer private usingCellRenderer: boolean; // the cellRenderer class to use - this is decided once when the grid is initialised private cellRendererType: string; // instance of the cellRenderer class private cellRenderer: ICellRendererComp; // the GUI is initially element or string, however once the UI is created, it becomes UI private cellRendererGui: HTMLElement; private cellEditor: ICellEditorComp; private autoHeightCell: boolean; private firstRightPinned: boolean; private lastLeftPinned: boolean; private rowComp: RowComp; private rangeSelectionEnabled: boolean; private value: any; private valueFormatted: any; private colsSpanning: Column[]; private rowSpan: number; private tooltip: any; private scope: null; // every time we go into edit mode, or back again, this gets incremented. // it's the components way of dealing with the async nature of framework components, // so if a framework component takes a while to be created, we know if the object // is still relevant when creating is finished. eg we could click edit / unedit 20 // times before the first React edit component comes back - we should discard // the first 19. private cellEditorVersion = 0; private cellRendererVersion = 0; constructor(scope: any, beans: Beans, column: Column, rowNode: RowNode, rowComp: RowComp, autoHeightCell: boolean) { super(); this.scope = scope; this.beans = beans; this.column = column; this.rowNode = rowNode; this.rowComp = rowComp; this.autoHeightCell = autoHeightCell; this.createGridCellVo(); this.rangeSelectionEnabled = beans.enterprise && beans.gridOptionsWrapper.isEnableRangeSelection(); this.cellFocused = this.beans.focusedCellController.isCellFocused(this.gridCell); this.firstRightPinned = this.column.isFirstRightPinned(); this.lastLeftPinned = this.column.isLastLeftPinned(); if (this.rangeSelectionEnabled) { this.rangeCount = this.beans.rangeController.getCellRangeCount(this.gridCell); } this.getValueAndFormat(); this.setUsingWrapper(); this.chooseCellRenderer(); this.setupColSpan(); this.rowSpan = this.column.getRowSpan(this.rowNode); } public getCreateTemplate(): string { let templateParts: string[] = []; let col = this.column; let width = this.getCellWidth(); let left = col.getLeft(); let valueToRender = this.getInitialValueToRender(); let valueSanitised = _.get(this.column, 'colDef.template', null) ? valueToRender : _.escape(valueToRender); this.tooltip = this.getToolTip(); let tooltipSanitised = _.escape(this.tooltip); let colIdSanitised = _.escape(col.getId()); let wrapperStartTemplate: string; let wrapperEndTemplate: string; let stylesFromColDef = this.preProcessStylesFromColDef(); let cssClasses = this.getInitialCssClasses(); let stylesForRowSpanning = this.getStylesForRowSpanning(); if (this.usingWrapper) { wrapperStartTemplate = '<span ref="eCellWrapper" class="ag-cell-wrapper"><span ref="eCellValue" class="ag-cell-value">'; wrapperEndTemplate = '</span></span>'; } // hey, this looks like React!!! templateParts.push(`<div`); templateParts.push(` tabindex="-1"`); templateParts.push(` role="gridcell"`); templateParts.push(` comp-id="${this.getCompId()}" `); templateParts.push(` col-id="${colIdSanitised}"`); templateParts.push(` class="${cssClasses.join(' ')}"`); templateParts.push(tooltipSanitised ? ` title="${tooltipSanitised}"` : ``); templateParts.push(` style="width: ${width}px; left: ${left}px; ${stylesFromColDef} ${stylesForRowSpanning}" >`); templateParts.push(wrapperStartTemplate); templateParts.push(valueSanitised); templateParts.push(wrapperEndTemplate); templateParts.push(`</div>`); return templateParts.join(''); } private getStylesForRowSpanning(): string { if (this.rowSpan===1) { return ''; } let singleRowHeight = this.beans.gridOptionsWrapper.getRowHeightAsNumber(); let totalRowHeight = singleRowHeight * this.rowSpan; return `height: ${totalRowHeight}px; z-index: 1;`; } public afterAttached(): void { let querySelector = `[comp-id="${this.getCompId()}"]`; let eGui = <HTMLElement> this.eParentRow.querySelector(querySelector); this.setGui(eGui); // all of these have dependencies on the eGui, so only do them after eGui is set this.addDomData(); this.populateTemplate(); this.attachCellRenderer(); this.angular1Compile(); this.addDestroyableEventListener(this.beans.eventService, Events.EVENT_CELL_FOCUSED, this.onCellFocused.bind(this)); this.addDestroyableEventListener(this.beans.eventService, Events.EVENT_FLASH_CELLS, this.onFlashCells.bind(this)); this.addDestroyableEventListener(this.beans.eventService, Events.EVENT_COLUMN_HOVER_CHANGED, this.onColumnHover.bind(this)); this.addDestroyableEventListener(this.rowNode, RowNode.EVENT_ROW_INDEX_CHANGED, this.onRowIndexChanged.bind(this)); this.addDestroyableEventListener(this.rowNode, RowNode.EVENT_CELL_CHANGED, this.onCellChanged.bind(this)); this.addDestroyableEventListener(this.column, Column.EVENT_LEFT_CHANGED, this.onLeftChanged.bind(this)); this.addDestroyableEventListener(this.column, Column.EVENT_WIDTH_CHANGED, this.onWidthChanged.bind(this)); this.addDestroyableEventListener(this.column, Column.EVENT_FIRST_RIGHT_PINNED_CHANGED, this.onFirstRightPinnedChanged.bind(this)); this.addDestroyableEventListener(this.column, Column.EVENT_LAST_LEFT_PINNED_CHANGED, this.onLastLeftPinnedChanged.bind(this)); // if not doing enterprise, then range selection service would be missing // so need to check before trying to use it if (this.rangeSelectionEnabled) { this.addDestroyableEventListener(this.beans.eventService, Events.EVENT_RANGE_SELECTION_CHANGED, this.onRangeSelectionChanged.bind(this)); } } private onColumnHover(): void { let isHovered = this.beans.columnHoverService.isHovered(this.column); _.addOrRemoveCssClass(this.getGui(), 'ag-column-hover', isHovered); } private onCellChanged(event: CellChangedEvent): void { let eventImpactsThisCell = event.column === this.column; if (eventImpactsThisCell) { this.refreshCell({}); } } private getCellLeft(): number { let mostLeftCol: Column; if (this.beans.gridOptionsWrapper.isEnableRtl() && this.colsSpanning) { mostLeftCol = this.colsSpanning[this.colsSpanning.length - 1]; } else { mostLeftCol = this.column; } return mostLeftCol.getLeft(); } private getCellWidth(): number { if (this.colsSpanning) { let result = 0; this.colsSpanning.forEach(col => result += col.getActualWidth()); return result; } else { return this.column.getActualWidth(); } } private onFlashCells(event: FlashCellsEvent): void { let cellId = this.gridCell.createId(); let shouldFlash = event.cells[cellId]; if (shouldFlash) { this.animateCell('highlight'); } } private setupColSpan(): void { // if no col span is active, then we don't set it up, as it would be wasteful of CPU if (_.missing(this.column.getColDef().colSpan)) { return; } // because we are col spanning, a reorder of the cols can change what cols we are spanning over this.addDestroyableEventListener(this.beans.eventService, Events.EVENT_DISPLAYED_COLUMNS_CHANGED, this.onDisplayColumnsChanged.bind(this)); // because we are spanning over multiple cols, we check for width any time any cols width changes. // this is expensive - really we should be explicitly checking only the cols we are spanning over // instead of every col, however it would be tricky code to track the cols we are spanning over, so // because hardly anyone will be using colSpan, am favoring this easier way for more maintainable code. this.addDestroyableEventListener(this.beans.eventService, Events.EVENT_DISPLAYED_COLUMNS_WIDTH_CHANGED, this.onWidthChanged.bind(this)); this.colsSpanning = this.getColSpanningList(); } private getColSpanningList(): Column[] { let colSpan = this.column.getColSpan(this.rowNode); let colsSpanning: Column[] = []; // if just one col, the col span is just the column we are in if (colSpan === 1) { colsSpanning.push(this.column); } else { let pointer = this.column; let pinned = this.column.getPinned(); for (let i = 0; i < colSpan; i++) { colsSpanning.push(pointer); pointer = this.beans.columnController.getDisplayedColAfter(pointer); if (_.missing(pointer)) { break; } // we do not allow col spanning to span outside of pinned areas if (pinned !== pointer.getPinned()) { break; } } } return colsSpanning; } private onDisplayColumnsChanged(): void { let colsSpanning: Column[] = this.getColSpanningList(); if (!_.compareArrays(this.colsSpanning, colsSpanning)) { this.colsSpanning = colsSpanning; this.onWidthChanged(); this.onLeftChanged(); // left changes when doing RTL } } private getInitialCssClasses(): string[] { let cssClasses: string[] = ["ag-cell", "ag-cell-not-inline-editing"]; // if we are putting the cell into a dummy container, to work out it's height, // then we don't put the height css in, as we want cell to fit height in that case. if (!this.autoHeightCell) { cssClasses.push('ag-cell-with-height'); } let doingFocusCss = !this.beans.gridOptionsWrapper.isSuppressCellSelection(); if (doingFocusCss) { // otherwise the class depends on the focus state cssClasses.push(this.cellFocused ? 'ag-cell-focus' : 'ag-cell-no-focus'); } else { // if we are not doing cell selection, then ag-cell-no-focus gets put onto every cell cssClasses.push('ag-cell-no-focus'); } if (this.firstRightPinned) { cssClasses.push('ag-cell-first-right-pinned'); } if (this.lastLeftPinned) { cssClasses.push('ag-cell-last-left-pinned'); } if (this.beans.columnHoverService.isHovered(this.column)) { cssClasses.push('ag-column-hover'); } _.pushAll(cssClasses, this.preProcessClassesFromColDef()); _.pushAll(cssClasses, this.preProcessCellClassRules()); _.pushAll(cssClasses, this.getRangeClasses()); // if using the wrapper, this class goes on the wrapper instead if (!this.usingWrapper) { cssClasses.push('ag-cell-value'); } return cssClasses; } public getInitialValueToRender(): string { // if using a cellRenderer, then render the html from the cell renderer if it exists if (this.usingCellRenderer) { if (typeof this.cellRendererGui === 'string') { return <string> this.cellRendererGui; } else { return ''; } } let colDef = this.column.getColDef(); if (colDef.template) { // template is really only used for angular 1 - as people using ng1 are used to providing templates with // bindings in it. in ng2, people will hopefully want to provide components, not templates. return colDef.template; } else if (colDef.templateUrl) { // likewise for templateUrl - it's for ng1 really - when we move away from ng1, we can take these out. // niall was pro angular 1 when writing template and templateUrl, if writing from scratch now, would // not do these, but would follow a pattern that was friendly towards components, not templates. let template = this.beans.templateService.getTemplate(colDef.templateUrl, this.refreshCell.bind(this, true)); if (template) { return template; } else { return ''; } } else { return this.getValueToUse(); } } public getRenderedRow(): RowComp { return this.rowComp; } public isSuppressNavigable(): boolean { return this.column.isSuppressNavigable(this.rowNode); } public getCellRenderer(): ICellRendererComp { return this.cellRenderer; } public getCellEditor(): ICellEditorComp { return this.cellEditor; } // + stop editing {forceRefresh: true, suppressFlash: true} // + event cellChanged {} // + cellRenderer.params.refresh() {} -> method passes 'as is' to the cellRenderer, so params could be anything // + rowComp: event dataChanged {animate: update, newData: !update} // + rowComp: api refreshCells() {animate: true/false} // + rowRenderer: api softRefreshView() {} public refreshCell(params?: { suppressFlash?: boolean, newData?: boolean, forceRefresh?: boolean }) { if (this.editingCell) { return; } let newData = params && params.newData; let suppressFlash = (params && params.suppressFlash) || this.column.getColDef().suppressCellFlash; let forceRefresh = params && params.forceRefresh; let oldValue = this.value; this.getValueAndFormat(); // for simple values only (not pojo's), see if the value is the same, and if it is, skip the refresh. // when never allow skipping after an edit, as after editing, we need to put the GUI back to the way // if was before the edit. let valuesDifferent = !this.valuesAreEqual(oldValue, this.value); let dataNeedsUpdating = forceRefresh || valuesDifferent; if (dataNeedsUpdating) { let cellRendererRefreshed: boolean; // if it's 'new data', then we don't refresh the cellRenderer, even if refresh method is available. // this is because if the whole data is new (ie we are showing stock price 'BBA' now and not 'SSD') // then we are not showing a movement in the stock price, rather we are showing different stock. if (newData || suppressFlash) { cellRendererRefreshed = false; } else { cellRendererRefreshed = this.attemptCellRendererRefresh(); } // we do the replace if not doing refresh, or if refresh was unsuccessful. // the refresh can be unsuccessful if we are using a framework (eg ng2 or react) and the framework // wrapper has the refresh method, but the underlying component doesn't if (!cellRendererRefreshed) { this.replaceContentsAfterRefresh(); } if (!suppressFlash) { let flashCell = this.beans.gridOptionsWrapper.isEnableCellChangeFlash() || this.column.getColDef().enableCellChangeFlash; if (flashCell) { this.flashCell(); } } // need to check rules. note, we ignore colDef classes and styles, these are assumed to be static this.postProcessStylesFromColDef(); this.postProcessClassesFromColDef(); } this.refreshToolTip(); // we do cellClassRules even if the value has not changed, so that users who have rules that // look at other parts of the row (where the other part of the row might of changed) will work. this.postProcessCellClassRules(); } // user can also call this via API public flashCell(): void { this.animateCell('data-changed'); } private animateCell(cssName: string): void { let fullName = 'ag-cell-' + cssName; let animationFullName = 'ag-cell-' + cssName + '-animation'; let element = this.getGui(); // we want to highlight the cells, without any animation _.addCssClass(element, fullName); _.removeCssClass(element, animationFullName); // then once that is applied, we remove the highlight with animation setTimeout(() => { _.removeCssClass(element, fullName); _.addCssClass(element, animationFullName); setTimeout(() => { // and then to leave things as we got them, we remove the animation _.removeCssClass(element, animationFullName); }, 1000); }, 500); } private replaceContentsAfterRefresh(): void { // otherwise we rip out the cell and replace it _.removeAllChildren(this.eParentOfValue); // remove old renderer component if it exists if (this.cellRenderer && this.cellRenderer.destroy) { this.cellRenderer.destroy(); } this.cellRenderer = null; this.cellRendererGui = null; // populate this.putDataIntoCellAfterRefresh(); this.angular1Compile(); } private angular1Compile(): void { // if angular compiling, then need to also compile the cell again (angular compiling sucks, please wait...) if (this.beans.gridOptionsWrapper.isAngularCompileRows()) { let eGui = this.getGui(); const compiledElement = this.beans.$compile(eGui)(this.scope); this.addDestroyFunc(() => { compiledElement.remove(); }); } } private postProcessStylesFromColDef() { let stylesToUse = this.processStylesFromColDef(); if (stylesToUse) { _.addStylesToElement(this.getGui(), stylesToUse); } } private preProcessStylesFromColDef(): string { let stylesToUse = this.processStylesFromColDef(); return _.cssStyleObjectToMarkup(stylesToUse); } private processStylesFromColDef(): any { let colDef = this.column.getColDef(); if (colDef.cellStyle) { let cssToUse: any; if (typeof colDef.cellStyle === 'function') { let cellStyleParams = { value: this.value, data: this.rowNode.data, node: this.rowNode, colDef: colDef, column: this.column, $scope: this.scope, context: this.beans.gridOptionsWrapper.getContext(), api: this.beans.gridOptionsWrapper.getApi() }; let cellStyleFunc = <Function>colDef.cellStyle; cssToUse = cellStyleFunc(cellStyleParams); } else { cssToUse = colDef.cellStyle; } return cssToUse; } } private postProcessClassesFromColDef() { this.processClassesFromColDef(className => _.addCssClass(this.getGui(), className)); } private preProcessClassesFromColDef(): string[] { let res: string[] = []; this.processClassesFromColDef(className => res.push(className)); return res; } private processClassesFromColDef(onApplicableClass: (className: string) => void): void { this.beans.stylingService.processStaticCellClasses( this.column.getColDef(), { value: this.value, data: this.rowNode.data, node: this.rowNode, colDef: this.column.getColDef(), rowIndex: this.rowNode.rowIndex, $scope: this.scope, api: this.beans.gridOptionsWrapper.getApi(), context: this.beans.gridOptionsWrapper.getContext() }, onApplicableClass ); } private putDataIntoCellAfterRefresh() { // template gets preference, then cellRenderer, then do it ourselves let colDef = this.column.getColDef(); if (colDef.template) { // template is really only used for angular 1 - as people using ng1 are used to providing templates with // bindings in it. in ng2, people will hopefully want to provide components, not templates. this.eParentOfValue.innerHTML = colDef.template; } else if (colDef.templateUrl) { // likewise for templateUrl - it's for ng1 really - when we move away from ng1, we can take these out. // niall was pro angular 1 when writing template and templateUrl, if writing from scratch now, would // not do these, but would follow a pattern that was friendly towards components, not templates. let template = this.beans.templateService.getTemplate(colDef.templateUrl, this.refreshCell.bind(this, true)); if (template) { this.eParentOfValue.innerHTML = template; } // use cell renderer if it exists } else if (this.usingCellRenderer) { this.attachCellRenderer(); } else { let valueToUse = this.getValueToUse(); if (valueToUse !== null && valueToUse !== undefined) { this.eParentOfValue.innerText = valueToUse; } } } public attemptCellRendererRefresh(): boolean { if (_.missing(this.cellRenderer) || _.missing(this.cellRenderer.refresh)) { return false; } // if the cell renderer has a refresh method, we call this instead of doing a refresh // note: should pass in params here instead of value?? so that client has formattedValue let params = this.createCellRendererParams(); let result: boolean | void = this.cellRenderer.refresh(params); // NOTE on undefined: previous version of the cellRenderer.refresh() interface // returned nothing, if the method existed, we assumed it refreshed. so for // backwards compatibility, we assume if method exists and returns nothing, // that it was successful. return result === true || result === undefined; } private refreshToolTip() { let newTooltip = this.getToolTip(); if (this.tooltip !== newTooltip) { this.tooltip = newTooltip; if (_.exists(newTooltip)) { let tooltipSanitised = _.escape(this.tooltip); this.eParentOfValue.setAttribute('title', tooltipSanitised); } else { this.eParentOfValue.removeAttribute('title'); } } } private valuesAreEqual(val1: any, val2: any): boolean { // if the user provided an equals method, use that, otherwise do simple comparison let colDef = this.column.getColDef(); let equalsMethod: (valueA: any, valueB: any) => boolean = colDef ? colDef.equals : null; if (equalsMethod) { return equalsMethod(val1, val2); } else { return val1 === val2; } } private getToolTip(): string { let colDef = this.column.getColDef(); let data = this.rowNode.data; if (colDef.tooltipField && _.exists(data)) { return _.getValueUsingField(data, colDef.tooltipField, this.column.isTooltipFieldContainsDots()); } else if (colDef.tooltip) { return colDef.tooltip({ value: this.value, valueFormatted: this.valueFormatted, data: this.rowNode.data, node: this.rowNode, colDef: this.column.getColDef(), api: this.beans.gridOptionsWrapper.getApi(), $scope: this.scope, context: this.beans.gridOptionsWrapper.getContext(), rowIndex: this.gridCell.rowIndex }); } else { return null; } } private processCellClassRules(onApplicableClass: (className: string) => void, onNotApplicableClass?: (className: string) => void): void { this.beans.stylingService.processClassRules( this.column.getColDef().cellClassRules, { value: this.value, data: this.rowNode.data, node: this.rowNode, colDef: this.column.getColDef(), rowIndex: this.gridCell.rowIndex, api: this.beans.gridOptionsWrapper.getApi(), $scope: this.scope, context: this.beans.gridOptionsWrapper.getContext() }, onApplicableClass, onNotApplicableClass); } private postProcessCellClassRules(): void { this.processCellClassRules( (className: string) => { _.addCssClass(this.getGui(), className); }, (className: string) => { _.removeCssClass(this.getGui(), className); } ); } private preProcessCellClassRules(): string[] { let res: string[] = []; this.processCellClassRules( (className: string) => { res.push(className); }, (className: string) => { // not catered for, if creating, no need // to remove class as it was never there } ); return res; } // a wrapper is used when we are putting a selection checkbox in the cell with the value public setUsingWrapper(): void { let colDef = this.column.getColDef(); // never allow selection or dragging on pinned rows if (this.rowNode.rowPinned) { this.usingWrapper = false; this.includeSelectionComponent = false; this.includeRowDraggingComponent = false; return; } let cbSelectionIsFunc = typeof colDef.checkboxSelection === 'function'; let rowDraggableIsFunc = typeof colDef.rowDrag === 'function'; this.includeSelectionComponent = cbSelectionIsFunc || colDef.checkboxSelection === true; this.includeRowDraggingComponent = rowDraggableIsFunc || colDef.rowDrag === true; this.usingWrapper = this.includeRowDraggingComponent || this.includeSelectionComponent; } private chooseCellRenderer(): void { // template gets preference, then cellRenderer, then do it ourselves let colDef = this.column.getColDef(); // templates are for ng1, ideally we wouldn't have these, they are ng1 support // inside the core which is bad if (colDef.template || colDef.templateUrl) { this.usingCellRenderer = false; return; } let params = this.createCellRendererParams(); let cellRenderer = this.beans.componentResolver.getComponentToUse(colDef, 'cellRenderer', params,null); let pinnedRowCellRenderer = this.beans.componentResolver.getComponentToUse(colDef, 'pinnedRowCellRenderer', params,null); if (pinnedRowCellRenderer && this.rowNode.rowPinned) { this.cellRendererType = 'pinnedRowCellRenderer'; this.usingCellRenderer = true; } else if (cellRenderer) { this.cellRendererType = 'cellRenderer'; this.usingCellRenderer = true; } else { this.usingCellRenderer = false; } } private createCellRendererInstance(): void { let params = this.createCellRendererParams(); this.cellRendererVersion++; let callback = this.afterCellRendererCreated.bind(this, this.cellRendererVersion); this.beans.componentResolver.createAgGridComponent(this.column.getColDef(), params, this.cellRendererType, params, null).then(callback); } private afterCellRendererCreated(cellRendererVersion: number, cellRenderer: ICellRendererComp): void { // see if daemon if (!this.isAlive() || (cellRendererVersion !== this.cellRendererVersion)) { if (cellRenderer.destroy) { cellRenderer.destroy(); } return; } this.cellRenderer = cellRenderer; this.cellRendererGui = this.cellRenderer.getGui(); if (_.missing(this.cellRendererGui)) { return; } // if async components, then it's possible the user started editing since // this call was made if (!this.editingCell) { this.eParentOfValue.appendChild(this.cellRendererGui); } } private attachCellRenderer(): void { if (!this.usingCellRenderer) { return; } this.createCellRendererInstance(); } private createCellRendererParams(): ICellRendererParams { let params = <ICellRendererParams> { value: this.value, valueFormatted: this.valueFormatted, getValue: this.getValue.bind(this), setValue: (value: any) => { this.beans.valueService.setValue(this.rowNode, this.column, value); }, formatValue: this.formatValue.bind(this), data: this.rowNode.data, node: this.rowNode, colDef: this.column.getColDef(), column: this.column, $scope: this.scope, rowIndex: this.gridCell.rowIndex, api: this.beans.gridOptionsWrapper.getApi(), columnApi: this.beans.gridOptionsWrapper.getColumnApi(), context: this.beans.gridOptionsWrapper.getContext(), refreshCell: this.refreshCell.bind(this), eGridCell: this.getGui(), eParentOfValue: this.eParentOfValue, // these bits are not documented anywhere, so we could drop them? // it was in the olden days to allow user to register for when rendered // row was removed (the row comp was removed), however now that the user // can provide components for cells, the destroy method gets call when this // happens so no longer need to fire event. addRowCompListener: this.rowComp ? this.rowComp.addEventListener.bind(this.rowComp) : null, addRenderedRowListener: (eventType: string, listener: Function) => { console.warn('ag-Grid: since ag-Grid .v11, params.addRenderedRowListener() is now params.addRowCompListener()'); if (this.rowComp) { this.rowComp.addEventListener(eventType, listener); } } }; return params; } private formatValue(value: any): any { let valueFormatted = this.beans.valueFormatterService.formatValue(this.column, this.rowNode, this.scope, value); let valueFormattedExists = valueFormatted !== null && valueFormatted !== undefined; return valueFormattedExists ? valueFormatted : value; } private getValueToUse(): any { let valueFormattedExists = this.valueFormatted !== null && this.valueFormatted !== undefined; return valueFormattedExists ? this.valueFormatted : this.value; } private getValueAndFormat(): void { this.value = this.getValue(); this.valueFormatted = this.beans.valueFormatterService.formatValue(this.column, this.rowNode, this.scope, this.value); } private getValue(): any { // if we don't check this, then the grid will render leaf groups as open even if we are not // allowing the user to open leaf groups. confused? remember for pivot mode we don't allow // opening leaf groups, so we have to force leafGroups to be closed in case the user expanded // them via the API, or user user expanded them in the UI before turning on pivot mode let lockedClosedGroup = this.rowNode.leafGroup && this.beans.columnController.isPivotMode(); let isOpenGroup = this.rowNode.group && this.rowNode.expanded && !this.rowNode.footer && !lockedClosedGroup; if (isOpenGroup && this.beans.gridOptionsWrapper.isGroupIncludeFooter()) { // if doing grouping and footers, we don't want to include the agg value // in the header when the group is open return this.beans.valueService.getValue(this.column, this.rowNode, false, true); } else { return this.beans.valueService.getValue(this.column, this.rowNode); } } public onMouseEvent(eventName: string, mouseEvent: MouseEvent): void { if (_.isStopPropagationForAgGrid(mouseEvent)) { return; } switch (eventName) { case 'click': this.onCellClicked(mouseEvent); break; case 'mousedown': this.onMouseDown(mouseEvent); break; case 'dblclick': this.onCellDoubleClicked(mouseEvent); break; case 'mouseout': this.onMouseOut(mouseEvent); break; case 'mouseover': this.onMouseOver(mouseEvent); break; } } public dispatchCellContextMenuEvent(event: Event) { let colDef = this.column.getColDef(); let cellContextMenuEvent: CellContextMenuEvent = this.createEvent(event, Events.EVENT_CELL_CONTEXT_MENU); this.beans.eventService.dispatchEvent(cellContextMenuEvent); if (colDef.onCellContextMenu) { // to make the callback async, do in a timeout setTimeout( ()=> colDef.onCellContextMenu(cellContextMenuEvent), 0); } } private createEvent(domEvent: Event, eventType: string): CellEvent { let event: CellEvent = { node: this.rowNode, data: this.rowNode.data, value: this.value, column: this.column, colDef: this.column.getColDef(), context: this.beans.gridOptionsWrapper.getContext(), api: this.beans.gridApi, columnApi: this.beans.columnApi, rowPinned: this.rowNode.rowPinned, event: domEvent, type: eventType, rowIndex: this.rowNode.rowIndex }; // because we are hacking in $scope for angular 1, we have to de-reference if (this.scope) { (<any>event).$scope = this.scope; } return event; } private onMouseOut(mouseEvent: MouseEvent): void { let cellMouseOutEvent: CellMouseOutEvent = this.createEvent(mouseEvent, Events.EVENT_CELL_MOUSE_OUT); this.beans.eventService.dispatchEvent(cellMouseOutEvent); this.beans.columnHoverService.clearMouseOver(); } private onMouseOver(mouseEvent: MouseEvent): void { let cellMouseOverEvent: CellMouseOverEvent = this.createEvent(mouseEvent, Events.EVENT_CELL_MOUSE_OVER); this.beans.eventService.dispatchEvent(cellMouseOverEvent); this.beans.columnHoverService.setMouseOver([this.column]); } private onCellDoubleClicked(mouseEvent: MouseEvent) { let colDef = this.column.getColDef(); // always dispatch event to eventService let cellDoubleClickedEvent: CellDoubleClickedEvent = this.createEvent(mouseEvent, Events.EVENT_CELL_DOUBLE_CLICKED); this.beans.eventService.dispatchEvent(cellDoubleClickedEvent); // check if colDef also wants to handle event if (typeof colDef.onCellDoubleClicked === 'function') { // to make the callback async, do in a timeout setTimeout( ()=> colDef.onCellDoubleClicked(cellDoubleClickedEvent), 0); } let editOnDoubleClick = !this.beans.gridOptionsWrapper.isSingleClickEdit() && !this.beans.gridOptionsWrapper.isSuppressClickEdit(); if (editOnDoubleClick) { this.startRowOrCellEdit(); } } // called by rowRenderer when user navigates via tab key public startRowOrCellEdit(keyPress?: number, charPress?: string): void { if (this.beans.gridOptionsWrapper.isFullRowEdit()) { this.rowComp.startRowEditing(keyPress, charPress, this); } else { this.startEditingIfEnabled(keyPress, charPress, true); } } public isCellEditable() { return this.column.isCellEditable(this.rowNode); } // either called internally if single cell editing, or called by rowRenderer if row editing public startEditingIfEnabled(keyPress: number = null, charPress: string = null, cellStartedEdit = false): void { // don't do it if not editable if (!this.isCellEditable()) { return; } // don't do it if already editing if (this.editingCell) { return; } this.editingCell = true; this.cellEditorVersion++; let callback = this.afterCellEditorCreated.bind(this, this.cellEditorVersion); let params = this.createCellEditorParams(keyPress, charPress, cellStartedEdit); this.beans.cellEditorFactory.createCellEditor(this.column.getColDef(), params).then(callback); // if we don't do this, and editor component is async, then there will be a period // when the component isn't present and keyboard navigation won't work - so example // of user hitting tab quickly (more quickly than renderers getting created) won't work let cellEditorAsync = _.missing(this.cellEditor); if (cellEditorAsync && cellStartedEdit) { this.focusCell(true); } } private afterCellEditorCreated(cellEditorVersion: number, cellEditor: ICellEditorComp): void { // if editingCell=false, means user cancelled the editor before component was ready. // if versionMismatch, then user cancelled the edit, then started the edit again, and this // is the first editor which is now stale. let versionMismatch = cellEditorVersion !== this.cellEditorVersion; if (versionMismatch || !this.editingCell) { if (cellEditor.destroy) { cellEditor.destroy(); } return; } if (cellEditor.isCancelBeforeStart && cellEditor.isCancelBeforeStart()) { if (cellEditor.destroy) { cellEditor.destroy(); } this.editingCell = false; return; } if (!cellEditor.getGui) { console.warn(`ag-Grid: cellEditor for column ${this.column.getId()} is missing getGui() method`); // no getGui, for React guys, see if they attached a react component directly if ((<any>cellEditor).render) { console.warn(`ag-Grid: we found 'render' on the component, are you trying to set a React renderer but added it as colDef.cellEditor instead of colDef.cellEditorFmk?`); } if (cellEditor.destroy) { cellEditor.destroy(); } this.editingCell = false; return; } this.cellEditor = cellEditor; this.cellEditorInPopup = cellEditor.isPopup && cellEditor.isPopup(); this.setInlineEditingClass(); if (this.cellEditorInPopup) { this.addPopupCellEditor(); } else { this.addInCellEditor(); } if (cellEditor.afterGuiAttached) { cellEditor.afterGuiAttached(); } let event: CellEditingStartedEvent = this.createEvent(null, Events.EVENT_CELL_EDITING_STARTED); this.beans.eventService.dispatchEvent(event); } private addInCellEditor(): void { _.removeAllChildren(this.getGui()); this.getGui().appendChild(this.cellEditor.getGui()); this.angular1Compile(); } private addPopupCellEditor(): void { let ePopupGui = this.cellEditor.getGui(); this.hideEditorPopup = this.beans.popupService.addAsModalPopup( ePopupGui, true, // callback for when popup disappears () => { this.onPopupEditorClosed(); } ); this.beans.popupService.positionPopupOverComponent({ column: this.column, rowNode: this.rowNode, type: 'popupCellEditor', eventSource: this.getGui(), ePopup: ePopupGui, keepWithinBounds: true }); this.angular1Compile(); } private onPopupEditorClosed(): void { // we only call stopEditing if we are editing, as // it's possible the popup called 'stop editing' // before this, eg if 'enter key' was pressed on // the editor. if (this.editingCell) { // note: this only happens when use clicks outside of the grid. if use clicks on another // cell, then the editing will have already stopped on this cell this.stopRowOrCellEdit(); // we only focus cell again if this cell is still focused. it is possible // it is not focused if the user cancelled the edit by clicking on another // cell outside of this one if (this.beans.focusedCellController.isCellFocused(this.gridCell)) { this.focusCell(true); } } } // if we are editing inline, then we don't have the padding in the cell (set in the themes) // to allow the text editor full access to the entire cell private setInlineEditingClass(): void { // ag-cell-inline-editing - appears when user is inline editing // ag-cell-not-inline-editing - appears when user is no inline editing // ag-cell-popup-editing - appears when user is editing cell in popup (appears on the cell, not on the popup) // note: one of {ag-cell-inline-editing, ag-cell-not-inline-editing} is always present, they toggle. // however {ag-cell-popup-editing} shows when popup, so you have both {ag-cell-popup-editing} // and {ag-cell-not-inline-editing} showing at the same time. let editingInline = this.editingCell && !this.cellEditorInPopup; let popupEditorShowing = this.editingCell && this.cellEditorInPopup; _.addOrRemoveCssClass(this.getGui(), "ag-cell-inline-editing", editingInline); _.addOrRemoveCssClass(this.getGui(), "ag-cell-not-inline-editing", !editingInline); _.addOrRemoveCssClass(this.getGui(), "ag-cell-popup-editing", popupEditorShowing); _.addOrRemoveCssClass(<HTMLElement>this.getGui().parentNode, "ag-row-inline-editing", editingInline); _.addOrRemoveCssClass(<HTMLElement>this.getGui().parentNode, "ag-row-not-inline-editing", !editingInline); } private createCellEditorParams(keyPress: number, charPress: string, cellStartedEdit: boolean): ICellEditorParams { let params: ICellEditorParams = { value: this.getValue(), keyPress: keyPress, charPress: charPress, column: this.column, rowIndex: this.gridCell.rowIndex, node: this.rowNode, api: this.beans.gridOptionsWrapper.getApi(), cellStartedEdit: cellStartedEdit, columnApi: this.beans.gridOptionsWrapper.getColumnApi(), context: this.beans.gridOptionsWrapper.getContext(), $scope: this.scope, onKeyDown: this.onKeyDown.bind(this), stopEditing: this.stopEditingAndFocus.bind(this), eGridCell: this.getGui(), parseValue: this.parseValue.bind(this), formatValue: this.formatValue.bind(this) }; return params; } // cell editors call this, when they want to stop for reasons other // than what we pick up on. eg selecting from a dropdown ends editing. private stopEditingAndFocus(suppressNavigateAfterEdit = false): void { this.stopRowOrCellEdit(); this.focusCell(true); if (!suppressNavigateAfterEdit) { this.navigateAfterEdit(); } } private parseValue(newValue: any): any { let params: NewValueParams = { node: this.rowNode, data: this.rowNode.data, oldValue: this.value, newValue: newValue, colDef: this.column.getColDef(), column: this.column, api: this.beans.gridOptionsWrapper.getApi(), columnApi: this.beans.gridOptionsWrapper.getColumnApi(), context: this.beans.gridOptionsWrapper.getContext() }; let valueParser = this.column.getColDef().valueParser; return _.exists(valueParser) ? this.beans.expressionService.evaluate(valueParser, params) : newValue; } public focusCell(forceBrowserFocus = false): void { this.beans.focusedCellController.setFocusedCell(this.gridCell.rowIndex, this.column, this.rowNode.rowPinned, forceBrowserFocus); } public setFocusInOnEditor(): void { if (this.editingCell) { if (this.cellEditor && this.cellEditor.focusIn) { // if the editor is present, then we just focus it this.cellEditor.focusIn(); } else { // if the editor is not present, it means async cell editor (eg React fibre) // and we are trying to set focus before the cell editor is present, so we // focus the cell instead this.focusCell(true); } } } public isEditing(): boolean { return this.editingCell; } public onKeyDown(event: KeyboardEvent): void { let key = event.which || event.keyCode; // give user a chance to cancel event processing if (this.doesUserWantToCancelKeyboardEvent(event)) { return; } switch (key) { case Constants.KEY_ENTER: this.onEnterKeyDown(); break; case Constants.KEY_F2: this.onF2KeyDown(); break; case Constants.KEY_ESCAPE: this.onEscapeKeyDown(); break; case Constants.KEY_TAB: this.onTabKeyDown(event); break; case Constants.KEY_BACKSPACE: case Constants.KEY_DELETE: this.onBackspaceOrDeleteKeyPressed(key); break; case Constants.KEY_DOWN: case Constants.KEY_UP: case Constants.KEY_RIGHT: case Constants.KEY_LEFT: this.onNavigationKeyPressed(event, key); break; } } public doesUserWantToCancelKeyboardEvent(event: KeyboardEvent): boolean { let callback = this.column.getColDef().suppressKeyboardEvent; if (_.missing(callback)) { return false; } else { // if editing is null or undefined, this sets it to false let params: SuppressKeyboardEventParams = { event: event, editing: this.editingCell, column: this.column, api: this.beans.gridOptionsWrapper.getApi(), node: this.rowNode, data: this.rowNode.data, colDef: this.column.getColDef(), context: this.beans.gridOptionsWrapper.getContext(), columnApi: this.beans.gridOptionsWrapper.getColumnApi() }; return callback(params); } } public setFocusOutOnEditor(): void { if (this.editingCell && this.cellEditor && this.cellEditor.focusOut) { this.cellEditor.focusOut(); } } private onNavigationKeyPressed(event: KeyboardEvent, key: number): void { if (this.editingCell) { this.stopRowOrCellEdit(); }