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..efc7a5b9f 100644
--- a/src/common.js
+++ b/src/common.js
@@ -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
@@ -133,5 +134,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..5a030db66 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,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('');
+ 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 = '';
+ }
}
};
}]);
diff --git a/test/select.spec.js b/test/select.spec.js
index 8be66afa8..94581faba 100644
--- a/test/select.spec.js
+++ b/test/select.spec.js
@@ -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();
@@ -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(
@@ -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
@@ -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);
+ });
+ });
+
});