Skip to content
This repository was archived by the owner on Oct 2, 2019. It is now read-only.

Commit 55ae88b

Browse files
author
Brian Feister
committed
Merge pull request #736 from cmlenz/feat-append-to-body2
Add an append-to-body attribute to <ui-select>
2 parents 032b152 + 91539d1 commit 55ae88b

File tree

6 files changed

+311
-5
lines changed

6 files changed

+311
-5
lines changed

examples/demo-append-to-body.html

Lines changed: 130 additions & 0 deletions
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

Lines changed: 18 additions & 1 deletion
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

Lines changed: 8 additions & 0 deletions
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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ var uis = angular.module('ui.select', [])
9595
closeOnSelect: true,
9696
generateId: function() {
9797
return latestId++;
98-
}
98+
},
99+
appendToBody: false
99100
})
100101

101102
// 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', [])
133134
return function(matchItem, query) {
134135
return query && matchItem ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '<span class="ui-select-highlight">$&</span>') : matchItem;
135136
};
136-
});
137+
})
137138

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

src/uiSelectDirective.js

Lines changed: 58 additions & 2 deletions
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,62 @@ 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+
var appendToBody = scope.$eval(attrs.appendToBody);
374+
if (appendToBody !== undefined ? appendToBody : uiSelectConfig.appendToBody) {
375+
scope.$watch('$select.open', function(isOpen) {
376+
if (isOpen) {
377+
positionDropdown();
378+
} else {
379+
resetDropdown();
380+
}
381+
});
382+
383+
// Move the dropdown back to its original location when the scope is destroyed. Otherwise
384+
// it might stick around when the user routes away or the select field is otherwise removed
385+
scope.$on('$destroy', function() {
386+
resetDropdown();
387+
});
388+
}
389+
390+
// Hold on to a reference to the .ui-select-container element for appendToBody support
391+
var placeholder = null;
392+
393+
function positionDropdown() {
394+
// Remember the absolute position of the element
395+
var offset = uisOffset(element);
396+
397+
// Clone the element into a placeholder element to take its original place in the DOM
398+
placeholder = angular.element('<div class="ui-select-placeholder"></div>');
399+
placeholder[0].style.width = offset.width + 'px';
400+
placeholder[0].style.height = offset.height + 'px';
401+
element.after(placeholder);
402+
403+
// Now move the actual dropdown element to the end of the body
404+
$document.find('body').append(element);
405+
406+
element[0].style.position = 'absolute';
407+
element[0].style.left = offset.left + 'px';
408+
element[0].style.top = offset.top + 'px';
409+
element[0].style.width = offset.width + 'px';
410+
}
411+
412+
function resetDropdown() {
413+
if (placeholder === null) {
414+
// The dropdown has not actually been display yet, so there's nothing to reset
415+
return;
416+
}
417+
418+
// Move the dropdown element back to its original location in the DOM
419+
placeholder.replaceWith(element);
420+
placeholder = null;
421+
422+
element[0].style.position = '';
423+
element[0].style.left = '';
424+
element[0].style.top = '';
425+
element[0].style.width = '';
426+
}
371427
}
372428
};
373429
}]);

test/select.spec.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ describe('ui-select tests', function() {
3737
});
3838

3939
beforeEach(module('ngSanitize', 'ui.select', 'wrapperDirective'));
40+
41+
beforeEach(function() {
42+
module(function($provide) {
43+
$provide.factory('uisOffset', function() {
44+
return function(el) {
45+
return {top: 100, left: 200, width: 300, height: 400};
46+
};
47+
});
48+
});
49+
});
50+
4051
beforeEach(inject(function(_$rootScope_, _$compile_, _$timeout_, _$injector_) {
4152
$rootScope = _$rootScope_;
4253
scope = $rootScope.$new();
@@ -92,6 +103,7 @@ describe('ui-select tests', function() {
92103
if (attrs.tagging !== undefined) { attrsHtml += ' tagging="' + attrs.tagging + '"'; }
93104
if (attrs.taggingTokens !== undefined) { attrsHtml += ' tagging-tokens="' + attrs.taggingTokens + '"'; }
94105
if (attrs.title !== undefined) { attrsHtml += ' title="' + attrs.title + '"'; }
106+
if (attrs.appendToBody != undefined) { attrsHtml += ' append-to-body="' + attrs.appendToBody + '"'; }
95107
}
96108

97109
return compileTemplate(
@@ -161,6 +173,12 @@ describe('ui-select tests', function() {
161173
scope.$digest();
162174
};
163175

176+
function closeDropdown(el) {
177+
var $select = el.scope().$select;
178+
$select.open = false;
179+
scope.$digest();
180+
}
181+
164182

165183
// Tests
166184

@@ -1791,4 +1809,60 @@ describe('ui-select tests', function() {
17911809
}
17921810
});
17931811
});
1812+
1813+
describe('select with the append to body option', function() {
1814+
var body;
1815+
1816+
beforeEach(inject(function($document) {
1817+
body = $document.find('body')[0];
1818+
}));
1819+
1820+
it('should only be moved to the body when the appendToBody option is true', function() {
1821+
var el = createUiSelect({appendToBody: false});
1822+
openDropdown(el);
1823+
expect(el.parent()[0]).not.toBe(body);
1824+
});
1825+
1826+
it('should be moved to the body when the appendToBody is true in uiSelectConfig', inject(function(uiSelectConfig) {
1827+
uiSelectConfig.appendToBody = true;
1828+
var el = createUiSelect();
1829+
openDropdown(el);
1830+
expect(el.parent()[0]).toBe(body);
1831+
}));
1832+
1833+
it('should be moved to the body when opened', function() {
1834+
var el = createUiSelect({appendToBody: true});
1835+
openDropdown(el);
1836+
expect(el.parent()[0]).toBe(body);
1837+
closeDropdown(el);
1838+
expect(el.parent()[0]).not.toBe(body);
1839+
});
1840+
1841+
it('should remove itself from the body when the scope is destroyed', function() {
1842+
var el = createUiSelect({appendToBody: true});
1843+
openDropdown(el);
1844+
expect(el.parent()[0]).toBe(body);
1845+
el.scope().$destroy();
1846+
expect(el.parent()[0]).not.toBe(body);
1847+
});
1848+
1849+
it('should have specific position and dimensions', function() {
1850+
var el = createUiSelect({appendToBody: true});
1851+
var originalPosition = el.css('position');
1852+
var originalTop = el.css('top');
1853+
var originalLeft = el.css('left');
1854+
var originalWidth = el.css('width');
1855+
openDropdown(el);
1856+
expect(el.css('position')).toBe('absolute');
1857+
expect(el.css('top')).toBe('100px');
1858+
expect(el.css('left')).toBe('200px');
1859+
expect(el.css('width')).toBe('300px');
1860+
closeDropdown(el);
1861+
expect(el.css('position')).toBe(originalPosition);
1862+
expect(el.css('top')).toBe(originalTop);
1863+
expect(el.css('left')).toBe(originalLeft);
1864+
expect(el.css('width')).toBe(originalWidth);
1865+
});
1866+
});
1867+
17941868
});

0 commit comments

Comments
 (0)