From 191055b7a5e608e5e0c63850af4094007a10427b Mon Sep 17 00:00:00 2001 From: penfold Date: Sat, 2 Nov 2013 22:44:12 +0000 Subject: [PATCH 1/6] feat(tooltip) - auto positioning --- src/tooltip/tooltip.js | 47 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js index cf6256c7e3..e234228ed0 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -185,10 +185,49 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap ttWidth = tooltip.prop( 'offsetWidth' ); ttHeight = tooltip.prop( 'offsetHeight' ); - // Calculate the tooltip's top and left coordinates to center it with - // this directive. - switch ( scope.tt_placement ) { - case 'right': + var fitsVerticallyCentered = (position.left + position.width / 2 + ttWidth / 2 <= $window.pageXOffset + $document.width()) && + (position.left + position.width / 2 - ttWidth / 2 >= $window.pageXOffset); + + var pageYOffset = ($window.pageYOffset || $document[0].body.scrollTop || $document[0].documentElement.scrollTop); + var pageXOffset = ($window.pageXOffset || $document[0].body.scrollLeft || $document[0].documentElement.scrollLeft); + + // Calculate the tooltip's top and left coordinates to center it with + // this directive. + switch (scope.tt_placement) { + case 'auto': + var ttLeft; + var ttTop; + if (fitsVerticallyCentered && (position.top - ttHeight >= pageYOffset)) { + // Top + ttLeft = position.left + position.width / 2 - ttWidth / 2; + ttTop = position.top - ttHeight; + } else if (fitsVerticallyCentered && (position.top + position.height + ttHeight <= pageYOffset + $document.height())) { + // Bottom + ttLeft = position.left + position.width / 2 - ttWidth / 2; + ttTop = position.top + position.height; + } else if ((position.left + position.width + ttWidth <= pageXOffset + $document.width()) && + (position.left + position.width + ttWidth >= pageXOffset)) { + // Right side + ttLeft = position.left + position.width; + ttTop = position.top + position.height / 2 - ttHeight / 2; + } else if ((position.left - ttWidth >= pageXOffset) && + (position.left + position.width + ttWidth >= pageXOffset)) { + // Left side + ttLeft = position.left - ttWidth; + ttTop = position.top + position.height / 2 - ttHeight / 2; + } else { + // Tooltip too big + ttLeft = pageXOffset; + ttTop = pageYOffset; + } + + ttPosition = { + top: ttTop, + left: ttLeft + }; + + break; + case 'right': ttPosition = { top: position.top + position.height / 2 - ttHeight / 2, left: position.left + position.width From c171e8af4275e6322910e37478875223b598ecf2 Mon Sep 17 00:00:00 2001 From: penfold Date: Mon, 4 Nov 2013 20:13:32 +0000 Subject: [PATCH 2/6] feat(tooltip) - fixed tooltip tag for auto positioning --- src/tooltip/tooltip.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js index e234228ed0..35422665d1 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -193,7 +193,10 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap // Calculate the tooltip's top and left coordinates to center it with // this directive. - switch (scope.tt_placement) { + + scope.tt_placement = scope.tt_requested_placement; + + switch (scope.tt_requested_placement) { case 'auto': var ttLeft; var ttTop; @@ -201,24 +204,30 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap // Top ttLeft = position.left + position.width / 2 - ttWidth / 2; ttTop = position.top - ttHeight; + scope.tt_placement = 'top'; } else if (fitsVerticallyCentered && (position.top + position.height + ttHeight <= pageYOffset + $document.height())) { // Bottom ttLeft = position.left + position.width / 2 - ttWidth / 2; - ttTop = position.top + position.height; + ttTop = position.top + position.height; + scope.tt_placement = 'bottom'; } else if ((position.left + position.width + ttWidth <= pageXOffset + $document.width()) && (position.left + position.width + ttWidth >= pageXOffset)) { // Right side ttLeft = position.left + position.width; ttTop = position.top + position.height / 2 - ttHeight / 2; + scope.tt_placement = 'right'; } else if ((position.left - ttWidth >= pageXOffset) && (position.left + position.width + ttWidth >= pageXOffset)) { // Left side ttLeft = position.left - ttWidth; ttTop = position.top + position.height / 2 - ttHeight / 2; + scope.tt_placement = 'left'; } else { // Tooltip too big ttLeft = pageXOffset; ttTop = pageYOffset; + + scope.tt_placement = 'top'; } ttPosition = { @@ -299,7 +308,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap }); attrs.$observe( prefix+'Placement', function ( val ) { - scope.tt_placement = angular.isDefined( val ) ? val : options.placement; + scope.tt_requested_placement = angular.isDefined( val ) ? val : options.placement; }); attrs.$observe( prefix+'Animation', function ( val ) { From 42194b29a0eca8ba6c7bf56ea45fe6ad5ff5d689 Mon Sep 17 00:00:00 2001 From: penfold Date: Mon, 4 Nov 2013 20:51:34 +0000 Subject: [PATCH 3/6] feat(tooltip) - fixed window sizing --- src/tooltip/tooltip.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js index 35422665d1..1b5b1fe3fc 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -185,11 +185,14 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap ttWidth = tooltip.prop( 'offsetWidth' ); ttHeight = tooltip.prop( 'offsetHeight' ); - var fitsVerticallyCentered = (position.left + position.width / 2 + ttWidth / 2 <= $window.pageXOffset + $document.width()) && - (position.left + position.width / 2 - ttWidth / 2 >= $window.pageXOffset); - var pageYOffset = ($window.pageYOffset || $document[0].body.scrollTop || $document[0].documentElement.scrollTop); var pageXOffset = ($window.pageXOffset || $document[0].body.scrollLeft || $document[0].documentElement.scrollLeft); + var innerWidth = $window.innerWidth || $document.documentElement.clientWidth || $document.body.clientWidth; + var innerHeight = $window.innerHeight || $document.documentElement.clientHeight || $document.body.clientHeight; + + + var fitsVerticallyCentered = (position.left + position.width / 2 + ttWidth / 2 <= pageXOffset + innerWidth) && + (position.left + position.width / 2 - ttWidth / 2 >= pageXOffset); // Calculate the tooltip's top and left coordinates to center it with // this directive. @@ -205,12 +208,12 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap ttLeft = position.left + position.width / 2 - ttWidth / 2; ttTop = position.top - ttHeight; scope.tt_placement = 'top'; - } else if (fitsVerticallyCentered && (position.top + position.height + ttHeight <= pageYOffset + $document.height())) { + } else if (fitsVerticallyCentered && (position.top + position.height + ttHeight <= pageYOffset + innerHeight)) { // Bottom ttLeft = position.left + position.width / 2 - ttWidth / 2; ttTop = position.top + position.height; scope.tt_placement = 'bottom'; - } else if ((position.left + position.width + ttWidth <= pageXOffset + $document.width()) && + } else if ((position.left + position.width + ttWidth <= pageXOffset + innerWidth) && (position.left + position.width + ttWidth >= pageXOffset)) { // Right side ttLeft = position.left + position.width; From da6d3973241f774b056521da94122eebbe60ea74 Mon Sep 17 00:00:00 2001 From: penfold Date: Mon, 4 Nov 2013 21:23:18 +0000 Subject: [PATCH 4/6] feat(tooltip) - first pass refactor --- src/tooltip/tooltip.js | 90 ++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 57 deletions(-) diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js index 1b5b1fe3fc..b33517999b 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -36,9 +36,9 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap * $tooltipProvider.options( { placement: 'left' } ); * }); */ - this.options = function( value ) { - angular.extend( globalOptions, value ); - }; + this.options = function( value ) { + angular.extend( globalOptions, value ); + }; /** * This allows you to extend the set of trigger mappings available. E.g.: @@ -184,61 +184,37 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap // Get the height and width of the tooltip so we can center it. ttWidth = tooltip.prop( 'offsetWidth' ); ttHeight = tooltip.prop( 'offsetHeight' ); + + scope.tt_placement = scope.tt_requested_placement; + + if (scope.tt_placement == 'auto'){ + var pageYOffset = ($window.pageYOffset || $document[0].body.scrollTop || $document[0].documentElement.scrollTop); + var pageXOffset = ($window.pageXOffset || $document[0].body.scrollLeft || $document[0].documentElement.scrollLeft); + var innerWidth = $window.innerWidth || $document.documentElement.clientWidth || $document.body.clientWidth; + var innerHeight = $window.innerHeight || $document.documentElement.clientHeight || $document.body.clientHeight; + + var fitsVerticallyCentered = (position.left + position.width / 2 + ttWidth / 2 <= pageXOffset + innerWidth) && + (position.left + position.width / 2 - ttWidth / 2 >= pageXOffset); - var pageYOffset = ($window.pageYOffset || $document[0].body.scrollTop || $document[0].documentElement.scrollTop); - var pageXOffset = ($window.pageXOffset || $document[0].body.scrollLeft || $document[0].documentElement.scrollLeft); - var innerWidth = $window.innerWidth || $document.documentElement.clientWidth || $document.body.clientWidth; - var innerHeight = $window.innerHeight || $document.documentElement.clientHeight || $document.body.clientHeight; - - - var fitsVerticallyCentered = (position.left + position.width / 2 + ttWidth / 2 <= pageXOffset + innerWidth) && - (position.left + position.width / 2 - ttWidth / 2 >= pageXOffset); - - // Calculate the tooltip's top and left coordinates to center it with - // this directive. - - scope.tt_placement = scope.tt_requested_placement; - - switch (scope.tt_requested_placement) { - case 'auto': - var ttLeft; - var ttTop; - if (fitsVerticallyCentered && (position.top - ttHeight >= pageYOffset)) { - // Top - ttLeft = position.left + position.width / 2 - ttWidth / 2; - ttTop = position.top - ttHeight; - scope.tt_placement = 'top'; - } else if (fitsVerticallyCentered && (position.top + position.height + ttHeight <= pageYOffset + innerHeight)) { - // Bottom - ttLeft = position.left + position.width / 2 - ttWidth / 2; - ttTop = position.top + position.height; - scope.tt_placement = 'bottom'; - } else if ((position.left + position.width + ttWidth <= pageXOffset + innerWidth) && - (position.left + position.width + ttWidth >= pageXOffset)) { - // Right side - ttLeft = position.left + position.width; - ttTop = position.top + position.height / 2 - ttHeight / 2; - scope.tt_placement = 'right'; - } else if ((position.left - ttWidth >= pageXOffset) && - (position.left + position.width + ttWidth >= pageXOffset)) { - // Left side - ttLeft = position.left - ttWidth; - ttTop = position.top + position.height / 2 - ttHeight / 2; - scope.tt_placement = 'left'; - } else { - // Tooltip too big - ttLeft = pageXOffset; - ttTop = pageYOffset; - - scope.tt_placement = 'top'; - } - - ttPosition = { - top: ttTop, - left: ttLeft - }; - - break; + if (fitsVerticallyCentered && (position.top - ttHeight >= pageYOffset)) { + scope.tt_placement = 'top'; + } else if (fitsVerticallyCentered && (position.top + position.height + ttHeight <= pageYOffset + innerHeight)) { + scope.tt_placement = 'bottom'; + } else if ((position.left + position.width + ttWidth <= pageXOffset + innerWidth) && + (position.left + position.width + ttWidth >= pageXOffset)) { + scope.tt_placement = 'right'; + } else if ((position.left - ttWidth >= pageXOffset) && + (position.left + position.width + ttWidth >= pageXOffset)) { + scope.tt_placement = 'left'; + } else { + // Tooltip too big - using top + scope.tt_placement = 'top'; + } + } + + // Calculate the tooltip's top and left coordinates to center it with + // this directive. + switch (scope.tt_placement) { case 'right': ttPosition = { top: position.top + position.height / 2 - ttHeight / 2, From b8c7505c136179e38c7a35bf37ee46d7ee8f629d Mon Sep 17 00:00:00 2001 From: penfold Date: Tue, 5 Nov 2013 21:29:29 +0000 Subject: [PATCH 5/6] feat(tooltip) - pulled out auto positioning logic into service --- src/position/position.js | 47 +- src/tooltip/tooltip.js | 638 +++++++++++------------ src/tooltipPlacement/tooltipPlacement.js | 33 ++ 3 files changed, 374 insertions(+), 344 deletions(-) create mode 100644 src/tooltipPlacement/tooltipPlacement.js diff --git a/src/position/position.js b/src/position/position.js index 3bca8b0ba7..bf9f5a7535 100644 --- a/src/position/position.js +++ b/src/position/position.js @@ -1,4 +1,4 @@ -angular.module('ui.bootstrap.position', []) +angular.module( 'ui.bootstrap.position', [] ) /** * A set of utility methods that can be use to retrieve position of DOM elements. @@ -6,13 +6,13 @@ angular.module('ui.bootstrap.position', []) * relation to other, existing elements (this is the case for tooltips, popovers, * typeahead suggestions etc.). */ - .factory('$position', ['$document', '$window', function ($document, $window) { + .factory( '$position', ['$document', '$window', function ( $document, $window ) { - function getStyle(el, cssprop) { + function getStyle( el, cssprop ) { if (el.currentStyle) { //IE return el.currentStyle[cssprop]; } else if ($window.getComputedStyle) { - return $window.getComputedStyle(el)[cssprop]; + return $window.getComputedStyle( el )[cssprop]; } // finally try and get inline style return el.style[cssprop]; @@ -22,18 +22,18 @@ angular.module('ui.bootstrap.position', []) * Checks if a given element is statically positioned * @param element - raw DOM element */ - function isStaticPositioned(element) { - return (getStyle(element, "position") || 'static' ) === 'static'; + function isStaticPositioned( element ) { + return (getStyle( element, "position" ) || 'static' ) === 'static'; } /** * returns the closest, non-statically positioned parentOffset of a given element * @param element */ - var parentOffsetEl = function (element) { + var parentOffsetEl = function ( element ) { var docDomEl = $document[0]; var offsetParent = element.offsetParent || docDomEl; - while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) { + while (offsetParent && offsetParent !== docDomEl && isStaticPositioned( offsetParent )) { offsetParent = offsetParent.offsetParent; } return offsetParent || docDomEl; @@ -44,19 +44,19 @@ angular.module('ui.bootstrap.position', []) * Provides read-only equivalent of jQuery's position function: * http://api.jquery.com/position/ */ - position: function (element) { - var elBCR = this.offset(element); + position: function ( element ) { + var elBCR = this.offset( element ); var offsetParentBCR = { top: 0, left: 0 }; - var offsetParentEl = parentOffsetEl(element[0]); + var offsetParentEl = parentOffsetEl( element[0] ); if (offsetParentEl != $document[0]) { - offsetParentBCR = this.offset(angular.element(offsetParentEl)); + offsetParentBCR = this.offset( angular.element( offsetParentEl ) ); offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop; offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft; } return { - width: element.prop('offsetWidth'), - height: element.prop('offsetHeight'), + width: element.prop( 'offsetWidth' ), + height: element.prop( 'offsetHeight' ), top: elBCR.top - offsetParentBCR.top, left: elBCR.left - offsetParentBCR.left }; @@ -66,14 +66,23 @@ angular.module('ui.bootstrap.position', []) * Provides read-only equivalent of jQuery's offset function: * http://api.jquery.com/offset/ */ - offset: function (element) { + offset: function ( element ) { var boundingClientRect = element[0].getBoundingClientRect(); return { - width: element.prop('offsetWidth'), - height: element.prop('offsetHeight'), + width: element.prop( 'offsetWidth' ), + height: element.prop( 'offsetHeight' ), top: boundingClientRect.top + ($window.pageYOffset || $document[0].body.scrollTop || $document[0].documentElement.scrollTop), - left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft || $document[0].documentElement.scrollLeft) + left: boundingClientRect.left + ($window.pageXOffset || $document[0].body.scrollLeft || $document[0].documentElement.scrollLeft) + }; + }, + + viewport: function () { + return { + pageYOffset: $window.pageYOffset || $document[0].body.scrollTop || $document[0].documentElement.scrollTop, + pageXOffset: $window.pageXOffset || $document[0].body.scrollLeft || $document[0].documentElement.scrollLeft, + innerWidth: $window.innerWidth || $document.documentElement.clientWidth || $document.body.clientWidth, + innerHeight: $window.innerHeight || $document.documentElement.clientHeight || $document.body.clientHeight }; } }; - }]); + }] ); diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js index b33517999b..44ba7dae51 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -3,372 +3,360 @@ * function, placement as a function, inside, support for more triggers than * just mouse enter/leave, html tooltips, and selector delegation. */ -angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap.bindHtml' ] ) +angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap.bindHtml', 'ui.bootstrap.tooltipPlacement' ] ) /** * The $tooltip service creates tooltip- and popover-like directives as well as * houses global options for them. */ -.provider( '$tooltip', function () { - // The default options tooltip and popover. - var defaultOptions = { - placement: 'top', - animation: true, - popupDelay: 0 - }; - - // Default hide triggers for each show trigger - var triggerMap = { - 'mouseenter': 'mouseleave', - 'click': 'click', - 'focus': 'blur' - }; - - // The options specified to the provider globally. - var globalOptions = {}; - - /** - * `options({})` allows global configuration of all tooltips in the - * application. - * - * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) { + .provider( '$tooltip', function () { + // The default options tooltip and popover. + var defaultOptions = { + placement: 'top', + animation: true, + popupDelay: 0 + }; + + // Default hide triggers for each show trigger + var triggerMap = { + 'mouseenter': 'mouseleave', + 'click': 'click', + 'focus': 'blur' + }; + + // The options specified to the provider globally. + var globalOptions = {}; + + /** + * `options({})` allows global configuration of all tooltips in the + * application. + * + * var app = angular.module( 'App', ['ui.bootstrap.tooltip'], function( $tooltipProvider ) { * // place tooltips left instead of top by default * $tooltipProvider.options( { placement: 'left' } ); * }); - */ - this.options = function( value ) { - angular.extend( globalOptions, value ); + */ + this.options = function ( value ) { + angular.extend( globalOptions, value ); + }; + + /** + * This allows you to extend the set of trigger mappings available. E.g.: + * + * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ); + */ + this.setTriggers = function setTriggers( triggers ) { + angular.extend( triggerMap, triggers ); }; - /** - * This allows you to extend the set of trigger mappings available. E.g.: - * - * $tooltipProvider.setTriggers( 'openTrigger': 'closeTrigger' ); - */ - this.setTriggers = function setTriggers ( triggers ) { - angular.extend( triggerMap, triggers ); - }; - - /** - * This is a helper function for translating camel-case to snake-case. - */ - function snake_case(name){ - var regexp = /[A-Z]/g; - var separator = '-'; - return name.replace(regexp, function(letter, pos) { - return (pos ? separator : '') + letter.toLowerCase(); - }); - } - - /** - * Returns the actual instance of the $tooltip service. - * TODO support multiple triggers - */ - this.$get = [ '$window', '$compile', '$timeout', '$parse', '$document', '$position', '$interpolate', function ( $window, $compile, $timeout, $parse, $document, $position, $interpolate ) { - return function $tooltip ( type, prefix, defaultTriggerShow ) { - var options = angular.extend( {}, defaultOptions, globalOptions ); - - /** - * Returns an object of show and hide triggers. - * - * If a trigger is supplied, - * it is used to show the tooltip; otherwise, it will use the `trigger` - * option passed to the `$tooltipProvider.options` method; else it will - * default to the trigger supplied to this directive factory. - * - * The hide trigger is based on the show trigger. If the `trigger` option - * was passed to the `$tooltipProvider.options` method, it will use the - * mapped trigger from `triggerMap` or the passed trigger if the map is - * undefined; otherwise, it uses the `triggerMap` value of the show - * trigger; else it will just use the show trigger. - */ - function getTriggers ( trigger ) { - var show = trigger || options.trigger || defaultTriggerShow; - var hide = triggerMap[show] || show; + /** + * This is a helper function for translating camel-case to snake-case. + */ + function snake_case( name ) { + var regexp = /[A-Z]/g; + var separator = '-'; + return name.replace( regexp, function ( letter, pos ) { + return (pos ? separator : '') + letter.toLowerCase(); + } ); + } + + /** + * Returns the actual instance of the $tooltip service. + * TODO support multiple triggers + */ + this.$get = [ '$window', '$compile', '$timeout', '$parse', '$document', '$position', '$interpolate', '$tooltipPlacement', function ( $window, $compile, $timeout, $parse, $document, $position, $interpolate, $tooltipPlacement ) { + return function $tooltip( type, prefix, defaultTriggerShow ) { + var options = angular.extend( {}, defaultOptions, globalOptions ); + + /** + * Returns an object of show and hide triggers. + * + * If a trigger is supplied, + * it is used to show the tooltip; otherwise, it will use the `trigger` + * option passed to the `$tooltipProvider.options` method; else it will + * default to the trigger supplied to this directive factory. + * + * The hide trigger is based on the show trigger. If the `trigger` option + * was passed to the `$tooltipProvider.options` method, it will use the + * mapped trigger from `triggerMap` or the passed trigger if the map is + * undefined; otherwise, it uses the `triggerMap` value of the show + * trigger; else it will just use the show trigger. + */ + function getTriggers( trigger ) { + var show = trigger || options.trigger || defaultTriggerShow; + var hide = triggerMap[show] || show; + return { + show: show, + hide: hide + }; + } + + var directiveName = snake_case( type ); + + var startSym = $interpolate.startSymbol(); + var endSym = $interpolate.endSymbol(); + var template = + '<' + directiveName + '-popup ' + + 'title="' + startSym + 'tt_title' + endSym + '" ' + + 'content="' + startSym + 'tt_content' + endSym + '" ' + + 'placement="' + startSym + 'tt_placement' + endSym + '" ' + + 'animation="tt_animation()" ' + + 'is-open="tt_isOpen"' + + '>' + + ''; + return { - show: show, - hide: hide - }; - } - - var directiveName = snake_case( type ); - - var startSym = $interpolate.startSymbol(); - var endSym = $interpolate.endSymbol(); - var template = - '<'+ directiveName +'-popup '+ - 'title="'+startSym+'tt_title'+endSym+'" '+ - 'content="'+startSym+'tt_content'+endSym+'" '+ - 'placement="'+startSym+'tt_placement'+endSym+'" '+ - 'animation="tt_animation()" '+ - 'is-open="tt_isOpen"'+ - '>'+ - ''; - - return { - restrict: 'EA', - scope: true, - link: function link ( scope, element, attrs ) { - var tooltip = $compile( template )( scope ); - var transitionTimeout; - var popupTimeout; - var $body; - var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; - var triggers = getTriggers( undefined ); - var hasRegisteredTriggers = false; - var hasEnableExp = angular.isDefined(attrs[prefix+'Enable']); - - // By default, the tooltip is not open. - // TODO add ability to start tooltip opened - scope.tt_isOpen = false; - - function toggleTooltipBind () { - if ( ! scope.tt_isOpen ) { - showTooltipBind(); - } else { - hideTooltipBind(); + restrict: 'EA', + scope: true, + link: function link( scope, element, attrs ) { + var tooltip = $compile( template )( scope ); + var transitionTimeout; + var popupTimeout; + var $body; + var appendToBody = angular.isDefined( options.appendToBody ) ? options.appendToBody : false; + var triggers = getTriggers( undefined ); + var hasRegisteredTriggers = false; + var hasEnableExp = angular.isDefined( attrs[prefix + 'Enable'] ); + + // By default, the tooltip is not open. + // TODO add ability to start tooltip opened + scope.tt_isOpen = false; + + function toggleTooltipBind() { + if (!scope.tt_isOpen) { + showTooltipBind(); + } else { + hideTooltipBind(); + } } - } - - // Show the tooltip with delay if specified, otherwise show it immediately - function showTooltipBind() { - if(hasEnableExp && !scope.$eval(attrs[prefix+'Enable'])) { - return; + + // Show the tooltip with delay if specified, otherwise show it immediately + function showTooltipBind() { + if (hasEnableExp && !scope.$eval( attrs[prefix + 'Enable'] )) { + return; + } + if (scope.tt_popupDelay) { + popupTimeout = $timeout( show, scope.tt_popupDelay ); + } else { + scope.$apply( show ); + } } - if ( scope.tt_popupDelay ) { - popupTimeout = $timeout( show, scope.tt_popupDelay ); - } else { - scope.$apply( show ); + + function hideTooltipBind() { + scope.$apply( function () { + hide(); + } ); } - } - function hideTooltipBind () { - scope.$apply(function () { - hide(); - }); - } - - // Show the tooltip popup element. - function show() { - var position, + // Show the tooltip popup element. + function show() { + var position, ttWidth, ttHeight, ttPosition; - // Don't show empty tooltips. - if ( ! scope.tt_content ) { - return; - } + // Don't show empty tooltips. + if (!scope.tt_content) { + return; + } - // If there is a pending remove transition, we must cancel it, lest the - // tooltip be mysteriously removed. - if ( transitionTimeout ) { - $timeout.cancel( transitionTimeout ); - } - - // Set the initial positioning. - tooltip.css({ top: 0, left: 0, display: 'block' }); - - // Now we add it to the DOM because need some info about it. But it's not - // visible yet anyway. - if ( appendToBody ) { + // If there is a pending remove transition, we must cancel it, lest the + // tooltip be mysteriously removed. + if (transitionTimeout) { + $timeout.cancel( transitionTimeout ); + } + + // Set the initial positioning. + tooltip.css( { top: 0, left: 0, display: 'block' } ); + + // Now we add it to the DOM because need some info about it. But it's not + // visible yet anyway. + if (appendToBody) { $body = $body || $document.find( 'body' ); $body.append( tooltip ); - } else { - element.after( tooltip ); - } + } else { + element.after( tooltip ); + } - // Get the position of the directive element. - position = appendToBody ? $position.offset( element ) : $position.position( element ); - - // Get the height and width of the tooltip so we can center it. - ttWidth = tooltip.prop( 'offsetWidth' ); - ttHeight = tooltip.prop( 'offsetHeight' ); - - scope.tt_placement = scope.tt_requested_placement; - - if (scope.tt_placement == 'auto'){ - var pageYOffset = ($window.pageYOffset || $document[0].body.scrollTop || $document[0].documentElement.scrollTop); - var pageXOffset = ($window.pageXOffset || $document[0].body.scrollLeft || $document[0].documentElement.scrollLeft); - var innerWidth = $window.innerWidth || $document.documentElement.clientWidth || $document.body.clientWidth; - var innerHeight = $window.innerHeight || $document.documentElement.clientHeight || $document.body.clientHeight; - - var fitsVerticallyCentered = (position.left + position.width / 2 + ttWidth / 2 <= pageXOffset + innerWidth) && - (position.left + position.width / 2 - ttWidth / 2 >= pageXOffset); - - if (fitsVerticallyCentered && (position.top - ttHeight >= pageYOffset)) { - scope.tt_placement = 'top'; - } else if (fitsVerticallyCentered && (position.top + position.height + ttHeight <= pageYOffset + innerHeight)) { - scope.tt_placement = 'bottom'; - } else if ((position.left + position.width + ttWidth <= pageXOffset + innerWidth) && - (position.left + position.width + ttWidth >= pageXOffset)) { - scope.tt_placement = 'right'; - } else if ((position.left - ttWidth >= pageXOffset) && - (position.left + position.width + ttWidth >= pageXOffset)) { - scope.tt_placement = 'left'; - } else { - // Tooltip too big - using top - scope.tt_placement = 'top'; - } - } - - // Calculate the tooltip's top and left coordinates to center it with - // this directive. - switch (scope.tt_placement) { - case 'right': - ttPosition = { - top: position.top + position.height / 2 - ttHeight / 2, - left: position.left + position.width - }; - break; - case 'bottom': - ttPosition = { - top: position.top + position.height, - left: position.left + position.width / 2 - ttWidth / 2 - }; - break; - case 'left': - ttPosition = { - top: position.top + position.height / 2 - ttHeight / 2, - left: position.left - ttWidth - }; - break; - default: - ttPosition = { - top: position.top - ttHeight, - left: position.left + position.width / 2 - ttWidth / 2 - }; - break; - } + // Get the position of the directive element. + position = appendToBody ? $position.offset( element ) : $position.position( element ); - ttPosition.top += 'px'; - ttPosition.left += 'px'; + // Get the height and width of the tooltip so we can center it. + ttWidth = tooltip.prop( 'offsetWidth' ); + ttHeight = tooltip.prop( 'offsetHeight' ); - // Now set the calculated positioning. - tooltip.css( ttPosition ); - - // And show the tooltip. - scope.tt_isOpen = true; - } - - // Hide the tooltip popup element. - function hide() { - // First things first: we don't show it anymore. - scope.tt_isOpen = false; + switch (scope.tt_requested_placement) { + case 'auto': + scope.tt_placement = $tooltipPlacement.getAutoPlacement( position, ttWidth, ttHeight ); + break; + default: + scope.tt_placement = scope.tt_requested_placement; + break; + } - //if tooltip is going to be shown after delay, we must cancel this - $timeout.cancel( popupTimeout ); - - // And now we remove it from the DOM. However, if we have animation, we - // need to wait for it to expire beforehand. - // FIXME: this is a placeholder for a port of the transitions library. - if ( angular.isDefined( scope.tt_animation ) && scope.tt_animation() ) { - transitionTimeout = $timeout( function () { tooltip.remove(); }, 500 ); - } else { - tooltip.remove(); + // Calculate the tooltip's top and left coordinates to center it with + // this directive. + switch (scope.tt_placement) { + case 'right': + ttPosition = { + top: position.top + position.height / 2 - ttHeight / 2, + left: position.left + position.width + }; + break; + case 'bottom': + ttPosition = { + top: position.top + position.height, + left: position.left + position.width / 2 - ttWidth / 2 + }; + break; + case 'left': + ttPosition = { + top: position.top + position.height / 2 - ttHeight / 2, + left: position.left - ttWidth + }; + break; + default: + ttPosition = { + top: position.top - ttHeight, + left: position.left + position.width / 2 - ttWidth / 2 + }; + break; + } + + ttPosition.top += 'px'; + ttPosition.left += 'px'; + + // Now set the calculated positioning. + tooltip.css( ttPosition ); + + // And show the tooltip. + scope.tt_isOpen = true; } - } - /** - * Observe the relevant attributes. - */ - attrs.$observe( type, function ( val ) { - if (val) { - scope.tt_content = val; - } else { - if ( scope.tt_isOpen ) { - hide(); + // Hide the tooltip popup element. + function hide() { + // First things first: we don't show it anymore. + scope.tt_isOpen = false; + + //if tooltip is going to be shown after delay, we must cancel this + $timeout.cancel( popupTimeout ); + + // And now we remove it from the DOM. However, if we have animation, we + // need to wait for it to expire beforehand. + // FIXME: this is a placeholder for a port of the transitions library. + if (angular.isDefined( scope.tt_animation ) && scope.tt_animation()) { + transitionTimeout = $timeout( function () { + tooltip.remove(); + }, 500 ); + } else { + tooltip.remove(); } } - }); - - attrs.$observe( prefix+'Title', function ( val ) { - scope.tt_title = val; - }); - attrs.$observe( prefix+'Placement', function ( val ) { - scope.tt_requested_placement = angular.isDefined( val ) ? val : options.placement; - }); + /** + * Observe the relevant attributes. + */ + attrs.$observe( type, function ( val ) { + if (val) { + scope.tt_content = val; + } else { + if (scope.tt_isOpen) { + hide(); + } + } + } ); - attrs.$observe( prefix+'Animation', function ( val ) { - scope.tt_animation = angular.isDefined( val ) ? $parse( val ) : function(){ return options.animation; }; - }); + attrs.$observe( prefix + 'Title', function ( val ) { + scope.tt_title = val; + } ); - attrs.$observe( prefix+'PopupDelay', function ( val ) { - var delay = parseInt( val, 10 ); - scope.tt_popupDelay = ! isNaN(delay) ? delay : options.popupDelay; - }); + attrs.$observe( prefix + 'Placement', function ( val ) { + scope.tt_requested_placement = angular.isDefined( val ) ? val : options.placement; + } ); - attrs.$observe( prefix+'Trigger', function ( val ) { + attrs.$observe( prefix + 'Animation', function ( val ) { + scope.tt_animation = angular.isDefined( val ) ? $parse( val ) : function () { + return options.animation; + }; + } ); - if (hasRegisteredTriggers) { - element.unbind( triggers.show, showTooltipBind ); - element.unbind( triggers.hide, hideTooltipBind ); - } + attrs.$observe( prefix + 'PopupDelay', function ( val ) { + var delay = parseInt( val, 10 ); + scope.tt_popupDelay = !isNaN( delay ) ? delay : options.popupDelay; + } ); - triggers = getTriggers( val ); + attrs.$observe( prefix + 'Trigger', function ( val ) { - if ( triggers.show === triggers.hide ) { - element.bind( triggers.show, toggleTooltipBind ); - } else { - element.bind( triggers.show, showTooltipBind ); - element.bind( triggers.hide, hideTooltipBind ); - } + if (hasRegisteredTriggers) { + element.unbind( triggers.show, showTooltipBind ); + element.unbind( triggers.hide, hideTooltipBind ); + } - hasRegisteredTriggers = true; - }); + triggers = getTriggers( val ); - attrs.$observe( prefix+'AppendToBody', function ( val ) { - appendToBody = angular.isDefined( val ) ? $parse( val )( scope ) : appendToBody; - }); + if (triggers.show === triggers.hide) { + element.bind( triggers.show, toggleTooltipBind ); + } else { + element.bind( triggers.show, showTooltipBind ); + element.bind( triggers.hide, hideTooltipBind ); + } - // if a tooltip is attached to we need to remove it on - // location change as its parent scope will probably not be destroyed - // by the change. - if ( appendToBody ) { - scope.$on('$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess () { - if ( scope.tt_isOpen ) { - hide(); + hasRegisteredTriggers = true; + } ); + + attrs.$observe( prefix + 'AppendToBody', function ( val ) { + appendToBody = angular.isDefined( val ) ? $parse( val )( scope ) : appendToBody; + } ); + + // if a tooltip is attached to we need to remove it on + // location change as its parent scope will probably not be destroyed + // by the change. + if (appendToBody) { + scope.$on( '$locationChangeSuccess', function closeTooltipOnLocationChangeSuccess() { + if (scope.tt_isOpen) { + hide(); + } + } ); } - }); - } - // Make sure tooltip is destroyed and removed. - scope.$on('$destroy', function onDestroyTooltip() { - if ( scope.tt_isOpen ) { - hide(); - } else { - tooltip.remove(); - } - }); - } + // Make sure tooltip is destroyed and removed. + scope.$on( '$destroy', function onDestroyTooltip() { + if (scope.tt_isOpen) { + hide(); + } else { + tooltip.remove(); + } + } ); + } + }; }; + }] + ; + } ) + + . + directive( 'tooltipPopup', function () { + return { + restrict: 'E', + replace: true, + scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-popup.html' + }; + } ) + + .directive( 'tooltip', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); + }] ) + + .directive( 'tooltipHtmlUnsafePopup', function () { + return { + restrict: 'E', + replace: true, + scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html' }; - }]; -}) - -.directive( 'tooltipPopup', function () { - return { - restrict: 'E', - replace: true, - scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/tooltip/tooltip-popup.html' - }; -}) - -.directive( 'tooltip', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); -}]) - -.directive( 'tooltipHtmlUnsafePopup', function () { - return { - restrict: 'E', - replace: true, - scope: { content: '@', placement: '@', animation: '&', isOpen: '&' }, - templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html' - }; -}) - -.directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) { - return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); -}]); + } ) + + .directive( 'tooltipHtmlUnsafe', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'tooltipHtmlUnsafe', 'tooltip', 'mouseenter' ); + }] ); diff --git a/src/tooltipPlacement/tooltipPlacement.js b/src/tooltipPlacement/tooltipPlacement.js new file mode 100644 index 0000000000..00d0c25c27 --- /dev/null +++ b/src/tooltipPlacement/tooltipPlacement.js @@ -0,0 +1,33 @@ +angular.module( 'ui.bootstrap.tooltipPlacement', ['ui.bootstrap.position'] ) + +/** + * A set of utility methods used to calculate the placement of a tooltip. + */ + .factory( '$tooltipPlacement', ['$position', function ( $position ) { + + return { + getAutoPlacement: function ( position, ttWidth, ttHeight ) { + + var viewport = $position.viewport(); + + var fitsVerticallyCentered = (position.left + position.width / 2 + ttWidth / 2 <= viewport.pageXOffset + viewport.innerWidth) && + (position.left + position.width / 2 - ttWidth / 2 >= viewport.pageXOffset); + + if (fitsVerticallyCentered && (position.top - ttHeight >= viewport.pageYOffset)) { + return 'top'; + } else if (fitsVerticallyCentered && (position.top + position.height + ttHeight <= viewport.pageYOffset + viewport.innerHeight)) { + return 'bottom'; + } else if ((position.left + position.width + ttWidth <= viewport.pageXOffset + viewport.innerWidth) && + (position.left + position.width + ttWidth >= viewport.pageXOffset)) { + return 'right'; + } else if ((position.left - ttWidth >= viewport.pageXOffset) && + (position.left + position.width + ttWidth >= viewport.pageXOffset)) { + return 'left'; + } else { + // Tooltip too big - using top + return 'top'; + } + } + }; + }] ) +; From 27347eacb388acd08c8ed978e6d795a1ced98fe2 Mon Sep 17 00:00:00 2001 From: penfold Date: Tue, 5 Nov 2013 21:42:49 +0000 Subject: [PATCH 6/6] feat(tooltip) - refactored tooltip positioning to tooltipPlacement service --- src/tooltip/tooltip.js | 27 +---------------------- src/tooltipPlacement/tooltipPlacement.js | 28 ++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js index 44ba7dae51..f63626815c 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -196,32 +196,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap // Calculate the tooltip's top and left coordinates to center it with // this directive. - switch (scope.tt_placement) { - case 'right': - ttPosition = { - top: position.top + position.height / 2 - ttHeight / 2, - left: position.left + position.width - }; - break; - case 'bottom': - ttPosition = { - top: position.top + position.height, - left: position.left + position.width / 2 - ttWidth / 2 - }; - break; - case 'left': - ttPosition = { - top: position.top + position.height / 2 - ttHeight / 2, - left: position.left - ttWidth - }; - break; - default: - ttPosition = { - top: position.top - ttHeight, - left: position.left + position.width / 2 - ttWidth / 2 - }; - break; - } + ttPosition = $tooltipPlacement.getPosition( scope.tt_placement, position, ttWidth, ttHeight ); ttPosition.top += 'px'; ttPosition.left += 'px'; diff --git a/src/tooltipPlacement/tooltipPlacement.js b/src/tooltipPlacement/tooltipPlacement.js index 00d0c25c27..161f0a1d13 100644 --- a/src/tooltipPlacement/tooltipPlacement.js +++ b/src/tooltipPlacement/tooltipPlacement.js @@ -27,7 +27,31 @@ angular.module( 'ui.bootstrap.tooltipPlacement', ['ui.bootstrap.position'] ) // Tooltip too big - using top return 'top'; } + }, + + getPosition: function ( tt_placement, position, ttWidth, ttHeight ) { + switch (tt_placement) { + case 'right': + return { + top: position.top + position.height / 2 - ttHeight / 2, + left: position.left + position.width + }; + case 'bottom': + return { + top: position.top + position.height, + left: position.left + position.width / 2 - ttWidth / 2 + }; + case 'left': + return { + top: position.top + position.height / 2 - ttHeight / 2, + left: position.left - ttWidth + }; + default: + return { + top: position.top - ttHeight, + left: position.left + position.width / 2 - ttWidth / 2 + }; + } } }; - }] ) -; + }] );