From dd53ed375b630810585a5a2c1f79e2b3923e6d8a Mon Sep 17 00:00:00 2001 From: bob Date: Tue, 6 Oct 2015 20:40:12 -0700 Subject: [PATCH] feat(typeahead): add uib- prefix --- src/typeahead/docs/demo.html | 12 +- .../typeahead-highlight-ngsanitize.spec.js | 4 +- .../test/typeahead-highlight.spec.js | 31 +- src/typeahead/test/typeahead-popup.spec.js | 48 +- src/typeahead/test/typeahead.spec.js | 172 +++--- src/typeahead/typeahead.js | 533 +++++++++++++++++- template/typeahead/typeahead-match.html | 2 +- template/typeahead/typeahead-popup.html | 2 +- 8 files changed, 719 insertions(+), 85 deletions(-) diff --git a/src/typeahead/docs/demo.html b/src/typeahead/docs/demo.html index ab633f65b7..ff4dea90db 100644 --- a/src/typeahead/docs/demo.html +++ b/src/typeahead/docs/demo.html @@ -29,7 +29,7 @@ @@ -44,7 +44,7 @@ @@ -54,11 +54,11 @@

Static arrays

Model: {{selected | json}}
- +

Asynchronous results

Model: {{asyncSelected | json}}
- +
No Results Found @@ -66,9 +66,9 @@

Asynchronous results

Custom templates for results

Model: {{customSelected | json}}
- +

Custom popup templates for typeahead's dropdown

Model: {{customPopupSelected | json}}
- +
diff --git a/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js b/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js index 346a4132b3..de24319361 100644 --- a/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js +++ b/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js @@ -3,8 +3,8 @@ describe('Security concerns', function() { beforeEach(module('ui.bootstrap.typeahead', 'ngSanitize')); - beforeEach(inject(function (typeaheadHighlightFilter, _$sanitize_, $log) { - highlightFilter = typeaheadHighlightFilter; + beforeEach(inject(function (uibTypeaheadHighlightFilter, _$sanitize_, $log) { + highlightFilter = uibTypeaheadHighlightFilter; $sanitize = _$sanitize_; logSpy = spyOn($log, 'warn'); })); diff --git a/src/typeahead/test/typeahead-highlight.spec.js b/src/typeahead/test/typeahead-highlight.spec.js index e587ee43c0..97fd5e94e3 100644 --- a/src/typeahead/test/typeahead-highlight.spec.js +++ b/src/typeahead/test/typeahead-highlight.spec.js @@ -10,8 +10,8 @@ describe('typeaheadHighlight', function () { logSpy = spyOn($log, 'warn'); })); - beforeEach(inject(function(typeaheadHighlightFilter) { - highlightFilter = typeaheadHighlightFilter; + beforeEach(inject(function(uibTypeaheadHighlightFilter) { + highlightFilter = uibTypeaheadHighlightFilter; })); it('should higlight a match', function() { @@ -49,3 +49,30 @@ describe('typeaheadHighlight', function () { expect(logSpy).toHaveBeenCalled(); }); }); + +describe('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..8e24bc705f 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,45 @@ describe('typeaheadPopup - result rendering', function() { expect($rootScope.select).toHaveBeenCalledWith(2); }); }); + +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 50fa8777a1..14cb082355 100644 --- a/src/typeahead/test/typeahead.spec.js +++ b/src/typeahead/test/typeahead.spec.js @@ -125,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'); @@ -141,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'; }); @@ -158,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'); @@ -180,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); @@ -190,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(); }); @@ -199,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'); @@ -210,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('prefixfoo'); }); 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); }); @@ -231,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'); @@ -247,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'); @@ -266,7 +266,7 @@ describe('typeahead tests', function() { }, 1000); }; - var element = prepareInputEl('
'); + var element = prepareInputEl('
'); changeInputValueTo(element, 'foo'); expect($scope.isLoading).toBeTruthy(); @@ -275,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(); @@ -289,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'); @@ -305,7 +305,7 @@ describe('typeahead tests', function() { values.push(viewValue); return $scope.source; }; - var element = prepareInputEl('
'); + var element = prepareInputEl('
'); changeInputValueTo(element, 'first'); $timeout.flush(); @@ -321,7 +321,7 @@ describe('typeahead tests', function() { it('should support custom popup templates', function() { $templateCache.put('custom.html', '
foo
'); - var element = prepareInputEl('
'); + var element = prepareInputEl('
'); changeInputValueTo(element, 'Al'); @@ -331,7 +331,7 @@ describe('typeahead tests', function() { 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'); @@ -341,7 +341,7 @@ describe('typeahead tests', function() { 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', {}); @@ -352,7 +352,7 @@ describe('typeahead tests', function() { it('should throw error on invalid expression', function() { var prepareInvalidDir = function() { - prepareInputEl('
'); + prepareInputEl('
'); }; expect(prepareInvalidDir).toThrow(); }); @@ -360,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'); @@ -372,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'); @@ -384,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'); @@ -396,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'); @@ -408,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'); @@ -428,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); @@ -440,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'); @@ -458,7 +458,7 @@ describe('typeahead tests', function() { }, 1000); }; - var element = prepareInputEl('
'); + var element = prepareInputEl('
'); changeInputValueTo(element, 'foo'); expect($scope.isNoResults).toBeFalsy(); @@ -474,7 +474,7 @@ describe('typeahead tests', function() { }, 1000); }; - var element = prepareInputEl('
'); + var element = prepareInputEl('
'); changeInputValueTo(element, 'foo'); expect($scope.isNoResults).toBeFalsy(); @@ -483,7 +483,7 @@ describe('typeahead tests', function() { })); it('should not focus the input if `typeahead-focus-on-select` is false', function() { - var element = prepareInputEl('
'); + var element = prepareInputEl('
'); $document.find('body').append(element); var inputEl = findInput(element); @@ -502,7 +502,7 @@ describe('typeahead tests', function() { 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'); @@ -515,7 +515,7 @@ describe('typeahead tests', function() { 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'); @@ -527,7 +527,7 @@ describe('typeahead tests', function() { 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'); @@ -540,7 +540,7 @@ describe('typeahead tests', function() { 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'); @@ -555,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() { @@ -605,7 +605,7 @@ describe('typeahead tests', function() { $scope.source = function() { return deferred.promise; }; - element = prepareInputEl('
'); + element = prepareInputEl('
'); })); it('should display matches from promise', function() { @@ -639,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'); @@ -650,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'); @@ -664,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'); @@ -682,7 +682,7 @@ describe('typeahead tests', function() { return [viewValue]; }); }; - var element = prepareInputEl('
'); + var element = prepareInputEl('
'); var inputEl = findInput(element); changeInputValueTo(element, 'match'); @@ -700,7 +700,7 @@ describe('typeahead tests', function() { return [viewValue]; }); }; - var element = prepareInputEl('
'); + var element = prepareInputEl('
'); var inputEl = findInput(element); changeInputValueTo(element, 'match'); @@ -718,7 +718,7 @@ describe('typeahead tests', function() { return [viewValue]; }); }; - var element = prepareInputEl('
'); + var element = prepareInputEl('
'); changeInputValueTo(element, 'match'); $scope.$digest(); @@ -738,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'); @@ -752,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 @@ -767,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 @@ -790,7 +790,7 @@ describe('typeahead tests', function() { return ['foo', 'bar']; }; - var element = prepareInputEl('
'); + var element = prepareInputEl('
'); changeInputValueTo(element, 'bar'); expect(element).toBeOpenWithActive(2, 0); @@ -799,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'); @@ -812,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'); @@ -826,7 +826,7 @@ describe('typeahead tests', function() { return state; } $scope.result = resultSetter; - var element = prepareInputEl('
'); + var element = prepareInputEl('
'); changeInputValueTo(element, 'Alaska'); triggerKeyDown(element, 13); @@ -839,7 +839,7 @@ describe('typeahead tests', 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); @@ -851,7 +851,7 @@ describe('typeahead tests', function() { return $model.code; }; - var element = prepareInputEl('
'), + var element = prepareInputEl('
'), inputEl = findInput(element); expect(inputEl.val()).toEqual('AL'); @@ -862,7 +862,7 @@ describe('typeahead tests', function() { describe('append to element id', function() { it('append typeahead results to element', function() { $document.find('body').append('
'); - var element = prepareInputEl('
'); + var element = prepareInputEl('
'); changeInputValueTo(element, 'al'); expect($document.find('#myElement')).toBeOpenWithActive(2, 0); $document.find('#myElement').remove(); @@ -871,19 +871,19 @@ describe('typeahead tests', function() { 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); @@ -909,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); @@ -931,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); @@ -971,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 @@ -991,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 @@ -1007,10 +1007,56 @@ 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); }); }); }); + + +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 18b42eccf2..0f82ee5bdf 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -4,11 +4,10 @@ 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) { + .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); @@ -28,7 +27,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) }; }]) - .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$uibPosition', 'typeaheadParser', + .directive('uibTypeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$uibPosition', 'uibTypeaheadParser', function($compile, $parse, $q, $timeout, $document, $window, $rootScope, $position, typeaheadParser) { var HOT_KEYS = [9, 13, 27, 38, 40]; var eventDebounceTime = 200; @@ -90,7 +89,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) }; //expressions used by typeahead - var parserResult = typeaheadParser.parse(attrs.typeahead); + var parserResult = typeaheadParser.parse(attrs.uibTypeahead); var hasFocus; @@ -442,7 +441,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) }]) - .directive('typeaheadPopup', function() { + .directive('uibTypeaheadPopup', function() { return { restrict: 'EA', scope: { @@ -479,7 +478,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) }; }) - .directive('typeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) { + .directive('uibTypeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) { return { restrict: 'EA', scope: { @@ -498,7 +497,522 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) }; }]) - .filter('typeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) { + .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; + }; + }]); + + 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.'); + } + angular.extend(this, 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 + 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.typeahead); + + 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'); + } + + resetMatches(); + + //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); + } + }; + + //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; + } + }); + + 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); + } + } + }; + + }]) + + .directive('typeaheadPopup', ['$typeaheadSuppressWarning', '$log', function($typeaheadSuppressWarning, $log) { + return { + restrict: 'EA', + 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) { + + if (!$typeaheadSuppressWarning) { + $log.warn('typeahead-popup is now deprecated. Use uib-typeahead-popup instead.'); + } + 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('typeaheadMatch', ['$templateRequest', '$compile', '$parse', '$typeaheadSuppressWarning', '$log', function($templateRequest, $compile, $parse, $typeaheadSuppressWarning, $log) { + return { + restrict: 'EA', + scope: { + index: '=', + match: '=', + 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) { + element.replaceWith(clonedElement); + }); + }); + } + }; + }]) + + .filter('typeaheadHighlight', ['$sce', '$injector', '$log', '$typeaheadSuppressWarning', function($sce, $injector, $log, $typeaheadSuppressWarning) { var isSanitizePresent; isSanitizePresent = $injector.has('$sanitize'); @@ -513,6 +1027,9 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) } return function(matchItem, query) { + 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 } @@ -523,3 +1040,5 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) return matchItem; }; }]); + + \ No newline at end of file diff --git a/template/typeahead/typeahead-match.html b/template/typeahead/typeahead-match.html index f8375988ca..c64462bc48 100644 --- a/template/typeahead/typeahead-match.html +++ b/template/typeahead/typeahead-match.html @@ -1 +1 @@ - + diff --git a/template/typeahead/typeahead-popup.html b/template/typeahead/typeahead-popup.html index 43693ac35e..20a362bd6c 100644 --- a/template/typeahead/typeahead-popup.html +++ b/template/typeahead/typeahead-popup.html @@ -1,5 +1,5 @@