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
Post a Comment