From a9d3d253eba51bb884a0cc0f70cabf53e2e86099 Mon Sep 17 00:00:00 2001 From: Jamie Czerwinski Date: Fri, 31 Jul 2015 15:00:37 -0600 Subject: [PATCH] feat(popover): add custom template support - Adds custom template support for popovers Closes #4056 Closes #4057 --- src/popover/docs/readme.md | 2 + src/popover/popover.js | 17 ++- src/popover/test/popover-html.spec.js | 185 ++++++++++++++++++++++++++ template/popover/popover-html.html | 11 ++ 4 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 src/popover/test/popover-html.spec.js create mode 100644 template/popover/popover-html.html 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( + '
Selector Text
' + ); + + $compile(elmBody)(scope); + scope.$digest(); + elm = elmBody.find('span'); + elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; + + elm.trigger( 'click' ); + expect( tooltipScope.isOpen ).toBe( true ); + expect(elmBody.children().eq(1)).not.toHaveClass('fade'); + })); + + describe('supports options', function () { + + describe('placement', function () { + + it('can specify an alternative, valid placement', inject(function ($compile) { + elmBody = angular.element( + '
Trigger here
' + ); + $compile(elmBody)(scope); + scope.$digest(); + elm = elmBody.find('span'); + elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; + + elm.trigger( 'click' ); + expect( tooltipScope.isOpen ).toBe( true ); + + expect( elmBody.children().length ).toBe( 2 ); + var ttipElement = elmBody.find('div.popover'); + expect(ttipElement).toHaveClass('left'); + })); + + }); + + describe('class', function () { + + it('can specify a custom class', inject(function ($compile) { + elmBody = angular.element( + '
Trigger here
' + ); + $compile(elmBody)(scope); + scope.$digest(); + elm = elmBody.find('span'); + elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; + + elm.trigger( 'click' ); + expect( tooltipScope.isOpen ).toBe( true ); + + expect( elmBody.children().length ).toBe( 2 ); + var ttipElement = elmBody.find('div.popover'); + expect(ttipElement).toHaveClass('custom'); + })); + + }); + + }); + +}); + + diff --git a/template/popover/popover-html.html b/template/popover/popover-html.html new file mode 100644 index 0000000000..0655274f35 --- /dev/null +++ b/template/popover/popover-html.html @@ -0,0 +1,11 @@ +
+
+ +
+

+
+
+