diff --git a/examples/demo-append-to-body.html b/examples/demo-append-to-body.html new file mode 100644 index 000000000..951d96bb9 --- /dev/null +++ b/examples/demo-append-to-body.html @@ -0,0 +1,130 @@ + + + + + AngularJS ui-select + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Bootstrap theme

+

Selected: {{address.selected.formatted_address}}

+ + {{$select.selected.formatted_address}} + +
+
+
+

The select dropdown menu should be displayed above this element.

+
+ +
+

Select2 theme

+

Selected: {{person.selected}}

+ + {{$select.selected.name}} + +
+ + email: {{person.email}} + age: + +
+
+

The select dropdown menu should be displayed above this element.

+
+ +
+

Selectize theme

+

Selected: {{country.selected}}

+ + {{$select.selected.name}} + + + + + +

The select dropdown menu should be displayed above this element.

+
+ + diff --git a/examples/demo.js b/examples/demo.js index 623385804..bbdc1c322 100644 --- a/examples/demo.js +++ b/examples/demo.js @@ -39,7 +39,7 @@ app.filter('propsFilter', function() { }; }); -app.controller('DemoCtrl', function($scope, $http, $timeout) { +app.controller('DemoCtrl', function($scope, $http, $timeout, $interval) { $scope.disabled = undefined; $scope.searchEnabled = undefined; @@ -147,6 +147,23 @@ app.controller('DemoCtrl', function($scope, $http, $timeout) { $scope.multipleDemo.selectedPeopleWithGroupBy = [$scope.people[8], $scope.people[6]]; $scope.multipleDemo.selectedPeopleSimple = ['samantha@email.com','wladimir@email.com']; + $scope.appendToBodyDemo = { + remainingToggleTime: 0, + present: true, + startToggleTimer: function() { + var scope = $scope.appendToBodyDemo; + var promise = $interval(function() { + if (scope.remainingTime < 1000) { + $interval.cancel(promise); + scope.present = !scope.present; + scope.remainingTime = 0; + } else { + scope.remainingTime -= 1000; + } + }, 1000); + scope.remainingTime = 3000; + } + }; $scope.address = {}; $scope.refreshAddresses = function(address) { diff --git a/src/common.css b/src/common.css index 944fa6c36..6939207ad 100644 --- a/src/common.css +++ b/src/common.css @@ -36,6 +36,10 @@ display:none; } +body > .select2-container { + z-index: 9999; /* The z-index Select2 applies to the select2-drop */ +} + /* Selectize theme */ /* Helper class to show styles when focus */ @@ -116,6 +120,10 @@ margin-top: -1px; } +body > .ui-select-bootstrap { + z-index: 1000; /* Standard Bootstrap dropdown z-index */ +} + .ui-select-multiple.ui-select-bootstrap { height: auto; padding: 3px 3px 0 3px; diff --git a/src/common.js b/src/common.js index f687a7062..efc7a5b9f 100644 --- a/src/common.js +++ b/src/common.js @@ -95,7 +95,8 @@ var uis = angular.module('ui.select', []) closeOnSelect: true, generateId: function() { return latestId++; - } + }, + appendToBody: false }) // See Rename minErr and make it accessible from outside https://github.com/angular/angular.js/issues/6913 @@ -133,5 +134,25 @@ var uis = angular.module('ui.select', []) return function(matchItem, query) { return query && matchItem ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; }; -}); +}) +/** + * A read-only equivalent of jQuery's offset function: http://api.jquery.com/offset/ + * + * Taken from AngularUI Bootstrap Position: + * See https://github.com/angular-ui/bootstrap/blob/master/src/position/position.js#L70 + */ +.factory('uisOffset', + ['$document', '$window', + function ($document, $window) { + + return function(element) { + var boundingClientRect = element[0].getBoundingClientRect(); + return { + width: boundingClientRect.width || element.prop('offsetWidth'), + height: boundingClientRect.height || element.prop('offsetHeight'), + top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop), + left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft) + }; + }; +}]); diff --git a/src/uiSelectDirective.js b/src/uiSelectDirective.js index 00fe1aa28..5a030db66 100644 --- a/src/uiSelectDirective.js +++ b/src/uiSelectDirective.js @@ -1,6 +1,6 @@ uis.directive('uiSelect', - ['$document', 'uiSelectConfig', 'uiSelectMinErr', '$compile', '$parse', '$timeout', - function($document, uiSelectConfig, uiSelectMinErr, $compile, $parse, $timeout) { + ['$document', 'uiSelectConfig', 'uiSelectMinErr', 'uisOffset', '$compile', '$parse', '$timeout', + function($document, uiSelectConfig, uiSelectMinErr, uisOffset, $compile, $parse, $timeout) { return { restrict: 'EA', @@ -368,6 +368,62 @@ uis.directive('uiSelect', } element.querySelectorAll('.ui-select-choices').replaceWith(transcludedChoices); }); + + // Support for appending the select field to the body when its open + var appendToBody = scope.$eval(attrs.appendToBody); + if (appendToBody !== undefined ? appendToBody : uiSelectConfig.appendToBody) { + scope.$watch('$select.open', function(isOpen) { + if (isOpen) { + positionDropdown(); + } else { + resetDropdown(); + } + }); + + // Move the dropdown back to its original location when the scope is destroyed. Otherwise + // it might stick around when the user routes away or the select field is otherwise removed + scope.$on('$destroy', function() { + resetDropdown(); + }); + } + + // Hold on to a reference to the .ui-select-container element for appendToBody support + var placeholder = null; + + function positionDropdown() { + // Remember the absolute position of the element + var offset = uisOffset(element); + + // Clone the element into a placeholder element to take its original place in the DOM + placeholder = angular.element('
'); + placeholder[0].style.width = offset.width + 'px'; + placeholder[0].style.height = offset.height + 'px'; + element.after(placeholder); + + // Now move the actual dropdown element to the end of the body + $document.find('body').append(element); + + element[0].style.position = 'absolute'; + element[0].style.left = offset.left + 'px'; + element[0].style.top = offset.top + 'px'; + element[0].style.width = offset.width + 'px'; + } + + function resetDropdown() { + if (placeholder === null) { + // The dropdown has not actually been display yet, so there's nothing to reset + return; + } + + // Move the dropdown element back to its original location in the DOM + placeholder.replaceWith(element); + placeholder = null; + + element[0].style.position = ''; + element[0].style.left = ''; + element[0].style.top = ''; + element[0].style.width = ''; + } } }; }]); diff --git a/test/select.spec.js b/test/select.spec.js index 8be66afa8..94581faba 100644 --- a/test/select.spec.js +++ b/test/select.spec.js @@ -37,6 +37,17 @@ describe('ui-select tests', function() { }); beforeEach(module('ngSanitize', 'ui.select', 'wrapperDirective')); + + beforeEach(function() { + module(function($provide) { + $provide.factory('uisOffset', function() { + return function(el) { + return {top: 100, left: 200, width: 300, height: 400}; + }; + }); + }); + }); + beforeEach(inject(function(_$rootScope_, _$compile_, _$timeout_, _$injector_) { $rootScope = _$rootScope_; scope = $rootScope.$new(); @@ -92,6 +103,7 @@ describe('ui-select tests', function() { if (attrs.tagging !== undefined) { attrsHtml += ' tagging="' + attrs.tagging + '"'; } if (attrs.taggingTokens !== undefined) { attrsHtml += ' tagging-tokens="' + attrs.taggingTokens + '"'; } if (attrs.title !== undefined) { attrsHtml += ' title="' + attrs.title + '"'; } + if (attrs.appendToBody != undefined) { attrsHtml += ' append-to-body="' + attrs.appendToBody + '"'; } } return compileTemplate( @@ -161,6 +173,12 @@ describe('ui-select tests', function() { scope.$digest(); }; + function closeDropdown(el) { + var $select = el.scope().$select; + $select.open = false; + scope.$digest(); + } + // Tests @@ -1791,4 +1809,60 @@ describe('ui-select tests', function() { } }); }); + + describe('select with the append to body option', function() { + var body; + + beforeEach(inject(function($document) { + body = $document.find('body')[0]; + })); + + it('should only be moved to the body when the appendToBody option is true', function() { + var el = createUiSelect({appendToBody: false}); + openDropdown(el); + expect(el.parent()[0]).not.toBe(body); + }); + + it('should be moved to the body when the appendToBody is true in uiSelectConfig', inject(function(uiSelectConfig) { + uiSelectConfig.appendToBody = true; + var el = createUiSelect(); + openDropdown(el); + expect(el.parent()[0]).toBe(body); + })); + + it('should be moved to the body when opened', function() { + var el = createUiSelect({appendToBody: true}); + openDropdown(el); + expect(el.parent()[0]).toBe(body); + closeDropdown(el); + expect(el.parent()[0]).not.toBe(body); + }); + + it('should remove itself from the body when the scope is destroyed', function() { + var el = createUiSelect({appendToBody: true}); + openDropdown(el); + expect(el.parent()[0]).toBe(body); + el.scope().$destroy(); + expect(el.parent()[0]).not.toBe(body); + }); + + it('should have specific position and dimensions', function() { + var el = createUiSelect({appendToBody: true}); + var originalPosition = el.css('position'); + var originalTop = el.css('top'); + var originalLeft = el.css('left'); + var originalWidth = el.css('width'); + openDropdown(el); + expect(el.css('position')).toBe('absolute'); + expect(el.css('top')).toBe('100px'); + expect(el.css('left')).toBe('200px'); + expect(el.css('width')).toBe('300px'); + closeDropdown(el); + expect(el.css('position')).toBe(originalPosition); + expect(el.css('top')).toBe(originalTop); + expect(el.css('left')).toBe(originalLeft); + expect(el.css('width')).toBe(originalWidth); + }); + }); + });