Search code examples
angularbootstrap-4bootstrap-modalangular8ng-bootstrap

ngBootstrap Angular 8 - Modal Resizable and Draggable


I am trying to use the bootstrap https://ng-bootstrap.github.io/#/components/modal/examples version 4 with Angular 8. I want to make modal re-sizable and draggable. I see a few examples for other versions like 3.xx but not with angular - http://jsfiddle.net/GDVdN/

Any references for Bootstrap4 with ANgular 8 - Modal resizable + draggable?.


Solution

  • The most closer I get is create a component that makes "resizable" any element inside.

    Update: "play" with styles of "modal-dialog"

    Imagine a component like

    <div class="resizable" [ngStyle]="style">
      <ng-content></ng-content>
      <div class="cell-border-top"></div>
      <div class="cell-border-bottom"></div>
      <div class="cell-border-left"></div>
      <div class="cell-border-right"></div>
      <div class="cell-top-right"></div>
      <div class="cell-bottom-right"></div>
      <div class="cell-top-left"></div>
      <div class="cell-bottom-left"></div>
    </div>
    

    the .css make that the divs was a position in left, right, top, bottom and in the four corners

    we listen mouseDown, and "getting" the className we can store in a variable the type of drag

    export enum TypeDrag {
      Move,
      Top,
      Bottom,
      Left,
      Right,
      TopRight,
      BottomRight,
      TopLeft,
      BottomLeft
    }
    

    When mouse down we subscribe to mouseUp and to mouseMove, a mouseUp simple remove the subscription to mouseMove

    The mouseMove change the style of the div to change the position and dimensions

    We need indicate as Input an ElementRef to drag the "modal"

    There're and addicional consideration that is that the ngb-modal place the modal changing the "margin-left" and "margin-top", so I need style the margin to 0 to a div with calssName=="modal-dialog". For this, we create a recursive function

    To get the "modal-dialog" we use a recursive function

    findModalContent(element:HTMLElement)
    {
      return element.className=="modal-dialog"?element:
                element.parentElement?this.findModalContent(element.parentElement):
                null
    }
    

    I try to explain with comments in the code

    And as usually this is the stackblitz

    @Component({
      selector: 'angular-window',
      templateUrl: './angular-window.component.html',
      styleUrls: ['./angular-window.component.css']
    })
    export class AngularWindowComponent implements OnInit {
      rect: any;
      incr: number[] = [0, 0, 0, 0];
      nativeElement: any;
      typeDrag: TypeDrag;
      origin: any;
      onDrag: boolean = false;
      moveSubscription: any;
      //div: any;  <--remove in the updated
    
      classNames = [
        'cell-top',
        'cell-border-top',
        'cell-border-bottom',
        'cell-border-left',
        'cell-border-right',
        'cell-top-right',
        'cell-bottom-right',
        'cell-top-left',
        'cell-bottom-left'
      ];
    
      style: any = null;
      constructor(private elementRef: ElementRef) {}
    
      @Input() set dragHolder(value) { //the drag holder will be a 
                                       //template reference variable
                                       //we add the class "cell-top"
    
        value.classList.add("cell-top");
      }
    
      /*It's not necesary now
      //I need indicate the background-color
      @Input('background-color') backgroundColor = 'white';
      */
    
      ngOnInit(): void {
    
        //get the "modalContent"
            this.modalContent=this.findModalContent(this.elementRef.nativeElement)
    
        //we subscribe to mouseDown
        fromEvent(this.elementRef.nativeElement, 'mousedown')
          .pipe(
            //we filter, only get if the className of element 
            //is one of the indicate by the variable "classNames"
            //or if the className include the "cell-top"
    
            filter((event: MouseEvent) => {
              const classs = (event.target as any).className;
              if (classs && typeof classs === 'string') {
                const className = classs.split(' ');
                return className.indexOf("cell-top")>=0?true:
                  this.classNames.indexOf(classs) >= 0;
              }
              return false;
            })
          )
          .subscribe((event: MouseEvent) => {
    
            this.div = this.elementRef.nativeElement.childNodes[0];
            this.rect = this.div.getBoundingClientRect();
            this.origin = { x: event.screenX, y: event.screenY };
    
            this.onDrag = true;
    
            const className = (event.target as any).className.split(' ');
            this.typeDrag =className.indexOf('cell-top')>=0?TypeDrag.Move:
             (this.classNames.indexOf(className[0])) as TypeDrag;
    
            //acording the typeDrag, I store in "this.incr" the move
              
            this.incr =
              this.typeDrag == TypeDrag.Move
                ? [1, 0, 1, 0]
                : this.typeDrag == TypeDrag.Top
                ? [1, -1, 0, 0]
                : this.typeDrag == TypeDrag.Bottom
                ? [0, 1, 0, 0]
                : this.typeDrag == TypeDrag.Right
                ? [0, 0, 0, 1]
                : this.typeDrag == TypeDrag.Left
                ? [0, 0, 1, -1]
                : this.typeDrag == TypeDrag.TopRight
                ? [1, -1, 0, 1]
                : this.typeDrag == TypeDrag.TopLeft
                ? [1, -1, 1, -1]
                : this.typeDrag == TypeDrag.BottomRight
                ? [0, 1, 0, 1]
                : [0, 1, 1, -1];
    
            this.onDrag = true;
    
            /*Not necesary
            //remove the "margin" in modal-dialog
            const modalContent=this.findModalContent(this.div.parentElement)
            if (modalContent)
              modalContent.style.margin=0;
            */
    
            //we subscribe to mouseUp    
            fromEvent(document, 'mouseup')
              .pipe(take(1))
              .subscribe(() => {
                if (this.moveSubscription) {
                  this.moveSubscription.unsubscribe();
                  this.moveSubscription = undefined;
                  this.onDrag = false;
                }
              });
    
            //we subscribe to mouseMove
    
            if (!this.moveSubscription) {
              this.moveSubscription = fromEvent(document, 'mousemove').pipe(
                startWith({screenY:this.origin.y,screenX:this.origin.x})
              ).subscribe(
                (moveEvent: MouseEvent) => {
                  const incrTop = moveEvent.screenY - this.origin.y;
                  const incrLeft = moveEvent.screenX - this.origin.x;
                  const width = this.rect.width + this.incr[3] * incrLeft;
                  const heigth = this.rect.height + this.incr[1] * incrTop;
                  /*before
                  this.style = {
                    position: 'absolute',
                    'z-index': 1051,
                    'background-color': this.backgroundColor,
                    top: this.rect.top + this.incr[0] * incrTop + 'px',
                    height: (heigth < 75 ? 75 : heigth) + 'px',
                    left: this.rect.left + this.incr[2] * incrLeft + 'px',
                    width: (width < 50 ? 50 : width) + 'px'
                  };
                  */
                  //now:
                  this.modalContent.style['max-width']=
                            (width < 50 ? 50 : width) + 'px'
                  this.modalContent.style['margin-top']=
                            this.rect.top + this.incr[0] * incrTop + 'px'
                  this.modalContent.style['margin-left']=
                            this.rect.left + this.incr[2] * incrLeft + 'px'
                  this.style={
                     width:(width < 50 ? 50 : width-1) + 'px',
                     height:(heigth < 75 ? 75 : heigth-1) + 'px'
                  }
          });
      }
    }
    

    the use is simple,e.g. (see how indicate the "dragHolder")

    <ng-template #content let-modal>
      <angular-window [dragHolder]="header">
        <div class="modal-header">
          <h4 #header class="modal-title w-100" 
               id="modal-basic-title">Profile update</h4>
        </div>
        <div class="modal-body">
        </div>
        <div class="modal-footer">
        </div>
      </angular-window>
    </ng-template>
    

    NOTE: To change the cursor in dragHolder we need add the class

    .cell-top {
      cursor: move;
    }