From 3d01c597b95994bba6c1f81a21fc4cad06ce1f2f Mon Sep 17 00:00:00 2001 From: Wesley Cho Date: Tue, 18 Aug 2015 07:33:29 -0700 Subject: [PATCH] feat(modal): support multiple open classes - Support multiple modal open classes when multiple modals are open Closes #4226 Fixes #4184 --- src/modal/modal.js | 71 +++++++++++++++++++++++++++++++-- src/modal/test/modal.spec.js | 46 +++++++++++++++++++++ src/modal/test/multiMap.spec.js | 58 +++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 src/modal/test/multiMap.spec.js diff --git a/src/modal/modal.js b/src/modal/modal.js index 68a458eb9f..b1f339dd84 100644 --- a/src/modal/modal.js +++ b/src/modal/modal.js @@ -54,6 +54,61 @@ angular.module('ui.bootstrap.modal', []) }; }) +/** + * A helper, internal data structure that stores all references attached to key + */ + .factory('$$multiMap', function() { + return { + createNew: function() { + var map = {}; + + return { + entries: function() { + return Object.keys(map).map(function(key) { + return { + key: key, + value: map[key] + }; + }); + }, + get: function(key) { + return map[key]; + }, + hasKey: function(key) { + return !!map[key]; + }, + keys: function() { + return Object.keys(map); + }, + put: function(key, value) { + if (!map[key]) { + map[key] = []; + } + + map[key].push(value); + }, + remove: function(key, value) { + var values = map[key]; + + if (!values) { + return; + } + + var idx = values.indexOf(value); + + if (idx !== -1) { + values.splice(idx, 1); + } + + if (!values.length) { + delete map[key]; + } + } + }; + } + }; + }) + /** * A helper directive for the $modal service. It creates a backdrop element. */ @@ -220,10 +275,12 @@ angular.module('ui.bootstrap.modal', []) '$animate', '$timeout', '$document', '$compile', '$rootScope', '$q', '$injector', + '$$multiMap', '$$stackedMap', function($animate , $timeout , $document , $compile , $rootScope , $q, $injector, + $$multiMap, $$stackedMap) { var $animateCss = null; @@ -235,6 +292,7 @@ angular.module('ui.bootstrap.modal', []) var backdropDomEl, backdropScope; var openedWindows = $$stackedMap.createNew(); + var openedClasses = $$multiMap.createNew(); var $modalStack = { NOW_CLOSING_EVENT: 'modal.stack.now-closing' }; @@ -264,7 +322,6 @@ angular.module('ui.bootstrap.modal', []) }); function removeModalWindow(modalInstance, elementToReceiveFocus) { - var body = $document.find('body').eq(0); var modalWindow = openedWindows.get(modalInstance).value; @@ -272,7 +329,9 @@ angular.module('ui.bootstrap.modal', []) openedWindows.remove(modalInstance); removeAfterAnimate(modalWindow.modalDomEl, modalWindow.modalScope, function() { - body.toggleClass(modalWindow.openedClass || OPENED_MODAL_CLASS, openedWindows.length() > 0); + var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS; + openedClasses.remove(modalBodyClass, modalInstance); + body.toggleClass(modalBodyClass, openedClasses.hasKey(modalBodyClass)); }); checkRemoveBackdrop(); @@ -377,7 +436,8 @@ angular.module('ui.bootstrap.modal', []) }); $modalStack.open = function(modalInstance, modal) { - var modalOpener = $document[0].activeElement; + var modalOpener = $document[0].activeElement, + modalBodyClass = modal.openedClass || OPENED_MODAL_CLASS; openedWindows.add(modalInstance, { deferred: modal.deferred, @@ -388,6 +448,8 @@ angular.module('ui.bootstrap.modal', []) openedClass: modal.openedClass }); + openedClasses.put(modalBodyClass, modalInstance); + var body = $document.find('body').eq(0), currBackdropIndex = backdropIndex(); @@ -419,7 +481,8 @@ angular.module('ui.bootstrap.modal', []) openedWindows.top().value.modalDomEl = modalDomEl; openedWindows.top().value.modalOpener = modalOpener; body.append(modalDomEl); - body.addClass(modal.openedClass || OPENED_MODAL_CLASS); + body.addClass(modalBodyClass); + $modalStack.clearFocusListCache(); }; diff --git a/src/modal/test/modal.spec.js b/src/modal/test/modal.spec.js index 456848b5fa..bbd02d62ec 100644 --- a/src/modal/test/modal.spec.js +++ b/src/modal/test/modal.spec.js @@ -798,6 +798,52 @@ describe('$modal', function () { expect(body).not.toHaveClass('foo'); }); + + it('should add multiple custom classes to the body element and remove appropriately', function() { + var modal1 = open({ + template: '
dummy modal
', + openedClass: 'foo' + }); + + expect(body).toHaveClass('foo'); + expect(body).not.toHaveClass('modal-open'); + + var modal2 = open({ + template: '
dummy modal
', + openedClass: 'bar' + }); + + expect(body).toHaveClass('foo'); + expect(body).toHaveClass('bar'); + expect(body).not.toHaveClass('modal-open'); + + var modal3 = open({ + template: '
dummy modal
', + openedClass: 'foo' + }); + + expect(body).toHaveClass('foo'); + expect(body).toHaveClass('bar'); + expect(body).not.toHaveClass('modal-open'); + + close(modal1); + + expect(body).toHaveClass('foo'); + expect(body).toHaveClass('bar'); + expect(body).not.toHaveClass('modal-open'); + + close(modal2); + + expect(body).toHaveClass('foo'); + expect(body).not.toHaveClass('bar'); + expect(body).not.toHaveClass('modal-open'); + + close(modal3); + + expect(body).not.toHaveClass('foo'); + expect(body).not.toHaveClass('bar'); + expect(body).not.toHaveClass('modal-open'); + }); }); }); diff --git a/src/modal/test/multiMap.spec.js b/src/modal/test/multiMap.spec.js new file mode 100644 index 0000000000..c89624e914 --- /dev/null +++ b/src/modal/test/multiMap.spec.js @@ -0,0 +1,58 @@ +describe('multi map', function() { + var multiMap; + + beforeEach(module('ui.bootstrap.modal')); + beforeEach(inject(function($$multiMap) { + multiMap = $$multiMap.createNew(); + })); + + it('should add and remove objects by key', function() { + multiMap.put('foo', 'bar'); + + expect(multiMap.get('foo')).toEqual(['bar']); + + multiMap.put('foo', 'baz'); + + expect(multiMap.get('foo')).toEqual(['bar', 'baz']); + + multiMap.remove('foo', 'bar'); + + expect(multiMap.get('foo')).toEqual(['baz']); + + multiMap.remove('foo', 'baz'); + + expect(multiMap.hasKey('foo')).toBe(false); + }); + + it('should support getting the keys', function() { + multiMap.put('foo', 'bar'); + multiMap.put('baz', 'boo'); + + expect(multiMap.keys()).toEqual(['foo', 'baz']); + }); + + it('should return all entries', function() { + multiMap.put('foo', 'bar'); + multiMap.put('foo', 'bar2'); + multiMap.put('baz', 'boo'); + + expect(multiMap.entries()).toEqual([ + { + key: 'foo', + value: ['bar', 'bar2'] + }, + { + key: 'baz', + value: ['boo'] + } + ]); + }); + + it('should preserve semantic of an empty key', function() { + expect(multiMap.get('key')).toBeUndefined(); + }); + + it('should respect removal of non-existing elements', function() { + expect(multiMap.remove('foo', 'bar')).toBeUndefined(); + }); +});