ag-grid-enterprise
Version:
ag-Grid Enterprise Features
469 lines (375 loc) • 16.5 kB
text/typescript
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 = '←';
private static CHAR_RIGHT_ARROW = '→';
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);
}
}
}