UNPKG

angular-ui-bootstrap

Version:

Native AngularJS (Angular) directives for Bootstrap

831 lines (716 loc) 30.5 kB
angular.module('ui.bootstrap.modal', ['ui.bootstrap.multiMap', 'ui.bootstrap.stackedMap', 'ui.bootstrap.position']) /** * Pluggable resolve mechanism for the modal resolve resolution * Supports UI Router's $resolve service */ .provider('$uibResolve', function() { var resolve = this; this.resolver = null; this.setResolver = function(resolver) { this.resolver = resolver; }; this.$get = ['$injector', '$q', function($injector, $q) { var resolver = resolve.resolver ? $injector.get(resolve.resolver) : null; return { resolve: function(invocables, locals, parent, self) { if (resolver) { return resolver.resolve(invocables, locals, parent, self); } var promises = []; angular.forEach(invocables, function(value) { if (angular.isFunction(value) || angular.isArray(value)) { promises.push($q.resolve($injector.invoke(value))); } else if (angular.isString(value)) { promises.push($q.resolve($injector.get(value))); } else { promises.push($q.resolve(value)); } }); return $q.all(promises).then(function(resolves) { var resolveObj = {}; var resolveIter = 0; angular.forEach(invocables, function(value, key) { resolveObj[key] = resolves[resolveIter++]; }); return resolveObj; }); } }; }]; }) /** * A helper directive for the $modal service. It creates a backdrop element. */ .directive('uibModalBackdrop', ['$animate', '$injector', '$uibModalStack', function($animate, $injector, $modalStack) { return { restrict: 'A', compile: function(tElement, tAttrs) { tElement.addClass(tAttrs.backdropClass); return linkFn; } }; function linkFn(scope, element, attrs) { if (attrs.modalInClass) { $animate.addClass(element, attrs.modalInClass); scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { var done = setIsAsync(); if (scope.modalOptions.animation) { $animate.removeClass(element, attrs.modalInClass).then(done); } else { done(); } }); } } }]) .directive('uibModalWindow', ['$uibModalStack', '$q', '$animateCss', '$document', function($modalStack, $q, $animateCss, $document) { return { scope: { index: '@' }, restrict: 'A', transclude: true, templateUrl: function(tElement, tAttrs) { return tAttrs.templateUrl || 'uib/template/modal/window.html'; }, link: function(scope, element, attrs) { 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 rendered. var modalRenderDeferObj = $q.defer(); // Resolve render promise post-digest scope.$$postDigest(function() { modalRenderDeferObj.resolve(); }); modalRenderDeferObj.promise.then(function() { var animationPromise = null; if (attrs.modalInClass) { animationPromise = $animateCss(element, { addClass: attrs.modalInClass }).start(); scope.$on($modalStack.NOW_CLOSING_EVENT, function(e, setIsAsync) { var done = setIsAsync(); $animateCss(element, { removeClass: attrs.modalInClass }).start().then(done); }); } $q.when(animationPromise).then(function() { // Notify {@link $modalStack} that modal is rendered. var modal = $modalStack.getTop(); if (modal) { $modalStack.modalRendered(modal.key); } /** * If something within the freshly-opened modal already has focus (perhaps via a * directive that causes focus) then there's no need to try to focus anything. */ if (!($document[0].activeElement && element[0].contains($document[0].activeElement))) { 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(); } } }); }); } }; }]) .directive('uibModalAnimationClass', function() { return { compile: function(tElement, tAttrs) { if (tAttrs.modalAnimation) { tElement.addClass(tAttrs.uibModalAnimationClass); } } }; }) .directive('uibModalTransclude', ['$animate', function($animate) { return { link: function(scope, element, attrs, controller, transclude) { transclude(scope.$parent, function(clone) { element.empty(); $animate.enter(clone, element); }); } }; }]) .factory('$uibModalStack', ['$animate', '$animateCss', '$document', '$compile', '$rootScope', '$q', '$$multiMap', '$$stackedMap', '$uibPosition', function($animate, $animateCss, $document, $compile, $rootScope, $q, $$multiMap, $$stackedMap, $uibPosition) { 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' }; var topModalIndex = 0; var previousTopOpenedModal = null; var ARIA_HIDDEN_ATTRIBUTE_NAME = 'data-bootstrap-modal-aria-hidden-count'; //Modal focus behavior var tabbableSelector = 'a[href], area[href], input:not([disabled]):not([tabindex=\'-1\']), ' + 'button:not([disabled]):not([tabindex=\'-1\']),select:not([disabled]):not([tabindex=\'-1\']), textarea:not([disabled]):not([tabindex=\'-1\']), ' + 'iframe, object, embed, *[tabindex]:not([tabindex=\'-1\']), *[contenteditable=true]'; var scrollbarPadding; var SNAKE_CASE_REGEXP = /[A-Z]/g; // TODO: extract into common dependency with tooltip function snake_case(name) { var separator = '-'; return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { return (pos ? separator : '') + letter.toLowerCase(); }); } function isVisible(element) { return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length); } 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; } } // If any backdrop exist, ensure that it's index is always // right below the top modal if (topBackdropIndex > -1 && topBackdropIndex < topModalIndex) { topBackdropIndex = topModalIndex; } return topBackdropIndex; } $rootScope.$watch(backdropIndex, function(newBackdropIndex) { if (backdropScope) { backdropScope.index = newBackdropIndex; } }); function removeModalWindow(modalInstance, elementToReceiveFocus) { var modalWindow = openedWindows.get(modalInstance).value; var appendToElement = modalWindow.appendTo; //clean up the stack openedWindows.remove(modalInstance); previousTopOpenedModal = openedWindows.top(); if (previousTopOpenedModal) { topModalIndex = parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10); } removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() { var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS; openedClasses.remove(modalBodyClass, modalInstance); var areAnyOpen = openedClasses.hasKey(modalBodyClass); appendToElement.toggleClass(modalBodyClass, areAnyOpen); if (!areAnyOpen && scrollbarPadding && scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) { if (scrollbarPadding.originalRight) { appendToElement.css({paddingRight: scrollbarPadding.originalRight + 'px'}); } else { appendToElement.css({paddingRight: ''}); } scrollbarPadding = null; } toggleTopWindowClass(true); }, modalWindow.closedDeferred); checkRemoveBackdrop(); //move focus to specified element if available, or else to body if (elementToReceiveFocus && elementToReceiveFocus.focus) { elementToReceiveFocus.focus(); } else if (appendToElement.focus) { appendToElement.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, closedDeferred) { 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; $animate.leave(domEl).then(function() { if (done) { done(); } domEl.remove(); if (closedDeferred) { closedDeferred.resolve(); } }); scope.$destroy(); } } $document.on('keydown', keydownListener); $rootScope.$on('$destroy', function() { $document.off('keydown', keydownListener); }); function keydownListener(evt) { if (evt.isDefaultPrevented()) { return evt; } var modal = openedWindows.top(); if (modal) { switch (evt.which) { case 27: { if (modal.value.keyboard) { evt.preventDefault(); $rootScope.$apply(function() { $modalStack.dismiss(modal.key, 'escape key press'); }); } break; } case 9: { var list = $modalStack.loadFocusElementList(modal); var focusChanged = false; if (evt.shiftKey) { if ($modalStack.isFocusInFirstItem(evt, list) || $modalStack.isModalFocused(evt, modal)) { focusChanged = $modalStack.focusLastFocusableElement(list); } } else { if ($modalStack.isFocusInLastItem(evt, list)) { focusChanged = $modalStack.focusFirstFocusableElement(list); } } 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); // Store the current top first, to determine what index we ought to use // for the current top modal previousTopOpenedModal = openedWindows.top(); openedWindows.add(modalInstance, { deferred: modal.deferred, renderDeferred: modal.renderDeferred, closedDeferred: modal.closedDeferred, modalScope: modal.scope, backdrop: modal.backdrop, keyboard: modal.keyboard, openedClass: modal.openedClass, windowTopClass: modal.windowTopClass, animation: modal.animation, appendTo: modal.appendTo }); openedClasses.put(modalBodyClass, modalInstance); var appendToElement = modal.appendTo, currBackdropIndex = backdropIndex(); if (currBackdropIndex >= 0 && !backdropDomEl) { backdropScope = $rootScope.$new(true); backdropScope.modalOptions = modal; backdropScope.index = currBackdropIndex; backdropDomEl = angular.element('<div uib-modal-backdrop="modal-backdrop"></div>'); backdropDomEl.attr({ 'class': 'modal-backdrop', 'ng-style': '{\'z-index\': 1040 + (index && 1 || 0) + index*10}', 'uib-modal-animation-class': 'fade', 'modal-in-class': 'in' }); if (modal.backdropClass) { backdropDomEl.addClass(modal.backdropClass); } if (modal.animation) { backdropDomEl.attr('modal-animation', 'true'); } $compile(backdropDomEl)(backdropScope); $animate.enter(backdropDomEl, appendToElement); if ($uibPosition.isScrollable(appendToElement)) { scrollbarPadding = $uibPosition.scrollbarPadding(appendToElement); if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) { appendToElement.css({paddingRight: scrollbarPadding.right + 'px'}); } } } var content; if (modal.component) { content = document.createElement(snake_case(modal.component.name)); content = angular.element(content); content.attr({ resolve: '$resolve', 'modal-instance': '$uibModalInstance', close: '$close($value)', dismiss: '$dismiss($value)' }); } else { content = modal.content; } // Set the top modal index based on the index of the previous top modal topModalIndex = previousTopOpenedModal ? parseInt(previousTopOpenedModal.value.modalDomEl.attr('index'), 10) + 1 : 0; var angularDomEl = angular.element('<div uib-modal-window="modal-window"></div>'); angularDomEl.attr({ 'class': 'modal', 'template-url': modal.windowTemplateUrl, 'window-top-class': modal.windowTopClass, 'role': 'dialog', 'aria-labelledby': modal.ariaLabelledBy, 'aria-describedby': modal.ariaDescribedBy, 'size': modal.size, 'index': topModalIndex, 'animate': 'animate', 'ng-style': '{\'z-index\': 1050 + $$topModalIndex*10, display: \'block\'}', 'tabindex': -1, 'uib-modal-animation-class': 'fade', 'modal-in-class': 'in' }).append(content); if (modal.windowClass) { angularDomEl.addClass(modal.windowClass); } if (modal.animation) { angularDomEl.attr('modal-animation', 'true'); } appendToElement.addClass(modalBodyClass); if (modal.scope) { // we need to explicitly add the modal index to the modal scope // because it is needed by ngStyle to compute the zIndex property. modal.scope.$$topModalIndex = topModalIndex; } $animate.enter($compile(angularDomEl)(modal.scope), appendToElement); openedWindows.top().value.modalDomEl = angularDomEl; openedWindows.top().value.modalOpener = modalOpener; applyAriaHidden(angularDomEl); function applyAriaHidden(el) { if (!el || el[0].tagName === 'BODY') { return; } getSiblings(el).forEach(function(sibling) { var elemIsAlreadyHidden = sibling.getAttribute('aria-hidden') === 'true', ariaHiddenCount = parseInt(sibling.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10); if (!ariaHiddenCount) { ariaHiddenCount = elemIsAlreadyHidden ? 1 : 0; } sibling.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, ariaHiddenCount + 1); sibling.setAttribute('aria-hidden', 'true'); }); return applyAriaHidden(el.parent()); function getSiblings(el) { var children = el.parent() ? el.parent().children() : []; return Array.prototype.filter.call(children, function(child) { return child !== el[0]; }); } } }; function broadcastClosing(modalWindow, resultOrReason, closing) { return !modalWindow.value.modalScope.$broadcast('modal.closing', resultOrReason, closing).defaultPrevented; } function unhideBackgroundElements() { Array.prototype.forEach.call( document.querySelectorAll('[' + ARIA_HIDDEN_ATTRIBUTE_NAME + ']'), function(hiddenEl) { var ariaHiddenCount = parseInt(hiddenEl.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10), newHiddenCount = ariaHiddenCount - 1; hiddenEl.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, newHiddenCount); if (!newHiddenCount) { hiddenEl.removeAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME); hiddenEl.removeAttribute('aria-hidden'); } } ); } $modalStack.close = function(modalInstance, result) { var modalWindow = openedWindows.get(modalInstance); unhideBackgroundElements(); 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); unhideBackgroundElements(); 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(list) { if (list.length > 0) { list[0].focus(); return true; } return false; }; $modalStack.focusLastFocusableElement = function(list) { if (list.length > 0) { list[list.length - 1].focus(); return true; } return false; }; $modalStack.isModalFocused = function(evt, modalWindow) { if (evt && modalWindow) { var modalDomEl = modalWindow.value.modalDomEl; if (modalDomEl && modalDomEl.length) { return (evt.target || evt.srcElement) === modalDomEl[0]; } } return false; }; $modalStack.isFocusInFirstItem = function(evt, list) { if (list.length > 0) { return (evt.target || evt.srcElement) === list[0]; } return false; }; $modalStack.isFocusInLastItem = function(evt, list) { if (list.length > 0) { return (evt.target || evt.srcElement) === list[list.length - 1]; } return false; }; $modalStack.loadFocusElementList = function(modalWindow) { if (modalWindow) { var modalDomE1 = modalWindow.value.modalDomEl; if (modalDomE1 && modalDomE1.length) { var elements = modalDomE1[0].querySelectorAll(tabbableSelector); return elements ? Array.prototype.filter.call(elements, function(element) { return isVisible(element); }) : elements; } } }; return $modalStack; }]) .provider('$uibModal', function() { var $modalProvider = { options: { animation: true, backdrop: true, //can also be false or 'static' keyboard: true }, $get: ['$rootScope', '$q', '$document', '$templateRequest', '$controller', '$uibResolve', '$uibModalStack', function ($rootScope, $q, $document, $templateRequest, $controller, $uibResolve, $modalStack) { var $modal = {}; function getTemplatePromise(options) { return options.template ? $q.when(options.template) : $templateRequest(angular.isFunction(options.templateUrl) ? options.templateUrl() : options.templateUrl); } var promiseChain = null; $modal.getPromiseChain = function() { return promiseChain; }; $modal.open = function(modalOptions) { var modalResultDeferred = $q.defer(); var modalOpenedDeferred = $q.defer(); var modalClosedDeferred = $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, closed: modalClosedDeferred.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 || {}; modalOptions.appendTo = modalOptions.appendTo || $document.find('body').eq(0); if (!modalOptions.appendTo.length) { throw new Error('appendTo element not found. Make sure that the element passed is in DOM.'); } //verify options if (!modalOptions.component && !modalOptions.template && !modalOptions.templateUrl) { throw new Error('One of component or template or templateUrl options is required.'); } var templateAndResolvePromise; if (modalOptions.component) { templateAndResolvePromise = $q.when($uibResolve.resolve(modalOptions.resolve, {}, null, null)); } else { templateAndResolvePromise = $q.all([getTemplatePromise(modalOptions), $uibResolve.resolve(modalOptions.resolve, {}, null, null)]); } 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 providedScope = modalOptions.scope || $rootScope; var modalScope = providedScope.$new(); modalScope.$close = modalInstance.close; modalScope.$dismiss = modalInstance.dismiss; modalScope.$on('$destroy', function() { if (!modalScope.$$uibDestructionScheduled) { modalScope.$dismiss('$uibUnscheduledDestruction'); } }); var modal = { scope: modalScope, deferred: modalResultDeferred, renderDeferred: modalRenderDeferred, closedDeferred: modalClosedDeferred, animation: modalOptions.animation, backdrop: modalOptions.backdrop, keyboard: modalOptions.keyboard, backdropClass: modalOptions.backdropClass, windowTopClass: modalOptions.windowTopClass, windowClass: modalOptions.windowClass, windowTemplateUrl: modalOptions.windowTemplateUrl, ariaLabelledBy: modalOptions.ariaLabelledBy, ariaDescribedBy: modalOptions.ariaDescribedBy, size: modalOptions.size, openedClass: modalOptions.openedClass, appendTo: modalOptions.appendTo }; var component = {}; var ctrlInstance, ctrlInstantiate, ctrlLocals = {}; if (modalOptions.component) { constructLocals(component, false, true, false); component.name = modalOptions.component; modal.component = component; } else if (modalOptions.controller) { constructLocals(ctrlLocals, true, false, true); // the third param will make the controller instantiate later,private api // @see https://github.com/angular/angular.js/blob/master/src/ng/controller.js#L126 ctrlInstantiate = $controller(modalOptions.controller, ctrlLocals, true, modalOptions.controllerAs); if (modalOptions.controllerAs && modalOptions.bindToController) { ctrlInstance = ctrlInstantiate.instance; ctrlInstance.$close = modalScope.$close; ctrlInstance.$dismiss = modalScope.$dismiss; angular.extend(ctrlInstance, { $resolve: ctrlLocals.$scope.$resolve }, providedScope); } ctrlInstance = ctrlInstantiate(); if (angular.isFunction(ctrlInstance.$onInit)) { ctrlInstance.$onInit(); } } if (!modalOptions.component) { modal.content = tplAndVars[0]; } $modalStack.open(modalInstance, modal); modalOpenedDeferred.resolve(true); function constructLocals(obj, template, instanceOnScope, injectable) { obj.$scope = modalScope; obj.$scope.$resolve = {}; if (instanceOnScope) { obj.$scope.$uibModalInstance = modalInstance; } else { obj.$uibModalInstance = modalInstance; } var resolves = template ? tplAndVars[1] : tplAndVars; angular.forEach(resolves, function(value, key) { if (injectable) { obj[key] = value; } obj.$scope.$resolve[key] = value; }); } }, function resolveError(reason) { modalOpenedDeferred.reject(reason); modalResultDeferred.reject(reason); })['finally'](function() { if (promiseChain === samePromise) { promiseChain = null; } }); return modalInstance; }; return $modal; } ] }; return $modalProvider; });