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;
}