From 7e3179ab9fdbba6c00ff1c30f97b432b58f9eafb Mon Sep 17 00:00:00 2001 From: Chris Chua <chris@thousandeyes.com> Date: Mon, 24 Feb 2014 00:08:20 -0800 Subject: [PATCH] feat(popover): add popover-template directive --- src/popover/docs/demo.html | 21 +++++-- src/popover/docs/demo.js | 7 ++- src/popover/docs/readme.md | 7 +++ src/popover/popover.js | 14 +++++ src/popover/test/popover-template.spec.js | 66 ++++++++++++++++++++ src/tooltip/tooltip.js | 76 ++++++++++++++++++++++- template/popover/popover-template.html | 10 +++ 7 files changed, 193 insertions(+), 8 deletions(-) create mode 100644 src/popover/test/popover-template.spec.js create mode 100644 template/popover/popover-template.html diff --git a/src/popover/docs/demo.html b/src/popover/docs/demo.html index 8955b1918e..d89b5c5ef1 100644 --- a/src/popover/docs/demo.html +++ b/src/popover/docs/demo.html @@ -2,14 +2,27 @@ <h4>Dynamic</h4> <div class="form-group"> <label>Popup Text:</label> - <input type="text" ng-model="dynamicPopover" class="form-control"> + <input type="text" ng-model="dynamicPopover.content" class="form-control"> </div> <div class="form-group"> <label>Popup Title:</label> - <input type="text" ng-model="dynamicPopoverTitle" class="form-control"> + <input type="text" ng-model="dynamicPopover.title" class="form-control"> </div> - <button popover="{{dynamicPopover}}" popover-title="{{dynamicPopoverTitle}}" class="btn btn-default">Dynamic Popover</button> - + <div class="form-group"> + <label>Popup Template:</label> + <input type="text" ng-model="dynamicPopover.templateUrl" class="form-control"> + </div> + <button popover="{{dynamicPopover.content}}" popover-title="{{dynamicPopover.title}}" class="btn btn-default">Dynamic Popover</button> + + <button popover-template="{{dynamicPopover.templateUrl}}" popover-template-title="{{dynamicPopover.title}}" class="btn btn-default">Popover With Template</button> + + <script type="text/ng-template" id="myPopoverTemplate.html"> + <div>{{dynamicPopover.content}}</div> + <div class="form-group"> + <label>Popup Title:</label> + <input type="text" ng-model="dynamicPopover.title" class="form-control"> + </div> + </script> <hr /> <h4>Positional</h4> <button popover-placement="top" popover="On the Top!" class="btn btn-default">Top</button> diff --git a/src/popover/docs/demo.js b/src/popover/docs/demo.js index 6cce89812b..e4e0e2ff29 100644 --- a/src/popover/docs/demo.js +++ b/src/popover/docs/demo.js @@ -1,4 +1,7 @@ angular.module('ui.bootstrap.demo').controller('PopoverDemoCtrl', function ($scope) { - $scope.dynamicPopover = 'Hello, World!'; - $scope.dynamicPopoverTitle = 'Title'; + $scope.dynamicPopover = { + content: 'Hello, World!', + templateUrl: 'myTemplatePopover.html', + title: 'Title' + }; }); diff --git a/src/popover/docs/readme.md b/src/popover/docs/readme.md index cbac5f3d26..dcfd6dff6f 100644 --- a/src/popover/docs/readme.md +++ b/src/popover/docs/readme.md @@ -4,6 +4,13 @@ directive supports multiple placements, optional transition animation, and more. Like the Bootstrap jQuery plugin, the popover **requires** the tooltip module. +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-template` takes text that specifies the location of a template to + use for the popover body. + The popover directives provides several optional attributes to control how it will display: diff --git a/src/popover/popover.js b/src/popover/popover.js index 2bea0a3e10..dee4c94381 100644 --- a/src/popover/popover.js +++ b/src/popover/popover.js @@ -5,6 +5,20 @@ */ angular.module( 'ui.bootstrap.popover', [ 'ui.bootstrap.tooltip' ] ) +.directive( 'popoverTemplatePopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&', + originScope: '&' }, + templateUrl: 'template/popover/popover-template.html' + }; +}) + +.directive( 'popoverTemplate', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'popoverTemplate', 'popoverTemplate', 'click' ); +}]) + .directive( 'popoverPopup', function () { return { restrict: 'EA', diff --git a/src/popover/test/popover-template.spec.js b/src/popover/test/popover-template.spec.js new file mode 100644 index 0000000000..cb504c28d9 --- /dev/null +++ b/src/popover/test/popover-template.spec.js @@ -0,0 +1,66 @@ +describe('popover template', 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')); + beforeEach(module('template/popover/popover-template.html')); + + beforeEach(inject(function ($templateCache) { + $templateCache.put('myUrl', [200, '<span>{{ myTemplateText }}</span>', {}]); + })); + + beforeEach(inject(function($rootScope, $compile) { + elmBody = angular.element( + '<div><span popover-template="{{ templateUrl }}">Selector Text</span></div>' + ); + + scope = $rootScope; + $compile(elmBody)(scope); + scope.templateUrl = 'myUrl'; + + scope.$digest(); + elm = elmBody.find('span'); + elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; + })); + + it('should open on click', inject(function() { + elm.trigger( 'click' ); + expect( tooltipScope.isOpen ).toBe( true ); + + expect( elmBody.children().length ).toBe( 2 ); + })); + + it('should not open on click if templateUrl is empty', inject(function() { + scope.templateUrl = null; + scope.$digest(); + + elm.trigger( 'click' ); + expect( tooltipScope.isOpen ).toBe( false ); + + expect( elmBody.children().length ).toBe( 1 ); + })); + + it('should show updated text', inject(function() { + scope.myTemplateText = 'some text'; + scope.$digest(); + + elm.trigger( 'click' ); + expect( tooltipScope.isOpen ).toBe( true ); + + expect( elmBody.children().eq(1).text().trim() ).toBe( 'some text' ); + + scope.myTemplateText = 'new text'; + scope.$digest(); + + expect( elmBody.children().eq(1).text().trim() ).toBe( 'new text' ); + })); +}); + diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js index 4c5dc825de..646eb58b72 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -103,6 +103,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap 'class="'+startSym+'class'+endSym+'" '+ 'animation="animation" '+ 'is-open="isOpen"'+ + 'origin-scope="origScope" '+ '>'+ '</div>'; @@ -111,7 +112,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap compile: function (tElem, tAttrs) { var tooltipLinker = $compile( template ); - return function link ( scope, element, attrs ) { + return function link ( scope, element, attrs, tooltipCtrl ) { var tooltip; var tooltipLinkedScope; var transitionTimeout; @@ -132,6 +133,9 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap tooltip.css( ttPosition ); }; + // Set up the correct scope to allow transclusion later + ttScope.origScope = scope; + // By default, the tooltip is not open. // TODO add ability to start tooltip opened ttScope.isOpen = false; @@ -197,7 +201,7 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap // And show the tooltip. ttScope.isOpen = true; - ttScope.$digest(); // digest required as $apply is not called + ttScope.$apply(); // digest required as $apply is not called // Return positioning function as promise callback for correct // positioning after draw. @@ -349,6 +353,74 @@ angular.module( 'ui.bootstrap.tooltip', [ 'ui.bootstrap.position', 'ui.bootstrap }]; }) +// This is mostly ngInclude code but with a custom scope +.directive( 'tooltipTemplateTransclude', [ + '$animate', '$sce', '$compile', '$templateRequest', +function ($animate , $sce , $compile , $templateRequest) { + return { + link: function ( scope, elem, attrs ) { + var origScope = scope.$eval(attrs.tooltipTemplateTranscludeScope); + + var changeCounter = 0, + currentScope, + previousElement, + currentElement; + + var cleanupLastIncludeContent = function() { + if (previousElement) { + previousElement.remove(); + previousElement = null; + } + if (currentScope) { + currentScope.$destroy(); + currentScope = null; + } + if (currentElement) { + $animate.leave(currentElement).then(function() { + previousElement = null; + }); + previousElement = currentElement; + currentElement = null; + } + }; + + scope.$watch($sce.parseAsResourceUrl(attrs.tooltipTemplateTransclude), function (src) { + var thisChangeId = ++changeCounter; + + if (src) { + //set the 2nd param to true to ignore the template request error so that the inner + //contents and scope can be cleaned up. + $templateRequest(src, true).then(function(response) { + if (thisChangeId !== changeCounter) { return; } + var newScope = origScope.$new(); + var template = response; + + var clone = $compile(template)(newScope, function(clone) { + cleanupLastIncludeContent(); + $animate.enter(clone, elem); + }); + + currentScope = newScope; + currentElement = clone; + + currentScope.$emit('$includeContentLoaded', src); + }, function() { + if (thisChangeId === changeCounter) { + cleanupLastIncludeContent(); + scope.$emit('$includeContentError', src); + } + }); + scope.$emit('$includeContentRequested', src); + } else { + cleanupLastIncludeContent(); + } + }); + + scope.$on('$destroy', cleanupLastIncludeContent); + } + }; +}]) + .directive( 'tooltipPopup', function () { return { restrict: 'EA', diff --git a/template/popover/popover-template.html b/template/popover/popover-template.html new file mode 100644 index 0000000000..43731c8284 --- /dev/null +++ b/template/popover/popover-template.html @@ -0,0 +1,10 @@ +<div class="popover {{placement}}" ng-class="{ in: isOpen(), fade: animation() }"> + <div class="arrow"></div> + + <div class="popover-inner"> + <h3 class="popover-title" ng-bind="title" ng-show="title"></h3> + <div class="popover-content" + tooltip-template-transclude="content" + tooltip-template-transclude-scope="originScope()"></div> + </div> +</div>