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

Add an append-to-body attribute to <ui-select> #736

Merged
merged 5 commits into from
Mar 10, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = ['[email protected]','[email protected]'];

$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
25 changes: 23 additions & 2 deletions src/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -133,5 +134,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)
};
};
}]);
60 changes: 58 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,62 @@ uis.directive('uiSelect',
}
element.querySelectorAll('.ui-select-choices').replaceWith(transcludedChoices);
});

// Support for appending the select field to the body when its open
var appendToBody = scope.$eval(attrs.appendToBody);
if (appendToBody !== undefined ? appendToBody : uiSelectConfig.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';
element[0].style.width = offset.width + '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 = '';
element[0].style.width = '';
}
}
};
}]);
74 changes: 74 additions & 0 deletions test/select.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -1791,4 +1809,60 @@ 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 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');
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(originalPosition);
expect(el.css('top')).toBe(originalTop);
expect(el.css('left')).toBe(originalLeft);
expect(el.css('width')).toBe(originalWidth);
});
});

});