diff --git a/src/dropdown/dropdown.js b/src/dropdown/dropdown.js index 39789fa9f6..01bdf72e3a 100644 --- a/src/dropdown/dropdown.js +++ b/src/dropdown/dropdown.js @@ -1,14 +1,33 @@ -angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) +angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.multiMap', 'ui.bootstrap.position']) .constant('uibDropdownConfig', { appendToOpenClass: 'uib-dropdown-open', openClass: 'open' }) -.service('uibDropdownService', ['$document', '$rootScope', function($document, $rootScope) { +.service('uibDropdownService', ['$document', '$rootScope', '$$multiMap', function($document, $rootScope, $$multiMap) { var openScope = null; + var openedContainers = $$multiMap.createNew(); + + this.isOnlyOpen = function(dropdownScope, appendTo) { + var openedDropdowns = openedContainers.get(appendTo); + if (openedDropdowns) { + var openDropdown = openedDropdowns.reduce(function(toClose, dropdown) { + if (dropdown.scope === dropdownScope) { + return dropdown; + } + + return toClose; + }, {}); + if (openDropdown) { + return openedDropdowns.length === 1; + } + } - this.open = function(dropdownScope, element) { + return false; + }; + + this.open = function(dropdownScope, element, appendTo) { if (!openScope) { $document.on('click', closeDropdown); } @@ -18,14 +37,52 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) } openScope = dropdownScope; + + if (!appendTo) { + return; + } + + var openedDropdowns = openedContainers.get(appendTo); + if (openedDropdowns) { + var openedScopes = openedDropdowns.map(function(dropdown) { + return dropdown.scope; + }); + if (openedScopes.indexOf(dropdownScope) === -1) { + openedContainers.put(appendTo, { + scope: dropdownScope + }); + } + } else { + openedContainers.put(appendTo, { + scope: dropdownScope + }); + } }; - this.close = function(dropdownScope, element) { + this.close = function(dropdownScope, element, appendTo) { if (openScope === dropdownScope) { $document.off('click', closeDropdown); $document.off('keydown', this.keybindFilter); openScope = null; } + + if (!appendTo) { + return; + } + + var openedDropdowns = openedContainers.get(appendTo); + if (openedDropdowns) { + var dropdownToClose = openedDropdowns.reduce(function(toClose, dropdown) { + if (dropdown.scope === dropdownScope) { + return dropdown; + } + + return toClose; + }, {}); + if (dropdownToClose) { + openedContainers.remove(appendTo, dropdownToClose); + } + } }; var closeDropdown = function(evt) { @@ -244,10 +301,18 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) } var openContainer = appendTo ? appendTo : $element; - var hasOpenClass = openContainer.hasClass(appendTo ? appendToOpenClass : openClass); + var dropdownOpenClass = appendTo ? appendToOpenClass : openClass; + var hasOpenClass = openContainer.hasClass(dropdownOpenClass); + var isOnlyOpen = uibDropdownService.isOnlyOpen($scope, appendTo); if (hasOpenClass === !isOpen) { - $animate[isOpen ? 'addClass' : 'removeClass'](openContainer, appendTo ? appendToOpenClass : openClass).then(function() { + var toggleClass; + if (appendTo) { + toggleClass = !isOnlyOpen ? 'addClass' : 'removeClass'; + } else { + toggleClass = isOpen ? 'addClass' : 'removeClass'; + } + $animate[toggleClass](openContainer, dropdownOpenClass).then(function() { if (angular.isDefined(isOpen) && isOpen !== wasOpen) { toggleInvoker($scope, { open: !!isOpen }); } @@ -270,10 +335,10 @@ angular.module('ui.bootstrap.dropdown', ['ui.bootstrap.position']) } scope.focusToggleElement(); - uibDropdownService.open(scope, $element); + uibDropdownService.open(scope, $element, appendTo); } else { $document.off('keydown', uibDropdownService.keybindFilter); - uibDropdownService.close(scope, $element); + uibDropdownService.close(scope, $element, appendTo); if (self.dropdownMenuTemplateUrl) { if (templateScope) { templateScope.$destroy(); diff --git a/src/dropdown/index-nocss.js b/src/dropdown/index-nocss.js index 3234abba0a..690d7008ff 100644 --- a/src/dropdown/index-nocss.js +++ b/src/dropdown/index-nocss.js @@ -1,3 +1,4 @@ +require('../multiMap'); require('../position/index-nocss.js'); require('./dropdown'); diff --git a/src/dropdown/test/dropdown.spec.js b/src/dropdown/test/dropdown.spec.js index 6f3b99d4e2..dce7542ba8 100644 --- a/src/dropdown/test/dropdown.spec.js +++ b/src/dropdown/test/dropdown.spec.js @@ -99,11 +99,11 @@ describe('uib-dropdown', function() { expect(elm1).not.toHaveClass(dropdownConfig.openClass); expect(elm2).not.toHaveClass(dropdownConfig.openClass); - clickDropdownToggle( elm1 ); + clickDropdownToggle(elm1); expect(elm1).toHaveClass(dropdownConfig.openClass); expect(elm2).not.toHaveClass(dropdownConfig.openClass); - clickDropdownToggle( elm2 ); + clickDropdownToggle(elm2); expect(elm1).not.toHaveClass(dropdownConfig.openClass); expect(elm2).toHaveClass(dropdownConfig.openClass); }); @@ -303,6 +303,37 @@ describe('uib-dropdown', function() { }); }); + describe('using dropdown-append-to with two dropdowns', function() { + function dropdown() { + return $compile('
')($rootScope); + } + + beforeEach(function() { + $document.find('body').append(angular.element('')); + + $rootScope.appendTo = $document.find('#dropdown-container'); + $rootScope.log = jasmine.createSpy('log'); + + element = dropdown(); + $document.find('body').append(element); + }); + + afterEach(function() { + // Cleanup the extra elements we appended + $document.find('#dropdown-container').remove(); + }); + + it('should keep the class when toggling from one dropdown to another with the same container', function() { + var container = $document.find('#dropdown-container'); + + expect(container).not.toHaveClass('uib-dropdown-open'); + element.find('.dropdown1 [uib-dropdown-toggle]').click(); + expect(container).toHaveClass('uib-dropdown-open'); + element.find('.dropdown2 [uib-dropdown-toggle]').click(); + expect(container).toHaveClass('uib-dropdown-open'); + }); + }); + describe('using is-open', function() { describe('with uib-dropdown-toggle', function() { beforeEach(function() { diff --git a/src/modal/index-nocss.js b/src/modal/index-nocss.js index ac03531079..d3f8a94b7d 100644 --- a/src/modal/index-nocss.js +++ b/src/modal/index-nocss.js @@ -1,3 +1,4 @@ +require('../multiMap'); require('../position/index-nocss.js'); require('../stackedMap'); require('../../template/modal/window.html.js'); diff --git a/src/modal/modal.js b/src/modal/modal.js index 3fd0e9de2f..eeb79c101e 100644 --- a/src/modal/modal.js +++ b/src/modal/modal.js @@ -1,59 +1,4 @@ -angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.position']) -/** - * 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]; - } - } - }; - } - }; - }) - +angular.module('ui.bootstrap.modal', ['ui.bootstrap.multiMap', 'ui.bootstrap.stackedMap', 'ui.bootstrap.position']) /** * Pluggable resolve mechanism for the modal resolve resolution * Supports UI Router's $resolve service @@ -569,7 +514,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p ariaHiddenCount = parseInt(sibling.getAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME), 10); if (!ariaHiddenCount) { - ariaHiddenCount = elemIsAlreadyHidden ? 1 : 0; + ariaHiddenCount = elemIsAlreadyHidden ? 1 : 0; } sibling.setAttribute(ARIA_HIDDEN_ATTRIBUTE_NAME, ariaHiddenCount + 1); @@ -607,7 +552,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.p } ); } - + $modalStack.close = function(modalInstance, result) { var modalWindow = openedWindows.get(modalInstance); unhideBackgroundElements(); diff --git a/src/multiMap/index.js b/src/multiMap/index.js new file mode 100644 index 0000000000..d3db908589 --- /dev/null +++ b/src/multiMap/index.js @@ -0,0 +1 @@ +require('./multiMap.js'); diff --git a/src/multiMap/multiMap.js b/src/multiMap/multiMap.js new file mode 100644 index 0000000000..47bf6d4639 --- /dev/null +++ b/src/multiMap/multiMap.js @@ -0,0 +1,55 @@ +angular.module('ui.bootstrap.multiMap', []) +/** + * 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]; + } + } + }; + } + }; + }); diff --git a/src/modal/test/multiMap.spec.js b/src/multiMap/test/multiMap.spec.js similarity index 96% rename from src/modal/test/multiMap.spec.js rename to src/multiMap/test/multiMap.spec.js index c89624e914..40345144c9 100644 --- a/src/modal/test/multiMap.spec.js +++ b/src/multiMap/test/multiMap.spec.js @@ -1,7 +1,7 @@ describe('multi map', function() { var multiMap; - beforeEach(module('ui.bootstrap.modal')); + beforeEach(module('ui.bootstrap.multiMap')); beforeEach(inject(function($$multiMap) { multiMap = $$multiMap.createNew(); }));