Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Making a ngb draggable modal [closed]

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.

like image 499
methgaard Avatar asked Aug 23 '17 08:08

methgaard


2 Answers

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)';
   }
 }
like image 162
diopside Avatar answered Nov 14 '22 20:11

diopside


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

like image 30
Brian Smith Avatar answered Nov 14 '22 20:11

Brian Smith