src/app/directives/ngdraggable/ngdraggable.directive.ts
OnInit
OnDestroy
OnChanges
AfterViewInit
Selector | [ngDraggable] |
Methods |
Inputs |
Outputs |
HostListeners |
Accessors |
constructor(el: ElementRef, renderer: Renderer2)
|
|||||||||
Parameters :
|
bounds
|
Set the bounds HTMLElement
Type : |
gridSize
|
Round the position to nearest grid
Default value : |
handle
|
Make the handle HTMLElement draggable
Type : |
inBounds
|
Whether to limit the element stay in the bounds
Default value : |
lockAxis
|
Lock axis: 'x' or 'y'
Type :
Default value : |
ngDraggable
|
Type : |
outOfBounds
|
List of allowed out of bounds edges *
Default value : |
position
|
Set initial position by offsets
Type :
Default value : |
preventDefaultEvent
|
Whether to prevent default event
Default value : |
scale
|
Input css scale transform of element so translations are correct
Default value : |
trackPosition
|
Whether the element should use it's previous drag position on a new drag event.
Default value : |
zIndex
|
Set z-index when not dragging
Type : |
zIndexMoving
|
Set z-index when dragging
Type : |
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
|
mousedown |
Arguments : '$event'
|
mousedown(event: MouseEvent | TouchEvent)
|
boundsCheck |
boundsCheck()
|
Returns :
{ top: boolean; right: boolean; bottom: boolean; left: boolean; }
|
checkHandleTarget | |||||||||
checkHandleTarget(target: EventTarget, element: Element)
|
|||||||||
Parameters :
Returns :
boolean
|
getCurrentOffset |
getCurrentOffset()
|
Get current offset
Returns :
any
|
onMouseMove | ||||||
onMouseMove(event: MouseEvent | TouchEvent)
|
||||||
Parameters :
Returns :
void
|
resetPosition |
resetPosition()
|
Returns :
void
|
zIndex | ||||||
setzIndex(setting: string)
|
||||||
Set z-index when not dragging
Parameters :
Returns :
void
|
ngDraggable | ||||||
setngDraggable(setting: any)
|
||||||
Parameters :
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()));
}
}
}