diff --git a/src/popover/docs/readme.md b/src/popover/docs/readme.md
index dcfd6dff6f..c18679f73d 100644
--- a/src/popover/docs/readme.md
+++ b/src/popover/docs/readme.md
@@ -8,6 +8,8 @@ There are two versions of the popover: `popover` and `popover-template`:
- `popover` takes text only and will escape any HTML provided for the popover
body.
+- `popover-html` takes an expression that evaluates to an html string. *The user is responsible for ensuring the
+ content is safe to put into the DOM!*
- `popover-template` takes text that specifies the location of a template to
use for the popover body.
diff --git a/src/popover/popover.js b/src/popover/popover.js
index dadf2dabc9..0259eb76ee 100644
--- a/src/popover/popover.js
+++ b/src/popover/popover.js
@@ -1,7 +1,7 @@
/**
* 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.
+ * just mouse enter/leave, and selector delegatation.
*/
angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] )
@@ -21,6 +21,21 @@ angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] )
} );
}])
+.directive( 'popoverHtmlPopup', function () {
+ return {
+ restrict: 'EA',
+ replace: true,
+ scope: { contentExp: '&', title: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
+ templateUrl: 'template/popover/popover-html.html'
+ };
+})
+
+.directive( 'popoverHtml', [ '$tooltip', function ( $tooltip ) {
+ return $tooltip( 'popoverHtml', 'popover', 'click', {
+ useContentExp: true
+ });
+}])
+
.directive( 'popoverPopup', function () {
return {
restrict: 'EA',
diff --git a/src/popover/test/popover-html.spec.js b/src/popover/test/popover-html.spec.js
new file mode 100644
index 0000000000..4e5a44226d
--- /dev/null
+++ b/src/popover/test/popover-html.spec.js
@@ -0,0 +1,185 @@
+describe('popover', function() {
+ var elm,
+ elmBody,
+ scope,
+ elmScope,
+ tooltipScope;
+
+ // load the popover code
+ beforeEach(module('ui.bootstrap.popover'));
+
+ // load the template
+ beforeEach(module('template/popover/popover-html.html'));
+
+ beforeEach(inject(function($rootScope, $compile, $sce) {
+ elmBody = angular.element(
+ '
Selector Text
'
+ );
+
+ scope = $rootScope;
+ scope.template = $sce.trustAsHtml('My template');
+ $compile(elmBody)(scope);
+ scope.$digest();
+ elm = elmBody.find('span');
+ elmScope = elm.scope();
+ tooltipScope = elmScope.$$childTail;
+ }));
+
+ it('should not be open initially', inject(function() {
+ expect( tooltipScope.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( tooltipScope.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( tooltipScope.isOpen ).toBe( false );
+ }));
+
+ it('should not open on click if template is empty', inject(function() {
+ scope.template = null;
+ scope.$digest();
+
+ elm.trigger( 'click' );
+ expect( tooltipScope.isOpen ).toBe( false );
+
+ expect( elmBody.children().length ).toBe( 1 );
+ }));
+
+ it('should show updated text', inject(function($sce) {
+ scope.template = $sce.trustAsHtml('My template');
+ scope.$digest();
+
+ elm.trigger( 'click' );
+ expect( tooltipScope.isOpen ).toBe( true );
+
+ expect( elmBody.children().eq(1).text().trim() ).toBe( 'My template' );
+
+ scope.template = $sce.trustAsHtml('Another template');
+ scope.$digest();
+
+ expect( elmBody.children().eq(1).text().trim() ).toBe( 'Another template' );
+ }));
+
+ it('should hide popover when template becomes empty', inject(function ($timeout) {
+ elm.trigger( 'click' );
+ expect( tooltipScope.isOpen ).toBe( true );
+
+ scope.template = '';
+ scope.$digest();
+
+ expect( tooltipScope.isOpen ).toBe( false );
+
+ $timeout.flush();
+ expect( elmBody.children().length ).toBe( 1 );
+ }));
+
+
+ it('should not unbind event handlers created by other directives - issue 456', inject( function( $compile ) {
+
+ scope.click = function() {
+ scope.clicked = !scope.clicked;
+ };
+
+ elmBody = angular.element(
+ ''
+ );
+ $compile(elmBody)(scope);
+ scope.$digest();
+
+ elm = elmBody.find('input');
+
+ elm.trigger( 'mouseenter' );
+ elm.trigger( 'mouseleave' );
+ expect(scope.clicked).toBeFalsy();
+
+ elm.click();
+ expect(scope.clicked).toBeTruthy();
+ }));
+
+ it('should popup with animate class by default', inject(function() {
+ elm.trigger( 'click' );
+ expect( tooltipScope.isOpen ).toBe( true );
+
+ expect(elmBody.children().eq(1)).toHaveClass('fade');
+ }));
+
+ it('should popup without animate class when animation disabled', inject(function($compile) {
+ elmBody = angular.element(
+ '