javascript - Promise not resolving in custom TypeAhead directive -


i edited typeahead directive part of angular ui angularjs give suggestions based on recent word, delimited space (" ").

i intend use query builder, dynamically giving suggestions based on surrounding syntax. works expected first word, once second word, promise not resolve anymore reason. value of inputvalue correct , expected, code inside

$q.when(parserresult.source(originalscope, locals)).then(function (matches) {

does not appear run. please advise.

my code (exactly same original except added function called getlastword truncates current expression :

angular.module('customtypeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindhtml'])  .factory('customtypeaheadparser', ['$parse', function ($parse) {      //                      00000111000000000000022200000000000000003333333333333330000000000044000     var typeahead_regexp = /^\s*([\s\s]+?)(?:\s+as\s+([\s\s]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\s]+?)$/;      return {         parse: function (input) {              var match = input.match(typeahead_regexp);             if (!match) {                 throw new error(                     'expected customtypeahead specification in form of "_modelvalue_ (as _label_)? _item_ in _collection_"' +                     ' got "' + input + '".');             }              return {                 itemname: match[3],                 source: $parse(match[4]),                 viewmapper: $parse(match[2] || match[1]),                 modelmapper: $parse(match[1])             };         }     }; }])  .directive('customtypeahead', ['$compile', '$parse', '$q', '$timeout', '$document', '$rootscope', '$position', 'customtypeaheadparser',     function ($compile, $parse, $q, $timeout, $document, $rootscope, $position, customtypeaheadparser) {          var hot_keys = [9, 13, 27, 38, 40];          return {             require: 'ngmodel',             link: function (originalscope, element, attrs, modelctrl) {                  //supported attributes (options)                  //minimal no of characters needs entered before customtypeahead kicks-in                 var minlength = originalscope.$eval(attrs.customtypeaheadminlength);                 if (!minlength && minlength !== 0) {                     minlength = 0;                 }                  //minimal wait time after last character typed before customtypeahead kicks-in                 var waittime = originalscope.$eval(attrs.customtypeaheadwaitms) || 0;                  //should restrict model values ones selected popup only?                 var iseditable = originalscope.$eval(attrs.customtypeaheadeditable) !== false;                  //binding variable indicates if matches being retrieved asynchronously                 var isloadingsetter = $parse(attrs.customtypeaheadloading).assign || angular.noop;                  //a callback executed when match selected                 var onselectcallback = $parse(attrs.customtypeaheadonselect);                  var inputformatter = attrs.customtypeaheadinputformatter ? $parse(attrs.customtypeaheadinputformatter) : undefined;                  var appendtobody = attrs.customtypeaheadappendtobody ? originalscope.$eval(attrs.customtypeaheadappendtobody) : false;                  var focusfirst = originalscope.$eval(attrs.customtypeaheadfocusfirst) !== false;                  //internal variables                  //model setter executed upon match selection                 var $setmodelvalue = $parse(attrs.ngmodel).assign;                  //expressions used customtypeahead                 var parserresult = customtypeaheadparser.parse(attrs.customtypeahead);                  var hasfocus;                  //create child scope customtypeahead directive not polluting original scope                 //with customtypeahead-specific data (matches, query etc.)                 var scope = originalscope.$new();                 originalscope.$on('$destroy', function () {                     scope.$destroy();                 });                  // wai-aria                 var popupid = 'customtypeahead-' + scope.$id + '-' + math.floor(math.random() * 10000);                 element.attr({                     'aria-autocomplete': 'list',                     'aria-expanded': false,                     'aria-owns': popupid                 });                  //pop-up element used display matches                 var popupel = angular.element('<div custom-typeahead-popup></div>');                 popupel.attr({                     id: popupid,                     matches: 'matches',                     active: 'activeidx',                     select: 'select(activeidx)',                     query: 'query',                     position: 'position'                 });                 //custom item template                 if (angular.isdefined(attrs.customtypeaheadtemplateurl)) {                     popupel.attr('template-url', attrs.customtypeaheadtemplateurl);                 }                  var resetmatches = function () {                     scope.matches = [];                     scope.activeidx = -1;                     element.attr('aria-expanded', false);                 };                  var getmatchid = function (index) {                     return popupid + '-option-' + index;                 };                  // indicate specified match active (pre-selected) item in list owned customtypeahead.                 // attribute added or removed automatically when `activeidx` changes.                 scope.$watch('activeidx', function (index) {                     if (index < 0) {                         element.removeattr('aria-activedescendant');                     } else {                         element.attr('aria-activedescendant', getmatchid(index));                     }                 });                  var getlastword = function (expression) {                     if (expression === "") {                         return "";                     }                     var temp = expression.split(" ");                     return temp[temp.length - 1];                 };                  var getmatchesasync = function (inputvalue) {                     inputvalue = getlastword(inputvalue);                     var locals = {$viewvalue: inputvalue};                     isloadingsetter(originalscope, true);                     $q.when(parserresult.source(originalscope, locals)).then(function (matches) {                          //it might happen several async queries in progress if user typing fast                         //but interested in responses correspond current view value                         var oncurrentrequest = (inputvalue === modelctrl.$viewvalue);                         if (oncurrentrequest && hasfocus) {                             if (matches && matches.length > 0) {                                  scope.activeidx = focusfirst ? 0 : -1;                                 scope.matches.length = 0;                                  //transform labels                                 (var = 0; < 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 matches - need re-calculate position each time opening window                                 //with matches pop-up might absolute-positioned , position of input might have changed on page                                 //due other elements being rendered                                 scope.position = appendtobody ? $position.offset(element) : $position.position(element);                                 scope.position.top = scope.position.top + element.prop('offsetheight');                                  element.attr('aria-expanded', true);                             } else {                                 resetmatches();                             }                         }                         if (oncurrentrequest) {                             isloadingsetter(originalscope, false);                         }                     }, function () {                         resetmatches();                         isloadingsetter(originalscope, false);                     });                 };                  resetmatches();                  //we need propagate user's query can highlight matches                 scope.query = undefined;                  //declare timeout promise var outside function scope stacked calls can cancelled later                 var timeoutpromise;                  var schedulesearchwithtimeout = function (inputvalue) {                     timeoutpromise = $timeout(function () {                         getmatchesasync(inputvalue);                     }, waittime);                 };                  var cancelprevioustimeout = function () {                     if (timeoutpromise) {                         $timeout.cancel(timeoutpromise);                     }                 };                  //plug $parsers pipeline open customtypeahead on view changes initiated dom                 //$parsers kick-in on changes coming view manually triggered $setviewvalue                 modelctrl.$parsers.unshift(function (inputvalue) {                     inputvalue = getlastword(inputvalue);                     hasfocus = true;                      if (minlength === 0 || inputvalue && inputvalue.length >= minlength) {                         if (waittime > 0) {                             cancelprevioustimeout();                             schedulesearchwithtimeout(inputvalue);                         } else {                             getmatchesasync(inputvalue);                         }                     } else {                         isloadingsetter(originalscope, false);                         cancelprevioustimeout();                         resetmatches();                     }                      if (iseditable) {                         return inputvalue;                     } else {                         if (!inputvalue) {                             // reset in case user had typed previously.                             modelctrl.$setvalidity('editable', true);                             return inputvalue;                         } else {                             modelctrl.$setvalidity('editable', false);                             return undefined;                         }                     }                 });                  modelctrl.$formatters.push(function (modelvalue) {                      var candidateviewvalue, emptyviewvalue;                     var locals = {};                      // validity may set false via $parsers (see above) if                     // model restricted selected values. if model                     // set manually considered valid.                     if (!iseditable) {                         modelctrl.$setvalidity('editable', true);                     }                      if (inputformatter) {                          locals.$model = modelvalue;                         return inputformatter(originalscope, locals);                      } else {                          //it might happen don't have enough info render input value                         //we need check situation , return model value if can't apply custom formatting                         locals[parserresult.itemname] = modelvalue;                         candidateviewvalue = parserresult.viewmapper(originalscope, locals);                         locals[parserresult.itemname] = undefined;                         emptyviewvalue = parserresult.viewmapper(originalscope, locals);                          return candidateviewvalue !== emptyviewvalue ? candidateviewvalue : modelvalue;                     }                 });                  scope.select = function (activeidx) {                     //called within $digest() cycle                     var locals = {};                     var model, item;                      locals[parserresult.itemname] = item = scope.matches[activeidx].model;                     model = parserresult.modelmapper(originalscope, locals);                     $setmodelvalue(originalscope, model);                     modelctrl.$setvalidity('editable', true);                     modelctrl.$setvalidity('parse', true);                      onselectcallback(originalscope, {                         $item: item,                         $model: model,                         $label: parserresult.viewmapper(originalscope, locals)                     });                      resetmatches();                      //return focus input element if match selected via mouse click event                     // use timeout avoid $rootscope:inprog error                     $timeout(function () {                         element[0].focus();                     }, 0, false);                 };                  //bind keyboard events: arrows up(38) / down(40), enter(13) , tab(9), esc(27)                 element.bind('keydown', function (evt) {                      //customtypeahead open , "interesting" key pressed                     if (scope.matches.length === 0 || hot_keys.indexof(evt.which) === -1) {                         return;                     }                      // if there's nothing selected (i.e. focusfirst) , enter hit, don't                     if (scope.activeidx == -1 && (evt.which === 13 || evt.which === 9)) {                         return;                     }                      evt.preventdefault();                      if (evt.which === 40) {                         scope.activeidx = (scope.activeidx + 1) % scope.matches.length;                         scope.$digest();                      } else if (evt.which === 38) {                         scope.activeidx = (scope.activeidx > 0 ? scope.activeidx : scope.matches.length) - 1;                         scope.$digest();                      } else if (evt.which === 13 || evt.which === 9) {                         scope.$apply(function () {                             scope.select(scope.activeidx);                         });                      } else if (evt.which === 27) {                         evt.stoppropagation();                          resetmatches();                         scope.$digest();                     }                 });                  element.bind('blur', function (evt) {                     hasfocus = false;                 });                  // keep reference click handler unbind it.                 var dismissclickhandler = function (evt) {                     if (element[0] !== evt.target) {                         resetmatches();                         if (!$rootscope.$$phase) {                             scope.$digest();                         }                     }                 };                  $document.bind('click', dismissclickhandler);                  originalscope.$on('$destroy', function () {                     $document.unbind('click', dismissclickhandler);                     if (appendtobody) {                         $popup.remove();                     }                     // prevent jquery cache memory leak                     popupel.remove();                 });                  var $popup = $compile(popupel)(scope);                  if (appendtobody) {                     $document.find('body').append($popup);                 } else {                     element.after($popup);                 }             }         };      }])  .directive('customtypeaheadpopup', function () {     return {         restrict: 'ea',         scope: {             matches: '=',             query: '=',             active: '=',             position: '&',             select: '&'         },         replace: true,         templateurl: 'html/templates/custom-typeahead-popup.html',         link: function (scope, element, attrs) {              scope.templateurl = attrs.templateurl;              scope.isopen = function () {                 return scope.matches.length > 0;             };              scope.isactive = function (matchidx) {                 return scope.active == matchidx;             };              scope.selectactive = function (matchidx) {                 scope.active = matchidx;             };              scope.selectmatch = function (activeidx) {                 scope.select({activeidx: activeidx});             };         }     }; })  .directive('customtypeaheadmatch', ['$templaterequest', '$compile', '$parse', function ($templaterequest, $compile, $parse) {     return {         restrict: 'ea',         scope: {             index: '=',             match: '=',             query: '='         },         link: function (scope, element, attrs) {             var tplurl = $parse(attrs.templateurl)(scope.$parent) || 'html/templates/custom-typeahead-match.html';             $templaterequest(tplurl).then(function (tplcontent) {                 $compile(tplcontent.trim())(scope, function (clonedelement) {                     element.replacewith(clonedelement);                 });             });         }     }; }])  .filter('customtypeaheadhighlight', function () {      function escaperegexp(querytoescape) {         return querytoescape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');     }      return function (matchitem, query) {         return query ? ('' + matchitem).replace(new regexp(escaperegexp(query), 'gi'), '<strong>$&</strong>') : matchitem;     }; }); 


Comments