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, 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, ICellRendererFunc, ICellRendererParams} from "./cellRenderers/iCellRenderer"; import {CheckboxSelectionComponent} from "./checkboxSelectionComponent"; import {NewValueParams, SuppressKeyboardEventParams} from "../entities/colDef"; import {Beans} from "./beans"; import {IAfterGuiAttachedParams, ICellRendererAfterGuiAttachedParams} from "../interfaces/iComponent"; import {RowComp} from "./rowComp"; 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 cellFocused: boolean; private editingCell: boolean; private cellEditorInPopup: boolean; private hideEditorPopup: Function; // 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 | string; private cellEditor: ICellEditorComp; private firstRightPinned: boolean; private lastLeftPinned: boolean; private rowComp: RowComp; private rangeSelectionEnabled: boolean; private value: any; private colsSpanning: Column[]; private scope: null; constructor(scope: any, beans: Beans, column: Column, rowNode: RowNode, rowComp: RowComp) { super(); this.scope = scope; this.beans = beans; this.column = column; this.rowNode = rowNode; this.rowComp = rowComp; 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.value = this.getValue(); this.setUsingWrapper(); this.chooseCellRenderer(); this.setupColSpan(); } public getCreateTemplate(): string { let templateParts: string[] = []; let col = this.column; let width = this.getCellWidth(); let left = col.getLeft(); let valueToRender = this.getInitialValueToRender(); let tooltip = this.getToolTip(); let wrapperStartTemplate: string; let wrapperEndTemplate: string; let stylesFromColDef = this.preProcessStylesFromColDef(); let cssClasses = this.getInitialCssClasses(); if (this.usingWrapper) { wrapperStartTemplate = ''; wrapperEndTemplate = ''; } // hey, this looks like React!!! templateParts.push(``); templateParts.push(wrapperStartTemplate); templateParts.push(valueToRender); templateParts.push(wrapperEndTemplate); templateParts.push(``); return templateParts.join(''); } public afterAttached(): void { let querySelector = `[comp-id="${this.getCompId()}"]`; let eGui = this.eParentRow.querySelector(querySelector); this.setHtmlElementNoHydrate(eGui); // all of these have dependencies on the eGui, so only do them after eGui is set this.addDomData(); this.addSelectionCheckbox(); 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.getHtmlElement(), '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 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 { let valueFormatted = this.beans.valueFormatterService.formatValue( this.column, this.rowNode, null, this.value); let valueFormattedExits = valueFormatted !== null && valueFormatted !== undefined; return valueFormattedExits ? valueFormatted : this.value; } } public getRenderedRow(): RowComp { return this.rowComp; } public isSuppressNavigable(): boolean { return this.column.isSuppressNavigable(this.rowNode); } // + 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, volatile?: boolean}) { if (this.editingCell) { return; } let newData = params && params.newData; let suppressFlash = params && params.suppressFlash; let volatile = params && params.volatile; let forceRefresh = params && params.forceRefresh; // if only refreshing volatile cells, then skip the refresh if we are not volatile if (volatile && !this.isVolatile()) { return; } let oldValue = this.value; this.value = this.getValue(); // 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 skipRefresh = !forceRefresh && this.valuesAreEqual(oldValue, this.value); if (skipRefresh) { return; } 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) { this.flashCell(); } // need to check rules. note, we ignore colDef classes and styles, these are assumed to be static this.postProcessStylesFromColDef(); this.postProcessClassesFromColDef(); this.postProcessCellClassRules(); } private flashCell(): void { if (this.beans.gridOptionsWrapper.isEnableCellChangeFlash() || this.column.getColDef().enableCellChangeFlash) { this.animateCell('data-changed'); } } private animateCell(cssName: string): void { let fullName = 'ag-cell-' + cssName; let animationFullName = 'ag-cell-' + cssName + '-animation'; let element = this.getHtmlElement(); // 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(); this.beans.$compile(eGui)(this.scope); } } 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 = colDef.cellStyle; cssToUse = cellStyleFunc(cellStyleParams); } else { cssToUse = colDef.cellStyle; } return cssToUse; } } private postProcessClassesFromColDef() { this.processClassesFromColDef(className => _.addCssClass(this.getHtmlElement(), 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 valueFormatted = this.beans.valueFormatterService.formatValue(this.column, this.rowNode, this.scope, this.value); let valueFormattedExits = valueFormatted !== null && valueFormatted !== undefined; let valueToRender = valueFormattedExits ? valueFormatted : this.value; if (valueToRender!==null && valueToRender!==undefined) { this.eParentOfValue.innerText = valueToRender; } } if (colDef.tooltipField) { let data = this.rowNode.data; if (_.exists(data)) { let tooltip = _.getValueUsingField(data, colDef.tooltipField, this.column.isTooltipFieldContainsDots()); if (_.exists(tooltip)) { this.eParentOfValue.setAttribute('title', tooltip); } } } } 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 valueFormatted = this.formatValue(this.value); let params = this.createCellRendererParams(valueFormatted); 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; } public isVolatile() { return this.column.getColDef().volatile; } 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 { 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(), context: this.beans.gridOptionsWrapper.getContext() }, onApplicableClass, onNotApplicableClass); } private postProcessCellClassRules(): void { this.processCellClassRules( (className:string)=>{ _.addCssClass(this.getHtmlElement(), className); }, (className:string)=>{ _.removeCssClass(this.getHtmlElement(), 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 on pinned rows if (this.rowNode.rowPinned) { this.usingWrapper = false; } else if (typeof colDef.checkboxSelection === 'boolean') { this.usingWrapper = colDef.checkboxSelection; } else if (typeof colDef.checkboxSelection === 'function') { // if checkboxSelection is a function, then the checkbox may or may not be present, // so we include the HTML wrapper to cater for either case this.usingWrapper = true; } else { this.usingWrapper = false; } } 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 cellRenderer = this.beans.componentResolver.getComponentToUse(colDef, 'cellRenderer'); let pinnedRowCellRenderer = this.beans.componentResolver.getComponentToUse(colDef, 'pinnedRowCellRenderer'); 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 valueToRender = this.formatValue(this.value); let params = this.createCellRendererParams(valueToRender); this.cellRenderer = this.beans.componentResolver.createAgGridComponent(this.column.getColDef(), params, this.cellRendererType); this.cellRendererGui = this.cellRenderer.getGui(); if (this.cellRendererGui===null || this.cellRendererGui===undefined) { console.warn('ag-Grid: cellRenderer should return back a string or a DOM object, but got ' + this.cellRendererGui); } } private attachCellRenderer(): void { if (!this.usingCellRenderer) { return; } this.createCellRendererInstance(); if (typeof this.cellRendererGui !== 'object') { //First check for simple return types this.eParentOfValue.innerHTML = this.cellRendererGui + ''; this.cellRendererGui = this.eParentOfValue.firstChild; } else if (this.cellRendererGui instanceof Node){ //Is this an HTML Element this.eParentOfValue.appendChild(this.cellRendererGui); } else if ((this.cellRendererGui).toString) { //Lastly before given up... Does it have a toString this.eParentOfValue.innerHTML = (this.cellRendererGui).toString() + ''; this.cellRendererGui = this.eParentOfValue.firstChild; } } private createCellRendererParams(valueFormatted: string): ICellRendererParams { let params = { value: this.value, valueFormatted: 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), // todo danger - in the new world, these are not present :( 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.addEventListener.bind(this.rowComp), addRenderedRowListener: (eventType: string, listener: Function) => { console.warn('ag-Grid: since ag-Grid .v11, params.addRenderedRowListener() is now params.addRowCompListener()'); 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 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, true); } else { return this.beans.valueService.getValue(this.column, this.rowNode); } } public onMouseEvent(eventName: string, mouseEvent: MouseEvent): void { switch (eventName) { case 'click': this.onCellClicked(mouseEvent); break; case 'mousedown': this.onMouseDown(); break; case 'dblclick': this.onCellDoubleClicked(mouseEvent); break; case 'contextmenu': this.onContextMenu(mouseEvent); break; case 'mouseout': this.onMouseOut(mouseEvent); break; case 'mouseover': this.onMouseOver(mouseEvent); break; } } 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) { (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); } private onMouseOver(mouseEvent: MouseEvent): void { let cellMouseOverEvent: CellMouseOverEvent = this.createEvent(mouseEvent, Events.EVENT_CELL_MOUSE_OVER); this.beans.eventService.dispatchEvent(cellMouseOverEvent); } private onContextMenu(mouseEvent: MouseEvent): void { // to allow us to debug in chrome, we ignore the event if ctrl is pressed. // not everyone wants this, so first 'if' below allows to turn this hack off. if (!this.beans.gridOptionsWrapper.isAllowContextMenuWithControlKey()) { // then do the check if (mouseEvent.ctrlKey || mouseEvent.metaKey) { return; } } let colDef = this.column.getColDef(); let cellContextMenuEvent: CellContextMenuEvent = this.createEvent(mouseEvent, Events.EVENT_CELL_CONTEXT_MENU); this.beans.eventService.dispatchEvent(cellContextMenuEvent); if (colDef.onCellContextMenu) { colDef.onCellContextMenu(cellContextMenuEvent); } if (this.beans.contextMenuFactory && !this.beans.gridOptionsWrapper.isSuppressContextMenu()) { this.beans.contextMenuFactory.showMenu(this.rowNode, this.column, this.value, mouseEvent); mouseEvent.preventDefault(); } } 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') { colDef.onCellDoubleClicked(cellDoubleClickedEvent); } 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) { // don't do it if not editable if (!this.isCellEditable()) { return; } // don't do it if already editing if (this.editingCell) { return; } let cellEditor = this.createCellEditor(keyPress, charPress, cellStartedEdit); if (cellEditor.isCancelBeforeStart && cellEditor.isCancelBeforeStart()) { if (cellEditor.destroy) { cellEditor.destroy(); } return false; } 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 ((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?`); } return false; } this.cellEditor = cellEditor; this.editingCell = true; this.cellEditorInPopup = this.cellEditor.isPopup && this.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); return true; } private addInCellEditor(): void { _.removeAllChildren(this.getHtmlElement()); this.getHtmlElement().appendChild(_.assertHtmlElement(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.getHtmlElement(), ePopup: _.assertHtmlElement(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 { let editingInline = this.editingCell && !this.cellEditorInPopup; _.addOrRemoveCssClass(this.getHtmlElement(), 'ag-cell-inline-editing', editingInline); _.addOrRemoveCssClass(this.getHtmlElement(), 'ag-cell-not-inline-editing', !editingInline); } private createCellEditor(keyPress: number, charPress: string, cellStartedEdit: boolean): ICellEditorComp { let params = this.createCellEditorParams(keyPress, charPress, cellStartedEdit); let cellEditor = this.beans.cellEditorFactory.createCellEditor(this.column.getColDef(), params); return cellEditor; } 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.getHtmlElement(), parseValue: this.parseValue.bind(this), formatValue: this.formatValue.bind(this) }; let colDef = this.column.getColDef(); if (colDef.cellEditorParams) { _.assign(params, colDef.cellEditorParams); } 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(): void { this.stopRowOrCellEdit(); this.focusCell(true); } 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 && this.cellEditor && this.cellEditor.focusIn) { this.cellEditor.focusIn(); } } 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 editing = this.editingCell === true; let params: SuppressKeyboardEventParams = { event: event, editing: editing, column: this.column, api: this.beans.gridOptionsWrapper.getApi(), node: this.rowNode, 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(); } this.beans.rowRenderer.navigateToNextCell(event, key, this.gridCell.rowIndex, this.column, this.rowNode.rowPinned); // if we don't prevent default, the grid will scroll with the navigation keys event.preventDefault(); } private onTabKeyDown(event: KeyboardEvent): void { if (this.beans.gridOptionsWrapper.isSuppressTabbing()) { return; } this.beans.rowRenderer.onTabKeyDown(this, event); } private onBackspaceOrDeleteKeyPressed(key: number): void { if (!this.editingCell) { this.startRowOrCellEdit(key); } } private onEnterKeyDown(): void { if (this.editingCell) { this.stopRowOrCellEdit(); this.focusCell(true); } else { this.startRowOrCellEdit(Constants.KEY_ENTER); } } private onF2KeyDown(): void { if (!this.editingCell) { this.startRowOrCellEdit(Constants.KEY_F2); } } private onEscapeKeyDown(): void { if (this.editingCell) { this.stopRowOrCellEdit(true); this.focusCell(true); } } public onKeyPress(event: KeyboardEvent): void { // check this, in case focus is on a (for example) a text field inside the cell, // in which cse we should not be listening for these key pressed let eventTarget = _.getTarget(event); let eventOnChildComponent = eventTarget!==this.getGui(); if (eventOnChildComponent) { return; } if (!this.editingCell) { let pressedChar = String.fromCharCode(event.charCode); if (pressedChar === ' ') { this.onSpaceKeyPressed(event); } else { if (_.isEventFromPrintableCharacter(event)) { this.startRowOrCellEdit(null, pressedChar); // if we don't prevent default, then the keypress also gets applied to the text field // (at least when doing the default editor), but we need to allow the editor to decide // what it wants to do. we only do this IF editing was started - otherwise it messes // up when the use is not doing editing, but using rendering with text fields in cellRenderer // (as it would block the the user from typing into text fields). event.preventDefault(); } } } } private onSpaceKeyPressed(event: KeyboardEvent): void { if (!this.editingCell && this.beans.gridOptionsWrapper.isRowSelection()) { let selected = this.rowNode.isSelected(); this.rowNode.setSelected(!selected); } // prevent default as space key, by default, moves browser scroll down event.preventDefault(); } private onMouseDown(): void { // 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. this.focusCell(false); // if it's a right click, then if the cell is already in range, // don't change the range, however if the cell is not in a range, // we set a new range if (this.beans.rangeController) { let thisCell = this.gridCell; let cellAlreadyInRange = this.beans.rangeController.isCellInAnyRange(thisCell); if (!cellAlreadyInRange) { this.beans.rangeController.setRangeToCell(thisCell); } } } private onCellClicked(mouseEvent: MouseEvent): void { let cellClickedEvent: CellClickedEvent = this.createEvent(mouseEvent, Events.EVENT_CELL_CLICKED); this.beans.eventService.dispatchEvent(cellClickedEvent); let colDef = this.column.getColDef(); if (colDef.onCellClicked) { colDef.onCellClicked(cellClickedEvent); } let editOnSingleClick = this.beans.gridOptionsWrapper.isSingleClickEdit() && !this.beans.gridOptionsWrapper.isSuppressClickEdit(); if (editOnSingleClick) { this.startRowOrCellEdit(); } this.doIeFocusHack(); } // https://ag-grid.com/forum/showthread.php?tid=4362 // when in IE or Edge, when you are editing a cell, then click on another cell, // the other cell doesn't keep focus, so navigation keys, type to start edit etc // don't work. appears that when you update the dom in IE it looses focus private doIeFocusHack(): void { if (_.isBrowserIE() || _.isBrowserEdge()) { if (_.missing(document.activeElement) || document.activeElement===document.body) { // console.log('missing focus'); this.getHtmlElement().focus(); } } } private createGridCellVo(): void { let gridCellDef = { rowIndex: this.rowNode.rowIndex, floating: this.rowNode.rowPinned, column: this.column }; this.gridCell = new GridCell(gridCellDef); } public getGridCell(): GridCell { return this.gridCell; } public getParentRow(): HTMLElement { return this.eParentRow; } public setParentRow(eParentRow: HTMLElement): void { this.eParentRow = eParentRow; } public getColumn(): Column { return this.column; } public detach(): void { this.eParentRow.removeChild(this.getHtmlElement()); } // if the row is also getting destroyed, then we don't need to remove from dom, // as the row will also get removed, so no need to take out the cells from the row // if the row is going (removing is an expensive operation, so only need to remove // the top part) public destroy(): void { super.destroy(); if (this.cellEditor && this.cellEditor.destroy) { this.cellEditor.destroy(); } if (this.cellRenderer && this.cellRenderer.destroy) { this.cellRenderer.destroy(); } } private onLeftChanged(): void { let left = this.getCellLeft(); this.getHtmlElement().style.left = left + 'px'; } private onWidthChanged(): void { let width = this.getCellWidth(); this.getHtmlElement().style.width = width + 'px'; } private getRangeClasses(): string[] { let res: string[] = []; if (!this.rangeSelectionEnabled) { return res; } if (this.rangeCount!==0) { res.push('ag-cell-range-selected'); } if (this.rangeCount===1) { res.push('ag-cell-range-selected-1'); } if (this.rangeCount===2) { res.push('ag-cell-range-selected-2'); } if (this.rangeCount===3) { res.push('ag-cell-range-selected-3'); } if (this.rangeCount>=4) { res.push('ag-cell-range-selected-4'); } return res; } private onRowIndexChanged(): void { // when index changes, this influences items that need the index, so we update the // grid cell so they are working off the new index. this.createGridCellVo(); // when the index of the row changes, ie means the cell may have lost or gained focus this.onCellFocused(); // check range selection this.onRangeSelectionChanged(); } private onRangeSelectionChanged(): void { if (!this.beans.enterprise) { return; } let newRangeCount = this.beans.rangeController.getCellRangeCount(this.gridCell); let element = this.getHtmlElement(); if (this.rangeCount !== newRangeCount) { _.addOrRemoveCssClass(element, 'ag-cell-range-selected', newRangeCount!==0); _.addOrRemoveCssClass(element, 'ag-cell-range-selected-1', newRangeCount===1); _.addOrRemoveCssClass(element, 'ag-cell-range-selected-2', newRangeCount===2); _.addOrRemoveCssClass(element, 'ag-cell-range-selected-3', newRangeCount===3); _.addOrRemoveCssClass(element, 'ag-cell-range-selected-4', newRangeCount>=4); this.rangeCount = newRangeCount; } } private onFirstRightPinnedChanged(): void { let firstRightPinned = this.column.isFirstRightPinned(); if (this.firstRightPinned !== firstRightPinned) { this.firstRightPinned = firstRightPinned; _.addOrRemoveCssClass(this.getHtmlElement(), 'ag-cell-first-right-pinned', firstRightPinned); } } private onLastLeftPinnedChanged(): void { let lastLeftPinned = this.column.isLastLeftPinned(); if (this.lastLeftPinned !== lastLeftPinned) { this.lastLeftPinned = lastLeftPinned; _.addOrRemoveCssClass(this.getHtmlElement(), 'ag-cell-last-left-pinned', lastLeftPinned); } } private addSelectionCheckbox(): void { if (this.usingWrapper) { this.eParentOfValue = this.getRefElement('eCellValue'); this.eCellWrapper = this.getRefElement('eCellWrapper'); let cbSelectionComponent = new CheckboxSelectionComponent(); this.beans.context.wireBean(cbSelectionComponent); let visibleFunc = this.column.getColDef().checkboxSelection; visibleFunc = typeof visibleFunc === 'function' ? visibleFunc : null; cbSelectionComponent.init({rowNode: this.rowNode, column: this.column, visibleFunc: visibleFunc}); this.addDestroyFunc( ()=> cbSelectionComponent.destroy() ); // put the checkbox in before the value this.eCellWrapper.insertBefore(cbSelectionComponent.getHtmlElement(), this.eParentOfValue); } else { this.eParentOfValue = this.getHtmlElement(); } } private addDomData(): void { let element = this.getHtmlElement(); this.beans.gridOptionsWrapper.setDomData(element, CellComp.DOM_DATA_KEY_CELL_COMP, this); this.addDestroyFunc( ()=> this.beans.gridOptionsWrapper.setDomData(element, CellComp.DOM_DATA_KEY_CELL_COMP, null) ); } private onCellFocused(event?: any): void { let cellFocused = this.beans.focusedCellController.isCellFocused(this.gridCell); // see if we need to change the classes on this cell if (cellFocused !== this.cellFocused) { _.addOrRemoveCssClass(this.getHtmlElement(), 'ag-cell-focus', cellFocused); _.addOrRemoveCssClass(this.getHtmlElement(), 'ag-cell-no-focus', !cellFocused); this.cellFocused = cellFocused; } // if this cell was just focused, see if we need to force browser focus, his can // happen if focus is programmatically set. if (cellFocused && event && event.forceBrowserFocus) { this.getHtmlElement().focus(); } // if another cell was focused, and we are editing, then stop editing let fullRowEdit = this.beans.gridOptionsWrapper.isFullRowEdit(); if (!cellFocused && !fullRowEdit && this.editingCell) { this.stopRowOrCellEdit(); } } // pass in 'true' to cancel the editing. public stopRowOrCellEdit(cancel: boolean = false) { if (this.beans.gridOptionsWrapper.isFullRowEdit()) { this.rowComp.stopRowEditing(cancel); } else { this.stopEditing(cancel); } } public stopEditing(cancel = false): void { if (!this.editingCell) { return; } if (!cancel) { // also have another option here to cancel after editing, so for example user could have a popup editor and // it is closed by user clicking outside the editor. then the editor will close automatically (with false // passed above) and we need to see if the editor wants to accept the new value. let userWantsToCancel = this.cellEditor.isCancelAfterEnd && this.cellEditor.isCancelAfterEnd(); if (!userWantsToCancel) { let newValue = this.cellEditor.getValue(); this.rowNode.setDataValue(this.column, newValue); this.value = this.getValue(); } } // it is important we set this after setValue() above, as otherwise the cell will flash // when editing stops. the 'refresh' method checks editing, and doesn't refresh editing cells. // thus it will skip the refresh on this cell until the end of this method where we call // refresh directly and we suppress the flash. this.editingCell = false; if (this.cellEditor.destroy) { this.cellEditor.destroy(); } if (this.cellEditorInPopup) { this.hideEditorPopup(); this.hideEditorPopup = null; } else { _.removeAllChildren(this.getHtmlElement()); // put the cell back the way it was before editing if (this.usingWrapper) { // if wrapper, then put the wrapper back this.getHtmlElement().appendChild(this.eCellWrapper); } else { // if cellRenderer, then put the gui back in. if the renderer has // a refresh, it will be called. however if it doesn't, then later // the renderer will be destroyed and a new one will be created. if (this.cellRenderer) { // we know it's a dom element (not a string) because we converted // it after the gui was attached if it was a string. let eCell = this.cellRendererGui; // can be null if cell was previously null / contained empty string, // this will result in new value not being rendered. if(eCell) { this.getHtmlElement().appendChild(eCell); } } } } this.setInlineEditingClass(); // we suppress the flash, as it is not correct to flash the cell the user has finished editing, // the user doesn't need to flash as they were the one who did the edit, the flash is pointless // (as the flash is meant to draw the user to a change that they didn't manually do themselves). this.refreshCell({forceRefresh: true, suppressFlash: true}); let event: CellEditingStoppedEvent = this.createEvent(null, Events.EVENT_CELL_EDITING_STOPPED); this.beans.eventService.dispatchEvent(event); } }