diff --git a/src/popover/docs/demo.js b/src/popover/docs/demo.js
index 0d94f28941..93f3a248ed 100644
--- a/src/popover/docs/demo.js
+++ b/src/popover/docs/demo.js
@@ -4,4 +4,22 @@ angular.module('ui.bootstrap.demo').controller('PopoverDemoCtrl', function ($sco
templateUrl: 'myPopoverTemplate.html',
title: 'Title'
};
+
+ $scope.placement = {
+ options: [
+ 'top',
+ 'top-left',
+ 'top-right',
+ 'bottom',
+ 'bottom-left',
+ 'bottom-right',
+ 'left',
+ 'left-top',
+ 'left-bottom',
+ 'right',
+ 'right-top',
+ 'right-bottom'
+ ],
+ selected: 'top'
+ };
});
diff --git a/src/popover/docs/readme.md b/src/popover/docs/readme.md
index 5f917725d5..186395d7d5 100644
--- a/src/popover/docs/readme.md
+++ b/src/popover/docs/readme.md
@@ -18,8 +18,22 @@ The popover directives provides several optional attributes to control how it
will display:
- `popover-title`: A string to display as a fancy title.
-- `popover-placement`: Where to place it? Defaults to "top", but also accepts
- "bottom", "left", "right".
+- `popover-placement`: Where to place it? Defaults to "top". Passing in 'auto' seperated by a space before the placement will
+ enable auto positioning, e.g: "auto bottom-left". The popover will attempt to position where it fits in
+ the closest scrollable ancestor. Accepts:
+
+ - "top" - popover on top, horizontally centered on host element.
+ - "top-left" - popover on top, left edge aligned with host element left edge.
+ - "top-right" - popover on top, right edge aligned with host element right edge.
+ - "bottom" - popover on bottom, horizontally centered on host element.
+ - "bottom-left" - popover on bottom, left edge aligned with host element left edge.
+ - "bottom-right" - popover on bottom, right edge aligned with host element right edge.
+ - "left" - popover on left, vertically centered on host element.
+ - "left-top" - popover on left, top edge aligned with host element top edge.
+ - "left-bottom" - popover on left, bottom edge aligned with host element bottom edge.
+ - "right" - popover on right, vertically centered on host element.
+ - "right-top" - popover on right, top edge aligned with host element top edge.
+ - "right-bottom" - popover on right, bottom edge aligned with host element bottom edge.
- `popover-animation`: Should it fade in and out? Defaults to "true".
- `popover-popup-delay`: For how long should the user have to have the mouse
over the element before the popover shows (in milliseconds)? Defaults to 0.
diff --git a/src/position/docs/demo.html b/src/position/docs/demo.html
new file mode 100644
index 0000000000..fcc063f433
--- /dev/null
+++ b/src/position/docs/demo.html
@@ -0,0 +1,34 @@
+
\ No newline at end of file
diff --git a/src/position/docs/demo.js b/src/position/docs/demo.js
new file mode 100644
index 0000000000..8be1343de4
--- /dev/null
+++ b/src/position/docs/demo.js
@@ -0,0 +1,27 @@
+angular.module('ui.bootstrap.demo').controller('PositionDemoCtrl', function ($scope, $window, $uibPosition) {
+
+ $scope.elemVals = {};
+ $scope.parentScrollable = true;
+ $scope.parentRelative = true;
+
+ $scope.getValues = function() {
+ var divEl = $window.document.querySelector('#posdemodiv');
+ var btnEl = $window.document.querySelector('#posdemobtn');
+
+ var offsetParent = $uibPosition.offsetParent(divEl);
+ $scope.elemVals.offsetParent = 'type: ' + offsetParent.tagName + ', id: ' + offsetParent.id;
+
+ var scrollParent = $uibPosition.scrollParent(divEl);
+ $scope.elemVals.scrollParent = 'type: ' + scrollParent.tagName + ', id: ' + scrollParent.id;
+
+ $scope.scrollbarWidth = $uibPosition.scrollbarWidth();
+
+ $scope.elemVals.position = $uibPosition.position(divEl);
+
+ $scope.elemVals.offset = $uibPosition.offset(divEl);
+
+ $scope.elemVals.viewportOffset = $uibPosition.viewportOffset(divEl);
+
+ $scope.elemVals.positionElements = $uibPosition.positionElements(btnEl, divEl, 'auto bottom-left');
+ };
+});
\ No newline at end of file
diff --git a/src/position/docs/readme.md b/src/position/docs/readme.md
new file mode 100644
index 0000000000..c6b3b6dc5f
--- /dev/null
+++ b/src/position/docs/readme.md
@@ -0,0 +1,167 @@
+The `$uibPosition` service provides a set of DOM utilities used internally to absolute-position an element in relation to another element (tooltips, popovers, typeaheads etc...).
+
+### getRawNode(element)
+ Takes a jQuery/jqLite element and converts it to a raw DOM element.
+
+##### parameters
+
+* `element` _(Type 'object')_ - The element to convert.
+
+##### returns
+
+ _(Type 'element')_ - A raw DOM element.
+
+### parseSyle(element)
+ Parses a numeric style value to a number. Strips units and will return 0 for invalid (NaN) numbers.
+
+##### parameters
+
+* `value` _(Type 'string')_ - The style value to parse.
+
+##### returns
+
+ _(Type 'number')_ - The numeric value of the style property.
+
+### offsetParent(element)
+ Gets the closest positioned ancestor.
+
+##### parameters
+
+* `element` _(Type 'element')_ - The element to get the offset parent for.
+
+##### returns
+
+ _(Type 'element')_ - The closest positioned ancestor.
+
+### scrollbarWidth()
+ Calculates the browser scrollbar width and caches the result for future calls. Concept from the TWBS measureScrollbar() function in [modal.js](https://github.com/twbs/bootstrap/blob/master/js/modal.js).
+
+##### returns
+
+ _(Type 'number')_ - The width of the browser scrollbar.
+
+### scrollParent(element, includeHidden)
+ Gets the closest scrollable ancestor. Concept from the jQueryUI [scrollParent.js](https://github.com/jquery/jquery-ui/blob/master/ui/scroll-parent.js).
+
+##### parameters
+
+* `element` _(Type 'element')_ - The element to get the closest scrollable ancestor for.
+
+* `includeHidden` _(Type 'boolean', optional - default is false)_ - Should scroll style of 'hidden' be considered.
+
+##### returns
+
+ _(Type 'element')_ - The closest scrollable ancestor.
+
+### position(element, includeMargins)
+ A read-only equivalent of jQuery's [position](http://api.jquery.com/position/) function, distance to closest positioned ancestor. Does not account for margins by default like jQuery's position.
+
+##### parameters
+
+* `element` _(Type 'element')_ - The element to get the position for.
+
+* `includeMagins` _(Type 'boolean', optional - default is false)_ - Should margins be accounted for.
+
+##### returns
+
+ _(Type 'object')_ - An object with the following properties:
+
+* width: _(Type 'number')_ The width of the element.
+* height: _(Type 'number')_ The height of the element.
+* top: _(Type 'number')_ Distance to top edge of offset parent.
+* left: _(Type 'number')_ Distance to left edge of offset parent.
+
+### offset(element)
+ A read-only equivalent of jQuery's [offset](http://api.jquery.com/offset/) function, distance to viewport.
+
+##### parameters
+
+* `element` _(Type 'element')_ - The element to get the offset for.
+
+##### returns
+
+ _(Type 'object')_ - An object with the following properties:
+
+* width: _(Type 'number')_ The width of the element.
+* height: _(Type 'number')_ The height of the element.
+* top: _(Type 'number')_ Distance to top edge of the viewport.
+* left: _(Type 'number')_ Distance to left edge of the viewport.
+
+### viewportOffset(element, useDocument, includePadding)
+ Gets the elements available space relative to the closest scrollable ancestor. Accounts for padding, border, and scrollbar width.
+ Right and bottom dimensions represent the distance to the respective edge of the viewport element, not the top and left edge.
+ If the element edge extends beyond the viewport, a negative value will be reported.
+
+##### parameters
+
+* `element` _(Type 'element')_ - The element to get the viewport offset for.
+* `useDocument` _(Type 'boolean', optional - default is false)_ - Should the viewport be the document element instead of the first scrollable element.
+* `includePadding` _(Type 'boolean', optional - default is true)_ - Should the padding on the viewport element be accounted for, default is true.
+
+##### returns
+
+ _(Type 'object')_ - An object with the following properties:
+
+* top: _(Type 'number')_ Distance to top content edge of the viewport.
+* bottom: _(Type 'number')_ Distance to bottom content edge of the viewport.
+* left: _(Type 'number')_ Distance to left content edge of the viewport.
+* right: _(Type 'number')_ Distance to right content edge of the viewport.
+
+### parsePlacement(placement)
+ Gets an array of placement values parsed from a placement string. Along with the 'auto' indicator, supported placement strings are:
+
+* top: element on top, horizontally centered on host element.
+* top-left: element on top, left edge aligned with host element left edge.
+* top-right: element on top, right edge aligned with host element right edge.
+* bottom: element on bottom, horizontally centered on host element.
+* bottom-left: element on bottom, left edge aligned with host element left edge.
+* bottom-right: element on bottom, right edge aligned with host element right edge.
+* left: element on left, vertically centered on host element.
+* left-top: element on left, top edge aligned with host element top edge.
+* left-bottom: element on left, bottom edge aligned with host element bottom edge.
+* right: element on right, vertically centered on host element.
+* right-top: element on right, top edge aligned with host element top edge.
+* right-bottom: element on right, bottom edge aligned with host element bottom edge.
+
+A placement string with an 'auto' indicator is expected to be space separated from the placement, i.e: 'auto bottom-left'.
+If the primary and secondary placement values do not match 'top, bottom, left, right' then 'top' will be the primary placement and
+'center' will be the secondary placement. If 'auto' is passed, true will be returned as the 3rd value of the array.
+
+##### parameters
+
+* `placement` _(Type 'string', e.g. 'auto top-left')_ - The placement string to parse.
+
+##### returns
+
+ _(Type 'array')_ - An array with the following values:
+
+* [0]: _(Type 'string')_ - The primary placement.
+* [1]: _(Type 'string')_ - The secondary placement.
+* [2]: _(Type 'boolean')_ - Is auto place enabled.
+
+### positionElements(hostElement, targetElement, placement, appendToBody)
+ Gets gets coordinates for an element to be positioned relative to another element.
+
+##### parameters
+
+ * `hostElement` _(Type 'element')_ - The element to position against.
+ * `targetElement` _(Type 'element')_ - The element to position.
+ * `placement` _(Type 'string', optional - default is top)_ - The placement for the target element. See the parsePlacement() function for
+ available options. If 'auto' placement is used, the viewportOffset() function is used to decide where the targetElement will fit.
+ * `appendToBody` _(Type 'boolean', optional - default is false)_ - Should the coordinates be cacluated from the body element.
+
+##### returns
+
+ _(Type 'object')_ - An object with the following properties:
+
+ * top: _(Type 'number')_ The targetElement top value.
+ * left: _(Type 'number')_ The targetElement left value.
+ * right: _(Type 'number')_ The resolved placement with 'auto' removed.
+
+### positionArrow(element, placement)
+ Positions the tooltip and popover arrow elements when using placement options beyond the standard top, left, bottom, or right.
+
+##### parameters
+
+ * `element` _(Type 'element')_ - The element to position the arrow element for.
+ * `placement` _(Type 'string')_ - The placement for the element.
\ No newline at end of file
diff --git a/src/position/position.js b/src/position/position.js
index 89f6a7bb38..2a9d368c3b 100644
--- a/src/position/position.js
+++ b/src/position/position.js
@@ -1,149 +1,530 @@
angular.module('ui.bootstrap.position', [])
/**
- * A set of utility methods that can be use to retrieve position of DOM elements.
- * It is meant to be used where we need to absolute-position DOM elements in
- * relation to other, existing elements (this is the case for tooltips, popovers,
+ * A set of utility methods for working with the DOM.
+ * It is meant to be used where we need to absolute-position elements in
+ * relation to another element (this is the case for tooltips, popovers,
* typeahead suggestions etc.).
*/
.factory('$uibPosition', ['$document', '$window', function($document, $window) {
- function getStyle(el, cssprop) {
- if (el.currentStyle) { //IE
- return el.currentStyle[cssprop];
- } else if ($window.getComputedStyle) {
- return $window.getComputedStyle(el)[cssprop];
- }
- // finally try and get inline style
- return el.style[cssprop];
- }
-
- /**
- * Checks if a given element is statically positioned
- * @param element - raw DOM element
- */
- function isStaticPositioned(element) {
- return (getStyle(element, 'position') || 'static' ) === 'static';
- }
-
/**
- * returns the closest, non-statically positioned parentOffset of a given element
- * @param element
+ * Used by scrollbarWidth() function to cache scrollbar's width.
+ * Do not access this variable directly, use scrollbarWidth() instead.
*/
- var parentOffsetEl = function(element) {
- var docDomEl = $document[0];
- var offsetParent = element.offsetParent || docDomEl;
- while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) {
- offsetParent = offsetParent.offsetParent;
- }
- return offsetParent || docDomEl;
+ var SCROLLBAR_WIDTH;
+ var OVERFLOW_REGEX = {
+ normal: /(auto|scroll)/,
+ hidden: /(auto|scroll|hidden)/
+ };
+ var PLACEMENT_REGEX = {
+ auto: /\s?auto?\s?/i,
+ primary: /^(top|bottom|left|right)$/,
+ secondary: /^(top|bottom|left|right|center)$/,
+ vertical: /^(top|bottom)$/
};
return {
+
+ /**
+ * Provides a raw DOM element from a jQuery/jQLite element.
+ *
+ * @param {element} elem - The element to convert.
+ *
+ * @returns {element} A HTML element.
+ */
+ getRawNode: function(elem) {
+ return elem[0] || elem;
+ },
+
+ /**
+ * Provides a parsed number for a style property. Strips
+ * units and casts invalid numbers to 0.
+ *
+ * @param {string} value - The style value to parse.
+ *
+ * @returns {number} A valid number.
+ */
+ parseStyle: function(value) {
+ value = parseFloat(value);
+ return isFinite(value) ? value : 0;
+ },
+
+ /**
+ * Provides the closest positioned ancestor.
+ *
+ * @param {element} element - The element to get the offest parent for.
+ *
+ * @returns {element} The closest positioned ancestor.
+ */
+ offsetParent: function(elem) {
+ elem = this.getRawNode(elem);
+
+ var offsetParent = elem.offsetParent || $document[0].documentElement;
+
+ function isStaticPositioned(el) {
+ return ($window.getComputedStyle(el).position || 'static') === 'static';
+ }
+
+ while (offsetParent && offsetParent !== $document[0].documentElement && isStaticPositioned(offsetParent)) {
+ offsetParent = offsetParent.offsetParent;
+ }
+
+ return offsetParent || $document[0].documentElement;
+ },
+
+ /**
+ * Provides the scrollbar width, concept from TWBS measureScrollbar()
+ * function in https://github.com/twbs/bootstrap/blob/master/js/modal.js
+ *
+ * @returns {number} The width of the browser scollbar.
+ */
+ scrollbarWidth: function() {
+ if (angular.isUndefined(SCROLLBAR_WIDTH)) {
+ var scrollElem = angular.element('');
+ $document.find('body').append(scrollElem);
+ SCROLLBAR_WIDTH = scrollElem[0].offsetWidth - scrollElem[0].clientWidth;
+ SCROLLBAR_WIDTH = isFinite(SCROLLBAR_WIDTH) ? SCROLLBAR_WIDTH : 0;
+ scrollElem.remove();
+ }
+
+ return SCROLLBAR_WIDTH;
+ },
+
+ /**
+ * Provides the closest scrollable ancestor.
+ * A port of the jQuery UI scrollParent method:
+ * https://github.com/jquery/jquery-ui/blob/master/ui/scroll-parent.js
+ *
+ * @param {element} elem - The element to find the scroll parent of.
+ * @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered,
+ * default is false.
+ *
+ * @returns {element} A HTML element.
+ */
+ scrollParent: function(elem, includeHidden) {
+ elem = this.getRawNode(elem);
+
+ var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal;
+ var documentEl = $document[0].documentElement;
+ var elemStyle = $window.getComputedStyle(elem);
+ var excludeStatic = elemStyle.position === 'absolute';
+ var scrollParent = elem.parentElement || documentEl;
+
+ if (scrollParent === documentEl || elemStyle.position === 'fixed') {
+ return documentEl;
+ }
+
+ while (scrollParent.parentElement && scrollParent !== documentEl) {
+ var spStyle = $window.getComputedStyle(scrollParent);
+ if (excludeStatic && spStyle.position !== 'static') {
+ excludeStatic = false;
+ }
+
+ if (!excludeStatic && overflowRegex.test(spStyle.overflow + spStyle.overflowY + spStyle.overflowX)) {
+ break;
+ }
+ scrollParent = scrollParent.parentElement;
+ }
+
+ return scrollParent;
+ },
+
/**
* Provides read-only equivalent of jQuery's position function:
- * http://api.jquery.com/position/
+ * http://api.jquery.com/position/ - distance to closest positioned
+ * ancestor. Does not account for margins by default like jQuery position.
+ *
+ * @param {element} elem - The element to caclulate the position on.
+ * @param {boolean=} [includeMargins=false] - Should margins be accounted
+ * for, default is false.
+ *
+ * @returns {object} An object with the following properties:
+ *
+ *
**width**: the width of the element
+ *
**height**: the height of the element
+ *
**top**: distance to top edge of offset parent
+ *
**left**: distance to left edge of offset parent
+ *
*/
- position: function(element) {
- var elBCR = this.offset(element);
- var offsetParentBCR = { top: 0, left: 0 };
- var offsetParentEl = parentOffsetEl(element[0]);
- if (offsetParentEl !== $document[0]) {
- offsetParentBCR = this.offset(angular.element(offsetParentEl));
- offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop;
- offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft;
+ position: function(elem, includeMagins) {
+ elem = this.getRawNode(elem);
+
+ var elemOffset = this.offset(elem);
+ if (includeMagins) {
+ var elemStyle = $window.getComputedStyle(elem);
+ elemOffset.top -= this.parseStyle(elemStyle.marginTop);
+ elemOffset.left -= this.parseStyle(elemStyle.marginLeft);
+ }
+ var parent = this.offsetParent(elem);
+ var parentOffset = {top: 0, left: 0};
+
+ if (parent !== $document[0].documentElement) {
+ parentOffset = this.offset(parent);
+ parentOffset.top += parent.clientTop - parent.scrollTop;
+ parentOffset.left += parent.clientLeft - parent.scrollLeft;
}
- var boundingClientRect = element[0].getBoundingClientRect();
return {
- width: boundingClientRect.width || element.prop('offsetWidth'),
- height: boundingClientRect.height || element.prop('offsetHeight'),
- top: elBCR.top - offsetParentBCR.top,
- left: elBCR.left - offsetParentBCR.left
+ width: Math.round(angular.isNumber(elemOffset.width) ? elemOffset.width : elem.offsetWidth),
+ height: Math.round(angular.isNumber(elemOffset.height) ? elemOffset.height : elem.offsetHeight),
+ top: Math.round(elemOffset.top - parentOffset.top),
+ left: Math.round(elemOffset.left - parentOffset.left)
};
},
/**
* Provides read-only equivalent of jQuery's offset function:
- * http://api.jquery.com/offset/
+ * http://api.jquery.com/offset/ - distance to viewport. Does
+ * not account for borders, margins, or padding on the body
+ * element.
+ *
+ * @param {element} elem - The element to calculate the offset on.
+ *
+ * @returns {object} An object with the following properties:
+ *
+ *
**width**: the width of the element
+ *
**height**: the height of the element
+ *
**top**: distance to top edge of viewport
+ *
**right**: distance to bottom edge of viewport
+ *
*/
- offset: function(element) {
- var boundingClientRect = element[0].getBoundingClientRect();
+ offset: function(elem) {
+ elem = this.getRawNode(elem);
+
+ var elemBCR = elem.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)
+ width: Math.round(angular.isNumber(elemBCR.width) ? elemBCR.width : elem.offsetWidth),
+ height: Math.round(angular.isNumber(elemBCR.height) ? elemBCR.height : elem.offsetHeight),
+ top: Math.round(elemBCR.top + ($window.pageYOffset || $document[0].documentElement.scrollTop)),
+ left: Math.round(elemBCR.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft))
};
},
/**
- * Provides coordinates for the targetEl in relation to hostEl
+ * Provides offset distance to the closest scrollable ancestor
+ * or viewport. Accounts for border and scrollbar width.
+ *
+ * Right and bottom dimensions represent the distance to the
+ * respective edge of the viewport element. If the element
+ * edge extends beyond the viewport, a negative value will be
+ * reported.
+ *
+ * @param {element} elem - The element to get the viewport offset for.
+ * @param {boolean=} [useDocument=false] - Should the viewport be the document element instead
+ * of the first scrollable element, default is false.
+ * @param {boolean=} [includePadding=true] - Should the padding on the offset parent element
+ * be accounted for, default is true.
+ *
+ * @returns {object} An object with the following properties:
+ *
+ *
**top**: distance to the top content edge of viewport element
+ *
**bottom**: distance to the bottom content edge of viewport element
+ *
**left**: distance to the left content edge of viewport element
+ *
**right**: distance to the right content edge of viewport element
top: element on top, horizontally centered on host element.
+ *
top-left: element on top, left edge aligned with host element left edge.
+ *
top-right: element on top, lerightft edge aligned with host element right edge.
+ *
bottom: element on bottom, horizontally centered on host element.
+ *
bottom-left: element on bottom, left edge aligned with host element left edge.
+ *
bottom-right: element on bottom, right edge aligned with host element right edge.
+ *
left: element on left, vertically centered on host element.
+ *
left-top: element on left, top edge aligned with host element top edge.
+ *
left-bottom: element on left, bottom edge aligned with host element bottom edge.
+ *
right: element on right, vertically centered on host element.
+ *
right-top: element on right, top edge aligned with host element top edge.
+ *
right-bottom: element on right, bottom edge aligned with host element bottom edge.
+ *
+ * A placement string with an 'auto' indicator is expected to be
+ * space separated from the placement, i.e: 'auto bottom-left' If
+ * the primary and secondary placement values do not match 'top,
+ * bottom, left, right' then 'top' will be the primary placement and
+ * 'center' will be the secondary placement. If 'auto' is passed, true
+ * will be returned as the 3rd value of the array.
+ *
+ * @param {string} placement - The placement string to parse.
+ *
+ * @returns {array} An array with the following values
+ *
+ *
**[0]**: The primary placement.
+ *
**[1]**: The secondary placement.
+ *
**[2]**: If auto is passed: true, else undefined.
+ *
+ */
+ parsePlacement: function(placement) {
+ var autoPlace = PLACEMENT_REGEX.auto.test(placement);
+ if (autoPlace) {
+ placement = placement.replace(PLACEMENT_REGEX.auto, '');
+ }
+
+ placement = placement.split('-');
+
+ placement[0] = placement[0] || 'top';
+ if (!PLACEMENT_REGEX.primary.test(placement[0])) {
+ placement[0] = 'top';
+ }
+
+ placement[1] = placement[1] || 'center';
+ if (!PLACEMENT_REGEX.secondary.test(placement[1])) {
+ placement[1] = 'center';
+ }
+
+ if (autoPlace) {
+ placement[2] = true;
+ } else {
+ placement[2] = false;
+ }
+
+ return placement;
+ },
+
+ /**
+ * Provides coordinates for an element to be positioned relative to
+ * another element. Passing 'auto' as part of the placement parameter
+ * will enable smart placement - where the element fits. i.e:
+ * 'auto left-top' will check to see if there is enough space to the left
+ * of the hostElem to fit the targetElem, if not place right (same for secondary
+ * top placement). Available space is calculated using the viewportOffset
+ * function.
+ *
+ * @param {element} hostElem - The element to position against.
+ * @param {element} targetElem - The element to position.
+ * @param {string=} [placement=top] - The placement for the targetElem,
+ * default is 'top'. 'center' is assumed as secondary placement for
+ * 'top', 'left', 'right', and 'bottom' placements. Available placements are:
+ *
+ *
top
+ *
top-right
+ *
top-left
+ *
bottom
+ *
bottom-left
+ *
bottom-right
+ *
left
+ *
left-top
+ *
left-bottom
+ *
right
+ *
right-top
+ *
right-bottom
+ *
+ * @param {boolean=} [appendToBody=false] - Should the top and left values returned
+ * be calculated from the body element, default is false.
+ *
+ * @returns {object} An object with the following properties:
+ *
');
@@ -57,7 +55,7 @@ describe('position elements', function () {
});
$document.find('body').append(el);
- var offset = $position.offset(el);
+ var offset = $uibPosition.offset(el);
expect(offset).toEqual({
width: 100,
@@ -70,12 +68,96 @@ describe('position elements', function () {
});
});
- describe('position', function() {
- var $document, el;
+ describe('viewportOffset', function() {
+ var el;
+
+ beforeEach(function() {
+ el = angular.element('
');
+ $document.find('body').append(el);
+ });
+
+ afterEach(function() {
+ el.remove();
+ });
+
+ it('measures the offset', function() {
+ var vpOffset = $uibPosition.viewportOffset(document.getElementById('inner'));
+ expect(vpOffset).toEqual({
+ top: 20,
+ bottom: 30,
+ left: 20,
+ right: 30
+ });
+ });
+
+ it('measures the offset without padding', function() {
+ var outerEl = document.getElementById('outer');
+ outerEl.style.paddingTop = '0px';
+ outerEl.style.paddingBottom = '0px';
+ outerEl.style.paddingLeft = '0px';
+ outerEl.style.paddingRight = '0px';
+
+ var vpOffset = $uibPosition.viewportOffset(document.getElementById('inner'));
+ expect(vpOffset).toEqual({
+ top: 20,
+ bottom: 80,
+ left: 20,
+ right: 80
+ });
+ });
+
+ it('measures the offset with borders', function() {
+ var outerEl = document.getElementById('outer');
+ outerEl.style.width = '220px';
+ outerEl.style.height = '220px';
+ outerEl.style.border = '10px solid black';
+
+ var vpOffset = $uibPosition.viewportOffset(document.getElementById('inner'));
+ expect(vpOffset).toEqual({
+ top: 20,
+ bottom: 30,
+ left: 20,
+ right: 30
+ });
+ });
+
+ it('measures the offset excluding padding', function() {
+ var vpOffset = $uibPosition.viewportOffset(document.getElementById('inner'), false, false);
+ expect(vpOffset).toEqual({
+ top: 45,
+ bottom: 55,
+ left: 45,
+ right: 55
+ });
+ });
- beforeEach(inject(function(_$document_) {
- $document = _$document_;
- }));
+ it('measures the offset when scrolled', function() {
+ var innerEl = document.getElementById('inner');
+ innerEl.style.width = '300px';
+ innerEl.style.height = '300px';
+ var outerEl = document.getElementById('outer');
+ outerEl.scrollTop = 25;
+ outerEl.scrollLeft = 25;
+
+ var vpOffset = $uibPosition.viewportOffset(document.getElementById('inner'));
+ expect(vpOffset.top).toEqual(-5);
+ expect(vpOffset.bottom).toBeGreaterThan(-180);
+ expect(vpOffset.left).toEqual(-5);
+ expect(vpOffset.right).toBeGreaterThan(-180);
+
+ //brittle
+ // expect(vpOffset).toEqual({
+ // top: -5,
+ // bottom: -162,
+ // left: -5,
+ // right: -162
+ // });
+ });
+
+ });
+
+ describe('position', function() {
+ var el;
afterEach(function() {
el.remove();
@@ -93,7 +175,7 @@ describe('position elements', function () {
$document.find('body').append(el);
- var position = $position.position(el);
+ var position = $uibPosition.position(el);
expect(position).toEqual({
width: 100,
@@ -103,7 +185,7 @@ describe('position elements', function () {
});
});
- it('gets position with another DOM as the relative parent', function() {
+ it('gets position with an element as the relative parent', function() {
el = angular.element('
Foo
');
$document.find('body').append(el);
@@ -124,7 +206,7 @@ describe('position elements', function () {
left: 5
});
- var position = $position.position(innerEl);
+ var position = $uibPosition.position(innerEl);
expect(position).toEqual({
width: 20,
@@ -135,10 +217,79 @@ describe('position elements', function () {
});
});
- describe('append-to-body: false', function() {
+ describe('scrollParent', function() {
+ var el;
+
+ afterEach(function() {
+ el.remove();
+ });
+
+ it('gets the closest scrollable ancestor', function() {
+ el = angular.element('
Foo
Bar
');
+
+ $document.find('body').css({overflow: 'auto'}).append(el);
+
+ var outerEl = document.getElementById('outer');
+ var innerEl = document.getElementById('inner');
+
+ var scrollParent = $uibPosition.scrollParent(innerEl);
+ expect(scrollParent).toEqual(outerEl);
+ });
+
+ it('gets the closest scrollable ancestor with overflow-x: scroll', function() {
+ el = angular.element('
Foo
Bar
');
+
+ $document.find('body').css({overflow: 'auto'}).append(el);
+
+ var outerEl = document.getElementById('outer');
+ var innerEl = document.getElementById('inner');
+
+ var scrollParent = $uibPosition.scrollParent(innerEl);
+ expect(scrollParent).toEqual(outerEl);
+ });
+
+ it('gets the closest scrollable ancestor with overflow-y: hidden', function() {
+ el = angular.element('
Foo
Bar
');
+
+ $document.find('body').css({overflow: 'auto'}).append(el);
+
+ var outerEl = document.getElementById('outer');
+ var innerEl = document.getElementById('inner');
+
+ var scrollParent = $uibPosition.scrollParent(innerEl, true);
+ expect(scrollParent).toEqual(outerEl);
+ });
+
+ it('gets the document element if no scrollable ancestor exists', function() {
+ el = angular.element('
Foo
Bar
');
+
+ $document.find('body').css({overflow: ''}).append(el);
+
+ var innerEl = document.getElementById('inner');
+
+ var scrollParent = $uibPosition.scrollParent(innerEl);
+ expect(scrollParent).toEqual($document[0].documentElement);
+ });
+
+ it('gets the closest scrollable ancestor after a positioned ancestor when positioned absolute', function() {
+ el = angular.element('
Foo
Bar
');
+
+ $document.find('body').css({overflow: 'auto'}).append(el);
+
+ var outerEl = document.getElementById('outer');
+ var innerEl = document.getElementById('inner');
+
+ var scrollParent = $uibPosition.scrollParent(innerEl);
+ expect(scrollParent).toEqual(outerEl);
+ });
+ });
+
+ describe('positionElements - append-to-body: false', function() {
+ var el;
+
beforeEach(function() {
//mock position info normally queried from the DOM
- $position.position = function() {
+ $uibPosition.position = function() {
return {
width: 20,
height: 20,
@@ -149,63 +300,63 @@ describe('position elements', function () {
});
it('should position element on top-center by default', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'other')).toBePositionedAt(90, 105);
- expect($position.positionElements({}, new TargetElMock(10, 10), 'top')).toBePositionedAt(90, 105);
- expect($position.positionElements({}, new TargetElMock(10, 10), 'top-center')).toBePositionedAt(90, 105);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'other')).toBePositionedAt(90, 105);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top')).toBePositionedAt(90, 105);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top-center')).toBePositionedAt(90, 105);
});
it('should position on top-left', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'top-left')).toBePositionedAt(90, 100);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top-left')).toBePositionedAt(90, 100);
});
it('should position on top-right', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'top-right')).toBePositionedAt(90, 120);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top-right')).toBePositionedAt(90, 110);
});
it('should position elements on bottom-center when "bottom" specified', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'bottom')).toBePositionedAt(120, 105);
- expect($position.positionElements({}, new TargetElMock(10, 10), 'bottom-center')).toBePositionedAt(120, 105);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom')).toBePositionedAt(120, 105);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom-center')).toBePositionedAt(120, 105);
});
it('should position elements on bottom-left', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'bottom-left')).toBePositionedAt(120, 100);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom-left')).toBePositionedAt(120, 100);
});
it('should position elements on bottom-right', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'bottom-right')).toBePositionedAt(120, 120);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom-right')).toBePositionedAt(120, 110);
});
it('should position elements on left-center when "left" specified', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'left')).toBePositionedAt(105, 90);
- expect($position.positionElements({}, new TargetElMock(10, 10), 'left-center')).toBePositionedAt(105, 90);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left')).toBePositionedAt(105, 90);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left-center')).toBePositionedAt(105, 90);
});
it('should position elements on left-top when "left-top" specified', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'left-top')).toBePositionedAt(100, 90);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left-top')).toBePositionedAt(100, 90);
});
it('should position elements on left-bottom when "left-bottom" specified', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'left-bottom')).toBePositionedAt(120, 90);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left-bottom')).toBePositionedAt(110, 90);
});
it('should position elements on right-center when "right" specified', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'right')).toBePositionedAt(105, 120);
- expect($position.positionElements({}, new TargetElMock(10, 10), 'right-center')).toBePositionedAt(105, 120);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right')).toBePositionedAt(105, 120);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right-center')).toBePositionedAt(105, 120);
});
it('should position elements on right-top when "right-top" specified', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'right-top')).toBePositionedAt(100, 120);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right-top')).toBePositionedAt(100, 120);
});
it('should position elements on right-top when "right-top" specified', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'right-bottom')).toBePositionedAt(120, 120);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right-bottom')).toBePositionedAt(110, 120);
});
});
- describe('append-to-body: true', function() {
+ describe('positionElements - append-to-body: true', function() {
beforeEach(function() {
//mock offset info normally queried from the DOM
- $position.offset = function() {
+ $uibPosition.offset = function() {
return {
width: 20,
height: 20,
@@ -216,56 +367,156 @@ describe('position elements', function () {
});
it('should position element on top-center by default', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'other', true)).toBePositionedAt(90, 105);
- expect($position.positionElements({}, new TargetElMock(10, 10), 'top', true)).toBePositionedAt(90, 105);
- expect($position.positionElements({}, new TargetElMock(10, 10), 'top-center', true)).toBePositionedAt(90, 105);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'other', true)).toBePositionedAt(90, 105);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top', true)).toBePositionedAt(90, 105);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top-center', true)).toBePositionedAt(90, 105);
});
it('should position on top-left', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'top-left', true)).toBePositionedAt(90, 100);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top-left', true)).toBePositionedAt(90, 100);
});
it('should position on top-right', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'top-right', true)).toBePositionedAt(90, 120);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'top-right', true)).toBePositionedAt(90, 110);
});
it('should position elements on bottom-center when "bottom" specified', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'bottom', true)).toBePositionedAt(120, 105);
- expect($position.positionElements({}, new TargetElMock(10, 10), 'bottom-center', true)).toBePositionedAt(120, 105);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom', true)).toBePositionedAt(120, 105);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom-center', true)).toBePositionedAt(120, 105);
});
it('should position elements on bottom-left', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'bottom-left', true)).toBePositionedAt(120, 100);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom-left', true)).toBePositionedAt(120, 100);
});
it('should position elements on bottom-right', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'bottom-right', true)).toBePositionedAt(120, 120);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'bottom-right', true)).toBePositionedAt(120, 110);
});
it('should position elements on left-center when "left" specified', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'left', true)).toBePositionedAt(105, 90);
- expect($position.positionElements({}, new TargetElMock(10, 10), 'left-center', true)).toBePositionedAt(105, 90);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left', true)).toBePositionedAt(105, 90);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left-center', true)).toBePositionedAt(105, 90);
});
it('should position elements on left-top when "left-top" specified', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'left-top', true)).toBePositionedAt(100, 90);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left-top', true)).toBePositionedAt(100, 90);
});
it('should position elements on left-bottom when "left-bottom" specified', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'left-bottom', true)).toBePositionedAt(120, 90);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'left-bottom', true)).toBePositionedAt(110, 90);
});
it('should position elements on right-center when "right" specified', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'right', true)).toBePositionedAt(105, 120);
- expect($position.positionElements({}, new TargetElMock(10, 10), 'right-center', true)).toBePositionedAt(105, 120);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right', true)).toBePositionedAt(105, 120);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right-center', true)).toBePositionedAt(105, 120);
});
it('should position elements on right-top when "right-top" specified', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'right-top', true)).toBePositionedAt(100, 120);
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right-top', true)).toBePositionedAt(100, 120);
});
- it('should position elements on right-top when "right-top" specified', function() {
- expect($position.positionElements({}, new TargetElMock(10, 10), 'right-bottom', true)).toBePositionedAt(120, 120);
+ it('should position elements on right-bottom when "right-bottom" specified', function() {
+ expect($uibPosition.positionElements({}, new TargetElMock(10, 10), 'right-bottom', true)).toBePositionedAt(110, 120);
+ });
+ });
+
+ describe('smart positioning', function() {
+ var viewportOffset, el;
+
+ beforeEach(function() {
+ el = angular.element('');
+ $document.find('body').append(el);
+
+ //mock position info normally queried from the DOM
+ $uibPosition.position = function() {
+ return {
+ width: 40,
+ height: 40,
+ top: 100,
+ left: 100
+ };
+ };
+
+ viewportOffset = {
+ width: 10,
+ height: 10,
+ top: 10,
+ bottom: 10,
+ left: 10,
+ right: 10
+ };
+
+ $uibPosition.viewportOffset = function() {
+ return viewportOffset;
+ };
+ });
+
+ afterEach(function() {
+ el.remove();
+ });
+
+ // tests primary top -> bottom
+ // tests secondary left -> right
+ it('should position element on bottom-right when top-left does not fit', function() {
+ viewportOffset.bottom = 20;
+ viewportOffset.left = 20;
+ el.css({ width: '60px', height: '20px' });
+ expect($uibPosition.positionElements({}, el, 'auto top-left')).toBePositionedAt(140, 80);
+ });
+
+ // tests primary bottom -> top
+ // tests secondary right -> left
+ it('should position element on top-left when bottom-right does not fit', function() {
+ viewportOffset.top = 20;
+ viewportOffset.right = 20;
+ el.css({ width: '60px', height: '20px' });
+ expect($uibPosition.positionElements({}, el, 'auto bottom-right')).toBePositionedAt(80, 100);
+ });
+
+ // tests primary left -> right
+ // tests secondary top -> bottom
+ it('should position element on right-bottom when left-top does not fit', function() {
+ viewportOffset.top = 20;
+ viewportOffset.right = 20;
+ el.css({ width: '20px', height: '60px' });
+ expect($uibPosition.positionElements({}, el, 'auto left-top')).toBePositionedAt(80, 140);
+ });
+
+ // tests primary right -> left
+ // tests secondary bottom -> top
+ it('should position element on left-top when right-bottom does not fit', function() {
+ viewportOffset.bottom = 20;
+ viewportOffset.left = 20;
+ el.css({ width: '20px', height: '60px' });
+ expect($uibPosition.positionElements({}, el, 'auto right-bottom')).toBePositionedAt(100, 80);
+ });
+
+ // tests vertical center -> top
+ it('should position element on left-top when left-center does not fit vetically', function() {
+ viewportOffset.bottom = 100;
+ el.css({ width: '20px', height: '120px' });
+ expect($uibPosition.positionElements({}, el, 'auto left')).toBePositionedAt(100, 80);
+ });
+
+ // tests vertical center -> bottom
+ it('should position element on left-bottom when left-center does not fit vertically', function() {
+ viewportOffset.top = 100;
+ el.css({ width: '20px', height: '120px' });
+ expect($uibPosition.positionElements({}, el, 'auto left')).toBePositionedAt(20, 80);
+ });
+
+ // tests horizontal center -> left
+ it('should position element on top-left when top-center does not fit horizontally', function() {
+ viewportOffset.right = 100;
+ el.css({ width: '120px', height: '20px' });
+ expect($uibPosition.positionElements({}, el, 'auto top')).toBePositionedAt(80, 100);
+ });
+
+ // tests horizontal center -> right
+ it('should position element on top-right when top-center does not fit horizontally', function() {
+ viewportOffset.left = 100;
+ el.css({ width: '120px', height: '20px' });
+ expect($uibPosition.positionElements({}, el, 'auto top')).toBePositionedAt(80, 20);
});
});
});
diff --git a/src/tooltip/docs/demo.html b/src/tooltip/docs/demo.html
index 795f3657b0..c9be614fdb 100644
--- a/src/tooltip/docs/demo.html
+++ b/src/tooltip/docs/demo.html
@@ -1,4 +1,12 @@
+
Positional
+
+
+
+
+
+
+
@@ -11,19 +19,14 @@
Pellentesque {{dynamicTooltipText}},
sit amet venenatis urna cursus eget nunc scelerisque viverra mauris, in
aliquam. Tincidunt lobortis feugiat vivamus at
- left eget
- arcu dictum varius duis at consectetur lorem. Vitae elementum curabitur
- right
- nunc sed velit dignissim sodales ut eu sem integer vitae. Turpis egestas
- bottom
- pharetra convallis posuere morbi leo urna,
fading
- at elementum eu, facilisis sed odio morbi quis commodo odio. In cursus
+ eget arcu dictum varius duis at consectetur lorem. Vitae elementum curabitur
show delay
- turpis massa tincidunt dui ut. In cursus
+ nunc sed velit dignissim sodales ut eu sem integer vitae. Turpis egestas
hide delay
+ pharetra convallis posuere morbi leo urna,
Custom template
- nunc sed velit dignissim sodales ut eu sem integer vitae. Turpis egestas
+ at elementum eu, facilisis sed odio morbi quis commodo odio.