File

src/app/directives/ngdraggable/ngdraggable.directive.ts

Implements

OnInit OnDestroy OnChanges AfterViewInit

Metadata

Selector [ngDraggable]

Index

Methods
Inputs
Outputs
HostListeners
Accessors

Constructor

constructor(el: ElementRef, renderer: Renderer2)
Parameters :
Name Type Optional
el ElementRef No
renderer Renderer2 No

Inputs

bounds

Set the bounds HTMLElement

Type : HTMLElement

gridSize

Round the position to nearest grid

Default value : 1

handle

Make the handle HTMLElement draggable

Type : HTMLElement

inBounds

Whether to limit the element stay in the bounds

Default value : false

lockAxis

Lock axis: 'x' or 'y'

Type : string

Default value : null

ngDraggable

Type : any

outOfBounds

List of allowed out of bounds edges *

Default value : { top: false, right: false, bottom: false, left: false }

position

Set initial position by offsets

Type : IPosition

Default value : { x: 0, y: 0 }

preventDefaultEvent

Whether to prevent default event

Default value : false

scale

Input css scale transform of element so translations are correct

Default value : 1

trackPosition

Whether the element should use it's previous drag position on a new drag event.

Default value : true

zIndex

Set z-index when not dragging

Type : string

zIndexMoving

Set z-index when dragging

Type : string

Outputs

edge $event Type: EventEmitter
endOffset

Emit position offsets when put back

$event Type: EventEmitter
movingOffset

Emit position offsets when moving

$event Type: EventEmitter
started $event Type: EventEmitter
stopped $event Type: EventEmitter

HostListeners

mousedown
Arguments : '$event'
mousedown(event: MouseEvent | TouchEvent)

Methods

boundsCheck
boundsCheck()
Returns : { top: boolean; right: boolean; bottom: boolean; left: boolean; }
checkHandleTarget
checkHandleTarget(target: EventTarget, element: Element)
Parameters :
Name Type Optional
target EventTarget No
element Element No
Returns : boolean
getCurrentOffset
getCurrentOffset()

Get current offset

Returns : any
onMouseMove
onMouseMove(event: MouseEvent | TouchEvent)
Parameters :
Name Type Optional
event MouseEvent | TouchEvent No
Returns : void
resetPosition
resetPosition()
Returns : void

Accessors

zIndex
setzIndex(setting: string)

Set z-index when not dragging

Parameters :
Name Type Optional
setting string No
Returns : void
ngDraggable
setngDraggable(setting: any)
Parameters :
Name Type Optional
setting any No
Returns : void
import {
  Directive,
  ElementRef,
  Renderer2,
  Input,
  Output,
  OnInit,
  HostListener,
  EventEmitter,
  OnChanges,
  SimpleChanges,
  OnDestroy,
  AfterViewInit
} from '@angular/core';

import { Subscription, fromEvent } from 'rxjs';
import { IPosition, Position } from './position';
import { HelperBlock } from './helper-block';

@Directive({
  selector: '[ngDraggable]',
  exportAs: 'ngDraggable'
})
export class AngularDraggableDirective implements OnInit, OnDestroy, OnChanges, AfterViewInit {
  private allowDrag = true;
  private moving = false;
  private orignal: Position = null;
  private oldTrans = new Position(0, 0);
  private tempTrans = new Position(0, 0);
  private currTrans = new Position(0, 0);
  private oldZIndex = '';
  private _zIndex = '';
  private needTransform = false;

  private draggingSub: Subscription = null;

  /**
   * Bugfix: iFrames, and context unrelated elements block all events, and are unusable
   * https://github.com/xieziyu/angular2-draggable/issues/84
   */
  private _helperBlock: HelperBlock = null;

  /**
   * Flag to indicate whether the element is dragged once after being initialised
   */
  private isDragged = false;

  @Output()
  started = new EventEmitter<any>();
  @Output()
  stopped = new EventEmitter<any>();
  @Output()
  edge = new EventEmitter<any>();

  /** Make the handle HTMLElement draggable */
  @Input()
  handle: HTMLElement;

  /** Set the bounds HTMLElement */
  @Input()
  bounds: HTMLElement;

  /** List of allowed out of bounds edges **/
  @Input()
  outOfBounds = {
    top: false,
    right: false,
    bottom: false,
    left: false
  };

  /** Round the position to nearest grid */
  @Input()
  gridSize = 1;

  /** Set z-index when dragging */
  @Input()
  zIndexMoving: string;

  /** Set z-index when not dragging */
  @Input()
  set zIndex(setting: string) {
    this.renderer.setStyle(this.el.nativeElement, 'z-index', setting);
    this._zIndex = setting;
  }
  /** Whether to limit the element stay in the bounds */
  @Input()
  inBounds = false;

  /** Whether the element should use it's previous drag position on a new drag event. */
  @Input()
  trackPosition = true;

  /** Input css scale transform of element so translations are correct */
  @Input()
  scale = 1;

  /** Whether to prevent default event */
  @Input()
  preventDefaultEvent = false;

  /** Set initial position by offsets */
  @Input()
  position: IPosition = { x: 0, y: 0 };

  /** Lock axis: 'x' or 'y' */
  @Input()
  lockAxis: string = null;

  /** Emit position offsets when moving */
  @Output()
  movingOffset = new EventEmitter<IPosition>();

  /** Emit position offsets when put back */
  @Output()
  endOffset = new EventEmitter<IPosition>();

  @Input()
  set ngDraggable(setting: any) {
    if (setting !== undefined && setting !== null && setting !== '') {
      this.allowDrag = !!setting;

      let element = this.getDragEl();

      if (this.allowDrag) {
        this.renderer.addClass(element, 'ng-draggable');
      } else {
        this.putBack();
        this.renderer.removeClass(element, 'ng-draggable');
      }
    }
  }

  constructor(private el: ElementRef, private renderer: Renderer2) {
    this._helperBlock = new HelperBlock(el.nativeElement, renderer);
  }

  ngOnInit() {
    if (this.allowDrag) {
      let element = this.getDragEl();
      this.renderer.addClass(element, 'ng-draggable');
    }
    this.resetPosition();
  }

  ngOnDestroy() {
    this.bounds = null;
    this.handle = null;
    this.orignal = null;
    this.oldTrans = null;
    this.tempTrans = null;
    this.currTrans = null;
    this._helperBlock.dispose();
    this._helperBlock = null;

    if (this.draggingSub) {
      this.draggingSub.unsubscribe();
    }
  }

  ngOnChanges(changes: SimpleChanges) {
    if (changes['position'] && !changes['position'].isFirstChange()) {
      let p = changes['position'].currentValue;

      if (!this.moving) {
        if (Position.isIPosition(p)) {
          this.oldTrans.set(p);
        } else {
          this.oldTrans.reset();
        }

        this.transform();
      } else {
        this.needTransform = true;
      }
    }

    if (this.isDragged && changes['scale'] && !changes['scale'].isFirstChange()) {
      this.oldTrans.x = this.currTrans.x * this.scale;
      this.oldTrans.y = this.currTrans.y * this.scale;
    }
  }

  ngAfterViewInit() {
    if (this.inBounds) {
      this.boundsCheck();
      this.oldTrans.add(this.tempTrans);
      this.tempTrans.reset();
    }
  }

  private getDragEl() {
    return this.handle ? this.handle : this.el.nativeElement;
  }

  resetPosition() {
    if (Position.isIPosition(this.position)) {
      this.oldTrans.set(this.position);
    } else {
      this.oldTrans.reset();
    }
    this.tempTrans.reset();
    this.transform();
  }

  private moveTo(p: Position) {
    if (this.orignal) {
      p.subtract(this.orignal);
      this.tempTrans.set(p);
      this.transform();

      if (this.bounds) {
        this.edge.emit(this.boundsCheck());
      }

      this.movingOffset.emit(this.currTrans.value);
    }
  }

  private transform() {
    let translateX = this.tempTrans.x + this.oldTrans.x + this.position.x;
    let translateY = this.tempTrans.y + this.oldTrans.y + this.position.y;

    if (this.lockAxis === 'x') {
      translateX = this.oldTrans.x;
      this.tempTrans.x = 0;
    } else if (this.lockAxis === 'y') {
      translateY = this.oldTrans.y;
      this.tempTrans.y = 0;
    }

    // Snap to grid: by grid size
    if (this.gridSize > 1) {
      translateX = Math.round(translateX / this.gridSize) * this.gridSize;
      translateY = Math.round(translateY / this.gridSize) * this.gridSize;
    }

    // done to prevent the element from bouncing off when
    // the parent element is scaled and element is dragged for first time
    if (this.tempTrans.x !== 0 || this.tempTrans.y !== 0) {
      if (this.isDragged === false) {
        this.oldTrans.x = this.currTrans.x * this.scale;
        this.oldTrans.y = this.currTrans.y * this.scale;
      }
      this.isDragged = true;
    }

    if (this.scale && this.scale !== 0 && this.isDragged) {
      translateX = translateX / this.scale;
      translateY = translateY / this.scale;
    }

    let value = `translate(${translateX}px, ${translateY}px)`;

    this.renderer.setStyle(this.el.nativeElement, 'transform', value);
    this.renderer.setStyle(this.el.nativeElement, '-webkit-transform', value);
    this.renderer.setStyle(this.el.nativeElement, '-ms-transform', value);
    this.renderer.setStyle(this.el.nativeElement, '-moz-transform', value);
    this.renderer.setStyle(this.el.nativeElement, '-o-transform', value);

    // save current position
    this.currTrans.x = translateX;
    this.currTrans.y = translateY;
  }

  private pickUp() {
    // get old z-index:
    this.oldZIndex = this.el.nativeElement.style.zIndex ? this.el.nativeElement.style.zIndex : '';

    if (window) {
      this.oldZIndex = window.getComputedStyle(this.el.nativeElement, null).getPropertyValue('z-index');
    }

    if (this.zIndexMoving) {
      this.renderer.setStyle(this.el.nativeElement, 'z-index', this.zIndexMoving);
    }

    if (!this.moving) {
      this.started.emit(this.el.nativeElement);
      this.moving = true;

      /**
       * Fix performance issue:
       * https://github.com/xieziyu/angular2-draggable/issues/112
       */
      this.subscribeEvents();
    }
  }

  private subscribeEvents() {
    this.draggingSub = fromEvent(document, 'mousemove', { passive: false }).subscribe(event =>
      this.onMouseMove(event as MouseEvent)
    );
    this.draggingSub.add(
      fromEvent(document, 'touchmove', { passive: false }).subscribe(event => this.onMouseMove(event as TouchEvent))
    );
    this.draggingSub.add(fromEvent(document, 'mouseup', { passive: false }).subscribe(() => this.putBack()));
    this.draggingSub.add(fromEvent(document, 'mouseleave', { passive: false }).subscribe(() => this.putBack()));
    this.draggingSub.add(fromEvent(document, 'touchend', { passive: false }).subscribe(() => this.putBack()));
    this.draggingSub.add(fromEvent(document, 'touchcancel', { passive: false }).subscribe(() => this.putBack()));
  }

  private unsubscribeEvents() {
    this.draggingSub.unsubscribe();
    this.draggingSub = null;
  }

  boundsCheck() {
    if (this.bounds) {
      let boundary = this.bounds.getBoundingClientRect();
      let elem = this.el.nativeElement.getBoundingClientRect();
      let result = {
        top: this.outOfBounds.top ? true : boundary.top < elem.top,
        right: this.outOfBounds.right ? true : boundary.right > elem.right,
        bottom: this.outOfBounds.bottom ? true : boundary.bottom > elem.bottom,
        left: this.outOfBounds.left ? true : boundary.left < elem.left
      };

      if (this.inBounds) {
        if (!result.top) {
          this.tempTrans.y -= elem.top - boundary.top;
        }

        if (!result.bottom) {
          this.tempTrans.y -= elem.bottom - boundary.bottom;
        }

        if (!result.right) {
          this.tempTrans.x -= elem.right - boundary.right;
        }

        if (!result.left) {
          this.tempTrans.x -= elem.left - boundary.left;
        }

        this.transform();
      }

      return result;
    }
  }

  /** Get current offset */
  getCurrentOffset() {
    return this.currTrans.value;
  }

  private putBack() {
    if (this._zIndex) {
      this.renderer.setStyle(this.el.nativeElement, 'z-index', this._zIndex);
    } else if (this.zIndexMoving) {
      if (this.oldZIndex) {
        this.renderer.setStyle(this.el.nativeElement, 'z-index', this.oldZIndex);
      } else {
        this.el.nativeElement.style.removeProperty('z-index');
      }
    }

    if (this.moving) {
      this.stopped.emit(this.el.nativeElement);

      // Remove the helper div:
      this._helperBlock.remove();

      if (this.needTransform) {
        if (Position.isIPosition(this.position)) {
          this.oldTrans.set(this.position);
        } else {
          this.oldTrans.reset();
        }

        this.transform();
        this.needTransform = false;
      }

      if (this.bounds) {
        this.edge.emit(this.boundsCheck());
      }

      this.moving = false;
      this.endOffset.emit(this.currTrans.value);

      if (this.trackPosition) {
        this.oldTrans.add(this.tempTrans);
      }

      this.tempTrans.reset();

      if (!this.trackPosition) {
        this.transform();
      }

      /**
       * Fix performance issue:
       * https://github.com/xieziyu/angular2-draggable/issues/112
       */
      this.unsubscribeEvents();
    }
  }

  checkHandleTarget(target: EventTarget, element: Element) {
    // Checks if the target is the element clicked, then checks each child element of element as well
    // Ignores button clicks

    // Ignore elements of type button
    if (element.tagName === 'BUTTON') {
      return false;
    }

    // If the target was found, return true (handle was found)
    if (element === target) {
      return true;
    }

    // Recursively iterate this elements children
    for (let child in element.children) {
      if (element.children.hasOwnProperty(child)) {
        if (this.checkHandleTarget(target, element.children[child])) {
          return true;
        }
      }
    }

    // Handle was not found in this lineage
    // Note: return false is ignore unless it is the parent element
    return false;
  }

  @HostListener('mousedown', ['$event'])
  @HostListener('touchstart', ['$event'])
  onMouseDown(event: MouseEvent | TouchEvent) {
    // 1. skip right click;
    if (event instanceof MouseEvent && event.button === 2) {
      return;
    }
    // 2. if handle is set, the element can only be moved by handle
    let target = event.target || event.srcElement;
    if (this.handle !== undefined && !this.checkHandleTarget(target, this.handle)) {
      return;
    }

    // 3. if allow drag is set to false, ignore the mousedown
    if (this.allowDrag === false) {
      return;
    }

    if (this.preventDefaultEvent) {
      event.stopPropagation();
      event.preventDefault();
    }

    this.orignal = Position.fromEvent(event, this.getDragEl());
    this.pickUp();
  }

  onMouseMove(event: MouseEvent | TouchEvent) {
    if (this.moving && this.allowDrag) {
      if (this.preventDefaultEvent) {
        event.stopPropagation();
        event.preventDefault();
      }

      // Add a transparent helper div:
      this._helperBlock.add();
      this.moveTo(Position.fromEvent(event, this.getDragEl()));
    }
  }
}

result-matching ""

    No results matching ""