Остановить распространение основного ng-click внутри события jQuery click

Загрузочный загрузочный файл Twitter dropdown находится внутри tr. tr можно кликать по ng-click. Щелчок в любом месте страницы приведет к сбрасыванию выпадающего меню. Это поведение определено в директиве через $document.bind('click', closeMenu).

Итак, когда меню открыто, и пользователь нажимает на строку, я хочу, чтобы меню закрывалось (как это происходит) И я хочу предотвратить событие щелчка в строке.

JSFiddle: http://jsfiddle.net/LMc2f/1/
JSFiddle + директива inline: http://jsfiddle.net/9DM8U/1/

Соответствующий код из 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);
        }
      });
    }
  };
}]);

Я не могу понять, как остановить базовое событие ng-click внутри closeMenu.

ПРИМЕЧАНИЕ. Я не могу найти способ доступа к $event, поэтому я не смог попробовать $event.stopPropagation().

Ответы

Ответ 1

Директива dropdown связывает событие click в документе, но когда вы нажимаете на строку, событие начинает распространяться с целевого элемента вниз на корневой документ node (tdtrtabledocument).

Итак, почему ваш обработчик ng-click, который у вас есть на вашей строке, всегда вызывается, хотя директива "останавливает" пузырь при нажатии на документ.

Решение заключается в использовании флага useCapture при добавлении обработчика кликов для документа.

После запуска захвата все события указанного типа будут отправляется зарегистрированному слушателю до отправки на любой EventTarget под ним в дереве DOM. mdn

Теперь, чтобы указать директиве выпадающего списка использовать собственный обработчик, вам нужно изменить источник директивы. Но это директива третьей стороны, и вы, вероятно, не хотите этого делать, по причинам, связанным с переносимостью.

Здесь вы найдете мощный декоратор angular $. Вы можете использовать $decorator для изменения источника стороннего модуля на лету, не затрагивая фактические исходные файлы.

Итак, с помощью декоратора на месте и с помощью специального обработчика событий в документе node, вы можете сделать это выпадающее меню:

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;
      }
  ]);

});

ОБНОВЛЕНИЕ:

Обратите внимание, что обновленный пользовательский обработчик предотвратит барботирование, если целевой элемент не является фактическим раскрывающимся вариантом. Это решит проблему, при которой событие click предотвращалось даже при нажатии на раскрывающиеся варианты.

Это все равно не будет препятствовать тому, чтобы событие опустилось до строки (из раскрывающегося списка), но это то, что никоим образом не связано с директивой раскрывающегося списка. Во всяком случае, для предотвращения такого барботажа вы можете передать объект $event в функцию выражения ng-click и использовать этот объект, чтобы остановить четное падение до строки таблицы:

<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();
    }
  }

}

Ответ 2

Я бы наклонился к простому вызову $event.stopPropagation() из самого шаблона. Логика, связанная с событиями, скорее всего, принадлежит. Следует также упростить модульное тестирование. Кто-то, смотрящий на шаблон, также будет знать, что событие не пузырилось, не глядя на базовый контроллер.

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

Ответ 3

вам нужно передать событие ng-click в строке 2 вашей скрипки, а затем сделать preventDefault и stopPropigation на этом объекте в вашем методе

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