UNPKG

ag-grid-enterprise

Version:

ag-Grid Enterprise Features

469 lines (375 loc) 16.5 kB
import { _, Column, Component, Context, DragAndDropService, DraggingEvent, DragSourceType, DropTarget, Events, EventService, GridOptionsWrapper, HDirection, Logger, LoggerFactory, Utils, VDirection } from "ag-grid-community/main"; import {DropZoneColumnComp} from "./dropZoneColumnComp"; export interface BaseDropZonePanelParams { dragAndDropIcon: string; emptyMessage: string; title: string; icon: HTMLElement; } export interface BaseDropZonePanelBeans { gridOptionsWrapper: GridOptionsWrapper; eventService: EventService; context: Context; loggerFactory: LoggerFactory; dragAndDropService: DragAndDropService; } export abstract class BaseDropZonePanel extends Component { private static STATE_NOT_DRAGGING = 'notDragging'; private static STATE_NEW_COLUMNS_IN = 'newColumnsIn'; private static STATE_REARRANGE_COLUMNS = 'rearrangeColumns'; private static CHAR_LEFT_ARROW = '&#8592;'; private static CHAR_RIGHT_ARROW = '&#8594;'; private state = BaseDropZonePanel.STATE_NOT_DRAGGING; private logger: Logger; private dropTarget: DropTarget; // when we are considering a drop from a dnd event, // the columns to be dropped go in here private potentialDndColumns: Column[]; private guiDestroyFunctions: (() => void)[] = []; private params: BaseDropZonePanelParams; private beans: BaseDropZonePanelBeans; private childColumnComponents: DropZoneColumnComp[] = []; private insertIndex: number; private horizontal: boolean; private valueColumn: boolean; // when this component is refreshed, we rip out all DOM elements and build it up // again from scratch. one exception is eColumnDropList, as we want to maintain the // scroll position between the refreshes, so we create one instance of it here and // reuse it. private eColumnDropList: HTMLElement; protected abstract isColumnDroppable(column: Column): boolean; protected abstract updateColumns(columns: Column[]): void; protected abstract getExistingColumns(): Column[]; protected abstract getIconName(): string; constructor(horizontal: boolean, valueColumn: boolean, name: string) { super(`<div class="ag-column-drop ag-font-style ag-column-drop-${horizontal ? 'horizontal' : 'vertical'} ag-column-drop-${name}"></div>`); this.horizontal = horizontal; this.valueColumn = valueColumn; this.eColumnDropList = _.loadTemplate('<div class="ag-column-drop-list"></div>'); } public isHorizontal(): boolean { return this.horizontal; } public setBeans(beans: BaseDropZonePanelBeans): void { this.beans = beans; } public destroy(): void { this.destroyGui(); super.destroy(); } private destroyGui(): void { this.guiDestroyFunctions.forEach((func) => func()); this.guiDestroyFunctions.length = 0; this.childColumnComponents.length = 0; Utils.removeAllChildren(this.getGui()); Utils.removeAllChildren(this.eColumnDropList); } public init(params: BaseDropZonePanelParams): void { this.params = params; this.logger = this.beans.loggerFactory.create('AbstractColumnDropPanel'); this.beans.eventService.addEventListener(Events.EVENT_COLUMN_EVERYTHING_CHANGED, this.refreshGui.bind(this)); this.addDestroyableEventListener(this.beans.gridOptionsWrapper, 'functionsReadOnly', this.refreshGui.bind(this)); this.setupDropTarget(); // we don't know if this bean will be initialised before columnController. // if columnController first, then below will work // if columnController second, then below will put blank in, and then above event gets first when columnController is set up this.refreshGui(); } private setupDropTarget(): void { this.dropTarget = { getContainer: this.getGui.bind(this), getIconName: this.getIconName.bind(this), onDragging: this.onDragging.bind(this), onDragEnter: this.onDragEnter.bind(this), onDragLeave: this.onDragLeave.bind(this), onDragStop: this.onDragStop.bind(this), isInterestedIn: this.isInterestedIn.bind(this) }; this.beans.dragAndDropService.addDropTarget(this.dropTarget); } private isInterestedIn(type: DragSourceType): boolean { // not interested in row drags return type === DragSourceType.HeaderCell || type === DragSourceType.ToolPanel; } private checkInsertIndex(draggingEvent: DraggingEvent): boolean { let newIndex: number; if (this.horizontal) { newIndex = this.getNewHorizontalInsertIndex(draggingEvent); } else { newIndex = this.getNewVerticalInsertIndex(draggingEvent); } // <0 happens when drag is no a direction we are interested in, eg drag is up/down but in horizontal panel if (newIndex < 0) { return false; } let changed = newIndex !== this.insertIndex; if (changed) { this.insertIndex = newIndex; } return changed; } private getNewHorizontalInsertIndex(draggingEvent: DraggingEvent): number { if (Utils.missing(draggingEvent.hDirection)) { return -1; } let newIndex = 0; let mouseEvent = draggingEvent.event; let enableRtl = this.beans.gridOptionsWrapper.isEnableRtl(); let goingLeft = draggingEvent.hDirection === HDirection.Left; let mouseX = mouseEvent.clientX; this.childColumnComponents.forEach(childColumn => { let rect = childColumn.getGui().getBoundingClientRect(); let rectX = goingLeft ? rect.right : rect.left; let horizontalFit = enableRtl ? (mouseX <= rectX) : (mouseX >= rectX); if (horizontalFit) { newIndex++; } }); return newIndex; } private getNewVerticalInsertIndex(draggingEvent: DraggingEvent): number { if (Utils.missing(draggingEvent.vDirection)) { return -1; } let newIndex = 0; let mouseEvent = draggingEvent.event; this.childColumnComponents.forEach(childColumn => { let rect = childColumn.getGui().getBoundingClientRect(); if (draggingEvent.vDirection === VDirection.Down) { let verticalFit = mouseEvent.clientY >= rect.top; if (verticalFit) { newIndex++; } } else { let verticalFit = mouseEvent.clientY >= rect.bottom; if (verticalFit) { newIndex++; } } }); return newIndex; } private checkDragStartedBySelf(draggingEvent: DraggingEvent): void { if (this.state !== BaseDropZonePanel.STATE_NOT_DRAGGING) { return; } this.state = BaseDropZonePanel.STATE_REARRANGE_COLUMNS; this.potentialDndColumns = draggingEvent.dragSource.dragItemCallback().columns || []; this.refreshGui(); this.checkInsertIndex(draggingEvent); this.refreshGui(); } private onDragging(draggingEvent: DraggingEvent): void { this.checkDragStartedBySelf(draggingEvent); let positionChanged = this.checkInsertIndex(draggingEvent); if (positionChanged) { this.refreshGui(); } } private onDragEnter(draggingEvent: DraggingEvent): void { // this will contain all columns that are potential drops let dragColumns: Column[] = draggingEvent.dragSource.dragItemCallback().columns || []; this.state = BaseDropZonePanel.STATE_NEW_COLUMNS_IN; // take out columns that are not groupable let goodDragColumns = Utils.filter(dragColumns, this.isColumnDroppable.bind(this)); let weHaveColumnsToDrag = goodDragColumns.length > 0; if (weHaveColumnsToDrag) { this.potentialDndColumns = goodDragColumns; this.checkInsertIndex(draggingEvent); this.refreshGui(); } } protected isPotentialDndColumns(): boolean { return Utils.existsAndNotEmpty(this.potentialDndColumns); } private onDragLeave(draggingEvent: DraggingEvent): void { // if the dragging started from us, we remove the group, however if it started // someplace else, then we don't, as it was only 'asking' if (this.state === BaseDropZonePanel.STATE_REARRANGE_COLUMNS) { let columns = draggingEvent.dragSource.dragItemCallback().columns || []; this.removeColumns(columns); } if (this.isPotentialDndColumns()) { this.potentialDndColumns = []; this.refreshGui(); } this.state = BaseDropZonePanel.STATE_NOT_DRAGGING; } private onDragStop(): void { if (this.isPotentialDndColumns()) { let success: boolean; if (this.state === BaseDropZonePanel.STATE_NEW_COLUMNS_IN) { this.addColumns(this.potentialDndColumns); success = true; } else { success = this.rearrangeColumns(this.potentialDndColumns); } this.potentialDndColumns = []; // if the function is passive, then we don't refresh, as we assume the client application // is going to call setRowGroups / setPivots / setValues at a later point which will then // cause a refresh. this gives a nice gui where the ghost stays until the app has caught // up with the changes. if (this.beans.gridOptionsWrapper.isFunctionsPassive()) { // when functions are passive, we don't refresh, // unless there was no change in the order, then we // do need to refresh to reset the columns if (!success) { this.refreshGui(); } } else { this.refreshGui(); } } this.state = BaseDropZonePanel.STATE_NOT_DRAGGING; } private removeColumns(columnsToRemove: Column[]): void { let newColumnList = this.getExistingColumns().slice(); columnsToRemove.forEach(column => Utils.removeFromArray(newColumnList, column)); this.updateColumns(newColumnList); } private addColumns(columnsToAdd: Column[]): void { let newColumnList = this.getExistingColumns().slice(); Utils.insertArrayIntoArray(newColumnList, columnsToAdd, this.insertIndex); this.updateColumns(newColumnList); } private rearrangeColumns(columnsToAdd: Column[]): boolean { let newColumnList = this.getNonGhostColumns().slice(); Utils.insertArrayIntoArray(newColumnList, columnsToAdd, this.insertIndex); let noChangeDetected = Utils.shallowCompare(newColumnList, this.getExistingColumns()); if (noChangeDetected) { return false; } else { this.updateColumns(newColumnList); return true; } } public refreshGui(): void { // we reset the scroll position after the refresh. // if we don't do this, then the list will always scroll to the top // each time we refresh it. this is because part of the refresh empties // out the list which sets scroll to zero. so the user could be just // reordering the list - we want to prevent the resetting of the scroll. // this is relevant for vertical display only (as horizontal has no scroll) let scrollTop = this.eColumnDropList.scrollTop; this.destroyGui(); this.addIconAndTitleToGui(); this.addEmptyMessageToGui(); this.addColumnsToGui(); if (!this.isHorizontal()) { this.eColumnDropList.scrollTop = scrollTop; } } private getNonGhostColumns(): Column[] { let existingColumns = this.getExistingColumns(); let nonGhostColumns: Column[]; if (this.isPotentialDndColumns()) { nonGhostColumns = Utils.filter(existingColumns, column => this.potentialDndColumns.indexOf(column) < 0); } else { nonGhostColumns = existingColumns; } return nonGhostColumns; } private addColumnsToGui(): void { let nonGhostColumns = this.getNonGhostColumns(); let itemsToAddToGui: DropZoneColumnComp[] = []; let addingGhosts = this.isPotentialDndColumns(); nonGhostColumns.forEach((column: Column, index: number) => { if (addingGhosts && index >= this.insertIndex) { return; } let columnComponent = this.createColumnComponent(column, false); itemsToAddToGui.push(columnComponent); }); if (this.isPotentialDndColumns()) { this.potentialDndColumns.forEach((column) => { let columnComponent = this.createColumnComponent(column, true); itemsToAddToGui.push(columnComponent); }); nonGhostColumns.forEach((column: Column, index: number) => { if (index < this.insertIndex) { return; } let columnComponent = this.createColumnComponent(column, false); itemsToAddToGui.push(columnComponent); }); } this.getGui().appendChild(this.eColumnDropList); itemsToAddToGui.forEach((columnComponent: DropZoneColumnComp, index: number) => { let needSeparator = index !== 0; if (needSeparator) { this.addArrow(this.eColumnDropList); } this.eColumnDropList.appendChild(columnComponent.getGui()); }); } private createColumnComponent(column: Column, ghost: boolean): DropZoneColumnComp { let columnComponent = new DropZoneColumnComp(column, this.dropTarget, ghost, this.valueColumn); columnComponent.addEventListener(DropZoneColumnComp.EVENT_COLUMN_REMOVE, this.removeColumns.bind(this, [column])); this.beans.context.wireBean(columnComponent); this.guiDestroyFunctions.push(() => columnComponent.destroy()); if (!ghost) { this.childColumnComponents.push(columnComponent); } return columnComponent; } private addIconAndTitleToGui(): void { let iconFaded = this.horizontal && this.isExistingColumnsEmpty(); let eGroupIcon = this.params.icon; let eContainer = document.createElement('div'); Utils.addCssClass(eGroupIcon, 'ag-column-drop-icon'); Utils.addOrRemoveCssClass(eGroupIcon, 'ag-faded', iconFaded); eContainer.appendChild(eGroupIcon); if (!this.horizontal) { let eTitle = document.createElement('span'); eTitle.innerHTML = this.params.title; Utils.addCssClass(eTitle, 'ag-column-drop-title'); Utils.addOrRemoveCssClass(eTitle, 'ag-faded', iconFaded); eContainer.appendChild(eTitle); } this.getGui().appendChild(eContainer); } private isExistingColumnsEmpty(): boolean { return this.getExistingColumns().length === 0; } private addEmptyMessageToGui(): void { let showEmptyMessage = this.isExistingColumnsEmpty() && !this.potentialDndColumns; if (!showEmptyMessage) { return; } let eMessage = document.createElement('span'); eMessage.innerHTML = this.params.emptyMessage; Utils.addCssClass(eMessage, 'ag-column-drop-empty-message'); this.getGui().appendChild(eMessage); } private addArrow(eParent: HTMLElement): void { // only add the arrows if the layout is horizontal if (this.horizontal) { // for RTL it's a left arrow, otherwise it's a right arrow let enableRtl = this.beans.gridOptionsWrapper.isEnableRtl(); let charCode = enableRtl ? BaseDropZonePanel.CHAR_LEFT_ARROW : BaseDropZonePanel.CHAR_RIGHT_ARROW; let spanClass = enableRtl ? 'ag-left-arrow' : 'ag-right-arrow'; let eArrow = document.createElement('span'); eArrow.className = spanClass; eArrow.innerHTML = charCode; eParent.appendChild(eArrow); } } }