import {Logger, LoggerFactory} from "../logger";
import {Qualifier, PostConstruct, Bean, Autowired, PreDestroy} from "../context/context";
import {Column} from "../entities/column";
import {Utils as _} from "../utils";
import {GridOptionsWrapper} from "../gridOptionsWrapper";
import {DragService, DragListenerParams} from "./dragService";
import {ColumnController} from "../columnController/columnController";
import {Environment} from "../environment";
import {RowNode} from "../entities/rowNode";
export enum DragSourceType { ToolPanel, HeaderCell, RowDrag }
export interface DragItem {
// if moving a row, the, this contains the row node
rowNode?: RowNode;
// if moving columns, this contains the columns and the visible state
columns?: Column[];
visibleState?: {[key: string]: boolean};
}
export interface DragSource {
/** So the drop target knows what type of event it is, useful for columns,
* we we re-ordering or moving dropping from toolPanel */
type: DragSourceType;
/** Element which, when dragged, will kick off the DnD process */
eElement: HTMLElement;
/** If eElement is dragged, then the dragItem is the object that gets passed around. */
dragItemCallback: () => DragItem;
/** This name appears in the ghost icon when dragging */
dragItemName: string;
/** The drop target associated with this dragSource. So when dragging starts, this target does not get
* onDragEnter event. */
dragSourceDropTarget?: DropTarget;
/** After how many pixels of dragging should the drag operation start. Default is 4px. */
dragStartPixels?: number;
/** Callback for drag started */
dragStarted?: ()=>void;
/** Callback for drag stopped */
dragStopped?: ()=>void;
}
export interface DropTarget {
/** The main container that will get the drop. */
getContainer(): HTMLElement;
/** If any secondary containers. For example when moving columns in ag-Grid, we listen for drops
* in the header as well as the body (main rows and pinned rows) of the grid. */
getSecondaryContainers?(): HTMLElement[];
/** Icon to show when drag is over*/
getIconName?(): string;
isInterestedIn(type: DragSourceType): boolean;
/** Callback for when drag enters */
onDragEnter?(params: DraggingEvent): void;
/** Callback for when drag leaves */
onDragLeave?(params: DraggingEvent): void;
/** Callback for when dragging */
onDragging?(params: DraggingEvent): void;
/** Callback for when drag stops */
onDragStop?(params: DraggingEvent): void;
}
export enum VDirection {Up, Down}
export enum HDirection {Left, Right}
export interface DraggingEvent {
event: MouseEvent;
x: number;
y: number;
vDirection: VDirection;
hDirection: HDirection;
dragSource: DragSource;
dragItem: DragItem;
fromNudge: boolean;
}
@Bean('dragAndDropService')
export class DragAndDropService {
@Autowired('gridOptionsWrapper') private gridOptionsWrapper: GridOptionsWrapper;
@Autowired('dragService') private dragService: DragService;
@Autowired('environment') private environment: Environment;
@Autowired('columnController') private columnController: ColumnController;
public static ICON_PINNED = 'pinned';
public static ICON_ADD = 'add';
public static ICON_MOVE = 'move';
public static ICON_LEFT = 'left';
public static ICON_RIGHT = 'right';
public static ICON_GROUP = 'group';
public static ICON_AGGREGATE = 'aggregate';
public static ICON_PIVOT = 'pivot';
public static ICON_NOT_ALLOWED = 'notAllowed';
public static GHOST_TEMPLATE =
'
';
private logger: Logger;
private dragSourceAndParamsList: {params: DragListenerParams, dragSource: DragSource}[] = [];
private dragItem: DragItem;
private eventLastTime: MouseEvent;
private dragSource: DragSource;
private dragging: boolean;
private eGhost: HTMLElement;
private eGhostParent: HTMLElement;
private eGhostIcon: HTMLElement;
private dropTargets: DropTarget[] = [];
private lastDropTarget: DropTarget;
private ePinnedIcon: HTMLElement;
private ePlusIcon: HTMLElement;
private eHiddenIcon: HTMLElement;
private eMoveIcon: HTMLElement;
private eLeftIcon: HTMLElement;
private eRightIcon: HTMLElement;
private eGroupIcon: HTMLElement;
private eAggregateIcon: HTMLElement;
private ePivotIcon: HTMLElement;
private eDropNotAllowedIcon: HTMLElement;
@PostConstruct
private init(): void {
this.ePinnedIcon = _.createIcon('columnMovePin', this.gridOptionsWrapper, null);
this.ePlusIcon = _.createIcon('columnMoveAdd', this.gridOptionsWrapper, null);
this.eHiddenIcon = _.createIcon('columnMoveHide', this.gridOptionsWrapper, null);
this.eMoveIcon = _.createIcon('columnMoveMove', this.gridOptionsWrapper, null);
this.eLeftIcon = _.createIcon('columnMoveLeft', this.gridOptionsWrapper, null);
this.eRightIcon = _.createIcon('columnMoveRight', this.gridOptionsWrapper, null);
this.eGroupIcon = _.createIcon('columnMoveGroup', this.gridOptionsWrapper, null);
this.eAggregateIcon = _.createIcon('columnMoveValue', this.gridOptionsWrapper, null);
this.ePivotIcon = _.createIcon('columnMovePivot', this.gridOptionsWrapper, null);
this.eDropNotAllowedIcon = _.createIcon('dropNotAllowed', this.gridOptionsWrapper, null);
}
private setBeans(@Qualifier('loggerFactory') loggerFactory: LoggerFactory) {
this.logger = loggerFactory.create('OldToolPanelDragAndDropService');
}
private getStringType(type: DragSourceType): string {
switch (type) {
case DragSourceType.RowDrag: return 'row';
case DragSourceType.HeaderCell: return 'headerCell';
case DragSourceType.ToolPanel: return 'toolPanel';
default:
console.warn(`ag-Grid: bug - unknown drag type ${type}`);
return null;
}
}
public addDragSource(dragSource: DragSource, allowTouch = false): void {
let params: DragListenerParams = {
eElement: dragSource.eElement,
dragStartPixels: dragSource.dragStartPixels,
onDragStart: this.onDragStart.bind(this, dragSource),
onDragStop: this.onDragStop.bind(this),
onDragging: this.onDragging.bind(this)
};
this.dragSourceAndParamsList.push({params: params, dragSource: dragSource});
this.dragService.addDragSource(params, allowTouch);
}
public removeDragSource(dragSource: DragSource): void {
let sourceAndParams = _.find(this.dragSourceAndParamsList, item => item.dragSource === dragSource);
if (sourceAndParams) {
this.dragService.removeDragSource(sourceAndParams.params);
_.removeFromArray(this.dragSourceAndParamsList, sourceAndParams);
}
}
@PreDestroy
private destroy(): void {
this.dragSourceAndParamsList.forEach( sourceAndParams => {
this.dragService.removeDragSource(sourceAndParams.params);
});
this.dragSourceAndParamsList.length = 0;
}
public nudge(): void {
if (this.dragging) {
this.onDragging(this.eventLastTime, true);
}
}
private onDragStart(dragSource: DragSource, mouseEvent: MouseEvent): void {
this.dragging = true;
this.dragSource = dragSource;
this.eventLastTime = mouseEvent;
this.dragItem = this.dragSource.dragItemCallback();
this.lastDropTarget = this.dragSource.dragSourceDropTarget;
if (this.dragSource.dragStarted) {
this.dragSource.dragStarted();
}
this.createGhost();
}
private onDragStop(mouseEvent: MouseEvent): void {
this.eventLastTime = null;
this.dragging = false;
if (this.dragSource.dragStopped) {
this.dragSource.dragStopped();
}
if (this.lastDropTarget && this.lastDropTarget.onDragStop) {
let draggingEvent = this.createDropTargetEvent(this.lastDropTarget, mouseEvent, null, null, false);
this.lastDropTarget.onDragStop(draggingEvent);
}
this.lastDropTarget = null;
this.dragItem = null;
this.removeGhost();
}
private onDragging(mouseEvent: MouseEvent, fromNudge: boolean): void {
let hDirection = this.workOutHDirection(mouseEvent);
let vDirection = this.workOutVDirection(mouseEvent);
this.eventLastTime = mouseEvent;
this.positionGhost(mouseEvent);
// check if mouseEvent intersects with any of the drop targets
let dropTarget = _.find(this.dropTargets, this.isMouseOnDropTarget.bind(this, mouseEvent));
if (dropTarget!==this.lastDropTarget) {
this.leaveLastTargetIfExists(mouseEvent, hDirection, vDirection, fromNudge);
this.enterDragTargetIfExists(dropTarget, mouseEvent, hDirection, vDirection, fromNudge);
this.lastDropTarget = dropTarget;
} else if (dropTarget) {
let draggingEvent = this.createDropTargetEvent(dropTarget, mouseEvent, hDirection, vDirection, fromNudge);
dropTarget.onDragging(draggingEvent);
}
}
private enterDragTargetIfExists(dropTarget: DropTarget, mouseEvent: MouseEvent, hDirection: HDirection, vDirection: VDirection, fromNudge: boolean): void {
if (!dropTarget) { return; }
let dragEnterEvent = this.createDropTargetEvent(dropTarget, mouseEvent, hDirection, vDirection, fromNudge);
dropTarget.onDragEnter(dragEnterEvent);
this.setGhostIcon(dropTarget.getIconName ? dropTarget.getIconName() : null);
}
private leaveLastTargetIfExists(mouseEvent: MouseEvent, hDirection: HDirection, vDirection: VDirection, fromNudge: boolean): void {
if (!this.lastDropTarget) { return; }
let dragLeaveEvent = this.createDropTargetEvent(this.lastDropTarget, mouseEvent, hDirection, vDirection, fromNudge);
this.lastDropTarget.onDragLeave(dragLeaveEvent);
this.setGhostIcon(null);
}
private getAllContainersFromDropTarget(dropTarget: DropTarget): HTMLElement[] {
let containers = [dropTarget.getContainer()];
let secondaryContainers = dropTarget.getSecondaryContainers ? dropTarget.getSecondaryContainers() : null;
if (secondaryContainers) {
containers = containers.concat(secondaryContainers);
}
return containers;
}
// checks if the mouse is on the drop target. it checks eContainer and eSecondaryContainers
private isMouseOnDropTarget(mouseEvent: MouseEvent, dropTarget: DropTarget): boolean {
let allContainers = this.getAllContainersFromDropTarget(dropTarget);
let mouseOverTarget: boolean = false;
allContainers.forEach( (eContainer: HTMLElement) => {
if (!eContainer) { return; } // secondary can be missing
let rect = eContainer.getBoundingClientRect();
// if element is not visible, then width and height are zero
if (rect.width===0 || rect.height===0) {
return;
}
let horizontalFit = mouseEvent.clientX >= rect.left && mouseEvent.clientX <= rect.right;
let verticalFit = mouseEvent.clientY >= rect.top && mouseEvent.clientY <= rect.bottom;
//console.log(`rect.width = ${rect.width} || rect.height = ${rect.height} ## verticalFit = ${verticalFit}, horizontalFit = ${horizontalFit}, `);
if (horizontalFit && verticalFit) {
mouseOverTarget = true;
}
});
if (mouseOverTarget) {
let mouseOverTargetAndInterested = dropTarget.isInterestedIn(this.dragSource.type);
return mouseOverTargetAndInterested;
} else {
return false;
}
}
public addDropTarget(dropTarget: DropTarget) {
this.dropTargets.push(dropTarget);
}
public workOutHDirection(event: MouseEvent): HDirection {
if (this.eventLastTime.clientX > event.clientX) {
return HDirection.Left;
} else if (this.eventLastTime.clientX < event.clientX) {
return HDirection.Right;
} else {
return null;
}
}
public workOutVDirection(event: MouseEvent): VDirection {
if (this.eventLastTime.clientY > event.clientY) {
return VDirection.Up;
} else if (this.eventLastTime.clientY < event.clientY) {
return VDirection.Down;
} else {
return null;
}
}
public createDropTargetEvent(dropTarget: DropTarget, event: MouseEvent, hDirection: HDirection, vDirection: VDirection, fromNudge: boolean): DraggingEvent {
// localise x and y to the target component
let rect = dropTarget.getContainer().getBoundingClientRect();
let x = event.clientX - rect.left;
let y = event.clientY - rect.top;
let dropTargetEvent: DraggingEvent = {
event: event,
x: x,
y: y,
vDirection: vDirection,
hDirection: hDirection,
dragSource: this.dragSource,
fromNudge: fromNudge,
dragItem: this.dragItem
};
return dropTargetEvent;
}
private positionGhost(event: MouseEvent): void {
let ghostRect = this.eGhost.getBoundingClientRect();
let ghostHeight = ghostRect.height;
// for some reason, without the '-2', it still overlapped by 1 or 2 pixels, which
// then brought in scrollbars to the browser. no idea why, but putting in -2 here
// works around it which is good enough for me.
let browserWidth = _.getBodyWidth() - 2;
let browserHeight = _.getBodyHeight() - 2;
// put ghost vertically in middle of cursor
let top = event.pageY - (ghostHeight / 2);
// horizontally, place cursor just right of icon
let left = event.pageX - 30;
let usrDocument = this.gridOptionsWrapper.getDocument();
let windowScrollY = window.pageYOffset || usrDocument.documentElement.scrollTop;
let windowScrollX = window.pageXOffset || usrDocument.documentElement.scrollLeft;
// check ghost is not positioned outside of the browser
if (browserWidth>0) {
if ( (left + this.eGhost.clientWidth) > (browserWidth + windowScrollX) ) {
left = browserWidth + windowScrollX - this.eGhost.clientWidth;
}
}
if (left < 0) {
left = 0;
}
if (browserHeight>0) {
if ( (top + this.eGhost.clientHeight) > (browserHeight + windowScrollY) ) {
top = browserHeight + windowScrollY - this.eGhost.clientHeight;
}
}
if (top < 0) {
top = 0;
}
this.eGhost.style.left = left + 'px';
this.eGhost.style.top = top + 'px';
}
private removeGhost(): void {
if (this.eGhost && this.eGhostParent) {
this.eGhostParent.removeChild(this.eGhost);
}
this.eGhost = null;
}
private createGhost(): void {
this.eGhost = _.loadTemplate(DragAndDropService.GHOST_TEMPLATE);
_.addCssClass(this.eGhost, this.environment.getTheme());
this.eGhostIcon = this.eGhost.querySelector('.ag-dnd-ghost-icon');
this.setGhostIcon(null);
let eText = this.eGhost.querySelector('.ag-dnd-ghost-label');
eText.innerHTML = this.dragSource.dragItemName;
this.eGhost.style.height = '25px';
this.eGhost.style.top = '20px';
this.eGhost.style.left = '20px';
let usrDocument = this.gridOptionsWrapper.getDocument();
this.eGhostParent = usrDocument.querySelector('body');
if (!this.eGhostParent) {
console.warn('ag-Grid: could not find document body, it is needed for dragging columns');
} else {
this.eGhostParent.appendChild(this.eGhost);
}
}
public setGhostIcon(iconName: string, shake = false): void {
_.removeAllChildren(this.eGhostIcon);
let eIcon: HTMLElement;
switch (iconName) {
case DragAndDropService.ICON_ADD: eIcon = this.ePlusIcon; break;
case DragAndDropService.ICON_PINNED: eIcon = this.ePinnedIcon; break;
case DragAndDropService.ICON_MOVE: eIcon = this.eMoveIcon; break;
case DragAndDropService.ICON_LEFT: eIcon = this.eLeftIcon; break;
case DragAndDropService.ICON_RIGHT: eIcon = this.eRightIcon; break;
case DragAndDropService.ICON_GROUP: eIcon = this.eGroupIcon; break;
case DragAndDropService.ICON_AGGREGATE: eIcon = this.eAggregateIcon; break;
case DragAndDropService.ICON_PIVOT: eIcon = this.ePivotIcon; break;
case DragAndDropService.ICON_NOT_ALLOWED: eIcon = this.eDropNotAllowedIcon; break;
default: eIcon = this.eHiddenIcon; break;
}
this.eGhostIcon.appendChild(eIcon);
_.addOrRemoveCssClass(this.eGhostIcon, 'ag-shake-left-to-right', shake);
}
}