<!DOCTYPE html>
<html ng-app="test">
<head lang="en">

	<link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
	<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.js"></script>
	<script src="//cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.10/angular-ui-router.min.js"></script>
	<script src="//angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.11.0.js"></script>
  <script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>

	<script src="2191.js"></script>
	<script src="modalstate.js"></script>
	<script src="script.js"></script>

</head>
<body ng-controller="MainCtrl">

  <div ui-view></div>
  
  <p>
    This is a demo of scenarios involving the lifecycle of $modal and the new
    modal.closing event.  In addition to direct use of $modal, it shows that it
    is possible (albeit complicated) to harmonize the event model when a modal
    is used to wrap a nested state view transition.
  </p>
  <p>
    The resolve and state exit options are global.  The allow close option
    is local to each modal instance.
  </p>
  <p>
    The setup for this demo is more elaborate than an ordinary use case would be 
    because each possible callback is wired to produce console output.  This is 
    useful for understanding the relationship and interaction sequence, particularly
    in state-based scenarios.
  </p>
  
</body>
</html>
function defineModal() {
  return {
    templateUrl: 'modal.html',
    controller: 'ModalCtrl',
    resolve: {
      resolved: function($rootScope, $q) {
        return $rootScope.flag.resolve ? $q.when({stuff:'asynchronous'}) : {stuff:'synchronous'}
      }
    }
  };
}

function bindModal(modal) {
  modal.opened.then(function() {
    console.log('client: opened');
  });
  modal.result.then(function(result) {
    console.log('client: resolved: ' + result);
  }, function(reason) {
    console.log('client: rejected: ' + reason);
  });
}

angular
  .module('test', ['ui.router', 'ui.bootstrap', 'modalstate'])

  .directive('modalButtons', function() {
    return {
      restrict: 'E',
      replace: true,
      controller: function($scope, $modal) {
        $scope.modal = function() {
          bindModal($modal.open(defineModal()));
        };
      },
      template: '<div class="panel panel-default"><div class="panel-body">'
        + '<button class="btn btn-link" ng-click="modal()" tooltip-placement="right" tooltip="Open another modal using $modal directly.">$modal</button><br />'
        + '<button class="btn btn-link" ui-sref=".modal" tooltip-placement="right" tooltip="Open a nested modal using a state transition.  Inherits from $rootScope.">ui-sref</button><br />'
        + '<button class="btn btn-link" ui-sref=".modal" modal-scope tooltip-placement="right" tooltip="Open a nested modal using a state transition.  Inherits from the current scope.">ui-sref modal-scope</button><br />'
        + '</div></div>'
    };
  })
	
  .config(function($stateProvider, modalStateProvider, $urlRouterProvider) {
  
    $stateProvider.state('main', {
      url: '/',
      template: '<pre>$state.name = {{$state.current.name}}</pre>'
        + '<label><input type="checkbox" ng-model="flag.resolve" /> Resolve a promise before each modal.</label><br />'
        + '<modal-buttons></modal-buttons>'
    });
    
    (function modalState(parent, depth) {
        var stateName = parent + '.modal';
        
        modalStateProvider.state(stateName, _.extend(defineModal(), {
          url: 'modal/',
          onModal: function(modal) {
            console.log('client: onModal');
            bindModal(modal);
          },
          onEnter: function() {
            console.log('client: onEnter');
          },
          onExit: function() {
            console.log('client: onExit');
          },
        }));
        
        if (depth > 0) {
          modalState(stateName, depth-1);
        }
      })('main', 20);
      
      $urlRouterProvider.otherwise('/');
  })
    
  .controller('MainCtrl', function($rootScope, $scope, $state, $modal) {
    $rootScope.$state = $state;
    $rootScope.flag = {resolve: true, leavable:true};

    console.log('client[' + $scope.$id + ']: main controller');

    $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
      console.log('$stateChangeStart: ' + fromState.name + ' => ' + toState.name);
      if (!$rootScope.flag.leavable) {
        console.log('client: $stateChangeStart: veto due to flag');
        event.preventDefault();
      }
    });
    
    $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) {
      console.log('$stateChangeSuccess: ' + fromState.name + ' => ' + toState.name);
    });
  })

  .controller('ModalCtrl', function ($rootScope, $scope, $modalInstance, $modalStack, modalState, resolved) {

      $scope.local = {closeMode: '1'};
      
      $scope.depth = ($scope.depth || 0) + 1;
      
      $scope.isStateBased = function() {
        return modalState.isTop();
      };

      var prefix = 'client[' + $scope.$id + ']: ';
      console.log(prefix + 'modal controller, resolved = ' + angular.toJson(resolved));

      $scope.$on('modal.closing', function(event, reason, closed) {
          console.log(prefix + 'modal.closing: ' + (closed ? 'close' : 'dismiss') + '(' + reason + ')');
          if ($scope.local.closeMode === '1') {
            // allow
          } else if ($scope.local.closeMode === '2') {
            if (!confirm('Are you sure?')) {
              console.log('\t' + prefix + 'veto (interactive)');
              event.preventDefault();
            }
          } else if ($scope.local.closeMode === '3') {
            console.log('\t' + prefix + 'veto (deterministic)');
            event.preventDefault();
          }
      });
      $scope.$on('$destroy', function() {
        console.log(prefix + '$destroy');
      });

      $scope.ok = function () {
        console.log(prefix + '$close returned ' + $scope.$close('ok'));
      };

      $scope.cancel = function () {
        console.log(prefix + '$dismiss returned ' + $scope.$dismiss('cancel'));
      };

      $scope.dismissAll = function() {
        if (modalState.getDepth() > 0) {
          console.log(prefix + 'dismissAll (state-based)');
          modalState.dismissAll('all:state-based').then(function(x){
            console.log(prefix + 'dismissAll resolved: ' + x);
          }, function(x) {
            console.log(prefix + 'dismissAll rejected: ' + x);
          });
        } else {
          console.log(prefix + 'dismissAll (native)');
          $modalStack.dismissAll('all:native');
        }
      };
  })

;
/*
 * This is yanked out of modal.js.  It overrides $modalStack and $modal.
 * See https://github.com/angular-ui/bootstrap/pull/2256
 */
angular
  .module('ui.bootstrap.modal')
  .factory('$modalStack', ['$transition', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap',
    function ($transition, $timeout, $document, $compile, $rootScope, $$stackedMap) {

      var OPENED_MODAL_CLASS = 'modal-open';

      var backdropDomEl, backdropScope;
      var openedWindows = $$stackedMap.createNew();
      var $modalStack = {};

      function backdropIndex() {
        var topBackdropIndex = -1;
        var opened = openedWindows.keys();
        for (var i = 0; i < opened.length; i++) {
          if (openedWindows.get(opened[i]).value.backdrop) {
            topBackdropIndex = i;
          }
        }
        return topBackdropIndex;
      }

      $rootScope.$watch(backdropIndex, function(newBackdropIndex){
        if (backdropScope) {
          backdropScope.index = newBackdropIndex;
        }
      });

      function removeModalWindow(modalInstance) {

        var body = $document.find('body').eq(0);
        var modalWindow = openedWindows.get(modalInstance).value;

        //clean up the stack
        openedWindows.remove(modalInstance);

        //remove window DOM element
        removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, function() {
          modalWindow.modalScope.$destroy();
          body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0);
          checkRemoveBackdrop();
        });
      }

      function checkRemoveBackdrop() {
          //remove backdrop if no longer needed
          if (backdropDomEl && backdropIndex() == -1) {
            var backdropScopeRef = backdropScope;
            removeAfterAnimate(backdropDomEl, backdropScope, 150, function () {
              backdropScopeRef.$destroy();
              backdropScopeRef = null;
            });
            backdropDomEl = undefined;
            backdropScope = undefined;
          }
      }

      function removeAfterAnimate(domEl, scope, emulateTime, done) {
        // Closing animation
        scope.animate = false;

        var transitionEndEventName = $transition.transitionEndEventName;
        if (transitionEndEventName) {
          // transition out
          var timeout = $timeout(afterAnimating, emulateTime);

          domEl.bind(transitionEndEventName, function () {
            $timeout.cancel(timeout);
            afterAnimating();
            scope.$apply();
          });
        } else {
          // Ensure this call is async
          $timeout(afterAnimating);
        }

        function afterAnimating() {
          if (afterAnimating.done) {
            return;
          }
          afterAnimating.done = true;

          domEl.remove();
          if (done) {
            done();
          }
        }
      }

      $document.bind('keydown', function (evt) {
        var modal;

        if (evt.which === 27) {
          modal = openedWindows.top();
          if (modal && modal.value.keyboard) {
            evt.preventDefault();
            $rootScope.$apply(function () {
              $modalStack.dismiss(modal.key, 'escape key press');
            });
          }
        }
      });

      $modalStack.open = function (modalInstance, modal) {

        openedWindows.add(modalInstance, {
          deferred: modal.deferred,
          modalScope: modal.scope,
          backdrop: modal.backdrop,
          keyboard: modal.keyboard
        });

        var body = $document.find('body').eq(0),
            currBackdropIndex = backdropIndex();

        if (currBackdropIndex >= 0 && !backdropDomEl) {
          backdropScope = $rootScope.$new(true);
          backdropScope.index = currBackdropIndex;
          var angularBackgroundDomEl = angular.element('<div modal-backdrop></div>');
          angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass);
          backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope);
          body.append(backdropDomEl);
        }

        var angularDomEl = angular.element('<div modal-window></div>');
        angularDomEl.attr({
          'template-url': modal.windowTemplateUrl,
          'window-class': modal.windowClass,
          'size': modal.size,
          'index': openedWindows.length() - 1,
          'animate': 'animate'
        }).html(modal.content);

        var modalDomEl = $compile(angularDomEl)(modal.scope);
        openedWindows.top().value.modalDomEl = modalDomEl;
        body.append(modalDomEl);
        body.addClass(OPENED_MODAL_CLASS);
      };

      function broadcastClosing(modalWindow, resultOrReason, dismissed) {
          return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, dismissed).defaultPrevented;
      }

      $modalStack.close = function (modalInstance, result) {
        var modalWindow = openedWindows.get(modalInstance);
        if (modalWindow && broadcastClosing(modalWindow, result, true)) {
          modalWindow.value.deferred.resolve(result);
          removeModalWindow(modalInstance);
          return true;
        }
        return !modalWindow;
      };

      $modalStack.dismiss = function (modalInstance, reason) {
        var modalWindow = openedWindows.get(modalInstance);
        if (modalWindow && broadcastClosing(modalWindow, reason, false)) {
          modalWindow.value.deferred.reject(reason);
          removeModalWindow(modalInstance);
          return true;
        }
        return !modalWindow;
      };

      $modalStack.dismissAll = function (reason) {
        var topModal = this.getTop();
        while (topModal && this.dismiss(topModal.key, reason)) {
          topModal = this.getTop();
        }
      };

      $modalStack.getTop = function () {
        return openedWindows.top();
      };

      return $modalStack;
    }])

  .provider('$modal', function () {

    var $modalProvider = {
      options: {
        backdrop: true, //can be also false or 'static'
        keyboard: true
      },
      $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack',
        function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) {

          var $modal = {};

          function getTemplatePromise(options) {
            return options.template ? $q.when(options.template) :
              $http.get(options.templateUrl, {cache: $templateCache}).then(function (result) {
                return result.data;
              });
          }

          function getResolvePromises(resolves) {
            var promisesArr = [];
            angular.forEach(resolves, function (value) {
              if (angular.isFunction(value) || angular.isArray(value)) {
                promisesArr.push($q.when($injector.invoke(value)));
              }
            });
            return promisesArr;
          }

          $modal.open = function (modalOptions) {

            var modalResultDeferred = $q.defer();
            var modalOpenedDeferred = $q.defer();

            //prepare an instance of a modal to be injected into controllers and returned to a caller
            var modalInstance = {
              result: modalResultDeferred.promise,
              opened: modalOpenedDeferred.promise,
              close: function (result) {
                return $modalStack.close(modalInstance, result);
              },
              dismiss: function (reason) {
                return $modalStack.dismiss(modalInstance, reason);
              }
            };

            //merge and clean up options
            modalOptions = angular.extend({}, $modalProvider.options, modalOptions);
            modalOptions.resolve = modalOptions.resolve || {};

            //verify options
            if (!modalOptions.template && !modalOptions.templateUrl) {
              throw new Error('One of template or templateUrl options is required.');
            }

            var templateAndResolvePromise =
              $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve)));


            templateAndResolvePromise.then(function resolveSuccess(tplAndVars) {

              var modalScope = (modalOptions.scope || $rootScope).$new();
              modalScope.$close = modalInstance.close;
              modalScope.$dismiss = modalInstance.dismiss;

              var ctrlLocals = {};
              var resolveIter = 1;

              //controllers
              if (modalOptions.controller) {
                ctrlLocals.$scope = modalScope;
                ctrlLocals.$modalInstance = modalInstance;
                angular.forEach(modalOptions.resolve, function (value, key) {
                  ctrlLocals[key] = tplAndVars[resolveIter++];
                });

                $controller(modalOptions.controllerAs ? modalOptions.controller + ' as ' + modalOptions.controllerAs : modalOptions.controller, ctrlLocals);
              }

              $modalStack.open(modalInstance, {
                scope: modalScope,
                deferred: modalResultDeferred,
                content: tplAndVars[0],
                backdrop: modalOptions.backdrop,
                keyboard: modalOptions.keyboard,
                backdropClass: modalOptions.backdropClass,
                windowClass: modalOptions.windowClass,
                windowTemplateUrl: modalOptions.windowTemplateUrl,
                size: modalOptions.size
              });

            }, function resolveError(reason) {
              modalResultDeferred.reject(reason);
            });

            templateAndResolvePromise.then(function () {
              modalOpenedDeferred.resolve(true);
            }, function () {
              modalOpenedDeferred.reject(false);
            });

            return modalInstance;
          };

          return $modal;
        }]
    };

    return $modalProvider;
  });
;
<div class="modal-header">
  <button type="button" class="close" ng-click="$dismiss()">&times;</button>
  <h4 class="modal-title">
    Modal
    <small ng-if="isStateBased()">
      <br /><label><input type="checkbox" ng-model="flag.leavable" /> allow state exit</label>
      <br /><a ui-sref="^">Parent State</a>
    </small>
  </h4>
</div>

	<div class="modal-body">
		Scope {{$parent.$id}} / {{$id}}, Depth {{depth}}<br />
		<label><input type="radio" value="1" ng-model="local.closeMode" /> allow close</label><br />
		<label><input type="radio" value="2" ng-model="local.closeMode" /> confirm close</label><br />
		<label><input type="radio" value="3" ng-model="local.closeMode" /> prevent close</label><br />
		<modal-buttons></modal-buttons>
	</div>
	<div class="modal-footer">
		<button class="btn btn-primary" ng-click="ok()">OK</button>
		<button class="btn btn-warning" ng-click="cancel()">Cancel</button>
		<button class="btn btn-danger" ng-click="dismissAll()">Dismiss All</button>
	</div>
/* License: MIT - https://github.com/angular-ui/bootstrap/blob/master/LICENSE */
angular
	.module('modalstate', ['ui.router', 'ui.bootstrap'])

	/**
	 * Encapsulates the boilerplate typically needed to implement a state-driven modal.  Implemented as a provider so it
	 * can be injected like $stateProvider during the config phase and used to declare modal states.  Modal is based
	 * on ui.bootstrap.modal.
	 *
	 * Usage:
	 *
	 *   module.config(function(modalStateProvider) {
	 *     modalStateProvider.state('state.name', options);
	 *   });
	 *
	 * The options argument is a mix of options for modalStateProvider, $stateProvider and $modal.
	 *
	 * modalStateProvider:
	 * - returnState: the state to go to after the modal closes (default is '^' which returns to the immediate parent)
	 * - onModal: function called with the modal instance after it is created.  Use to access $modal's result/opened promises.
	 *
	 * $stateProvider receives the 'onEnter', 'onExit', 'url', 'params', 'abstract', 'reloadOnSearch' and 'data' options
	 * if provided.  $modal receives all remaining options.  No view or controller parameters are passed to $stateProvider;
	 * these are handled by $modal.  Resolve is also handled by $modal.
	 *
	 * modalStateProvider attempts to harmonize the state change event model with a modal's local event model.  Both
	 * models support veto of a requested transition, but the state model is global and consequently must be observed
	 * asynchronously while the modal model is scoped to the modal controller and normally synchronous.  Using a modal
	 * state definition, it's possible to add synchronous decision logic as a global `$stateChangeStart` listener, a
	 * local `modal.closing` listener or both.  All must pass before the modal can close, and none will be called more
	 * than once per close attempt (making synchronous confirm interactions safe).  There are some minor caveats (see
	 * comments below about `$close` and `$dismiss` on the scope and `dismissAll` on `$modalStack`), but the overall
	 * behavior is reliable.
	 *
	 * Note that due to transclusion, $modal's view scope will not be the same as its controller scope; it will be a
	 * child.  This is a basic characteristic of $modal.  Use nested (dot) references in the view to avoid unexpected
	 * issues with prototypal inheritance.  Also note that $modal's scopes inherit from $rootScope by default.  You can
	 * pass a different scope in the state definition, but during the configuration phase, this is often impractical.
	 * The `modalScope` directive help with this by stashing the current scope for the next modal state transition to
	 * find and use.  Add `modal-scope` to the same element as `ui-sref` for a modal state and the resulting modal will
	 * inherit the current scope as though it had been embedded at that point in the document.
	 */
	.provider('modalState', function($stateProvider) {

		var _stack = [];
		var _parentScope = null;

		var _dismissAllIntent = false;
		var _dismissAllLast = null;
		var _dismissAllReason = null;
		var _dismissAllResult = null;

		function _dismissAll($modalStack, $timeout) {
			if (_dismissAllIntent) {
console.log('modalstate: dismissAll cycle');
				var last = _dismissAllLast;
				_dismissAllLast = $modalStack.getTop();
				if (_dismissAllLast === undefined || _dismissAllLast === last) { // end when the stack is empty or no change occurred
console.log('modalstate: dismissAll terminated with ' + (_dismissAllLast === undefined ? 'resolve' : 'reject'));
					_dismissAllResult[_dismissAllLast === undefined ? 'resolve' : 'reject'](_dismissAllReason);
					_dismissAllResult = null;
					_dismissAllReason = null;
					_dismissAllLast = null;
					_dismissAllIntent = false;
				} else {
					$modalStack.dismissAll(_dismissAllReason);
					$timeout(function() {
						_dismissAll($modalStack, $timeout);
					});
				}
			}
		}

		this.$get = function($q, $timeout, $modalStack) {
			return {
				/**
				 * Allows modal states to be defined dynamically in the run phase.
				 */
				state: this.state,

				/**
				 * @returns {number} How many state-based modals are currently open.
				 */
				getDepth: function() {
					return _stack.length;
				},

				/**
				 * @returns {*|boolean} True if a state-driven modal is at the top of $modalStack.
				 */
				isTop: function() {
					var top = $modalStack.getTop();
					return top && _stack.length && top.value.modalScope === _stack[_stack.length-1].scope;
				},

				/**
				 * Sets a scope to be used as the parent of the next modal instance to be created.
				 * Add a modal-scope directive to a ui-sref to spawn the modal with inherited access to the current scope.
				 */
				setParentScope: function(parentScope) {
					_parentScope = parentScope;
				},

				/**
				 * Use instead of $modalStack.dismissAll(reason).  The coordination between modal lifecycle and state lifecycle
				 * requires that the close process happen asynchronously.  As a consequence, $modalStack's dismissAll method
				 * only succeeds in closing the first modal.  Use this method to propagate the dismiss intent until all modals
				 * are closed or a modal vetos the close.
				 *
				 * Note that in scenarios involving a mixed stack of manual and state-driven modals, this method may not
				 * yield a result.
				 *
				 * @param reason
				 * @return {Promise} a promise yielding a boolean indicating whether all modals were dismissed
				 */
				dismissAll: function(reason) {
					if (!_dismissAllIntent) {
						_dismissAllIntent = true;
						_dismissAllReason = reason;
						_dismissAllResult = $q.defer();
					}
					_dismissAll($modalStack, $timeout);
					return _dismissAllResult ? _dismissAllResult.promise : $q.when(undefined);
				}
			};
		};

		/**
		 * Defines a new state whose lifecycle is coordinated with the display and dismissal of a bootstrap modal.
		 * @param stateName
		 * @param options
		 * @returns {*} this for chaining
		 */
		this.state = function(stateName, options) {
			// Internal properties of the modal state instance.
			var instanceDef = {
				childPattern: new RegExp('^' + stateName.replace(/\./g, '\\.') + '\\.'),
				returnState: options.returnState || '^',
				onModal: options.onModal || null,
				onEnter: options.onEnter || null,
				onExit: options.onExit || null
			};

			// $stateProvider options (view/controller properties are handled by $modal)
			var stateDef = _.pick(options, ['url', 'params', 'abstract', 'reloadOnSearch', 'data']);

			stateDef.onEnter = /*@ngInject*/function($rootScope, $modal, $injector, $state, $timeout) {
console.log('modalstate: onEnter');
				_stack.push(instanceDef);

				if (_.isFunction(instanceDef.onEnter)) {
					$injector.invoke(instanceDef.onEnter);
				}

				instanceDef.broadcast = true; // used to prevent double-broadcast if close is modal-initiated
				instanceDef.result = null;
				instanceDef.closed = false;

				instanceDef.startListener = $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
					if (fromState.name === stateName && !instanceDef.childPattern.test(toState.name)) {
						if (instanceDef.broadcast) {
							instanceDef.result = undefined;
							instanceDef.closed = false; // treat state-driven transition as a dismissal
							if (instanceDef.scope && instanceDef.scope.$broadcast('modal.closing', instanceDef.result, instanceDef.closed).defaultPrevented) {
console.log('modalstate: $stateChangeStart: modal.closing was vetoed');
								event.preventDefault();
							} else {
console.log('modalstate: $stateChangeStart: modal.closing is allowed');
							}
						} else {
console.log('modalstate: $stateChangeStart: modal-driven');
							instanceDef.broadcast = true;
						}
					}
				});

				// $modal options
				var modalDef = _(options).omit(_.keys(stateDef)).omit(['returnState', 'onModal', 'onEnter', 'onExit', 'scope']).value();
				modalDef.scope = options.scope || _parentScope || $rootScope;
				_parentScope = null; // one-time use

				instanceDef.modal = $modal.open(modalDef);
				instanceDef.modal.opened.then(function() {
					/*
					 * $modal doesn't provide specific access to the scope it creates for the modal controller, but we
					 * can find it by walking the children of the known starting scope looking for the signature of an
					 * object we know to be present.  We need this scope in order to control close event handling.
					 */
					var childScope;
					for(childScope = modalDef.scope.$$childHead; childScope; childScope = childScope.$$nextSibling) {
						if (childScope.$close === instanceDef.modal.close) {
							break;
						}
					}
					if (!childScope) {
						throw new Error('modalState: failed to identify controller scope under modal scope ' + modalDef.scope.$id);
					}

					instanceDef.scope = childScope;

					/*
					 * Closing can be state-initiated or modal-initiated.  We want each case to have equal veto opportunity.
					 * If the event is modal-initiated, then we need to initially veto and fire a state transition that
					 * will make the final decision.  Our only hook is the scope event which requires some trickery to
					 * intercept.  A consequence of this overall approach is that a modal controller's calls to the
					 * synchronous $close and $dismiss scope methods that are added by $modal will return false even if
					 * the event may eventually be approved by the state transition.
					 */
					childScope.$on('modal.closing', function(event, result, closed) {
						if ($state.current.name === stateName) { // the broadcast may reach multiple modals
							var defaultPrevented = event.defaultPrevented; // the controller registers first and my already have vetoed
							event.preventDefault(); // even if other listenrs don't veto, we need to stop the event so we can turn it into a state transition
							event.preventDefault = function() {
								defaultPrevented = true; // did any other listeners veto?
							};
							$timeout(function() { // wait for other listeners to process the event
console.log('modalstate: intercepted modal.closing: ' + (defaultPrevented ? 'vetoed' : 'allowed'));
								if (!defaultPrevented) {
									instanceDef.broadcast = false; // event is already cleared as far as the modal is concerned
									instanceDef.result = result;
									instanceDef.closed = closed;
									$state.go(instanceDef.returnState); // drive the close as a state transition instead
								}
							});
						} else {
console.log('modalstate: ignore modal.closing in different state');
						}
					});
				});

				if (_.isFunction(instanceDef.onModal)) {
					instanceDef.onModal(instanceDef.modal);
				}
			};

			stateDef.onExit = /*@ngInject*/function($injector) {
				instanceDef.startListener();
				var $broadcast = instanceDef.scope.$broadcast;
				instanceDef.scope.$broadcast = function(name, args) {
console.log('modalstate: intercept $broadcast of ' + name + ' on closing scope: ' + (name === 'modal.closing' ? 'suppress' : 'allow'));
					if (name === 'modal.closing') {
						return {defaultPrevented:false}; // ignore this event; treat it as allowed
					} else {
						return $broadcast.apply(this, arguments);
					}
				};
				instanceDef.modal[instanceDef.closed ? 'close' : 'dismiss'](instanceDef.result);
				instanceDef.scope.$broadcast = $broadcast;
				delete instanceDef.startListener;
				delete instanceDef.scope;
				delete instanceDef.modal;
				delete instanceDef.result;

				if (_.isFunction(instanceDef.onExit)) {
					$injector.invoke(instanceDef.onExit);
				}

				_stack.pop();
console.log('modalstate: onExit');
			};

			$stateProvider.state(stateName, stateDef);

			return this; // chaining
		};
	})
	
	/**
	 * Use with ui-sref to pass the current scope to a modal opened by the state transition.
	 * Modals normally inherit from the root scope, but this means they lose access to any lexically enclosing scope
	 * properties they might have expected.
   */
	.directive('modalScope', function(modalState) {
    return {
		  restrict: 'A',
		  link: function(scope, elem, attrs) {
			  // Don't have to worry about precedence here because ui-sref uses $timeout.
			  elem.bind("click", function() {
				  modalState.setParentScope(scope);
			  });
		  }
	  };
	})

;//end module