From c7705262369a383a51ec200ab71bcdb4bf463d82 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Sat, 7 Dec 2013 01:29:28 -0800 Subject: [PATCH 01/18] refactor(typeahead): Move typeaheadEditable out Unfortunately, this depends on typeaheadEditable running after the post linking function of the typeahead directive. Note this and solve this later. --- src/typeahead/typeahead.js | 46 +++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 5dfecbcae7..2764815478 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -46,9 +46,6 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap //minimal wait time after last character typed before typehead 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; @@ -166,18 +163,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap resetMatches(); } - if (isEditable) { - return inputValue; - } else { - if (!inputValue) { - // Reset in case user had typed something previously. - modelCtrl.$setValidity('editable', true); - return inputValue; - } else { - modelCtrl.$setValidity('editable', false); - return undefined; - } - } + return inputValue; }); modelCtrl.$formatters.push(function (modelValue) { @@ -337,6 +323,36 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap }; }]) + .directive('typeaheadEditable', [ + function () { + return { + restrict: 'A', + require: 'ngModel', + link: function (scope, element, attrs, ngModelCtrl) { + + //should it restrict model values to the ones selected from the popup only? + var isEditable = scope.$eval(attrs.typeaheadEditable) !== false; + + // push instead of unshift as this has to come after the parser added by typeahead + ngModelCtrl.$parsers.push(function (inputValue) { + if (isEditable) { + return inputValue; + } else { + if (!inputValue) { + // Reset in case user had typed something previously. + ngModelCtrl.$setValidity('editable', true); + return inputValue; + } else { + ngModelCtrl.$setValidity('editable', false); + return undefined; + } + } + }); + } + }; + } + ]) + .filter('typeaheadHighlight', function() { function escapeRegexp(queryToEscape) { From b1eb54fe75b2f67e2bdd23d02a72119b258a3aaa Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Sun, 8 Dec 2013 23:23:20 -0800 Subject: [PATCH 02/18] feat(typeahead): Add typeaheadController Maintain feature parity. Some duplicate code. --- src/typeahead/typeahead.js | 174 ++++++++++++++++++++++--------------- 1 file changed, 102 insertions(+), 72 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 2764815478..0444140725 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -29,112 +29,142 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap }; }]) - .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser', - function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) { - - var HOT_KEYS = [9, 13, 27, 38, 40]; - - return { - require:'ngModel', - link:function (originalScope, element, attrs, modelCtrl) { + .controller('typeaheadController', [ + '$scope', '$compile', '$q', '$attrs', '$parse', '$element', '$position', 'typeaheadParser', + function ($scope , $compile , $q , $attrs , $parse , $element , $position , typeaheadParser) { - //SUPPORTED ATTRIBUTES (OPTIONS) - - //minimal no of characters that needs to be entered before typeahead kicks-in - var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; - - //minimal wait time after last character typed before typehead kicks-in - var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; + var ctrl = this; //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); - - var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; + var isLoadingSetter = $parse($attrs.typeaheadLoading).assign || angular.noop; - var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; + var appendToBody = $attrs.typeaheadAppendToBody ? $scope.$eval($attrs.typeaheadAppendToBody) : false; //INTERNAL VARIABLES - //model setter executed upon match selection - var $setModelValue = $parse(attrs.ngModel).assign; - //expressions used by typeahead - var parserResult = typeaheadParser.parse(attrs.typeahead); - - var hasFocus; - - //pop-up element used to display matches - var popUpEl = angular.element('
'); - popUpEl.attr({ - matches: 'matches', - active: 'activeIdx', - select: 'select(activeIdx)', - query: 'query', - position: 'position' - }); - //custom item template - if (angular.isDefined(attrs.typeaheadTemplateUrl)) { - popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); - } + var parserResult = typeaheadParser.parse($attrs.typeahead); //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 taScope = this.taScope = $scope.$new(); + taScope.typeaheadCtrl = this; - var resetMatches = function() { - scope.matches = []; - scope.activeIdx = -1; + ctrl.setQuery = function (query) { + ctrl.query = query; + if (!query && query !== '') { + resetMatches(); + return; + } + ctrl.getMatches(query); }; - var getMatchesAsync = function(inputValue) { + ctrl.getMatches = function(inputValue) { var locals = {$viewValue: inputValue}; - isLoadingSetter(originalScope, true); - $q.when(parserResult.source(originalScope, locals)).then(function(matches) { + isLoadingSetter($scope, true); + $q.when(parserResult.source($scope, 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 - if (inputValue === modelCtrl.$viewValue && hasFocus) { + if (inputValue === ctrl.query) { if (matches.length > 0) { - scope.activeIdx = 0; - scope.matches.length = 0; + taScope.activeIdx = 0; + taScope.matches.length = 0; //transform labels for(var i=0; i'); + popUpEl.attr({ + typeaheadCtrl: 'typeaheadCtrl', + matches: 'matches', + active: 'activeIdx', + select: 'select(activeIdx)', + query: 'query', + position: 'position' + }); + //custom item template + if (angular.isDefined($attrs.typeaheadTemplateUrl)) { + popUpEl.attr('template-url', $attrs.typeaheadTemplateUrl); + } + + function resetMatches() { + taScope.matches = []; + taScope.activeIdx = -1; + isLoadingSetter($scope, false); + } + + ctrl.popUpEl = $compile(popUpEl)(taScope); + } + ]) + + .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser', + function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) { + + var HOT_KEYS = [9, 13, 27, 38, 40]; + + return { + require: ['typeahead', 'ngModel'], + controller: 'typeaheadController', + link:function (originalScope, element, attrs, controllers) { + + var typeaheadCtrl = controllers[0], + modelCtrl = controllers[1], + scope = typeaheadCtrl.taScope; + + //SUPPORTED ATTRIBUTES (OPTIONS) + + //minimal no of characters that needs to be entered before typeahead kicks-in + var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; + + //minimal wait time after last character typed before typehead kicks-in + var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; + + //a callback executed when a match is selected + var onSelectCallback = $parse(attrs.typeaheadOnSelect); + + var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; + + var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; + + //INTERNAL VARIABLES + + //model setter executed upon match selection + var $setModelValue = $parse(attrs.ngModel).assign; + + //expressions used by typeahead + var parserResult = typeaheadParser.parse(attrs.typeahead); + //we need to propagate user's query so we can higlight matches scope.query = undefined; @@ -145,21 +175,18 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap //$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 (inputValue && inputValue.length >= minSearch) { if (waitTime > 0) { if (timeoutPromise) { $timeout.cancel(timeoutPromise);//cancel previous timeout } timeoutPromise = $timeout(function () { - getMatchesAsync(inputValue); + typeaheadCtrl.setQuery(inputValue); }, waitTime); } else { - getMatchesAsync(inputValue); + typeaheadCtrl.setQuery(inputValue); } } else { - isLoadingSetter(originalScope, false); resetMatches(); } @@ -242,8 +269,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap } }); - element.bind('blur', function (evt) { - hasFocus = false; + element.bind('blur', function () { + resetMatches(); }); // Keep reference to click handler to unbind it. @@ -260,11 +287,14 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap $document.unbind('click', dismissClickHandler); }); - var $popup = $compile(popUpEl)(scope); if ( appendToBody ) { - $document.find('body').append($popup); + $document.find('body').append(typeaheadCtrl.popUpEl); } else { - element.after($popup); + element.after(typeaheadCtrl.popUpEl); + } + + function resetMatches() { + typeaheadCtrl.setQuery(null); } } }; From d8dd02fd961a0066d1891ac6d67c4a619bbc3dc3 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Mon, 9 Dec 2013 01:08:18 -0800 Subject: [PATCH 03/18] refactor(typeahead): Remove extra calls to loading --- src/typeahead/typeahead.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 0444140725..b2e49792e5 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -89,15 +89,13 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap taScope.position = appendToBody ? $position.offset($element) : $position.position($element); taScope.position = $position.position($element); taScope.position.top = taScope.position.top + $element.prop('offsetHeight'); - + isLoadingSetter($scope, false); } else { resetMatches(); } - isLoadingSetter($scope, false); } }, function(){ resetMatches(); - isLoadingSetter($scope, false); }); }; From 1a2e371ec575f531e154c4b92c26b1f6dc8ffc87 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Mon, 9 Dec 2013 01:49:06 -0800 Subject: [PATCH 04/18] refactor(typeahead): Clean up select logic --- src/typeahead/typeahead.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index b2e49792e5..9c73282841 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -78,7 +78,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap locals[parserResult.itemName] = matches[i]; taScope.matches.push({ label: parserResult.viewMapper($scope, locals), - model: matches[i] + model: parserResult.modelMapper($scope, locals), + item: matches[i] }); } @@ -216,18 +217,18 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap scope.select = function (activeIdx) { //called from within the $digest() cycle - var locals = {}; - var model, item; + var selectedMatch = scope.matches[activeIdx], + item = selectedMatch.item, + model = selectedMatch.model, + label = selectedMatch.label; - locals[parserResult.itemName] = item = scope.matches[activeIdx].model; - model = parserResult.modelMapper(originalScope, locals); $setModelValue(originalScope, model); modelCtrl.$setValidity('editable', true); onSelectCallback(originalScope, { $item: item, $model: model, - $label: parserResult.viewMapper(originalScope, locals) + $label: label }); resetMatches(); From 77c8dcd76a91d522d5ab5a815f9c18303b3b3049 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Wed, 11 Dec 2013 00:54:36 -0800 Subject: [PATCH 05/18] feat(typeahead): Move select into controller - Add selectListeners --- src/typeahead/typeahead.js | 86 ++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 32 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 9c73282841..a61b054e6c 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -42,6 +42,9 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap //INTERNAL VARIABLES + //model setter executed upon match selection + var $setModelValue = $parse($attrs.ngModel).assign; + //expressions used by typeahead var parserResult = typeaheadParser.parse($attrs.typeahead); @@ -50,6 +53,9 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap var taScope = this.taScope = $scope.$new(); taScope.typeaheadCtrl = this; + // Called with model, itemDetails (model, {item, model, label}) + ctrl.selectListeners = []; + ctrl.setQuery = function (query) { ctrl.query = query; if (!query && query !== '') { @@ -100,6 +106,18 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap }); }; + ctrl.select = function (match) { + $setModelValue($scope, match.model); + resetMatches(); + + for (var i = 0; i < ctrl.selectListeners.length; i++) { + ctrl.selectListeners[i](match.model, match); + } + + //return focus to the input element if a mach was selected via a mouse click event + $element[0].focus(); + }; + resetMatches(); //pop-up element used to display matches @@ -108,7 +126,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap typeaheadCtrl: 'typeaheadCtrl', matches: 'matches', active: 'activeIdx', - select: 'select(activeIdx)', + select: 'typeaheadCtrl.select(matches[activeIdx])', query: 'query', position: 'position' }); @@ -149,18 +167,12 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap //minimal wait time after last character typed before typehead kicks-in var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; - //a callback executed when a match is selected - var onSelectCallback = $parse(attrs.typeaheadOnSelect); - var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; //INTERNAL VARIABLES - //model setter executed upon match selection - var $setModelValue = $parse(attrs.ngModel).assign; - //expressions used by typeahead var parserResult = typeaheadParser.parse(attrs.typeahead); @@ -215,28 +227,6 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap } }); - scope.select = function (activeIdx) { - //called from within the $digest() cycle - var selectedMatch = scope.matches[activeIdx], - item = selectedMatch.item, - model = selectedMatch.model, - label = selectedMatch.label; - - $setModelValue(originalScope, model); - modelCtrl.$setValidity('editable', true); - - onSelectCallback(originalScope, { - $item: item, - $model: model, - $label: label - }); - - resetMatches(); - - //return focus to the input element if a mach was selected via a mouse click event - element[0].focus(); - }; - //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) element.bind('keydown', function (evt) { @@ -257,7 +247,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap } else if (evt.which === 13 || evt.which === 9) { scope.$apply(function () { - scope.select(scope.activeIdx); + typeaheadCtrl.select(scope.matches[scope.activeIdx]); }); } else if (evt.which === 27) { @@ -352,12 +342,40 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap }; }]) + .directive('typeaheadOnSelect', [ + '$parse', + function ($parse) { + return { + restrict: 'A', + require: 'typeahead', + link: function (scope, element, attrs, typeaheadCtrl) { + + //a callback executed when a match is selected + var onSelectCallback = $parse(attrs.typeaheadOnSelect); + + typeaheadCtrl.selectListeners.push(function (model, match) { + onSelectCallback(scope, { + $item: match.item, + $model: match.model, + $label: match.label + }); + }); + + } + }; + } + ]) + + .directive('typeaheadEditable', [ function () { return { restrict: 'A', - require: 'ngModel', - link: function (scope, element, attrs, ngModelCtrl) { + require: ['ngModel', 'typeahead'], + link: function (scope, element, attrs, controllers) { + + var ngModelCtrl = controllers[0], + typeaheadCtrl = controllers[1]; //should it restrict model values to the ones selected from the popup only? var isEditable = scope.$eval(attrs.typeaheadEditable) !== false; @@ -377,6 +395,10 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap } } }); + + typeaheadCtrl.selectListeners.push(function () { + ngModelCtrl.$setValidity('editable', true); + }); } }; } From a7f0bc6117bd53b796f133352763589f9a7fbdb5 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Wed, 11 Dec 2013 01:08:55 -0800 Subject: [PATCH 06/18] feat(typeahead): Move scope variables into ctrl --- src/typeahead/typeahead.js | 50 +++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index a61b054e6c..15e3877ed1 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -56,8 +56,11 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap // Called with model, itemDetails (model, {item, model, label}) ctrl.selectListeners = []; + // Check this value to see if it's the latest query + var lastQuery = null; + ctrl.setQuery = function (query) { - ctrl.query = query; + lastQuery = query; if (!query && query !== '') { resetMatches(); return; @@ -73,29 +76,28 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap //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 - if (inputValue === ctrl.query) { + if (inputValue === lastQuery) { if (matches.length > 0) { - taScope.activeIdx = 0; - taScope.matches.length = 0; + ctrl.activeIdx = 0; + ctrl.matches.length = 0; //transform labels for(var i=0; i'); popUpEl.attr({ typeaheadCtrl: 'typeaheadCtrl', - matches: 'matches', - active: 'activeIdx', - select: 'typeaheadCtrl.select(matches[activeIdx])', - query: 'query', - position: 'position' + matches: 'typeaheadCtrl.matches', + active: 'typeaheadCtrl.activeIdx', + select: 'typeaheadCtrl.select(typeaheadCtrl.matches[activeIdx])', + query: 'typeaheadCtrl.query', + position: 'typeaheadCtrl.position' }); //custom item template if (angular.isDefined($attrs.typeaheadTemplateUrl)) { @@ -136,8 +138,9 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap } function resetMatches() { - taScope.matches = []; - taScope.activeIdx = -1; + lastQuery = null; + ctrl.matches = []; + ctrl.activeIdx = -1; isLoadingSetter($scope, false); } @@ -145,8 +148,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap } ]) - .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser', - function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser) { + .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', 'typeaheadParser', + function ($compile, $parse, $q, $timeout, $document, typeaheadParser) { var HOT_KEYS = [9, 13, 27, 38, 40]; @@ -176,9 +179,6 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap //expressions used by typeahead var parserResult = typeaheadParser.parse(attrs.typeahead); - //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; @@ -231,23 +231,23 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap 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) { + if (typeaheadCtrl.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { return; } evt.preventDefault(); if (evt.which === 40) { - scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; + typeaheadCtrl.activeIdx = (typeaheadCtrl.activeIdx + 1) % typeaheadCtrl.matches.length; scope.$digest(); } else if (evt.which === 38) { - scope.activeIdx = (scope.activeIdx ? scope.activeIdx : scope.matches.length) - 1; + typeaheadCtrl.activeIdx = (typeaheadCtrl.activeIdx ? typeaheadCtrl.activeIdx : typeaheadCtrl.matches.length) - 1; scope.$digest(); } else if (evt.which === 13 || evt.which === 9) { scope.$apply(function () { - typeaheadCtrl.select(scope.matches[scope.activeIdx]); + typeaheadCtrl.select(typeaheadCtrl.matches[typeaheadCtrl.activeIdx]); }); } else if (evt.which === 27) { From 8a3b1e9d216af5d2a981c510f5b92c301a1ab0d6 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Wed, 11 Dec 2013 01:21:07 -0800 Subject: [PATCH 07/18] feat(typeahead): Add convenience methods Right now these are private methods. --- src/typeahead/typeahead.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 15e3877ed1..213fb329b5 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -120,6 +120,21 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap $element[0].focus(); }; + ctrl._selectActive = function (activeIdx) { + if (typeof activeIdx != 'undefined') { + ctrl.activeIdx = activeIdx; + } + ctrl.select(ctrl.matches[ctrl.activeIdx]); + }; + + ctrl._nextMatch = function () { + ctrl.activeIdx = (ctrl.activeIdx + 1) % ctrl.matches.length; + }; + + ctrl._prevMatch = function () { + ctrl.activeIdx = (ctrl.activeIdx ? ctrl.activeIdx : ctrl.matches.length) - 1; + }; + resetMatches(); //pop-up element used to display matches @@ -128,7 +143,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap typeaheadCtrl: 'typeaheadCtrl', matches: 'typeaheadCtrl.matches', active: 'typeaheadCtrl.activeIdx', - select: 'typeaheadCtrl.select(typeaheadCtrl.matches[activeIdx])', + select: 'typeaheadCtrl._selectActive(activeIdx)', query: 'typeaheadCtrl.query', position: 'typeaheadCtrl.position' }); @@ -238,16 +253,16 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap evt.preventDefault(); if (evt.which === 40) { - typeaheadCtrl.activeIdx = (typeaheadCtrl.activeIdx + 1) % typeaheadCtrl.matches.length; + typeaheadCtrl._nextMatch(); scope.$digest(); } else if (evt.which === 38) { - typeaheadCtrl.activeIdx = (typeaheadCtrl.activeIdx ? typeaheadCtrl.activeIdx : typeaheadCtrl.matches.length) - 1; + typeaheadCtrl._prevMatch(); scope.$digest(); } else if (evt.which === 13 || evt.which === 9) { scope.$apply(function () { - typeaheadCtrl.select(typeaheadCtrl.matches[typeaheadCtrl.activeIdx]); + typeaheadCtrl._selectActive(); }); } else if (evt.which === 27) { From 1949507726c4d90afad15fa79434b478110a7079 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Wed, 11 Dec 2013 01:43:08 -0800 Subject: [PATCH 08/18] feat(typeahead): Add queryParsers queryParsers support promises, which can get interesting... --- src/typeahead/typeahead.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 213fb329b5..adfd71dd92 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -56,6 +56,10 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap // Called with model, itemDetails (model, {item, model, label}) ctrl.selectListeners = []; + // Methods that are called on query similar to ngModelController.$parsers but supports + // promises. + ctrl.queryParsers = []; + // Check this value to see if it's the latest query var lastQuery = null; @@ -65,10 +69,25 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap resetMatches(); return; } - ctrl.getMatches(query); + + var queryPromise = $q.when(query); + + for (var i = 0; i < ctrl.queryParsers.length; i++) { + queryPromise = queryPromise.then(ctrl.queryParsers[i]); + } + + queryPromise.then(function (value) { + // async query + if (query === lastQuery && typeof value !== 'undefined') { + ctrl.getMatches(value); + } + }); }; + var lastMatchInputValue = null; + ctrl.getMatches = function(inputValue) { + lastMatchInputValue = inputValue; var locals = {$viewValue: inputValue}; isLoadingSetter($scope, true); @@ -76,7 +95,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap //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 - if (inputValue === lastQuery) { + if (inputValue === lastMatchInputValue) { if (matches.length > 0) { ctrl.activeIdx = 0; @@ -154,6 +173,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap function resetMatches() { lastQuery = null; + lastMatchInputValue = null; ctrl.matches = []; ctrl.activeIdx = -1; isLoadingSetter($scope, false); From 56dd0b5e6df07d70b2580ec2e6a7931ae9548a75 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Wed, 11 Dec 2013 02:03:44 -0800 Subject: [PATCH 09/18] refactor(typeahead): Move out typeaheadMinLength Using query parsers, this can now be its own directive. --- src/typeahead/typeahead.js | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index adfd71dd92..fa101815bb 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -199,9 +199,6 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap //SUPPORTED ATTRIBUTES (OPTIONS) - //minimal no of characters that needs to be entered before typeahead kicks-in - var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; - //minimal wait time after last character typed before typehead kicks-in var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; @@ -221,7 +218,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue modelCtrl.$parsers.unshift(function (inputValue) { - if (inputValue && inputValue.length >= minSearch) { + if (inputValue) { if (waitTime > 0) { if (timeoutPromise) { $timeout.cancel(timeoutPromise);//cancel previous timeout @@ -377,6 +374,27 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap }; }]) + .directive('typeaheadMinLength', [ + function () { + return { + restrict: 'A', + require: 'typeahead', + link: function (scope, element, attrs, typeaheadCtrl) { + + //minimal no of characters that needs to be entered before typeahead kicks-in + var minSearch = scope.$eval(attrs.typeaheadMinLength) || 1; + + typeaheadCtrl.queryParsers.push(function (value) { + if (value.length >= minSearch) { + return value; + } + }); + + } + }; + } + ]) + .directive('typeaheadOnSelect', [ '$parse', function ($parse) { From 6839016583fc867806969bb803f31a04bcf1f35e Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Wed, 11 Dec 2013 02:08:58 -0800 Subject: [PATCH 10/18] refactor(typeahead): Move out waitMs logic And the magic of promises... --- src/typeahead/typeahead.js | 52 +++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index fa101815bb..b5b746e460 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -183,8 +183,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap } ]) - .directive('typeahead', ['$compile', '$parse', '$q', '$timeout', '$document', 'typeaheadParser', - function ($compile, $parse, $q, $timeout, $document, typeaheadParser) { + .directive('typeahead', ['$compile', '$parse', '$document', 'typeaheadParser', + function ($compile, $parse, $document, typeaheadParser) { var HOT_KEYS = [9, 13, 27, 38, 40]; @@ -199,9 +199,6 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap //SUPPORTED ATTRIBUTES (OPTIONS) - //minimal wait time after last character typed before typehead kicks-in - var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; - var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; @@ -211,24 +208,12 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap //expressions used by typeahead var parserResult = typeaheadParser.parse(attrs.typeahead); - //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later - var 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) { if (inputValue) { - if (waitTime > 0) { - if (timeoutPromise) { - $timeout.cancel(timeoutPromise);//cancel previous timeout - } - timeoutPromise = $timeout(function () { - typeaheadCtrl.setQuery(inputValue); - }, waitTime); - } else { - typeaheadCtrl.setQuery(inputValue); - } + typeaheadCtrl.setQuery(inputValue); } else { resetMatches(); } @@ -395,6 +380,37 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap } ]) + .directive('typeaheadWaitMs', [ + '$timeout', + function ($timeout) { + return { + restrict: 'A', + require: 'typeahead', + link: function (scope, element, attrs, typeaheadCtrl) { + + //minimal wait time after last character typed before typehead kicks-in + var waitTime = scope.$eval(attrs.typeaheadWaitMs) || 0; + + if (waitTime > 0) { + //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later + var timeoutPromise; + + typeaheadCtrl.queryParsers.push(function (value) { + if (timeoutPromise) { + $timeout.cancel(timeoutPromise);//cancel previous timeout + } + timeoutPromise = $timeout(function () { + return value; + }, waitTime); + return timeoutPromise; + }); + } + + } + }; + } + ]) + .directive('typeaheadOnSelect', [ '$parse', function ($parse) { From 628bc3b390b16bfb41707eccd78c110ab70e6ad1 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Wed, 8 Jan 2014 22:30:14 -0800 Subject: [PATCH 11/18] fix(typeahead): don't rely on order of $parsers (WIP) Add tests later for order of $parsers. --- src/typeahead/typeahead.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index b5b746e460..68f3bba7f0 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -208,9 +208,9 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap //expressions used by typeahead var parserResult = typeaheadParser.parse(attrs.typeahead); - //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) { + //plug into modelCtrl pipeline to open a typeahead on view changes + modelCtrl._$setViewValue = modelCtrl.$setViewValue; + modelCtrl.$setViewValue = function (inputValue) { if (inputValue) { typeaheadCtrl.setQuery(inputValue); @@ -218,8 +218,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap resetMatches(); } - return inputValue; - }); + return modelCtrl._$setViewValue(inputValue); + }; modelCtrl.$formatters.push(function (modelValue) { From 496c6512ff05e4350475b844a43bddd5ad831865 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Wed, 8 Jan 2014 22:45:34 -0800 Subject: [PATCH 12/18] refactor(typeahead): remove duplicate parserResult --- src/typeahead/typeahead.js | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 68f3bba7f0..445940e608 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -46,7 +46,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap var $setModelValue = $parse($attrs.ngModel).assign; //expressions used by typeahead - var parserResult = typeaheadParser.parse($attrs.typeahead); + ctrl.parserResult = typeaheadParser.parse($attrs.typeahead); //create a child scope for the typeahead directive so we are not polluting original scope //with typeahead-specific data (matches, query etc.) @@ -91,7 +91,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap var locals = {$viewValue: inputValue}; isLoadingSetter($scope, true); - $q.when(parserResult.source($scope, locals)).then(function(matches) { + $q.when(ctrl.parserResult.source($scope, 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 @@ -103,10 +103,10 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap //transform labels for(var i=0; i Date: Wed, 8 Jan 2014 22:13:28 -0800 Subject: [PATCH 13/18] refactor(typeahead): move out input formatter --- src/typeahead/typeahead.js | 52 +++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 445940e608..887f018553 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -53,6 +53,19 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap var taScope = this.taScope = $scope.$new(); taScope.typeaheadCtrl = this; + ctrl.inputFormatter = function (scope, locals) { + var candidateViewValue, emptyViewValue; + + //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[ctrl.parserResult.itemName] = locals.$model; + candidateViewValue = ctrl.parserResult.viewMapper(scope, locals); + locals[ctrl.parserResult.itemName] = undefined; + emptyViewValue = ctrl.parserResult.viewMapper(scope, locals); + + return candidateViewValue !== emptyViewValue ? candidateViewValue : locals.$model; + }; + // Called with model, itemDetails (model, {item, model, label}) ctrl.selectListeners = []; @@ -199,8 +212,6 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap //SUPPORTED ATTRIBUTES (OPTIONS) - var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; - var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; //INTERNAL VARIABLES @@ -220,25 +231,11 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap modelCtrl.$formatters.push(function (modelValue) { - var candidateViewValue, emptyViewValue; - var locals = {}; - - 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[typeaheadCtrl.parserResult.itemName] = modelValue; - candidateViewValue = typeaheadCtrl.parserResult.viewMapper(originalScope, locals); - locals[typeaheadCtrl.parserResult.itemName] = undefined; - emptyViewValue = typeaheadCtrl.parserResult.viewMapper(originalScope, locals); + var locals = { + $model: modelValue + }; - return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue; - } + return typeaheadCtrl.inputFormatter(originalScope, locals); }); //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) @@ -470,6 +467,21 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap } ]) + .directive('typeaheadInputFormatter', [ + '$parse', + function ($parse) { + return { + restrict: 'A', + require: 'typeahead', + link: function (scope, element, attrs, typeaheadCtrl) { + if (attrs.typeaheadInputFormatter) { + typeaheadCtrl.inputFormatter = $parse(attrs.typeaheadInputFormatter); + } + } + }; + } + ]) + .filter('typeaheadHighlight', function() { function escapeRegexp(queryToEscape) { From 1e93edc5c10911732785c1a5b89d5b0f64737c53 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Thu, 9 Jan 2014 00:18:47 -0800 Subject: [PATCH 14/18] refactor(typeahead): move out typeaheadLoading --- src/typeahead/typeahead.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 887f018553..c944b08000 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -36,7 +36,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap var ctrl = this; //binding to a variable that indicates if matches are being retrieved asynchronously - var isLoadingSetter = $parse($attrs.typeaheadLoading).assign || angular.noop; + ctrl.setIsLoading = angular.noop; var appendToBody = $attrs.typeaheadAppendToBody ? $scope.$eval($attrs.typeaheadAppendToBody) : false; @@ -103,7 +103,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap lastMatchInputValue = inputValue; var locals = {$viewValue: inputValue}; - isLoadingSetter($scope, true); + ctrl.setIsLoading($scope, true); $q.when(ctrl.parserResult.source($scope, locals)).then(function(matches) { //it might happen that several async queries were in progress if a user were typing fast @@ -130,7 +130,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap //due to other elements being rendered ctrl.position = appendToBody ? $position.offset($element) : $position.position($element); ctrl.position.top = ctrl.position.top + $element.prop('offsetHeight'); - isLoadingSetter($scope, false); + ctrl.setIsLoading($scope, false); } else { resetMatches(); } @@ -189,7 +189,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap lastMatchInputValue = null; ctrl.matches = []; ctrl.activeIdx = -1; - isLoadingSetter($scope, false); + ctrl.setIsLoading($scope, false); } ctrl.popUpEl = $compile(popUpEl)(taScope); @@ -482,6 +482,19 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap } ]) + .directive('typeaheadLoading', [ + '$parse', + function ($parse) { + return { + restrict: 'A', + require: 'typeahead', + link: function (scope, element, attrs, typeaheadCtrl) { + typeaheadCtrl.setIsLoading = $parse(attrs.typeaheadLoading).assign || angular.noop; + } + }; + } + ]) + .filter('typeaheadHighlight', function() { function escapeRegexp(queryToEscape) { From ce7830f53547a7b5fae50ede2bcf940eb6c7c914 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Thu, 9 Jan 2014 00:54:00 -0800 Subject: [PATCH 15/18] refactor(typeahead): rearrange methods An attempt to make things clearer. --- src/typeahead/typeahead.js | 54 +++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index c944b08000..deeceba629 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -48,24 +48,6 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap //expressions used by typeahead ctrl.parserResult = typeaheadParser.parse($attrs.typeahead); - //create a child scope for the typeahead directive so we are not polluting original scope - //with typeahead-specific data (matches, query etc.) - var taScope = this.taScope = $scope.$new(); - taScope.typeaheadCtrl = this; - - ctrl.inputFormatter = function (scope, locals) { - var candidateViewValue, emptyViewValue; - - //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[ctrl.parserResult.itemName] = locals.$model; - candidateViewValue = ctrl.parserResult.viewMapper(scope, locals); - locals[ctrl.parserResult.itemName] = undefined; - emptyViewValue = ctrl.parserResult.viewMapper(scope, locals); - - return candidateViewValue !== emptyViewValue ? candidateViewValue : locals.$model; - }; - // Called with model, itemDetails (model, {item, model, label}) ctrl.selectListeners = []; @@ -140,6 +122,19 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap }); }; + ctrl.inputFormatter = function (scope, locals) { + var candidateViewValue, emptyViewValue; + + //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[ctrl.parserResult.itemName] = locals.$model; + candidateViewValue = ctrl.parserResult.viewMapper(scope, locals); + locals[ctrl.parserResult.itemName] = undefined; + emptyViewValue = ctrl.parserResult.viewMapper(scope, locals); + + return candidateViewValue !== emptyViewValue ? candidateViewValue : locals.$model; + }; + ctrl.select = function (match) { $setModelValue($scope, match.model); resetMatches(); @@ -152,6 +147,16 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap $element[0].focus(); }; + resetMatches(); + + function resetMatches() { + lastQuery = null; + lastMatchInputValue = null; + ctrl.matches = []; + ctrl.activeIdx = -1; + ctrl.setIsLoading($scope, false); + } + ctrl._selectActive = function (activeIdx) { if (typeof activeIdx != 'undefined') { ctrl.activeIdx = activeIdx; @@ -167,8 +172,6 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap ctrl.activeIdx = (ctrl.activeIdx ? ctrl.activeIdx : ctrl.matches.length) - 1; }; - resetMatches(); - //pop-up element used to display matches var popUpEl = angular.element('
'); popUpEl.attr({ @@ -184,13 +187,10 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap popUpEl.attr('template-url', $attrs.typeaheadTemplateUrl); } - function resetMatches() { - lastQuery = null; - lastMatchInputValue = null; - ctrl.matches = []; - ctrl.activeIdx = -1; - ctrl.setIsLoading($scope, false); - } + //create a child scope for the typeahead directive so we are not polluting original scope + //with typeahead-specific data (matches, query etc.) + var taScope = this.taScope = $scope.$new(); + taScope.typeaheadCtrl = this; ctrl.popUpEl = $compile(popUpEl)(taScope); } From c492d763a0d864df3d2d7e25eed430dbae50cc39 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Tue, 21 Jan 2014 23:06:25 -0800 Subject: [PATCH 16/18] refactor(typeahead): capitalize controller name --- src/typeahead/typeahead.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index deeceba629..fcfd7783c5 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -29,7 +29,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap }; }]) - .controller('typeaheadController', [ + .controller('TypeaheadController', [ '$scope', '$compile', '$q', '$attrs', '$parse', '$element', '$position', 'typeaheadParser', function ($scope , $compile , $q , $attrs , $parse , $element , $position , typeaheadParser) { @@ -203,7 +203,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap return { require: ['typeahead', 'ngModel'], - controller: 'typeaheadController', + controller: 'TypeaheadController', link:function (originalScope, element, attrs, controllers) { var typeaheadCtrl = controllers[0], From dd86cec76b541bba2afa1e8658455d2a4e28ff10 Mon Sep 17 00:00:00 2001 From: Chris Chua Date: Tue, 21 Jan 2014 23:43:19 -0800 Subject: [PATCH 17/18] fix(typeahead): clicking to select was broken On blur event check if mouse is over the drop down before resetting matches. Introduce mouseIsOver in the controller but this will be removed shortly. --- src/typeahead/typeahead.js | 12 ++++++++++-- template/typeahead/typeahead-popup.html | 4 +++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index fcfd7783c5..f2b13dbd56 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -55,6 +55,9 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap // promises. ctrl.queryParsers = []; + // FIXME: Remove when switching to isolate scope + ctrl.mouseIsOver = false; + // Check this value to see if it's the latest query var lastQuery = null; @@ -180,7 +183,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap active: 'typeaheadCtrl.activeIdx', select: 'typeaheadCtrl._selectActive(activeIdx)', query: 'typeaheadCtrl.query', - position: 'typeaheadCtrl.position' + position: 'typeaheadCtrl.position', + mouseisover: 'typeaheadCtrl.mouseIsOver' }); //custom item template if (angular.isDefined($attrs.typeaheadTemplateUrl)) { @@ -270,7 +274,10 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap }); element.bind('blur', function () { - resetMatches(); + if (!typeaheadCtrl.mouseIsOver) { + resetMatches(); + scope.$digest(); + } }); // Keep reference to click handler to unbind it. @@ -309,6 +316,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap query:'=', active:'=', position:'=', + mouseIsOver:'=mouseisover', select:'&' }, replace:true, diff --git a/template/typeahead/typeahead-popup.html b/template/typeahead/typeahead-popup.html index 6cf53f5cc1..96b25a6dc8 100644 --- a/template/typeahead/typeahead-popup.html +++ b/template/typeahead/typeahead-popup.html @@ -1,4 +1,6 @@ -