From 87d5afef7d1c88a7bbc643e530dfe076afd50e93 Mon Sep 17 00:00:00 2001 From: Michael Ball Date: Thu, 31 Jan 2013 08:22:24 -0500 Subject: [PATCH] #99 Add Popover directive --- src/popover/docs/demo.html | 23 +++++ src/popover/docs/demo.js | 5 ++ src/popover/docs/readme.md | 2 + src/popover/popover.js | 154 ++++++++++++++++++++++++++++++++ src/popover/test/popoverSpec.js | 118 ++++++++++++++++++++++++ template/popover/popover.html | 8 ++ 6 files changed, 310 insertions(+) create mode 100644 src/popover/docs/demo.html create mode 100644 src/popover/docs/demo.js create mode 100644 src/popover/docs/readme.md create mode 100644 src/popover/popover.js create mode 100644 src/popover/test/popoverSpec.js create mode 100644 template/popover/popover.html diff --git a/src/popover/docs/demo.html b/src/popover/docs/demo.html new file mode 100644 index 0000000000..d8bbf5b10c --- /dev/null +++ b/src/popover/docs/demo.html @@ -0,0 +1,23 @@ +
+
+
+

Dynamic

+
Dynamic Popover :
+
Dynamic Popover Popup Text:
+
Dynamic Popover Popup Title:
+
+
+
+

Positional

+ + + + +
+
+

Other

+ + +
+
+
diff --git a/src/popover/docs/demo.js b/src/popover/docs/demo.js new file mode 100644 index 0000000000..687cce0c3c --- /dev/null +++ b/src/popover/docs/demo.js @@ -0,0 +1,5 @@ +var PopoverDemoCtrl = function ($scope) { + $scope.dynamicPopover = "Hello, World!"; + $scope.dynamicPopoverText = "dynamic"; + $scope.dynamicPopoverTitle = "Title"; +}; diff --git a/src/popover/docs/readme.md b/src/popover/docs/readme.md new file mode 100644 index 0000000000..eb5f53428a --- /dev/null +++ b/src/popover/docs/readme.md @@ -0,0 +1,2 @@ +A lightweight, extensible directive for fancy popover creation. The popover +directive supports multiple placements, optional transition animation, and more. diff --git a/src/popover/popover.js b/src/popover/popover.js new file mode 100644 index 0000000000..f6d027511f --- /dev/null +++ b/src/popover/popover.js @@ -0,0 +1,154 @@ +/** + * The following features are still outstanding: popup delay, animation as a + * function, placement as a function, inside, support for more triggers than + * just mouse enter/leave, html popovers, and selector delegatation. + */ +angular.module( 'ui.bootstrap.popover', [] ) +.directive( 'popoverPopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { popoverTitle: '@', popoverContent: '@', placement: '@', animation: '&', isOpen: '&' }, + templateUrl: 'template/popover/popover.html' + }; +}) +.directive( 'popover', [ '$compile', '$timeout', '$parse', function ( $compile, $timeout, $parse ) { + + var template = + ''+ + ''; + + return { + scope: true, + link: function ( scope, element, attr ) { + var popover = $compile( template )( scope ), + transitionTimeout; + + attr.$observe( 'popover', function ( val ) { + scope.tt_popover = val; + }); + + attr.$observe( 'popoverTitle', function ( val ) { + scope.tt_title = val; + }); + + attr.$observe( 'popoverPlacement', function ( val ) { + // If no placement was provided, default to 'top'. + scope.tt_placement = val || 'top'; + }); + + attr.$observe( 'popoverAnimation', function ( val ) { + scope.tt_animation = $parse( val ); + }); + + // By default, the popover is not open. + scope.tt_isOpen = false; + + // Calculate the current position and size of the directive element. + function getPosition() { + return { + width: element.prop( 'offsetWidth' ), + height: element.prop( 'offsetHeight' ), + top: element.prop( 'offsetTop' ), + left: element.prop( 'offsetLeft' ) + }; + } + + // Show the popover popup element. + function show() { + var position, + ttWidth, + ttHeight, + ttPosition; + + // If there is a pending remove transition, we must cancel it, lest the + // toolip be mysteriously removed. + if ( transitionTimeout ) { + $timeout.cancel( transitionTimeout ); + } + + // Set the initial positioning. + popover.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. + element.after( popover ); + + // Get the position of the directive element. + position = getPosition(); + + // Get the height and width of the popover so we can center it. + ttWidth = popover.prop( 'offsetWidth' ); + ttHeight = popover.prop( 'offsetHeight' ); + + // Calculate the popover'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) + 'px', + left: (position.left + position.width) + 'px' + }; + break; + case 'bottom': + ttPosition = { + top: (position.top + position.height) + 'px', + left: (position.left + position.width / 2 - ttWidth / 2) + 'px' + }; + break; + case 'left': + ttPosition = { + top: (position.top + position.height / 2 - ttHeight / 2) + 'px', + left: (position.left - ttWidth) + 'px' + }; + break; + default: + ttPosition = { + top: (position.top - ttHeight) + 'px', + left: (position.left + position.width / 2 - ttWidth / 2) + 'px' + }; + break; + } + + // Now set the calculated positioning. + popover.css( ttPosition ); + + // And show the popover. + scope.tt_isOpen = true; + } + + // Hide the popover popup element. + function hide() { + // First things first: we don't show it anymore. + //popover.removeClass( 'in' ); + scope.tt_isOpen = false; + + // 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 () { popover.remove(); }, 500 ); + } else { + popover.remove(); + } + } + + // Register the event listeners. + element.bind( 'click', function() { + if(scope.tt_isOpen){ + scope.$apply( hide ); + } else { + scope.$apply( show ); + } + + }); + } + }; +}]); + diff --git a/src/popover/test/popoverSpec.js b/src/popover/test/popoverSpec.js new file mode 100644 index 0000000000..f98d292b21 --- /dev/null +++ b/src/popover/test/popoverSpec.js @@ -0,0 +1,118 @@ +describe('popover', function() { + var elm, + elmBody, + scope, + elmScope; + + // load the popover code + beforeEach(module('ui.bootstrap.popover')); + + // load the template + beforeEach(module('template/popover/popover.html')); + + beforeEach(inject(function($rootScope, $compile) { + elmBody = angular.element( + '
Selector Text
' + ); + + scope = $rootScope; + $compile(elmBody)(scope); + scope.$digest(); + elm = elmBody.find('span'); + elmScope = elm.scope(); + })); + + it('should not be open initially', inject(function() { + expect( elmScope.tt_isOpen ).toBe( false ); + + // We can only test *that* the popover-popup element wasn't created as the + // implementation is templated and replaced. + expect( elmBody.children().length ).toBe( 1 ); + })); + + it('should open on click', inject(function() { + elm.trigger( 'click' ); + expect( elmScope.tt_isOpen ).toBe( true ); + + // We can only test *that* the popover-popup element was created as the + // implementation is templated and replaced. + expect( elmBody.children().length ).toBe( 2 ); + })); + + it('should close on second click', inject(function() { + elm.trigger( 'click' ); + elm.trigger( 'click' ); + expect( elmScope.tt_isOpen ).toBe( false ); + })); + + it('should have default placement of "top"', inject(function() { + elm.trigger( 'click' ); + expect( elmScope.tt_placement ).toBe( "top" ); + })); + + it('should allow specification of placement', inject( function( $compile ) { + elm = $compile( angular.element( + 'Selector Text' + ) )( scope ); + elmScope = elm.scope(); + + elm.trigger( 'click' ); + expect( elmScope.tt_placement ).toBe( "bottom" ); + })); + + it('should work inside an ngRepeat', inject( function( $compile ) { + + elm = $compile( angular.element( + '' + ) )( scope ); + + scope.items = [ + { name: "One", popover: "First popover" } + ]; + + scope.$digest(); + + var tt = angular.element( elm.find("li > span")[0] ); + + tt.trigger( 'click' ); + + expect( tt.text() ).toBe( scope.items[0].name ); + expect( tt.scope().tt_popover ).toBe( scope.items[0].popover ); + + tt.trigger( 'click' ); + })); + + it('should only have an isolate scope on the popup', inject( function ( $compile ) { + var ttScope; + + scope.popoverMsg = "popover Text"; + scope.popoverTitle = "popover Text"; + scope.alt = "Alt Message"; + + elmBody = $compile( angular.element( + '
Selector Text
' + ) )( scope ); + + $compile( elmBody )( scope ); + scope.$digest(); + elm = elmBody.find( 'span' ); + elmScope = elm.scope(); + + elm.trigger( 'click' ); + expect( elm.attr( 'alt' ) ).toBe( scope.alt ); + + ttScope = angular.element( elmBody.children()[1] ).scope(); + expect( ttScope.placement ).toBe( 'top' ); + expect( ttScope.popoverTitle ).toBe( scope.popoverTitle ); + expect( ttScope.popoverContent ).toBe( scope.popoverMsg ); + + elm.trigger( 'click' ); + })); + +}); + + diff --git a/template/popover/popover.html b/template/popover/popover.html new file mode 100644 index 0000000000..1d1104dd2b --- /dev/null +++ b/template/popover/popover.html @@ -0,0 +1,8 @@ +
+
+ +
+

+
+
+