Skip to content

Commit 1207f14

Browse files
cmlenzAsimov4
authored andcommitted
Add an append-to-body attribute to the <ui-select> 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 <ui-select> 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 angular-ui#41 (and quite a few dupes).
1 parent 3fad52c commit 1207f14

File tree

5 files changed

+232
-4
lines changed

5 files changed

+232
-4
lines changed

examples/demo-append-to-body.html

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<!DOCTYPE html>
2+
<html lang="en" ng-app="demo">
3+
<head>
4+
<meta charset="utf-8">
5+
<title>AngularJS ui-select</title>
6+
7+
<!--
8+
IE8 support, see AngularJS Internet Explorer Compatibility http://docs.angularjs.org/guide/ie
9+
For Firefox 3.6, you will also need to include jQuery and ECMAScript 5 shim
10+
-->
11+
<!--[if lt IE 9]>
12+
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.js"></script>
13+
<script src="http://cdnjs.cloudflare.com/ajax/libs/es5-shim/2.2.0/es5-shim.js"></script>
14+
<script>
15+
document.createElement('ui-select');
16+
document.createElement('ui-select-match');
17+
document.createElement('ui-select-choices');
18+
</script>
19+
<![endif]-->
20+
21+
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.18/angular.js"></script>
22+
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.18/angular-sanitize.js"></script>
23+
<link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.css">
24+
25+
<!-- ui-select files -->
26+
<script src="../dist/select.js"></script>
27+
<link rel="stylesheet" href="../dist/select.css">
28+
29+
<script src="demo.js"></script>
30+
31+
<!-- Select2 theme -->
32+
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/select2/3.4.5/select2.css">
33+
34+
<!--
35+
Selectize theme
36+
Less versions are available at https://github.com/brianreavis/selectize.js/tree/master/dist/less
37+
-->
38+
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.8.5/css/selectize.default.css">
39+
<!-- <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.8.5/css/selectize.bootstrap2.css"> -->
40+
<!-- <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.8.5/css/selectize.bootstrap3.css"> -->
41+
42+
<style>
43+
body {
44+
padding: 15px;
45+
}
46+
47+
.select2 > .select2-choice.ui-select-match {
48+
/* Because of the inclusion of Bootstrap */
49+
height: 29px;
50+
}
51+
52+
.selectize-control > .selectize-dropdown {
53+
top: 36px;
54+
}
55+
56+
/* Some additional styling to demonstrate that append-to-body helps achieve the proper z-index layering. */
57+
.select-box {
58+
background: #fff;
59+
position: relative;
60+
z-index: 1;
61+
}
62+
.alert-info.positioned {
63+
margin-top: 1em;
64+
position: relative;
65+
z-index: 10000; // The select2 dropdown has a z-index of 9999
66+
}
67+
</style>
68+
</head>
69+
70+
<body ng-controller="DemoCtrl">
71+
<script src="demo.js"></script>
72+
73+
<button class="btn btn-default btn-xs" ng-click="enable()">Enable ui-select</button>
74+
<button class="btn btn-default btn-xs" ng-click="disable()">Disable ui-select</button>
75+
<button class="btn btn-default btn-xs" ng-click="appendToBodyDemo.startToggleTimer()"
76+
ng-disabled="appendToBodyDemo.remainingTime">
77+
{{ appendToBodyDemo.remainingTime ? 'Toggling in ' + (appendToBodyDemo.remainingTime / 1000) + ' seconds' : 'Toggle ui-select presence' }}
78+
</button>
79+
<button class="btn btn-default btn-xs" ng-click="clear()">Clear ng-model</button>
80+
81+
<div class="select-box" ng-show="appendToBodyDemo.present">
82+
<h3>Bootstrap theme</h3>
83+
<p>Selected: {{address.selected.formatted_address}}</p>
84+
<ui-select ng-model="address.selected"
85+
theme="bootstrap"
86+
ng-disabled="disabled"
87+
reset-search-input="false"
88+
style="width: 300px;"
89+
title="Choose an address"
90+
append-to-body="true">
91+
<ui-select-match placeholder="Enter an address...">{{$select.selected.formatted_address}}</ui-select-match>
92+
<ui-select-choices repeat="address in addresses track by $index"
93+
refresh="refreshAddresses($select.search)"
94+
refresh-delay="0">
95+
<div ng-bind-html="address.formatted_address | highlight: $select.search"></div>
96+
</ui-select-choices>
97+
</ui-select>
98+
<p class="alert alert-info positioned">The select dropdown menu should be displayed above this element.</p>
99+
</div>
100+
101+
<div class="select-box" ng-if="appendToBodyDemo.present">
102+
<h3>Select2 theme</h3>
103+
<p>Selected: {{person.selected}}</p>
104+
<ui-select ng-model="person.selected" theme="select2" ng-disabled="disabled" style="min-width: 300px;" title="Choose a person" append-to-body="true">
105+
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.name}}</ui-select-match>
106+
<ui-select-choices repeat="person in people | propsFilter: {name: $select.search, age: $select.search}">
107+
<div ng-bind-html="person.name | highlight: $select.search"></div>
108+
<small>
109+
email: {{person.email}}
110+
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
111+
</small>
112+
</ui-select-choices>
113+
</ui-select>
114+
<p class="alert alert-info positioned">The select dropdown menu should be displayed above this element.</p>
115+
</div>
116+
117+
<div class="select-box" ng-if="appendToBodyDemo.present">
118+
<h3>Selectize theme</h3>
119+
<p>Selected: {{country.selected}}</p>
120+
<ui-select ng-model="country.selected" theme="selectize" ng-disabled="disabled" style="width: 300px;" title="Choose a country" append-to-body="true">
121+
<ui-select-match placeholder="Select or search a country in the list...">{{$select.selected.name}}</ui-select-match>
122+
<ui-select-choices repeat="country in countries | filter: $select.search">
123+
<span ng-bind-html="country.name | highlight: $select.search"></span>
124+
<small ng-bind-html="country.code | highlight: $select.search"></small>
125+
</ui-select-choices>
126+
</ui-select>
127+
<p class="alert alert-info positioned">The select dropdown menu should be displayed above this element.</p>
128+
</div>
129+
</body>
130+
</html>

examples/demo.js

+18-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ app.filter('propsFilter', function() {
3939
};
4040
});
4141

42-
app.controller('DemoCtrl', function($scope, $http, $timeout) {
42+
app.controller('DemoCtrl', function($scope, $http, $timeout, $interval) {
4343
$scope.disabled = undefined;
4444
$scope.searchEnabled = undefined;
4545

@@ -147,6 +147,23 @@ app.controller('DemoCtrl', function($scope, $http, $timeout) {
147147
$scope.multipleDemo.selectedPeopleWithGroupBy = [$scope.people[8], $scope.people[6]];
148148
$scope.multipleDemo.selectedPeopleSimple = ['[email protected]','[email protected]'];
149149

150+
$scope.appendToBodyDemo = {
151+
remainingToggleTime: 0,
152+
present: true,
153+
startToggleTimer: function() {
154+
var scope = $scope.appendToBodyDemo;
155+
var promise = $interval(function() {
156+
if (scope.remainingTime < 1000) {
157+
$interval.cancel(promise);
158+
scope.present = !scope.present;
159+
scope.remainingTime = 0;
160+
} else {
161+
scope.remainingTime -= 1000;
162+
}
163+
}, 1000);
164+
scope.remainingTime = 3000;
165+
}
166+
};
150167

151168
$scope.address = {};
152169
$scope.refreshAddresses = function(address) {

src/common.css

+8
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
display:none;
3737
}
3838

39+
body > .select2-container {
40+
z-index: 9999; /* The z-index Select2 applies to the select2-drop */
41+
}
42+
3943
/* Selectize theme */
4044

4145
/* Helper class to show styles when focus */
@@ -116,6 +120,10 @@
116120
margin-top: -1px;
117121
}
118122

123+
body > .ui-select-bootstrap {
124+
z-index: 1000; /* Standard Bootstrap dropdown z-index */
125+
}
126+
119127
.ui-select-multiple.ui-select-bootstrap {
120128
height: auto;
121129
padding: 3px 3px 0 3px;

src/common.js

+21-1
Original file line numberDiff line numberDiff line change
@@ -133,5 +133,25 @@ var uis = angular.module('ui.select', [])
133133
return function(matchItem, query) {
134134
return query && matchItem ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '<span class="ui-select-highlight">$&</span>') : matchItem;
135135
};
136-
});
136+
})
137137

138+
/**
139+
* A read-only equivalent of jQuery's offset function: http://api.jquery.com/offset/
140+
*
141+
* Taken from AngularUI Bootstrap Position:
142+
* See https://github.com/angular-ui/bootstrap/blob/master/src/position/position.js#L70
143+
*/
144+
.factory('uisOffset',
145+
['$document', '$window',
146+
function ($document, $window) {
147+
148+
return function(element) {
149+
var boundingClientRect = element[0].getBoundingClientRect();
150+
return {
151+
width: boundingClientRect.width || element.prop('offsetWidth'),
152+
height: boundingClientRect.height || element.prop('offsetHeight'),
153+
top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
154+
left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
155+
};
156+
};
157+
}]);

src/uiSelectDirective.js

+55-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
uis.directive('uiSelect',
2-
['$document', 'uiSelectConfig', 'uiSelectMinErr', '$compile', '$parse', '$timeout',
3-
function($document, uiSelectConfig, uiSelectMinErr, $compile, $parse, $timeout) {
2+
['$document', 'uiSelectConfig', 'uiSelectMinErr', 'uisOffset', '$compile', '$parse', '$timeout',
3+
function($document, uiSelectConfig, uiSelectMinErr, uisOffset, $compile, $parse, $timeout) {
44

55
return {
66
restrict: 'EA',
@@ -368,6 +368,59 @@ uis.directive('uiSelect',
368368
}
369369
element.querySelectorAll('.ui-select-choices').replaceWith(transcludedChoices);
370370
});
371+
372+
// Support for appending the select field to the body when its open
373+
if (scope.$eval(attrs.appendToBody)) {
374+
scope.$watch('$select.open', function(isOpen) {
375+
if (isOpen) {
376+
positionDropdown();
377+
} else {
378+
resetDropdown();
379+
}
380+
});
381+
382+
// Move the dropdown back to its original location when the scope is destroyed. Otherwise
383+
// it might stick around when the user routes away or the select field is otherwise removed
384+
scope.$on('$destroy', function() {
385+
resetDropdown();
386+
});
387+
}
388+
389+
// Hold on to a reference to the .ui-select-container element for appendToBody support
390+
var placeholder = null;
391+
392+
function positionDropdown() {
393+
// Remember the absolute position of the element
394+
var offset = uisOffset(element);
395+
396+
// Clone the element into a placeholder element to take its original place in the DOM
397+
placeholder = angular.element('<div class="ui-select-placeholder"></div>');
398+
placeholder[0].style.width = offset.width + 'px';
399+
placeholder[0].style.height = offset.height + 'px';
400+
element.after(placeholder);
401+
402+
// Now move the actual dropdown element to the end of the body
403+
$document.find('body').append(element);
404+
405+
element[0].style.position = 'absolute';
406+
element[0].style.left = offset.left + 'px';
407+
element[0].style.top = offset.top + 'px';
408+
}
409+
410+
function resetDropdown() {
411+
if (placeholder === null) {
412+
// The dropdown has not actually been display yet, so there's nothing to reset
413+
return;
414+
}
415+
416+
// Move the dropdown element back to its original location in the DOM
417+
placeholder.replaceWith(element);
418+
placeholder = null;
419+
420+
element[0].style.position = '';
421+
element[0].style.left = '';
422+
element[0].style.top = '';
423+
}
371424
}
372425
};
373426
}]);

0 commit comments

Comments
 (0)