ag-grid
Version:
Advanced Data Grid / Data Table supporting Javascript / React / AngularJS / Web Components
319 lines (252 loc) • 12.7 kB
text/typescript
import {Bean, PreDestroy, Autowired, PostConstruct, Optional} from "../context/context";
import {LoggerFactory, Logger} from "../logger";
import {Utils as _} from "../utils";
import {EventService} from "../eventService";
import {DragStartedEvent, DragStoppedEvent, Events} from "../events";
import {GridOptionsWrapper} from "../gridOptionsWrapper";
import {ColumnApi} from "../columnController/columnApi";
import {GridApi} from "../gridApi";
/** Adds drag listening onto an element. In ag-Grid this is used twice, first is resizing columns,
* second is moving the columns and column groups around (ie the 'drag' part of Drag and Drop. */
export class DragService {
private loggerFactory: LoggerFactory;
private eventService: EventService;
private gridOptionsWrapper: GridOptionsWrapper;
private columnApi: ColumnApi;
private gridApi: GridApi;
private currentDragParams: DragListenerParams;
private dragging: boolean;
private mouseEventLastTime: MouseEvent;
private mouseStartEvent: MouseEvent;
private touchLastTime: Touch;
private touchStart: Touch;
private onMouseUpListener = this.onMouseUp.bind(this);
private onMouseMoveListener = this.onMouseMove.bind(this);
private onTouchEndListener = this.onTouchUp.bind(this);
private onTouchMoveListener = this.onTouchMove.bind(this);
private logger: Logger;
private dragEndFunctions: Function[] = [];
private dragSources: DragSourceAndListener[] = [];
private init(): void {
this.logger = this.loggerFactory.create('DragService');
}
private destroy(): void {
this.dragSources.forEach( this.removeListener.bind(this) );
this.dragSources.length = 0;
}
private removeListener(dragSourceAndListener: DragSourceAndListener): void {
let element = dragSourceAndListener.dragSource.eElement;
let mouseDownListener = dragSourceAndListener.mouseDownListener;
element.removeEventListener('mousedown', mouseDownListener);
// remove touch listener only if it exists
if (dragSourceAndListener.touchEnabled) {
let touchStartListener = dragSourceAndListener.touchStartListener;
element.removeEventListener('touchstart', touchStartListener, <any>{passive:true});
}
}
public removeDragSource(params: DragListenerParams): void {
let dragSourceAndListener = _.find( this.dragSources, item => item.dragSource === params);
if (!dragSourceAndListener) { return; }
this.removeListener(dragSourceAndListener);
_.removeFromArray(this.dragSources, dragSourceAndListener);
}
private setNoSelectToBody(noSelect: boolean): void {
let usrDocument = this.gridOptionsWrapper.getDocument();
let eBody = <HTMLElement> usrDocument.querySelector('body');
if (_.exists(eBody)) {
_.addOrRemoveCssClass(eBody, 'ag-body-no-select', noSelect);
}
}
public addDragSource(params: DragListenerParams, includeTouch: boolean = false): void {
let mouseListener = this.onMouseDown.bind(this, params);
params.eElement.addEventListener('mousedown', mouseListener);
let touchListener: (touchEvent: TouchEvent)=>void = null;
let suppressTouch = this.gridOptionsWrapper.isSuppressTouch();
let reallyIncludeTouch = includeTouch && !suppressTouch;
if (reallyIncludeTouch) {
touchListener = this.onTouchStart.bind(this, params);
params.eElement.addEventListener('touchstart', touchListener, <any>{passive:false});
}
this.dragSources.push({
dragSource: params,
mouseDownListener: mouseListener,
touchStartListener: touchListener,
touchEnabled: includeTouch
});
}
// gets called whenever mouse down on any drag source
private onTouchStart(params: DragListenerParams, touchEvent: TouchEvent): void {
this.currentDragParams = params;
this.dragging = false;
let touch = touchEvent.touches[0];
this.touchLastTime = touch;
this.touchStart = touch;
touchEvent.preventDefault();
// we temporally add these listeners, for the duration of the drag, they
// are removed in touch end handling.
params.eElement.addEventListener('touchmove', this.onTouchMoveListener, <any>{passive:true});
params.eElement.addEventListener('touchend', this.onTouchEndListener, <any>{passive:true});
params.eElement.addEventListener('touchcancel', this.onTouchEndListener, <any>{passive:true});
this.dragEndFunctions.push( ()=> {
params.eElement.removeEventListener('touchmove', this.onTouchMoveListener, <any>{passive:true});
params.eElement.removeEventListener('touchend', this.onTouchEndListener, <any>{passive:true});
params.eElement.removeEventListener('touchcancel', this.onTouchEndListener, <any>{passive:true});
});
// see if we want to start dragging straight away
if (params.dragStartPixels===0) {
this.onCommonMove(touch, this.touchStart);
}
}
// gets called whenever mouse down on any drag source
private onMouseDown(params: DragListenerParams, mouseEvent: MouseEvent): void {
// we ignore when shift key is pressed. this is for the range selection, as when
// user shift-clicks a cell, this should not be interpreted as the start of a drag.
// if (mouseEvent.shiftKey) { return; }
if (params.skipMouseEvent) {
if (params.skipMouseEvent(mouseEvent)) { return; }
}
// if there are two elements with parent / child relationship, and both are draggable,
// when we drag the child, we should NOT drag the parent. an example of this is row moving
// and range selection - row moving should get preference when use drags the rowDrag component.
if ((<any>mouseEvent)._alreadyProcessedByDragService) { return; }
(<any>mouseEvent)._alreadyProcessedByDragService = true;
// only interested in left button clicks
if (mouseEvent.button!==0) { return; }
this.currentDragParams = params;
this.dragging = false;
this.mouseEventLastTime = mouseEvent;
this.mouseStartEvent = mouseEvent;
let usrDocument = this.gridOptionsWrapper.getDocument();
// we temporally add these listeners, for the duration of the drag, they
// are removed in mouseup handling.
usrDocument.addEventListener('mousemove', this.onMouseMoveListener);
usrDocument.addEventListener('mouseup', this.onMouseUpListener);
this.dragEndFunctions.push( ()=> {
usrDocument.removeEventListener('mousemove', this.onMouseMoveListener);
usrDocument.removeEventListener('mouseup', this.onMouseUpListener);
});
// see if we want to start dragging straight away
if (params.dragStartPixels===0) {
this.onMouseMove(mouseEvent);
}
}
// returns true if the event is close to the original event by X pixels either vertically or horizontally.
// we only start dragging after X pixels so this allows us to know if we should start dragging yet.
private isEventNearStartEvent(currentEvent: MouseEvent|Touch, startEvent: MouseEvent|Touch): boolean {
// by default, we wait 4 pixels before starting the drag
let requiredPixelDiff = _.exists(this.currentDragParams.dragStartPixels) ? this.currentDragParams.dragStartPixels : 4;
return _.areEventsNear(currentEvent, startEvent, requiredPixelDiff);
}
private getFirstActiveTouch(touchList: TouchList): Touch {
for (let i = 0; i<touchList.length; i++) {
let matches = touchList[i].identifier === this.touchStart.identifier;
if (matches) {
return touchList[i];
}
}
return null;
}
private onCommonMove(currentEvent: MouseEvent|Touch, startEvent: MouseEvent|Touch): void {
if (!this.dragging) {
// if mouse hasn't travelled from the start position enough, do nothing
let toEarlyToDrag = !this.dragging && this.isEventNearStartEvent(currentEvent, startEvent);
if (toEarlyToDrag) {
return;
} else {
// alert(`started`);
this.dragging = true;
let event: DragStartedEvent = {
type: Events.EVENT_DRAG_STARTED,
api: this.gridApi,
columnApi: this.columnApi
};
this.eventService.dispatchEvent(event);
this.currentDragParams.onDragStart(startEvent);
this.setNoSelectToBody(true);
}
}
this.currentDragParams.onDragging(currentEvent);
}
private onTouchMove(touchEvent: TouchEvent): void {
let touch = this.getFirstActiveTouch(touchEvent.touches);
if (!touch) { return; }
// this.___statusBar.setInfoText(Math.random() + ' onTouchMove preventDefault stopPropagation');
// if we don't preview default, then the browser will try and do it's own touch stuff,
// like do 'back button' (chrome does this) or scroll the page (eg drag column could be confused
// with scroll page in the app)
// touchEvent.preventDefault();
this.onCommonMove(touch, this.touchStart);
}
// only gets called after a mouse down - as this is only added after mouseDown
// and is removed when mouseUp happens
private onMouseMove(mouseEvent: MouseEvent): void {
this.onCommonMove(mouseEvent, this.mouseStartEvent);
}
public onTouchUp(touchEvent: TouchEvent): void {
let touch = this.getFirstActiveTouch(touchEvent.changedTouches);
// i haven't worked this out yet, but there is no matching touch
// when we get the touch up event. to get around this, we swap in
// the last touch. this is a hack to 'get it working' while we
// figure out what's going on, why we are not getting a touch in
// current event.
if (!touch) {
touch = this.touchLastTime;
}
// if mouse was left up before we started to move, then this is a tap.
// we check this before onUpCommon as onUpCommon resets the dragging
// let tap = !this.dragging;
// let tapTarget = this.currentDragParams.eElement;
this.onUpCommon(touch);
// if tap, tell user
// console.log(`${Math.random()} tap = ${tap}`);
// if (tap) {
// tapTarget.click();
// }
}
public onMouseUp(mouseEvent: MouseEvent): void {
this.onUpCommon(mouseEvent);
}
public onUpCommon(eventOrTouch: MouseEvent|Touch): void {
if (this.dragging) {
this.dragging = false;
this.currentDragParams.onDragStop(eventOrTouch);
let event: DragStoppedEvent = {
type: Events.EVENT_DRAG_STOPPED,
api: this.gridApi,
columnApi: this.columnApi
};
this.eventService.dispatchEvent(event);
}
this.setNoSelectToBody(false);
this.mouseStartEvent = null;
this.mouseEventLastTime = null;
this.touchStart = null;
this.touchLastTime = null;
this.currentDragParams = null;
this.dragEndFunctions.forEach( func => func() );
this.dragEndFunctions.length = 0;
}
}
interface DragSourceAndListener {
dragSource: DragListenerParams;
mouseDownListener: (mouseEvent: MouseEvent)=>void;
touchEnabled: boolean;
touchStartListener: (touchEvent: TouchEvent)=>void;
}
export interface DragListenerParams {
/** After how many pixels of dragging should the drag operation start. Default is 4px. */
dragStartPixels?: number;
/** Dom element to add the drag handling to */
eElement: HTMLElement;
/** Some places may wish to ignore certain events, eg range selection ignores shift clicks */
skipMouseEvent?: (mouseEvent: MouseEvent) => boolean;
/** Callback for drag starting */
onDragStart: (mouseEvent: MouseEvent|Touch) => void;
/** Callback for drag stopping */
onDragStop: (mouseEvent: MouseEvent|Touch) => void;
/** Callback for mouse move while dragging */
onDragging: (mouseEvent: MouseEvent|Touch) => void;
}