From 62655f116d4c42bfdfbde34a3f321a8393b1990b Mon Sep 17 00:00:00 2001 From: Christopher Lenz Date: Wed, 4 Mar 2015 15:13:20 +0100 Subject: [PATCH] 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 = ''; + } } }; }]);