How can i apply a draggable directive to bootstrap modal using angularJS?

All we need is an easy explanation of the problem, so here it is.

I am using bootstrap modal in my Angular Application, it works fine. I need to make it draggable and resizeable, so i have defined a directive. The issue now is it getting applied to the content inside the modal window, hence the modal window becomes transparent.

how can i assign the draggable directive to the modal window when opening the window?
Here is the code,

HTML:

<div ng-controller="CustomWidgetCtrl">
    <div class="box-header-btns pull-right" style="top:10px" >
        <a title="settings" ng-click="openSettings(widget)"><i class="glyphicon glyphicon-cog"></i></a>
</div>
</div>

App.js:

var routerApp = angular.module('DiginRt',  ['ui.bootstrap','ngRoute']);
routerApp.controller('CustomWidgetCtrl', ['$scope', '$modal',
  function($scope, $modal) {

    $scope.openSettings = function(widget) {
          $modal.open({
            scope: $scope,
            templateUrl: 'chart_settings.html',
            controller: 'chartSettingsCtrl',        
            resolve: {
              widget: function() {
                return widget;
              }
            }
          });
        };
    }
    ])

Chart Settings is another HTML page. Here is my Draggable directive.

UPDATE:

I have update the issue with Plunker

Issue:
enter image description here

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

I couldn’t find a way to add the directive to the modal opened by ui-bootstrap, as it wraps the template with a modal-dialog..

So what i did is setting the events for drag to the modal-dialog itself(not the directive) using the following code.

I know it is not the best practice to add events to another element inside a directive but not a bad practice as well in cases like these, where you cant set a directive directly to the element.

the reason doing this is because ui-bootstrap doesnt provide a way to add a directive to the modal-dialog on modal.open function

here is the code to be put at the start of the directive:

element= angular.element(document.getElementsByClassName("modal-dialog"));

and the plunkr

Method 2

I upvoted @Naeem_Shaikh’s answer which is basically an improvement on the standard angular directive.

However, there is another problem with the standard angular draggable directive that I have been able to fix.

The standard directive imposes draggability on the whole dialog. On the one hand, this is what we want. We want to drag the whole dialog. But this has unfortunate side effect that clicks in various edit fields in the dialog do not work: the default behavior is prevented! Somehow buttons have been coded in bootstrap to overcome this but not text edit fields. I’m guessing that the authors weren’t considering my use case. But dialogs are more than just buttons!

It’s tricky since it is the whole dialog that needs to be dragged, but you only want drags initiated by clicks in the header “hotspot”. In other words, the hotspot to initiate dragging is a subset of the area to be dragged.

Naeem’s fix enabled me to get it to work so that only clicks in the header initiated drags. Without his fix, the coordinate conversion was getting confused.

function clickedWithinHeader(event) {
    var target = event.currentTarget;
    var hotspot = null;
    var hotspots = target.getElementsByClassName("modal-header");
    if (hotspots.length > 0) {
        hotspot = hotspots.item(0);
    }
    if (hotspot !== null) {
        var eY = event.clientY;
        var tOT = target.offsetTop;
        var y = eY - tOT;
        var hH = hotspot.offsetHeight;
        // since the header occupies the full width across the top
        // no need to check X.  Note that this assumes the header
        // is on the top, which should be a safe assumption
        var within = (y <= hH);
        return within;
    } else {
        return true;
    }
}


// Draggable directive from: http://docs.angularjs.org/guide/compiler
// Modified so that only clicks in the dialog header trigger drag behavior.
// Otherwise clicking on dialog widgets did not give them focus.
angular.module('drag', []).directive('draggable', function($document) {
"use strict";
return function(scope, element) {
    var startX = 0, startY = 0, x = 0, y = 0;
    element= angular.element(document.getElementsByClassName("modal-dialog"));
    element.css({
        position : 'fixed',
        cursor : 'move',
    });
    element.on('mousedown', function(event) {
//      // OK, where did they touch?  Only want to do this
//      // when they clicked on the header.
        if (!clickedWithinHeader(event)) {
            return;
        }
        // Prevent default dragging of selected content
        event.preventDefault();
        ...

Note that the clickedWithinHeader() logic only works with Naeem’s improvement. There may be a better way to do it, but this works. Only clicks in the header initiate dragging and clicks elsewhere do what they’re supposed to do.

However, that wasn’t the whole answer since the standard directive also imposed the move cursor over the whole dialog which is very disconcerting, even if, or especially if you can’t drag where it appears.

Removing that from the element.css in the directive:

element.css({
    position : 'fixed',
});

and tying the move cursor only to the modal header using CSS

.modal-header 
{
    cursor: move;
}

provides a complete solution.

Method 3

I incorporated the various code fragments here and came up with a simpler solution. This one doesn’t rely on detecting if the click event happened in the modal-header, because the mousedown event is bound to the header specifically.

This solution also doesn’t rely on searching the entire DOM for the modal-dialog class, as it searches the parents for the modal-dialog element. This will allow you to have multiple dialogs on screen and only one be draggable.

I also created a gist that has checking to prevent the dialog from being moved outside the visible bounds.

(function (angular) {
    "use strict";

    angular.module('MyModule')
        .directive('modalDraggable', ['$document', modalDraggable]);

    function modalDraggable($document) {
        return function (scope, element) {
            var startX = 0,
                startY = 0,
                x = 0,
                y = 0;

            var draggable = angular.element(element.parents('.modal-dialog')[0]);

            draggable.find('.modal-header')
                .css('cursor', 'move')
                .on('mousedown', function (event) {
                    // Prevent default dragging of selected content
                    event.preventDefault();
                    startX = event.screenX - x;
                    startY = event.screenY - y;

                    $document.on('mousemove', mousemove);
                    $document.on('mouseup', mouseup);
                });

            function mousemove(event) {
                y = event.screenY - startY;
                x = event.screenX - startX;

                draggable.css({
                    top: y + 'px',
                    left: x + 'px'
                });
            }

            function mouseup() {
                $document.unbind('mousemove', mousemove);
                $document.unbind('mouseup', mouseup);
            }
        };
    }
}(window.angular));

Method 4

I change an existing solution to more than one modal on screen.

Html:

….

OR

… // modal header, body, footer.

JS call modal:
var modalInstance = $uibModal.open({
animation: true,
controller: ‘ModalAndamentoController’,
controllerAs: ‘vm’,
windowClass: ‘center-modal’,
size: ‘lg’,
templateUrl: ‘modalAndamento.html’
});

Directive JS:
(function () {
‘use strict’;

angular
.module(‘app’)
.directive(‘draggable’, draggableDirective);

/** @ngInject */
function draggableDirective($document) {

  //busca pelo elemento 
  var serachElement = function (parentElement, element) {
      //se o elemento pai corrente é igual ao elemento, então este foi encontrado
      if (parentElement == element[0]) {
          return true;
      }

      //aprofunda mais um nível na árvore procurando pelo elemento
      var i = 0;
      for (i = 0; i < parentElement.childNodes.length; i++) {
          return serachElement(parentElement.childNodes[i], element);
      }
      return false;
  };

  //recupera o elemento referente ao dialog
  var getDialogFromElement = function (element) {
      //recupera todos os dialogs da tela
      var dialogs = document.getElementsByClassName("modal-dialog")
      //se tiver apenas um, então esse é o dialog procurado e o mesmo é retornado
      if (dialogs.length == 1) {
          return angular.element(dialogs[0]);
      }
      //senão, varre todos os dialogs, procurando por aquele que contém o elemento corrente na sua árvore de elementos
      var i = 0;
      for (i = 0; i < dialogs.length; i++) {
          //se encontrar o elemento correte, então esse é o dialog procurado
          if (serachElement(dialogs[i], element)) {
              return angular.element(dialogs[i]);
          }              
      }
  };

  //movimenta o dialog correspondente ao elemento informado (element)
  //O elemento que chega aqui é aquele que contém o atributo draggable
  return function (scope, element) {          
      var startX = 0,
        startY = 0,
        x = 0,
        y = 0;

      //recupera o dialog correspondente ao element corrente
      element = getDialogFromElement(element);

      //coloca o cursor de movimento no header
      var header = angular.element(document.getElementsByClassName("modal-header"));
      header.css({              
          cursor: 'move'
      });          

      element.on('mousedown', function (event) {
          // Prevent default dragging of selected content
          //event.preventDefault();
          startX = event.screenX - x;
          startY = event.screenY - y;
          $document.on('mousemove', mousemove);
          $document.on('mouseup', mouseup);
      });

      function mousemove(event) {
          y = event.screenY - startY;
          x = event.screenX - startX;
          element.css({
              top: y + 'px',
              left: x + 'px'
          });
      }

      function mouseup() {
          $document.unbind('mousemove', mousemove);
          $document.unbind('mouseup', mouseup);
      }
  };

}
})();

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply