diff --git a/src/dialog/README.md b/src/dialog/README.md deleted file mode 100644 index d1c16236df..0000000000 --- a/src/dialog/README.md +++ /dev/null @@ -1,99 +0,0 @@ -# $dialogProvider (service in ui.bootstrap) - -## Description - -Used for configuring global options for dialogs. - -### Methods - -#### `options(opts)` - -Sets the default global options for your application. Options can be overridden when opening dialogs. Available options are: - -* `backdrop`: a boolean value indicating whether a backdrop should be used or not, defaults to true -* `dialogClass`: the css class for the modal div, defaults to 'modal' -* `backdropClass`: the css class for the backdrop, defaults to 'modal-backdrop' -* `transitionClass`: the css class that applies transitions to the modal and backdrop, defaults to 'fade' -* `triggerClass`: the css class that triggers the transitions, defaults to 'in' -* `dialogOpenClass`: the css class that is added to body when dialog is opened, defaults to 'modal-open' -* `resolve`: members that will be resolved and passed to the controller as locals -* `controller`: the controller to associate with the included partial view -* `backdropFade`: a boolean value indicating whether the backdrop should fade in and out using a CSS transition, defaults to false -* `dialogFade`: a boolean value indicating whether the modal should fade in and out using a CSS transition, defaults to false -* `keyboard`: indicates whether the dialog should be closable by hitting the ESC key, defaults to true -* `backdropClick`: indicates whether the dialog should be closable by clicking the backdrop area, defaults to true -* `template`: the template for dialog -* `templateUrl`: path to the template for dialog - -Example: - - var app = angular.module('App', ['ui.bootstrap.dialog'] , function($dialogProvider){ - $dialogProvider.options({backdropClick: false, dialogFade: true}); - }); - -# $dialog service - -## Description - -Allows you to open dialogs from within your controller. - -### Methods - -#### `dialog([templateUrl[, controller]])` - -Creates a new dialog, optionally setting the `templateUrl`, and `controller` options. - -Example: - - app.controller('MainCtrl', function($dialog, $scope) { - $scope.openItemEditor = function(item){ - var d = $dialog.dialog({dialogFade: false, resolve: {item: function(){ return angular.copy(item); } }}); - d.open('dialogs/item-editor.html', 'EditItemController'); - }; - }); - - // note that the resolved item as well as the dialog are injected in the dialog's controller - app.controller('EditItemController', ['$scope', 'dialog', 'item', function($scope, dialog, item){ - $scope.item = item; - $scope.submit = function(){ - dialog.close('ok'); - }; - }]); - -#### `messageBox(title, message, buttons)` - -Opens a message box with the specified `title`, `message` and a series of `buttons` can be provided, every button can specify: - -* `label`: the label of the button -* `result`: the result used to invoke the close method of the dialog -* `cssClass`: optional, the CSS class (e.g. btn-primary) to apply to the button - -Example: - - app.controller('MainCtrl', function($dialog, $scope) { - $scope.deleteItem = function(item){ - var msgbox = $dialog.messageBox('Delete Item', 'Are you sure?', [{label:'Yes, I\'m sure', result: 'yes'},{label:'Nope', result: 'no'}]); - msgbox.open().then(function(result){ - if(result === 'yes') {deleteItem(item);} - }); - }; - }); - -## Dialog class - -The dialog object returned by the `$dialog` service methods `open` and `message`. - -### Methods - -#### `open` - -(Re)Opens the dialog and returns a promise. - -#### `close([result])` - -Closes the dialog. Optionally a result can be specified. The result is used to resolve the promise returned by the `open` method. - -#### `isOpen` - -Returns true if the dialog is shown, else returns false. - diff --git a/src/dialog/dialog.js b/src/dialog/dialog.js deleted file mode 100644 index 42d63f59e2..0000000000 --- a/src/dialog/dialog.js +++ /dev/null @@ -1,281 +0,0 @@ -// The `$dialogProvider` can be used to configure global defaults for your -// `$dialog` service. -var dialogModule = angular.module('ui.bootstrap.dialog', ['ui.bootstrap.transition']); - -dialogModule.controller('MessageBoxController', ['$scope', 'dialog', 'model', function($scope, dialog, model){ - $scope.title = model.title; - $scope.message = model.message; - $scope.buttons = model.buttons; - $scope.close = function(res){ - dialog.close(res); - }; -}]); - -dialogModule.provider("$dialog", function(){ - - // The default options for all dialogs. - var defaults = { - backdrop: true, - dialogClass: 'modal', - backdropClass: 'modal-backdrop', - transitionClass: 'fade', - triggerClass: 'in', - dialogOpenClass: 'modal-open', - resolve:{}, - backdropFade: false, - dialogFade:false, - keyboard: true, // close with esc key - backdropClick: true // only in conjunction with backdrop=true - /* other options: template, templateUrl, controller */ - }; - - var globalOptions = {}; - - var activeBackdrops = {value : 0}; - - // The `options({})` allows global configuration of all dialogs in the application. - // - // var app = angular.module('App', ['ui.bootstrap.dialog'], function($dialogProvider){ - // // don't close dialog when backdrop is clicked by default - // $dialogProvider.options({backdropClick: false}); - // }); - this.options = function(value){ - globalOptions = value; - }; - - // Returns the actual `$dialog` service that is injected in controllers - this.$get = ["$http", "$document", "$compile", "$rootScope", "$controller", "$templateCache", "$q", "$transition", "$injector", - function ($http, $document, $compile, $rootScope, $controller, $templateCache, $q, $transition, $injector) { - - var body = $document.find('body'); - - function createElement(clazz) { - var el = angular.element("
"); - el.addClass(clazz); - return el; - } - - // The `Dialog` class represents a modal dialog. The dialog class can be invoked by providing an options object - // containing at lest template or templateUrl and controller: - // - // var d = new Dialog({templateUrl: 'foo.html', controller: 'BarController'}); - // - // Dialogs can also be created using templateUrl and controller as distinct arguments: - // - // var d = new Dialog('path/to/dialog.html', MyDialogController); - function Dialog(opts) { - - var self = this, options = this.options = angular.extend({}, defaults, globalOptions, opts); - this._open = false; - - this.backdropEl = createElement(options.backdropClass); - if(options.backdropFade){ - this.backdropEl.addClass(options.transitionClass); - this.backdropEl.removeClass(options.triggerClass); - } - - this.modalEl = createElement(options.dialogClass); - if(options.dialogFade){ - this.modalEl.addClass(options.transitionClass); - this.modalEl.removeClass(options.triggerClass); - } - - this.handledEscapeKey = function(e) { - if (e.which === 27) { - self.close(); - e.preventDefault(); - self.$scope.$apply(); - } - }; - - this.handleBackDropClick = function(e) { - self.close(); - e.preventDefault(); - self.$scope.$apply(); - }; - } - - // The `isOpen()` method returns wether the dialog is currently visible. - Dialog.prototype.isOpen = function(){ - return this._open; - }; - - // The `open(templateUrl, controller)` method opens the dialog. - // Use the `templateUrl` and `controller` arguments if specifying them at dialog creation time is not desired. - Dialog.prototype.open = function(templateUrl, controller){ - var self = this, options = this.options; - - if(templateUrl){ - options.templateUrl = templateUrl; - } - if(controller){ - options.controller = controller; - } - - if(!(options.template || options.templateUrl)) { - throw new Error('Dialog.open expected template or templateUrl, neither found. Use options or open method to specify them.'); - } - - this._loadResolves().then(function(locals) { - var $scope = locals.$scope = self.$scope = locals.$scope ? locals.$scope : $rootScope.$new(); - - self.modalEl.html(locals.$template); - - if (self.options.controller) { - var ctrl = $controller(self.options.controller, locals); - self.modalEl.children().data('ngControllerController', ctrl); - } - - $compile(self.modalEl)($scope); - self._addElementsToDom(); - - // trigger tranisitions - setTimeout(function(){ - if(self.options.dialogFade){ self.modalEl.addClass(self.options.triggerClass); } - if(self.options.backdropFade){ self.backdropEl.addClass(self.options.triggerClass); } - }); - body.addClass(defaults.dialogOpenClass); - self._bindEvents(); - }); - - this.deferred = $q.defer(); - return this.deferred.promise; - }; - - // closes the dialog and resolves the promise returned by the `open` method with the specified result. - Dialog.prototype.close = function(result){ - var self = this; - var fadingElements = this._getFadingElements(); - - if(fadingElements.length > 0){ - for (var i = fadingElements.length - 1; i >= 0; i--) { - $transition(fadingElements[i], removeTriggerClass).then(onCloseComplete); - } - return; - } - - this._onCloseComplete(result); - - function removeTriggerClass(el){ - el.removeClass(self.options.triggerClass); - } - - function onCloseComplete(){ - if(self._open){ - self._onCloseComplete(result); - } - } - }; - - Dialog.prototype._getFadingElements = function(){ - var elements = []; - if(this.options.dialogFade){ - elements.push(this.modalEl); - } - if(this.options.backdropFade){ - elements.push(this.backdropEl); - } - - return elements; - }; - - Dialog.prototype._bindEvents = function() { - if(this.options.keyboard){ body.bind('keydown', this.handledEscapeKey); } - if(this.options.backdrop && this.options.backdropClick){ this.backdropEl.bind('click', this.handleBackDropClick); } - }; - - Dialog.prototype._unbindEvents = function() { - if(this.options.keyboard){ body.unbind('keydown', this.handledEscapeKey); } - if(this.options.backdrop && this.options.backdropClick){ this.backdropEl.unbind('click', this.handleBackDropClick); } - }; - - Dialog.prototype._onCloseComplete = function(result) { - this._removeElementsFromDom(); - this._unbindEvents(); - body.removeClass(defaults.dialogOpenClass); - this.deferred.resolve(result); - }; - - Dialog.prototype._addElementsToDom = function(){ - body.append(this.modalEl); - - if(this.options.backdrop) { - if (activeBackdrops.value === 0) { - body.append(this.backdropEl); - } - activeBackdrops.value++; - } - - this._open = true; - }; - - Dialog.prototype._removeElementsFromDom = function(){ - this.modalEl.remove(); - - if(this.options.backdrop) { - activeBackdrops.value--; - if (activeBackdrops.value === 0) { - this.backdropEl.remove(); - } - } - this._open = false; - }; - - // Loads all `options.resolve` members to be used as locals for the controller associated with the dialog. - Dialog.prototype._loadResolves = function(){ - var values = [], keys = [], templatePromise, self = this; - - if (this.options.template) { - templatePromise = $q.when(this.options.template); - } else if (this.options.templateUrl) { - templatePromise = $http.get(this.options.templateUrl, {cache:$templateCache}) - .then(function(response) { return response.data; }); - } - - angular.forEach(this.options.resolve || [], function(value, key) { - keys.push(key); - values.push(angular.isString(value) ? $injector.get(value) : $injector.invoke(value)); - }); - - keys.push('$template'); - values.push(templatePromise); - - return $q.all(values).then(function(values) { - var locals = {}; - angular.forEach(values, function(value, index) { - locals[keys[index]] = value; - }); - locals.dialog = self; - return locals; - }); - }; - - // The actual `$dialog` service that is injected in controllers. - return { - // Creates a new `Dialog` with the specified options. - dialog: function(opts){ - return new Dialog(opts); - }, - // creates a new `Dialog` tied to the default message box template and controller. - // - // Arguments `title` and `message` are rendered in the modal header and body sections respectively. - // The `buttons` array holds an object with the following members for each button to include in the - // modal footer section: - // - // * `result`: the result to pass to the `close` method of the dialog when the button is clicked - // * `label`: the label of the button - // * `cssClass`: additional css class(es) to apply to the button for styling - messageBox: function(title, message, buttons){ - return new Dialog({templateUrl: 'template/dialog/message.html', controller: 'MessageBoxController', resolve: - {model: function() { - return { - title: title, - message: message, - buttons: buttons - }; - } - }}); - } - }; - }]; -}); diff --git a/src/dialog/docs/demo.html b/src/dialog/docs/demo.html deleted file mode 100644 index b4e3cc8dcb..0000000000 --- a/src/dialog/docs/demo.html +++ /dev/null @@ -1,19 +0,0 @@ -
-
-
- - - -
- - -
-
-

Change options at will and press the open dialog button below!

-

- -

Alternatively open a simple message box:

-

-
-
-
diff --git a/src/dialog/docs/demo.js b/src/dialog/docs/demo.js deleted file mode 100644 index 6b9756c93b..0000000000 --- a/src/dialog/docs/demo.js +++ /dev/null @@ -1,50 +0,0 @@ -function DialogDemoCtrl($scope, $dialog){ - - // Inlined template for demo - var t = ''+ - ''+ - ''; - - $scope.opts = { - backdrop: true, - keyboard: true, - backdropClick: true, - template: t, // OR: templateUrl: 'path/to/view.html', - controller: 'TestDialogController' - }; - - $scope.openDialog = function(){ - var d = $dialog.dialog($scope.opts); - d.open().then(function(result){ - if(result) - { - alert('dialog closed with result: ' + result); - } - }); - }; - - $scope.openMessageBox = function(){ - var title = 'This is a message box'; - var msg = 'This is the content of the message box'; - var btns = [{result:'cancel', label: 'Cancel'}, {result:'ok', label: 'OK', cssClass: 'btn-primary'}]; - - $dialog.messageBox(title, msg, btns) - .open() - .then(function(result){ - alert('dialog closed with result: ' + result); - }); - }; -} - -// the dialog is injected in the specified controller -function TestDialogController($scope, dialog){ - $scope.close = function(result){ - dialog.close(result); - }; -} diff --git a/src/dialog/docs/readme.md b/src/dialog/docs/readme.md deleted file mode 100644 index e45f967a85..0000000000 --- a/src/dialog/docs/readme.md +++ /dev/null @@ -1,6 +0,0 @@ -The `$dialog` service allows you to open dialogs and message boxes from within your controllers. Very useful in case loading dialog content in the DOM up-front is tedious or not desired. - -Creating custom dialogs is straightforward: create a partial view, its controller and reference them when using the service. -Generic message boxes (title, message and buttons) are also provided for your convenience. - -For more information, see the [dialog readme](https://github.com/angular-ui/bootstrap/blob/master/src/dialog/README.md) on github. \ No newline at end of file diff --git a/src/dialog/test/dialog.spec.js b/src/dialog/test/dialog.spec.js deleted file mode 100644 index 1998bf0466..0000000000 --- a/src/dialog/test/dialog.spec.js +++ /dev/null @@ -1,309 +0,0 @@ -describe('Given ui.bootstrap.dialog', function(){ - - var $document, $compile, $scope, $rootScope, $dialog, q, provider; - var template = '
I\'m a template
'; - - beforeEach(module('ui.bootstrap.dialog')); - beforeEach(module('template/dialog/message.html')); - - beforeEach(function(){ - module(function($dialogProvider){ - provider = $dialogProvider; - }); - inject(function(_$document_, _$compile_, _$rootScope_, _$dialog_, _$q_){ - $document = _$document_; - $compile = _$compile_; - $scope = _$rootScope_.$new(); - $rootScope = _$rootScope_; - $dialog = _$dialog_; - q = _$q_; - }); - }); - - // clean-up after ourselves - afterEach(function(){ - closeDialog(); - clearGlobalOptions(); - }); - - it('provider service should be injected', function(){ - expect(provider).toBeDefined(); - }); - - it('dialog service should be injected', function(){ - expect($dialog).toBeDefined(); - }); - - var dialog; - - var createDialog = function(opts){ - dialog = $dialog.dialog(opts); - }; - - var openDialog = function(templateUrl, controller){ - dialog.open(templateUrl, controller); - $scope.$apply(); - }; - - var closeDialog = function(result){ - if(dialog){ - dialog.close(result); - $rootScope.$apply(); - } - }; - - var setGlobalOptions = function(opts){ - provider.options(opts); - }; - - var clearGlobalOptions = function(){ - provider.options({}); - }; - - - var dialogShouldBeClosed = function(){ - it('should not include a backdrop in the DOM', function(){ - expect($document.find('body > div.modal-backdrop').length).toBe(0); - }); - - it('should not include the modal in the DOM', function(){ - expect($document.find('body > div.modal').length).toBe(0); - }); - - it('should return false for isOpen()', function(){ - expect(dialog.isOpen()).toBeFalsy(); - }); - }; - - var dialogShouldBeOpen = function(){ - it('the dialog.isOpen() should be true', function(){ - expect(dialog.isOpen()).toBe(true); - }); - - it('the backdrop should be displayed', function(){ - expect($document.find('body > div.modal-backdrop').css('display')).toBe('block'); - }); - - it('the modal should be displayed', function(){ - expect($document.find('body > div.modal').css('display')).toBe('block'); - }); - }; - - describe('Given global option', function(){ - - var useDialogWithGlobalOption = function(opts){ - beforeEach(function(){ - setGlobalOptions(opts); - createDialog({template:template}); - openDialog(); - }); - }; - - describe('backdrop:false', function(){ - useDialogWithGlobalOption({backdrop: false}); - - it('should not include a backdrop in the DOM', function(){ - expect($document.find('body > div.modal-backdrop').length).toBe(0); - }); - - it('should include the modal in the DOM', function(){ - expect($document.find('body > div.modal').length).toBe(1); - }); - }); - - describe('dialogClass:foo, backdropClass:bar', function(){ - useDialogWithGlobalOption({dialogClass: 'foo', backdropClass: 'bar'}); - - it('backdrop class should be changed', function(){ - expect($document.find('body > div.bar').length).toBe(1); - }); - - it('the modal should be change', function(){ - expect($document.find('body > div.foo').length).toBe(1); - }); - }); - - /* - describe('dialogFade:true, backdropFade:true', function(){ - useDialogWithGlobalOption({dialogFade:true, backdropFade:true}); - - it('backdrop class should be changed', function(){ - expect($document.find('body > div.modal.fade').length).toBe(1); - }); - - it('the modal should be change', function(){ - expect($document.find('body > div.modal-backdrop.fade').length).toBe(1); - }); - });*/ - }); - - describe('Opening a dialog', function(){ - - beforeEach(function(){ - createDialog({template:template}); - openDialog(); - }); - - dialogShouldBeOpen(); - }); - - describe('When opening a dialog with a controller', function(){ - - var resolvedDialog; - function Ctrl(dialog){ - resolvedDialog = dialog; - } - - beforeEach(function(){ - createDialog({template:template, controller: Ctrl}); - openDialog(); - }); - - dialogShouldBeOpen(); - - it('should inject the current dialog in the controller', function(){ - expect(resolvedDialog).toBe(dialog); - }); - }); - - describe('When opening a dialog with resolves', function(){ - - var resolvedFoo, resolvedBar, deferred, resolveObj; - function Ctrl(foo, bar){ - resolvedFoo = foo; - resolvedBar = bar; - } - - beforeEach(function(){ - deferred = q.defer(); - resolveObj = { - foo: function(){return 'foo';}, - bar: function(){return deferred.promise;} - }; - - createDialog({template:template, resolve: resolveObj, controller: Ctrl}); - deferred.resolve('bar'); - openDialog(); - }); - - dialogShouldBeOpen(); - - it('should inject resolved promises in the controller', function(){ - expect(resolvedBar).toBe('bar'); - }); - - it('should inject simple values in the controller', function(){ - expect(resolvedFoo).toBe('foo'); - }); - }); - - describe('when closing a dialog', function(){ - - beforeEach(function(){ - createDialog({template:template}); - openDialog(); - closeDialog(); - }); - - dialogShouldBeClosed(); - - describe('When opening it again', function(){ - beforeEach(function(){ - expect($document.find('body > div.modal-backdrop').length).toBe(0); - openDialog(); - }); - - dialogShouldBeOpen(); - }); - }); - - describe('when closing a dialog with a result', function(){ - var res; - beforeEach(function(){ - createDialog({template:template}); - dialog.open().then(function(result){ res = result; }); - $rootScope.$apply(); - - closeDialog('the result'); - }); - - dialogShouldBeClosed(); - - it('should call the then method with the specified result', function(){ - expect(res).toBe('the result'); - }); - }); - - describe('when closing a dialog with backdrop click', function(){ - beforeEach(function(){ - createDialog({template:'foo'}); - openDialog(); - $document.find('body > div.modal-backdrop').click(); - }); - - dialogShouldBeClosed(); - }); - - describe('when closing a dialog with escape key', function(){ - beforeEach(function(){ - createDialog({template:'foo'}); - openDialog(); - var e = $.Event('keydown'); - e.which = 27; - $document.find('body').trigger(e); - }); - - dialogShouldBeClosed(); - }); - - describe('When opening a dialog with a template url', function(){ - - beforeEach(function(){ - createDialog({templateUrl:'template/dialog/message.html'}); - openDialog(); - }); - - dialogShouldBeOpen(); - }); - - describe('When opening a dialog by passing template and controller to open method', function(){ - - var controllerIsCreated; - function Controller($scope, dialog){ - controllerIsCreated = true; - } - - beforeEach(function(){ - createDialog({templateUrl:'this/will/not/be/used.html', controller: 'foo'}); - openDialog('template/dialog/message.html', Controller); - }); - - dialogShouldBeOpen(); - - it('should used the specified controller', function(){ - expect(controllerIsCreated).toBe(true); - }); - - it('should use the specified template', function(){ - expect($document.find('body > div.modal > div.modal-header').length).toBe(1); - }); - }); - - describe('when opening it with a template containing white-space', function(){ - - var controllerIsCreated; - function Controller($scope, dialog){ - controllerIsCreated = true; - } - - beforeEach(function(){ - createDialog({ - template:'
Has whitespace that IE8 does not like assigning data() to
', - controller: Controller - }); - openDialog(); - }); - - dialogShouldBeOpen(); - }); -}); diff --git a/src/modal/docs/demo.html b/src/modal/docs/demo.html index 524bf8bf95..9acdcba1bf 100644 --- a/src/modal/docs/demo.html +++ b/src/modal/docs/demo.html @@ -1,16 +1,22 @@
- -
+ + + +
Selection from a modal: {{ selected }}
\ No newline at end of file diff --git a/src/modal/docs/demo.js b/src/modal/docs/demo.js index ff307fea8c..0eb06c330b 100644 --- a/src/modal/docs/demo.js +++ b/src/modal/docs/demo.js @@ -1,19 +1,39 @@ -var ModalDemoCtrl = function ($scope) { +var ModalDemoCtrl = function ($scope, $modal, $log) { + + $scope.items = ['item1', 'item2', 'item3']; $scope.open = function () { - $scope.shouldBeOpen = true; - }; - $scope.close = function () { - $scope.closeMsg = 'I was closed at: ' + new Date(); - $scope.shouldBeOpen = false; + var modalInstance = $modal.open({ + templateUrl: 'myModalContent.html', + controller: ModalInstanceCtrl, + resolve: { + items: function () { + return $scope.items; + } + } + }); + + modalInstance.result.then(function (selectedItem) { + $scope.selected = selectedItem; + }, function () { + $log.info('Modal dismissed at: ' + new Date()); + }); }; +}; - $scope.items = ['item1', 'item2']; +var ModalInstanceCtrl = function ($scope, $modalInstance, items) { - $scope.opts = { - backdropFade: true, - dialogFade:true + $scope.items = items; + $scope.selected = { + item: $scope.items[0] }; + $scope.ok = function () { + $modalInstance.close($scope.selected.item); + }; + + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; }; \ No newline at end of file diff --git a/src/modal/docs/readme.md b/src/modal/docs/readme.md index f72d844c57..697a587cfd 100644 --- a/src/modal/docs/readme.md +++ b/src/modal/docs/readme.md @@ -1,5 +1,18 @@ -`modal` is a directive that reuses `$dialog` service to provide simple creation of modals that are already in your DOM without the hassle of creating partial views and controllers. +`$modal` is a s service to quickly create AngularJS-powered modal windows. +Creating custom modals is straightforward: create a partial view, its controller and reference them when using the service. -The directive shares `$dialog` global options. +The `$modal` service has only one method: `open(options)` where available options are like follows: -For more information, see the [dialog readme](https://github.com/angular-ui/bootstrap/blob/master/src/dialog/README.md) on github. +* `templateUrl` - a path to a template representing modal's content +* `scope` - a scope instance to be used for the modal's content (actually the `$modal` service is going to create a child scope of a a provided scope). Defaults to `$rootScope` +* `controller` - a controller for a modal instance - it can initialize scope used by modal. A controller can be injected with `$modalInstance` +* `resolve` - members that will be resolved and passed to the controller as locals; it is equivalent of the `resolve` property for AngularJS routes +* `backdrop` - controls presence of a backdrop. Allowed values: true (default), false (no backdrop), `'static'` - backdrop is present but modal window is not closed when clicking outside of the modal window. +* `keyboard` - indicates whether the dialog should be closable by hitting the ESC key, defaults to true + +The `open` method returns a modal instance, an object with the following properties: + +* `close(result)` - a method that can be used to close a modal, passing a result +* `dismiss(reason)` - a method that can be used to dismiss a modal, passing a reason +* `result` - a promise that is resolved when a modal is closed and rejected when a modal is dismissed +* `opened` - a promise that is resolved when a modal gets opened after downloading content's template and resolving all variables \ No newline at end of file diff --git a/src/modal/modal.js b/src/modal/modal.js index 7f8a828a6d..11a5424152 100644 --- a/src/modal/modal.js +++ b/src/modal/modal.js @@ -1,47 +1,287 @@ -angular.module('ui.bootstrap.modal', ['ui.bootstrap.dialog']) -.directive('modal', ['$parse', '$dialog', function($parse, $dialog) { - return { - restrict: 'EA', - terminal: true, - link: function(scope, elm, attrs) { - var opts = angular.extend({}, scope.$eval(attrs.uiOptions || attrs.bsOptions || attrs.options)); - var shownExpr = attrs.modal || attrs.show; - var setClosed; - - // Create a dialog with the template as the contents of the directive - // Add the current scope as the resolve in order to make the directive scope as a dialog controller scope - opts = angular.extend(opts, { - template: elm.html(), - resolve: { $scope: function() { return scope; } } - }); - var dialog = $dialog.dialog(opts); +angular.module('ui.bootstrap.modal', []) - elm.remove(); +/** + * A helper, internal data structure that acts as a map but also allows getting / removing + * elements in the LIFO order + */ + .factory('$$stackedMap', function () { + return { + createNew: function () { + var stack = []; - if (attrs.close) { - setClosed = function() { - $parse(attrs.close)(scope); + return { + add: function (key, value) { + stack.push({ + key: key, + value: value + }); + }, + get: function (key) { + for (var i = 0; i < stack.length; i++) { + if (key == stack[i].key) { + return stack[i]; + } + } + }, + top: function () { + return stack[stack.length - 1]; + }, + remove: function (key) { + var idx = -1; + for (var i = 0; i < stack.length; i++) { + if (key == stack[i].key) { + idx = i; + break; + } + } + return stack.splice(idx, 1)[0]; + }, + removeTop: function () { + return stack.splice(stack.length - 1, 1)[0]; + }, + length: function () { + return stack.length; + } }; - } else { - setClosed = function() { - if (angular.isFunction($parse(shownExpr).assign)) { - $parse(shownExpr).assign(scope, false); + } + }; + }) + +/** + * A helper directive for the $modal service. It creates a backdrop element. + */ + .directive('modalBackdrop', ['$modalStack', '$timeout', function ($modalStack, $timeout) { + return { + restrict: 'EA', + scope: {}, + replace: true, + templateUrl: 'template/modal/backdrop.html', + link: function (scope, element, attrs) { + + //trigger CSS transitions + $timeout(function () { + scope.animate = true; + }); + + scope.close = function (evt) { + var modal = $modalStack.getTop(); + //TODO: this logic is duplicated with the place where modal gets opened + if (modal && modal.window.backdrop && modal.window.backdrop != 'static') { + evt.preventDefault(); + evt.stopPropagation(); + $modalStack.dismiss(modal.instance, 'backdrop click'); } }; } + }; + }]) + + .directive('modalWindow', ['$timeout', function ($timeout) { + return { + restrict: 'EA', + scope: {}, + replace: true, + transclude: true, + templateUrl: 'template/modal/window.html', + link: function (scope, element, attrs) { + //trigger CSS transitions + $timeout(function () { + scope.animate = true; + }); + } + }; + }]) + + .factory('$modalStack', ['$document', '$compile', '$rootScope', '$$stackedMap', + function ($document, $compile, $rootScope, $$stackedMap) { + + var body = $document.find('body').eq(0); + var openedWindows = $$stackedMap.createNew(); + var $modalStack = {}; + + function removeModalWindow(modalInstance) { + + var modalWindow = openedWindows.get(modalInstance).value; + + //clean up the stack + openedWindows.remove(modalInstance); + + //remove DOM element + modalWindow.modalDomEl.remove(); + + //remove backdrop + if (modalWindow.backdropDomEl) { + modalWindow.backdropDomEl.remove(); + } + + //destroy scope + modalWindow.modalScope.$destroy(); + } - scope.$watch(shownExpr, function(isShown, oldShown) { - if (isShown) { - dialog.open().then(function(){ - setClosed(); - }); - } else { - //Make sure it is not opened - if (dialog.isOpen()){ - dialog.close(); + $document.bind('keydown', function (evt) { + var modal; + + if (evt.which === 27) { + modal = openedWindows.top(); + if (modal && modal.value.keyboard) { + $rootScope.$apply(function () { + $modalStack.dismiss(modal.key); + }); } } }); - } - }; -}]); \ No newline at end of file + + $modalStack.open = function (modalInstance, modal) { + + var backdropDomEl; + var modalDomEl = $compile(angular.element('').html(modal.content))(modal.scope); + body.append(modalDomEl); + + if (modal.backdrop) { + backdropDomEl = $compile(angular.element(''))($rootScope); + body.append(backdropDomEl); + } + + openedWindows.add(modalInstance, { + deferred: modal.deferred, + modalScope: modal.scope, + modalDomEl: modalDomEl, + backdrop: modal.backdrop, + backdropDomEl: backdropDomEl, + keyboard: modal.keyboard + }); + }; + + $modalStack.close = function (modalInstance, result) { + var modal = openedWindows.get(modalInstance); + if (modal) { + modal.value.deferred.resolve(result); + removeModalWindow(modalInstance); + } + }; + + $modalStack.dismiss = function (modalInstance, reason) { + var modalWindow = openedWindows.get(modalInstance).value; + if (modalWindow) { + modalWindow.deferred.reject(reason); + removeModalWindow(modalInstance); + } + }; + + $modalStack.getTop = function () { + var top = openedWindows.top(); + if (top) { + return { + instance: top.key, + window: top.value + }; + } + }; + + return $modalStack; + }]) + + .provider('$modal', function () { + + var defaultOptions = { + backdrop: true, //can be also false or 'static' + keyboard: true + }; + + return { + options: defaultOptions, + $get: ['$injector', '$rootScope', '$q', '$http', '$templateCache', '$controller', '$modalStack', + function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) { + + var $modal = {}; + + function getTemplatePromise(options) { + return options.template ? $q.when(options.template) : + $http.get(options.templateUrl, {cache: $templateCache}).then(function (result) { + return result.data; + }); + } + + function getResolvePromises(resolves) { + var promisesArr = []; + angular.forEach(resolves, function (value, key) { + if (angular.isFunction(value) || angular.isArray(value)) { + promisesArr.push($q.when($injector.invoke(value))); + } + }); + return promisesArr; + } + + $modal.open = function (modalOptions) { + + var modalResultDeferred = $q.defer(); + var modalOpenedDeferred = $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, + close: function (result) { + $modalStack.close(this, result); + }, + dismiss: function (reason) { + $modalStack.dismiss(this, reason); + } + }; + + //merge and clean up options + modalOptions = angular.extend(defaultOptions, 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))); + + + templateAndResolvePromise.then(function resolveSuccess(tplAndVars) { + + var modalScope = (modalOptions.scope || $rootScope).$new(); + + var ctrlInstance, ctrlLocals = {}; + var resolveIter = 1; + + //controllers + if (modalOptions.controller) { + ctrlLocals.$scope = modalScope; + ctrlLocals.$modalInstance = modalInstance; + angular.forEach(modalOptions.resolve, function (value, key) { + ctrlLocals[key] = tplAndVars[resolveIter++]; + }); + + ctrlInstance = $controller(modalOptions.controller, ctrlLocals); + } + + $modalStack.open(modalInstance, { + scope: modalScope, + deferred: modalResultDeferred, + content: tplAndVars[0], + backdrop: modalOptions.backdrop, + keyboard: modalOptions.keyboard + }); + + }, function resolveError(reason) { + modalResultDeferred.reject(reason); + }); + + templateAndResolvePromise.then(function () { + modalOpenedDeferred.resolve(true); + }, function () { + modalOpenedDeferred.reject(false); + }); + + return modalInstance; + }; + + return $modal; + }] + }; + }); \ No newline at end of file diff --git a/src/modal/test/modal.spec.js b/src/modal/test/modal.spec.js index e212b0e14a..0182a68058 100644 --- a/src/modal/test/modal.spec.js +++ b/src/modal/test/modal.spec.js @@ -1,171 +1,403 @@ -describe('Give ui.boostrap.modal', function() { - - var $document, $compile, $scope, $rootScope, provider; - - beforeEach(module('ui.bootstrap.modal')); - - beforeEach(function(){ - module(function($dialogProvider){ - provider = $dialogProvider; - }); - inject(function(_$document_, _$compile_, _$rootScope_){ - $document = _$document_; - $compile = _$compile_; - $scope = _$rootScope_.$new(); - $rootScope = _$rootScope_; - }); - }); - - var elm; - - var templateGenerator = function(expr, scopeExpressionContent, closeExpr) { - var additionalExpression = scopeExpressionContent ? scopeExpressionContent : ''; - var closingExpr = closeExpr ? ' close="' + closeExpr + '" ': ''; - return '
' + - additionalExpression + 'Hello!
'; - }; - - it('should have just one backdrop', function() { - var numberOfSimultaneousModals = 5; - var elems = []; - for (var i = 0; i< 5; i++) { - elems[i] = $compile(templateGenerator('modalShown' + i))($scope); - $scope.$apply('modalShown' + i + ' = true'); - } - expect($document.find('body > div.modal-backdrop').length).toBe(1); - expect($document.find('body > div.modal').length).toBe(numberOfSimultaneousModals); - - for (i = 0; i< 5; i++) { - $scope.$apply('modalShown' + i + ' = false'); - } - }); - - it('should work with expression instead of a variable', function() { - $scope.foo = true; - $scope.shown = function() { return $scope.foo; }; - elm = $compile(templateGenerator('shown()'))($scope); - $scope.$apply(); - expect($document.find('body > div.modal').length).toBe(1); - $scope.$apply('foo = false'); - expect($document.find('body > div.modal').length).toBe(0); - }); - - it('should work with a close expression and escape close', function() { - $scope.bar = true; - $scope.show = function() { return $scope.bar; }; - elm = $compile(templateGenerator('show()', ' ', 'bar=false'))($scope); - $scope.$apply(); - expect($document.find('body > div.modal').length).toBe(1); - var e = $.Event('keydown'); - e.which = 27; - $document.find('body').trigger(e); - expect($document.find('body > div.modal').length).toBe(0); - expect($scope.bar).not.toBeTruthy(); - }); - - it('should work with a close expression and backdrop close', function() { - $scope.baz = 1; - $scope.hello = function() { return $scope.baz===1; }; - elm = $compile(templateGenerator('hello()', ' ', 'baz=0'))($scope); - $scope.$apply(); - expect($document.find('body > div.modal').length).toBe(1); - $document.find('body > div.modal-backdrop').click(); - expect($document.find('body > div.modal').length).toBe(0); - expect($scope.baz).toBe(0); - }); - - it('should not close on escape if option is false', function() { - $scope.modalOpts = {keyboard:false}; - elm = $compile(templateGenerator('modalShown'))($scope); - $scope.modalShown = true; - $scope.$apply(); - var e = $.Event('keydown'); - e.which = 27; - expect($document.find('body > div.modal').length).toBe(1); - $document.find('body').trigger(e); - expect($document.find('body > div.modal').length).toBe(1); - $scope.$apply('modalShown = false'); - }); - - it('should not close on backdrop click if option is false', function() { - $scope.modalOpts = {backdropClick:false}; - elm = $compile(templateGenerator('modalShown'))($scope); - $scope.modalShown = true; - $scope.$apply(); - expect($document.find('body > div.modal').length).toBe(1); - $document.find('body > div.modal-backdrop').click(); - expect($document.find('body > div.modal').length).toBe(1); - $scope.$apply('modalShown = false'); - }); - - it('should use global $dialog options', function() { - elm = $compile(templateGenerator('modalShown'))($scope); - expect($document.find('.test-open-modal').length).toBe(0); - $scope.$apply('modalShown = true'); - expect($document.find('body > div.modal').length).toBe(1); - $scope.$apply('modalShown = false'); - }); - - describe('dialog generated should have directives scope', function() { - - afterEach(function() { - $scope.$apply('modalShown = false'); - }); - - it('should call scope methods', function() { - var clickSpy = jasmine.createSpy('localScopeFunction'); - $scope.myFunc = clickSpy; - elm = $compile(templateGenerator('modalShown', ''))($scope); - $scope.$apply('modalShown = true'); - $document.find('body > div.modal button').click(); - expect(clickSpy).toHaveBeenCalled(); - }); - - it('should resolve scope vars', function() { - $scope.buttonName = 'my button'; - elm = $compile(templateGenerator('modalShown', ''))($scope); - $scope.$apply('modalShown = true'); - expect($document.find('body > div.modal button').text()).toBe('my button'); - }); - - }); - - describe('toogle modal dialog on model change', function() { - - beforeEach(function(){ - elm = $compile(templateGenerator('modalShown'))($scope); - $scope.$apply('modalShown = true'); - }); - - afterEach(function() { - $scope.$apply('modalShown = false'); - }); - - it('the backdrop should be displayed if specified (true by default)', function(){ - expect($document.find('body > div.modal-backdrop').css('display')).toBe('block'); - }); - - it('the modal should be displayed', function(){ - expect($document.find('body > div.modal').css('display')).toBe('block'); - }); - - it('the modal should not be displayed', function(){ - $scope.$apply('modalShown = false'); - expect($document.find('body > div.modal').length).toBe(0); - }); - - it('should update the model if the backdrop is clicked', function() { - $document.find('body > div.modal-backdrop').click(); - $scope.$digest(); - expect($scope.modalShown).not.toBeTruthy(); - }); - - it('should update the model if the esc is pressed', function() { - var e = $.Event('keydown'); - e.which = 27; - $document.find('body').trigger(e); - $scope.$digest(); - expect($scope.modalShown).not.toBeTruthy(); - }); - }); +describe('$modal', function () { + var $rootScope, $document, $compile, $templateCache, $timeout, $q; + var $modal, $modalProvider; + + var triggerKeyDown = function (element, keyCode) { + var e = $.Event("keydown"); + e.which = keyCode; + element.trigger(e); + }; + + beforeEach(module('ui.bootstrap.modal')); + beforeEach(module('template/modal/backdrop.html')); + beforeEach(module('template/modal/window.html')); + beforeEach(module(function(_$modalProvider_){ + $modalProvider = _$modalProvider_; + })); + + beforeEach(inject(function (_$rootScope_, _$document_, _$compile_, _$templateCache_, _$timeout_, _$q_, _$modal_) { + $rootScope = _$rootScope_; + $document = _$document_; + $compile = _$compile_; + $templateCache = _$templateCache_; + $timeout = _$timeout_; + $q = _$q_; + $modal = _$modal_; + })); + + beforeEach(inject(function ($rootScope) { + this.addMatchers({ + + toBeResolvedWith: function(value) { + var resolved; + this.message = function() { + return "Expected '" + angular.mock.dump(this.actual) + "' to be resolved with '" + value + "'."; + }; + this.actual.then(function(result){ + resolved = result; + }); + $rootScope.$digest(); + + return resolved === value; + }, + + toBeRejectedWith: function(value) { + var rejected; + this.message = function() { + return "Expected '" + angular.mock.dump(this.actual) + "' to be rejected with '" + value + "'."; + }; + this.actual.then(angular.noop, function(reason){ + rejected = reason; + }); + $rootScope.$digest(); + + return rejected === value; + }, + + toHaveModalOpenWithContent: function(content, selector) { + + var contentToCompare, modalDomEls = this.actual.find('body > div.modal'); + + this.message = function() { + return "Expected '" + angular.mock.dump(modalDomEls) + "' to be open with '" + content + "'."; + }; + + contentToCompare = selector ? modalDomEls.find(selector) : modalDomEls; + return modalDomEls.css('display') === 'block' && contentToCompare.html() == content; + }, + + toHaveModalsOpen: function(noOfModals) { + + var modalDomEls = this.actual.find('body > div.modal'); + return modalDomEls.length === noOfModals; + }, + + toHaveBackdrop: function() { + + var backdropDomEls = this.actual.find('body > div.modal-backdrop'); + this.message = function() { + return "Expected '" + angular.mock.dump(backdropDomEls) + "' to be a backdrop element'."; + }; + + return backdropDomEls.length === 1; + } + }); + })); + + afterEach(function () { + var body = $document.find('body'); + body.find('div.modal').remove(); + body.find('div.modal-backdrop').remove(); + }); + + function open(modalOptions) { + var modal = $modal.open(modalOptions); + $rootScope.$digest(); + return modal; + } + + function close(modal, result) { + modal.close(result); + $rootScope.$digest(); + } + + function dismiss(modal, reason) { + modal.dismiss(reason); + $rootScope.$digest(); + } + + describe('basic scenarios with default options', function () { + + it('should open and dismiss a modal with a minimal set of options', function () { + + var modal = open({template: '
Content
'}); + + expect($document).toHaveModalsOpen(1); + expect($document).toHaveModalOpenWithContent('Content', 'div'); + expect($document).toHaveBackdrop(); + + dismiss(modal, 'closing in test'); + + expect($document).toHaveModalsOpen(0); + expect($document).not.toHaveBackdrop(); + }); + + it('should open a modal from templateUrl', function () { + + $templateCache.put('content.html', '
URL Content
'); + var modal = open({templateUrl: 'content.html'}); + + expect($document).toHaveModalsOpen(1); + expect($document).toHaveModalOpenWithContent('URL Content', 'div'); + expect($document).toHaveBackdrop(); + + dismiss(modal, 'closing in test'); + + expect($document).toHaveModalsOpen(0); + expect($document).not.toHaveBackdrop(); + }); + + it('should support closing on ESC', function () { + + var modal = open({template: '
Content
'}); + expect($document).toHaveModalsOpen(1); + + triggerKeyDown($document, 27); + $rootScope.$digest(); + + expect($document).toHaveModalsOpen(0); + }); + + it('should support closing on backdrop click', function () { + + var modal = open({template: '
Content
'}); + expect($document).toHaveModalsOpen(1); + + $document.find('body > div.modal-backdrop').click(); + $rootScope.$digest(); + + expect($document).toHaveModalsOpen(0); + }); + + it('should resolve returned promise on close', function () { + var modal = open({template: '
Content
'}); + close(modal, 'closed ok'); + + expect(modal.result).toBeResolvedWith('closed ok'); + }); + + it('should reject returned promise on dismiss', function () { + + var modal = open({template: '
Content
'}); + dismiss(modal, 'esc'); + + expect(modal.result).toBeRejectedWith('esc'); + }); + + it('should expose a promise linked to the templateUrl / resolve promises', function () { + var modal = open({template: '
Content
', resolve: { + ok: function() {return $q.when('ok');} + }} + ); + expect(modal.opened).toBeResolvedWith(true); + }); + + it('should expose a promise linked to the templateUrl / resolve promises and reject it if needed', function () { + var modal = open({template: '
Content
', resolve: { + ok: function() {return $q.reject('ko');} + }} + ); + expect(modal.opened).toBeRejectedWith(false); + }); + + }); + + describe('default options can be changed in a provider', function () { + + it('should allow overriding default options in a provider', function () { + + $modalProvider.options.backdrop = false; + var modal = open({template: '
Content
'}); + + expect($document).toHaveModalOpenWithContent('Content', 'div'); + expect($document).not.toHaveBackdrop(); + }); + }); + + describe('option by option', function () { + + describe('template and templateUrl', function () { + + it('should throw an error if none of template and templateUrl are provided', function () { + expect(function(){ + var modal = open({}); + }).toThrow(new Error('One of template or templateUrl options is required.')); + }); + + it('should not fail if a templateUrl contains leading / trailing white spaces', function () { + + $templateCache.put('whitespace.html', '
Whitespaces
'); + open({templateUrl: 'whitespace.html'}); + expect($document).toHaveModalOpenWithContent('Whitespaces', 'div'); + }); + + }); + + describe('controllers', function () { + + it('should accept controllers and inject modal instances', function () { + + var TestCtrl = function($scope, $modalInstance) { + $scope.fromCtrl = 'Content from ctrl'; + $scope.isModalInstance = angular.isObject($modalInstance) && angular.isFunction($modalInstance.close); + }; + + var modal = open({template: '
{{fromCtrl}} {{isModalInstance}}
', controller: TestCtrl}); + expect($document).toHaveModalOpenWithContent('Content from ctrl true', 'div'); + }); + }); + + describe('resolve', function () { + + var ExposeCtrl = function($scope, value) { + $scope.value = value; + }; + + function modalDefinition(template, resolve) { + return { + template: template, + controller: ExposeCtrl, + resolve: resolve + }; + } + + it('should resolve simple values', function () { + open(modalDefinition('
{{value}}
', { + value: function () { + return 'Content from resolve'; + } + })); + + expect($document).toHaveModalOpenWithContent('Content from resolve', 'div'); + }); + + it('should delay showing modal if one of the resolves is a promise', function () { + + open(modalDefinition('
{{value}}
', { + value: function () { + return $timeout(function(){ return 'Promise'; }, 100); + } + })); + expect($document).toHaveModalsOpen(0); + + $timeout.flush(); + expect($document).toHaveModalOpenWithContent('Promise', 'div'); + }); + + it('should not open dialog (and reject returned promise) if one of resolve fails', function () { + + var deferred = $q.defer(); + + var modal = open(modalDefinition('
{{value}}
', { + value: function () { + return deferred.promise; + } + })); + expect($document).toHaveModalsOpen(0); + + deferred.reject('error in test'); + $rootScope.$digest(); + + expect($document).toHaveModalsOpen(0); + expect(modal.result).toBeRejectedWith('error in test'); + }); + + it('should support injection with minification-safe syntax in resolve functions', function () { + + open(modalDefinition('
{{value.id}}
', { + value: ['$locale', function (e) { + return e; + }] + })); + + expect($document).toHaveModalOpenWithContent('en-us', 'div'); + }); + + //TODO: resolves with dependency injection - do we want to support them? + }); + + describe('scope', function () { + + it('should custom scope if provided', function () { + var $scope = $rootScope.$new(); + $scope.fromScope = 'Content from custom scope'; + open({ + template: '
{{fromScope}}
', + scope: $scope + }); + expect($document).toHaveModalOpenWithContent('Content from custom scope', 'div'); + }); + }); + + describe('keyboard', function () { + + it('should not close modals if keyboard option is set to false', function () { + open({ + template: '
No keyboard
', + keyboard: false + }); + + expect($document).toHaveModalsOpen(1); + + triggerKeyDown($document, 27); + $rootScope.$digest(); + + expect($document).toHaveModalsOpen(1); + }); + }); + + describe('backdrop', function () { + + it('should not have any backdrop element if backdrop set to false', function () { + open({ + template: '
No backdrop
', + backdrop: false + }); + expect($document).toHaveModalOpenWithContent('No backdrop', 'div'); + expect($document).not.toHaveBackdrop(); + }); + + it('should not close modal on backdrop click if backdrop is specified as "static"', function () { + open({ + template: '
Static backdrop
', + backdrop: 'static' + }); + + $document.find('body > div.modal-backdrop').click(); + $rootScope.$digest(); + + expect($document).toHaveModalOpenWithContent('Static backdrop', 'div'); + expect($document).toHaveBackdrop(); + }); + }); + }); + + describe('multiple modals', function () { + + it('it should allow opening of multiple modals', function () { + + var modal1 = open({template: '
Modal1
'}); + var modal2 = open({template: '
Modal2
'}); + expect($document).toHaveModalsOpen(2); + + dismiss(modal2); + expect($document).toHaveModalsOpen(1); + expect($document).toHaveModalOpenWithContent('Modal1', 'div'); + + dismiss(modal1); + expect($document).toHaveModalsOpen(0); + }); + + it('should not close any modals on ESC if the topmost one does not allow it', function () { + + var modal1 = open({template: '
Modal1
'}); + var modal2 = open({template: '
Modal2
', keyboard: false}); + + triggerKeyDown($document, 27); + $rootScope.$digest(); + + expect($document).toHaveModalsOpen(2); + }); + + it('should not close any modals on click if a topmost modal does not have backdrop', function () { + + var modal1 = open({template: '
Modal1
'}); + var modal2 = open({template: '
Modal2
', backdrop: false}); + + $document.find('body > div.modal-backdrop').click(); + $rootScope.$digest(); + + expect($document).toHaveModalsOpen(2); + }); + }); }); \ No newline at end of file diff --git a/src/modal/test/stackedMap.spec.js b/src/modal/test/stackedMap.spec.js new file mode 100644 index 0000000000..d1a9d68cfe --- /dev/null +++ b/src/modal/test/stackedMap.spec.js @@ -0,0 +1,50 @@ +describe('stacked map', function () { + + var stackedMap; + + beforeEach(module('ui.bootstrap.modal')); + beforeEach(inject(function ($$stackedMap) { + stackedMap = $$stackedMap.createNew(); + })); + + it('should add and remove objects by key', function () { + + stackedMap.add('foo', 'foo_value'); + expect(stackedMap.length()).toEqual(1); + expect(stackedMap.get('foo').key).toEqual('foo'); + expect(stackedMap.get('foo').value).toEqual('foo_value'); + + stackedMap.remove('foo'); + expect(stackedMap.length()).toEqual(0); + expect(stackedMap.get('foo')).toBeUndefined(); + }); + + it('should get topmost element', function () { + + stackedMap.add('foo', 'foo_value'); + stackedMap.add('bar', 'bar_value'); + expect(stackedMap.length()).toEqual(2); + + expect(stackedMap.top().key).toEqual('bar'); + expect(stackedMap.length()).toEqual(2); + }); + + it('should remove topmost element', function () { + + stackedMap.add('foo', 'foo_value'); + stackedMap.add('bar', 'bar_value'); + + expect(stackedMap.removeTop().key).toEqual('bar'); + expect(stackedMap.removeTop().key).toEqual('foo'); + }); + + it('should preserve semantic of an empty stackedMap', function () { + + expect(stackedMap.length()).toEqual(0); + expect(stackedMap.top()).toBeUndefined(); + }); + + it('should ignore removal of non-existing elements', function () { + expect(stackedMap.remove('non-existing')).toBeUndefined(); + }); +}); \ No newline at end of file diff --git a/template/modal/backdrop.html b/template/modal/backdrop.html new file mode 100644 index 0000000000..da5d62ad54 --- /dev/null +++ b/template/modal/backdrop.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/template/modal/window.html b/template/modal/window.html new file mode 100644 index 0000000000..e83cf51c75 --- /dev/null +++ b/template/modal/window.html @@ -0,0 +1 @@ + \ No newline at end of file