2 * ngDialog - easy modals and popup windows
3 * http://github.com/likeastore/ngDialog
4 * (c) 2013-2015 MIT License, https://likeastore.com
7 (function (root, factory) {
8 if (typeof module !== 'undefined' && module.exports) {
10 if (typeof angular === 'undefined') {
11 factory(require('angular'));
15 module.exports = 'ngDialog';
16 } else if (typeof define === 'function' && define.amd) {
18 define(['angular'], factory);
21 factory(root.angular);
23 }(this, function (angular) {
26 var m = angular.module('ngDialog', []);
28 var $el = angular.element;
29 var isDef = angular.isDefined;
30 var style = (document.body || document.documentElement).style;
31 var animationEndSupport = isDef(style.animation) || isDef(style.WebkitAnimation) || isDef(style.MozAnimation) || isDef(style.MsAnimation) || isDef(style.OAnimation);
32 var animationEndEvent = 'animationend webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend';
33 var focusableElementSelector = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]';
34 var disabledAnimationClass = 'ngdialog-disabled-animation';
35 var forceElementsReload = { html: false, body: false };
38 var keydownIsBound = false;
39 var openOnePerName = false;
42 m.provider('ngDialog', function () {
43 var defaults = this.defaults = {
44 className: 'ngdialog-theme-default',
46 disableAnimation: false,
49 closeByDocument: true,
51 closeByNavigation: false,
53 preCloseCallback: false,
60 ariaLabelledById: null,
61 ariaLabelledBySelector: null,
62 ariaDescribedById: null,
63 ariaDescribedBySelector: null,
64 bodyClassName: 'ngdialog-open',
69 this.setForceHtmlReload = function (_useIt) {
70 forceElementsReload.html = _useIt || false;
73 this.setForceBodyReload = function (_useIt) {
74 forceElementsReload.body = _useIt || false;
77 this.setDefaults = function (newDefaults) {
78 angular.extend(defaults, newDefaults);
81 this.setOpenOnePerName = function (isOpenOne) {
82 openOnePerName = isOpenOne || false;
85 var globalID = 0, dialogsCount = 0, closeByDocumentHandler, defers = {};
87 this.$get = ['$document', '$templateCache', '$compile', '$q', '$http', '$rootScope', '$timeout', '$window', '$controller', '$injector',
88 function ($document, $templateCache, $compile, $q, $http, $rootScope, $timeout, $window, $controller, $injector) {
91 var privateMethods = {
92 onDocumentKeydown: function (event) {
93 if (event.keyCode === 27) {
94 publicMethods.close('$escape');
98 activate: function($dialog) {
99 var options = $dialog.data('$ngDialogOptions');
101 if (options.trapFocus) {
102 $dialog.on('keydown', privateMethods.onTrapFocusKeydown);
104 // Catch rogue changes (eg. after unfocusing everything by clicking a non-focusable element)
105 $elements.body.on('keydown', privateMethods.onTrapFocusKeydown);
109 deactivate: function ($dialog) {
110 $dialog.off('keydown', privateMethods.onTrapFocusKeydown);
111 $elements.body.off('keydown', privateMethods.onTrapFocusKeydown);
114 deactivateAll: function (els) {
115 angular.forEach(els,function(el) {
116 var $dialog = angular.element(el);
117 privateMethods.deactivate($dialog);
121 setBodyPadding: function (width) {
122 var originalBodyPadding = parseInt(($elements.body.css('padding-right') || 0), 10);
123 $elements.body.css('padding-right', (originalBodyPadding + width) + 'px');
124 $elements.body.data('ng-dialog-original-padding', originalBodyPadding);
125 $rootScope.$broadcast('ngDialog.setPadding', width);
128 resetBodyPadding: function () {
129 var originalBodyPadding = $elements.body.data('ng-dialog-original-padding');
130 if (originalBodyPadding) {
131 $elements.body.css('padding-right', originalBodyPadding + 'px');
133 $elements.body.css('padding-right', '');
135 $rootScope.$broadcast('ngDialog.setPadding', 0);
138 performCloseDialog: function ($dialog, value) {
139 var options = $dialog.data('$ngDialogOptions');
140 var id = $dialog.attr('id');
141 var scope = scopes[id];
148 if (typeof $window.Hammer !== 'undefined') {
149 var hammerTime = scope.hammerTime;
150 hammerTime.off('tap', closeByDocumentHandler);
151 hammerTime.destroy && hammerTime.destroy();
152 delete scope.hammerTime;
154 $dialog.unbind('click');
157 if (dialogsCount === 1) {
158 $elements.body.unbind('keydown', privateMethods.onDocumentKeydown);
161 if (!$dialog.hasClass('ngdialog-closing')){
165 var previousFocus = $dialog.data('$ngDialogPreviousFocus');
166 if (previousFocus && previousFocus.focus) {
167 previousFocus.focus();
170 $rootScope.$broadcast('ngDialog.closing', $dialog, value);
171 dialogsCount = dialogsCount < 0 ? 0 : dialogsCount;
172 if (animationEndSupport && !options.disableAnimation) {
174 $dialog.unbind(animationEndEvent).bind(animationEndEvent, function () {
175 privateMethods.closeDialogElement($dialog, value);
176 }).addClass('ngdialog-closing');
179 privateMethods.closeDialogElement($dialog, value);
186 remainingDialogs: dialogsCount
193 openIdStack.splice(openIdStack.indexOf(id), 1);
194 if (!openIdStack.length) {
195 $elements.body.unbind('keydown', privateMethods.onDocumentKeydown);
196 keydownIsBound = false;
200 closeDialogElement: function($dialog, value) {
201 var options = $dialog.data('$ngDialogOptions');
203 if (dialogsCount === 0) {
204 $elements.html.removeClass(options.bodyClassName);
205 $elements.body.removeClass(options.bodyClassName);
206 privateMethods.resetBodyPadding();
208 $rootScope.$broadcast('ngDialog.closed', $dialog, value);
211 closeDialog: function ($dialog, value) {
212 var preCloseCallback = $dialog.data('$ngDialogPreCloseCallback');
214 if (preCloseCallback && angular.isFunction(preCloseCallback)) {
216 var preCloseCallbackResult = preCloseCallback.call($dialog, value);
218 if (angular.isObject(preCloseCallbackResult)) {
219 if (preCloseCallbackResult.closePromise) {
220 preCloseCallbackResult.closePromise.then(function () {
221 privateMethods.performCloseDialog($dialog, value);
226 preCloseCallbackResult.then(function () {
227 privateMethods.performCloseDialog($dialog, value);
232 } else if (preCloseCallbackResult !== false) {
233 privateMethods.performCloseDialog($dialog, value);
238 privateMethods.performCloseDialog($dialog, value);
242 onTrapFocusKeydown: function(ev) {
243 var el = angular.element(ev.currentTarget);
246 if (el.hasClass('ngdialog')) {
249 $dialog = privateMethods.getActiveDialog();
251 if ($dialog === null) {
256 var isTab = (ev.keyCode === 9);
257 var backward = (ev.shiftKey === true);
260 privateMethods.handleTab($dialog, ev, backward);
264 handleTab: function($dialog, ev, backward) {
265 var focusableElements = privateMethods.getFocusableElements($dialog);
267 if (focusableElements.length === 0) {
268 if (document.activeElement && document.activeElement.blur) {
269 document.activeElement.blur();
274 var currentFocus = document.activeElement;
275 var focusIndex = Array.prototype.indexOf.call(focusableElements, currentFocus);
277 var isFocusIndexUnknown = (focusIndex === -1);
278 var isFirstElementFocused = (focusIndex === 0);
279 var isLastElementFocused = (focusIndex === focusableElements.length - 1);
281 var cancelEvent = false;
284 if (isFocusIndexUnknown || isFirstElementFocused) {
285 focusableElements[focusableElements.length - 1].focus();
289 if (isFocusIndexUnknown || isLastElementFocused) {
290 focusableElements[0].focus();
297 ev.stopPropagation();
301 autoFocus: function($dialog) {
302 var dialogEl = $dialog[0];
304 // Browser's (Chrome 40, Forefix 37, IE 11) don't appear to honor autofocus on the dialog, but we should
305 var autoFocusEl = dialogEl.querySelector('*[autofocus]');
306 if (autoFocusEl !== null) {
309 if (document.activeElement === autoFocusEl) {
313 // Autofocus element might was display: none, so let's continue
316 var focusableElements = privateMethods.getFocusableElements($dialog);
318 if (focusableElements.length > 0) {
319 focusableElements[0].focus();
323 // We need to focus something for the screen readers to notice the dialog
324 var contentElements = privateMethods.filterVisibleElements(dialogEl.querySelectorAll('h1,h2,h3,h4,h5,h6,p,span'));
326 if (contentElements.length > 0) {
327 var contentElement = contentElements[0];
328 $el(contentElement).attr('tabindex', '-1').css('outline', '0');
329 contentElement.focus();
333 getFocusableElements: function ($dialog) {
334 var dialogEl = $dialog[0];
336 var rawElements = dialogEl.querySelectorAll(focusableElementSelector);
338 // Ignore untabbable elements, ie. those with tabindex = -1
339 var tabbableElements = privateMethods.filterTabbableElements(rawElements);
341 return privateMethods.filterVisibleElements(tabbableElements);
344 filterTabbableElements: function (els) {
345 var tabbableFocusableElements = [];
347 for (var i = 0; i < els.length; i++) {
350 if ($el(el).attr('tabindex') !== '-1') {
351 tabbableFocusableElements.push(el);
355 return tabbableFocusableElements;
358 filterVisibleElements: function (els) {
359 var visibleFocusableElements = [];
361 for (var i = 0; i < els.length; i++) {
364 if (el.offsetWidth > 0 || el.offsetHeight > 0) {
365 visibleFocusableElements.push(el);
369 return visibleFocusableElements;
372 getActiveDialog: function () {
373 var dialogs = document.querySelectorAll('.ngdialog');
375 if (dialogs.length === 0) {
379 // TODO: This might be incorrect if there are a mix of open dialogs with different 'appendTo' values
380 return $el(dialogs[dialogs.length - 1]);
383 applyAriaAttributes: function ($dialog, options) {
384 if (options.ariaAuto) {
385 if (!options.ariaRole) {
386 var detectedRole = (privateMethods.getFocusableElements($dialog).length > 0) ?
390 options.ariaRole = detectedRole;
393 if (!options.ariaLabelledBySelector) {
394 options.ariaLabelledBySelector = 'h1,h2,h3,h4,h5,h6';
397 if (!options.ariaDescribedBySelector) {
398 options.ariaDescribedBySelector = 'article,section,p';
402 if (options.ariaRole) {
403 $dialog.attr('role', options.ariaRole);
406 privateMethods.applyAriaAttribute(
407 $dialog, 'aria-labelledby', options.ariaLabelledById, options.ariaLabelledBySelector);
409 privateMethods.applyAriaAttribute(
410 $dialog, 'aria-describedby', options.ariaDescribedById, options.ariaDescribedBySelector);
413 applyAriaAttribute: function($dialog, attr, id, selector) {
415 $dialog.attr(attr, id);
419 var dialogId = $dialog.attr('id');
421 var firstMatch = $dialog[0].querySelector(selector);
427 var generatedId = dialogId + '-' + attr;
429 $el(firstMatch).attr('id', generatedId);
431 $dialog.attr(attr, generatedId);
437 detectUIRouter: function() {
438 //Detect if ui-router module is installed if not return false
440 angular.module('ui.router');
447 getRouterLocationEventName: function() {
448 if(privateMethods.detectUIRouter()) {
449 return '$stateChangeStart';
451 return '$locationChangeStart';
455 var publicMethods = {
456 __PRIVATE__: privateMethods,
459 * @param {Object} options:
460 * - template {String} - id of ng-template, url for partial, plain string (if enabled)
461 * - plain {Boolean} - enable plain string templates, default false
463 * - controller {String}
464 * - controllerAs {String}
465 * - className {String} - dialog theme class
466 * - appendClassName {String} - dialog theme class to be appended to defaults
467 * - disableAnimation {Boolean} - set to true to disable animation
468 * - showClose {Boolean} - show close button, default true
469 * - closeByEscape {Boolean} - default true
470 * - closeByDocument {Boolean} - default true
471 * - preCloseCallback {String|Function} - user supplied function name/function called before closing dialog (if set)
472 * - bodyClassName {String} - class added to body at open dialog
473 * @return {Object} dialog
475 open: function (opts) {
478 if (openOnePerName && opts.name) {
479 dialogID = opts.name.toLowerCase().replace(/\s/g, '-') + '-dialog';
480 if (this.isOpen(dialogID)) {
484 var options = angular.copy(defaults);
485 var localID = ++globalID;
486 dialogID = dialogID || 'ngdialog' + localID;
487 openIdStack.push(dialogID);
489 // Merge opts.data with predefined via setDefaults
490 if (typeof options.data !== 'undefined') {
491 if (typeof opts.data === 'undefined') {
494 opts.data = angular.merge(angular.copy(options.data), opts.data);
497 angular.extend(options, opts);
500 defers[dialogID] = defer = $q.defer();
503 scopes[dialogID] = scope = angular.isObject(options.scope) ? options.scope.$new() : $rootScope.$new();
505 var $dialog, $dialogParent, $dialogContent;
507 var resolve = angular.extend({}, options.resolve);
509 angular.forEach(resolve, function (value, key) {
510 resolve[key] = angular.isString(value) ? $injector.get(value) : $injector.invoke(value, null, null, key);
514 template: loadTemplate(options.template || options.templateUrl),
515 locals: $q.all(resolve)
516 }).then(function (setup) {
517 var template = setup.template,
518 locals = setup.locals;
520 if (options.showClose) {
521 template += '<div class="ngdialog-close"></div>';
524 var hasOverlayClass = options.overlay ? '' : ' ngdialog-no-overlay';
525 $dialog = $el('<div id="' + dialogID + '" class="ngdialog' + hasOverlayClass + '"></div>');
526 $dialog.html((options.overlay ?
527 '<div class="ngdialog-overlay"></div><div class="ngdialog-content" role="document">' + template + '</div>' :
528 '<div class="ngdialog-content" role="document">' + template + '</div>'));
530 $dialog.data('$ngDialogOptions', options);
532 scope.ngDialogId = dialogID;
534 if (options.data && angular.isString(options.data)) {
535 var firstLetter = options.data.replace(/^\s*/, '')[0];
536 scope.ngDialogData = (firstLetter === '{' || firstLetter === '[') ? angular.fromJson(options.data) : new String(options.data);
537 scope.ngDialogData.ngDialogId = dialogID;
538 } else if (options.data && angular.isObject(options.data)) {
539 scope.ngDialogData = options.data;
540 scope.ngDialogData.ngDialogId = dialogID;
543 if (options.className) {
544 $dialog.addClass(options.className);
547 if (options.appendClassName) {
548 $dialog.addClass(options.appendClassName);
552 $dialogContent = $dialog[0].querySelector('.ngdialog-content');
553 if (angular.isString(options.width)) {
554 $dialogContent.style.width = options.width;
556 $dialogContent.style.width = options.width + 'px';
560 if (options.height) {
561 $dialogContent = $dialog[0].querySelector('.ngdialog-content');
562 if (angular.isString(options.height)) {
563 $dialogContent.style.height = options.height;
565 $dialogContent.style.height = options.height + 'px';
569 if (options.disableAnimation) {
570 $dialog.addClass(disabledAnimationClass);
573 if (options.appendTo && angular.isString(options.appendTo)) {
574 $dialogParent = angular.element(document.querySelector(options.appendTo));
576 $dialogParent = $elements.body;
579 privateMethods.applyAriaAttributes($dialog, options);
581 if (options.preCloseCallback) {
582 var preCloseCallback;
584 if (angular.isFunction(options.preCloseCallback)) {
585 preCloseCallback = options.preCloseCallback;
586 } else if (angular.isString(options.preCloseCallback)) {
588 if (angular.isFunction(scope[options.preCloseCallback])) {
589 preCloseCallback = scope[options.preCloseCallback];
590 } else if (scope.$parent && angular.isFunction(scope.$parent[options.preCloseCallback])) {
591 preCloseCallback = scope.$parent[options.preCloseCallback];
592 } else if ($rootScope && angular.isFunction($rootScope[options.preCloseCallback])) {
593 preCloseCallback = $rootScope[options.preCloseCallback];
598 if (preCloseCallback) {
599 $dialog.data('$ngDialogPreCloseCallback', preCloseCallback);
603 scope.closeThisDialog = function (value) {
604 privateMethods.closeDialog($dialog, value);
607 if (options.controller && (angular.isString(options.controller) || angular.isArray(options.controller) || angular.isFunction(options.controller))) {
611 if (options.controllerAs && angular.isString(options.controllerAs)) {
612 label = options.controllerAs;
615 var controllerInstance = $controller(options.controller, angular.extend(
625 if(options.bindToController) {
626 angular.extend(controllerInstance.instance, {ngDialogId: scope.ngDialogId, ngDialogData: scope.ngDialogData, closeThisDialog: scope.closeThisDialog, confirm: scope.confirm});
629 if(typeof controllerInstance === 'function'){
630 $dialog.data('$ngDialogControllerController', controllerInstance());
632 $dialog.data('$ngDialogControllerController', controllerInstance);
636 $timeout(function () {
637 var $activeDialogs = document.querySelectorAll('.ngdialog');
638 privateMethods.deactivateAll($activeDialogs);
640 $compile($dialog)(scope);
641 var widthDiffs = $window.innerWidth - $elements.body.prop('clientWidth');
642 $elements.html.addClass(options.bodyClassName);
643 $elements.body.addClass(options.bodyClassName);
644 var scrollBarWidth = widthDiffs - ($window.innerWidth - $elements.body.prop('clientWidth'));
645 if (scrollBarWidth > 0) {
646 privateMethods.setBodyPadding(scrollBarWidth);
648 $dialogParent.append($dialog);
650 privateMethods.activate($dialog);
652 if (options.trapFocus) {
653 privateMethods.autoFocus($dialog);
657 $rootScope.$broadcast('ngDialog.opened', {dialog: $dialog, name: options.name});
659 $rootScope.$broadcast('ngDialog.opened', $dialog);
663 if (!keydownIsBound) {
664 $elements.body.bind('keydown', privateMethods.onDocumentKeydown);
665 keydownIsBound = true;
668 if (options.closeByNavigation) {
669 var eventName = privateMethods.getRouterLocationEventName();
670 $rootScope.$on(eventName, function ($event) {
671 if (privateMethods.closeDialog($dialog) === false)
672 $event.preventDefault();
676 if (options.preserveFocus) {
677 $dialog.data('$ngDialogPreviousFocus', document.activeElement);
680 closeByDocumentHandler = function (event) {
681 var isOverlay = options.closeByDocument ? $el(event.target).hasClass('ngdialog-overlay') : false;
682 var isCloseBtn = $el(event.target).hasClass('ngdialog-close');
684 if (isOverlay || isCloseBtn) {
685 publicMethods.close($dialog.attr('id'), isCloseBtn ? '$closeButton' : '$document');
689 if (typeof $window.Hammer !== 'undefined') {
690 var hammerTime = scope.hammerTime = $window.Hammer($dialog[0]);
691 hammerTime.on('tap', closeByDocumentHandler);
693 $dialog.bind('click', closeByDocumentHandler);
698 return publicMethods;
703 closePromise: defer.promise,
704 close: function (value) {
705 privateMethods.closeDialog($dialog, value);
709 function loadTemplateUrl (tmpl, config) {
710 var config = config || {};
711 config.headers = config.headers || {};
713 angular.extend(config.headers, {'Accept': 'text/html'});
715 $rootScope.$broadcast('ngDialog.templateLoading', tmpl);
716 return $http.get(tmpl, config).then(function(res) {
717 $rootScope.$broadcast('ngDialog.templateLoaded', tmpl);
718 return res.data || '';
722 function loadTemplate (tmpl) {
724 return 'Empty template';
727 if (angular.isString(tmpl) && options.plain) {
731 if (typeof options.cache === 'boolean' && !options.cache) {
732 return loadTemplateUrl(tmpl, {cache: false});
735 return loadTemplateUrl(tmpl, {cache: $templateCache});
740 * @param {Object} options:
741 * - template {String} - id of ng-template, url for partial, plain string (if enabled)
742 * - plain {Boolean} - enable plain string templates, default false
745 * - controller {String}
746 * - controllerAs {String}
747 * - className {String} - dialog theme class
748 * - appendClassName {String} - dialog theme class to be appended to defaults
749 * - showClose {Boolean} - show close button, default true
750 * - closeByEscape {Boolean} - default false
751 * - closeByDocument {Boolean} - default false
752 * - preCloseCallback {String|Function} - user supplied function name/function called before closing dialog (if set); not called on confirm
753 * - bodyClassName {String} - class added to body at open dialog
755 * @return {Object} dialog
757 openConfirm: function (opts) {
758 var defer = $q.defer();
759 var options = angular.copy(defaults);
763 // Merge opts.data with predefined via setDefaults
764 if (typeof options.data !== 'undefined') {
765 if (typeof opts.data === 'undefined') {
768 opts.data = angular.merge(angular.copy(options.data), opts.data);
771 angular.extend(options, opts);
773 options.scope = angular.isObject(options.scope) ? options.scope.$new() : $rootScope.$new();
774 options.scope.confirm = function (value) {
775 defer.resolve(value);
776 var $dialog = $el(document.getElementById(openResult.id));
777 privateMethods.performCloseDialog($dialog, value);
780 var openResult = publicMethods.open(options);
782 openResult.closePromise.then(function (data) {
784 return defer.reject(data.value);
786 return defer.reject();
788 return defer.promise;
792 isOpen: function(id) {
793 var $dialog = $el(document.getElementById(id));
794 return $dialog.length > 0;
799 * @return {Object} dialog
801 close: function (id, value) {
802 var $dialog = $el(document.getElementById(id));
804 if ($dialog.length) {
805 privateMethods.closeDialog($dialog, value);
807 if (id === '$escape') {
808 var topDialogId = openIdStack[openIdStack.length - 1];
809 $dialog = $el(document.getElementById(topDialogId));
810 if ($dialog.data('$ngDialogOptions').closeByEscape) {
811 privateMethods.closeDialog($dialog, '$escape');
814 publicMethods.closeAll(value);
818 return publicMethods;
821 closeAll: function (value) {
822 var $all = document.querySelectorAll('.ngdialog');
824 // Reverse order to ensure focus restoration works as expected
825 for (var i = $all.length - 1; i >= 0; i--) {
826 var dialog = $all[i];
827 privateMethods.closeDialog($el(dialog), value);
831 getOpenDialogs: function() {
835 getDefaults: function () {
842 function(elementName) {
843 $elements[elementName] = $document.find(elementName);
844 if (forceElementsReload[elementName]) {
845 var eventName = privateMethods.getRouterLocationEventName();
846 $rootScope.$on(eventName, function () {
847 $elements[elementName] = $document.find(elementName);
853 return publicMethods;
857 m.directive('ngDialog', ['ngDialog', function (ngDialog) {
863 link: function (scope, elem, attrs) {
864 elem.on('click', function (e) {
867 var ngDialogScope = angular.isDefined(scope.ngDialogScope) ? scope.ngDialogScope : 'noScope';
868 angular.isDefined(attrs.ngDialogClosePrevious) && ngDialog.close(attrs.ngDialogClosePrevious);
870 var defaults = ngDialog.getDefaults();
873 template: attrs.ngDialog,
874 className: attrs.ngDialogClass || defaults.className,
875 appendClassName: attrs.ngDialogAppendClass,
876 controller: attrs.ngDialogController,
877 controllerAs: attrs.ngDialogControllerAs,
878 bindToController: attrs.ngDialogBindToController,
879 disableAnimation: attrs.ngDialogDisableAnimation,
880 scope: ngDialogScope,
881 data: attrs.ngDialogData,
882 showClose: attrs.ngDialogShowClose === 'false' ? false : (attrs.ngDialogShowClose === 'true' ? true : defaults.showClose),
883 closeByDocument: attrs.ngDialogCloseByDocument === 'false' ? false : (attrs.ngDialogCloseByDocument === 'true' ? true : defaults.closeByDocument),
884 closeByEscape: attrs.ngDialogCloseByEscape === 'false' ? false : (attrs.ngDialogCloseByEscape === 'true' ? true : defaults.closeByEscape),
885 overlay: attrs.ngDialogOverlay === 'false' ? false : (attrs.ngDialogOverlay === 'true' ? true : defaults.overlay),
886 preCloseCallback: attrs.ngDialogPreCloseCallback || defaults.preCloseCallback,
887 bodyClassName: attrs.ngDialogBodyClass || defaults.bodyClassName