angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap']) /** * A helper, internal data structure that stores all references attached to key */ .factory('$$multiMap', function() { return { createNew: function() { var map = {}; return { entries: function() { return Object.keys(map).map(function(key) { return { key: key, value: map[key] }; }); }, get: function(key) { return map[key]; }, hasKey: function(key) { return !!map[key]; }, keys: function() { return Object.keys(map); }, put: function(key, value) { if (!map[key]) { map[key] = []; } map[key].push(value); }, remove: function(key, value) { var values = map[key]; if (!values) { return; } var idx = values.indexOf(value); if (idx !== -1) { values.splice(idx, 1); } if (!values.length) { delete map[key]; } } }; } }; }) /** * A helper directive for the $modal service. It creates a backdrop element. */ .directive('uibModalBackdrop', [ '$animate', '$injector', '$uibModalStack', function($animate , $injector, $modalStack) { var $animateCss = null; if ($injector.has('$animateCss')) { $animateCss = $injector.get('$animateCss'); } return { replace: true, templateUrl: 'template/modal/backdrop.html', compile: function(tElement, tAttrs) { tElement.addClass(tAttrs.backdropClass); return linkFn; } }; function linkFn(scope, element, attrs) { // Temporary fix for prefixing element.addClass('modal-backdrop'); if (attrs.modalInClass) { if ($animateCss) { $animateCss(element, { addClass: attrs.modalInClass }).start(); } else { $animate.addClass(element, attrs.modalInClass); } scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { var done = setIsAsync(); if ($animateCss) { $animateCss(element, { removeClass: attrs.modalInClass }).start().then(done); } else { $animate.removeClass(element, attrs.modalInClass).then(done); } }); } } }]) .directive('uibModalWindow', [ '$uibModalStack', '$q', '$animate', '$injector', function($modalStack , $q , $animate, $injector) { var $animateCss = null; if ($injector.has('$animateCss')) { $animateCss = $injector.get('$animateCss'); } return { scope: { index: '@' }, replace: true, transclude: true, templateUrl: function(tElement, tAttrs) { return tAttrs.templateUrl || 'template/modal/window.html'; }, link: function(scope, element, attrs) { element.addClass(attrs.windowClass || ''); element.addClass(attrs.windowTopClass || ''); scope.size = attrs.size; scope.close = function(evt) { var modal = $modalStack.getTop(); if (modal && modal.value.backdrop && modal.value.backdrop !== 'static' && (evt.target === evt.currentTarget)) { evt.preventDefault(); evt.stopPropagation(); $modalStack.dismiss(modal.key, 'backdrop click'); } }; // moved from template to fix issue #2280 element.on('click', scope.close); // This property is only added to the scope for the purpose of detecting when this directive is rendered. // We can detect that by using this property in the template associated with this directive and then use // {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}. scope.$isRendered = true; // Deferred object that will be resolved when this modal is render. var modalRenderDeferObj = $q.defer(); // Observe function will be called on next digest cycle after compilation, ensuring that the DOM is ready. // In order to use this way of finding whether DOM is ready, we need to observe a scope property used in modal's template. attrs.$observe('modalRender', function(value) { if (value == 'true') { modalRenderDeferObj.resolve(); } }); modalRenderDeferObj.promise.then(function() { var animationPromise = null; if (attrs.modalInClass) { if ($animateCss) { animationPromise = $animateCss(element, { addClass: attrs.modalInClass }).start(); } else { animationPromise = $animate.addClass(element, attrs.modalInClass); } scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { var done = setIsAsync(); if ($animateCss) { $animateCss(element, { removeClass: attrs.modalInClass }).start().then(done); } else { $animate.removeClass(element, attrs.modalInClass).then(done); } }); } $q.when(animationPromise).then(function() { var inputWithAutofocus = element[0].querySelector('[autofocus]'); /** * Auto-focusing of a freshly-opened modal element causes any child elements * with the autofocus attribute to lose focus. This is an issue on touch * based devices which will show and then hide the onscreen keyboard. * Attempts to refocus the autofocus element via JavaScript will not reopen * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus * the modal element if the modal does not contain an autofocus element. */ if (inputWithAutofocus) { inputWithAutofocus.focus(); } else { element[0].focus(); } }); // Notify {@link $modalStack} that modal is rendered. var modal = $modalStack.getTop(); if (modal) { $modalStack.modalRendered(modal.key); } }); } }; }]) .directive('uibModalAnimationClass', function() { return { compile: function(tElement, tAttrs) { if (tAttrs.modalAnimation) { tElement.addClass(tAttrs.uibModalAnimationClass); } } }; }) .directive('uibModalTransclude', function() { return { link: function($scope, $element, $attrs, controller, $transclude) { $transclude($scope.$parent, function(clone) { $element.empty(); $element.append(clone); }); } }; }) .factory('$uibModalStack', [ '$animate', '$timeout', '$document', '$compile', '$rootScope', '$q', '$injector', '$$multiMap', '$$stackedMap', function($animate , $timeout , $document , $compile , $rootScope , $q, $injector, $$multiMap, $$stackedMap) { var $animateCss = null; if ($injector.has('$animateCss')) { $animateCss = $injector.get('$animateCss'); } var OPENED_MODAL_CLASS = 'modal-open'; var backdropDomEl, backdropScope; var openedWindows = $$stackedMap.createNew(); var openedClasses = $$multiMap.createNew(); var $modalStack = { NOW_CLOSING_EVENT: 'modal.stack.now-closing' }; //Modal focus behavior var focusableElementList; var focusIndex = 0; var tababbleSelector = 'a[href], area[href], input:not([disabled]), ' + 'button:not([disabled]),select:not([disabled]), textarea:not([disabled]), ' + 'iframe, object, embed, *[tabindex], *[contenteditable=true]'; 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, elementToReceiveFocus) { var body = $document.find('body').eq(0); var modalWindow = openedWindows.get(modalInstance).value; //clean up the stack openedWindows.remove(modalInstance); removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() { var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS; openedClasses.remove(modalBodyClass, modalInstance); body.toggleClass(modalBodyClass, openedClasses.hasKey(modalBodyClass)); toggleTopWindowClass(true); }); checkRemoveBackdrop(); //move focus to specified element if available, or else to body if (elementToReceiveFocus && elementToReceiveFocus.focus) { elementToReceiveFocus.focus(); } else { body.focus(); } } // Add or remove "windowTopClass" from the top window in the stack function toggleTopWindowClass(toggleSwitch) { var modalWindow; if (openedWindows.length() > 0) { modalWindow = openedWindows.top().value; modalWindow.modalDomEl.toggleClass(modalWindow.windowTopClass || '', toggleSwitch); } } function checkRemoveBackdrop() { //remove backdrop if no longer needed if (backdropDomEl && backdropIndex() == -1) { var backdropScopeRef = backdropScope; removeAfterAnimate(backdropDomEl, backdropScope, function() { backdropScopeRef = null; }); backdropDomEl = undefined; backdropScope = undefined; } } function removeAfterAnimate(domEl, scope, done) { var asyncDeferred; var asyncPromise = null; var setIsAsync = function() { if (!asyncDeferred) { asyncDeferred = $q.defer(); asyncPromise = asyncDeferred.promise; } return function asyncDone() { asyncDeferred.resolve(); }; }; scope.$broadcast($modalStack.NOW_CLOSING_EVENT, setIsAsync); // Note that it's intentional that asyncPromise might be null. // That's when setIsAsync has not been called during the // NOW_CLOSING_EVENT broadcast. return $q.when(asyncPromise).then(afterAnimating); function afterAnimating() { if (afterAnimating.done) { return; } afterAnimating.done = true; if ($animateCss) { $animateCss(domEl, { event: 'leave' }).start().then(function() { domEl.remove(); }); } else { $animate.leave(domEl); } scope.$destroy(); if (done) { done(); } } } $document.bind('keydown', function(evt) { if (evt.isDefaultPrevented()) { return evt; } var modal = openedWindows.top(); if (modal && modal.value.keyboard) { switch (evt.which) { case 27: { evt.preventDefault(); $rootScope.$apply(function() { $modalStack.dismiss(modal.key, 'escape key press'); }); break; } case 9: { $modalStack.loadFocusElementList(modal); var focusChanged = false; if (evt.shiftKey) { if ($modalStack.isFocusInFirstItem(evt)) { focusChanged = $modalStack.focusLastFocusableElement(); } } else { if ($modalStack.isFocusInLastItem(evt)) { focusChanged = $modalStack.focusFirstFocusableElement(); } } if (focusChanged) { evt.preventDefault(); evt.stopPropagation(); } break; } } } }); $modalStack.open = function(modalInstance, modal) { var modalOpener = $document[0].activeElement, modalBodyClass = modal.openedClass || OPENED_MODAL_CLASS; toggleTopWindowClass(false); openedWindows.add(modalInstance, { deferred: modal.deferred, renderDeferred: modal.renderDeferred, modalScope: modal.scope, backdrop: modal.backdrop, keyboard: modal.keyboard, openedClass: modal.openedClass, windowTopClass: modal.windowTopClass }); openedClasses.put(modalBodyClass, modalInstance); var body = $document.find('body').eq(0), currBackdropIndex = backdropIndex(); if (currBackdropIndex >= 0 && !backdropDomEl) { backdropScope = $rootScope.$new(true); backdropScope.index = currBackdropIndex; var angularBackgroundDomEl = angular.element('
'); angularBackgroundDomEl.attr('backdrop-class', modal.backdropClass); if (modal.animation) { angularBackgroundDomEl.attr('modal-animation', 'true'); } backdropDomEl = $compile(angularBackgroundDomEl)(backdropScope); body.append(backdropDomEl); } var angularDomEl = angular.element('
'); angularDomEl.attr({ 'template-url': modal.windowTemplateUrl, 'window-class': modal.windowClass, 'window-top-class': modal.windowTopClass, 'size': modal.size, 'index': openedWindows.length() - 1, 'animate': 'animate' }).html(modal.content); if (modal.animation) { angularDomEl.attr('modal-animation', 'true'); } var modalDomEl = $compile(angularDomEl)(modal.scope); openedWindows.top().value.modalDomEl = modalDomEl; openedWindows.top().value.modalOpener = modalOpener; body.append(modalDomEl); body.addClass(modalBodyClass); $modalStack.clearFocusListCache(); }; function broadcastClosing(modalWindow, resultOrReason, closing) { return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented; } $modalStack.close = function(modalInstance, result) { var modalWindow = openedWindows.get(modalInstance); if (modalWindow && broadcastClosing(modalWindow, result, true)) { modalWindow.value.modalScope.$$uibDestructionScheduled = true; modalWindow.value.deferred.resolve(result); removeModalWindow(modalInstance, modalWindow.value.modalOpener); return true; } return !modalWindow; }; $modalStack.dismiss = function(modalInstance, reason) { var modalWindow = openedWindows.get(modalInstance); if (modalWindow && broadcastClosing(modalWindow, reason, false)) { modalWindow.value.modalScope.$$uibDestructionScheduled = true; modalWindow.value.deferred.reject(reason); removeModalWindow(modalInstance, modalWindow.value.modalOpener); 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(); }; $modalStack.modalRendered = function(modalInstance) { var modalWindow = openedWindows.get(modalInstance); if (modalWindow) { modalWindow.value.renderDeferred.resolve(); } }; $modalStack.focusFirstFocusableElement = function() { if (focusableElementList.length > 0) { focusableElementList[0].focus(); return true; } return false; }; $modalStack.focusLastFocusableElement = function() { if (focusableElementList.length > 0) { focusableElementList[focusableElementList.length - 1].focus(); return true; } return false; }; $modalStack.isFocusInFirstItem = function(evt) { if (focusableElementList.length > 0) { return (evt.target || evt.srcElement) == focusableElementList[0]; } return false; }; $modalStack.isFocusInLastItem = function(evt) { if (focusableElementList.length > 0) { return (evt.target || evt.srcElement) == focusableElementList[focusableElementList.length - 1]; } return false; }; $modalStack.clearFocusListCache = function() { focusableElementList = []; focusIndex = 0; }; $modalStack.loadFocusElementList = function(modalWindow) { if (focusableElementList === undefined || !focusableElementList.length) { if (modalWindow) { var modalDomE1 = modalWindow.value.modalDomEl; if (modalDomE1 && modalDomE1.length) { focusableElementList = modalDomE1[0].querySelectorAll(tababbleSelector); } } } }; return $modalStack; }]) .provider('$uibModal', function() { var $modalProvider = { options: { animation: true, backdrop: true, //can also be false or 'static' keyboard: true }, $get: ['$injector', '$rootScope', '$q', '$templateRequest', '$controller', '$uibModalStack', '$modalSuppressWarning', '$log', function ($injector, $rootScope, $q, $templateRequest, $controller, $modalStack, $modalSuppressWarning, $log) { var $modal = {}; function getTemplatePromise(options) { return options.template ? $q.when(options.template) : $templateRequest(angular.isFunction(options.templateUrl) ? (options.templateUrl)() : options.templateUrl); } function getResolvePromises(resolves) { var promisesArr = []; angular.forEach(resolves, function(value) { if (angular.isFunction(value) || angular.isArray(value)) { promisesArr.push($q.when($injector.invoke(value))); } else if (angular.isString(value)) { promisesArr.push($q.when($injector.get(value))); } else { promisesArr.push($q.when(value)); } }); return promisesArr; } var promiseChain = null; $modal.getPromiseChain = function() { return promiseChain; }; $modal.open = function(modalOptions) { var modalResultDeferred = $q.defer(); var modalOpenedDeferred = $q.defer(); var modalRenderDeferred = $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, rendered: modalRenderDeferred.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))); function resolveWithTemplate() { return templateAndResolvePromise; } // Wait for the resolution of the existing promise chain. // Then switch to our own combined promise dependency (regardless of how the previous modal fared). // Then add to $modalStack and resolve opened. // Finally clean up the chain variable if no subsequent modal has overwritten it. var samePromise; samePromise = promiseChain = $q.all([promiseChain]) .then(resolveWithTemplate, resolveWithTemplate) .then(function resolveSuccess(tplAndVars) { var modalScope = (modalOptions.scope || $rootScope).$new(); modalScope.$close = modalInstance.close; modalScope.$dismiss = modalInstance.dismiss; modalScope.$on('$destroy', function() { if (!modalScope.$$uibDestructionScheduled) { modalScope.$dismiss('$uibUnscheduledDestruction'); } }); var ctrlInstance, ctrlLocals = {}; var resolveIter = 1; //controllers if (modalOptions.controller) { ctrlLocals.$scope = modalScope; ctrlLocals.$uibModalInstance = modalInstance; Object.defineProperty(ctrlLocals, '$modalInstance', { get: function() { if (!$modalSuppressWarning) { $log.warn('$modalInstance is now deprecated. Use $uibModalInstance instead.'); } return modalInstance; } }); angular.forEach(modalOptions.resolve, function(value, key) { ctrlLocals[key] = tplAndVars[resolveIter++]; }); ctrlInstance = $controller(modalOptions.controller, ctrlLocals); if (modalOptions.controllerAs) { if (modalOptions.bindToController) { angular.extend(ctrlInstance, modalScope); } modalScope[modalOptions.controllerAs] = ctrlInstance; } } $modalStack.open(modalInstance, { scope: modalScope, deferred: modalResultDeferred, renderDeferred: modalRenderDeferred, content: tplAndVars[0], animation: modalOptions.animation, backdrop: modalOptions.backdrop, keyboard: modalOptions.keyboard, backdropClass: modalOptions.backdropClass, windowTopClass: modalOptions.windowTopClass, windowClass: modalOptions.windowClass, windowTemplateUrl: modalOptions.windowTemplateUrl, size: modalOptions.size, openedClass: modalOptions.openedClass }); modalOpenedDeferred.resolve(true); }, function resolveError(reason) { modalOpenedDeferred.reject(reason); modalResultDeferred.reject(reason); }) .finally(function() { if (promiseChain === samePromise) { promiseChain = null; } }); return modalInstance; }; return $modal; } ] }; return $modalProvider; }); /* deprecated modal below */ angular.module('ui.bootstrap.modal') .value('$modalSuppressWarning', false) /** * A helper directive for the $modal service. It creates a backdrop element. */ .directive('modalBackdrop', [ '$animate', '$injector', '$modalStack', '$log', '$modalSuppressWarning', function($animate , $injector, $modalStack, $log, $modalSuppressWarning) { var $animateCss = null; if ($injector.has('$animateCss')) { $animateCss = $injector.get('$animateCss'); } return { replace: true, templateUrl: 'template/modal/backdrop.html', compile: function(tElement, tAttrs) { tElement.addClass(tAttrs.backdropClass); return linkFn; } }; function linkFn(scope, element, attrs) { if (!$modalSuppressWarning) { $log.warn('modal-backdrop is now deprecated. Use uib-modal-backdrop instead.'); } element.addClass('modal-backdrop'); if (attrs.modalInClass) { if ($animateCss) { $animateCss(element, { addClass: attrs.modalInClass }).start(); } else { $animate.addClass(element, attrs.modalInClass); } scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { var done = setIsAsync(); if ($animateCss) { $animateCss(element, { removeClass: attrs.modalInClass }).start().then(done); } else { $animate.removeClass(element, attrs.modalInClass).then(done); } }); } } }]) .directive('modalWindow', [ '$modalStack', '$q', '$animate', '$injector', '$log', '$modalSuppressWarning', function($modalStack , $q , $animate, $injector, $log, $modalSuppressWarning) { var $animateCss = null; if ($injector.has('$animateCss')) { $animateCss = $injector.get('$animateCss'); } return { scope: { index: '@' }, replace: true, transclude: true, templateUrl: function(tElement, tAttrs) { return tAttrs.templateUrl || 'template/modal/window.html'; }, link: function(scope, element, attrs) { if (!$modalSuppressWarning) { $log.warn('modal-window is now deprecated. Use uib-modal-window instead.'); } element.addClass(attrs.windowClass || ''); element.addClass(attrs.windowTopClass || ''); scope.size = attrs.size; scope.close = function(evt) { var modal = $modalStack.getTop(); if (modal && modal.value.backdrop && modal.value.backdrop !== 'static' && (evt.target === evt.currentTarget)) { evt.preventDefault(); evt.stopPropagation(); $modalStack.dismiss(modal.key, 'backdrop click'); } }; // moved from template to fix issue #2280 element.on('click', scope.close); // This property is only added to the scope for the purpose of detecting when this directive is rendered. // We can detect that by using this property in the template associated with this directive and then use // {@link Attribute#$observe} on it. For more details please see {@link TableColumnResize}. scope.$isRendered = true; // Deferred object that will be resolved when this modal is render. var modalRenderDeferObj = $q.defer(); // Observe function will be called on next digest cycle after compilation, ensuring that the DOM is ready. // In order to use this way of finding whether DOM is ready, we need to observe a scope property used in modal's template. attrs.$observe('modalRender', function(value) { if (value == 'true') { modalRenderDeferObj.resolve(); } }); modalRenderDeferObj.promise.then(function() { var animationPromise = null; if (attrs.modalInClass) { if ($animateCss) { animationPromise = $animateCss(element, { addClass: attrs.modalInClass }).start(); } else { animationPromise = $animate.addClass(element, attrs.modalInClass); } scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { var done = setIsAsync(); if ($animateCss) { $animateCss(element, { removeClass: attrs.modalInClass }).start().then(done); } else { $animate.removeClass(element, attrs.modalInClass).then(done); } }); } $q.when(animationPromise).then(function() { var inputWithAutofocus = element[0].querySelector('[autofocus]'); /** * Auto-focusing of a freshly-opened modal element causes any child elements * with the autofocus attribute to lose focus. This is an issue on touch * based devices which will show and then hide the onscreen keyboard. * Attempts to refocus the autofocus element via JavaScript will not reopen * the onscreen keyboard. Fixed by updated the focusing logic to only autofocus * the modal element if the modal does not contain an autofocus element. */ if (inputWithAutofocus) { inputWithAutofocus.focus(); } else { element[0].focus(); } }); // Notify {@link $modalStack} that modal is rendered. var modal = $modalStack.getTop(); if (modal) { $modalStack.modalRendered(modal.key); } }); } }; }]) .directive('modalAnimationClass', [ '$log', '$modalSuppressWarning', function ($log, $modalSuppressWarning) { return { compile: function(tElement, tAttrs) { if (!$modalSuppressWarning) { $log.warn('modal-animation-class is now deprecated. Use uib-modal-animation-class instead.'); } if (tAttrs.modalAnimation) { tElement.addClass(tAttrs.modalAnimationClass); } } }; }]) .directive('modalTransclude', [ '$log', '$modalSuppressWarning', function ($log, $modalSuppressWarning) { return { link: function($scope, $element, $attrs, controller, $transclude) { if (!$modalSuppressWarning) { $log.warn('modal-transclude is now deprecated. Use uib-modal-transclude instead.'); } $transclude($scope.$parent, function(clone) { $element.empty(); $element.append(clone); }); } }; }]) .service('$modalStack', [ '$animate', '$timeout', '$document', '$compile', '$rootScope', '$q', '$injector', '$$multiMap', '$$stackedMap', '$uibModalStack', '$log', '$modalSuppressWarning', function($animate , $timeout , $document , $compile , $rootScope , $q, $injector, $$multiMap, $$stackedMap, $uibModalStack, $log, $modalSuppressWarning) { if (!$modalSuppressWarning) { $log.warn('$modalStack is now deprecated. Use $uibModalStack instead.'); } angular.extend(this, $uibModalStack); }]) .provider('$modal', ['$uibModalProvider', function($uibModalProvider) { angular.extend(this, $uibModalProvider); this.$get = ['$injector', '$log', '$modalSuppressWarning', function ($injector, $log, $modalSuppressWarning) { if (!$modalSuppressWarning) { $log.warn('$modal is now deprecated. Use $uibModal instead.'); } return $injector.invoke($uibModalProvider.$get); }]; }]);