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

Add popover directive #113

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/popover/docs/demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<div ng-controller="PopoverDemoCtrl">
<div class="well">
<div>
<h4>Dynamic</h4>
<div>Dynamic Popover : <input type="text" ng-model="dynamicPopoverText"></div>
<div>Dynamic Popover Popup Text: <input type="text" ng-model="dynamicPopover"></div>
<div>Dynamic Popover Popup Title: <input type="text" ng-model="dynamicPopoverTitle"></div>
<div><button popover="{{dynamicPopover}}" popover-title="{{dynamicPopoverTitle}}" class="btn">{{dynamicPopoverText}}</button></div>
</div>
<div>
<h4>Positional</h4>
<button popover-placement="top" popover="On the Top!" class="btn">Top</button>
<button popover-placement="left" popover="On the Left!" class="btn">Left</button>
<button popover-placement="right" popover="On the Right!" class="btn">Right</button>
<button popover-placement="bottom" popover="On the Bottom!" class="btn">Bottom</button>
</div>
<div>
<h4>Other</h4>
<button Popover-animation="true" popover="I fade in and out!" class="btn">fading</button>
<button popover="I have a title!" popover-title="The title." class="btn">title</button>
</div>
</div>
</div>
5 changes: 5 additions & 0 deletions src/popover/docs/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
var PopoverDemoCtrl = function ($scope) {
$scope.dynamicPopover = "Hello, World!";
$scope.dynamicPopoverText = "dynamic";
$scope.dynamicPopoverTitle = "Title";
};
2 changes: 2 additions & 0 deletions src/popover/docs/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
A lightweight, extensible directive for fancy popover creation. The popover
directive supports multiple placements, optional transition animation, and more.
154 changes: 154 additions & 0 deletions src/popover/popover.js
Original file line number Diff line number Diff line change
@@ -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 =
'<popover-popup '+
'popover-title="{{tt_title}}" '+
'popover-content="{{tt_popover}}" '+
'placement="{{tt_placement}}" '+
'animation="tt_animation()" '+
'is-open="tt_isOpen"'+
'>'+
'</popover-popup>';

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 );
}

});
}
};
}]);

118 changes: 118 additions & 0 deletions src/popover/test/popoverSpec.js
Original file line number Diff line number Diff line change
@@ -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(
'<div><span popover="popover text">Selector Text</span></div>'
);

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(
'<span popover="popover text" popover-placement="bottom">Selector Text</span>'
) )( 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(
'<ul>'+
'<li ng-repeat="item in items">'+
'<span popover="{{item.popover}}">{{item.name}}</span>'+
'</li>'+
'</ul>'
) )( 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(
'<div><span alt={{alt}} popover="{{popoverMsg}}" popover-title="{{popoverTitle}}">Selector Text</span></div>'
) )( 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' );
}));

});


8 changes: 8 additions & 0 deletions template/popover/popover.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div class="popover {{placement}}" ng-class="{ in: isOpen(), fade: animation() }">
<div class="arrow"></div>

<div class="popover-inner">
<h3 class="popover-title" ng-bind="popoverTitle" ng-show="popoverTitle"></h3>
<div class="popover-content" ng-bind="popoverContent"></div>
</div>
</div>