From 7e3179ab9fdbba6c00ff1c30f97b432b58f9eafb Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Mon, 24 Feb 2014 00:08:20 -0800 Subject: [PATCH 1/2] 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 @@

Dynamic

- +
- +
- - +
+ + +
+ + + + +

Positional

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, '{{ myTemplateText }}', {}]); + })); + + beforeEach(inject(function($rootScope, $compile) { + elmBody = angular.element( + '
Selector Text
' + ); + + 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" '+ '>'+ ''; @@ -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 @@ +
+
+ +
+

+
+
+
From a1695114a245312878d315dfc9e369f98d573eae Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Tue, 24 Mar 2015 23:31:44 -0700 Subject: [PATCH 2/2] feat(tooltip): add tooltip-template directive Fixes #220 --- src/tooltip/docs/demo.html | 6 ++ src/tooltip/docs/readme.md | 15 +++-- src/tooltip/test/tooltip-template.spec.js | 65 ++++++++++++++++++++ src/tooltip/tooltip.js | 17 +++++ template/tooltip/tooltip-template-popup.html | 6 ++ 5 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 src/tooltip/test/tooltip-template.spec.js create mode 100644 template/tooltip/tooltip-template-popup.html diff --git a/src/tooltip/docs/demo.html b/src/tooltip/docs/demo.html index 09aa0ba496..914dc91ccd 100644 --- a/src/tooltip/docs/demo.html +++ b/src/tooltip/docs/demo.html @@ -20,6 +20,8 @@ fading at elementum eu, facilisis sed odio morbi quis commodo odio. In cursus delayed turpis massa tincidunt dui ut. + Custom template + nunc sed velit dignissim sodales ut eu sem integer vitae. Turpis egestas

@@ -58,4 +60,8 @@ tooltip-enable="!inputModel" /> + + diff --git a/src/tooltip/docs/readme.md b/src/tooltip/docs/readme.md index 960a7d154f..fcea6c11b0 100644 --- a/src/tooltip/docs/readme.md +++ b/src/tooltip/docs/readme.md @@ -1,11 +1,16 @@ A lightweight, extensible directive for fancy tooltip creation. The tooltip directive supports multiple placements, optional transition animation, and more. -There are two versions of the tooltip: `tooltip` and `tooltip-html-unsafe`. The -former takes text only and will escape any HTML provided. The latter takes -whatever HTML is provided and displays it in a tooltip; it's called "unsafe" -because the HTML is not sanitized. *The user is responsible for ensuring the -content is safe to put into the DOM!* +There are three versions of the tooltip: `tooltip`, `tooltip-template`, and +`tooltip-html-unsafe`: + +- `tooltip` takes text only and will escape any HTML provided. +- `tooltip-template` takes text that specifies the location of a template to + use for the tooltip. +- `tooltip-html-unsafe` takes + whatever HTML is provided and displays it in a tooltip; it's called "unsafe" + because the HTML is not sanitized. *The user is responsible for ensuring the + content is safe to put into the DOM!* The tooltip directives provide several optional attributes to control how they will display: diff --git a/src/tooltip/test/tooltip-template.spec.js b/src/tooltip/test/tooltip-template.spec.js new file mode 100644 index 0000000000..dab8d34863 --- /dev/null +++ b/src/tooltip/test/tooltip-template.spec.js @@ -0,0 +1,65 @@ +describe('tooltip template', function() { + var elm, + elmBody, + scope, + elmScope, + tooltipScope; + + // load the popover code + beforeEach(module('ui.bootstrap.tooltip')); + + // load the template + beforeEach(module('template/tooltip/tooltip-template-popup.html')); + + beforeEach(inject(function ($templateCache) { + $templateCache.put('myUrl', [200, '{{ myTemplateText }}', {}]); + })); + + beforeEach(inject(function($rootScope, $compile) { + elmBody = angular.element( + '

Selector Text
' + ); + + scope = $rootScope; + $compile(elmBody)(scope); + scope.templateUrl = 'myUrl'; + + scope.$digest(); + elm = elmBody.find('span'); + elmScope = elm.scope(); + tooltipScope = elmScope.$$childTail; + })); + + it('should open on mouseenter', inject(function() { + elm.trigger( 'mouseenter' ); + expect( tooltipScope.isOpen ).toBe( true ); + + expect( elmBody.children().length ).toBe( 2 ); + })); + + it('should not open on mouseenter if templateUrl is empty', inject(function() { + scope.templateUrl = null; + scope.$digest(); + + elm.trigger( 'mouseenter' ); + 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( 'mouseenter' ); + 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 646eb58b72..d7afc7d1fd 100644 --- a/src/tooltip/tooltip.js +++ b/src/tooltip/tooltip.js @@ -434,6 +434,23 @@ function ($animate , $sce , $compile , $templateRequest) { return $tooltip( 'tooltip', 'tooltip', 'mouseenter' ); }]) +.directive( 'tooltipTemplatePopup', function () { + return { + restrict: 'EA', + replace: true, + scope: { title: '@', content: '@', placement: '@', animation: '&', isOpen: '&', + originScope: '&' }, + templateUrl: 'template/tooltip/tooltip-template-popup.html' + }; +}) + +.directive( 'tooltipTemplate', [ '$tooltip', function ( $tooltip ) { + return $tooltip( 'tooltipTemplate', 'tooltipTemplate', 'mouseenter' ); +}]) + +/* +Deprecated +*/ .directive( 'tooltipHtmlUnsafePopup', function () { return { restrict: 'EA', diff --git a/template/tooltip/tooltip-template-popup.html b/template/tooltip/tooltip-template-popup.html new file mode 100644 index 0000000000..c934fc338d --- /dev/null +++ b/template/tooltip/tooltip-template-popup.html @@ -0,0 +1,6 @@ +
+
+
+