I have an Angular application in which i use ng-bootstrap to display a modal.
My issue is that the ngb team doesn't support dragging these modals and apparently has no plans of doing so any time soon.
So my question is: Can anyone tell me how i can make such a modal draggable?
Thanks in advance.
Here is the one I have written - there's a lot of unnecessary stuff that's there just to squeak out every bit of performance as possible while also following angular best practices - i'll follow it with a trimmed down / simplified version.
They both only need the directive alone, no css or html changes / queries other than adding the directive selector to the element you want it on. They both use translate3d rather than changing top and left position, which can trigger GPU acceleration on certain browsers, but even without that it usually just performs smoother than changing position anyway. Its what the translate transform was made for - moving an element relative to iself. They both use HostBinding to bind to the property, rather than directly accessing a nativeElement attribute (which unnecessarily couples the directive to the DOM). The second one is nice bc it doesn't require dependencies for ElementRef or Renderer2, but it adds an always-on listener to the document object so i rarely use it, even though its cleaner looking.
I use the first one most because it only adds the mousemove listener when the modal is clicked, and removes it when its no longer needed. Additionally, it runs all of the movement functions outside of angular so dragging the modal won't constantly be triggering angular's change detection for no reason (nothing inside the modal box will be changing while its being dragged, i suspect, so its unnecessary to check). And then, since most of my modals are created dynamically and destroyed when they are closed, they can also remove event listeners in that case. I inject elementref so I can get a reference to the directive's parent element which requires accessing nativeElement, but I'm not actually modifying the values, just reading them once to get a reference. So I think its forgiveable in Angular doctrine :p
import { Directive,
Input,
NgZone,
Renderer2,
HostListener,
HostBinding,
OnInit,
ElementRef } from '@angular/core';
class Position {
x: number; y: number;
constructor (x, y) { this.x = x; this.y = y; }
};
@Directive({
selector: '[lt-drag]'
})
export class LtDragDirective {
private allowDrag = true;
private moving = false;
private origin = null;
// for adding / detaching mouse listeners dynamically so they're not *always* listening
private moveFunc: Function;
private clickFunc: Function;
constructor(private el:ElementRef,
private zone: NgZone,
private rend: Renderer2 ) {}
@Input('handle') handle: HTMLElement;
@HostBinding('style.transform') transform: string = 'translate3d(0,0,0)';
ngOnInit() {
let host = this.el.nativeElement.offsetParent;
// applies mousemove and mouseup listeners to the parent component, typically my app componennt window, I prefer doing it like this so I'm not binding to a window or document object
this.clickFunc = this.rend.listen(host, 'mouseup' , ()=>{
this.moving = false;
});
// uses ngzone to run moving outside angular for better performance
this.moveFunc = this.rend.listen(host, 'mousemove' ,($event)=>{
if (this.moving && this.allowDrag) {
this.zone.runOutsideAngular(()=>{
event.preventDefault();
this.moveTo($event.clientX, $event.clientY);
});
}
});
}
// detach listeners if host element is removed from DOM
ngOnDestroy() {
if (this.clickFunc ) { this.clickFunc(); }
if (this.moveFunc ) { this.moveFunc(); }
}
// parses css translate string for exact px position
private getPosition(x:number, y:number) : Position {
let transVal:string[] = this.transform.split(',');
let newX = parseInt(transVal[0].replace('translate3d(',''));
let newY = parseInt(transVal[1]);
return new Position(x - newX, y - newY);
}
private moveTo(x:number, y:number) : void {
if (this.origin) {
this.transform = this.getTranslate( (x - this.origin.x), (y - this.origin.y) );
}
}
private getTranslate(x:number,y:number) : string{
return 'translate3d('+x+'px,'+y+'px,0px)';
}
@HostListener('mousedown',['$event'])
onMouseDown(event: MouseEvent) {
if (event.button == 2 || (this.handle !== undefined && event.target !==
this.handle)) {
return;
}
else {
this.moving = true;
this.origin = this.getPosition(event.clientX, event.clientY);
}
}
}
simpler version below -- if you're not concerned with keeping event listeners open or binding to the document object
import { Directive,
Input,
HostListener,
HostBinding,
OnInit } from '@angular/core';
class Position {
x: number; y: number;
constructor (x, y) { this.x = x; this.y = y; }
};
@Directive({
selector: '[lt-drag]'
})
export class LtDragDirective {
private moving = false;
private origin = null;
constructor( ) {}
@Input('handle') handle: HTMLElement;
@HostBinding('style.transform') transform: string = 'translate3d(0,0,0)';
@HostListener('document:mousemove',[$event]) mousemove($event:MouseEvent) {
event.preventDefault();
this.moveTo($event.clientX, $event.clientY);
}
@HostListener('document:mouseup') mouseup() {
this.moving = false;
}
@HostListener('mousedown',['$event'])
onMouseDown(event: MouseEvent) {
if (event.button == 2 || (this.handle !== undefined && event.target !== this.handle)) {
return; // if handle was provided and not clicked, ignore
}
else {
this.moving = true;
this.origin = this.getPosition(event.clientX, event.clientY);
}
}
private getPosition(x:number, y:number) : Position {
let transVal:string[] = this.transform.split(',');
let newX = parseInt(transVal[0].replace('translate3d(',''));
let newY = parseInt(transVal[1]);
return new Position(x - newX, y - newY);
}
private moveTo(x:number, y:number) : void {
if (this.origin) {
this.transform = this.getTranslate( (x - this.origin.x), (y -
this.origin.y) );
}
}
private getTranslate(x:number,y:number) : string{
return 'translate3d('+x+'px,'+y+'px,0px)';
}
}
You can use the NPM package jquery and jqueryui
npm install --save jquery jqueryui
Then it is as simple as doing the following.
Add imports:
import * as $ from 'jquery';
import 'jqueryui';
OR use
declare var $: any;
Add the following to your ngOnInit() method:
$(document).ready(function(){
let modalContent: any = $('.modal-content');
let modalHeader = $('.modal-header');
modalHeader.addClass('cursor-all-scroll');
modalContent.draggable({
handle: '.modal-header'
});
});
**Note, I have a class for changing the cursor and I only want the header of the modal to be the handle for dragging around.
DEMO
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With