/* * ngDialog - easy modals and popup windows * http://github.com/likeastore/ngDialog * (c) 2013-2015 MIT License, https://likeastore.com */ (function (root, factory) { if (typeof module !== 'undefined' && module.exports) { // CommonJS if (typeof angular === 'undefined') { factory(require('angular')); } else { factory(angular); } module.exports = 'ngDialog'; } else if (typeof define === 'function' && define.amd) { // AMD define(['angular'], factory); } else { // Global Variables factory(root.angular); } }(this, function (angular) { 'use strict'; var m = angular.module('ngDialog', []); var $el = angular.element; var isDef = angular.isDefined; var style = (document.body || document.documentElement).style; var animationEndSupport = isDef(style.animation) || isDef(style.WebkitAnimation) || isDef(style.MozAnimation) || isDef(style.MsAnimation) || isDef(style.OAnimation); var animationEndEvent = 'animationend webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend'; var focusableElementSelector = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]'; var disabledAnimationClass = 'ngdialog-disabled-animation'; var forceElementsReload = { html: false, body: false }; var scopes = {}; var openIdStack = []; var keydownIsBound = false; var openOnePerName = false; m.provider('ngDialog', function () { var defaults = this.defaults = { className: 'ngdialog-theme-default', appendClassName: '', disableAnimation: false, plain: false, showClose: true, closeByDocument: true, closeByEscape: true, closeByNavigation: false, appendTo: false, preCloseCallback: false, overlay: true, cache: true, trapFocus: true, preserveFocus: true, ariaAuto: true, ariaRole: null, ariaLabelledById: null, ariaLabelledBySelector: null, ariaDescribedById: null, ariaDescribedBySelector: null, bodyClassName: 'ngdialog-open', width: null, height: null }; this.setForceHtmlReload = function (_useIt) { forceElementsReload.html = _useIt || false; }; this.setForceBodyReload = function (_useIt) { forceElementsReload.body = _useIt || false; }; this.setDefaults = function (newDefaults) { angular.extend(defaults, newDefaults); }; this.setOpenOnePerName = function (isOpenOne) { openOnePerName = isOpenOne || false; }; var globalID = 0, dialogsCount = 0, closeByDocumentHandler, defers = {}; this.$get = ['$document', '$templateCache', '$compile', '$q', '$http', '$rootScope', '$timeout', '$window', '$controller', '$injector', function ($document, $templateCache, $compile, $q, $http, $rootScope, $timeout, $window, $controller, $injector) { var $elements = []; var privateMethods = { onDocumentKeydown: function (event) { if (event.keyCode === 27) { publicMethods.close('$escape'); } }, activate: function($dialog) { var options = $dialog.data('$ngDialogOptions'); if (options.trapFocus) { $dialog.on('keydown', privateMethods.onTrapFocusKeydown); // Catch rogue changes (eg. after unfocusing everything by clicking a non-focusable element) $elements.body.on('keydown', privateMethods.onTrapFocusKeydown); } }, deactivate: function ($dialog) { $dialog.off('keydown', privateMethods.onTrapFocusKeydown); $elements.body.off('keydown', privateMethods.onTrapFocusKeydown); }, deactivateAll: function (els) { angular.forEach(els,function(el) { var $dialog = angular.element(el); privateMethods.deactivate($dialog); }); }, setBodyPadding: function (width) { var originalBodyPadding = parseInt(($elements.body.css('padding-right') || 0), 10); $elements.body.css('padding-right', (originalBodyPadding + width) + 'px'); $elements.body.data('ng-dialog-original-padding', originalBodyPadding); $rootScope.$broadcast('ngDialog.setPadding', width); }, resetBodyPadding: function () { var originalBodyPadding = $elements.body.data('ng-dialog-original-padding'); if (originalBodyPadding) { $elements.body.css('padding-right', originalBodyPadding + 'px'); } else { $elements.body.css('padding-right', ''); } $rootScope.$broadcast('ngDialog.setPadding', 0); }, performCloseDialog: function ($dialog, value) { var options = $dialog.data('$ngDialogOptions'); var id = $dialog.attr('id'); var scope = scopes[id]; if (!scope) { // Already closed return; } if (typeof $window.Hammer !== 'undefined') { var hammerTime = scope.hammerTime; hammerTime.off('tap', closeByDocumentHandler); hammerTime.destroy && hammerTime.destroy(); delete scope.hammerTime; } else { $dialog.unbind('click'); } if (dialogsCount === 1) { $elements.body.unbind('keydown', privateMethods.onDocumentKeydown); } if (!$dialog.hasClass('ngdialog-closing')){ dialogsCount -= 1; } var previousFocus = $dialog.data('$ngDialogPreviousFocus'); if (previousFocus && previousFocus.focus) { previousFocus.focus(); } $rootScope.$broadcast('ngDialog.closing', $dialog, value); dialogsCount = dialogsCount < 0 ? 0 : dialogsCount; if (animationEndSupport && !options.disableAnimation) { scope.$destroy(); $dialog.unbind(animationEndEvent).bind(animationEndEvent, function () { privateMethods.closeDialogElement($dialog, value); }).addClass('ngdialog-closing'); } else { scope.$destroy(); privateMethods.closeDialogElement($dialog, value); } if (defers[id]) { defers[id].resolve({ id: id, value: value, $dialog: $dialog, remainingDialogs: dialogsCount }); delete defers[id]; } if (scopes[id]) { delete scopes[id]; } openIdStack.splice(openIdStack.indexOf(id), 1); if (!openIdStack.length) { $elements.body.unbind('keydown', privateMethods.onDocumentKeydown); keydownIsBound = false; } }, closeDialogElement: function($dialog, value) { var options = $dialog.data('$ngDialogOptions'); $dialog.remove(); if (dialogsCount === 0) { $elements.html.removeClass(options.bodyClassName); $elements.body.removeClass(options.bodyClassName); privateMethods.resetBodyPadding(); } $rootScope.$broadcast('ngDialog.closed', $dialog, value); }, closeDialog: function ($dialog, value) { var preCloseCallback = $dialog.data('$ngDialogPreCloseCallback'); if (preCloseCallback && angular.isFunction(preCloseCallback)) { var preCloseCallbackResult = preCloseCallback.call($dialog, value); if (angular.isObject(preCloseCallbackResult)) { if (preCloseCallbackResult.closePromise) { preCloseCallbackResult.closePromise.then(function () { privateMethods.performCloseDialog($dialog, value); }, function () { return false; }); } else { preCloseCallbackResult.then(function () { privateMethods.performCloseDialog($dialog, value); }, function () { return false; }); } } else if (preCloseCallbackResult !== false) { privateMethods.performCloseDialog($dialog, value); } else { return false; } } else { privateMethods.performCloseDialog($dialog, value); } }, onTrapFocusKeydown: function(ev) { var el = angular.element(ev.currentTarget); var $dialog; if (el.hasClass('ngdialog')) { $dialog = el; } else { $dialog = privateMethods.getActiveDialog(); if ($dialog === null) { return; } } var isTab = (ev.keyCode === 9); var backward = (ev.shiftKey === true); if (isTab) { privateMethods.handleTab($dialog, ev, backward); } }, handleTab: function($dialog, ev, backward) { var focusableElements = privateMethods.getFocusableElements($dialog); if (focusableElements.length === 0) { if (document.activeElement && document.activeElement.blur) { document.activeElement.blur(); } return; } var currentFocus = document.activeElement; var focusIndex = Array.prototype.indexOf.call(focusableElements, currentFocus); var isFocusIndexUnknown = (focusIndex === -1); var isFirstElementFocused = (focusIndex === 0); var isLastElementFocused = (focusIndex === focusableElements.length - 1); var cancelEvent = false; if (backward) { if (isFocusIndexUnknown || isFirstElementFocused) { focusableElements[focusableElements.length - 1].focus(); cancelEvent = true; } } else { if (isFocusIndexUnknown || isLastElementFocused) { focusableElements[0].focus(); cancelEvent = true; } } if (cancelEvent) { ev.preventDefault(); ev.stopPropagation(); } }, autoFocus: function($dialog) { var dialogEl = $dialog[0]; // Browser's (Chrome 40, Forefix 37, IE 11) don't appear to honor autofocus on the dialog, but we should var autoFocusEl = dialogEl.querySelector('*[autofocus]'); if (autoFocusEl !== null) { autoFocusEl.focus(); if (document.activeElement === autoFocusEl) { return; } // Autofocus element might was display: none, so let's continue } var focusableElements = privateMethods.getFocusableElements($dialog); if (focusableElements.length > 0) { focusableElements[0].focus(); return; } // We need to focus something for the screen readers to notice the dialog var contentElements = privateMethods.filterVisibleElements(dialogEl.querySelectorAll('h1,h2,h3,h4,h5,h6,p,span')); if (contentElements.length > 0) { var contentElement = contentElements[0]; $el(contentElement).attr('tabindex', '-1').css('outline', '0'); contentElement.focus(); } }, getFocusableElements: function ($dialog) { var dialogEl = $dialog[0]; var rawElements = dialogEl.querySelectorAll(focusableElementSelector); // Ignore untabbable elements, ie. those with tabindex = -1 var tabbableElements = privateMethods.filterTabbableElements(rawElements); return privateMethods.filterVisibleElements(tabbableElements); }, filterTabbableElements: function (els) { var tabbableFocusableElements = []; for (var i = 0; i < els.length; i++) { var el = els[i]; if ($el(el).attr('tabindex') !== '-1') { tabbableFocusableElements.push(el); } } return tabbableFocusableElements; }, filterVisibleElements: function (els) { var visibleFocusableElements = []; for (var i = 0; i < els.length; i++) { var el = els[i]; if (el.offsetWidth > 0 || el.offsetHeight > 0) { visibleFocusableElements.push(el); } } return visibleFocusableElements; }, getActiveDialog: function () { var dialogs = document.querySelectorAll('.ngdialog'); if (dialogs.length === 0) { return null; } // TODO: This might be incorrect if there are a mix of open dialogs with different 'appendTo' values return $el(dialogs[dialogs.length - 1]); }, applyAriaAttributes: function ($dialog, options) { if (options.ariaAuto) { if (!options.ariaRole) { var detectedRole = (privateMethods.getFocusableElements($dialog).length > 0) ? 'dialog' : 'alertdialog'; options.ariaRole = detectedRole; } if (!options.ariaLabelledBySelector) { options.ariaLabelledBySelector = 'h1,h2,h3,h4,h5,h6'; } if (!options.ariaDescribedBySelector) { options.ariaDescribedBySelector = 'article,section,p'; } } if (options.ariaRole) { $dialog.attr('role', options.ariaRole); } privateMethods.applyAriaAttribute( $dialog, 'aria-labelledby', options.ariaLabelledById, options.ariaLabelledBySelector); privateMethods.applyAriaAttribute( $dialog, 'aria-describedby', options.ariaDescribedById, options.ariaDescribedBySelector); }, applyAriaAttribute: function($dialog, attr, id, selector) { if (id) { $dialog.attr(attr, id); } if (selector) { var dialogId = $dialog.attr('id'); var firstMatch = $dialog[0].querySelector(selector); if (!firstMatch) { return; } var generatedId = dialogId + '-' + attr; $el(firstMatch).attr('id', generatedId); $dialog.attr(attr, generatedId); return generatedId; } }, detectUIRouter: function() { //Detect if ui-router module is installed if not return false try { angular.module('ui.router'); return true; } catch(err) { return false; } }, getRouterLocationEventName: function() { if(privateMethods.detectUIRouter()) { return '$stateChangeStart'; } return '$locationChangeStart'; } }; var publicMethods = { __PRIVATE__: privateMethods, /* * @param {Object} options: * - template {String} - id of ng-template, url for partial, plain string (if enabled) * - plain {Boolean} - enable plain string templates, default false * - scope {Object} * - controller {String} * - controllerAs {String} * - className {String} - dialog theme class * - appendClassName {String} - dialog theme class to be appended to defaults * - disableAnimation {Boolean} - set to true to disable animation * - showClose {Boolean} - show close button, default true * - closeByEscape {Boolean} - default true * - closeByDocument {Boolean} - default true * - preCloseCallback {String|Function} - user supplied function name/function called before closing dialog (if set) * - bodyClassName {String} - class added to body at open dialog * @return {Object} dialog */ open: function (opts) { var dialogID = null; opts = opts || {}; if (openOnePerName && opts.name) { dialogID = opts.name.toLowerCase().replace(/\s/g, '-') + '-dialog'; if (this.isOpen(dialogID)) { return; } } var options = angular.copy(defaults); var localID = ++globalID; dialogID = dialogID || 'ngdialog' + localID; openIdStack.push(dialogID); // Merge opts.data with predefined via setDefaults if (typeof options.data !== 'undefined') { if (typeof opts.data === 'undefined') { opts.data = {}; } opts.data = angular.merge(angular.copy(options.data), opts.data); } angular.extend(options, opts); var defer; defers[dialogID] = defer = $q.defer(); var scope; scopes[dialogID] = scope = angular.isObject(options.scope) ? options.scope.$new() : $rootScope.$new(); var $dialog, $dialogParent, $dialogContent; var resolve = angular.extend({}, options.resolve); angular.forEach(resolve, function (value, key) { resolve[key] = angular.isString(value) ? $injector.get(value) : $injector.invoke(value, null, null, key); }); $q.all({ template: loadTemplate(options.template || options.templateUrl), locals: $q.all(resolve) }).then(function (setup) { var template = setup.template, locals = setup.locals; if (options.showClose) { template += '
'; } var hasOverlayClass = options.overlay ? '' : ' ngdialog-no-overlay'; $dialog = $el('
'); $dialog.html((options.overlay ? '
' + template + '
' : '
' + template + '
')); $dialog.data('$ngDialogOptions', options); scope.ngDialogId = dialogID; if (options.data && angular.isString(options.data)) { var firstLetter = options.data.replace(/^\s*/, '')[0]; scope.ngDialogData = (firstLetter === '{' || firstLetter === '[') ? angular.fromJson(options.data) : new String(options.data); scope.ngDialogData.ngDialogId = dialogID; } else if (options.data && angular.isObject(options.data)) { scope.ngDialogData = options.data; scope.ngDialogData.ngDialogId = dialogID; } if (options.className) { $dialog.addClass(options.className); } if (options.appendClassName) { $dialog.addClass(options.appendClassName); } if (options.width) { $dialogContent = $dialog[0].querySelector('.ngdialog-content'); if (angular.isString(options.width)) { $dialogContent.style.width = options.width; } else { $dialogContent.style.width = options.width + 'px'; } } if (options.height) { $dialogContent = $dialog[0].querySelector('.ngdialog-content'); if (angular.isString(options.height)) { $dialogContent.style.height = options.height; } else { $dialogContent.style.height = options.height + 'px'; } } if (options.disableAnimation) { $dialog.addClass(disabledAnimationClass); } if (options.appendTo && angular.isString(options.appendTo)) { $dialogParent = angular.element(document.querySelector(options.appendTo)); } else { $dialogParent = $elements.body; } privateMethods.applyAriaAttributes($dialog, options); if (options.preCloseCallback) { var preCloseCallback; if (angular.isFunction(options.preCloseCallback)) { preCloseCallback = options.preCloseCallback; } else if (angular.isString(options.preCloseCallback)) { if (scope) { if (angular.isFunction(scope[options.preCloseCallback])) { preCloseCallback = scope[options.preCloseCallback]; } else if (scope.$parent && angular.isFunction(scope.$parent[options.preCloseCallback])) { preCloseCallback = scope.$parent[options.preCloseCallback]; } else if ($rootScope && angular.isFunction($rootScope[options.preCloseCallback])) { preCloseCallback = $rootScope[options.preCloseCallback]; } } } if (preCloseCallback) { $dialog.data('$ngDialogPreCloseCallback', preCloseCallback); } } scope.closeThisDialog = function (value) { privateMethods.closeDialog($dialog, value); }; if (options.controller && (angular.isString(options.controller) || angular.isArray(options.controller) || angular.isFunction(options.controller))) { var label; if (options.controllerAs && angular.isString(options.controllerAs)) { label = options.controllerAs; } var controllerInstance = $controller(options.controller, angular.extend( locals, { $scope: scope, $element: $dialog }), true, label ); if(options.bindToController) { angular.extend(controllerInstance.instance, {ngDialogId: scope.ngDialogId, ngDialogData: scope.ngDialogData, closeThisDialog: scope.closeThisDialog, confirm: scope.confirm}); } if(typeof controllerInstance === 'function'){ $dialog.data('$ngDialogControllerController', controllerInstance()); } else { $dialog.data('$ngDialogControllerController', controllerInstance); } } $timeout(function () { var $activeDialogs = document.querySelectorAll('.ngdialog'); privateMethods.deactivateAll($activeDialogs); $compile($dialog)(scope); var widthDiffs = $window.innerWidth - $elements.body.prop('clientWidth'); $elements.html.addClass(options.bodyClassName); $elements.body.addClass(options.bodyClassName); var scrollBarWidth = widthDiffs - ($window.innerWidth - $elements.body.prop('clientWidth')); if (scrollBarWidth > 0) { privateMethods.setBodyPadding(scrollBarWidth); } $dialogParent.append($dialog); privateMethods.activate($dialog); if (options.trapFocus) { privateMethods.autoFocus($dialog); } if (options.name) { $rootScope.$broadcast('ngDialog.opened', {dialog: $dialog, name: options.name}); } else { $rootScope.$broadcast('ngDialog.opened', $dialog); } }); if (!keydownIsBound) { $elements.body.bind('keydown', privateMethods.onDocumentKeydown); keydownIsBound = true; } if (options.closeByNavigation) { var eventName = privateMethods.getRouterLocationEventName(); $rootScope.$on(eventName, function ($event) { if (privateMethods.closeDialog($dialog) === false) $event.preventDefault(); }); } if (options.preserveFocus) { $dialog.data('$ngDialogPreviousFocus', document.activeElement); } closeByDocumentHandler = function (event) { var isOverlay = options.closeByDocument ? $el(event.target).hasClass('ngdialog-overlay') : false; var isCloseBtn = $el(event.target).hasClass('ngdialog-close'); if (isOverlay || isCloseBtn) { publicMethods.close($dialog.attr('id'), isCloseBtn ? '$closeButton' : '$document'); } }; if (typeof $window.Hammer !== 'undefined') { var hammerTime = scope.hammerTime = $window.Hammer($dialog[0]); hammerTime.on('tap', closeByDocumentHandler); } else { $dialog.bind('click', closeByDocumentHandler); } dialogsCount += 1; return publicMethods; }); return { id: dialogID, closePromise: defer.promise, close: function (value) { privateMethods.closeDialog($dialog, value); } }; function loadTemplateUrl (tmpl, config) { var config = config || {}; config.headers = config.headers || {}; angular.extend(config.headers, {'Accept': 'text/html'}); $rootScope.$broadcast('ngDialog.templateLoading', tmpl); return $http.get(tmpl, config).then(function(res) { $rootScope.$broadcast('ngDialog.templateLoaded', tmpl); return res.data || ''; }); } function loadTemplate (tmpl) { if (!tmpl) { return 'Empty template'; } if (angular.isString(tmpl) && options.plain) { return tmpl; } if (typeof options.cache === 'boolean' && !options.cache) { return loadTemplateUrl(tmpl, {cache: false}); } return loadTemplateUrl(tmpl, {cache: $templateCache}); } }, /* * @param {Object} options: * - template {String} - id of ng-template, url for partial, plain string (if enabled) * - plain {Boolean} - enable plain string templates, default false * - name {String} * - scope {Object} * - controller {String} * - controllerAs {String} * - className {String} - dialog theme class * - appendClassName {String} - dialog theme class to be appended to defaults * - showClose {Boolean} - show close button, default true * - closeByEscape {Boolean} - default false * - closeByDocument {Boolean} - default false * - preCloseCallback {String|Function} - user supplied function name/function called before closing dialog (if set); not called on confirm * - bodyClassName {String} - class added to body at open dialog * * @return {Object} dialog */ openConfirm: function (opts) { var defer = $q.defer(); var options = angular.copy(defaults); opts = opts || {}; // Merge opts.data with predefined via setDefaults if (typeof options.data !== 'undefined') { if (typeof opts.data === 'undefined') { opts.data = {}; } opts.data = angular.merge(angular.copy(options.data), opts.data); } angular.extend(options, opts); options.scope = angular.isObject(options.scope) ? options.scope.$new() : $rootScope.$new(); options.scope.confirm = function (value) { defer.resolve(value); var $dialog = $el(document.getElementById(openResult.id)); privateMethods.performCloseDialog($dialog, value); }; var openResult = publicMethods.open(options); if (openResult) { openResult.closePromise.then(function (data) { if (data) { return defer.reject(data.value); } return defer.reject(); }); return defer.promise; } }, isOpen: function(id) { var $dialog = $el(document.getElementById(id)); return $dialog.length > 0; }, /* * @param {String} id * @return {Object} dialog */ close: function (id, value) { var $dialog = $el(document.getElementById(id)); if ($dialog.length) { privateMethods.closeDialog($dialog, value); } else { if (id === '$escape') { var topDialogId = openIdStack[openIdStack.length - 1]; $dialog = $el(document.getElementById(topDialogId)); if ($dialog.data('$ngDialogOptions').closeByEscape) { privateMethods.closeDialog($dialog, '$escape'); } } else { publicMethods.closeAll(value); } } return publicMethods; }, closeAll: function (value) { var $all = document.querySelectorAll('.ngdialog'); // Reverse order to ensure focus restoration works as expected for (var i = $all.length - 1; i >= 0; i--) { var dialog = $all[i]; privateMethods.closeDialog($el(dialog), value); } }, getOpenDialogs: function() { return openIdStack; }, getDefaults: function () { return defaults; } }; angular.forEach( ['html', 'body'], function(elementName) { $elements[elementName] = $document.find(elementName); if (forceElementsReload[elementName]) { var eventName = privateMethods.getRouterLocationEventName(); $rootScope.$on(eventName, function () { $elements[elementName] = $document.find(elementName); }); } } ); return publicMethods; }]; }); m.directive('ngDialog', ['ngDialog', function (ngDialog) { return { restrict: 'A', scope: { ngDialogScope: '=' }, link: function (scope, elem, attrs) { elem.on('click', function (e) { e.preventDefault(); var ngDialogScope = angular.isDefined(scope.ngDialogScope) ? scope.ngDialogScope : 'noScope'; angular.isDefined(attrs.ngDialogClosePrevious) && ngDialog.close(attrs.ngDialogClosePrevious); var defaults = ngDialog.getDefaults(); ngDialog.open({ template: attrs.ngDialog, className: attrs.ngDialogClass || defaults.className, appendClassName: attrs.ngDialogAppendClass, controller: attrs.ngDialogController, controllerAs: attrs.ngDialogControllerAs, bindToController: attrs.ngDialogBindToController, disableAnimation: attrs.ngDialogDisableAnimation, scope: ngDialogScope, data: attrs.ngDialogData, showClose: attrs.ngDialogShowClose === 'false' ? false : (attrs.ngDialogShowClose === 'true' ? true : defaults.showClose), closeByDocument: attrs.ngDialogCloseByDocument === 'false' ? false : (attrs.ngDialogCloseByDocument === 'true' ? true : defaults.closeByDocument), closeByEscape: attrs.ngDialogCloseByEscape === 'false' ? false : (attrs.ngDialogCloseByEscape === 'true' ? true : defaults.closeByEscape), overlay: attrs.ngDialogOverlay === 'false' ? false : (attrs.ngDialogOverlay === 'true' ? true : defaults.overlay), preCloseCallback: attrs.ngDialogPreCloseCallback || defaults.preCloseCallback, bodyClassName: attrs.ngDialogBodyClass || defaults.bodyClassName }); }); } }; }]); return m; }));