Skip to content

Commit

Permalink
Add an append-to-body attribute to the <ui-select> directive that…
Browse files Browse the repository at this point in the history
… 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).
  • Loading branch information
cmlenz committed Mar 9, 2015
1 parent 032b152 commit 62655f1
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 4 deletions.
130 changes: 130 additions & 0 deletions examples/demo-append-to-body.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="en" ng-app="demo">
<head>
<meta charset="utf-8">
<title>AngularJS ui-select</title>

<!--
IE8 support, see AngularJS Internet Explorer Compatibility http://docs.angularjs.org/guide/ie
For Firefox 3.6, you will also need to include jQuery and ECMAScript 5 shim
-->
<!--[if lt IE 9]>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/es5-shim/2.2.0/es5-shim.js"></script>
<script>
document.createElement('ui-select');
document.createElement('ui-select-match');
document.createElement('ui-select-choices');
</script>
<![endif]-->

<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.18/angular.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.18/angular-sanitize.js"></script>
<link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.css">

<!-- ui-select files -->
<script src="../dist/select.js"></script>
<link rel="stylesheet" href="../dist/select.css">

<script src="demo.js"></script>

<!-- Select2 theme -->
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/select2/3.4.5/select2.css">

<!--
Selectize theme
Less versions are available at https://github.com/brianreavis/selectize.js/tree/master/dist/less
-->
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.8.5/css/selectize.default.css">
<!-- <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.8.5/css/selectize.bootstrap2.css"> -->
<!-- <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.8.5/css/selectize.bootstrap3.css"> -->

<style>
body {
padding: 15px;
}

.select2 > .select2-choice.ui-select-match {
/* Because of the inclusion of Bootstrap */
height: 29px;
}

.selectize-control > .selectize-dropdown {
top: 36px;
}

/* Some additional styling to demonstrate that append-to-body helps achieve the proper z-index layering. */
.select-box {
background: #fff;
position: relative;
z-index: 1;
}
.alert-info.positioned {
margin-top: 1em;
position: relative;
z-index: 10000; // The select2 dropdown has a z-index of 9999
}
</style>
</head>

<body ng-controller="DemoCtrl">
<script src="demo.js"></script>

<button class="btn btn-default btn-xs" ng-click="enable()">Enable ui-select</button>
<button class="btn btn-default btn-xs" ng-click="disable()">Disable ui-select</button>
<button class="btn btn-default btn-xs" ng-click="appendToBodyDemo.startToggleTimer()"
ng-disabled="appendToBodyDemo.remainingTime">
{{ appendToBodyDemo.remainingTime ? 'Toggling in ' + (appendToBodyDemo.remainingTime / 1000) + ' seconds' : 'Toggle ui-select presence' }}
</button>
<button class="btn btn-default btn-xs" ng-click="clear()">Clear ng-model</button>

<div class="select-box" ng-show="appendToBodyDemo.present">
<h3>Bootstrap theme</h3>
<p>Selected: {{address.selected.formatted_address}}</p>
<ui-select ng-model="address.selected"
theme="bootstrap"
ng-disabled="disabled"
reset-search-input="false"
style="width: 300px;"
title="Choose an address"
append-to-body="true">
<ui-select-match placeholder="Enter an address...">{{$select.selected.formatted_address}}</ui-select-match>
<ui-select-choices repeat="address in addresses track by $index"
refresh="refreshAddresses($select.search)"
refresh-delay="0">
<div ng-bind-html="address.formatted_address | highlight: $select.search"></div>
</ui-select-choices>
</ui-select>
<p class="alert alert-info positioned">The select dropdown menu should be displayed above this element.</p>
</div>

<div class="select-box" ng-if="appendToBodyDemo.present">
<h3>Select2 theme</h3>
<p>Selected: {{person.selected}}</p>
<ui-select ng-model="person.selected" theme="select2" ng-disabled="disabled" style="min-width: 300px;" title="Choose a person" append-to-body="true">
<ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.name}}</ui-select-match>
<ui-select-choices repeat="person in people | propsFilter: {name: $select.search, age: $select.search}">
<div ng-bind-html="person.name | highlight: $select.search"></div>
<small>
email: {{person.email}}
age: <span ng-bind-html="''+person.age | highlight: $select.search"></span>
</small>
</ui-select-choices>
</ui-select>
<p class="alert alert-info positioned">The select dropdown menu should be displayed above this element.</p>
</div>

<div class="select-box" ng-if="appendToBodyDemo.present">
<h3>Selectize theme</h3>
<p>Selected: {{country.selected}}</p>
<ui-select ng-model="country.selected" theme="selectize" ng-disabled="disabled" style="width: 300px;" title="Choose a country" append-to-body="true">
<ui-select-match placeholder="Select or search a country in the list...">{{$select.selected.name}}</ui-select-match>
<ui-select-choices repeat="country in countries | filter: $select.search">
<span ng-bind-html="country.name | highlight: $select.search"></span>
<small ng-bind-html="country.code | highlight: $select.search"></small>
</ui-select-choices>
</ui-select>
<p class="alert alert-info positioned">The select dropdown menu should be displayed above this element.</p>
</div>
</body>
</html>
19 changes: 18 additions & 1 deletion examples/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down
8 changes: 8 additions & 0 deletions src/common.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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;
Expand Down
22 changes: 21 additions & 1 deletion src/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,5 +133,25 @@ var uis = angular.module('ui.select', [])
return function(matchItem, query) {
return query && matchItem ? matchItem.replace(new RegExp(escapeRegexp(query), 'gi'), '<span class="ui-select-highlight">$&</span>') : 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)
};
};
}]);
57 changes: 55 additions & 2 deletions src/uiSelectDirective.js
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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('<div class="ui-select-placeholder"></div>');
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 = '';
}
}
};
}]);

0 comments on commit 62655f1

Please sign in to comment.