From 62655f116d4c42bfdfbde34a3f321a8393b1990b Mon Sep 17 00:00:00 2001 From: Christopher Lenz Date: Wed, 4 Mar 2015 15:13:20 +0100 Subject: [PATCH 1/5] Add an `append-to-body` attribute to the `` directive that moves the dropdown element to the end of the body element before opening it, thereby solving problems with the dropdown being displayed below elements that follow the `` element in the document. This implementation is modeled after the `typeahead-append-to-body` support from UI Bootstrap, but adds the whole select element to the body, not just the dropdown menu, which is needed for the Select2 theme. See #41 (and quite a few dupes). --- examples/demo-append-to-body.html | 130 ++++++++++++++++++++++++++++++ examples/demo.js | 19 ++++- src/common.css | 8 ++ src/common.js | 22 ++++- src/uiSelectDirective.js | 57 ++++++++++++- 5 files changed, 232 insertions(+), 4 deletions(-) create mode 100644 examples/demo-append-to-body.html 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..47955b13e 100644 --- a/src/common.js +++ b/src/common.js @@ -133,5 +133,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..0a2102744 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,59 @@ uis.directive('uiSelect', } element.querySelectorAll('.ui-select-choices').replaceWith(transcludedChoices); }); + + // Support for appending the select field to the body when its open + if (scope.$eval(attrs.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'; + } + + 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 = ''; + } } }; }]); From 755765ff48beb57527bffc70f3009656151e08cc Mon Sep 17 00:00:00 2001 From: Christopher Lenz Date: Mon, 9 Mar 2015 16:53:01 +0100 Subject: [PATCH 2/5] Need to explicitly set the width of the select field when it is moved to the body. --- src/uiSelectDirective.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/uiSelectDirective.js b/src/uiSelectDirective.js index 0a2102744..8608dfb70 100644 --- a/src/uiSelectDirective.js +++ b/src/uiSelectDirective.js @@ -405,6 +405,7 @@ uis.directive('uiSelect', 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() { @@ -420,6 +421,7 @@ uis.directive('uiSelect', element[0].style.position = ''; element[0].style.left = ''; element[0].style.top = ''; + element[0].style.width = ''; } } }; From e3ff7c15b65bad73dc48fccb8a8527df7f78ff27 Mon Sep 17 00:00:00 2001 From: Christopher Lenz Date: Mon, 9 Mar 2015 18:50:02 +0100 Subject: [PATCH 3/5] Add `appendToBody` option to `uiSelectConfig`. --- src/common.js | 3 ++- src/uiSelectDirective.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/common.js b/src/common.js index 47955b13e..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 diff --git a/src/uiSelectDirective.js b/src/uiSelectDirective.js index 8608dfb70..5a030db66 100644 --- a/src/uiSelectDirective.js +++ b/src/uiSelectDirective.js @@ -370,7 +370,8 @@ uis.directive('uiSelect', }); // Support for appending the select field to the body when its open - if (scope.$eval(attrs.appendToBody)) { + var appendToBody = scope.$eval(attrs.appendToBody); + if (appendToBody !== undefined ? appendToBody : uiSelectConfig.appendToBody) { scope.$watch('$select.open', function(isOpen) { if (isOpen) { positionDropdown(); From 6bdfbba633e5864bd50bf7181badfd2ec2686426 Mon Sep 17 00:00:00 2001 From: Christopher Lenz Date: Mon, 9 Mar 2015 18:50:27 +0100 Subject: [PATCH 4/5] Add some tests for the `appendToBody` support. --- test/select.spec.js | 71 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/test/select.spec.js b/test/select.spec.js index 8be66afa8..825949445 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,57 @@ 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 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(''); + expect(el.css('top')).toBe(''); + expect(el.css('left')).toBe(''); + expect(el.css('width')).toBe(originalWidth); + }); + }); + }); From 91539d1327c26fbe23cfe2a1c1d813eb28cb8f6d Mon Sep 17 00:00:00 2001 From: Christopher Lenz Date: Mon, 9 Mar 2015 18:58:35 +0100 Subject: [PATCH 5/5] Fix test of Firefox. --- test/select.spec.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/select.spec.js b/test/select.spec.js index 825949445..94581faba 100644 --- a/test/select.spec.js +++ b/test/select.spec.js @@ -1848,6 +1848,9 @@ describe('ui-select tests', function() { 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'); @@ -1855,9 +1858,9 @@ describe('ui-select tests', function() { expect(el.css('left')).toBe('200px'); expect(el.css('width')).toBe('300px'); closeDropdown(el); - expect(el.css('position')).toBe(''); - expect(el.css('top')).toBe(''); - expect(el.css('left')).toBe(''); + expect(el.css('position')).toBe(originalPosition); + expect(el.css('top')).toBe(originalTop); + expect(el.css('left')).toBe(originalLeft); expect(el.css('width')).toBe(originalWidth); }); });