From 19057252ed479eafeaa957d88e658c02df06b7c1 Mon Sep 17 00:00:00 2001 From: shootermv Date: Sun, 27 Sep 2015 12:12:07 +0300 Subject: [PATCH 01/11] feat(typeahead): adding clear button --- src/typeahead/typeahead.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index fd19ed1632..d785b448f4 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -115,6 +115,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) //pop-up element used to display matches var popUpEl = angular.element('
'); + //element used for clear button + var clearBtnEl = angular.element(''); popUpEl.attr({ id: popupId, matches: 'matches', From 63ec31dd8488198b4f6168c80e965c979442c900 Mon Sep 17 00:00:00 2001 From: shootermv Date: Sun, 27 Sep 2015 12:18:57 +0300 Subject: [PATCH 02/11] feat(typeahead): adding clear button- step02 --- src/typeahead/typeahead.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index d785b448f4..f220152a9d 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -255,6 +255,10 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) function recalculatePosition() { scope.position = appendToBody ? $position.offset(element) : $position.position(element); scope.position.top += element.prop('offsetHeight'); + + scope.positionClearBtn = $position.position(element); + scope.positionClearBtn.top += element.prop('offsetHeight')/4; + scope.positionClearBtn.left += element.prop('offsetWidth')-20; } resetMatches(); @@ -334,12 +338,20 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) return candidateViewValue!== emptyViewValue ? candidateViewValue : modelValue; } }); - + //method for clear blutton + scope.clearModel = function(){ + $setModelValue(originalScope, ''); + scope.selectedM = false; + scope.$digest(); + }; + scope.select = function(activeIdx) { //called from within the $digest() cycle var locals = {}; var model, item; - + //indicator that some choise selected for clear button + scope.selectedM = true; + selected = true; locals[parserResult.itemName] = item = scope.matches[activeIdx].model; model = parserResult.modelMapper(originalScope, locals); @@ -434,12 +446,14 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) }); var $popup = $compile(popUpEl)(scope); + var $clearBtn = $compile(clearBtnEl)(scope); if (appendToBody) { $document.find('body').append($popup); } else { element.after($popup); } + element.after($clearBtn); } }; From b6ac4d98221abe32dcc0827bc9aec4c985d2b409 Mon Sep 17 00:00:00 2001 From: shootermv Date: Wed, 17 Aug 2016 15:19:18 +0300 Subject: [PATCH 03/11] chore(release): v0.14.0 --- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90c677805e..037b58eb2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,46 @@ + +# [0.14.0](https://github.com/angular-ui/bootstrap/compare/0.13.4...v0.14.0) (2016-08-17) + + +### Bug Fixes + +* **accordion:** coerce to boolean ([b864aa9](https://github.com/angular-ui/bootstrap/commit/b864aa9)), closes [#4385](https://github.com/angular-ui/bootstrap/issues/4385) +* **alert:** properly pass $event as local ([eb2366f](https://github.com/angular-ui/bootstrap/commit/eb2366f)), closes [#4386](https://github.com/angular-ui/bootstrap/issues/4386) [#4387](https://github.com/angular-ui/bootstrap/issues/4387) +* **datepicker:** add check for `contains` ([868c0e2](https://github.com/angular-ui/bootstrap/commit/868c0e2)), closes [#4423](https://github.com/angular-ui/bootstrap/issues/4423) [#4411](https://github.com/angular-ui/bootstrap/issues/4411) +* **datepicker:** change to `$popup` ([65814f1](https://github.com/angular-ui/bootstrap/commit/65814f1)) +* **datepicker:** remove focus management on date selection by keyboard ([36ecf60](https://github.com/angular-ui/bootstrap/commit/36ecf60)), closes [#4409](https://github.com/angular-ui/bootstrap/issues/4409) +* **modal:** fix for conflicts with ngTouch module on mobile devices ([508aceb](https://github.com/angular-ui/bootstrap/commit/508aceb)), closes [#2280](https://github.com/angular-ui/bootstrap/issues/2280) [#4357](https://github.com/angular-ui/bootstrap/issues/4357) +* **tooltip:** add display block to style ([b413a22](https://github.com/angular-ui/bootstrap/commit/b413a22)), closes [#4363](https://github.com/angular-ui/bootstrap/issues/4363) [#4379](https://github.com/angular-ui/bootstrap/issues/4379) +* **tooltip:** correct flash of reposition ([8fee75d](https://github.com/angular-ui/bootstrap/commit/8fee75d)), closes [#4363](https://github.com/angular-ui/bootstrap/issues/4363) [#4195](https://github.com/angular-ui/bootstrap/issues/4195) +* **tooltip:** do nothing if `$scope` doesn't exist ([1e039e8](https://github.com/angular-ui/bootstrap/commit/1e039e8)), closes [#4346](https://github.com/angular-ui/bootstrap/issues/4346) [#3347](https://github.com/angular-ui/bootstrap/issues/3347) +* **tooltip:** fix binding to multiple triggers ([d6cda93](https://github.com/angular-ui/bootstrap/commit/d6cda93)), closes [#4371](https://github.com/angular-ui/bootstrap/issues/4371) [#4384](https://github.com/angular-ui/bootstrap/issues/4384) +* **tooltip:** isOpen to work with expressions ([5f68280](https://github.com/angular-ui/bootstrap/commit/5f68280)), closes [#4380](https://github.com/angular-ui/bootstrap/issues/4380) [#4362](https://github.com/angular-ui/bootstrap/issues/4362) +* **tooltip:** properly gc popupTimeout ([ff52f52](https://github.com/angular-ui/bootstrap/commit/ff52f52)), closes [#2786](https://github.com/angular-ui/bootstrap/issues/2786) +* **tooltip:** set `visibility: hidden` to avoid flicker ([f7cb8bc](https://github.com/angular-ui/bootstrap/commit/f7cb8bc)), closes [#4342](https://github.com/angular-ui/bootstrap/issues/4342) + +### Features + +* **accordion:** use uib- prefix ([0328a76](https://github.com/angular-ui/bootstrap/commit/0328a76)), closes [#4389](https://github.com/angular-ui/bootstrap/issues/4389) +* **alert:** use uib- prefix ([5e3a87a](https://github.com/angular-ui/bootstrap/commit/5e3a87a)), closes [#4406](https://github.com/angular-ui/bootstrap/issues/4406) +* **buttons:** use uib- prefix ([5a1c2c9](https://github.com/angular-ui/bootstrap/commit/5a1c2c9)), closes [#4445](https://github.com/angular-ui/bootstrap/issues/4445) +* **collapse:** convert to use `$animateCss` ([533a9f0](https://github.com/angular-ui/bootstrap/commit/533a9f0)), closes [#4257](https://github.com/angular-ui/bootstrap/issues/4257) +* **collapse:** use uib- prefix ([9bdb32e](https://github.com/angular-ui/bootstrap/commit/9bdb32e)), closes [#4370](https://github.com/angular-ui/bootstrap/issues/4370) +* **dateparser:** reset parsers when $locale.id changes ([d9a521a](https://github.com/angular-ui/bootstrap/commit/d9a521a)), closes [#4286](https://github.com/angular-ui/bootstrap/issues/4286) [#4425](https://github.com/angular-ui/bootstrap/issues/4425) +* **modal:** Added ability to add CSS class to top window ([bd38e8f](https://github.com/angular-ui/bootstrap/commit/bd38e8f)), closes [#2524](https://github.com/angular-ui/bootstrap/issues/2524) +* **progressbar:** add `aria-labelledby` support ([e6f3b87](https://github.com/angular-ui/bootstrap/commit/e6f3b87)), closes [#4350](https://github.com/angular-ui/bootstrap/issues/4350) [#4347](https://github.com/angular-ui/bootstrap/issues/4347) +* **rating:** add `aria-valuetext` attribute ([72de2d8](https://github.com/angular-ui/bootstrap/commit/72de2d8)), closes [#4349](https://github.com/angular-ui/bootstrap/issues/4349) [#4347](https://github.com/angular-ui/bootstrap/issues/4347) +* **tooltip:** hide tooltip when `esc` is hit ([c08509a](https://github.com/angular-ui/bootstrap/commit/c08509a)), closes [#4367](https://github.com/angular-ui/bootstrap/issues/4367) [#4248](https://github.com/angular-ui/bootstrap/issues/4248) +* **typeahead:** add customClass support for dropdown ([fa1cdfc](https://github.com/angular-ui/bootstrap/commit/fa1cdfc)), closes [#4332](https://github.com/angular-ui/bootstrap/issues/4332) [#4410](https://github.com/angular-ui/bootstrap/issues/4410) +* **typeahead:** adding clear button ([1905725](https://github.com/angular-ui/bootstrap/commit/1905725)) +* **typeahead:** adding clear button- step02 ([63ec31d](https://github.com/angular-ui/bootstrap/commit/63ec31d)) + + +### BREAKING CHANGES + +* Removes focus on datepicker on selection of a date via keyboard for accessibility reasons + + + ## [0.13.4](https://github.com/angular-ui/bootstrap/compare/0.13.3...v0.13.4) (2015-09-03) diff --git a/package.json b/package.json index ac4597281b..0f0eb7ad15 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "author": "https://github.com/angular-ui/bootstrap/graphs/contributors", "name": "angular-ui-bootstrap", - "version": "0.14.0-SNAPSHOT", + "version": "0.14.0", "homepage": "http://angular-ui.github.io/bootstrap/", "dependencies": {}, "scripts":{ From fef6b1a3ba318ac31ef234833ecdef0027a39bd0 Mon Sep 17 00:00:00 2001 From: shootermv Date: Wed, 17 Aug 2016 15:19:20 +0300 Subject: [PATCH 04/11] chore(release): Starting v0.15.0-SNAPSHOT --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0f0eb7ad15..5273ba7629 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "author": "https://github.com/angular-ui/bootstrap/graphs/contributors", "name": "angular-ui-bootstrap", - "version": "0.14.0", + "version": "0.15.0-SNAPSHOT", "homepage": "http://angular-ui.github.io/bootstrap/", "dependencies": {}, "scripts":{ From d22db36b7008b57988ec465d296c01ef6ff735c6 Mon Sep 17 00:00:00 2001 From: shootermv Date: Thu, 18 Aug 2016 16:56:41 +0300 Subject: [PATCH 05/11] feat(typeahead): clear-button added --- src/typeahead/typeahead.js | 921 +++++++++++++++++++------------------ 1 file changed, 469 insertions(+), 452 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 126fd1fd47..b6351900cd 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -13,7 +13,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap if (!match) { throw new Error( 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' + - ' but got "' + input + '".'); + ' but got "' + input + '".'); } return { @@ -28,550 +28,567 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap .controller('UibTypeaheadController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$$debounce', '$uibPosition', 'uibTypeaheadParser', function(originalScope, element, attrs, $compile, $parse, $q, $timeout, $document, $window, $rootScope, $$debounce, $position, typeaheadParser) { - var HOT_KEYS = [9, 13, 27, 38, 40]; - var eventDebounceTime = 200; - var modelCtrl, ngModelOptions; - //SUPPORTED ATTRIBUTES (OPTIONS) - - //minimal no of characters that needs to be entered before typeahead kicks-in - var minLength = originalScope.$eval(attrs.typeaheadMinLength); - if (!minLength && minLength !== 0) { - minLength = 1; - } + var HOT_KEYS = [9, 13, 27, 38, 40]; + var eventDebounceTime = 200; + var modelCtrl, ngModelOptions; + //SUPPORTED ATTRIBUTES (OPTIONS) + + //minimal no of characters that needs to be entered before typeahead kicks-in + var minLength = originalScope.$eval(attrs.typeaheadMinLength); + if (!minLength && minLength !== 0) { + minLength = 1; + } - originalScope.$watch(attrs.typeaheadMinLength, function (newVal) { + originalScope.$watch(attrs.typeaheadMinLength, function (newVal) { minLength = !newVal && newVal !== 0 ? 1 : newVal; - }); + }); - //minimal wait time after last character typed before typeahead kicks-in - var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; + //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; - originalScope.$watch(attrs.typeaheadEditable, function (newVal) { - isEditable = newVal !== false; - }); + //should it restrict model values to the ones selected from the popup only? + var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; + originalScope.$watch(attrs.typeaheadEditable, function (newVal) { + isEditable = newVal !== false; + }); - //binding to a variable that indicates if matches are being retrieved asynchronously - var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; + //binding to a variable that indicates if matches are being retrieved asynchronously + var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; - //a function to determine if an event should cause selection - var isSelectEvent = attrs.typeaheadShouldSelect ? $parse(attrs.typeaheadShouldSelect) : function(scope, vals) { - var evt = vals.$event; - return evt.which === 13 || evt.which === 9; - }; + //a function to determine if an event should cause selection + var isSelectEvent = attrs.typeaheadShouldSelect ? $parse(attrs.typeaheadShouldSelect) : function(scope, vals) { + var evt = vals.$event; + return evt.which === 13 || evt.which === 9; + }; - //a callback executed when a match is selected - var onSelectCallback = $parse(attrs.typeaheadOnSelect); + //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; + //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; + //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 inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; - var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; + var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; - var appendTo = attrs.typeaheadAppendTo ? - originalScope.$eval(attrs.typeaheadAppendTo) : null; + var appendTo = attrs.typeaheadAppendTo ? + originalScope.$eval(attrs.typeaheadAppendTo) : null; - var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== 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; + //If input matches an item of the list exactly, select it automatically + var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false; - //binding to a variable that indicates if dropdown is open - var isOpenSetter = $parse(attrs.typeaheadIsOpen).assign || angular.noop; + //binding to a variable that indicates if dropdown is open + var isOpenSetter = $parse(attrs.typeaheadIsOpen).assign || angular.noop; - var showHint = originalScope.$eval(attrs.typeaheadShowHint) || false; + var showHint = originalScope.$eval(attrs.typeaheadShowHint) || false; - //INTERNAL VARIABLES + //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}); - } + //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}); + } - return parsedModel.assign(scope, newValue); - }; + return parsedModel.assign(scope, newValue); + }; - //expressions used by typeahead - var parserResult = typeaheadParser.parse(attrs.uibTypeahead); - - var hasFocus; - - //Used to avoid bug in iOS webview where iOS keyboard does not fire - //mousedown & mouseup events - //Issue #3699 - var selected; - - //create a child scope for the typeahead directive so we are not polluting original scope - //with typeahead-specific data (matches, query etc.) - var scope = originalScope.$new(); - var offDestroy = originalScope.$on('$destroy', function() { - scope.$destroy(); - }); - scope.$on('$destroy', offDestroy); - - // WAI-ARIA - var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); - element.attr({ - 'aria-autocomplete': 'list', - 'aria-expanded': false, - 'aria-owns': popupId - }); - - var inputsContainer, hintInputElem; - //add read-only input to show hint - if (showHint) { - inputsContainer = angular.element('
'); - inputsContainer.css('position', 'relative'); - element.after(inputsContainer); - hintInputElem = element.clone(); - hintInputElem.attr('placeholder', ''); - hintInputElem.attr('tabindex', '-1'); - hintInputElem.val(''); - hintInputElem.css({ - 'position': 'absolute', - 'top': '0px', - 'left': '0px', - 'border-color': 'transparent', - 'box-shadow': 'none', - 'opacity': 1, - 'background': 'none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)', - 'color': '#999' - }); - element.css({ - 'position': 'relative', - 'vertical-align': 'top', - 'background-color': 'transparent' - }); + //expressions used by typeahead + var parserResult = typeaheadParser.parse(attrs.uibTypeahead); - if (hintInputElem.attr('id')) { - hintInputElem.removeAttr('id'); // remove duplicate id if present. - } - inputsContainer.append(hintInputElem); - hintInputElem.after(element); - } + var hasFocus; - //pop-up element used to display matches - var popUpEl = angular.element('
'); - popUpEl.attr({ - id: popupId, - matches: 'matches', - active: 'activeIdx', - select: 'select(activeIdx, evt)', - 'move-in-progress': 'moveInProgress', - query: 'query', - position: 'position', - 'assign-is-open': 'assignIsOpen(isOpen)', - debounce: 'debounceUpdate' - }); - //custom item template - if (angular.isDefined(attrs.typeaheadTemplateUrl)) { - popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); - } + //Used to avoid bug in iOS webview where iOS keyboard does not fire + //mousedown & mouseup events + //Issue #3699 + var selected; - if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) { - popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl); - } + //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 + }); - var resetHint = function() { + var inputsContainer, hintInputElem; + //add read-only input to show hint if (showHint) { + inputsContainer = angular.element('
'); + inputsContainer.css('position', 'relative'); + element.after(inputsContainer); + hintInputElem = element.clone(); + hintInputElem.attr('placeholder', ''); + hintInputElem.attr('tabindex', '-1'); hintInputElem.val(''); - } - }; - - var resetMatches = function() { - scope.matches = []; - scope.activeIdx = -1; - element.attr('aria-expanded', false); - resetHint(); - }; - - var getMatchId = function(index) { - return popupId + '-option-' + index; - }; + hintInputElem.css({ + 'position': 'absolute', + 'top': '0px', + 'left': '0px', + 'border-color': 'transparent', + 'box-shadow': 'none', + 'opacity': 1, + 'background': 'none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)', + 'color': '#999' + }); + element.css({ + 'position': 'relative', + 'vertical-align': 'top', + 'background-color': 'transparent' + }); - // 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)); + if (hintInputElem.attr('id')) { + hintInputElem.removeAttr('id'); // remove duplicate id if present. + } + inputsContainer.append(hintInputElem); + hintInputElem.after(element); } - }); - var inputIsExactMatch = function(inputValue, index) { - if (scope.matches.length > index && inputValue) { - return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase(); + //pop-up element used to display matches + var popUpEl = angular.element('
'); + var clearBtnEl = angular.element(''); + popUpEl.attr({ + id: popupId, + matches: 'matches', + active: 'activeIdx', + select: 'select(activeIdx, evt)', + 'move-in-progress': 'moveInProgress', + query: 'query', + position: 'position', + 'assign-is-open': 'assignIsOpen(isOpen)', + debounce: 'debounceUpdate' + }); + //custom item template + if (angular.isDefined(attrs.typeaheadTemplateUrl)) { + popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); } - return false; - }; + if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) { + popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl); + } - var getMatchesAsync = function(inputValue, evt) { - 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] - }); - } + var resetHint = function() { + if (showHint) { + hintInputElem.val(''); + } + }; + + var resetMatches = function() { + scope.matches = []; + scope.activeIdx = -1; + element.attr('aria-expanded', false); + resetHint(); + }; + + 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)); + } + }); - 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(); + var inputIsExactMatch = function(inputValue, index) { + if (scope.matches.length > index && inputValue) { + return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase(); + } - element.attr('aria-expanded', true); + return false; + }; + + var getMatchesAsync = function(inputValue, evt) { + 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] + }); + } - //Select the single remaining option if user input matches - if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) { - if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { - $$debounce(function() { + 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)) { + if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { + $$debounce(function() { + scope.select(0, evt); + }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); + } else { scope.select(0, evt); - }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); - } else { - scope.select(0, evt); + } } - } - if (showHint) { - var firstLabel = scope.matches[0].label; - if (angular.isString(inputValue) && - inputValue.length > 0 && - firstLabel.slice(0, inputValue.length).toUpperCase() === inputValue.toUpperCase()) { - hintInputElem.val(inputValue + firstLabel.slice(inputValue.length)); - } else { - hintInputElem.val(''); + if (showHint) { + var firstLabel = scope.matches[0].label; + if (angular.isString(inputValue) && + inputValue.length > 0 && + firstLabel.slice(0, inputValue.length).toUpperCase() === inputValue.toUpperCase()) { + hintInputElem.val(inputValue + firstLabel.slice(inputValue.length)); + } else { + hintInputElem.val(''); + } } + } else { + resetMatches(); + isNoResultsSetter(originalScope, true); } - } else { - resetMatches(); - isNoResultsSetter(originalScope, true); } - } - if (onCurrentRequest) { + if (onCurrentRequest) { + isLoadingSetter(originalScope, false); + } + }, function() { + resetMatches(); 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).on('resize', fireRecalculating); - $document.find('body').on('scroll', fireRecalculating); - } + isNoResultsSetter(originalScope, true); + }); + }; - // Declare the debounced function outside recalculating for - // proper debouncing - var debouncedRecalculate = $$debounce(function() { - // if popup is visible - if (scope.matches.length) { - recalculatePosition(); + // bind events only if appendToBody params exist - performance feature + if (appendToBody) { + angular.element($window).on('resize', fireRecalculating); + $document.find('body').on('scroll', fireRecalculating); } - scope.moveInProgress = false; - }, eventDebounceTime); + // Declare the debounced function outside recalculating for + // proper debouncing + var debouncedRecalculate = $$debounce(function() { + // if popup is visible + if (scope.matches.length) { + recalculatePosition(); + } - // Default progress type - scope.moveInProgress = false; + scope.moveInProgress = false; + }, eventDebounceTime); - function fireRecalculating() { - if (!scope.moveInProgress) { - scope.moveInProgress = true; - scope.$digest(); - } + // Default progress type + scope.moveInProgress = false; - debouncedRecalculate(); - } + function fireRecalculating() { + if (!scope.moveInProgress) { + scope.moveInProgress = true; + scope.$digest(); + } - // 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'); - } + debouncedRecalculate(); + } - //we need to propagate user's query so we can higlight matches - scope.query = undefined; + // 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'); - //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later - var timeoutPromise; + scope.positionClearBtn = $position.position(element); + scope.positionClearBtn.top += element.prop('offsetHeight')/4; + scope.positionClearBtn.left += element.prop('offsetWidth')-20; - var scheduleSearchWithTimeout = function(inputValue) { - timeoutPromise = $timeout(function() { - getMatchesAsync(inputValue); - }, waitTime); - }; - var cancelPreviousTimeout = function() { - if (timeoutPromise) { - $timeout.cancel(timeoutPromise); } - }; - resetMatches(); + //we need to propagate user's query so we can higlight matches + scope.query = undefined; - scope.assignIsOpen = function (isOpen) { - isOpenSetter(originalScope, isOpen); - }; + //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later + var timeoutPromise; - scope.select = function(activeIdx, evt) { - //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), - $event: evt - }); + var scheduleSearchWithTimeout = function(inputValue) { + timeoutPromise = $timeout(function() { + getMatchesAsync(inputValue); + }, waitTime); + }; - resetMatches(); + var cancelPreviousTimeout = function() { + if (timeoutPromise) { + $timeout.cancel(timeoutPromise); + } + }; - //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); - } - }; + resetMatches(); + //method for clear blutton + scope.clearModel = function(){ + $setModelValue(originalScope, ''); + scope.selectedM = false; + scope.$digest(); + }; + scope.assignIsOpen = function (isOpen) { + isOpenSetter(originalScope, isOpen); + }; - //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) - element.on('keydown', function(evt) { - //typeahead is open and an "interesting" key was pressed - if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { - return; - } + scope.select = function(activeIdx, evt) { + //called from within the $digest() cycle + var locals = {}; + var model, item; - var shouldSelect = isSelectEvent(originalScope, {$event: evt}); + selected = true; + //indicator that some choise selected for clear button + scope.selectedM = 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), + $event: evt + }); - /** - * if there's nothing selected (i.e. focusFirst) and enter or tab is hit - * or - * shift + tab is pressed to bring focus to the previous element - * then clear the results - */ - if (scope.activeIdx === -1 && shouldSelect || evt.which === 9 && !!evt.shiftKey) { resetMatches(); - scope.$digest(); - return; - } - evt.preventDefault(); - var target; - switch (evt.which) { - case 27: // escape - evt.stopPropagation(); + //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.on('keydown', function(evt) { + //typeahead is open and an "interesting" key was pressed + if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { + return; + } + var shouldSelect = isSelectEvent(originalScope, {$event: evt}); + + /** + * if there's nothing selected (i.e. focusFirst) and enter or tab is hit + * or + * shift + tab is pressed to bring focus to the previous element + * then clear the results + */ + if (scope.activeIdx === -1 && shouldSelect || evt.which === 9 && !!evt.shiftKey) { resetMatches(); - originalScope.$digest(); - break; - case 38: // up arrow - scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1; - scope.$digest(); - target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx]; - target.parentNode.scrollTop = target.offsetTop; - break; - case 40: // down arrow - scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; scope.$digest(); - target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx]; - target.parentNode.scrollTop = target.offsetTop; - break; - default: - if (shouldSelect) { - scope.$apply(function() { - if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { - $$debounce(function() { + return; + } + + evt.preventDefault(); + var target; + switch (evt.which) { + case 27: // escape + evt.stopPropagation(); + + resetMatches(); + originalScope.$digest(); + break; + case 38: // up arrow + scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1; + scope.$digest(); + target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx]; + target.parentNode.scrollTop = target.offsetTop; + break; + case 40: // down arrow + scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; + scope.$digest(); + target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx]; + target.parentNode.scrollTop = target.offsetTop; + break; + default: + if (shouldSelect) { + scope.$apply(function() { + if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { + $$debounce(function() { + scope.select(scope.activeIdx, evt); + }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); + } else { scope.select(scope.activeIdx, evt); - }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); - } else { - scope.select(scope.activeIdx, evt); - } - }); - } - } - }); - - element.bind('focus', function (evt) { - hasFocus = true; - if (minLength === 0 && !modelCtrl.$viewValue) { - $timeout(function() { - getMatchesAsync(modelCtrl.$viewValue, evt); - }, 0); - } - }); + } + }); + } + } + }); - element.bind('blur', function(evt) { - if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) { - selected = true; - scope.$apply(function() { - if (angular.isObject(scope.debounceUpdate) && angular.isNumber(scope.debounceUpdate.blur)) { - $$debounce(function() { + element.bind('focus', function (evt) { + hasFocus = true; + if (minLength === 0 && !modelCtrl.$viewValue) { + $timeout(function() { + getMatchesAsync(modelCtrl.$viewValue, evt); + }, 0); + } + }); + + element.bind('blur', function(evt) { + if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) { + selected = true; + scope.$apply(function() { + if (angular.isObject(scope.debounceUpdate) && angular.isNumber(scope.debounceUpdate.blur)) { + $$debounce(function() { + scope.select(scope.activeIdx, evt); + }, scope.debounceUpdate.blur); + } else { scope.select(scope.activeIdx, evt); - }, scope.debounceUpdate.blur); - } else { - scope.select(scope.activeIdx, evt); + } + }); + } + if (!isEditable && modelCtrl.$error.editable) { + modelCtrl.$setViewValue(); + scope.$apply(function() { + // Reset validity as we are clearing + modelCtrl.$setValidity('editable', true); + modelCtrl.$setValidity('parse', true); + }); + element.val(''); + } + 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) { + originalScope.$digest(); } - }); - } - if (!isEditable && modelCtrl.$error.editable) { - modelCtrl.$setViewValue(); - scope.$apply(function() { - // Reset validity as we are clearing - modelCtrl.$setValidity('editable', true); - modelCtrl.$setValidity('parse', true); - }); - element.val(''); - } - 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) { - originalScope.$digest(); } - } - }; + }; - $document.on('click', dismissClickHandler); + $document.on('click', dismissClickHandler); - originalScope.$on('$destroy', function() { - $document.off('click', dismissClickHandler); - if (appendToBody || appendTo) { - $popup.remove(); - } + originalScope.$on('$destroy', function() { + $document.off('click', dismissClickHandler); + if (appendToBody || appendTo) { + $popup.remove(); + } - if (appendToBody) { - angular.element($window).off('resize', fireRecalculating); - $document.find('body').off('scroll', fireRecalculating); - } - // Prevent jQuery cache memory leak - popUpEl.remove(); + if (appendToBody) { + angular.element($window).off('resize', fireRecalculating); + $document.find('body').off('scroll', fireRecalculating); + } + // Prevent jQuery cache memory leak + popUpEl.remove(); - if (showHint) { + if (showHint) { inputsContainer.remove(); - } - }); + } + }); - var $popup = $compile(popUpEl)(scope); + var $popup = $compile(popUpEl)(scope); + var $clearBtn = $compile(clearBtnEl)(scope); - if (appendToBody) { - $document.find('body').append($popup); - } else if (appendTo) { - angular.element(appendTo).eq(0).append($popup); - } else { - element.after($popup); - } + if (appendToBody) { + $document.find('body').append($popup); + } else if (appendTo) { + angular.element(appendTo).eq(0).append($popup); + } else { + element.after($popup); + } + element.after($clearBtn); - this.init = function(_modelCtrl, _ngModelOptions) { - modelCtrl = _modelCtrl; - ngModelOptions = _ngModelOptions; + this.init = function(_modelCtrl, _ngModelOptions) { + modelCtrl = _modelCtrl; + ngModelOptions = _ngModelOptions; - scope.debounceUpdate = modelCtrl.$options && $parse(modelCtrl.$options.debounce)(originalScope); + scope.debounceUpdate = modelCtrl.$options && $parse(modelCtrl.$options.debounce)(originalScope); - //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; + //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); + if (minLength === 0 || inputValue && inputValue.length >= minLength) { + if (waitTime > 0) { + cancelPreviousTimeout(); + scheduleSearchWithTimeout(inputValue); + } else { + getMatchesAsync(inputValue); + } } else { - getMatchesAsync(inputValue); + isLoadingSetter(originalScope, false); + cancelPreviousTimeout(); + resetMatches(); } - } else { - isLoadingSetter(originalScope, false); - cancelPreviousTimeout(); - resetMatches(); - } - if (isEditable) { - return inputValue; - } + if (isEditable) { + return inputValue; + } - if (!inputValue) { - // Reset in case user had typed something previously. - modelCtrl.$setValidity('editable', true); - return null; - } + if (!inputValue) { + // Reset in case user had typed something previously. + modelCtrl.$setValidity('editable', true); + return null; + } - modelCtrl.$setValidity('editable', false); - return undefined; - }); + modelCtrl.$setValidity('editable', false); + return undefined; + }); - modelCtrl.$formatters.push(function(modelValue) { - var candidateViewValue, emptyViewValue; - var locals = {}; + 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); - } + // 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); - } + if (inputFormatter) { + locals.$model = modelValue; + return inputFormatter(originalScope, locals); + } - //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); + //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; - }); - }; - }]) + return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue; + }); + }; + }]) .directive('uibTypeahead', function() { return { From 420de6bdc663dfd68cc93dfb8f8b65c1e99f7435 Mon Sep 17 00:00:00 2001 From: shootermv Date: Fri, 19 Aug 2016 00:37:43 +0300 Subject: [PATCH 06/11] feat(typeahead): tests created for clear button --- src/typeahead/test/typeahead.spec.js | 46 ++++++++++++++++++++++++++++ src/typeahead/typeahead.js | 8 +++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/typeahead/test/typeahead.spec.js b/src/typeahead/test/typeahead.spec.js index 4423b8a66e..301a1b56a7 100644 --- a/src/typeahead/test/typeahead.spec.js +++ b/src/typeahead/test/typeahead.spec.js @@ -64,6 +64,11 @@ describe('typeahead tests', function() { return findDropDown(element).find('li'); }; + var findClearBtn = function(element) { + return element.find('.glyphicon-remove'); + }; + + var triggerKeyDown = function(element, keyCode, options) { options = options || {}; var inputEl = findInput(element); @@ -232,6 +237,47 @@ describe('typeahead tests', function() { expect($scope.result).toEqual('prefixfoo'); }); + it('should display X button after some option selected', function() { + $scope.updaterFn = function(selectedItem) { + return 'prefix' + selectedItem; + }; + var element = prepareInputEl('
'); + changeInputValueTo(element, 'f'); + triggerKeyDown(element, 13); + expect($scope.result).toEqual('prefixfoo'); + var clearBtn = findClearBtn(element); + expect(clearBtn).not.toHaveClass('ng-hide'); + }); + + it('should hide X button when selected item removed and input got empty', function() { + $scope.updaterFn = function(selectedItem) { + return 'prefix' + selectedItem; + }; + var element = prepareInputEl('
'); + changeInputValueTo(element, 'f'); + triggerKeyDown(element, 13); + expect($scope.result).toEqual('prefixfoo'); + var clearBtn = findClearBtn(element); + changeInputValueTo(element, ''); + expect(clearBtn).toHaveClass('ng-hide'); + }); + + it('when pressing X button should clear the ngModel', function() { + $scope.updaterFn = function(selectedItem) { + return 'prefix' + selectedItem; + }; + var element = prepareInputEl('
'); + changeInputValueTo(element, 'f'); + triggerKeyDown(element, 13); + expect($scope.result).toEqual('prefixfoo'); + var clearBtn = findClearBtn(element); + $timeout(function(){ + clearBtn.click(); + $scope.$digest(); + expect($scope.result).toEqual(''); + },1); + }); + it('should support custom label rendering function', function() { $scope.formatterFn = function(sourceItem) { return 'prefix' + sourceItem; diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index b6351900cd..ad3c624b14 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -99,6 +99,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap } return parsedModel.assign(scope, newValue); + }; //expressions used by typeahead @@ -267,7 +268,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap inputValue.length > 0 && firstLabel.slice(0, inputValue.length).toUpperCase() === inputValue.toUpperCase()) { hintInputElem.val(inputValue + firstLabel.slice(inputValue.length)); - } else { + } else { hintInputElem.val(''); } } @@ -546,6 +547,9 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap isLoadingSetter(originalScope, false); cancelPreviousTimeout(); resetMatches(); + if(!inputValue) { + scope.selectedM = false;//hide clear button if input empty + } } if (isEditable) { @@ -554,7 +558,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap if (!inputValue) { // Reset in case user had typed something previously. - modelCtrl.$setValidity('editable', true); + modelCtrl.$setValidity('editable', true); return null; } From 4e78977142ae3ad20d9ca3aa16ff7ecb85cd55c8 Mon Sep 17 00:00:00 2001 From: shootermv Date: Fri, 19 Aug 2016 01:00:18 +0300 Subject: [PATCH 07/11] feat(typeahead): removed all extra indents and $scope.$digest --- src/typeahead/test/typeahead.spec.js | 10 +- src/typeahead/typeahead.js | 944 +++++++++++++-------------- 2 files changed, 475 insertions(+), 479 deletions(-) diff --git a/src/typeahead/test/typeahead.spec.js b/src/typeahead/test/typeahead.spec.js index 301a1b56a7..ff1c42510e 100644 --- a/src/typeahead/test/typeahead.spec.js +++ b/src/typeahead/test/typeahead.spec.js @@ -270,12 +270,10 @@ describe('typeahead tests', function() { changeInputValueTo(element, 'f'); triggerKeyDown(element, 13); expect($scope.result).toEqual('prefixfoo'); - var clearBtn = findClearBtn(element); - $timeout(function(){ - clearBtn.click(); - $scope.$digest(); - expect($scope.result).toEqual(''); - },1); + var clearBtn = findClearBtn(element); + clearBtn.click(); + $scope.$digest(); + expect($scope.result).toEqual(''); }); it('should support custom label rendering function', function() { diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index ad3c624b14..138f7f1ec0 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -13,7 +13,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap if (!match) { throw new Error( 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' + - ' but got "' + input + '".'); + ' but got "' + input + '".'); } return { @@ -28,571 +28,569 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap .controller('UibTypeaheadController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$$debounce', '$uibPosition', 'uibTypeaheadParser', function(originalScope, element, attrs, $compile, $parse, $q, $timeout, $document, $window, $rootScope, $$debounce, $position, typeaheadParser) { - var HOT_KEYS = [9, 13, 27, 38, 40]; - var eventDebounceTime = 200; - var modelCtrl, ngModelOptions; - //SUPPORTED ATTRIBUTES (OPTIONS) - - //minimal no of characters that needs to be entered before typeahead kicks-in - var minLength = originalScope.$eval(attrs.typeaheadMinLength); - if (!minLength && minLength !== 0) { - minLength = 1; - } + var HOT_KEYS = [9, 13, 27, 38, 40]; + var eventDebounceTime = 200; + var modelCtrl, ngModelOptions; + //SUPPORTED ATTRIBUTES (OPTIONS) + + //minimal no of characters that needs to be entered before typeahead kicks-in + var minLength = originalScope.$eval(attrs.typeaheadMinLength); + if (!minLength && minLength !== 0) { + minLength = 1; + } - originalScope.$watch(attrs.typeaheadMinLength, function (newVal) { + originalScope.$watch(attrs.typeaheadMinLength, function (newVal) { minLength = !newVal && newVal !== 0 ? 1 : newVal; - }); + }); - //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; - originalScope.$watch(attrs.typeaheadEditable, function (newVal) { - isEditable = newVal !== false; - }); + //minimal wait time after last character typed before typeahead kicks-in + var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; - //binding to a variable that indicates if matches are being retrieved asynchronously - var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; + //should it restrict model values to the ones selected from the popup only? + var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; + originalScope.$watch(attrs.typeaheadEditable, function (newVal) { + isEditable = newVal !== false; + }); - //a function to determine if an event should cause selection - var isSelectEvent = attrs.typeaheadShouldSelect ? $parse(attrs.typeaheadShouldSelect) : function(scope, vals) { - var evt = vals.$event; - return evt.which === 13 || evt.which === 9; - }; + //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); + //a function to determine if an event should cause selection + var isSelectEvent = attrs.typeaheadShouldSelect ? $parse(attrs.typeaheadShouldSelect) : function(scope, vals) { + var evt = vals.$event; + return evt.which === 13 || evt.which === 9; + }; - //should it select highlighted popup value when losing focus? - var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false; + //a callback executed when a match is selected + var onSelectCallback = $parse(attrs.typeaheadOnSelect); - //binding to a variable that indicates if there were no results after the query is completed - var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop; + //should it select highlighted popup value when losing focus? + var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false; - var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; + //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 appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; + var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; - var appendTo = attrs.typeaheadAppendTo ? - originalScope.$eval(attrs.typeaheadAppendTo) : null; + var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; - var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false; + var appendTo = attrs.typeaheadAppendTo ? + originalScope.$eval(attrs.typeaheadAppendTo) : null; - //If input matches an item of the list exactly, select it automatically - var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false; + var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false; - //binding to a variable that indicates if dropdown is open - var isOpenSetter = $parse(attrs.typeaheadIsOpen).assign || angular.noop; + //If input matches an item of the list exactly, select it automatically + var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false; - var showHint = originalScope.$eval(attrs.typeaheadShowHint) || false; + //binding to a variable that indicates if dropdown is open + var isOpenSetter = $parse(attrs.typeaheadIsOpen).assign || angular.noop; - //INTERNAL VARIABLES + var showHint = originalScope.$eval(attrs.typeaheadShowHint) || false; - //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}); - } + //INTERNAL VARIABLES - return parsedModel.assign(scope, newValue); + //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}); + } - }; + return parsedModel.assign(scope, newValue); + }; - //expressions used by typeahead - var parserResult = typeaheadParser.parse(attrs.uibTypeahead); + //expressions used by typeahead + var parserResult = typeaheadParser.parse(attrs.uibTypeahead); + + var hasFocus; + + //Used to avoid bug in iOS webview where iOS keyboard does not fire + //mousedown & mouseup events + //Issue #3699 + var selected; + + //create a child scope for the typeahead directive so we are not polluting original scope + //with typeahead-specific data (matches, query etc.) + var scope = originalScope.$new(); + var offDestroy = originalScope.$on('$destroy', function() { + scope.$destroy(); + }); + scope.$on('$destroy', offDestroy); + + // WAI-ARIA + var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); + element.attr({ + 'aria-autocomplete': 'list', + 'aria-expanded': false, + 'aria-owns': popupId + }); + + var inputsContainer, hintInputElem; + //add read-only input to show hint + if (showHint) { + inputsContainer = angular.element('
'); + inputsContainer.css('position', 'relative'); + element.after(inputsContainer); + hintInputElem = element.clone(); + hintInputElem.attr('placeholder', ''); + hintInputElem.attr('tabindex', '-1'); + hintInputElem.val(''); + hintInputElem.css({ + 'position': 'absolute', + 'top': '0px', + 'left': '0px', + 'border-color': 'transparent', + 'box-shadow': 'none', + 'opacity': 1, + 'background': 'none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)', + 'color': '#999' + }); + element.css({ + 'position': 'relative', + 'vertical-align': 'top', + 'background-color': 'transparent' + }); - var hasFocus; + if (hintInputElem.attr('id')) { + hintInputElem.removeAttr('id'); // remove duplicate id if present. + } + inputsContainer.append(hintInputElem); + hintInputElem.after(element); + } - //Used to avoid bug in iOS webview where iOS keyboard does not fire - //mousedown & mouseup events - //Issue #3699 - var selected; + //pop-up element used to display matches + var popUpEl = angular.element('
'); + var clearBtnEl = angular.element(''); + popUpEl.attr({ + id: popupId, + matches: 'matches', + active: 'activeIdx', + select: 'select(activeIdx, evt)', + 'move-in-progress': 'moveInProgress', + query: 'query', + position: 'position', + 'assign-is-open': 'assignIsOpen(isOpen)', + debounce: 'debounceUpdate' + }); + //custom item template + if (angular.isDefined(attrs.typeaheadTemplateUrl)) { + popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); + } - //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 - }); + if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) { + popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl); + } - var inputsContainer, hintInputElem; - //add read-only input to show hint + var resetHint = function() { if (showHint) { - inputsContainer = angular.element('
'); - inputsContainer.css('position', 'relative'); - element.after(inputsContainer); - hintInputElem = element.clone(); - hintInputElem.attr('placeholder', ''); - hintInputElem.attr('tabindex', '-1'); hintInputElem.val(''); - hintInputElem.css({ - 'position': 'absolute', - 'top': '0px', - 'left': '0px', - 'border-color': 'transparent', - 'box-shadow': 'none', - 'opacity': 1, - 'background': 'none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)', - 'color': '#999' - }); - element.css({ - 'position': 'relative', - 'vertical-align': 'top', - 'background-color': 'transparent' - }); - - if (hintInputElem.attr('id')) { - hintInputElem.removeAttr('id'); // remove duplicate id if present. - } - inputsContainer.append(hintInputElem); - hintInputElem.after(element); } + }; - //pop-up element used to display matches - var popUpEl = angular.element('
'); - var clearBtnEl = angular.element(''); - popUpEl.attr({ - id: popupId, - matches: 'matches', - active: 'activeIdx', - select: 'select(activeIdx, evt)', - 'move-in-progress': 'moveInProgress', - query: 'query', - position: 'position', - 'assign-is-open': 'assignIsOpen(isOpen)', - debounce: 'debounceUpdate' - }); - //custom item template - if (angular.isDefined(attrs.typeaheadTemplateUrl)) { - popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); + var resetMatches = function() { + scope.matches = []; + scope.activeIdx = -1; + element.attr('aria-expanded', false); + resetHint(); + }; + + 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)); } + }); - if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) { - popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl); + var inputIsExactMatch = function(inputValue, index) { + if (scope.matches.length > index && inputValue) { + return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase(); } - var resetHint = function() { - if (showHint) { - hintInputElem.val(''); - } - }; - - var resetMatches = function() { - scope.matches = []; - scope.activeIdx = -1; - element.attr('aria-expanded', false); - resetHint(); - }; - - 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)); - } - }); + return false; + }; - var inputIsExactMatch = function(inputValue, index) { - if (scope.matches.length > index && inputValue) { - return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase(); - } + var getMatchesAsync = function(inputValue, evt) { + 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] + }); + } - return false; - }; - - var getMatchesAsync = function(inputValue, evt) { - 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); - 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)) { - if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { - $$debounce(function() { - scope.select(0, evt); - }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); - } else { + //Select the single remaining option if user input matches + if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) { + if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { + $$debounce(function() { scope.select(0, evt); - } + }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); + } else { + scope.select(0, evt); } + } - if (showHint) { - var firstLabel = scope.matches[0].label; - if (angular.isString(inputValue) && - inputValue.length > 0 && - firstLabel.slice(0, inputValue.length).toUpperCase() === inputValue.toUpperCase()) { - hintInputElem.val(inputValue + firstLabel.slice(inputValue.length)); - } else { - hintInputElem.val(''); - } + if (showHint) { + var firstLabel = scope.matches[0].label; + if (angular.isString(inputValue) && + inputValue.length > 0 && + firstLabel.slice(0, inputValue.length).toUpperCase() === inputValue.toUpperCase()) { + hintInputElem.val(inputValue + firstLabel.slice(inputValue.length)); + } else { + hintInputElem.val(''); } - } else { - resetMatches(); - isNoResultsSetter(originalScope, true); } + } else { + resetMatches(); + isNoResultsSetter(originalScope, true); } - if (onCurrentRequest) { - isLoadingSetter(originalScope, false); - } - }, function() { - resetMatches(); + } + if (onCurrentRequest) { isLoadingSetter(originalScope, false); - isNoResultsSetter(originalScope, true); - }); - }; - - // bind events only if appendToBody params exist - performance feature - if (appendToBody) { - angular.element($window).on('resize', fireRecalculating); - $document.find('body').on('scroll', fireRecalculating); - } - - // Declare the debounced function outside recalculating for - // proper debouncing - var debouncedRecalculate = $$debounce(function() { - // if popup is visible - if (scope.matches.length) { - recalculatePosition(); } + }, function() { + resetMatches(); + isLoadingSetter(originalScope, false); + isNoResultsSetter(originalScope, true); + }); + }; - scope.moveInProgress = false; - }, eventDebounceTime); + // bind events only if appendToBody params exist - performance feature + if (appendToBody) { + angular.element($window).on('resize', fireRecalculating); + $document.find('body').on('scroll', fireRecalculating); + } + + // Declare the debounced function outside recalculating for + // proper debouncing + var debouncedRecalculate = $$debounce(function() { + // if popup is visible + if (scope.matches.length) { + recalculatePosition(); + } - // Default progress type scope.moveInProgress = false; + }, eventDebounceTime); - function fireRecalculating() { - if (!scope.moveInProgress) { - scope.moveInProgress = true; - scope.$digest(); - } + // Default progress type + scope.moveInProgress = false; - debouncedRecalculate(); + function fireRecalculating() { + if (!scope.moveInProgress) { + scope.moveInProgress = true; + scope.$digest(); } - // 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'); + debouncedRecalculate(); + } - scope.positionClearBtn = $position.position(element); - scope.positionClearBtn.top += element.prop('offsetHeight')/4; - scope.positionClearBtn.left += element.prop('offsetWidth')-20; + // 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'); + scope.positionClearBtn = $position.position(element); + scope.positionClearBtn.top += element.prop('offsetHeight')/4; + scope.positionClearBtn.left += element.prop('offsetWidth')-20; - } - //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; + //we need to propagate user's query so we can higlight matches + scope.query = undefined; - var scheduleSearchWithTimeout = function(inputValue) { - timeoutPromise = $timeout(function() { - getMatchesAsync(inputValue); - }, waitTime); - }; + //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later + var timeoutPromise; - var cancelPreviousTimeout = function() { - if (timeoutPromise) { - $timeout.cancel(timeoutPromise); - } - }; + var scheduleSearchWithTimeout = function(inputValue) { + timeoutPromise = $timeout(function() { + getMatchesAsync(inputValue); + }, waitTime); + }; - resetMatches(); - //method for clear blutton - scope.clearModel = function(){ - $setModelValue(originalScope, ''); - scope.selectedM = false; - scope.$digest(); - }; - scope.assignIsOpen = function (isOpen) { - isOpenSetter(originalScope, isOpen); - }; + var cancelPreviousTimeout = function() { + if (timeoutPromise) { + $timeout.cancel(timeoutPromise); + } + }; - scope.select = function(activeIdx, evt) { - //called from within the $digest() cycle - var locals = {}; - var model, item; + resetMatches(); + //method for clear blutton + scope.clearModel = function(){ + $setModelValue(originalScope, ''); + scope.selectedM = false; + }; + scope.assignIsOpen = function (isOpen) { + isOpenSetter(originalScope, isOpen); + }; - selected = true; - //indicator that some choise selected for clear button - scope.selectedM = 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), - $event: evt - }); + scope.select = function(activeIdx, evt) { + //called from within the $digest() cycle + var locals = {}; + var model, item; + + selected = true; + //indicator that some choise selected for clear button + scope.selectedM = 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), + $event: evt + }); - resetMatches(); + 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); - } - }; + //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.on('keydown', function(evt) { - //typeahead is open and an "interesting" key was pressed - if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { - return; - } + //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) + element.on('keydown', function(evt) { + //typeahead is open and an "interesting" key was pressed + if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { + return; + } - var shouldSelect = isSelectEvent(originalScope, {$event: evt}); + var shouldSelect = isSelectEvent(originalScope, {$event: evt}); - /** - * if there's nothing selected (i.e. focusFirst) and enter or tab is hit - * or - * shift + tab is pressed to bring focus to the previous element - * then clear the results - */ - if (scope.activeIdx === -1 && shouldSelect || evt.which === 9 && !!evt.shiftKey) { - resetMatches(); - scope.$digest(); - return; - } + /** + * if there's nothing selected (i.e. focusFirst) and enter or tab is hit + * or + * shift + tab is pressed to bring focus to the previous element + * then clear the results + */ + if (scope.activeIdx === -1 && shouldSelect || evt.which === 9 && !!evt.shiftKey) { + resetMatches(); + scope.$digest(); + return; + } - evt.preventDefault(); - var target; - switch (evt.which) { - case 27: // escape - evt.stopPropagation(); + evt.preventDefault(); + var target; + switch (evt.which) { + case 27: // escape + evt.stopPropagation(); - resetMatches(); - originalScope.$digest(); - break; - case 38: // up arrow - scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1; - scope.$digest(); - target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx]; - target.parentNode.scrollTop = target.offsetTop; - break; - case 40: // down arrow - scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; - scope.$digest(); - target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx]; - target.parentNode.scrollTop = target.offsetTop; - break; - default: - if (shouldSelect) { - scope.$apply(function() { - if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { - $$debounce(function() { - scope.select(scope.activeIdx, evt); - }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); - } else { + resetMatches(); + originalScope.$digest(); + break; + case 38: // up arrow + scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1; + scope.$digest(); + target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx]; + target.parentNode.scrollTop = target.offsetTop; + break; + case 40: // down arrow + scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; + scope.$digest(); + target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx]; + target.parentNode.scrollTop = target.offsetTop; + break; + default: + if (shouldSelect) { + scope.$apply(function() { + if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { + $$debounce(function() { scope.select(scope.activeIdx, evt); - } - }); - } - } - }); - - element.bind('focus', function (evt) { - hasFocus = true; - if (minLength === 0 && !modelCtrl.$viewValue) { - $timeout(function() { - getMatchesAsync(modelCtrl.$viewValue, evt); - }, 0); - } - }); - - element.bind('blur', function(evt) { - if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) { - selected = true; - scope.$apply(function() { - if (angular.isObject(scope.debounceUpdate) && angular.isNumber(scope.debounceUpdate.blur)) { - $$debounce(function() { + }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); + } else { scope.select(scope.activeIdx, evt); - }, scope.debounceUpdate.blur); - } else { - scope.select(scope.activeIdx, evt); - } - }); - } - if (!isEditable && modelCtrl.$error.editable) { - modelCtrl.$setViewValue(); - scope.$apply(function() { - // Reset validity as we are clearing - modelCtrl.$setValidity('editable', true); - modelCtrl.$setValidity('parse', true); - }); - element.val(''); - } - hasFocus = false; - selected = false; - }); + } + }); + } + } + }); + + element.bind('focus', function (evt) { + hasFocus = true; + if (minLength === 0 && !modelCtrl.$viewValue) { + $timeout(function() { + getMatchesAsync(modelCtrl.$viewValue, evt); + }, 0); + } + }); - // 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) { - originalScope.$digest(); + element.bind('blur', function(evt) { + if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) { + selected = true; + scope.$apply(function() { + if (angular.isObject(scope.debounceUpdate) && angular.isNumber(scope.debounceUpdate.blur)) { + $$debounce(function() { + scope.select(scope.activeIdx, evt); + }, scope.debounceUpdate.blur); + } else { + scope.select(scope.activeIdx, evt); } + }); + } + if (!isEditable && modelCtrl.$error.editable) { + modelCtrl.$setViewValue(); + scope.$apply(function() { + // Reset validity as we are clearing + modelCtrl.$setValidity('editable', true); + modelCtrl.$setValidity('parse', true); + }); + element.val(''); + } + 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) { + originalScope.$digest(); } - }; + } + }; - $document.on('click', dismissClickHandler); + $document.on('click', dismissClickHandler); - originalScope.$on('$destroy', function() { - $document.off('click', dismissClickHandler); - if (appendToBody || appendTo) { - $popup.remove(); - } + originalScope.$on('$destroy', function() { + $document.off('click', dismissClickHandler); + if (appendToBody || appendTo) { + $popup.remove(); + } - if (appendToBody) { - angular.element($window).off('resize', fireRecalculating); - $document.find('body').off('scroll', fireRecalculating); - } - // Prevent jQuery cache memory leak - popUpEl.remove(); + if (appendToBody) { + angular.element($window).off('resize', fireRecalculating); + $document.find('body').off('scroll', fireRecalculating); + } + // Prevent jQuery cache memory leak + popUpEl.remove(); - if (showHint) { + if (showHint) { inputsContainer.remove(); - } - }); + } + }); - var $popup = $compile(popUpEl)(scope); - var $clearBtn = $compile(clearBtnEl)(scope); + var $popup = $compile(popUpEl)(scope); + var $clearBtn = $compile(clearBtnEl)(scope); - if (appendToBody) { - $document.find('body').append($popup); - } else if (appendTo) { - angular.element(appendTo).eq(0).append($popup); - } else { - element.after($popup); - } - element.after($clearBtn); + if (appendToBody) { + $document.find('body').append($popup); + } else if (appendTo) { + angular.element(appendTo).eq(0).append($popup); + } else { + element.after($popup); + } + element.after($clearBtn); - this.init = function(_modelCtrl, _ngModelOptions) { - modelCtrl = _modelCtrl; - ngModelOptions = _ngModelOptions; + this.init = function(_modelCtrl, _ngModelOptions) { + modelCtrl = _modelCtrl; + ngModelOptions = _ngModelOptions; - scope.debounceUpdate = modelCtrl.$options && $parse(modelCtrl.$options.debounce)(originalScope); + scope.debounceUpdate = modelCtrl.$options && $parse(modelCtrl.$options.debounce)(originalScope); - //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; + //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); + if (minLength === 0 || inputValue && inputValue.length >= minLength) { + if (waitTime > 0) { cancelPreviousTimeout(); - resetMatches(); - if(!inputValue) { - scope.selectedM = false;//hide clear button if input empty - } + scheduleSearchWithTimeout(inputValue); + } else { + getMatchesAsync(inputValue); } - - if (isEditable) { - return inputValue; + } else { + isLoadingSetter(originalScope, false); + cancelPreviousTimeout(); + resetMatches(); + if(!inputValue) { + scope.selectedM = false;//hide clear button if input empty } + } - if (!inputValue) { - // Reset in case user had typed something previously. - modelCtrl.$setValidity('editable', true); - return null; - } + if (isEditable) { + return inputValue; + } - modelCtrl.$setValidity('editable', false); - return undefined; - }); + if (!inputValue) { + // Reset in case user had typed something previously. + modelCtrl.$setValidity('editable', true); + return null; + } - modelCtrl.$formatters.push(function(modelValue) { - var candidateViewValue, emptyViewValue; - var locals = {}; + modelCtrl.$setValidity('editable', false); + return undefined; + }); - // 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); - } + modelCtrl.$formatters.push(function(modelValue) { + var candidateViewValue, emptyViewValue; + var locals = {}; - if (inputFormatter) { - locals.$model = modelValue; - return inputFormatter(originalScope, 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); + } - //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); + if (inputFormatter) { + locals.$model = modelValue; + return inputFormatter(originalScope, locals); + } - return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue; - }); - }; - }]) + //it might happen that we don't have enough info to properly render input value + //we need to check for this situation and simply return model value if we can't apply custom formatting + locals[parserResult.itemName] = modelValue; + candidateViewValue = parserResult.viewMapper(originalScope, locals); + locals[parserResult.itemName] = undefined; + emptyViewValue = parserResult.viewMapper(originalScope, locals); + + return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue; + }); + }; + }]) .directive('uibTypeahead', function() { return { From cf5b2b63d5c2873c55e53384c4bfad5a71f936f0 Mon Sep 17 00:00:00 2001 From: shootermv Date: Fri, 19 Aug 2016 10:23:05 +0300 Subject: [PATCH 08/11] feat(typeahead): removed some extra spaces & indents --- src/typeahead/typeahead.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 138f7f1ec0..105de30c4e 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -322,8 +322,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap scope.position.top += element.prop('offsetHeight'); scope.positionClearBtn = $position.position(element); - scope.positionClearBtn.top += element.prop('offsetHeight')/4; - scope.positionClearBtn.left += element.prop('offsetWidth')-20; + scope.positionClearBtn.top += element.prop('offsetHeight') / 4; + scope.positionClearBtn.left += element.prop('offsetWidth') - 20; } @@ -348,7 +348,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap resetMatches(); //method for clear blutton - scope.clearModel = function(){ + scope.clearModel = function() { $setModelValue(originalScope, ''); scope.selectedM = false; }; From 2f0eecdbe3a7b57fa251a9c2195251560d492630 Mon Sep 17 00:00:00 2001 From: shootermv Date: Fri, 19 Aug 2016 10:23:48 +0300 Subject: [PATCH 09/11] feat(typeahead): removed some extra spaces & indents 2 --- src/typeahead/test/typeahead.spec.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/typeahead/test/typeahead.spec.js b/src/typeahead/test/typeahead.spec.js index ff1c42510e..fb57c5d0fd 100644 --- a/src/typeahead/test/typeahead.spec.js +++ b/src/typeahead/test/typeahead.spec.js @@ -68,7 +68,6 @@ describe('typeahead tests', function() { return element.find('.glyphicon-remove'); }; - var triggerKeyDown = function(element, keyCode, options) { options = options || {}; var inputEl = findInput(element); @@ -246,7 +245,7 @@ describe('typeahead tests', function() { triggerKeyDown(element, 13); expect($scope.result).toEqual('prefixfoo'); var clearBtn = findClearBtn(element); - expect(clearBtn).not.toHaveClass('ng-hide'); + expect(clearBtn).not.toHaveClass('ng-hide'); }); it('should hide X button when selected item removed and input got empty', function() { @@ -259,7 +258,7 @@ describe('typeahead tests', function() { expect($scope.result).toEqual('prefixfoo'); var clearBtn = findClearBtn(element); changeInputValueTo(element, ''); - expect(clearBtn).toHaveClass('ng-hide'); + expect(clearBtn).toHaveClass('ng-hide'); }); it('when pressing X button should clear the ngModel', function() { @@ -270,10 +269,10 @@ describe('typeahead tests', function() { changeInputValueTo(element, 'f'); triggerKeyDown(element, 13); expect($scope.result).toEqual('prefixfoo'); - var clearBtn = findClearBtn(element); + var clearBtn = findClearBtn(element); clearBtn.click(); $scope.$digest(); - expect($scope.result).toEqual(''); + expect($scope.result).toEqual(''); }); it('should support custom label rendering function', function() { From 3a58a600df4633a374c407d184f4e4fade0d5cf4 Mon Sep 17 00:00:00 2001 From: shootermv Date: Fri, 19 Aug 2016 00:37:43 +0300 Subject: [PATCH 10/11] feat(typeahead): tests created for clear button feat(typeahead): removed all extra indents and $scope.$digest feat(typeahead): removed some extra spaces & indents feat(typeahead): removed some extra spaces & indents 2 --- src/typeahead/test/typeahead.spec.js | 43 ++ src/typeahead/typeahead.js | 940 ++++++++++++++------------- 2 files changed, 514 insertions(+), 469 deletions(-) diff --git a/src/typeahead/test/typeahead.spec.js b/src/typeahead/test/typeahead.spec.js index 4423b8a66e..fb57c5d0fd 100644 --- a/src/typeahead/test/typeahead.spec.js +++ b/src/typeahead/test/typeahead.spec.js @@ -64,6 +64,10 @@ describe('typeahead tests', function() { return findDropDown(element).find('li'); }; + var findClearBtn = function(element) { + return element.find('.glyphicon-remove'); + }; + var triggerKeyDown = function(element, keyCode, options) { options = options || {}; var inputEl = findInput(element); @@ -232,6 +236,45 @@ describe('typeahead tests', function() { expect($scope.result).toEqual('prefixfoo'); }); + it('should display X button after some option selected', function() { + $scope.updaterFn = function(selectedItem) { + return 'prefix' + selectedItem; + }; + var element = prepareInputEl('
'); + changeInputValueTo(element, 'f'); + triggerKeyDown(element, 13); + expect($scope.result).toEqual('prefixfoo'); + var clearBtn = findClearBtn(element); + expect(clearBtn).not.toHaveClass('ng-hide'); + }); + + it('should hide X button when selected item removed and input got empty', function() { + $scope.updaterFn = function(selectedItem) { + return 'prefix' + selectedItem; + }; + var element = prepareInputEl('
'); + changeInputValueTo(element, 'f'); + triggerKeyDown(element, 13); + expect($scope.result).toEqual('prefixfoo'); + var clearBtn = findClearBtn(element); + changeInputValueTo(element, ''); + expect(clearBtn).toHaveClass('ng-hide'); + }); + + it('when pressing X button should clear the ngModel', function() { + $scope.updaterFn = function(selectedItem) { + return 'prefix' + selectedItem; + }; + var element = prepareInputEl('
'); + changeInputValueTo(element, 'f'); + triggerKeyDown(element, 13); + expect($scope.result).toEqual('prefixfoo'); + var clearBtn = findClearBtn(element); + clearBtn.click(); + $scope.$digest(); + expect($scope.result).toEqual(''); + }); + it('should support custom label rendering function', function() { $scope.formatterFn = function(sourceItem) { return 'prefix' + sourceItem; diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index b6351900cd..105de30c4e 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -13,7 +13,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap if (!match) { throw new Error( 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' + - ' but got "' + input + '".'); + ' but got "' + input + '".'); } return { @@ -28,567 +28,569 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap .controller('UibTypeaheadController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$$debounce', '$uibPosition', 'uibTypeaheadParser', function(originalScope, element, attrs, $compile, $parse, $q, $timeout, $document, $window, $rootScope, $$debounce, $position, typeaheadParser) { - var HOT_KEYS = [9, 13, 27, 38, 40]; - var eventDebounceTime = 200; - var modelCtrl, ngModelOptions; - //SUPPORTED ATTRIBUTES (OPTIONS) - - //minimal no of characters that needs to be entered before typeahead kicks-in - var minLength = originalScope.$eval(attrs.typeaheadMinLength); - if (!minLength && minLength !== 0) { - minLength = 1; - } + var HOT_KEYS = [9, 13, 27, 38, 40]; + var eventDebounceTime = 200; + var modelCtrl, ngModelOptions; + //SUPPORTED ATTRIBUTES (OPTIONS) + + //minimal no of characters that needs to be entered before typeahead kicks-in + var minLength = originalScope.$eval(attrs.typeaheadMinLength); + if (!minLength && minLength !== 0) { + minLength = 1; + } - originalScope.$watch(attrs.typeaheadMinLength, function (newVal) { + originalScope.$watch(attrs.typeaheadMinLength, function (newVal) { minLength = !newVal && newVal !== 0 ? 1 : newVal; - }); + }); - //minimal wait time after last character typed before typeahead kicks-in - var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0; + //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; - originalScope.$watch(attrs.typeaheadEditable, function (newVal) { - isEditable = newVal !== false; - }); + //should it restrict model values to the ones selected from the popup only? + var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false; + originalScope.$watch(attrs.typeaheadEditable, function (newVal) { + isEditable = newVal !== false; + }); - //binding to a variable that indicates if matches are being retrieved asynchronously - var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; + //binding to a variable that indicates if matches are being retrieved asynchronously + var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop; - //a function to determine if an event should cause selection - var isSelectEvent = attrs.typeaheadShouldSelect ? $parse(attrs.typeaheadShouldSelect) : function(scope, vals) { - var evt = vals.$event; - return evt.which === 13 || evt.which === 9; - }; + //a function to determine if an event should cause selection + var isSelectEvent = attrs.typeaheadShouldSelect ? $parse(attrs.typeaheadShouldSelect) : function(scope, vals) { + var evt = vals.$event; + return evt.which === 13 || evt.which === 9; + }; - //a callback executed when a match is selected - var onSelectCallback = $parse(attrs.typeaheadOnSelect); + //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; + //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; + //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 inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; - var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; + var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; - var appendTo = attrs.typeaheadAppendTo ? - originalScope.$eval(attrs.typeaheadAppendTo) : null; + var appendTo = attrs.typeaheadAppendTo ? + originalScope.$eval(attrs.typeaheadAppendTo) : null; - var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== 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; + //If input matches an item of the list exactly, select it automatically + var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false; - //binding to a variable that indicates if dropdown is open - var isOpenSetter = $parse(attrs.typeaheadIsOpen).assign || angular.noop; + //binding to a variable that indicates if dropdown is open + var isOpenSetter = $parse(attrs.typeaheadIsOpen).assign || angular.noop; - var showHint = originalScope.$eval(attrs.typeaheadShowHint) || false; + var showHint = originalScope.$eval(attrs.typeaheadShowHint) || false; - //INTERNAL VARIABLES + //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}); - } + //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}); + } - return parsedModel.assign(scope, newValue); - }; + return parsedModel.assign(scope, newValue); + }; - //expressions used by typeahead - var parserResult = typeaheadParser.parse(attrs.uibTypeahead); + //expressions used by typeahead + var parserResult = typeaheadParser.parse(attrs.uibTypeahead); + + var hasFocus; + + //Used to avoid bug in iOS webview where iOS keyboard does not fire + //mousedown & mouseup events + //Issue #3699 + var selected; + + //create a child scope for the typeahead directive so we are not polluting original scope + //with typeahead-specific data (matches, query etc.) + var scope = originalScope.$new(); + var offDestroy = originalScope.$on('$destroy', function() { + scope.$destroy(); + }); + scope.$on('$destroy', offDestroy); + + // WAI-ARIA + var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000); + element.attr({ + 'aria-autocomplete': 'list', + 'aria-expanded': false, + 'aria-owns': popupId + }); + + var inputsContainer, hintInputElem; + //add read-only input to show hint + if (showHint) { + inputsContainer = angular.element('
'); + inputsContainer.css('position', 'relative'); + element.after(inputsContainer); + hintInputElem = element.clone(); + hintInputElem.attr('placeholder', ''); + hintInputElem.attr('tabindex', '-1'); + hintInputElem.val(''); + hintInputElem.css({ + 'position': 'absolute', + 'top': '0px', + 'left': '0px', + 'border-color': 'transparent', + 'box-shadow': 'none', + 'opacity': 1, + 'background': 'none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)', + 'color': '#999' + }); + element.css({ + 'position': 'relative', + 'vertical-align': 'top', + 'background-color': 'transparent' + }); - var hasFocus; + if (hintInputElem.attr('id')) { + hintInputElem.removeAttr('id'); // remove duplicate id if present. + } + inputsContainer.append(hintInputElem); + hintInputElem.after(element); + } - //Used to avoid bug in iOS webview where iOS keyboard does not fire - //mousedown & mouseup events - //Issue #3699 - var selected; + //pop-up element used to display matches + var popUpEl = angular.element('
'); + var clearBtnEl = angular.element(''); + popUpEl.attr({ + id: popupId, + matches: 'matches', + active: 'activeIdx', + select: 'select(activeIdx, evt)', + 'move-in-progress': 'moveInProgress', + query: 'query', + position: 'position', + 'assign-is-open': 'assignIsOpen(isOpen)', + debounce: 'debounceUpdate' + }); + //custom item template + if (angular.isDefined(attrs.typeaheadTemplateUrl)) { + popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); + } - //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 - }); + if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) { + popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl); + } - var inputsContainer, hintInputElem; - //add read-only input to show hint + var resetHint = function() { if (showHint) { - inputsContainer = angular.element('
'); - inputsContainer.css('position', 'relative'); - element.after(inputsContainer); - hintInputElem = element.clone(); - hintInputElem.attr('placeholder', ''); - hintInputElem.attr('tabindex', '-1'); hintInputElem.val(''); - hintInputElem.css({ - 'position': 'absolute', - 'top': '0px', - 'left': '0px', - 'border-color': 'transparent', - 'box-shadow': 'none', - 'opacity': 1, - 'background': 'none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)', - 'color': '#999' - }); - element.css({ - 'position': 'relative', - 'vertical-align': 'top', - 'background-color': 'transparent' - }); - - if (hintInputElem.attr('id')) { - hintInputElem.removeAttr('id'); // remove duplicate id if present. - } - inputsContainer.append(hintInputElem); - hintInputElem.after(element); } + }; - //pop-up element used to display matches - var popUpEl = angular.element('
'); - var clearBtnEl = angular.element(''); - popUpEl.attr({ - id: popupId, - matches: 'matches', - active: 'activeIdx', - select: 'select(activeIdx, evt)', - 'move-in-progress': 'moveInProgress', - query: 'query', - position: 'position', - 'assign-is-open': 'assignIsOpen(isOpen)', - debounce: 'debounceUpdate' - }); - //custom item template - if (angular.isDefined(attrs.typeaheadTemplateUrl)) { - popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); + var resetMatches = function() { + scope.matches = []; + scope.activeIdx = -1; + element.attr('aria-expanded', false); + resetHint(); + }; + + 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)); } + }); - if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) { - popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl); + var inputIsExactMatch = function(inputValue, index) { + if (scope.matches.length > index && inputValue) { + return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase(); } - var resetHint = function() { - if (showHint) { - hintInputElem.val(''); - } - }; - - var resetMatches = function() { - scope.matches = []; - scope.activeIdx = -1; - element.attr('aria-expanded', false); - resetHint(); - }; - - 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)); - } - }); + return false; + }; - var inputIsExactMatch = function(inputValue, index) { - if (scope.matches.length > index && inputValue) { - return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase(); - } + var getMatchesAsync = function(inputValue, evt) { + 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] + }); + } - return false; - }; - - var getMatchesAsync = function(inputValue, evt) { - 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); - 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)) { - if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { - $$debounce(function() { - scope.select(0, evt); - }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); - } else { + //Select the single remaining option if user input matches + if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) { + if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { + $$debounce(function() { scope.select(0, evt); - } + }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); + } else { + scope.select(0, evt); } + } - if (showHint) { - var firstLabel = scope.matches[0].label; - if (angular.isString(inputValue) && - inputValue.length > 0 && - firstLabel.slice(0, inputValue.length).toUpperCase() === inputValue.toUpperCase()) { - hintInputElem.val(inputValue + firstLabel.slice(inputValue.length)); - } else { - hintInputElem.val(''); - } + if (showHint) { + var firstLabel = scope.matches[0].label; + if (angular.isString(inputValue) && + inputValue.length > 0 && + firstLabel.slice(0, inputValue.length).toUpperCase() === inputValue.toUpperCase()) { + hintInputElem.val(inputValue + firstLabel.slice(inputValue.length)); + } else { + hintInputElem.val(''); } - } else { - resetMatches(); - isNoResultsSetter(originalScope, true); } + } else { + resetMatches(); + isNoResultsSetter(originalScope, true); } - if (onCurrentRequest) { - isLoadingSetter(originalScope, false); - } - }, function() { - resetMatches(); + } + if (onCurrentRequest) { isLoadingSetter(originalScope, false); - isNoResultsSetter(originalScope, true); - }); - }; - - // bind events only if appendToBody params exist - performance feature - if (appendToBody) { - angular.element($window).on('resize', fireRecalculating); - $document.find('body').on('scroll', fireRecalculating); - } - - // Declare the debounced function outside recalculating for - // proper debouncing - var debouncedRecalculate = $$debounce(function() { - // if popup is visible - if (scope.matches.length) { - recalculatePosition(); } + }, function() { + resetMatches(); + isLoadingSetter(originalScope, false); + isNoResultsSetter(originalScope, true); + }); + }; - scope.moveInProgress = false; - }, eventDebounceTime); + // bind events only if appendToBody params exist - performance feature + if (appendToBody) { + angular.element($window).on('resize', fireRecalculating); + $document.find('body').on('scroll', fireRecalculating); + } + + // Declare the debounced function outside recalculating for + // proper debouncing + var debouncedRecalculate = $$debounce(function() { + // if popup is visible + if (scope.matches.length) { + recalculatePosition(); + } - // Default progress type scope.moveInProgress = false; + }, eventDebounceTime); - function fireRecalculating() { - if (!scope.moveInProgress) { - scope.moveInProgress = true; - scope.$digest(); - } + // Default progress type + scope.moveInProgress = false; - debouncedRecalculate(); + function fireRecalculating() { + if (!scope.moveInProgress) { + scope.moveInProgress = true; + scope.$digest(); } - // 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'); + debouncedRecalculate(); + } - scope.positionClearBtn = $position.position(element); - scope.positionClearBtn.top += element.prop('offsetHeight')/4; - scope.positionClearBtn.left += element.prop('offsetWidth')-20; + // 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'); + scope.positionClearBtn = $position.position(element); + scope.positionClearBtn.top += element.prop('offsetHeight') / 4; + scope.positionClearBtn.left += element.prop('offsetWidth') - 20; - } - //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; + //we need to propagate user's query so we can higlight matches + scope.query = undefined; - var scheduleSearchWithTimeout = function(inputValue) { - timeoutPromise = $timeout(function() { - getMatchesAsync(inputValue); - }, waitTime); - }; + //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later + var timeoutPromise; - var cancelPreviousTimeout = function() { - if (timeoutPromise) { - $timeout.cancel(timeoutPromise); - } - }; + var scheduleSearchWithTimeout = function(inputValue) { + timeoutPromise = $timeout(function() { + getMatchesAsync(inputValue); + }, waitTime); + }; - resetMatches(); - //method for clear blutton - scope.clearModel = function(){ - $setModelValue(originalScope, ''); - scope.selectedM = false; - scope.$digest(); - }; - scope.assignIsOpen = function (isOpen) { - isOpenSetter(originalScope, isOpen); - }; + var cancelPreviousTimeout = function() { + if (timeoutPromise) { + $timeout.cancel(timeoutPromise); + } + }; - scope.select = function(activeIdx, evt) { - //called from within the $digest() cycle - var locals = {}; - var model, item; + resetMatches(); + //method for clear blutton + scope.clearModel = function() { + $setModelValue(originalScope, ''); + scope.selectedM = false; + }; + scope.assignIsOpen = function (isOpen) { + isOpenSetter(originalScope, isOpen); + }; - selected = true; - //indicator that some choise selected for clear button - scope.selectedM = 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), - $event: evt - }); + scope.select = function(activeIdx, evt) { + //called from within the $digest() cycle + var locals = {}; + var model, item; + + selected = true; + //indicator that some choise selected for clear button + scope.selectedM = 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), + $event: evt + }); - resetMatches(); + 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); - } - }; + //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.on('keydown', function(evt) { - //typeahead is open and an "interesting" key was pressed - if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { - return; - } + //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27) + element.on('keydown', function(evt) { + //typeahead is open and an "interesting" key was pressed + if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) { + return; + } - var shouldSelect = isSelectEvent(originalScope, {$event: evt}); + var shouldSelect = isSelectEvent(originalScope, {$event: evt}); - /** - * if there's nothing selected (i.e. focusFirst) and enter or tab is hit - * or - * shift + tab is pressed to bring focus to the previous element - * then clear the results - */ - if (scope.activeIdx === -1 && shouldSelect || evt.which === 9 && !!evt.shiftKey) { - resetMatches(); - scope.$digest(); - return; - } + /** + * if there's nothing selected (i.e. focusFirst) and enter or tab is hit + * or + * shift + tab is pressed to bring focus to the previous element + * then clear the results + */ + if (scope.activeIdx === -1 && shouldSelect || evt.which === 9 && !!evt.shiftKey) { + resetMatches(); + scope.$digest(); + return; + } - evt.preventDefault(); - var target; - switch (evt.which) { - case 27: // escape - evt.stopPropagation(); + evt.preventDefault(); + var target; + switch (evt.which) { + case 27: // escape + evt.stopPropagation(); - resetMatches(); - originalScope.$digest(); - break; - case 38: // up arrow - scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1; - scope.$digest(); - target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx]; - target.parentNode.scrollTop = target.offsetTop; - break; - case 40: // down arrow - scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; - scope.$digest(); - target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx]; - target.parentNode.scrollTop = target.offsetTop; - break; - default: - if (shouldSelect) { - scope.$apply(function() { - if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { - $$debounce(function() { - scope.select(scope.activeIdx, evt); - }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); - } else { + resetMatches(); + originalScope.$digest(); + break; + case 38: // up arrow + scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1; + scope.$digest(); + target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx]; + target.parentNode.scrollTop = target.offsetTop; + break; + case 40: // down arrow + scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; + scope.$digest(); + target = popUpEl[0].querySelectorAll('.uib-typeahead-match')[scope.activeIdx]; + target.parentNode.scrollTop = target.offsetTop; + break; + default: + if (shouldSelect) { + scope.$apply(function() { + if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) { + $$debounce(function() { scope.select(scope.activeIdx, evt); - } - }); - } - } - }); - - element.bind('focus', function (evt) { - hasFocus = true; - if (minLength === 0 && !modelCtrl.$viewValue) { - $timeout(function() { - getMatchesAsync(modelCtrl.$viewValue, evt); - }, 0); - } - }); - - element.bind('blur', function(evt) { - if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) { - selected = true; - scope.$apply(function() { - if (angular.isObject(scope.debounceUpdate) && angular.isNumber(scope.debounceUpdate.blur)) { - $$debounce(function() { + }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']); + } else { scope.select(scope.activeIdx, evt); - }, scope.debounceUpdate.blur); - } else { - scope.select(scope.activeIdx, evt); - } - }); - } - if (!isEditable && modelCtrl.$error.editable) { - modelCtrl.$setViewValue(); - scope.$apply(function() { - // Reset validity as we are clearing - modelCtrl.$setValidity('editable', true); - modelCtrl.$setValidity('parse', true); - }); - element.val(''); - } - hasFocus = false; - selected = false; - }); + } + }); + } + } + }); + + element.bind('focus', function (evt) { + hasFocus = true; + if (minLength === 0 && !modelCtrl.$viewValue) { + $timeout(function() { + getMatchesAsync(modelCtrl.$viewValue, evt); + }, 0); + } + }); - // 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) { - originalScope.$digest(); + element.bind('blur', function(evt) { + if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) { + selected = true; + scope.$apply(function() { + if (angular.isObject(scope.debounceUpdate) && angular.isNumber(scope.debounceUpdate.blur)) { + $$debounce(function() { + scope.select(scope.activeIdx, evt); + }, scope.debounceUpdate.blur); + } else { + scope.select(scope.activeIdx, evt); } + }); + } + if (!isEditable && modelCtrl.$error.editable) { + modelCtrl.$setViewValue(); + scope.$apply(function() { + // Reset validity as we are clearing + modelCtrl.$setValidity('editable', true); + modelCtrl.$setValidity('parse', true); + }); + element.val(''); + } + 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) { + originalScope.$digest(); } - }; + } + }; - $document.on('click', dismissClickHandler); + $document.on('click', dismissClickHandler); - originalScope.$on('$destroy', function() { - $document.off('click', dismissClickHandler); - if (appendToBody || appendTo) { - $popup.remove(); - } + originalScope.$on('$destroy', function() { + $document.off('click', dismissClickHandler); + if (appendToBody || appendTo) { + $popup.remove(); + } - if (appendToBody) { - angular.element($window).off('resize', fireRecalculating); - $document.find('body').off('scroll', fireRecalculating); - } - // Prevent jQuery cache memory leak - popUpEl.remove(); + if (appendToBody) { + angular.element($window).off('resize', fireRecalculating); + $document.find('body').off('scroll', fireRecalculating); + } + // Prevent jQuery cache memory leak + popUpEl.remove(); - if (showHint) { + if (showHint) { inputsContainer.remove(); - } - }); + } + }); - var $popup = $compile(popUpEl)(scope); - var $clearBtn = $compile(clearBtnEl)(scope); + var $popup = $compile(popUpEl)(scope); + var $clearBtn = $compile(clearBtnEl)(scope); - if (appendToBody) { - $document.find('body').append($popup); - } else if (appendTo) { - angular.element(appendTo).eq(0).append($popup); - } else { - element.after($popup); - } - element.after($clearBtn); + if (appendToBody) { + $document.find('body').append($popup); + } else if (appendTo) { + angular.element(appendTo).eq(0).append($popup); + } else { + element.after($popup); + } + element.after($clearBtn); - this.init = function(_modelCtrl, _ngModelOptions) { - modelCtrl = _modelCtrl; - ngModelOptions = _ngModelOptions; + this.init = function(_modelCtrl, _ngModelOptions) { + modelCtrl = _modelCtrl; + ngModelOptions = _ngModelOptions; - scope.debounceUpdate = modelCtrl.$options && $parse(modelCtrl.$options.debounce)(originalScope); + scope.debounceUpdate = modelCtrl.$options && $parse(modelCtrl.$options.debounce)(originalScope); - //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; + //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); + if (minLength === 0 || inputValue && inputValue.length >= minLength) { + if (waitTime > 0) { cancelPreviousTimeout(); - resetMatches(); + scheduleSearchWithTimeout(inputValue); + } else { + getMatchesAsync(inputValue); } - - if (isEditable) { - return inputValue; + } else { + isLoadingSetter(originalScope, false); + cancelPreviousTimeout(); + resetMatches(); + if(!inputValue) { + scope.selectedM = false;//hide clear button if input empty } + } - if (!inputValue) { - // Reset in case user had typed something previously. - modelCtrl.$setValidity('editable', true); - return null; - } + if (isEditable) { + return inputValue; + } - modelCtrl.$setValidity('editable', false); - return undefined; - }); + if (!inputValue) { + // Reset in case user had typed something previously. + modelCtrl.$setValidity('editable', true); + return null; + } - modelCtrl.$formatters.push(function(modelValue) { - var candidateViewValue, emptyViewValue; - var locals = {}; + modelCtrl.$setValidity('editable', false); + return undefined; + }); - // 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); - } + modelCtrl.$formatters.push(function(modelValue) { + var candidateViewValue, emptyViewValue; + var locals = {}; - if (inputFormatter) { - locals.$model = modelValue; - return inputFormatter(originalScope, 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); + } - //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); + if (inputFormatter) { + locals.$model = modelValue; + return inputFormatter(originalScope, locals); + } - return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue; - }); - }; - }]) + //it might happen that we don't have enough info to properly render input value + //we need to check for this situation and simply return model value if we can't apply custom formatting + locals[parserResult.itemName] = modelValue; + candidateViewValue = parserResult.viewMapper(originalScope, locals); + locals[parserResult.itemName] = undefined; + emptyViewValue = parserResult.viewMapper(originalScope, locals); + + return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue; + }); + }; + }]) .directive('uibTypeahead', function() { return { From a6d3dc8c2e4dbecbebb48dc7e82bd9f4c339e702 Mon Sep 17 00:00:00 2001 From: shootermv Date: Sun, 21 Aug 2016 10:10:24 +0300 Subject: [PATCH 11/11] feat(typeahead): following recent fixes --- src/typeahead/typeahead.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index 105de30c4e..2eaa5a38a4 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -162,7 +162,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap //pop-up element used to display matches var popUpEl = angular.element('
'); - var clearBtnEl = angular.element(''); + var clearBtnEl = angular.element(''); popUpEl.attr({ id: popupId, matches: 'matches', @@ -320,12 +320,9 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap function recalculatePosition() { scope.position = appendToBody ? $position.offset(element) : $position.position(element); scope.position.top += element.prop('offsetHeight'); - scope.positionClearBtn = $position.position(element); scope.positionClearBtn.top += element.prop('offsetHeight') / 4; scope.positionClearBtn.left += element.prop('offsetWidth') - 20; - - } //we need to propagate user's query so we can higlight matches @@ -350,7 +347,7 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap //method for clear blutton scope.clearModel = function() { $setModelValue(originalScope, ''); - scope.selectedM = false; + scope.showClearBtn = false; }; scope.assignIsOpen = function (isOpen) { isOpenSetter(originalScope, isOpen); @@ -362,9 +359,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap var model, item; selected = true; - //indicator that some choise selected for clear button - scope.selectedM = true; + scope.showClearBtn = true; // Indicator that some choice selected (clear button should appear) locals[parserResult.itemName] = item = scope.matches[activeIdx].model; model = parserResult.modelMapper(originalScope, locals); $setModelValue(originalScope, model); @@ -545,8 +541,8 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap isLoadingSetter(originalScope, false); cancelPreviousTimeout(); resetMatches(); - if(!inputValue) { - scope.selectedM = false;//hide clear button if input empty + if (!inputValue) { + scope.showClearBtn = false; // Hide clear button if input empty } }