1 angular.module('ui.bootstrap.modal', ['ui.bootstrap.transition'])
4 * A helper, internal data structure that acts as a map but also allows getting / removing
5 * elements in the LIFO order
7 .factory('$$stackedMap', function () {
9 createNew: function () {
13 add: function (key, value) {
20 for (var i = 0; i < stack.length; i++) {
21 if (key == stack[i].key) {
28 for (var i = 0; i < stack.length; i++) {
29 keys.push(stack[i].key);
34 return stack[stack.length - 1];
36 remove: function (key) {
38 for (var i = 0; i < stack.length; i++) {
39 if (key == stack[i].key) {
44 return stack.splice(idx, 1)[0];
46 removeTop: function () {
47 return stack.splice(stack.length - 1, 1)[0];
58 * A helper directive for the $modal service. It creates a backdrop element.
60 .directive('modalBackdrop', ['$timeout', function ($timeout) {
64 templateUrl: 'template/modal/backdrop.html',
65 link: function (scope, element, attrs) {
66 scope.backdropClass = attrs.backdropClass || '';
68 scope.animate = false;
70 //trigger CSS transitions
71 $timeout(function () {
78 .directive('modalWindow', ['$modalStack', '$timeout', function ($modalStack, $timeout) {
87 templateUrl: function(tElement, tAttrs) {
88 return tAttrs.templateUrl || 'template/modal/window.html';
90 link: function (scope, element, attrs) {
91 element.addClass(attrs.windowClass || '');
92 scope.size = attrs.size;
94 // moved from template to fix issue #2280
95 element.on('click', function(evt) {
99 $timeout(function () {
100 // trigger CSS transitions
101 scope.animate = true;
104 * Auto-focusing of a freshly-opened modal element causes any child elements
105 * with the autofocus attribute to lose focus. This is an issue on touch
106 * based devices which will show and then hide the onscreen keyboard.
107 * Attempts to refocus the autofocus element via JavaScript will not reopen
108 * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus
109 * the modal element if the modal does not contain an autofocus element.
111 if (!element[0].querySelectorAll('[autofocus]').length) {
116 scope.close = function (evt) {
117 var modal = $modalStack.getTop();
118 if (modal && modal.value.backdrop && modal.value.backdrop != 'static' && (evt.target === evt.currentTarget)) {
119 evt.preventDefault();
120 evt.stopPropagation();
121 $modalStack.dismiss(modal.key, 'backdrop click');
128 .directive('modalTransclude', function () {
130 link: function($scope, $element, $attrs, controller, $transclude) {
131 $transclude($scope.$parent, function(clone) {
133 $element.append(clone);
139 .factory('$modalStack', ['$transition', '$timeout', '$document', '$compile', '$rootScope', '$$stackedMap',
140 function ($transition, $timeout, $document, $compile, $rootScope, $$stackedMap) {
142 var OPENED_MODAL_CLASS = 'modal-open';
144 var backdropDomEl, backdropScope;
145 var openedWindows = $$stackedMap.createNew();
146 var $modalStack = {};
148 function backdropIndex() {
149 var topBackdropIndex = -1;
150 var opened = openedWindows.keys();
151 for (var i = 0; i < opened.length; i++) {
152 if (openedWindows.get(opened[i]).value.backdrop) {
153 topBackdropIndex = i;
156 return topBackdropIndex;
159 $rootScope.$watch(backdropIndex, function(newBackdropIndex){
161 backdropScope.index = newBackdropIndex;
165 function removeModalWindow(modalInstance) {
167 var body = $document.find('body').eq(0);
168 var modalWindow = openedWindows.get(modalInstance).value;
171 openedWindows.remove(modalInstance);
173 //remove window DOM element
174 removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, 300, function() {
175 modalWindow.modalScope.$destroy();
176 body.toggleClass(OPENED_MODAL_CLASS, openedWindows.length() > 0);
177 checkRemoveBackdrop();
181 function checkRemoveBackdrop() {
182 //remove backdrop if no longer needed
183 if (backdropDomEl && backdropIndex() == -1) {
184 var backdropScopeRef = backdropScope;
185 removeAfterAnimate(backdropDomEl, backdropScope, 150, function () {
186 backdropScopeRef.$destroy();
187 backdropScopeRef = null;
189 backdropDomEl = undefined;
190 backdropScope = undefined;
194 function removeAfterAnimate(domEl, scope, emulateTime, done) {
196 scope.animate = false;
198 var transitionEndEventName = $transition.transitionEndEventName;
199 if (transitionEndEventName) {
201 var timeout = $timeout(afterAnimating, emulateTime);
203 domEl.bind(transitionEndEventName, function () {
204 $timeout.cancel(timeout);
209 // Ensure this call is async
210 $timeout(afterAnimating);
213 function afterAnimating() {
214 if (afterAnimating.done) {
217 afterAnimating.done = true;
226 $document.bind('keydown', function (evt) {
229 if (evt.which === 27) {
230 modal = openedWindows.top();
231 if (modal && modal.value.keyboard) {
232 evt.preventDefault();
233 $rootScope.$apply(function () {
234 $modalStack.dismiss(modal.key, 'escape key press');
240 $modalStack.open = function (modalInstance, modal) {
242 openedWindows.add(modalInstance, {
243 deferred: modal.deferred,
244 modalScope: modal.scope,
245 backdrop: modal.backdrop,
246 keyboard: modal.keyboard
249 var body = $document.find('body').eq(0),
250 currBackdropIndex = backdropIndex();
252 if (currBackdropIndex >= 0 && !backdropDomEl) {
253 backdropScope = $rootScope.$new(true);
254 backdropScope.index = currBackdropIndex;
255 var angularBackgroundDomEl = angular.element('<div modal-backdrop></div>');
256 angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass);
257 backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope);
258 body.append(backdropDomEl);
261 var angularDomEl = angular.element('<div modal-window></div>');
263 'template-url': modal.windowTemplateUrl,
264 'window-class': modal.windowClass,
266 'index': openedWindows.length() - 1,
268 }).html(modal.content);
270 var modalDomEl = $compile(angularDomEl)(modal.scope);
271 openedWindows.top().value.modalDomEl = modalDomEl;
272 body.append(modalDomEl);
273 body.addClass(OPENED_MODAL_CLASS);
276 $modalStack.close = function (modalInstance, result) {
277 var modalWindow = openedWindows.get(modalInstance);
279 modalWindow.value.deferred.resolve(result);
280 removeModalWindow(modalInstance);
284 $modalStack.dismiss = function (modalInstance, reason) {
285 var modalWindow = openedWindows.get(modalInstance);
287 modalWindow.value.deferred.reject(reason);
288 removeModalWindow(modalInstance);
292 $modalStack.dismissAll = function (reason) {
293 var topModal = this.getTop();
295 this.dismiss(topModal.key, reason);
296 topModal = this.getTop();
300 $modalStack.getTop = function () {
301 return openedWindows.top();
307 .provider('$modal', function () {
309 var $modalProvider = {
311 backdrop: true, //can be also false or 'static'
314 $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack',
315 function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) {
319 function getTemplatePromise(options) {
320 return options.template ? $q.when(options.template) :
321 $http.get(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl,
322 {cache: $templateCache}).then(function (result) {
327 function getResolvePromises(resolves) {
328 var promisesArr = [];
329 angular.forEach(resolves, function (value) {
330 if (angular.isFunction(value) || angular.isArray(value)) {
331 promisesArr.push($q.when($injector.invoke(value)));
337 $modal.open = function (modalOptions) {
339 var modalResultDeferred = $q.defer();
340 var modalOpenedDeferred = $q.defer();
342 //prepare an instance of a modal to be injected into controllers and returned to a caller
343 var modalInstance = {
344 result: modalResultDeferred.promise,
345 opened: modalOpenedDeferred.promise,
346 close: function (result) {
347 $modalStack.close(modalInstance, result);
349 dismiss: function (reason) {
350 $modalStack.dismiss(modalInstance, reason);
354 //merge and clean up options
355 modalOptions = angular.extend({}, $modalProvider.options, modalOptions);
356 modalOptions.resolve = modalOptions.resolve || {};
359 if (!modalOptions.template && !modalOptions.templateUrl) {
360 throw new Error('One of template or templateUrl options is required.');
363 var templateAndResolvePromise =
364 $q.all([getTemplatePromise(modalOptions)].concat(getResolvePromises(modalOptions.resolve)));
367 templateAndResolvePromise.then(function resolveSuccess(tplAndVars) {
369 var modalScope = (modalOptions.scope || $rootScope).$new();
370 modalScope.$close = modalInstance.close;
371 modalScope.$dismiss = modalInstance.dismiss;
373 var ctrlInstance, ctrlLocals = {};
377 if (modalOptions.controller) {
378 ctrlLocals.$scope = modalScope;
379 ctrlLocals.$modalInstance = modalInstance;
380 angular.forEach(modalOptions.resolve, function (value, key) {
381 ctrlLocals[key] = tplAndVars[resolveIter++];
384 ctrlInstance = $controller(modalOptions.controller, ctrlLocals);
385 if (modalOptions.controllerAs) {
386 modalScope[modalOptions.controllerAs] = ctrlInstance;
390 $modalStack.open(modalInstance, {
392 deferred: modalResultDeferred,
393 content: tplAndVars[0],
394 backdrop: modalOptions.backdrop,
395 keyboard: modalOptions.keyboard,
396 backdropClass: modalOptions.backdropClass,
397 windowClass: modalOptions.windowClass,
398 windowTemplateUrl: modalOptions.windowTemplateUrl,
399 size: modalOptions.size
402 }, function resolveError(reason) {
403 modalResultDeferred.reject(reason);
406 templateAndResolvePromise.then(function () {
407 modalOpenedDeferred.resolve(true);
409 modalOpenedDeferred.reject(false);
412 return modalInstance;
419 return $modalProvider;