Disable tooltips conditionally:
diff --git a/src/tooltip/docs/readme.md b/src/tooltip/docs/readme.md
index b0ae385518..16d4cdb6c9 100644
--- a/src/tooltip/docs/readme.md
+++ b/src/tooltip/docs/readme.md
@@ -1,16 +1,16 @@
A lightweight, extensible directive for fancy tooltip creation. The tooltip
directive supports multiple placements, optional transition animation, and more.
-There are three versions of the tooltip: `tooltip`, `tooltip-template`, and
-`tooltip-html-unsafe`:
+There are three versions of the tooltip: `uib-tooltip`, `uib-tooltip-template`, and
+`uib-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
+- `uib-tooltip` takes text only and will escape any HTML provided.
+- `uib-tooltip-template` takes text that specifies the location of a template to
use for the tooltip. Note that this needs to be wrapped in a tag.
-- `tooltip-html` takes
+- `uib-tooltip-html` takes
whatever HTML is provided and displays it in a tooltip; *The user is responsible for ensuring the
content is safe to put into the DOM!*
-- `tooltip-html-unsafe` -- deprecated in favour of `tooltip-html`
+- `uib-tooltip-html-unsafe` -- deprecated in favour of `tooltip-html`
The tooltip directives provide several optional attributes to control how they
will display:
@@ -20,6 +20,8 @@ will display:
- `tooltip-animation`: Should it fade in and out? Defaults to "true".
- `tooltip-popup-delay`: For how long should the user have to have the mouse
over the element before the tooltip shows (in milliseconds)? Defaults to 0.
+- `tooltip-close-popup-delay`: For how long should the tooltip remain open
+ after the close trigger event? Defaults to 0.
- `tooltip-trigger`: What should trigger a show of the tooltip? Supports a space separated list of event names.
Note: this attribute is no longer observable. See `tooltip-enable`.
- `tooltip-enable`: Is it enabled? It will enable or disable the configured
@@ -47,15 +49,15 @@ For any non-supported value, the trigger will be used to both show and hide the
tooltip. Using the 'none' trigger will disable the internal trigger(s), one can
then use the `tooltip-is-open` attribute exclusively to show and hide the tooltip.
-**$tooltipProvider**
+**$uibTooltipProvider**
-Through the `$tooltipProvider`, you can change the way tooltips and popovers
+Through the `$uibTooltipProvider`, you can change the way tooltips and popovers
behave by default; the attributes above always take precedence. The following
methods are available:
-- `setTriggers( obj )`: Extends the default trigger mappings mentioned above
+- `setTriggers(obj)`: Extends the default trigger mappings mentioned above
with mappings of your own. E.g. `{ 'openTrigger': 'closeTrigger' }`.
-- `options( obj )`: Provide a set of defaults for certain tooltip and popover
+- `options(obj)`: Provide a set of defaults for certain tooltip and popover
attributes. Currently supports 'placement', 'animation', 'popupDelay', and
`appendToBody`. Here are the defaults:
@@ -63,6 +65,16 @@ methods are available:
placement: 'top',
animation: true,
popupDelay: 0,
+ popupCloseDelay: 500,
appendToBody: false
+**Known issues**
+
+For Safari 7+ support, if you want to use the **focus** `tooltip-trigger`, you need to use an anchor tag with a tab index. For example:
+
+```
+
+ Click Me
+
+```
diff --git a/src/tooltip/test/tooltip-template.spec.js b/src/tooltip/test/tooltip-template.spec.js
index 6071baa17c..ad1c97318f 100644
--- a/src/tooltip/test/tooltip-template.spec.js
+++ b/src/tooltip/test/tooltip-template.spec.js
@@ -17,7 +17,7 @@ describe('tooltip template', function() {
beforeEach(inject(function($rootScope, $compile) {
elmBody = angular.element(
- '
Selector Text
'
+ '
Selector Text
'
);
scope = $rootScope;
@@ -30,18 +30,25 @@ describe('tooltip template', function() {
tooltipScope = elmScope.$$childTail;
}));
+ function trigger(element, evt) {
+ evt = new Event(evt);
+
+ element[0].dispatchEvent(evt);
+ element.scope().$$childTail.$digest();
+ }
+
it('should open on mouseenter', inject(function() {
- elm.trigger('mouseenter');
- expect( tooltipScope.isOpen ).toBe( true );
+ trigger(elm, 'mouseenter');
+ expect(tooltipScope.isOpen).toBe(true);
- expect( elmBody.children().length ).toBe( 2 );
+ 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');
+ trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(false);
expect(elmBody.children().length).toBe(1);
@@ -49,10 +56,10 @@ describe('tooltip template', function() {
it('should show updated text', inject(function() {
scope.myTemplateText = 'some text';
- scope.$digest();
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
+ scope.$digest();
expect(elmBody.children().eq(1).text().trim()).toBe('some text');
@@ -63,7 +70,7 @@ describe('tooltip template', function() {
}));
it('should hide tooltip when template becomes empty', inject(function($timeout) {
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
scope.templateUrl = '';
@@ -76,3 +83,64 @@ describe('tooltip template', function() {
}));
});
+/* Deprecation tests below */
+
+describe('tooltip template deprecation', function() {
+ beforeEach(module('ui.bootstrap.tooltip'));
+ beforeEach(module('template/tooltip/tooltip-template-popup.html'));
+
+ var elm, elmBody, elmScope, tooltipScope;
+
+ function trigger(element, evt) {
+ evt = new Event(evt);
+
+ element[0].dispatchEvent(evt);
+ element.scope().$$childTail.$digest();
+ }
+
+ it('should suppress warning', function() {
+ module(function($provide) {
+ $provide.value('$tooltipSuppressWarning', true);
+ });
+
+ inject(function($compile, $log, $rootScope, $templateCache) {
+ spyOn($log, 'warn');
+ $templateCache.put('myUrl', [200, '
{{ myTemplateText }} ', {}]);
+ $rootScope.templateUrl = 'myUrl';
+
+ elmBody = angular.element('
Selector Text
');
+ $compile(elmBody)($rootScope);
+ $rootScope.$digest();
+ elm = elmBody.find('span');
+ elmScope = elm.scope();
+ tooltipScope = elmScope.$$childTail;
+
+ trigger(elm, 'mouseenter');
+
+ expect($log.warn.calls.count()).toBe(0);
+ });
+ });
+
+ it('should give warning by default', inject(function($compile, $log, $rootScope, $templateCache) {
+ spyOn($log, 'warn');
+ $templateCache.put('myUrl', [200, '
{{ myTemplateText }} ', {}]);
+ $rootScope.templateUrl = 'myUrl';
+
+ var element = '
Selector Text
';
+ element = $compile(element)($rootScope);
+ $rootScope.$digest();
+
+ elmBody = angular.element('
Selector Text
');
+ $compile(elmBody)($rootScope);
+ $rootScope.$digest();
+ elm = elmBody.find('span');
+ elmScope = elm.scope();
+ tooltipScope = elmScope.$$childTail;
+
+ trigger(elm, 'mouseenter');
+
+ expect($log.warn.calls.count()).toBe(2);
+ expect($log.warn.calls.argsFor(0)).toEqual(['$tooltip is now deprecated. Use $uibTooltip instead.']);
+ expect($log.warn.calls.argsFor(1)).toEqual(['tooltip-template-popup is now deprecated. Use uib-tooltip-template-popup instead.']);
+ }));
+});
diff --git a/src/tooltip/test/tooltip.spec.js b/src/tooltip/test/tooltip.spec.js
index 2d7e8df96e..30e495ee80 100644
--- a/src/tooltip/test/tooltip.spec.js
+++ b/src/tooltip/test/tooltip.spec.js
@@ -3,7 +3,8 @@ describe('tooltip', function() {
elmBody,
scope,
elmScope,
- tooltipScope;
+ tooltipScope,
+ $document;
// load the tooltip code
beforeEach(module('ui.bootstrap.tooltip'));
@@ -11,11 +12,12 @@ describe('tooltip', function() {
// load the template
beforeEach(module('template/tooltip/tooltip-popup.html'));
- beforeEach(inject(function($rootScope, $compile) {
+ beforeEach(inject(function($rootScope, $compile, _$document_) {
elmBody = angular.element(
- '
Selector Text
'
+ '
Selector Text
'
);
+ $document = _$document_;
scope = $rootScope;
$compile(elmBody)(scope);
scope.$digest();
@@ -24,6 +26,13 @@ describe('tooltip', function() {
tooltipScope = elmScope.$$childTail;
}));
+ function trigger(element, evt) {
+ evt = new Event(evt);
+
+ element[0].dispatchEvent(evt);
+ element.scope().$$childTail.$digest();
+ }
+
it('should not be open initially', inject(function() {
expect(tooltipScope.isOpen).toBe(false);
@@ -33,7 +42,7 @@ describe('tooltip', function() {
}));
it('should open on mouseenter', inject(function() {
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
// We can only test *that* the tooltip-popup element was created as the
@@ -42,8 +51,8 @@ describe('tooltip', function() {
}));
it('should close on mouseleave', inject(function() {
- elm.trigger('mouseenter');
- elm.trigger('mouseleave');
+ trigger(elm, 'mouseenter');
+ trigger(elm, 'mouseleave');
expect(tooltipScope.isOpen).toBe(false);
}));
@@ -52,32 +61,32 @@ describe('tooltip', function() {
}));
it('should have default placement of "top"', inject(function() {
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
expect(tooltipScope.placement).toBe('top');
}));
it('should allow specification of placement', inject(function($compile) {
elm = $compile(angular.element(
- '
Selector Text '
+ '
Selector Text '
))(scope);
scope.$apply();
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
expect(tooltipScope.placement).toBe('bottom');
}));
it('should update placement dynamically', inject(function($compile, $timeout) {
scope.place = 'bottom';
- elm = $compile( angular.element(
- '
Selector Text '
+ elm = $compile(angular.element(
+ '
Selector Text '
))(scope);
scope.$apply();
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
expect(tooltipScope.placement).toBe('bottom');
scope.place = 'right';
@@ -90,7 +99,7 @@ describe('tooltip', function() {
elm = $compile(angular.element(
'
'+
''+
- '{{item.name}} '+
+ '{{item.name}} '+
' '+
' '
))(scope);
@@ -102,22 +111,22 @@ describe('tooltip', function() {
scope.$digest();
var tt = angular.element(elm.find('li > span')[0]);
-
- tt.trigger('mouseenter');
+ trigger(tt, 'mouseenter');
expect(tt.text()).toBe(scope.items[0].name);
tooltipScope = tt.scope().$$childTail;
expect(tooltipScope.content).toBe(scope.items[0].tooltip);
- tt.trigger('mouseleave');
+ trigger(tt, 'mouseleave');
+ expect(tooltipScope.isOpen).toBeFalsy();
}));
it('should show correct text when in an ngRepeat', inject(function($compile, $timeout) {
elm = $compile(angular.element(
'
'+
''+
- '{{item.name}} '+
+ '{{item.name}} '+
' '+
' '
))(scope);
@@ -132,12 +141,12 @@ describe('tooltip', function() {
var tt_1 = angular.element(elm.find('li > span')[0]);
var tt_2 = angular.element(elm.find('li > span')[1]);
- tt_1.trigger('mouseenter');
- tt_1.trigger('mouseleave');
+ trigger(tt_1, 'mouseenter');
+ trigger(tt_1, 'mouseleave');
$timeout.flush();
- tt_2.trigger('mouseenter');
+ trigger(tt_2, 'mouseenter');
expect(tt_1.text()).toBe(scope.items[0].name);
expect(tt_2.text()).toBe(scope.items[1].name);
@@ -146,7 +155,7 @@ describe('tooltip', function() {
expect(tooltipScope.content).toBe(scope.items[1].tooltip);
expect(elm.find('.tooltip-inner').text()).toBe(scope.items[1].tooltip);
- tt_2.trigger('mouseleave');
+ trigger(tt_2, 'mouseleave');
}));
it('should only have an isolate scope on the popup', inject(function($compile) {
@@ -155,8 +164,8 @@ describe('tooltip', function() {
scope.tooltipMsg = 'Tooltip Text';
scope.alt = 'Alt Message';
- elmBody = $compile( angular.element(
- '
Selector Text
'
+ elmBody = $compile(angular.element(
+ '
Selector Text
'
))(scope);
$compile(elmBody)(scope);
@@ -164,17 +173,17 @@ describe('tooltip', function() {
elm = elmBody.find('span');
elmScope = elm.scope();
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
expect(elm.attr('alt')).toBe(scope.alt);
ttScope = angular.element(elmBody.children()[1]).isolateScope();
expect(ttScope.placement).toBe('top');
expect(ttScope.content).toBe(scope.tooltipMsg);
- elm.trigger('mouseleave');
+ trigger(elm, 'mouseleave');
//Isolate scope contents should be the same after hiding and showing again (issue 1191)
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
ttScope = angular.element(elmBody.children()[1]).isolateScope();
expect(ttScope.placement).toBe('top');
@@ -183,7 +192,7 @@ describe('tooltip', function() {
it('should not show tooltips if there is nothing to show - issue #129', inject(function($compile) {
elmBody = $compile(angular.element(
- '
Selector Text
'
+ '
Selector Text
'
))(scope);
scope.$digest();
elmBody.find('span').trigger('mouseenter');
@@ -191,8 +200,8 @@ describe('tooltip', function() {
expect(elmBody.children().length).toBe(1);
}));
- it( 'should close the tooltip when its trigger element is destroyed', inject(function() {
- elm.trigger('mouseenter');
+ it('should close the tooltip when its trigger element is destroyed', inject(function() {
+ trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
elm.remove();
@@ -202,27 +211,27 @@ describe('tooltip', function() {
it('issue 1191 - scope on the popup should always be child of correct element scope', function() {
var ttScope;
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
- ttScope = angular.element( elmBody.children()[1] ).scope();
- expect(ttScope.$parent).toBe( tooltipScope );
+ ttScope = angular.element(elmBody.children()[1]).scope();
+ expect(ttScope.$parent).toBe(tooltipScope);
- elm.trigger('mouseleave');
+ trigger(elm, 'mouseleave');
// After leaving and coming back, the scope's parent should be the same
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
ttScope = angular.element(elmBody.children()[1]).scope();
expect(ttScope.$parent).toBe(tooltipScope);
- elm.trigger('mouseleave');
+ trigger(elm, 'mouseleave');
});
describe('with specified enable expression', function() {
beforeEach(inject(function($compile) {
scope.enable = false;
elmBody = $compile(angular.element(
- '
Selector Text
'
+ '
Selector Text
'
))(scope);
scope.$digest();
elm = elmBody.find('span');
@@ -231,7 +240,7 @@ describe('tooltip', function() {
}));
it('should not open ', inject(function() {
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBeFalsy();
expect(elmBody.children().length).toBe(1);
}));
@@ -239,49 +248,51 @@ describe('tooltip', function() {
it('should open', inject(function() {
scope.enable = true;
scope.$digest();
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBeTruthy();
expect(elmBody.children().length).toBe(2);
}));
});
describe('with specified popup delay', function() {
- beforeEach(inject(function($compile) {
- scope.delay='1000';
+ var $timeout;
+ beforeEach(inject(function($compile, _$timeout_) {
+ $timeout = _$timeout_;
+ scope.delay = '1000';
elm = $compile(angular.element(
- '
Selector Text '
+ '
Selector Text '
))(scope);
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
scope.$digest();
}));
- it('should open after timeout', inject(function($timeout) {
- elm.trigger('mouseenter');
+ it('should open after timeout', function() {
+ trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(false);
$timeout.flush();
expect(tooltipScope.isOpen).toBe(true);
- }));
+ });
- it('should not open if mouseleave before timeout', inject(function($timeout) {
- elm.trigger('mouseenter');
+ it('should not open if mouseleave before timeout', function() {
+ trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(false);
- elm.trigger('mouseleave');
+ trigger(elm, 'mouseleave');
$timeout.flush();
expect(tooltipScope.isOpen).toBe(false);
- }));
+ });
it('should use default popup delay if specified delay is not a number', function() {
- scope.delay='text1000';
+ scope.delay = 'text1000';
scope.$digest();
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
});
- it('should not open if disabled is present', inject(function($timeout) {
- elm.trigger('mouseenter');
+ it('should not open if disabled is present', function() {
+ trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(false);
$timeout.flush(500);
@@ -289,23 +300,137 @@ describe('tooltip', function() {
elmScope.disabled = true;
elmScope.$digest();
+ expect(tooltipScope.isOpen).toBe(false);
+ });
+
+ it('should open when not disabled after being disabled - issue #4204', function() {
+ trigger(elm, 'mouseenter');
+ expect(tooltipScope.isOpen).toBe(false);
+
+ $timeout.flush(500);
+ elmScope.disabled = true;
+ elmScope.$digest();
+
+ $timeout.flush(500);
+ expect(tooltipScope.isOpen).toBe(false);
+
+ elmScope.disabled = false;
+ elmScope.$digest();
+
+ trigger(elm, 'mouseenter');
$timeout.flush();
+
+ expect(tooltipScope.isOpen).toBe(true);
+ });
+
+ it('should close the tooltips in order', inject(function($compile) {
+ var elm2 = $compile('
Selector Text
')(scope);
+ scope.$digest();
+ elm2 = elm2.find('span');
+ var tooltipScope2 = elm2.scope().$$childTail;
+ tooltipScope2.isOpen = false;
+ scope.$digest();
+
+ trigger(elm, 'mouseenter');
+ tooltipScope2.$digest();
+ $timeout.flush();
+ expect(tooltipScope.isOpen).toBe(true);
+ expect(tooltipScope2.isOpen).toBe(false);
+
+ trigger(elm2, 'mouseenter');
+ tooltipScope2.$digest();
+ $timeout.flush();
+ expect(tooltipScope.isOpen).toBe(true);
+ expect(tooltipScope2.isOpen).toBe(true);
+
+ var evt = $.Event('keypress');
+ evt.which = 27;
+
+ $document.trigger(evt);
+ tooltipScope.$digest();
+ tooltipScope2.$digest();
+
+ expect(tooltipScope.isOpen).toBe(true);
+ expect(tooltipScope2.isOpen).toBe(false);
+
+ var evt2 = $.Event('keypress');
+ evt2.which = 27;
+
+ $document.trigger(evt2);
+ tooltipScope.$digest();
+ tooltipScope2.$digest();
+
expect(tooltipScope.isOpen).toBe(false);
+ expect(tooltipScope2.isOpen).toBe(false);
}));
});
-
- describe( 'with an is-open attribute', function() {
+
+ describe('with specified popup close delay', function() {
+ var $timeout;
+ beforeEach(inject(function($compile, _$timeout_) {
+ $timeout = _$timeout_;
+ scope.delay = '1000';
+ elm = $compile(angular.element(
+ '
Selector Text '
+ ))(scope);
+ elmScope = elm.scope();
+ tooltipScope = elmScope.$$childTail;
+ scope.$digest();
+ }));
+
+ it('should close after timeout', function() {
+ trigger(elm, 'mouseenter');
+ expect(tooltipScope.isOpen).toBe(true);
+ trigger(elm, 'mouseleave');
+ $timeout.flush();
+ expect(tooltipScope.isOpen).toBe(false);
+ });
+
+ it('should use default popup close delay if specified delay is not a number and close immediately', function() {
+ scope.delay = 'text1000';
+ scope.$digest();
+ trigger(elm, 'mouseenter');
+ expect(tooltipScope.popupCloseDelay).toBe(0);
+ expect(tooltipScope.isOpen).toBe(true);
+ trigger(elm, 'mouseleave');
+ $timeout.flush();
+ expect(tooltipScope.isOpen).toBe(false);
+ });
+
+ it('should open when not disabled after being disabled and close after delay - issue #4204', function() {
+ trigger(elm, 'mouseenter');
+ expect(tooltipScope.isOpen).toBe(true);
+
+ elmScope.disabled = true;
+ elmScope.$digest();
+
+ $timeout.flush(500);
+ expect(tooltipScope.isOpen).toBe(false);
+
+ elmScope.disabled = false;
+ elmScope.$digest();
+
+ trigger(elm, 'mouseenter');
+
+ expect(tooltipScope.isOpen).toBe(true);
+ trigger(elm, 'mouseleave');
+ $timeout.flush();
+ expect(tooltipScope.isOpen).toBe(false);
+ });
+ });
+
+ describe('with an is-open attribute', function() {
beforeEach(inject(function ($compile) {
scope.isOpen = false;
elm = $compile(angular.element(
- '
Selector Text '
+ '
Selector Text '
))(scope);
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
scope.$digest();
}));
-
- it( 'should show and hide with the controller value', function() {
+
+ it('should show and hide with the controller value', function() {
expect(tooltipScope.isOpen).toBe(false);
elmScope.isOpen = true;
elmScope.$digest();
@@ -314,25 +439,47 @@ describe('tooltip', function() {
elmScope.$digest();
expect(tooltipScope.isOpen).toBe(false);
});
-
- it( 'should update the controller value', function() {
- elm.trigger('mouseenter');
+
+ it('should update the controller value', function() {
+ trigger(elm, 'mouseenter');
expect(elmScope.isOpen).toBe(true);
- elm.trigger('mouseleave');
+ trigger(elm, 'mouseleave');
expect(elmScope.isOpen).toBe(false);
});
});
- describe( 'with a trigger attribute', function() {
+ describe('with an is-open attribute expression', function() {
+ beforeEach(inject(function($compile) {
+ scope.isOpen = false;
+ elm = $compile(angular.element(
+ '
Selector Text '
+ ))(scope);
+ elmScope = elm.scope();
+ tooltipScope = elmScope.$$childTail;
+ scope.$digest();
+ }));
+
+ it('should show and hide with the expression', function() {
+ expect(tooltipScope.isOpen).toBe(false);
+ elmScope.isOpen = true;
+ elmScope.$digest();
+ expect(tooltipScope.isOpen).toBe(true);
+ elmScope.isOpen = false;
+ elmScope.$digest();
+ expect(tooltipScope.isOpen).toBe(false);
+ });
+ });
+
+ describe('with a trigger attribute', function() {
var scope, elmBody, elm, elmScope;
- beforeEach( inject( function( $rootScope ) {
+ beforeEach(inject(function($rootScope) {
scope = $rootScope;
}));
- it( 'should use it to show but set the hide trigger based on the map for mapped triggers', inject(function($compile) {
+ it('should use it to show but set the hide trigger based on the map for mapped triggers', inject(function($compile) {
elmBody = angular.element(
- '
'
+ '
'
);
$compile(elmBody)(scope);
scope.$apply();
@@ -341,15 +488,15 @@ describe('tooltip', function() {
tooltipScope = elmScope.$$childTail;
expect(tooltipScope.isOpen).toBeFalsy();
- elm.trigger('focus');
+ trigger(elm, 'focus');
expect(tooltipScope.isOpen).toBeTruthy();
- elm.trigger('blur');
+ trigger(elm, 'blur');
expect(tooltipScope.isOpen).toBeFalsy();
}));
- it( 'should use it as both the show and hide triggers for unmapped triggers', inject(function($compile) {
+ it('should use it as both the show and hide triggers for unmapped triggers', inject(function($compile) {
elmBody = angular.element(
- '
'
+ '
'
);
$compile(elmBody)(scope);
scope.$apply();
@@ -358,9 +505,9 @@ describe('tooltip', function() {
tooltipScope = elmScope.$$childTail;
expect(tooltipScope.isOpen).toBeFalsy();
- elm.trigger('fakeTriggerAttr');
+ trigger(elm, 'fakeTriggerAttr');
expect(tooltipScope.isOpen).toBeTruthy();
- elm.trigger('fakeTriggerAttr');
+ trigger(elm, 'fakeTriggerAttr');
expect(tooltipScope.isOpen).toBeFalsy();
}));
@@ -368,8 +515,8 @@ describe('tooltip', function() {
scope.test = true;
elmBody = angular.element(
'
' +
- ' ' +
- ' ' +
+ ' ' +
+ ' ' +
'
'
);
@@ -388,13 +535,13 @@ describe('tooltip', function() {
expect(tooltipScope2.isOpen).toBeFalsy();
// mouseenter trigger is still set
- elm2.trigger('mouseenter');
+ trigger(elm2, 'mouseenter');
expect(tooltipScope2.isOpen).toBeTruthy();
}));
- it( 'should accept multiple triggers based on the map for mapped triggers', inject(function($compile) {
+ it('should accept multiple triggers based on the map for mapped triggers', inject(function($compile) {
elmBody = angular.element(
- '
'
+ '
'
);
$compile(elmBody)(scope);
scope.$apply();
@@ -402,20 +549,20 @@ describe('tooltip', function() {
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
- expect( tooltipScope.isOpen ).toBeFalsy();
- elm.trigger('focus');
- expect( tooltipScope.isOpen ).toBeTruthy();
- elm.trigger('blur');
- expect( tooltipScope.isOpen ).toBeFalsy();
- elm.trigger('fakeTriggerAttr');
- expect( tooltipScope.isOpen ).toBeTruthy();
- elm.trigger('fakeTriggerAttr');
- expect( tooltipScope.isOpen ).toBeFalsy();
+ expect(tooltipScope.isOpen).toBeFalsy();
+ trigger(elm, 'focus');
+ expect(tooltipScope.isOpen).toBeTruthy();
+ trigger(elm, 'blur');
+ expect(tooltipScope.isOpen).toBeFalsy();
+ trigger(elm, 'fakeTriggerAttr');
+ expect(tooltipScope.isOpen).toBeTruthy();
+ trigger(elm, 'fakeTriggerAttr');
+ expect(tooltipScope.isOpen).toBeFalsy();
}));
-
- it( 'should not show when trigger is set to "none"', inject(function($compile) {
+
+ it('should not show when trigger is set to "none"', inject(function($compile) {
elmBody = angular.element(
- '
'
+ '
'
);
$compile(elmBody)(scope);
scope.$apply();
@@ -442,7 +589,7 @@ describe('tooltip', function() {
it('should append to the body', inject(function($compile, $document) {
$body = $document.find('body');
elmBody = angular.element(
- '
Selector Text
'
+ '
Selector Text
'
);
$compile(elmBody)(scope);
@@ -452,7 +599,7 @@ describe('tooltip', function() {
tooltipScope = elmScope.$$childTail;
var bodyLength = $body.children().length;
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().length).toBe(1);
@@ -476,14 +623,14 @@ describe('tooltip', function() {
}
beforeEach(inject(function($compile, $rootScope) {
- elmBody = angular.element('
');
+ elmBody = angular.element('
');
$compile(elmBody)($rootScope);
$rootScope.$apply();
elm = elmBody.find('input');
elmScope = elm.scope();
- elm.trigger('fooTrigger');
+ trigger(elm, 'fooTrigger');
tooltipScope = elmScope.$$childTail.$$childTail;
}));
@@ -493,6 +640,46 @@ describe('tooltip', function() {
expect(inCache()).toBeFalsy();
}));
});
+
+ describe('observers', function() {
+ var elmBody, elm, elmScope, scope, tooltipScope;
+
+ beforeEach(inject(function($compile, $rootScope) {
+ scope = $rootScope;
+ scope.content = 'tooltip content';
+ scope.placement = 'top';
+ elmBody = angular.element('
');
+ $compile(elmBody)(scope);
+ scope.$apply();
+
+ elm = elmBody.find('input');
+ elmScope = elm.scope();
+ tooltipScope = elmScope.$$childTail;
+ }));
+
+ it('should be removed when tooltip hides', inject(function($timeout) {
+ expect(tooltipScope.content).toBe(undefined);
+ expect(tooltipScope.placement).toBe(undefined);
+
+ trigger(elm, 'mouseenter');
+ expect(tooltipScope.content).toBe('tooltip content');
+ expect(tooltipScope.placement).toBe('top');
+ scope.content = 'tooltip content updated';
+
+ scope.placement = 'bottom';
+ scope.$apply();
+ expect(tooltipScope.content).toBe('tooltip content updated');
+ expect(tooltipScope.placement).toBe('bottom');
+
+ trigger(elm, 'mouseleave');
+ $timeout.flush();
+ scope.content = 'tooltip content updated after close';
+ scope.placement = 'left';
+ scope.$apply();
+ expect(tooltipScope.content).toBe('tooltip content updated');
+ expect(tooltipScope.placement).toBe('bottom');
+ }));
+ });
});
describe('tooltipWithDifferentSymbols', function() {
@@ -505,19 +692,26 @@ describe('tooltipWithDifferentSymbols', function() {
beforeEach(module('template/tooltip/tooltip-popup.html'));
// configure interpolate provider to use [[ ]] instead of {{ }}
- beforeEach(module( function($interpolateProvider) {
- $interpolateProvider.startSymbol('[[');
- $interpolateProvider.startSymbol(']]');
- }));
+ beforeEach(module(function($interpolateProvider) {
+ $interpolateProvider.startSymbol('[[');
+ $interpolateProvider.startSymbol(']]');
+ }));
+
+ function trigger(element, evt) {
+ evt = new Event(evt);
+
+ element[0].dispatchEvent(evt);
+ element.scope().$$childTail.$digest();
+ }
it('should show the correct tooltip text', inject(function($compile, $rootScope) {
elmBody = angular.element(
- '
'
+ '
'
);
$compile(elmBody)($rootScope);
$rootScope.$apply();
var elmInput = elmBody.find('input');
- elmInput.trigger('focus');
+ trigger(elmInput, 'focus');
expect(elmInput.next().find('div').next().html()).toBe('My tooltip');
}));
@@ -535,24 +729,31 @@ describe('tooltip positioning', function() {
// load the template
beforeEach(module('template/tooltip/tooltip-popup.html'));
- beforeEach(inject(function($rootScope, $compile, _$position_) {
- $position = _$position_;
+ beforeEach(inject(function($rootScope, $compile, $uibPosition) {
+ $position = $uibPosition;
spyOn($position, 'positionElements').and.callThrough();
scope = $rootScope;
scope.text = 'Some Text';
- elmBody = $compile( angular.element(
- '
Selector Text
'
- ))( scope);
+ elmBody = $compile(angular.element(
+ '
Selector Text
'
+ ))(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
}));
+ function trigger(element, evt) {
+ evt = new Event(evt);
+
+ element[0].dispatchEvent(evt);
+ element.scope().$$childTail.$digest();
+ }
+
it('should re-position when value changes', inject(function($timeout) {
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
scope.$digest();
$timeout.flush();
@@ -561,7 +762,7 @@ describe('tooltip positioning', function() {
scope.text = 'New Text';
scope.$digest();
$timeout.flush();
- expect(elm.attr('tooltip')).toBe( 'New Text' );
+ expect(elm.attr('uib-tooltip')).toBe('New Text');
expect($position.positionElements.calls.count()).toEqual(startingPositionCalls + 1);
// Check that positionElements was called with elm
expect($position.positionElements.calls.argsFor(startingPositionCalls)[0][0])
@@ -593,88 +794,44 @@ describe('tooltipHtml', function() {
scope.html = 'I say:
Hello! ';
scope.safeHtml = $sce.trustAsHtml(scope.html);
- elmBody = $compile( angular.element(
- '
Selector Text
'
- ))( scope );
+ elmBody = $compile(angular.element(
+ '
Selector Text
'
+ ))(scope);
scope.$digest();
elm = elmBody.find('span');
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
}));
+ function trigger(element, evt) {
+ evt = new Event(evt);
+
+ element[0].dispatchEvent(evt);
+ element.scope().$$childTail.$digest();
+ }
+
it('should render html properly', inject(function() {
- elm.trigger('mouseenter');
- expect( elmBody.find('.tooltip-inner').html()).toBe(scope.html);
+ trigger(elm, 'mouseenter');
+ expect(elmBody.find('.tooltip-inner').html()).toBe(scope.html);
}));
it('should not open if html is empty', function() {
scope.safeHtml = null;
scope.$digest();
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(false);
});
it('should show on mouseenter and hide on mouseleave', inject(function($sce) {
expect(tooltipScope.isOpen).toBe(false);
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().length).toBe(2);
expect($sce.getTrustedHtml(tooltipScope.contentExp())).toEqual(scope.html);
- elm.trigger('mouseleave');
- expect(tooltipScope.isOpen).toBe(false);
- expect(elmBody.children().length).toBe(1);
- }));
-});
-
-describe('tooltipHtmlUnsafe', function() {
- var elm, elmBody, elmScope, tooltipScope, scope;
- var logWarnSpy;
-
- // load the tooltip code
- beforeEach(module('ui.bootstrap.tooltip', function($tooltipProvider) {
- $tooltipProvider.options({ animation: false });
- }));
-
- // load the template
- beforeEach(module('template/tooltip/tooltip-html-unsafe-popup.html'));
-
- beforeEach(inject(function($rootScope, $compile, $log) {
- scope = $rootScope;
- scope.html = 'I say:
Hello! ';
-
- logWarnSpy = spyOn($log, 'warn');
-
- elmBody = $compile( angular.element(
- '
Selector Text
'
- ))(scope);
- scope.$digest();
- elm = elmBody.find('span');
- elmScope = elm.scope();
- tooltipScope = elmScope.$$childTail;
- }));
-
- it('should warn that this is deprecated', function() {
- expect(logWarnSpy).toHaveBeenCalledWith(jasmine.stringMatching('deprecated'));
- });
-
- it('should render html properly', inject(function() {
- elm.trigger('mouseenter');
- expect(elmBody.find('.tooltip-inner').html()).toBe(scope.html);
- }));
-
- it('should show on mouseenter and hide on mouseleave', inject(function() {
- expect(tooltipScope.isOpen).toBe(false);
-
- elm.trigger('mouseenter');
- expect(tooltipScope.isOpen).toBe(true);
- expect(elmBody.children().length).toBe(2);
-
- expect(tooltipScope.content).toEqual(scope.html);
-
- elm.trigger('mouseleave');
+ trigger(elm, 'mouseleave');
expect(tooltipScope.isOpen).toBe(false);
expect(elmBody.children().length).toBe(1);
}));
@@ -687,6 +844,13 @@ describe('$tooltipProvider', function() {
elmScope,
tooltipScope;
+ function trigger(element, evt) {
+ evt = new Event(evt);
+
+ element[0].dispatchEvent(evt);
+ element.scope().$$childTail.$digest();
+ }
+
describe('popupDelay', function() {
beforeEach(module('ui.bootstrap.tooltip', function($tooltipProvider) {
$tooltipProvider.options({popupDelay: 1000});
@@ -697,7 +861,7 @@ describe('$tooltipProvider', function() {
beforeEach(inject(function($rootScope, $compile) {
elmBody = angular.element(
- '
Selector Text
'
+ '
Selector Text
'
);
scope = $rootScope;
@@ -709,7 +873,7 @@ describe('$tooltipProvider', function() {
}));
it('should open after timeout', inject(function($timeout) {
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(false);
$timeout.flush();
@@ -722,17 +886,17 @@ describe('$tooltipProvider', function() {
beforeEach(module('template/tooltip/tooltip-popup.html'));
beforeEach(module('ui.bootstrap.tooltip', function($tooltipProvider) {
- $tooltipProvider.options({ appendToBody: true });
+ $tooltipProvider.options({ appendToBody: true });
}));
afterEach(function() {
$body.find('.tooltip').remove();
});
- it( 'should append to the body', inject(function($rootScope, $compile, $document) {
+ it('should append to the body', inject(function($rootScope, $compile, $document) {
$body = $document.find('body');
elmBody = angular.element(
- '
Selector Text
'
+ '
Selector Text
'
);
scope = $rootScope;
@@ -743,7 +907,7 @@ describe('$tooltipProvider', function() {
tooltipScope = elmScope.$$childTail;
var bodyLength = $body.children().length;
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
expect(elmBody.children().length).toBe(1);
@@ -752,7 +916,7 @@ describe('$tooltipProvider', function() {
it('should close on location change', inject(function($rootScope, $compile) {
elmBody = angular.element(
- '
Selector Text
'
+ '
Selector Text
'
);
scope = $rootScope;
@@ -762,7 +926,7 @@ describe('$tooltipProvider', function() {
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBe(true);
scope.$broadcast('$locationChangeSuccess');
@@ -771,18 +935,18 @@ describe('$tooltipProvider', function() {
}));
});
- describe( 'triggers', function() {
- describe( 'triggers with a mapped value', function() {
- beforeEach(module('ui.bootstrap.tooltip', function($tooltipProvider) {
- $tooltipProvider.options({trigger: 'focus'});
+ describe('triggers', function() {
+ describe('triggers with a mapped value', function() {
+ beforeEach(module('ui.bootstrap.tooltip', function($uibTooltipProvider) {
+ $uibTooltipProvider.options({trigger: 'focus'});
}));
// load the template
beforeEach(module('template/tooltip/tooltip-popup.html'));
- it( 'should use the show trigger and the mapped value for the hide trigger', inject(function($rootScope, $compile) {
+ it('should use the show trigger and the mapped value for the hide trigger', inject(function($rootScope, $compile) {
elmBody = angular.element(
- '
'
+ '
'
);
scope = $rootScope;
@@ -793,15 +957,15 @@ describe('$tooltipProvider', function() {
tooltipScope = elmScope.$$childTail;
expect(tooltipScope.isOpen).toBeFalsy();
- elm.trigger('focus');
+ trigger(elm, 'focus');
expect(tooltipScope.isOpen).toBeTruthy();
- elm.trigger('blur');
+ trigger(elm, 'blur');
expect(tooltipScope.isOpen).toBeFalsy();
}));
- it( 'should override the show and hide triggers if there is an attribute', inject(function($rootScope, $compile) {
+ it('should override the show and hide triggers if there is an attribute', inject(function($rootScope, $compile) {
elmBody = angular.element(
- '
'
+ '
'
);
scope = $rootScope;
@@ -812,25 +976,25 @@ describe('$tooltipProvider', function() {
tooltipScope = elmScope.$$childTail;
expect(tooltipScope.isOpen).toBeFalsy();
- elm.trigger('mouseenter');
+ trigger(elm, 'mouseenter');
expect(tooltipScope.isOpen).toBeTruthy();
- elm.trigger('mouseleave');
+ trigger(elm, 'mouseleave');
expect(tooltipScope.isOpen).toBeFalsy();
}));
});
describe('triggers with a custom mapped value', function() {
- beforeEach(module('ui.bootstrap.tooltip', function($tooltipProvider) {
- $tooltipProvider.setTriggers({ 'customOpenTrigger': 'customCloseTrigger' });
- $tooltipProvider.options({trigger: 'customOpenTrigger'});
+ beforeEach(module('ui.bootstrap.tooltip', function($uibTooltipProvider) {
+ $uibTooltipProvider.setTriggers({ customOpenTrigger: 'foo bar' });
+ $uibTooltipProvider.options({trigger: 'customOpenTrigger'});
}));
// load the template
beforeEach(module('template/tooltip/tooltip-popup.html'));
- it( 'should use the show trigger and the mapped value for the hide trigger', inject(function($rootScope, $compile) {
+ it('should use the show trigger and the mapped value for the hide trigger', inject(function($rootScope, $compile) {
elmBody = angular.element(
- '
'
+ '
'
);
scope = $rootScope;
@@ -841,24 +1005,28 @@ describe('$tooltipProvider', function() {
tooltipScope = elmScope.$$childTail;
expect(tooltipScope.isOpen).toBeFalsy();
- elm.trigger('customOpenTrigger');
+ trigger(elm, 'customOpenTrigger');
+ expect(tooltipScope.isOpen).toBeTruthy();
+ trigger(elm, 'foo');
+ expect(tooltipScope.isOpen).toBeFalsy();
+ trigger(elm, 'customOpenTrigger');
expect(tooltipScope.isOpen).toBeTruthy();
- elm.trigger('customCloseTrigger');
+ trigger(elm, 'bar');
expect(tooltipScope.isOpen).toBeFalsy();
}));
});
describe('triggers without a mapped value', function() {
- beforeEach(module('ui.bootstrap.tooltip', function($tooltipProvider) {
- $tooltipProvider.options({trigger: 'fakeTrigger'});
+ beforeEach(module('ui.bootstrap.tooltip', function($uibTooltipProvider) {
+ $uibTooltipProvider.options({trigger: 'fakeTrigger'});
}));
// load the template
beforeEach(module('template/tooltip/tooltip-popup.html'));
- it( 'should use the show trigger to hide', inject(function($rootScope, $compile) {
+ it('should use the show trigger to hide', inject(function($rootScope, $compile) {
elmBody = angular.element(
- '
Selector Text
'
+ '
Selector Text
'
);
scope = $rootScope;
@@ -868,11 +1036,11 @@ describe('$tooltipProvider', function() {
elmScope = elm.scope();
tooltipScope = elmScope.$$childTail;
- expect( tooltipScope.isOpen ).toBeFalsy();
- elm.trigger('fakeTrigger');
- expect( tooltipScope.isOpen ).toBeTruthy();
- elm.trigger('fakeTrigger');
- expect( tooltipScope.isOpen ).toBeFalsy();
+ expect(tooltipScope.isOpen).toBeFalsy();
+ trigger(elm, 'fakeTrigger');
+ expect(tooltipScope.isOpen).toBeTruthy();
+ trigger(elm, 'fakeTrigger');
+ expect(tooltipScope.isOpen).toBeFalsy();
}));
});
});
diff --git a/src/tooltip/test/tooltip2.spec.js b/src/tooltip/test/tooltip2.spec.js
index eca3678c74..2ed8e2b1a4 100644
--- a/src/tooltip/test/tooltip2.spec.js
+++ b/src/tooltip/test/tooltip2.spec.js
@@ -5,7 +5,6 @@ describe('tooltip directive', function() {
beforeEach(module('template/tooltip/tooltip-popup.html'));
beforeEach(module('template/tooltip/tooltip-template-popup.html'));
beforeEach(module('template/tooltip/tooltip-html-popup.html'));
- beforeEach(module('template/tooltip/tooltip-html-unsafe-popup.html'));
beforeEach(inject(function(_$rootScope_, _$compile_, _$document_, _$timeout_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
@@ -39,23 +38,31 @@ describe('tooltip directive', function() {
});
function compileTooltip(ttipMarkup) {
- var fragment = $compile('
'+ttipMarkup+'
')($rootScope);
+ var fragment = $compile('
' + ttipMarkup + '
')($rootScope);
$rootScope.$digest();
return fragment;
}
- function closeTooltip(hostEl, trigger, shouldNotFlush) {
- hostEl.trigger(trigger || 'mouseleave');
+ function closeTooltip(hostEl, triggerEvt, shouldNotFlush) {
+ trigger(hostEl, triggerEvt || 'mouseleave');
+ hostEl.scope().$$childTail.$digest();
if (!shouldNotFlush) {
$timeout.flush();
}
}
+ function trigger(element, evt) {
+ evt = new Event(evt);
+
+ element[0].dispatchEvent(evt);
+ element.scope().$$childTail.$digest();
+ }
+
describe('basic scenarios with default options', function() {
it('shows default tooltip on mouse enter and closes on mouse leave', function() {
- var fragment = compileTooltip('
Trigger here ');
+ var fragment = compileTooltip('
Trigger here ');
- fragment.find('span').trigger('mouseenter');
+ trigger(fragment.find('span'), 'mouseenter');
expect(fragment).toHaveOpenTooltips();
closeTooltip(fragment.find('span'));
@@ -63,16 +70,16 @@ describe('tooltip directive', function() {
});
it('should not show a tooltip when its content is empty', function() {
- var fragment = compileTooltip('
');
- fragment.find('span').trigger('mouseenter');
+ var fragment = compileTooltip('
');
+ trigger(fragment.find('span'), 'mouseenter');
expect(fragment).not.toHaveOpenTooltips();
});
it('should not show a tooltip when its content becomes empty', function() {
$rootScope.content = 'some text';
- var fragment = compileTooltip('
');
+ var fragment = compileTooltip('
');
- fragment.find('span').trigger('mouseenter');
+ trigger(fragment.find('span'), 'mouseenter');
expect(fragment).toHaveOpenTooltips();
$rootScope.content = '';
@@ -83,22 +90,21 @@ describe('tooltip directive', function() {
it('should update tooltip when its content becomes empty', function() {
$rootScope.content = 'some text';
- var fragment = compileTooltip('
');
+ var fragment = compileTooltip('
');
$rootScope.content = '';
$rootScope.$digest();
- fragment.find('span').trigger('mouseenter');
+ trigger(fragment.find('span'), 'mouseenter');
expect(fragment).not.toHaveOpenTooltips();
});
});
describe('option by option', function() {
var tooltipTypes = {
- 'tooltip': 'tooltip="tooltip text"',
- 'tooltip-html': 'tooltip-html="tooltipSafeHtml"',
- 'tooltip-html-unsafe': 'tooltip-html-unsafe="tooltip text"',
- 'tooltip-template': 'tooltip-template="\'tooltipTextUrl\'"'
+ 'tooltip': 'uib-tooltip="tooltip text"',
+ 'tooltip-html': 'uib-tooltip-html="tooltipSafeHtml"',
+ 'tooltip-template': 'uib-tooltip-template="\'tooltipTextUrl\'"'
};
beforeEach(inject(function($sce, $templateCache) {
@@ -108,11 +114,11 @@ describe('tooltip directive', function() {
}));
angular.forEach(tooltipTypes, function(html, key) {
- describe(key, function () {
+ describe(key, function() {
describe('placement', function() {
it('can specify an alternative, valid placement', function() {
var fragment = compileTooltip('
Trigger here ');
- fragment.find('span').trigger('mouseenter');
+ trigger(fragment.find('span'), 'mouseenter');
var ttipElement = fragment.find('div.tooltip');
expect(fragment).toHaveOpenTooltips();
@@ -126,7 +132,7 @@ describe('tooltip directive', function() {
describe('class', function() {
it('can specify a custom class', function() {
var fragment = compileTooltip('
Trigger here ');
- fragment.find('span').trigger('mouseenter');
+ trigger(fragment.find('span'), 'mouseenter');
var ttipElement = fragment.find('div.tooltip');
expect(fragment).toHaveOpenTooltips();
@@ -141,9 +147,9 @@ describe('tooltip directive', function() {
});
it('should show even after close trigger is called multiple times - issue #1847', function() {
- var fragment = compileTooltip('
Trigger here ');
+ var fragment = compileTooltip('
Trigger here ');
- fragment.find('span').trigger('mouseenter');
+ trigger(fragment.find('span'), 'mouseenter');
expect(fragment).toHaveOpenTooltips();
closeTooltip(fragment.find('span'), null, true);
@@ -153,7 +159,7 @@ describe('tooltip directive', function() {
closeTooltip(fragment.find('span'), null, true);
expect(fragment).toHaveOpenTooltips();
- fragment.find('span').trigger('mouseenter');
+ trigger(fragment.find('span'), 'mouseenter');
expect(fragment).toHaveOpenTooltips();
$timeout.flush();
@@ -161,24 +167,116 @@ describe('tooltip directive', function() {
});
it('should hide even after show trigger is called multiple times', function() {
- var fragment = compileTooltip('
Trigger here ');
+ var fragment = compileTooltip('
Trigger here ');
- fragment.find('span').trigger('mouseenter');
- fragment.find('span').trigger('mouseenter');
+ trigger(fragment.find('span'), 'mouseenter');
+ trigger(fragment.find('span'), 'mouseenter');
closeTooltip(fragment.find('span'));
expect(fragment).not.toHaveOpenTooltips();
});
it('should not show tooltips element is disabled (button) - issue #3167', function() {
- var fragment = compileTooltip('
Cancel ');
+ var fragment = compileTooltip('
Cancel ');
- fragment.find('button').trigger('mouseenter');
+ trigger(fragment.find('button'), 'mouseenter');
expect(fragment).toHaveOpenTooltips();
- fragment.find('button').trigger('click');
+ trigger(fragment.find('button'), 'click');
$timeout.flush();
// One needs to flush deferred functions before checking there is no tooltip.
expect(fragment).not.toHaveOpenTooltips();
});
});
+
+/* Deprecation tests below */
+
+describe('tooltip deprecation', function() {
+ beforeEach(module('ui.bootstrap.tooltip'));
+ beforeEach(module('template/tooltip/tooltip-popup.html'));
+ beforeEach(module('template/tooltip/tooltip-template-popup.html'));
+ beforeEach(module('template/tooltip/tooltip-html-popup.html'));
+
+ describe('tooltip', function() {
+ it('should suppress warning', function() {
+ module(function($provide) {
+ $provide.value('$tooltipSuppressWarning', true);
+ });
+
+ inject(function($compile, $log, $rootScope) {
+ spyOn($log, 'warn');
+
+ var element = '
Trigger here
';
+ element = $compile(element)($rootScope);
+ $rootScope.$digest();
+ expect($log.warn.calls.count()).toBe(0);
+ });
+ });
+
+ it('should give warning by default', inject(function($compile, $log, $rootScope) {
+ spyOn($log, 'warn');
+
+ var element = '
Trigger here
';
+ element = $compile(element)($rootScope);
+ $rootScope.$digest();
+
+ expect($log.warn.calls.count()).toBe(1);
+ expect($log.warn.calls.argsFor(0)).toEqual(['$tooltip is now deprecated. Use $uibTooltip instead.']);
+ }));
+ });
+
+ describe('tooltip html', function() {
+ var elm, elmBody, elmScope, tooltipScope;
+
+ function trigger(element, evt) {
+ evt = new Event(evt);
+
+ element[0].dispatchEvent(evt);
+ element.scope().$$childTail.$digest();
+ }
+
+ it('should suppress warning', function() {
+ module(function($provide) {
+ $provide.value('$tooltipSuppressWarning', true);
+ });
+
+ inject(function($compile, $log, $rootScope, $sce) {
+ spyOn($log, 'warn');
+
+ $rootScope.html = 'I say:
Hello! ';
+ $rootScope.safeHtml = $sce.trustAsHtml($rootScope.html);
+ elmBody = angular.element('
Selector Text
');
+ $compile(elmBody)($rootScope);
+ $rootScope.$digest();
+ elm = elmBody.find('span');
+ elmScope = elm.scope();
+ tooltipScope = elmScope.$$childTail;
+
+ trigger(elm, 'mouseenter');
+ tooltipScope.$digest();
+
+ expect($log.warn.calls.count()).toBe(0);
+ });
+ });
+
+ it('should give warning by default', inject(function($compile, $log, $rootScope, $sce) {
+ spyOn($log, 'warn');
+
+ $rootScope.html = 'I say:
Hello! ';
+ $rootScope.safeHtml = $sce.trustAsHtml($rootScope.html);
+ elmBody = angular.element('
Selector Text
');
+ $compile(elmBody)($rootScope);
+ $rootScope.$digest();
+ elm = elmBody.find('span');
+ elmScope = elm.scope();
+ tooltipScope = elmScope.$$childTail;
+
+ trigger(elm, 'mouseenter');
+ tooltipScope.$digest();
+
+ expect($log.warn.calls.count()).toBe(2);
+ expect($log.warn.calls.argsFor(0)).toEqual(['$tooltip is now deprecated. Use $uibTooltip instead.']);
+ expect($log.warn.calls.argsFor(1)).toEqual(['tooltip-html-popup is now deprecated. Use uib-tooltip-html-popup instead.']);
+ }));
+ });
+});
diff --git a/src/tooltip/tooltip.js b/src/tooltip/tooltip.js
index 8a20c52d02..f2d1dd7297 100644
--- a/src/tooltip/tooltip.js
+++ b/src/tooltip/tooltip.js
@@ -3,18 +3,19 @@
* function, placement as a function, inside, support for more triggers than
* just mouse enter/leave, html tooltips, and selector delegation.
*/
-angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml'])
+angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.stackedMap'])
/**
* The $tooltip service creates tooltip- and popover-like directives as well as
* houses global options for them.
*/
-.provider('$tooltip', function() {
+.provider('$uibTooltip', function() {
// The default options tooltip and popover.
var defaultOptions = {
placement: 'top',
animation: true,
popupDelay: 0,
+ popupCloseDelay: 0,
useContentExp: false
};
@@ -66,8 +67,20 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.b
* Returns the actual instance of the $tooltip service.
* TODO support multiple triggers
*/
- this.$get = ['$window', '$compile', '$timeout', '$document', '$position', '$interpolate', '$rootScope', '$parse', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse) {
- return function $tooltip(type, prefix, defaultTriggerShow, options) {
+ this.$get = ['$window', '$compile', '$timeout', '$document', '$uibPosition', '$interpolate', '$rootScope', '$parse', '$$stackedMap', function($window, $compile, $timeout, $document, $position, $interpolate, $rootScope, $parse, $$stackedMap) {
+ var openedTooltips = $$stackedMap.createNew();
+ $document.on('keypress', function(e) {
+ if (e.which === 27) {
+ var last = openedTooltips.top();
+ if (last) {
+ last.value.close();
+ openedTooltips.removeTop();
+ last = null;
+ }
+ }
+ });
+
+ return function $tooltip(ttType, prefix, defaultTriggerShow, options) {
options = angular.extend({}, defaultOptions, globalOptions, options);
/**
@@ -95,54 +108,64 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.b
};
}
- var directiveName = snake_case(type);
+ var directiveName = snake_case(ttType);
var startSym = $interpolate.startSymbol();
var endSym = $interpolate.endSymbol();
var template =
- '
'+
+ 'content="' + startSym + 'content' + endSym + '" ') +
+ 'placement="' + startSym + 'placement' + endSym + '" '+
+ 'popup-class="' + startSym + 'popupClass' + endSym + '" '+
+ 'animation="animation" ' +
+ 'is-open="isOpen"' +
+ 'origin-scope="origScope" ' +
+ 'style="visibility: hidden; display: block;"' +
+ '>' +
'
';
return {
- restrict: 'EA',
compile: function(tElem, tAttrs) {
- var tooltipLinker = $compile( template );
+ var tooltipLinker = $compile(template);
return function link(scope, element, attrs, tooltipCtrl) {
var tooltip;
var tooltipLinkedScope;
var transitionTimeout;
- var popupTimeout;
+ var showTimeout;
+ var hideTimeout;
+ var positionTimeout;
var appendToBody = angular.isDefined(options.appendToBody) ? options.appendToBody : false;
var triggers = getTriggers(undefined);
var hasEnableExp = angular.isDefined(attrs[prefix + 'Enable']);
var ttScope = scope.$new(true);
var repositionScheduled = false;
- var isOpenExp = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false;
+ var isOpenParse = angular.isDefined(attrs[prefix + 'IsOpen']) ? $parse(attrs[prefix + 'IsOpen']) : false;
+ var contentParse = options.useContentExp ? $parse(attrs[ttType]) : false;
+ var observers = [];
var positionTooltip = function() {
- if (!tooltip) { return; }
-
- var ttPosition = $position.positionElements(element, tooltip, ttScope.placement, appendToBody);
- ttPosition.top += 'px';
- ttPosition.left += 'px';
-
- // Now set the calculated positioning.
- tooltip.css(ttPosition);
- };
-
- var positionTooltipAsync = function() {
- $timeout(positionTooltip, 0, false);
+ // check if tooltip exists and is not empty
+ if (!tooltip || !tooltip.html()) { return; }
+
+ if (!positionTimeout) {
+ positionTimeout = $timeout(function() {
+ // Reset the positioning.
+ tooltip.css({ top: 0, left: 0 });
+
+ // Now set the calculated positioning.
+ var ttCss = $position.positionElements(element, tooltip, ttScope.placement, appendToBody);
+ ttCss.top += 'px';
+ ttCss.left += 'px';
+ ttCss.visibility = 'visible';
+ tooltip.css(ttCss);
+
+ positionTimeout = null;
+ }, 0, false);
+ }
};
// Set up the correct scope to allow transclusion later
@@ -151,6 +174,9 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.b
// By default, the tooltip is not open.
// TODO add ability to start tooltip opened
ttScope.isOpen = false;
+ openedTooltips.add(ttScope, {
+ close: hide
+ });
function toggleTooltipBind() {
if (!ttScope.isOpen) {
@@ -171,89 +197,96 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.b
if (ttScope.popupDelay) {
// Do nothing if the tooltip was already scheduled to pop-up.
// This happens if show is triggered multiple times before any hide is triggered.
- if (!popupTimeout) {
- popupTimeout = $timeout(show, ttScope.popupDelay, false);
- popupTimeout.then(function(reposition) { reposition(); });
+ if (!showTimeout) {
+ showTimeout = $timeout(show, ttScope.popupDelay, false);
}
} else {
- show()();
+ show();
}
}
- function hideTooltipBind () {
- hide();
- if (!$rootScope.$$phase) {
- $rootScope.$digest();
+ function hideTooltipBind() {
+ if (ttScope.popupCloseDelay) {
+ hideTimeout = $timeout(hide, ttScope.popupCloseDelay, false);
+ } else {
+ hide();
}
}
// Show the tooltip popup element.
function show() {
- popupTimeout = null;
+ if (showTimeout) {
+ $timeout.cancel(showTimeout);
+ showTimeout = null;
+ }
// If there is a pending remove transition, we must cancel it, lest the
// tooltip be mysteriously removed.
+ if (hideTimeout) {
+ $timeout.cancel(hideTimeout);
+ hideTimeout = null;
+ }
if (transitionTimeout) {
$timeout.cancel(transitionTimeout);
transitionTimeout = null;
}
// Don't show empty tooltips.
- if (!(options.useContentExp ? ttScope.contentExp() : ttScope.content)) {
+ if (!ttScope.content) {
return angular.noop;
}
createTooltip();
- // Set the initial positioning.
- tooltip.css({ top: 0, left: 0, display: 'block' });
-
- positionTooltip();
-
// And show the tooltip.
- ttScope.isOpen = true;
- if (isOpenExp) {
- isOpenExp.assign(ttScope.origScope, ttScope.isOpen);
- }
-
- if (!$rootScope.$$phase) {
- ttScope.$apply(); // digest required as $apply is not called
- }
-
- // Return positioning function as promise callback for correct
- // positioning after draw.
- return positionTooltip;
+ ttScope.$evalAsync(function() {
+ ttScope.isOpen = true;
+ assignIsOpen(true);
+ positionTooltip();
+ });
}
// Hide the tooltip popup element.
function hide() {
- // First things first: we don't show it anymore.
- ttScope.isOpen = false;
- if (isOpenExp) {
- isOpenExp.assign(ttScope.origScope, ttScope.isOpen);
+ if (!ttScope) {
+ return;
}
-
+
//if tooltip is going to be shown after delay, we must cancel this
- $timeout.cancel(popupTimeout);
- popupTimeout = null;
-
- // 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 (ttScope.animation) {
- if (!transitionTimeout) {
- transitionTimeout = $timeout(removeTooltip, 500);
- }
- } else {
- removeTooltip();
+ if (showTimeout) {
+ $timeout.cancel(showTimeout);
+ showTimeout = null;
}
+
+ if (positionTimeout) {
+ $timeout.cancel(positionTimeout);
+ positionTimeout = null;
+ }
+
+ // First things first: we don't show it anymore.
+ ttScope.$evalAsync(function() {
+ ttScope.isOpen = false;
+ assignIsOpen(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.
+ // The fade transition in TWBS is 150ms.
+ if (ttScope.animation) {
+ if (!transitionTimeout) {
+ transitionTimeout = $timeout(removeTooltip, 150, false);
+ }
+ } else {
+ removeTooltip();
+ }
+ });
}
function createTooltip() {
// There can only be one tooltip element per directive shown at once.
if (tooltip) {
- removeTooltip();
+ return;
}
+
tooltipLinkedScope = ttScope.$new();
tooltip = tooltipLinker(tooltipLinkedScope, function(tooltip) {
if (appendToBody) {
@@ -263,29 +296,12 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.b
}
});
- if (options.useContentExp) {
- tooltipLinkedScope.$watch('contentExp()', function(val) {
- if (!val && ttScope.isOpen) {
- hide();
- }
- });
-
- tooltipLinkedScope.$watch(function() {
- if (!repositionScheduled) {
- repositionScheduled = true;
- tooltipLinkedScope.$$postDigest(function() {
- repositionScheduled = false;
- if (ttScope.isOpen) {
- positionTooltipAsync();
- }
- });
- }
- });
-
- }
+ prepObservers();
}
function removeTooltip() {
+ unregisterObservers();
+
transitionTimeout = null;
if (tooltip) {
tooltip.remove();
@@ -297,34 +313,45 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.b
}
}
+ /**
+ * Set the inital scope values. Once
+ * the tooltip is created, the observers
+ * will be added to keep things in synch.
+ */
function prepareTooltip() {
- prepPopupClass();
- prepPlacement();
- prepPopupDelay();
+ ttScope.title = attrs[prefix + 'Title'];
+ if (contentParse) {
+ ttScope.content = contentParse(scope);
+ } else {
+ ttScope.content = attrs[ttType];
+ }
+
+ ttScope.popupClass = attrs[prefix + 'Class'];
+ ttScope.placement = angular.isDefined(attrs[prefix + 'Placement']) ? attrs[prefix + 'Placement'] : options.placement;
+
+ var delay = parseInt(attrs[prefix + 'PopupDelay'], 10);
+ var closeDelay = parseInt(attrs[prefix + 'PopupCloseDelay'], 10);
+ ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay;
+ ttScope.popupCloseDelay = !isNaN(closeDelay) ? closeDelay : options.popupCloseDelay;
+ }
+
+ function assignIsOpen(isOpen) {
+ if (isOpenParse && angular.isFunction(isOpenParse.assign)) {
+ isOpenParse.assign(scope, isOpen);
+ }
}
ttScope.contentExp = function() {
- return scope.$eval(attrs[type]);
+ return ttScope.content;
};
/**
* Observe the relevant attributes.
*/
- if (!options.useContentExp) {
- attrs.$observe(type, function(val) {
- ttScope.content = val;
-
- if (!val && ttScope.isOpen) {
- hide();
- } else {
- positionTooltipAsync();
- }
- });
- }
-
attrs.$observe('disabled', function(val) {
- if (popupTimeout && val) {
- $timeout.cancel(popupTimeout);
+ if (showTimeout && val) {
+ $timeout.cancel(showTimeout);
+ showTimeout = null;
}
if (val && ttScope.isOpen) {
@@ -332,41 +359,81 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.b
}
});
- attrs.$observe(prefix + 'Title', function(val) {
- ttScope.title = val;
- positionTooltipAsync();
- });
-
- attrs.$observe( prefix + 'Placement', function() {
- if (ttScope.isOpen) {
- $timeout(function() {
- prepPlacement();
- show()();
- }, 0, false);
- }
- });
-
- if (isOpenExp) {
- scope.$watch(isOpenExp, function(val) {
- if (val !== ttScope.isOpen) {
+ if (isOpenParse) {
+ scope.$watch(isOpenParse, function(val) {
+ /*jshint -W018 */
+ if (!val === ttScope.isOpen) {
toggleTooltipBind();
}
+ /*jshint +W018 */
});
}
- function prepPopupClass() {
- ttScope.popupClass = attrs[prefix + 'Class'];
- }
+ function prepObservers() {
+ observers.length = 0;
+
+ if (contentParse) {
+ observers.push(
+ scope.$watch(contentParse, function(val) {
+ ttScope.content = val;
+ if (!val && ttScope.isOpen) {
+ hide();
+ }
+ })
+ );
+
+ observers.push(
+ tooltipLinkedScope.$watch(function() {
+ if (!repositionScheduled) {
+ repositionScheduled = true;
+ tooltipLinkedScope.$$postDigest(function() {
+ repositionScheduled = false;
+ if (ttScope && ttScope.isOpen) {
+ positionTooltip();
+ }
+ });
+ }
+ })
+ );
+ } else {
+ observers.push(
+ attrs.$observe(ttType, function(val) {
+ ttScope.content = val;
+ if (!val && ttScope.isOpen) {
+ hide();
+ } else {
+ positionTooltip();
+ }
+ })
+ );
+ }
- function prepPlacement() {
- var val = attrs[prefix + 'Placement'];
- ttScope.placement = angular.isDefined(val) ? val : options.placement;
+ observers.push(
+ attrs.$observe(prefix + 'Title', function(val) {
+ ttScope.title = val;
+ if (ttScope.isOpen) {
+ positionTooltip();
+ }
+ })
+ );
+
+ observers.push(
+ attrs.$observe(prefix + 'Placement', function(val) {
+ ttScope.placement = val ? val : options.placement;
+ if (ttScope.isOpen) {
+ positionTooltip();
+ }
+ })
+ );
}
- function prepPopupDelay() {
- var val = attrs[prefix + 'PopupDelay'];
- var delay = parseInt(val, 10);
- ttScope.popupDelay = !isNaN(delay) ? delay : options.popupDelay;
+ function unregisterObservers() {
+ if (observers.length) {
+ angular.forEach(observers, function(observer) {
+ observer();
+ });
+ observers.length = 0;
+ }
}
var unregisterTriggers = function() {
@@ -374,7 +441,9 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.b
element.unbind(trigger, showTooltipBind);
});
triggers.hide.forEach(function(trigger) {
- element.unbind(trigger, hideTooltipBind);
+ trigger.split(' ').forEach(function(hideTrigger) {
+ element[0].removeEventListener(hideTrigger, hideTooltipBind);
+ });
});
};
@@ -386,15 +455,25 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.b
if (triggers.show !== 'none') {
triggers.show.forEach(function(trigger, idx) {
+ // Using raw addEventListener due to jqLite/jQuery bug - #4060
if (trigger === triggers.hide[idx]) {
- element.bind(trigger, toggleTooltipBind);
+ element[0].addEventListener(trigger, toggleTooltipBind);
} else if (trigger) {
- element.bind(trigger, showTooltipBind);
- element.bind(triggers.hide[idx], hideTooltipBind);
+ element[0].addEventListener(trigger, showTooltipBind);
+ triggers.hide[idx].split(' ').forEach(function(trigger) {
+ element[0].addEventListener(trigger, hideTooltipBind);
+ });
}
+
+ element.on('keypress', function(e) {
+ if (e.which === 27) {
+ hideTooltipBind();
+ }
+ });
});
}
}
+
prepTriggers();
var animation = scope.$eval(attrs[prefix + 'Animation']);
@@ -416,10 +495,13 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.b
// Make sure tooltip is destroyed and removed.
scope.$on('$destroy', function onDestroyTooltip() {
- $timeout.cancel( transitionTimeout );
- $timeout.cancel( popupTimeout );
+ $timeout.cancel(transitionTimeout);
+ $timeout.cancel(showTimeout);
+ $timeout.cancel(hideTimeout);
+ $timeout.cancel(positionTimeout);
unregisterTriggers();
removeTooltip();
+ openedTooltips.remove(ttScope);
ttScope = null;
});
};
@@ -430,7 +512,7 @@ angular.module('ui.bootstrap.tooltip', ['ui.bootstrap.position', 'ui.bootstrap.b
})
// This is mostly ngInclude code but with a custom scope
-.directive('tooltipTemplateTransclude', [
+.directive('uibTooltipTemplateTransclude', [
'$animate', '$sce', '$compile', '$templateRequest',
function ($animate , $sce , $compile , $templateRequest) {
return {
@@ -447,10 +529,12 @@ function ($animate , $sce , $compile , $templateRequest) {
previousElement.remove();
previousElement = null;
}
+
if (currentScope) {
currentScope.$destroy();
currentScope = null;
}
+
if (currentElement) {
$animate.leave(currentElement).then(function() {
previousElement = null;
@@ -460,7 +544,7 @@ function ($animate , $sce , $compile , $templateRequest) {
}
};
- scope.$watch($sce.parseAsResourceUrl(attrs.tooltipTemplateTransclude), function(src) {
+ scope.$watch($sce.parseAsResourceUrl(attrs.uibTooltipTemplateTransclude), function(src) {
var thisChangeId = ++changeCounter;
if (src) {
@@ -502,16 +586,18 @@ function ($animate , $sce , $compile , $templateRequest) {
* They must not be animated as they're expected to be present on the tooltip on
* initialization.
*/
-.directive('tooltipClasses', function() {
+.directive('uibTooltipClasses', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
if (scope.placement) {
element.addClass(scope.placement);
}
+
if (scope.popupClass) {
element.addClass(scope.popupClass);
}
+
if (scope.animation()) {
element.addClass(attrs.tooltipAnimationClass);
}
@@ -519,68 +605,225 @@ function ($animate , $sce , $compile , $templateRequest) {
};
})
-.directive('tooltipPopup', function() {
+.directive('uibTooltipPopup', function() {
return {
- restrict: 'EA',
replace: true,
scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
- templateUrl: 'template/tooltip/tooltip-popup.html'
+ templateUrl: 'template/tooltip/tooltip-popup.html',
+ link: function(scope, element) {
+ element.addClass('tooltip');
+ }
};
})
-.directive('tooltip', [ '$tooltip', function($tooltip) {
- return $tooltip('tooltip', 'tooltip', 'mouseenter');
+.directive('uibTooltip', [ '$uibTooltip', function($uibTooltip) {
+ return $uibTooltip('uibTooltip', 'tooltip', 'mouseenter');
}])
-.directive('tooltipTemplatePopup', function() {
+.directive('uibTooltipTemplatePopup', function() {
return {
- restrict: 'EA',
replace: true,
scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&',
originScope: '&' },
- templateUrl: 'template/tooltip/tooltip-template-popup.html'
+ templateUrl: 'template/tooltip/tooltip-template-popup.html',
+ link: function(scope, element) {
+ element.addClass('tooltip');
+ }
};
})
-.directive('tooltipTemplate', ['$tooltip', function($tooltip) {
- return $tooltip('tooltipTemplate', 'tooltip', 'mouseenter', {
+.directive('uibTooltipTemplate', ['$uibTooltip', function($uibTooltip) {
+ return $uibTooltip('uibTooltipTemplate', 'tooltip', 'mouseenter', {
useContentExp: true
});
}])
-.directive('tooltipHtmlPopup', function() {
+.directive('uibTooltipHtmlPopup', function() {
return {
- restrict: 'EA',
replace: true,
scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
- templateUrl: 'template/tooltip/tooltip-html-popup.html'
+ templateUrl: 'template/tooltip/tooltip-html-popup.html',
+ link: function(scope, element) {
+ element.addClass('tooltip');
+ }
};
})
-.directive('tooltipHtml', ['$tooltip', function($tooltip) {
- return $tooltip('tooltipHtml', 'tooltip', 'mouseenter', {
+.directive('uibTooltipHtml', ['$uibTooltip', function($uibTooltip) {
+ return $uibTooltip('uibTooltipHtml', 'tooltip', 'mouseenter', {
useContentExp: true
});
+}]);
+
+/* Deprecated tooltip below */
+
+angular.module('ui.bootstrap.tooltip')
+
+.value('$tooltipSuppressWarning', false)
+
+.provider('$tooltip', ['$uibTooltipProvider', function($uibTooltipProvider) {
+ angular.extend(this, $uibTooltipProvider);
+
+ this.$get = ['$log', '$tooltipSuppressWarning', '$injector', function($log, $tooltipSuppressWarning, $injector) {
+ if (!$tooltipSuppressWarning) {
+ $log.warn('$tooltip is now deprecated. Use $uibTooltip instead.');
+ }
+
+ return $injector.invoke($uibTooltipProvider.$get);
+ }];
+}])
+
+// This is mostly ngInclude code but with a custom scope
+.directive('tooltipTemplateTransclude', [
+ '$animate', '$sce', '$compile', '$templateRequest', '$log', '$tooltipSuppressWarning',
+function ($animate , $sce , $compile , $templateRequest, $log, $tooltipSuppressWarning) {
+ return {
+ link: function(scope, elem, attrs) {
+ if (!$tooltipSuppressWarning) {
+ $log.warn('tooltip-template-transclude is now deprecated. Use uib-tooltip-template-transclude instead.');
+ }
+
+ 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);
+ }
+ };
}])
-/*
-Deprecated
-*/
-.directive('tooltipHtmlUnsafePopup', function() {
+.directive('tooltipClasses', ['$log', '$tooltipSuppressWarning', function($log, $tooltipSuppressWarning) {
+ return {
+ restrict: 'A',
+ link: function(scope, element, attrs) {
+ if (!$tooltipSuppressWarning) {
+ $log.warn('tooltip-classes is now deprecated. Use uib-tooltip-classes instead.');
+ }
+
+ if (scope.placement) {
+ element.addClass(scope.placement);
+ }
+ if (scope.popupClass) {
+ element.addClass(scope.popupClass);
+ }
+ if (scope.animation()) {
+ element.addClass(attrs.tooltipAnimationClass);
+ }
+ }
+ };
+}])
+
+.directive('tooltipPopup', ['$log', '$tooltipSuppressWarning', function($log, $tooltipSuppressWarning) {
return {
- restrict: 'EA',
replace: true,
scope: { content: '@', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
- templateUrl: 'template/tooltip/tooltip-html-unsafe-popup.html'
+ templateUrl: 'template/tooltip/tooltip-popup.html',
+ link: function(scope, element) {
+ if (!$tooltipSuppressWarning) {
+ $log.warn('tooltip-popup is now deprecated. Use uib-tooltip-popup instead.');
+ }
+
+ element.addClass('tooltip');
+ }
};
-})
+}])
-.value('tooltipHtmlUnsafeSuppressDeprecated', false)
-.directive('tooltipHtmlUnsafe', [
- '$tooltip', 'tooltipHtmlUnsafeSuppressDeprecated', '$log',
-function($tooltip , tooltipHtmlUnsafeSuppressDeprecated , $log) {
- if (!tooltipHtmlUnsafeSuppressDeprecated) {
- $log.warn('tooltip-html-unsafe is now deprecated. Use tooltip-html or tooltip-template instead.');
- }
- return $tooltip('tooltipHtmlUnsafe', 'tooltip', 'mouseenter');
+.directive('tooltip', ['$tooltip', function($tooltip) {
+ return $tooltip('tooltip', 'tooltip', 'mouseenter');
+}])
+
+.directive('tooltipTemplatePopup', ['$log', '$tooltipSuppressWarning', function($log, $tooltipSuppressWarning) {
+ return {
+ replace: true,
+ scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&',
+ originScope: '&' },
+ templateUrl: 'template/tooltip/tooltip-template-popup.html',
+ link: function(scope, element) {
+ if (!$tooltipSuppressWarning) {
+ $log.warn('tooltip-template-popup is now deprecated. Use uib-tooltip-template-popup instead.');
+ }
+
+ element.addClass('tooltip');
+ }
+ };
+}])
+
+.directive('tooltipTemplate', ['$tooltip', function($tooltip) {
+ return $tooltip('tooltipTemplate', 'tooltip', 'mouseenter', {
+ useContentExp: true
+ });
+}])
+
+.directive('tooltipHtmlPopup', ['$log', '$tooltipSuppressWarning', function($log, $tooltipSuppressWarning) {
+ return {
+ replace: true,
+ scope: { contentExp: '&', placement: '@', popupClass: '@', animation: '&', isOpen: '&' },
+ templateUrl: 'template/tooltip/tooltip-html-popup.html',
+ link: function(scope, element) {
+ if (!$tooltipSuppressWarning) {
+ $log.warn('tooltip-html-popup is now deprecated. Use uib-tooltip-html-popup instead.');
+ }
+
+ element.addClass('tooltip');
+ }
+ };
+}])
+
+.directive('tooltipHtml', ['$tooltip', function($tooltip) {
+ return $tooltip('tooltipHtml', 'tooltip', 'mouseenter', {
+ useContentExp: true
+ });
}]);
diff --git a/src/transition/test/transition.spec.js b/src/transition/test/transition.spec.js
deleted file mode 100644
index aafe1bfb2a..0000000000
--- a/src/transition/test/transition.spec.js
+++ /dev/null
@@ -1,147 +0,0 @@
-
-describe('$transition', function() {
-
- // Work out if we are running IE
- var ie = (function(){
- var v = 3,
- div = document.createElement('div'),
- all = div.getElementsByTagName('i');
- do {
- div.innerHTML = '';
- } while(all[0]);
- return v > 4 ? v : undefined;
- }());
-
- var $transition, $timeout;
-
- beforeEach(module('ui.bootstrap.transition'));
-
- beforeEach(inject(function(_$transition_, _$timeout_) {
- $transition = _$transition_;
- $timeout = _$timeout_;
- }));
-
- it('returns our custom promise', function() {
- var element = angular.element('
');
- var promise = $transition(element, '');
- expect(promise.then).toEqual(jasmine.any(Function));
- expect(promise.cancel).toEqual(jasmine.any(Function));
- });
-
- it('changes the css if passed a string', function() {
- var element = angular.element('
');
- spyOn(element, 'addClass');
- $transition(element, 'triggerClass');
- $timeout.flush();
-
- expect(element.addClass).toHaveBeenCalledWith('triggerClass');
- });
-
- it('changes the style if passed an object', function() {
- var element = angular.element('
');
- var triggerStyle = { height: '11px' };
- spyOn(element, 'css');
- $transition(element, triggerStyle);
- $timeout.flush();
- expect(element.css).toHaveBeenCalledWith(triggerStyle);
- });
-
- it('calls the function if passed', function() {
- var element = angular.element('
');
- var triggerFunction = jasmine.createSpy('triggerFunction');
- $transition(element, triggerFunction);
- $timeout.flush();
- expect(triggerFunction).toHaveBeenCalledWith(element);
- });
-
- // Versions of Internet Explorer before version 10 do not have CSS transitions
- if ( !ie || ie > 9 ) {
- describe('transitionEnd event', function() {
- var element, triggerTransitionEnd;
-
- beforeEach(function() {
- element = angular.element('
');
- // Mock up the element.bind method
- spyOn(element, 'bind').and.callFake(function(element, handler) {
- // Store the handler to be used to simulate the end of the transition later
- triggerTransitionEnd = handler;
- });
- // Mock up the element.unbind method
- spyOn(element, 'unbind');
- });
-
- describe('transitionEndEventName', function() {
- it('should be a string ending with transitionend', function() {
- expect($transition.transitionEndEventName).toMatch(/transitionend$/i);
- });
- });
-
- describe('animationEndEventName', function() {
- it('should be a string ending with animationend', function() {
- expect($transition.animationEndEventName).toMatch(/animationend$/i);
- });
- });
-
- it('binds a transitionEnd handler to the element', function() {
- $transition(element, '');
- expect(element.bind).toHaveBeenCalledWith($transition.transitionEndEventName, jasmine.any(Function));
- });
-
- it('binds an animationEnd handler to the element if option is given', function() {
- $transition(element, '', {animation: true});
- expect(element.bind).toHaveBeenCalledWith($transition.animationEndEventName, jasmine.any(Function));
- });
-
- it('resolves the promise when the transitionEnd is triggered', function() {
- var resolutionHandler = jasmine.createSpy('resolutionHandler');
-
- // Run the transition
- $transition(element, '').then(resolutionHandler);
-
- // Simulate the end of transition event
- triggerTransitionEnd();
- $timeout.flush();
-
- expect(resolutionHandler).toHaveBeenCalledWith(element);
- });
-
- it('rejects the promise if transition is cancelled', function() {
- var rejectionHandler = jasmine.createSpy('rejectionHandler');
-
- var promise = $transition(element, '');
- promise.then(null, rejectionHandler);
-
- promise.cancel();
- inject(function($rootScope) {
- $rootScope.$digest();
- });
- expect(rejectionHandler).toHaveBeenCalledWith(jasmine.any(String));
- expect(element.unbind).toHaveBeenCalledWith($transition.transitionEndEventName, jasmine.any(Function));
- });
- });
- } else {
-
- describe('transitionEndEventName', function() {
- it('should be undefined', function() {
- expect($transition.transitionEndEventName).not.toBeDefined();
- });
- });
-
- it('does not bind a transitionEnd handler to the element', function() {
- var element = angular.element('
');
- spyOn(element, 'bind');
- $transition(element, '');
- expect(element.bind).not.toHaveBeenCalledWith($transition.transitionEndEventName, jasmine.any(Function));
- });
-
- it('resolves the promise', function() {
- var element = angular.element('
');
- var transitionEndHandler = jasmine.createSpy('transitionEndHandler');
- $transition(element, '').then(transitionEndHandler);
- $timeout.flush();
- expect(transitionEndHandler).toHaveBeenCalledWith(element);
- });
-
- }
-});
-
diff --git a/src/transition/transition.js b/src/transition/transition.js
deleted file mode 100644
index 490cdf7bee..0000000000
--- a/src/transition/transition.js
+++ /dev/null
@@ -1,89 +0,0 @@
-angular.module('ui.bootstrap.transition', [])
-
-.value('$transitionSuppressDeprecated', false)
-/**
- * $transition service provides a consistent interface to trigger CSS 3 transitions and to be informed when they complete.
- * @param {DOMElement} element The DOMElement that will be animated.
- * @param {string|object|function} trigger The thing that will cause the transition to start:
- * - As a string, it represents the css class to be added to the element.
- * - As an object, it represents a hash of style attributes to be applied to the element.
- * - As a function, it represents a function to be called that will cause the transition to occur.
- * @return {Promise} A promise that is resolved when the transition finishes.
- */
-.factory('$transition', [
- '$q', '$timeout', '$rootScope', '$log', '$transitionSuppressDeprecated',
-function($q , $timeout , $rootScope , $log , $transitionSuppressDeprecated) {
-
- if (!$transitionSuppressDeprecated) {
- $log.warn('$transition is now deprecated. Use $animate from ngAnimate instead.');
- }
-
- var $transition = function(element, trigger, options) {
- options = options || {};
- var deferred = $q.defer();
- var endEventName = $transition[options.animation ? 'animationEndEventName' : 'transitionEndEventName'];
-
- var transitionEndHandler = function(event) {
- $rootScope.$apply(function() {
- element.unbind(endEventName, transitionEndHandler);
- deferred.resolve(element);
- });
- };
-
- if (endEventName) {
- element.bind(endEventName, transitionEndHandler);
- }
-
- // Wrap in a timeout to allow the browser time to update the DOM before the transition is to occur
- $timeout(function() {
- if ( angular.isString(trigger) ) {
- element.addClass(trigger);
- } else if ( angular.isFunction(trigger) ) {
- trigger(element);
- } else if ( angular.isObject(trigger) ) {
- element.css(trigger);
- }
- //If browser does not support transitions, instantly resolve
- if ( !endEventName ) {
- deferred.resolve(element);
- }
- });
-
- // Add our custom cancel function to the promise that is returned
- // We can call this if we are about to run a new transition, which we know will prevent this transition from ending,
- // i.e. it will therefore never raise a transitionEnd event for that transition
- deferred.promise.cancel = function() {
- if ( endEventName ) {
- element.unbind(endEventName, transitionEndHandler);
- }
- deferred.reject('Transition cancelled');
- };
-
- return deferred.promise;
- };
-
- // Work out the name of the transitionEnd event
- var transElement = document.createElement('trans');
- var transitionEndEventNames = {
- 'WebkitTransition': 'webkitTransitionEnd',
- 'MozTransition': 'transitionend',
- 'OTransition': 'oTransitionEnd',
- 'transition': 'transitionend'
- };
- var animationEndEventNames = {
- 'WebkitTransition': 'webkitAnimationEnd',
- 'MozTransition': 'animationend',
- 'OTransition': 'oAnimationEnd',
- 'transition': 'animationend'
- };
- function findEndEventName(endEventNames) {
- for (var name in endEventNames){
- if (transElement.style[name] !== undefined) {
- return endEventNames[name];
- }
- }
- }
- $transition.transitionEndEventName = findEndEventName(transitionEndEventNames);
- $transition.animationEndEventName = findEndEventName(animationEndEventNames);
- return $transition;
-}]);
diff --git a/src/typeahead/docs/demo.html b/src/typeahead/docs/demo.html
index 3808bc854d..ff4dea90db 100644
--- a/src/typeahead/docs/demo.html
+++ b/src/typeahead/docs/demo.html
@@ -1,18 +1,64 @@
+
+
-
+
+
+
+
Static arrays
Model: {{selected | json}}
-
+
Asynchronous results
Model: {{asyncSelected | json}}
-
+
diff --git a/src/typeahead/docs/readme.md b/src/typeahead/docs/readme.md
index 71892ac6dd..6f48c7d74c 100644
--- a/src/typeahead/docs/readme.md
+++ b/src/typeahead/docs/readme.md
@@ -24,6 +24,9 @@ The typeahead directives provide several attributes:
* `typeahead-append-to-body`
_(Defaults: false)_ : Should the typeahead popup be appended to $body instead of the parent element?
+* `typeahead-append-to-element-id`
+ _(Defaults: false)_ : Should the typeahead popup be appended to an element id instead of the parent element?
+
* `typeahead-editable`
_(Defaults: true)_ :
Should it restrict model values to the ones selected from the popup only ?
@@ -60,6 +63,10 @@ The typeahead directives provide several attributes:
:
Set custom item template
+* `typeahead-popup-template-url`
+ _(Defaults: `template/typeahead/typeahead-popup.html`)_ :
+ Set custom popup template
+
* `typeahead-wait-ms`
_(Defaults: 0)_ :
Minimal wait time after last character typed before typeahead kicks-in
@@ -67,3 +74,7 @@ The typeahead directives provide several attributes:
* `typeahead-select-on-blur`
_(Defaults: false)_ :
On blur, select the currently highlighted match
+
+* `typeahead-focus-on-select`
+ _(Defaults: true) :
+ On selection, focus the input element the typeahead directive is associated with
diff --git a/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js b/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js
new file mode 100644
index 0000000000..de24319361
--- /dev/null
+++ b/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js
@@ -0,0 +1,16 @@
+describe('Security concerns', function() {
+ var highlightFilter, $sanitize, logSpy;
+
+ beforeEach(module('ui.bootstrap.typeahead', 'ngSanitize'));
+
+ beforeEach(inject(function (uibTypeaheadHighlightFilter, _$sanitize_, $log) {
+ highlightFilter = uibTypeaheadHighlightFilter;
+ $sanitize = _$sanitize_;
+ logSpy = spyOn($log, 'warn');
+ }));
+
+ it('should not call the $log service when ngSanitize is present', function() {
+ highlightFilter('before after', 'match');
+ expect(logSpy).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/typeahead/test/typeahead-highlight.spec.js b/src/typeahead/test/typeahead-highlight.spec.js
index 1a5d8b1c5d..c696587751 100644
--- a/src/typeahead/test/typeahead-highlight.spec.js
+++ b/src/typeahead/test/typeahead-highlight.spec.js
@@ -1,38 +1,80 @@
-describe('typeaheadHighlight', function() {
- var highlightFilter;
+describe('typeaheadHighlight', function () {
+
+ var highlightFilter, $log, $sce, logSpy;
beforeEach(module('ui.bootstrap.typeahead'));
- beforeEach(inject(function(typeaheadHighlightFilter) {
- highlightFilter = typeaheadHighlightFilter;
+
+ beforeEach(inject(function(_$log_, _$sce_) {
+ $log = _$log_;
+ $sce = _$sce_;
+ logSpy = spyOn($log, 'warn');
+ }));
+
+ beforeEach(inject(function(uibTypeaheadHighlightFilter) {
+ highlightFilter = uibTypeaheadHighlightFilter;
}));
it('should higlight a match', function() {
- expect(highlightFilter('before match after', 'match')).toEqual('before
match after');
+ expect($sce.getTrustedHtml(highlightFilter('before match after', 'match'))).toEqual('before
match after');
});
it('should higlight a match with mixed case', function() {
- expect(highlightFilter('before MaTch after', 'match')).toEqual('before
MaTch after');
+ expect($sce.getTrustedHtml(highlightFilter('before MaTch after', 'match'))).toEqual('before
MaTch after');
});
it('should higlight all matches', function() {
- expect(highlightFilter('before MaTch after match', 'match')).toEqual('before
MaTch after
match ');
+ expect($sce.getTrustedHtml(highlightFilter('before MaTch after match', 'match'))).toEqual('before
MaTch after
match ');
});
it('should do nothing if no match', function() {
- expect(highlightFilter('before match after', 'nomatch')).toEqual('before match after');
+ expect($sce.getTrustedHtml(highlightFilter('before match after', 'nomatch'))).toEqual('before match after');
});
it('should do nothing if no or empty query', function() {
- expect(highlightFilter('before match after', '')).toEqual('before match after');
- expect(highlightFilter('before match after', null)).toEqual('before match after');
- expect(highlightFilter('before match after', undefined)).toEqual('before match after');
+ expect($sce.getTrustedHtml(highlightFilter('before match after', ''))).toEqual('before match after');
+ expect($sce.getTrustedHtml(highlightFilter('before match after', null))).toEqual('before match after');
+ expect($sce.getTrustedHtml(highlightFilter('before match after', undefined))).toEqual('before match after');
});
it('issue 316 - should work correctly for regexp reserved words', function() {
- expect(highlightFilter('before (match after', '(match')).toEqual('before
(match after');
+ expect($sce.getTrustedHtml(highlightFilter('before (match after', '(match'))).toEqual('before
(match after');
});
it('issue 1777 - should work correctly with numeric values', function() {
- expect(highlightFilter(123, '2')).toEqual('1
2 3');
+ expect($sce.getTrustedHtml(highlightFilter(123, '2'))).toEqual('1
2 3');
+ });
+
+ it('should show a warning when this component is being used unsafely', function() {
+ highlightFilter('
before match after', 'match');
+ expect(logSpy).toHaveBeenCalled();
+ });
+});
+
+/* Deprecation tests below */
+
+describe('typeahead highlightFilter deprecated', function(){
+ var highlightFilter, $log, $sce, logSpy;
+
+ beforeEach(module('ui.bootstrap.typeahead'));
+
+ it('should supress the warning by default', function(){
+ module(function($provide) {
+ $provide.value('$typeaheadSuppressWarning', true);
+ });
+
+ inject(function($compile, $log, $rootScope, typeaheadHighlightFilter, $sce){
+ spyOn($log, 'warn');
+ var highlightFilter = typeaheadHighlightFilter;
+ $sce.getTrustedHtml(highlightFilter('before match after', 'match'));
+ expect($log.warn.calls.count()).toBe(0);
+ });
});
+
+ it('should decrecate typeaheadHighlightFilter', inject(function($compile, $log, $rootScope, typeaheadHighlightFilter, $sce){
+ spyOn($log, 'warn');
+ var highlightFilter = typeaheadHighlightFilter;
+ $sce.getTrustedHtml(highlightFilter('before match after', 'match'));
+ expect($log.warn.calls.count()).toBe(1);
+ expect($log.warn.calls.argsFor(0)).toEqual(['typeaheadHighlight is now deprecated. Use uibTypeaheadHighlight instead.']);
+ }));
});
diff --git a/src/typeahead/test/typeahead-popup.spec.js b/src/typeahead/test/typeahead-popup.spec.js
index 5b2b94ae3c..63936609f0 100644
--- a/src/typeahead/test/typeahead-popup.spec.js
+++ b/src/typeahead/test/typeahead-popup.spec.js
@@ -14,7 +14,7 @@ describe('typeaheadPopup - result rendering', function() {
scope.matches = ['foo', 'bar', 'baz'];
scope.active = 1;
- var el = $compile('
')(scope);
+ var el = $compile('
')(scope);
$rootScope.$digest();
var liElems = el.find('li');
@@ -28,7 +28,7 @@ describe('typeaheadPopup - result rendering', function() {
scope.matches = ['foo', 'bar', 'baz'];
scope.active = 1;
- var el = $compile('
')(scope);
+ var el = $compile('
')(scope);
$rootScope.$digest();
var liElems = el.find('li');
@@ -47,7 +47,7 @@ describe('typeaheadPopup - result rendering', function() {
$rootScope.select = angular.noop;
spyOn($rootScope, 'select');
- var el = $compile('
')(scope);
+ var el = $compile('
')(scope);
$rootScope.$digest();
var liElems = el.find('li');
@@ -55,3 +55,47 @@ describe('typeaheadPopup - result rendering', function() {
expect($rootScope.select).toHaveBeenCalledWith(2);
});
});
+
+/* Deprecation tests below */
+
+describe('typeaheadPopup deprecation', function() {
+ beforeEach(module('ui.bootstrap.typeahead'));
+ beforeEach(module('ngSanitize'));
+ beforeEach(module('template/typeahead/typeahead-popup.html'));
+ beforeEach(module('template/typeahead/typeahead-match.html'));
+
+ it('should suppress warning', function() {
+ module(function($provide) {
+ $provide.value('$typeaheadSuppressWarning', true);
+ });
+
+ inject(function($compile, $log, $rootScope) {
+ var scope = $rootScope.$new();
+ scope.matches = ['foo', 'bar', 'baz'];
+ scope.active = 1;
+ $rootScope.select = angular.noop;
+ spyOn($log, 'warn');
+
+ var element = '
';
+ element = $compile(element)(scope);
+ $rootScope.$digest();
+ expect($log.warn.calls.count()).toBe(0);
+ });
+ });
+
+ it('should give warning by default', inject(function($compile, $log, $rootScope) {
+ var scope = $rootScope.$new();
+ scope.matches = ['foo', 'bar', 'baz'];
+ scope.active = 1;
+ $rootScope.select = angular.noop;
+ spyOn($log, 'warn');
+
+ var element = '
';
+ element = $compile(element)(scope);
+
+ $rootScope.$digest();
+
+ expect($log.warn.calls.count()).toBe(1);
+ expect($log.warn.calls.argsFor(0)).toEqual(['typeahead-popup is now deprecated. Use uib-typeahead-popup instead.']);
+ }));
+});
\ No newline at end of file
diff --git a/src/typeahead/test/typeahead.spec.js b/src/typeahead/test/typeahead.spec.js
index 92a75de3f4..a410abba39 100644
--- a/src/typeahead/test/typeahead.spec.js
+++ b/src/typeahead/test/typeahead.spec.js
@@ -1,8 +1,9 @@
describe('typeahead tests', function() {
- var $scope, $compile, $document, $timeout;
+ var $scope, $compile, $document, $templateCache, $timeout;
var changeInputValueTo;
beforeEach(module('ui.bootstrap.typeahead'));
+ beforeEach(module('ngSanitize'));
beforeEach(module('template/typeahead/typeahead-popup.html'));
beforeEach(module('template/typeahead/typeahead-match.html'));
beforeEach(module(function($compileProvider) {
@@ -24,7 +25,7 @@ describe('typeahead tests', function() {
};
});
}));
- beforeEach(inject(function(_$rootScope_, _$compile_, _$document_, _$timeout_, $sniffer) {
+ beforeEach(inject(function(_$rootScope_, _$compile_, _$document_, _$templateCache_, _$timeout_, $sniffer) {
$scope = _$rootScope_;
$scope.source = ['foo', 'bar', 'baz'];
$scope.states = [
@@ -33,6 +34,7 @@ describe('typeahead tests', function() {
];
$compile = _$compile_;
$document = _$document_;
+ $templateCache = _$templateCache_;
$timeout = _$timeout_;
changeInputValueTo = function(element, value) {
var inputEl = findInput(element);
@@ -123,14 +125,14 @@ describe('typeahead tests', function() {
//coarse grained, "integration" tests
describe('initial state and model changes', function() {
it('should be closed by default', function() {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
expect(element).toBeClosed();
});
it('should correctly render initial state if the "as" keyword is used', function() {
$scope.result = $scope.states[0];
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
var inputEl = findInput(element);
expect(inputEl.val()).toEqual('Alaska');
@@ -139,14 +141,14 @@ describe('typeahead tests', function() {
it('should default to bound model for initial rendering if there is not enough info to render label', function() {
$scope.result = $scope.states[0].code;
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
var inputEl = findInput(element);
expect(inputEl.val()).toEqual('AL');
});
it('should not get open on model change', function() {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
$scope.$apply(function () {
$scope.result = 'foo';
});
@@ -156,7 +158,7 @@ describe('typeahead tests', function() {
describe('basic functionality', function() {
it('should open and close typeahead based on matches', function() {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
var inputEl = findInput(element);
var ownsId = inputEl.attr('aria-owns');
@@ -178,7 +180,7 @@ describe('typeahead tests', function() {
});
it('should allow expressions over multiple lines', function() {
- var element = prepareInputEl('
');
changeInputValueTo(element, 'ba');
expect(element).toBeOpenWithActive(2, 0);
@@ -188,7 +190,7 @@ describe('typeahead tests', function() {
});
it('should not open typeahead if input value smaller than a defined threshold', function() {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'b');
expect(element).toBeClosed();
});
@@ -197,7 +199,7 @@ describe('typeahead tests', function() {
$scope.updaterFn = function(selectedItem) {
return 'prefix' + selectedItem;
};
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'f');
triggerKeyDown(element, 13);
expect($scope.result).toEqual('prefixfoo');
@@ -208,20 +210,20 @@ describe('typeahead tests', function() {
return 'prefix' + sourceItem;
};
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'fo');
var matchHighlight = findMatches(element).find('a').html();
expect(matchHighlight).toEqual('prefix
fo o');
});
it('should by default bind view value to model even if not part of matches', function() {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'not in matches');
expect($scope.result).toEqual('not in matches');
});
it('should support the editable property to limit model bindings to matches only', function() {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'not in matches');
expect($scope.result).toEqual(undefined);
});
@@ -229,7 +231,7 @@ describe('typeahead tests', function() {
it('should set validation errors for non-editable inputs', function() {
var element = prepareInputEl(
'
');
changeInputValueTo(element, 'not in matches');
@@ -245,7 +247,7 @@ describe('typeahead tests', function() {
it('should not set editable validation error for empty input', function() {
var element = prepareInputEl(
'
');
changeInputValueTo(element, 'not in matches');
@@ -264,7 +266,7 @@ describe('typeahead tests', function() {
}, 1000);
};
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'foo');
expect($scope.isLoading).toBeTruthy();
@@ -273,7 +275,7 @@ describe('typeahead tests', function() {
}));
it('should support timeout before trying to match $viewValue', inject(function($timeout) {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'foo');
expect(element).toBeClosed();
@@ -287,7 +289,7 @@ describe('typeahead tests', function() {
values.push(viewValue);
return $scope.source;
};
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'first');
changeInputValueTo(element, 'second');
@@ -303,7 +305,7 @@ describe('typeahead tests', function() {
values.push(viewValue);
return $scope.source;
};
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'first');
$timeout.flush();
@@ -316,31 +318,41 @@ describe('typeahead tests', function() {
expect(values).toContain('second');
}));
- it('should support custom templates for matched items', inject(function($templateCache) {
+ it('should support custom popup templates', function() {
+ $templateCache.put('custom.html', '
foo
');
+
+ var element = prepareInputEl('
');
+
+ changeInputValueTo(element, 'Al');
+
+ expect(element.find('.custom').text()).toBe('foo');
+ });
+
+ it('should support custom templates for matched items', function() {
$templateCache.put('custom.html', '
{{ index }} {{ match.label }}
');
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'Al');
expect(findMatches(element).eq(0).find('p').text()).toEqual('0 Alaska');
- }));
+ });
- it('should support directives which require controllers in custom templates for matched items', inject(function($templateCache) {
+ it('should support directives which require controllers in custom templates for matched items', function() {
$templateCache.put('custom.html', '
{{ index }} {{ match.label }}
');
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
element.data('$parentDirectiveController', {});
changeInputValueTo(element, 'Al');
expect(findMatches(element).eq(0).find('p').text()).toEqual('0 Alaska');
- }));
+ });
it('should throw error on invalid expression', function() {
var prepareInvalidDir = function() {
- prepareInputEl('
');
+ prepareInputEl('
');
};
expect(prepareInvalidDir).toThrow();
});
@@ -348,7 +360,7 @@ describe('typeahead tests', function() {
describe('selecting a match', function() {
it('should select a match on enter', function() {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'b');
@@ -360,7 +372,7 @@ describe('typeahead tests', function() {
});
it('should select a match on tab', function() {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'b');
@@ -372,7 +384,7 @@ describe('typeahead tests', function() {
});
it('should not select any match on blur without \'select-on-blur=true\' option', function() {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'b');
@@ -384,7 +396,7 @@ describe('typeahead tests', function() {
});
it('should select a match on blur with \'select-on-blur=true\' option', function() {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'b');
@@ -396,7 +408,7 @@ describe('typeahead tests', function() {
});
it('should select match on click', function() {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'b');
@@ -416,7 +428,7 @@ describe('typeahead tests', function() {
$scope.$model = $model;
$scope.$label = $label;
};
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'Alas');
triggerKeyDown(element, 13);
@@ -428,7 +440,7 @@ describe('typeahead tests', function() {
});
it('should correctly update inputs value on mapping where label is not derived from the model', function() {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'Alas');
@@ -446,7 +458,7 @@ describe('typeahead tests', function() {
}, 1000);
};
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'foo');
expect($scope.isNoResults).toBeFalsy();
@@ -462,61 +474,77 @@ describe('typeahead tests', function() {
}, 1000);
};
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'foo');
expect($scope.isNoResults).toBeFalsy();
$timeout.flush();
expect($scope.isNoResults).toBeFalsy();
}));
+
+ it('should not focus the input if `typeahead-focus-on-select` is false', function() {
+ var element = prepareInputEl('
');
+ $document.find('body').append(element);
+ var inputEl = findInput(element);
+
+ changeInputValueTo(element, 'b');
+ var match = $(findMatches(element)[1]).find('a')[0];
+
+ $(match).click();
+ $scope.$digest();
+ $timeout.flush();
+
+ expect(document.activeElement).not.toBe(inputEl[0]);
+ expect($scope.result).toEqual('baz');
+ });
});
-
+
describe('select on exact match', function() {
it('should select on an exact match when set', function() {
$scope.onSelect = jasmine.createSpy('onSelect');
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'bar');
-
+
expect($scope.result).toEqual('bar');
expect(inputEl.val()).toEqual('bar');
expect(element).toBeClosed();
expect($scope.onSelect).toHaveBeenCalled();
});
-
+
it('should not select on an exact match by default', function() {
$scope.onSelect = jasmine.createSpy('onSelect');
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
var inputEl = findInput(element);
-
+
changeInputValueTo(element, 'bar');
-
+
expect($scope.result).toBeUndefined();
expect(inputEl.val()).toEqual('bar');
expect($scope.onSelect.calls.any()).toBe(false);
});
-
+
it('should not be case sensitive when select on an exact match', function() {
$scope.onSelect = jasmine.createSpy('onSelect');
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'BaR');
-
+
expect($scope.result).toEqual('bar');
expect(inputEl.val()).toEqual('bar');
expect(element).toBeClosed();
expect($scope.onSelect).toHaveBeenCalled();
});
-
+
it('should not auto select when not a match with one potential result left', function() {
$scope.onSelect = jasmine.createSpy('onSelect');
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'fo');
-
+
expect($scope.result).toBeUndefined();
expect(inputEl.val()).toEqual('fo');
expect($scope.onSelect.calls.any()).toBe(false);
@@ -527,7 +555,7 @@ describe('typeahead tests', function() {
var element;
beforeEach(function() {
- element = prepareInputEl('
');
+ element = prepareInputEl('
');
});
it('should activate prev/next matches on up/down keys', function() {
@@ -577,7 +605,7 @@ describe('typeahead tests', function() {
$scope.source = function() {
return deferred.promise;
};
- element = prepareInputEl('
');
+ element = prepareInputEl('
');
}));
it('should display matches from promise', function() {
@@ -611,7 +639,7 @@ describe('typeahead tests', function() {
describe('non-regressions tests', function() {
it('issue 231 - closes matches popup on click outside typeahead', function() {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'b');
@@ -622,13 +650,13 @@ describe('typeahead tests', function() {
});
it('issue 591 - initial formatting for un-selected match and complex label expression', function() {
- var inputEl = findInput(prepareInputEl('
'));
+ var inputEl = findInput(prepareInputEl('
'));
expect(inputEl.val()).toEqual('');
});
it('issue 786 - name of internal model should not conflict with scope model name', function() {
$scope.state = $scope.states[0];
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
var inputEl = findInput(element);
expect(inputEl.val()).toEqual('Alaska');
@@ -636,7 +664,7 @@ describe('typeahead tests', function() {
it('issue 863 - it should work correctly with input type="email"', function() {
$scope.emails = ['foo@host.com', 'bar@host.com'];
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'bar');
@@ -654,7 +682,7 @@ describe('typeahead tests', function() {
return [viewValue];
});
};
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'match');
@@ -672,7 +700,7 @@ describe('typeahead tests', function() {
return [viewValue];
});
};
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'match');
@@ -690,7 +718,7 @@ describe('typeahead tests', function() {
return [viewValue];
});
};
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'match');
$scope.$digest();
@@ -710,7 +738,7 @@ describe('typeahead tests', function() {
values.push(viewValue);
return $scope.source;
};
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'match');
changeInputValueTo(element, 'm');
@@ -724,7 +752,7 @@ describe('typeahead tests', function() {
var element;
it('does not close matches popup on click in input', function() {
- element = prepareInputEl('
');
+ element = prepareInputEl('
');
var inputEl = findInput(element);
// Note that this bug can only be found when element is in the document
@@ -739,7 +767,7 @@ describe('typeahead tests', function() {
});
it('issue #1773 - should not trigger an error when used with ng-focus', function() {
- element = prepareInputEl('
');
+ element = prepareInputEl('
');
var inputEl = findInput(element);
// Note that this bug can only be found when element is in the document
@@ -762,7 +790,7 @@ describe('typeahead tests', function() {
return ['foo', 'bar'];
};
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'bar');
expect(element).toBeOpenWithActive(2, 0);
@@ -771,7 +799,7 @@ describe('typeahead tests', function() {
it('issue #3318 - should set model validity to true when set manually', function() {
var element = prepareInputEl(
'
');
changeInputValueTo(element, 'not in matches');
@@ -784,7 +812,7 @@ describe('typeahead tests', function() {
});
it('issue #3166 - should set \'parse\' key as valid when selecting a perfect match and not editable', function() {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
var inputEl = findInput(element);
changeInputValueTo(element, 'Alaska');
@@ -792,13 +820,26 @@ describe('typeahead tests', function() {
expect($scope.test.typeahead.$error.parse).toBeUndefined();
});
+
+ it('issue #3823 - should support ng-model-options getterSetter', function() {
+ function resultSetter(state) {
+ return state;
+ }
+ $scope.result = resultSetter;
+ var element = prepareInputEl('
');
+
+ changeInputValueTo(element, 'Alaska');
+ triggerKeyDown(element, 13);
+
+ expect($scope.result).toBe(resultSetter);
+ });
});
describe('input formatting', function() {
it('should co-operate with existing formatters', function() {
$scope.result = $scope.states[0];
- var element = prepareInputEl('
'),
+ var element = prepareInputEl('
'),
inputEl = findInput(element);
expect(inputEl.val()).toEqual('formatted' + $scope.result.name);
@@ -810,7 +851,7 @@ describe('typeahead tests', function() {
return $model.code;
};
- var element = prepareInputEl('
'),
+ var element = prepareInputEl('
'),
inputEl = findInput(element);
expect(inputEl.val()).toEqual('AL');
@@ -818,21 +859,31 @@ describe('typeahead tests', function() {
});
});
+ describe('append to element id', function() {
+ it('append typeahead results to element', function() {
+ $document.find('body').append('
');
+ var element = prepareInputEl('
');
+ changeInputValueTo(element, 'al');
+ expect($document.find('#myElement')).toBeOpenWithActive(2, 0);
+ $document.find('#myElement').remove();
+ });
+ });
+
describe('append to body', function() {
it('append typeahead results to body', function() {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'ba');
expect($document.find('body')).toBeOpenWithActive(2, 0);
});
it('should not append to body when value of the attribute is false', function() {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'ba');
expect(findDropDown($document.find('body')).length).toEqual(0);
});
it('should have right position after scroll', function() {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
var dropdown = findDropDown($document.find('body'));
var body = angular.element(document.body);
@@ -858,7 +909,7 @@ describe('typeahead tests', function() {
describe('focus first', function() {
it('should focus the first element by default', function() {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'b');
expect(element).toBeOpenWithActive(2, 0);
@@ -880,7 +931,7 @@ describe('typeahead tests', function() {
});
it('should not focus the first element until keys are pressed', function() {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'b');
expect(element).toBeOpenWithActive(2, -1);
@@ -920,7 +971,7 @@ describe('typeahead tests', function() {
$scope.onSelect = function($item, $model, $label) {
$scope.select_count = $scope.select_count + 1;
};
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'b');
// enter key should not be captured when nothing is focused
@@ -940,7 +991,7 @@ describe('typeahead tests', function() {
$scope.onSelect = function($item, $model, $label) {
$scope.select_count = $scope.select_count + 1;
};
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, 'b');
// down key should be captured and focus first element
@@ -956,10 +1007,57 @@ describe('typeahead tests', function() {
describe('minLength set to 0', function() {
it('should open typeahead if input is changed to empty string if defined threshold is 0', function() {
- var element = prepareInputEl('
');
+ var element = prepareInputEl('
');
changeInputValueTo(element, '');
-
expect(element).toBeOpenWithActive(3, 0);
});
});
});
+
+/* Deprecation tests below */
+
+describe('typeahead deprecation', function() {
+ beforeEach(module('ui.bootstrap.typeahead'));
+ beforeEach(module('ngSanitize'));
+ beforeEach(module('template/typeahead/typeahead-popup.html'));
+ beforeEach(module('template/typeahead/typeahead-match.html'));
+
+ it('should suppress warning', function() {
+ module(function($provide) {
+ $provide.value('$typeaheadSuppressWarning', true);
+ });
+
+ inject(function($compile, $log, $rootScope) {
+ spyOn($log, 'warn');
+
+ var element = '
';
+ element = $compile(element)($rootScope);
+ $rootScope.$digest();
+ expect($log.warn.calls.count()).toBe(0);
+ });
+ });
+
+ it('should give warning by default', inject(function($compile, $log, $rootScope) {
+ spyOn($log, 'warn');
+
+ var element = '
';
+ element = $compile(element)($rootScope);
+ $rootScope.$digest();
+
+ expect($log.warn.calls.count()).toBe(3);
+ expect($log.warn.calls.argsFor(0)).toEqual(['typeaheadParser is now deprecated. Use uibTypeaheadParser instead.']);
+ expect($log.warn.calls.argsFor(1)).toEqual(['typeahead is now deprecated. Use uib-typeahead instead.']);
+ expect($log.warn.calls.argsFor(2)).toEqual(['typeahead-popup is now deprecated. Use uib-typeahead-popup instead.']);
+ }));
+
+ it('should deprecate typeaheadMatch', inject(function($compile, $log, $rootScope, $templateCache, $sniffer){
+ spyOn($log, 'warn');
+
+ var element = '
';
+ element = $compile(element)($rootScope);
+ $rootScope.$digest();
+
+ expect($log.warn.calls.count()).toBe(1);
+ expect($log.warn.calls.argsFor(0)).toEqual(['typeahead-match is now deprecated. Use uib-typeahead-match instead.']);
+ }));
+});
\ No newline at end of file
diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js
index db661ba0ca..12be26dc59 100644
--- a/src/typeahead/typeahead.js
+++ b/src/typeahead/typeahead.js
@@ -1,42 +1,556 @@
-angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml'])
+angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position'])
/**
* A helper service that can parse typeahead's syntax (string provided by users)
* Extracted to a separate service for ease of unit testing
*/
- .factory('typeaheadParser', ['$parse', function($parse) {
-
- // 00000111000000000000022200000000000000003333333333333330000000000044000
- var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;
-
- return {
- parse: function(input) {
- var match = input.match(TYPEAHEAD_REGEXP);
- if (!match) {
- throw new Error(
- 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' +
- ' but got "' + input + '".');
- }
+ .factory('uibTypeaheadParser', ['$parse', function($parse) {
+ // 00000111000000000000022200000000000000003333333333333330000000000044000
+ var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;
+ return {
+ parse: function(input) {
+ var match = input.match(TYPEAHEAD_REGEXP);
+ if (!match) {
+ throw new Error(
+ 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' +
+ ' but got "' + input + '".');
+ }
- return {
- itemName:match[3],
- source:$parse(match[4]),
- viewMapper:$parse(match[2] || match[1]),
- modelMapper:$parse(match[1])
- };
- }
- };
-}])
+ return {
+ itemName: match[3],
+ source: $parse(match[4]),
+ viewMapper: $parse(match[2] || match[1]),
+ modelMapper: $parse(match[1])
+ };
+ }
+ };
+ }])
- .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$position', 'typeaheadParser',
- function($compile, $parse, $q, $timeout, $document, $window, $rootScope, $position, typeaheadParser) {
+ .controller('UibTypeaheadController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$uibPosition', 'uibTypeaheadParser',
+ function(originalScope, element, attrs, $compile, $parse, $q, $timeout, $document, $window, $rootScope, $position, typeaheadParser) {
var HOT_KEYS = [9, 13, 27, 38, 40];
var eventDebounceTime = 200;
+ var modelCtrl, ngModelOptions;
+ //SUPPORTED ATTRIBUTES (OPTIONS)
+
+ //minimal no of characters that needs to be entered before typeahead kicks-in
+ var minLength = originalScope.$eval(attrs.typeaheadMinLength);
+ if (!minLength && minLength !== 0) {
+ minLength = 1;
+ }
+
+ //minimal wait time after last character typed before typeahead kicks-in
+ var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
+
+ //should it restrict model values to the ones selected from the popup only?
+ var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
+
+ //binding to a variable that indicates if matches are being retrieved asynchronously
+ var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
+
+ //a callback executed when a match is selected
+ var onSelectCallback = $parse(attrs.typeaheadOnSelect);
+
+ //should it select highlighted popup value when losing focus?
+ var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false;
+
+ //binding to a variable that indicates if there were no results after the query is completed
+ var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop;
+
+ var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
+
+ var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false;
+
+ var appendToElementId = attrs.typeaheadAppendToElementId || false;
+
+ var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false;
+
+ //If input matches an item of the list exactly, select it automatically
+ var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false;
+
+ //INTERNAL VARIABLES
+
+ //model setter executed upon match selection
+ var parsedModel = $parse(attrs.ngModel);
+ var invokeModelSetter = $parse(attrs.ngModel + '($$$p)');
+ var $setModelValue = function(scope, newValue) {
+ if (angular.isFunction(parsedModel(originalScope)) &&
+ ngModelOptions && ngModelOptions.$options && ngModelOptions.$options.getterSetter) {
+ return invokeModelSetter(scope, {$$$p: newValue});
+ } else {
+ return parsedModel.assign(scope, newValue);
+ }
+ };
+
+ //expressions used by typeahead
+ var parserResult = typeaheadParser.parse(attrs.uibTypeahead);
+
+ var hasFocus;
+
+ //Used to avoid bug in iOS webview where iOS keyboard does not fire
+ //mousedown & mouseup events
+ //Issue #3699
+ var selected;
+
+ //create a child scope for the typeahead directive so we are not polluting original scope
+ //with typeahead-specific data (matches, query etc.)
+ var scope = originalScope.$new();
+ var offDestroy = originalScope.$on('$destroy', function() {
+ scope.$destroy();
+ });
+ scope.$on('$destroy', offDestroy);
+
+ // WAI-ARIA
+ var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
+ element.attr({
+ 'aria-autocomplete': 'list',
+ 'aria-expanded': false,
+ 'aria-owns': popupId
+ });
+
+ //pop-up element used to display matches
+ var popUpEl = angular.element('
');
+ popUpEl.attr({
+ id: popupId,
+ matches: 'matches',
+ active: 'activeIdx',
+ select: 'select(activeIdx)',
+ 'move-in-progress': 'moveInProgress',
+ query: 'query',
+ position: 'position'
+ });
+ //custom item template
+ if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
+ popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
+ }
+
+ if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) {
+ popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl);
+ }
+
+ var resetMatches = function() {
+ scope.matches = [];
+ scope.activeIdx = -1;
+ element.attr('aria-expanded', false);
+ };
+
+ var getMatchId = function(index) {
+ return popupId + '-option-' + index;
+ };
+
+ // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead.
+ // This attribute is added or removed automatically when the `activeIdx` changes.
+ scope.$watch('activeIdx', function(index) {
+ if (index < 0) {
+ element.removeAttr('aria-activedescendant');
+ } else {
+ element.attr('aria-activedescendant', getMatchId(index));
+ }
+ });
+
+ var inputIsExactMatch = function(inputValue, index) {
+ if (scope.matches.length > index && inputValue) {
+ return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase();
+ }
+
+ return false;
+ };
+
+ var getMatchesAsync = function(inputValue) {
+ var locals = {$viewValue: inputValue};
+ isLoadingSetter(originalScope, true);
+ isNoResultsSetter(originalScope, false);
+ $q.when(parserResult.source(originalScope, locals)).then(function(matches) {
+ //it might happen that several async queries were in progress if a user were typing fast
+ //but we are interested only in responses that correspond to the current view value
+ var onCurrentRequest = (inputValue === modelCtrl.$viewValue);
+ if (onCurrentRequest && hasFocus) {
+ if (matches && matches.length > 0) {
+ scope.activeIdx = focusFirst ? 0 : -1;
+ isNoResultsSetter(originalScope, false);
+ scope.matches.length = 0;
+
+ //transform labels
+ for (var i = 0; i < matches.length; i++) {
+ locals[parserResult.itemName] = matches[i];
+ scope.matches.push({
+ id: getMatchId(i),
+ label: parserResult.viewMapper(scope, locals),
+ model: matches[i]
+ });
+ }
+
+ scope.query = inputValue;
+ //position pop-up with matches - we need to re-calculate its position each time we are opening a window
+ //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
+ //due to other elements being rendered
+ recalculatePosition();
+
+ element.attr('aria-expanded', true);
+
+ //Select the single remaining option if user input matches
+ if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) {
+ scope.select(0);
+ }
+ } else {
+ resetMatches();
+ isNoResultsSetter(originalScope, true);
+ }
+ }
+ if (onCurrentRequest) {
+ isLoadingSetter(originalScope, false);
+ }
+ }, function() {
+ resetMatches();
+ isLoadingSetter(originalScope, false);
+ isNoResultsSetter(originalScope, true);
+ });
+ };
+
+ // bind events only if appendToBody params exist - performance feature
+ if (appendToBody) {
+ angular.element($window).bind('resize', fireRecalculating);
+ $document.find('body').bind('scroll', fireRecalculating);
+ }
+
+ // Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
+ var timeoutEventPromise;
+
+ // Default progress type
+ scope.moveInProgress = false;
+
+ function fireRecalculating() {
+ if (!scope.moveInProgress) {
+ scope.moveInProgress = true;
+ scope.$digest();
+ }
+ // Cancel previous timeout
+ if (timeoutEventPromise) {
+ $timeout.cancel(timeoutEventPromise);
+ }
+
+ // Debounced executing recalculate after events fired
+ timeoutEventPromise = $timeout(function() {
+ // if popup is visible
+ if (scope.matches.length) {
+ recalculatePosition();
+ }
+
+ scope.moveInProgress = false;
+ }, eventDebounceTime);
+ }
+
+ // recalculate actual position and set new values to scope
+ // after digest loop is popup in right position
+ function recalculatePosition() {
+ scope.position = appendToBody ? $position.offset(element) : $position.position(element);
+ scope.position.top += element.prop('offsetHeight');
+ }
+
+ //we need to propagate user's query so we can higlight matches
+ scope.query = undefined;
+
+ //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
+ var timeoutPromise;
+
+ var scheduleSearchWithTimeout = function(inputValue) {
+ timeoutPromise = $timeout(function() {
+ getMatchesAsync(inputValue);
+ }, waitTime);
+ };
+
+ var cancelPreviousTimeout = function() {
+ if (timeoutPromise) {
+ $timeout.cancel(timeoutPromise);
+ }
+ };
+
+ resetMatches();
+
+ scope.select = function(activeIdx) {
+ //called from within the $digest() cycle
+ var locals = {};
+ var model, item;
+
+ selected = true;
+ locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
+ model = parserResult.modelMapper(originalScope, locals);
+ $setModelValue(originalScope, model);
+ modelCtrl.$setValidity('editable', true);
+ modelCtrl.$setValidity('parse', true);
+
+ onSelectCallback(originalScope, {
+ $item: item,
+ $model: model,
+ $label: parserResult.viewMapper(originalScope, locals)
+ });
+
+ resetMatches();
+
+ //return focus to the input element if a match was selected via a mouse click event
+ // use timeout to avoid $rootScope:inprog error
+ if (scope.$eval(attrs.typeaheadFocusOnSelect) !== false) {
+ $timeout(function() { element[0].focus(); }, 0, false);
+ }
+ };
+
+ //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
+ element.bind('keydown', function(evt) {
+ //typeahead is open and an "interesting" key was pressed
+ if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
+ return;
+ }
+
+ // if there's nothing selected (i.e. focusFirst) and enter or tab is hit, clear the results
+ if (scope.activeIdx === -1 && (evt.which === 9 || evt.which === 13)) {
+ resetMatches();
+ scope.$digest();
+ return;
+ }
+
+ evt.preventDefault();
+
+ if (evt.which === 40) {
+ scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
+ scope.$digest();
+ } else if (evt.which === 38) {
+ scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1;
+ scope.$digest();
+ } else if (evt.which === 13 || evt.which === 9) {
+ scope.$apply(function () {
+ scope.select(scope.activeIdx);
+ });
+ } else if (evt.which === 27) {
+ evt.stopPropagation();
+
+ resetMatches();
+ scope.$digest();
+ }
+ });
+
+ element.bind('blur', function() {
+ if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) {
+ selected = true;
+ scope.$apply(function() {
+ scope.select(scope.activeIdx);
+ });
+ }
+ hasFocus = false;
+ selected = false;
+ });
+
+ // Keep reference to click handler to unbind it.
+ var dismissClickHandler = function(evt) {
+ // Issue #3973
+ // Firefox treats right click as a click on document
+ if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) {
+ resetMatches();
+ if (!$rootScope.$$phase) {
+ scope.$digest();
+ }
+ }
+ };
+
+ $document.bind('click', dismissClickHandler);
+
+ originalScope.$on('$destroy', function() {
+ $document.unbind('click', dismissClickHandler);
+ if (appendToBody || appendToElementId) {
+ $popup.remove();
+ }
+ // Prevent jQuery cache memory leak
+ popUpEl.remove();
+ });
+
+ var $popup = $compile(popUpEl)(scope);
+
+ if (appendToBody) {
+ $document.find('body').append($popup);
+ } else if (appendToElementId !== false) {
+ angular.element($document[0].getElementById(appendToElementId)).append($popup);
+ } else {
+ element.after($popup);
+ }
+
+ this.init = function(_modelCtrl, _ngModelOptions) {
+ modelCtrl = _modelCtrl;
+ ngModelOptions = _ngModelOptions;
+
+ //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
+ //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
+ modelCtrl.$parsers.unshift(function(inputValue) {
+ hasFocus = true;
+
+ if (minLength === 0 || inputValue && inputValue.length >= minLength) {
+ if (waitTime > 0) {
+ cancelPreviousTimeout();
+ scheduleSearchWithTimeout(inputValue);
+ } else {
+ getMatchesAsync(inputValue);
+ }
+ } else {
+ isLoadingSetter(originalScope, false);
+ cancelPreviousTimeout();
+ resetMatches();
+ }
+
+ if (isEditable) {
+ return inputValue;
+ } else {
+ if (!inputValue) {
+ // Reset in case user had typed something previously.
+ modelCtrl.$setValidity('editable', true);
+ return null;
+ } else {
+ modelCtrl.$setValidity('editable', false);
+ return undefined;
+ }
+ }
+ });
+
+ modelCtrl.$formatters.push(function(modelValue) {
+ var candidateViewValue, emptyViewValue;
+ var locals = {};
+
+ // The validity may be set to false via $parsers (see above) if
+ // the model is restricted to selected values. If the model
+ // is set manually it is considered to be valid.
+ if (!isEditable) {
+ modelCtrl.$setValidity('editable', true);
+ }
+
+ if (inputFormatter) {
+ locals.$model = modelValue;
+ return inputFormatter(originalScope, locals);
+ } else {
+ //it might happen that we don't have enough info to properly render input value
+ //we need to check for this situation and simply return model value if we can't apply custom formatting
+ locals[parserResult.itemName] = modelValue;
+ candidateViewValue = parserResult.viewMapper(originalScope, locals);
+ locals[parserResult.itemName] = undefined;
+ emptyViewValue = parserResult.viewMapper(originalScope, locals);
+
+ return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue;
+ }
+ });
+ };
+ }])
+
+ .directive('uibTypeahead', function() {
+ return {
+ controller: 'UibTypeaheadController',
+ require: ['ngModel', '^?ngModelOptions', 'uibTypeahead'],
+ link: function(originalScope, element, attrs, ctrls) {
+ ctrls[2].init(ctrls[0], ctrls[1]);
+ }
+ };
+ })
+
+ .directive('uibTypeaheadPopup', function() {
+ return {
+ scope: {
+ matches: '=',
+ query: '=',
+ active: '=',
+ position: '&',
+ moveInProgress: '=',
+ select: '&'
+ },
+ replace: true,
+ templateUrl: function(element, attrs) {
+ return attrs.popupTemplateUrl || 'template/typeahead/typeahead-popup.html';
+ },
+ link: function(scope, element, attrs) {
+ scope.templateUrl = attrs.templateUrl;
+
+ scope.isOpen = function() {
+ return scope.matches.length > 0;
+ };
+
+ scope.isActive = function(matchIdx) {
+ return scope.active == matchIdx;
+ };
+
+ scope.selectActive = function(matchIdx) {
+ scope.active = matchIdx;
+ };
+
+ scope.selectMatch = function(activeIdx) {
+ scope.select({activeIdx:activeIdx});
+ };
+ }
+ };
+ })
+
+ .directive('uibTypeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) {
return {
- require: 'ngModel',
- link: function(originalScope, element, attrs, modelCtrl) {
+ scope: {
+ index: '=',
+ match: '=',
+ query: '='
+ },
+ link:function(scope, element, attrs) {
+ var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html';
+ $templateRequest(tplUrl).then(function(tplContent) {
+ $compile(tplContent.trim())(scope, function(clonedElement) {
+ element.replaceWith(clonedElement);
+ });
+ });
+ }
+ };
+ }])
+
+ .filter('uibTypeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) {
+ var isSanitizePresent;
+ isSanitizePresent = $injector.has('$sanitize');
+ function escapeRegexp(queryToEscape) {
+ // Regex: capture the whole query string and replace it with the string that will be used to match
+ // the results, for example if the capture is "a" the result will be \a
+ return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
+ }
+
+ function containsHtml(matchItem) {
+ return /<.*>/g.test(matchItem);
+ }
+
+ return function(matchItem, query) {
+ if (!isSanitizePresent && containsHtml(matchItem)) {
+ $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger
+ }
+ matchItem = query? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '
$& ') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag
+ if (!isSanitizePresent) {
+ matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive
+ }
+ return matchItem;
+ };
+ }]);
+
+/* Deprecated typeahead below */
+
+angular.module('ui.bootstrap.typeahead')
+ .value('$typeaheadSuppressWarning', false)
+ .service('typeaheadParser', ['$parse', 'uibTypeaheadParser', '$log', '$typeaheadSuppressWarning', function($parse, uibTypeaheadParser, $log, $typeaheadSuppressWarning) {
+ if (!$typeaheadSuppressWarning) {
+ $log.warn('typeaheadParser is now deprecated. Use uibTypeaheadParser instead.');
+ }
+
+ return uibTypeaheadParser;
+ }])
+
+ .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$uibPosition', 'typeaheadParser', '$log', '$typeaheadSuppressWarning',
+ function($compile, $parse, $q, $timeout, $document, $window, $rootScope, $position, typeaheadParser, $log, $typeaheadSuppressWarning) {
+ var HOT_KEYS = [9, 13, 27, 38, 40];
+ var eventDebounceTime = 200;
+ return {
+ require: ['ngModel', '^?ngModelOptions'],
+ link: function(originalScope, element, attrs, ctrls) {
+ if (!$typeaheadSuppressWarning) {
+ $log.warn('typeahead is now deprecated. Use uib-typeahead instead.');
+ }
+ var modelCtrl = ctrls[0];
+ var ngModelOptions = ctrls[1];
//SUPPORTED ATTRIBUTES (OPTIONS)
//minimal no of characters that needs to be entered before typeahead kicks-in
@@ -67,6 +581,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false;
+ var appendToElementId = attrs.typeaheadAppendToElementId || false;
+
var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false;
//If input matches an item of the list exactly, select it automatically
@@ -75,7 +591,16 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
//INTERNAL VARIABLES
//model setter executed upon match selection
- var $setModelValue = $parse(attrs.ngModel).assign;
+ var parsedModel = $parse(attrs.ngModel);
+ var invokeModelSetter = $parse(attrs.ngModel + '($$$p)');
+ var $setModelValue = function(scope, newValue) {
+ if (angular.isFunction(parsedModel(originalScope)) &&
+ ngModelOptions && ngModelOptions.$options && ngModelOptions.$options.getterSetter) {
+ return invokeModelSetter(scope, {$$$p: newValue});
+ } else {
+ return parsedModel.assign(scope, newValue);
+ }
+ };
//expressions used by typeahead
var parserResult = typeaheadParser.parse(attrs.typeahead);
@@ -90,9 +615,10 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
//create a child scope for the typeahead directive so we are not polluting original scope
//with typeahead-specific data (matches, query etc.)
var scope = originalScope.$new();
- originalScope.$on('$destroy', function() {
- scope.$destroy();
+ var offDestroy = originalScope.$on('$destroy', function() {
+ scope.$destroy();
});
+ scope.$on('$destroy', offDestroy);
// WAI-ARIA
var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
@@ -118,6 +644,10 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
}
+ if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) {
+ popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl);
+ }
+
var resetMatches = function() {
scope.matches = [];
scope.activeIdx = -1;
@@ -156,13 +686,12 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
var onCurrentRequest = (inputValue === modelCtrl.$viewValue);
if (onCurrentRequest && hasFocus) {
if (matches && matches.length > 0) {
-
scope.activeIdx = focusFirst ? 0 : -1;
isNoResultsSetter(originalScope, false);
scope.matches.length = 0;
//transform labels
- for(var i=0; i
0 ? scope.activeIdx : scope.matches.length) - 1;
scope.$digest();
-
} else if (evt.which === 13 || evt.which === 9) {
scope.$apply(function () {
scope.select(scope.activeIdx);
});
-
} else if (evt.which === 27) {
evt.stopPropagation();
@@ -408,7 +934,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
originalScope.$on('$destroy', function() {
$document.unbind('click', dismissClickHandler);
- if (appendToBody) {
+ if (appendToBody || appendToElementId) {
$popup.remove();
}
// Prevent jQuery cache memory leak
@@ -419,17 +945,17 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
if (appendToBody) {
$document.find('body').append($popup);
+ } else if (appendToElementId !== false) {
+ angular.element($document[0].getElementById(appendToElementId)).append($popup);
} else {
element.after($popup);
}
}
};
-
}])
-
- .directive('typeaheadPopup', function() {
+
+ .directive('typeaheadPopup', ['$typeaheadSuppressWarning', '$log', function($typeaheadSuppressWarning, $log) {
return {
- restrict: 'EA',
scope: {
matches: '=',
query: '=',
@@ -439,8 +965,14 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
select: '&'
},
replace: true,
- templateUrl: 'template/typeahead/typeahead-popup.html',
+ templateUrl: function(element, attrs) {
+ return attrs.popupTemplateUrl || 'template/typeahead/typeahead-popup.html';
+ },
link: function(scope, element, attrs) {
+
+ if (!$typeaheadSuppressWarning) {
+ $log.warn('typeahead-popup is now deprecated. Use uib-typeahead-popup instead.');
+ }
scope.templateUrl = attrs.templateUrl;
scope.isOpen = function() {
@@ -460,9 +992,9 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
};
}
};
- })
-
- .directive('typeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) {
+ }])
+
+ .directive('typeaheadMatch', ['$templateRequest', '$compile', '$parse', '$typeaheadSuppressWarning', '$log', function($templateRequest, $compile, $parse, $typeaheadSuppressWarning, $log) {
return {
restrict: 'EA',
scope: {
@@ -471,6 +1003,10 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
query: '='
},
link:function(scope, element, attrs) {
+ if (!$typeaheadSuppressWarning) {
+ $log.warn('typeahead-match is now deprecated. Use uib-typeahead-match instead.');
+ }
+
var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-match.html';
$templateRequest(tplUrl).then(function(tplContent) {
$compile(tplContent.trim())(scope, function(clonedElement) {
@@ -480,13 +1016,35 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap
}
};
}])
+
+ .filter('typeaheadHighlight', ['$sce', '$injector', '$log', '$typeaheadSuppressWarning', function($sce, $injector, $log, $typeaheadSuppressWarning) {
+ var isSanitizePresent;
+ isSanitizePresent = $injector.has('$sanitize');
- .filter('typeaheadHighlight', function() {
function escapeRegexp(queryToEscape) {
+ // Regex: capture the whole query string and replace it with the string that will be used to match
+ // the results, for example if the capture is "a" the result will be \a
return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
}
+ function containsHtml(matchItem) {
+ return /<.*>/g.test(matchItem);
+ }
+
return function(matchItem, query) {
- return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$& ') : matchItem;
+ if (!$typeaheadSuppressWarning) {
+ $log.warn('typeaheadHighlight is now deprecated. Use uibTypeaheadHighlight instead.');
+ }
+
+ if (!isSanitizePresent && containsHtml(matchItem)) {
+ $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger
+ }
+
+ matchItem = query? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$& ') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag
+ if (!isSanitizePresent) {
+ matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive
+ }
+
+ return matchItem;
};
- });
+ }]);
diff --git a/template/accordion/accordion-group.html b/template/accordion/accordion-group.html
index 0b6274e233..1dc4c8e785 100644
--- a/template/accordion/accordion-group.html
+++ b/template/accordion/accordion-group.html
@@ -1,10 +1,10 @@
-
-
+
+
-
diff --git a/template/alert/alert.html b/template/alert/alert.html
index fc46b61cd3..0885587d98 100644
--- a/template/alert/alert.html
+++ b/template/alert/alert.html
@@ -1,5 +1,5 @@
-
+
×
Close
diff --git a/template/carousel/carousel.html b/template/carousel/carousel.html
index 6577b73f45..372c5473b7 100644
--- a/template/carousel/carousel.html
+++ b/template/carousel/carousel.html
@@ -1,8 +1,16 @@
+
+
+
+ previous
+
+
+
+ next
+
+
+
+ slide {{ $index + 1 }} of {{ slides.length }}, currently active
+
+
+
\ No newline at end of file
diff --git a/template/datepicker/datepicker.html b/template/datepicker/datepicker.html
index 1ecb3c50b4..d515832e53 100644
--- a/template/datepicker/datepicker.html
+++ b/template/datepicker/datepicker.html
@@ -1,5 +1,5 @@
-
-
-
+
+
+
\ No newline at end of file
diff --git a/template/datepicker/popup.html b/template/datepicker/popup.html
index 13b29a155f..e3dda67f28 100644
--- a/template/datepicker/popup.html
+++ b/template/datepicker/popup.html
@@ -1,8 +1,8 @@
-