web-dev-qa-db-fra.com

Arrêtez la propagation du ng-click sous-jacent à l'intérieur d'un événement click jQuery

Un Twitter Bootstrap dropdown est imbriqué dans un tr. Le tr est cliquable via ng-click. En cliquant n'importe où sur le Cette page réduira le menu déroulant. Ce comportement est défini dans une directive via $document.bind('click', closeMenu).

Donc, lorsque le menu est ouvert et que l'utilisateur clique sur une ligne, je veux que le menu se ferme (comme il le fait) ET je veux empêcher l'événement de clic sur la ligne.

JSFiddle: http://jsfiddle.net/LMc2f/1/
JSFiddle + directive en ligne : http://jsfiddle.net/9DM8U/1/

Code pertinent de ui-bootstrap-tpls-0.10.0.js:

angular.module('ui.bootstrap.dropdownToggle', []).directive('dropdownToggle', ['$document', '$location', function ($document, $location) {
  var openElement = null,
      closeMenu   = angular.noop;
  return {
    restrict: 'CA',
    link: function(scope, element, attrs) {
      scope.$watch('$location.path', function() { closeMenu(); });
      element.parent().bind('click', function() { closeMenu(); });
      element.bind('click', function (event) {

        var elementWasOpen = (element === openElement);

        event.preventDefault();
        event.stopPropagation();

        if (!!openElement) {
          closeMenu();
        }

        if (!elementWasOpen && !element.hasClass('disabled') && !element.prop('disabled')) {
          element.parent().addClass('open');
          openElement = element;
          closeMenu = function (event) {
            if (event) {
              event.preventDefault();
              event.stopPropagation();
            }
            $document.unbind('click', closeMenu);
            element.parent().removeClass('open');
            closeMenu = angular.noop;
            openElement = null;
          };
          $document.bind('click', closeMenu);
        }
      });
    }
  };
}]);

Je n'arrive pas à comprendre comment arrêter l'événement ng-click Sous-jacent dans closeMenu.

REMARQUE: je ne trouve pas de moyen d'accéder à $event, Je n'ai donc pas pu essayer $event.stopPropagation().

17
maxbeaudoin

La directive déroulante lie l'événement click sur le document, mais lorsque vous cliquez sur la ligne, l'événement commence à se propager de l'élément cible vers le bas vers le document racine nœud (td -> tr -> table -> document).

Voilà pourquoi votre ng-click, le gestionnaire que vous avez sur votre ligne est toujours appelé, même si la directive "arrête" la bulle lors du clic sur le document.

La solution consiste à utiliser l'indicateur useCapture lors de l'ajout du gestionnaire de clic pour le document.

Après avoir lancé la capture, tous les événements du type spécifié seront envoyés à l'écouteur enregistré avant d'être envoyés à n'importe quel EventTarget situé en dessous dans l'arborescence DOM. mdn

Maintenant, pour demander à la directive déroulante d'utiliser votre propre gestionnaire, vous devez modifier la source de la directive. Mais c'est une directive tierce, et vous ne voulez probablement pas le faire, pour des raisons de maintenabilité.

C'est là que le puissant angular $ décorateur entre en jeu. Vous pouvez utiliser le $ décorateur pour changer la source du module tiers à la volée, sans toucher réellement aux fichiers source réels.

Ainsi, avec le décorateur en place et le gestionnaire d'événements personnalisé sur le nœud du document, voici comment vous pouvez faire en sorte que le menu déroulant se comporte:

FIDDLE

var myApp = angular.module('myApp', []);

/**
 * Original dropdownToggle directive from ui-bootstrap.
 * Nothing changed here.
 */
myApp.directive('dropdownToggle', ['$document', '$location', function ($document, $location) {
  var openElement = null,
      closeMenu   = angular.noop;
  return {
    restrict: 'CA',
    link: function(scope, element, attrs) {
      scope.$watch('$location.path', function() { closeMenu(); });
      element.parent().bind('click', function() { closeMenu(); });
      element.bind('click', function (event) {

        var elementWasOpen = (element === openElement);

        event.preventDefault();
        event.stopPropagation();

        if (!!openElement) {
          closeMenu();
        }

        if (!elementWasOpen && !element.hasClass('disabled') && !element.prop('disabled')) {
          element.parent().addClass('open');
          openElement = element;
          closeMenu = function (event) {
            if (event) {
              event.preventDefault();
              event.stopPropagation();
            }
            $document.unbind('click', closeMenu);
            element.parent().removeClass('open');
            closeMenu = angular.noop;
            openElement = null;
          };
          $document.bind('click', closeMenu); /* <--- CAUSE OF ALL PROBLEMS ----- */
        }
      });
    }
  };
}]);


/**
 * This is were we decorate the dropdownToggle directive
 * in order to change the way the document click handler works
 */
myApp.config(function($provide){
  'use strict';

  $provide.decorator('dropdownToggleDirective', [
      '$delegate',
      '$document',
      function ($delegate, $document) {

        var directive = $delegate[0];
        var openElement = null;
        var closeMenu = angular.noop;

        function handler(e){
            var Elm = angular.element(e.target);
          if(!Elm.parents('.dropdown-menu').length){
            e.stopPropagation();
            e.preventDefault();
          }
          closeMenu();
          // After closing the menu, we remove the all-seeing handler
          // to allow the application click events to work nnormally
          $document[0].removeEventListener('click', handler, true);
        }

        directive.compile = function(){
          return function(scope, element) {
            scope.$watch('$location.path', closeMenu);
            element.parent().bind('click', closeMenu);
            element.bind('click', function (event) {

              var elementWasOpen = (element === openElement);

              event.preventDefault();
              event.stopPropagation();

              if (!!openElement) {
                closeMenu();
              }

              if (!elementWasOpen && !element.hasClass('disabled') && !element.prop('disabled')) {
                element.parent().addClass('open');
                openElement = element;
                closeMenu = function (event) {
                  if (event) {
                    event.preventDefault();
                    event.stopPropagation();
                  }
                  $document.unbind('click', closeMenu);
                  element.parent().removeClass('open');
                  closeMenu = angular.noop;
                  openElement = null;
                };


                // We attach the click handler by specifying the third "useCapture" parameter as true
                $document[0].addEventListener('click', handler, true);
              }
            });
          };
        };

        return $delegate;
      }
  ]);

});

MISE À JOUR:

Notez que le gestionnaire personnalisé mis à jour empêchera la propagation à moins que l'élément cible ne soit l'option de liste déroulante réelle. Cela résoudra le problème où l'événement de clic était empêché même lorsque vous cliquez sur les options déroulantes.

Cela n'empêchera toujours pas l'événement de se propager à la ligne (à partir de l'option de liste déroulante), mais c'est quelque chose qui n'est en rien lié à la directive de liste déroulante. Quoi qu'il en soit, pour éviter de telles bulles, vous pouvez passer le $event objet à ng-click fonction d'expression et utilisez cet objet pour arrêter la bulle même jusqu'à la ligne du tableau:

<div ng-controller="DropdownCtrl">
  <table>
    <tr ng-click="clicked('row')">
      <td>

        <div class="btn-group">
          <button type="button" class="btn btn-default dropdown-toggle">
            Action <span class="caret"></span>
          </button>
          <ul class="dropdown-menu" role="menu">
            <li ng-repeat="choice in items">
              <a ng-click="clicked('link element', $event)">{{choice}}</a>
            </li>
          </ul>
        </div>

      </td>
    </tr>
  </table>
</div>
function DropdownCtrl($scope) {
  $scope.items = [
    "Action",
    "Another action",
    "Something else here"
  ];

  $scope.clicked = function(what, event) {
    alert(what + ' clicked');
    if(event){
      event.stopPropagation();
      event.preventDefault();
    }
  }

}
16
Stewie

Je pencherais simplement pour appeler $event.stopPropagation() à partir du modèle lui-même. La logique liée aux événements y appartient très probablement. Cela devrait également faciliter les tests unitaires. Quelqu'un qui regarde le modèle sait également que l'événement ne s'est pas propagé sans regarder le contrôleur sous-jacent.

<div ng-click="parentHandler()">
    <div ng-click="childHandler(); $event.stopPropagation()"></div>
</div>
20
user2479438

vous devez passer l'événement de ng-click sur la ligne 2 de votre violon, puis faire preventDefault et stopPropagation sur cet objet dans votre méthode do

<tr ng-click="do($event)">
12
koolunix