1/* 2 * angucomplete-alt 3 * Autocomplete directive for AngularJS 4 * This is a fork of Daryl Rowland's angucomplete with some extra features. 5 * By Hidenari Nozaki 6 * Version 2.5.0 7 */ 8 9/*! Copyright (c) 2014 Hidenari Nozaki and contributors | Licensed under the MIT license */ 10 11// Todo: 12// If this code needs to be updated, mark it with a comment like this one so that it can be recreated if 13// This library needs to be replaced with a later version. 14// CUSTOMIZED CODE 15 16(function (root, factory) { 17 'use strict'; 18 if (typeof module !== 'undefined' && module.exports) { 19 // CommonJS 20 module.exports = factory(require('angular')); 21 } else if (typeof define === 'function' && define.amd) { 22 // AMD 23 define(['angular'], factory); 24 } else { 25 // Global Variables 26 factory(root.angular); 27 } 28}(window, function (angular) { 29 'use strict'; 30 31 angular.module('angucomplete-alt', []).directive('angucompleteAlt', ['$q', '$parse', '$http', '$sce', '$timeout', '$templateCache', '$interpolate', function ($q, $parse, $http, $sce, $timeout, $templateCache, $interpolate) { 32 // keyboard events 33 var KEY_DW = 40; 34 var KEY_RT = 39; 35 var KEY_UP = 38; 36 var KEY_LF = 37; 37 var KEY_ES = 27; 38 var KEY_EN = 13; 39 var KEY_TAB = 9; 40 41 var MIN_LENGTH = 3; 42 var MAX_LENGTH = 524288; // the default max length per the html maxlength attribute 43 var PAUSE = 500; 44 var BLUR_TIMEOUT = 200; 45 46 // string constants 47 var REQUIRED_CLASS = 'autocomplete-required'; 48 var TEXT_SEARCHING = 'Searching...'; 49 var TEXT_NORESULTS = 'No results found'; 50 var TEMPLATE_URL = '/angucomplete-alt/index.html'; 51 52 // CUSTOMZED CODE 53 var lastKeycode = 0; 54 var oldInputValue = ''; 55 var androidDelete = false; 56 57 // Set the default template for this directive 58 $templateCache.put(TEMPLATE_URL, 59 '<div class="angucomplete-holder" ng-class="{\'angucomplete-dropdown-visible\': showDropdown}">' + 60 ' <input id="{{id}}_value" name="{{inputName}}" tabindex="{{fieldTabindex}}" ng-class="{\'angucomplete-input-not-empty\': notEmpty}" ng-model="searchStr" ng-disabled="disableInput" type="{{inputType}}" placeholder="{{placeholder}}" maxlength="{{maxlength}}" ng-focus="onFocusHandler()" class="{{inputClass}}" ng-focus="resetHideResults()" ng-blur="hideResults($event)" autocapitalize="off" autocorrect="off" autocomplete="off" ng-change="inputChangeHandler(searchStr)"/>' + 61 ' <div id="{{id}}_dropdown" class="angucomplete-dropdown" ng-show="showDropdown">' + 62 ' <div class="angucomplete-searching" ng-show="searching" ng-bind="textSearching"></div>' + 63 ' <div class="angucomplete-searching" ng-show="!searching && (!results || results.length == 0)" ng-bind="textNoResults"></div>' + 64 ' <div class="angucomplete-row" ng-repeat="result in results" ng-click="selectResult(result)" ng-mouseenter="hoverRow($index)" ng-class="{\'angucomplete-selected-row\': $index == currentIndex}">' + 65 ' <div ng-if="imageField" class="angucomplete-image-holder">' + 66 ' <img ng-if="result.image && result.image != \'\'" ng-src="{{result.image}}" class="angucomplete-image"/>' + 67 ' <div ng-if="!result.image && result.image != \'\'" class="angucomplete-image-default"></div>' + 68 ' </div>' + 69 ' <div class="angucomplete-title" ng-if="matchClass" ng-bind-html="result.title"></div>' + 70 ' <div class="angucomplete-title" ng-if="!matchClass">{{ result.title }}</div>' + 71 ' <div ng-if="matchClass && result.description && result.description != \'\'" class="angucomplete-description" ng-bind-html="result.description"></div>' + 72 ' <div ng-if="!matchClass && result.description && result.description != \'\'" class="angucomplete-description">{{result.description}}</div>' + 73 ' </div>' + 74 ' </div>' + 75 '</div>' 76 ); 77 78 function link(scope, elem, attrs, ctrl) { 79 var inputField = elem.find('input'); 80 var minlength = MIN_LENGTH; 81 var searchTimer = null; 82 var hideTimer; 83 var requiredClassName = REQUIRED_CLASS; 84 var responseFormatter;
vendor: 15,383 bytes, lines 85-560
85 var validState = null; 86 var httpCanceller = null; 87 var httpCallInProgress = false; 88 var dd = elem[0].querySelector('.angucomplete-dropdown'); 89 var isScrollOn = false; 90 var mousedownOn = null; 91 var unbindInitialValue; 92 var displaySearching; 93 var displayNoResults; 94 95 elem.on('mousedown', function(event) { 96 if (event.target.id) { 97 mousedownOn = event.target.id; 98 if (mousedownOn === scope.id + '_dropdown') { 99 document.body.addEventListener('click', clickoutHandlerForDropdown); 100 } 101 } 102 else { 103 mousedownOn = event.target.className; 104 } 105 }); 106 107 scope.currentIndex = scope.focusFirst ? 0 : null; 108 scope.searching = false; 109 unbindInitialValue = scope.$watch('initialValue', function(newval) { 110 if (newval) { 111 // remove scope listener 112 unbindInitialValue(); 113 // change input 114 handleInputChange(newval, true); 115 } 116 }); 117 118 scope.$watch('fieldRequired', function(newval, oldval) { 119 if (newval !== oldval) { 120 if (!newval) { 121 ctrl[scope.inputName].$setValidity(requiredClassName, true); 122 } 123 else if (!validState || scope.currentIndex === -1) { 124 handleRequired(false); 125 } 126 else { 127 handleRequired(true); 128 } 129 } 130 }); 131 132 scope.$on('angucomplete-alt:clearInput', function (event, elementId) { 133 if (!elementId || elementId === scope.id) { 134 scope.searchStr = null; 135 callOrAssign(); 136 handleRequired(false); 137 clearResults(); 138 } 139 }); 140 141 scope.$on('angucomplete-alt:changeInput', function (event, elementId, newval) { 142 if (!!elementId && elementId === scope.id) { 143 handleInputChange(newval); 144 } 145 }); 146 147 function handleInputChange(newval, initial) { 148 if (newval) { 149 if (typeof newval === 'object') { 150 scope.searchStr = extractTitle(newval); 151 callOrAssign({originalObject: newval}); 152 } else if (typeof newval === 'string' && newval.length > 0) { 153 scope.searchStr = newval; 154 } else { 155 if (console && console.error) { 156 console.error('Tried to set ' + (!!initial ? 'initial' : '') + ' value of angucomplete to', newval, 'which is an invalid value'); 157 } 158 } 159 160 handleRequired(true); 161 } 162 } 163 164 // #194 dropdown list not consistent in collapsing (bug). 165 function clickoutHandlerForDropdown(event) { 166 mousedownOn = null; 167 scope.hideResults(event); 168 document.body.removeEventListener('click', clickoutHandlerForDropdown); 169 } 170 171 // for IE8 quirkiness about event.which 172 function ie8EventNormalizer(event) { 173 return event.which ? event.which : event.keyCode; 174 } 175 176 function callOrAssign(value) { 177 if (typeof scope.selectedObject === 'function') { 178 scope.selectedObject(value, scope.selectedObjectData); 179 } 180 else { 181 scope.selectedObject = value; 182 } 183 184 if (value) { 185 handleRequired(true); 186 } 187 else { 188 handleRequired(false); 189 } 190 } 191 192 function callFunctionOrIdentity(fn) { 193 return function(data) { 194 return scope[fn] ? scope[fn](data) : data; 195 }; 196 } 197 198 function setInputString(str) { 199 callOrAssign({originalObject: str}); 200 201 if (scope.clearSelected) { 202 scope.searchStr = null; 203 } 204 clearResults(); 205 } 206 207 function extractTitle(data) { 208 // split title fields and run extractValue for each and join with ' ' 209 return scope.titleField.split(',') 210 .map(function(field) { 211 return extractValue(data, field); 212 }) 213 .join(' '); 214 } 215 216 function extractValue(obj, key) { 217 var keys, result; 218 if (key) { 219 keys= key.split('.'); 220 result = obj; 221 for (var i = 0; i < keys.length; i++) { 222 result = result[keys[i]]; 223 } 224 } 225 else { 226 result = obj; 227 } 228 return result; 229 } 230 231 function findMatchString(target, str) { 232 var result, matches, re; 233 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions 234 // Escape user input to be treated as a literal string within a regular expression 235 re = new RegExp(str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'); 236 if (!target) { return; } 237 if (!target.match || !target.replace) { target = target.toString(); } 238 matches = target.match(re); 239 if (matches) { 240 result = target.replace(re, 241 '<span class="'+ scope.matchClass +'">'+ matches[0] +'</span>'); 242 } 243 else { 244 result = target; 245 } 246 return $sce.trustAsHtml(result); 247 } 248 249 function handleRequired(valid) { 250 scope.notEmpty = valid; 251 validState = scope.searchStr; 252 if (scope.fieldRequired && ctrl && scope.inputName) { 253 ctrl[scope.inputName].$setValidity(requiredClassName, valid); 254 } 255 } 256 257 function keyupHandler(event) { 258 var which = ie8EventNormalizer(event); 259 260 // CUSTOMIZED CODE 261 lastKeycode = which; 262 263 // CUSTOMIZED CODE 264 androidDelete = false; 265 // Android sometimes doesn't send keycodes or sends them as all 229, but we need to check if delete was pressed 266 if (lastKeycode === undefined || lastKeycode === 229) { 267 if (oldInputValue.length > inputField.val().length) { 268 androidDelete = true; 269 } 270 } 271 oldInputValue = inputField.val(); 272 273 // CUSTOMIZED CODE 274 // undefined keycode check fixes an issue on android browsers where results needed to be tapped twice. 275 if (which === KEY_LF || which === KEY_RT || which === undefined) { 276 return; 277 } 278 279 if (which === KEY_LF || which === KEY_RT) { 280 // do nothing 281 return; 282 } 283 284 if (which === KEY_UP || which === KEY_EN) { 285 event.preventDefault(); 286 } 287 else if (which === KEY_DW) { 288 event.preventDefault(); 289 if (!scope.showDropdown && scope.searchStr && scope.searchStr.length >= minlength) { 290 initResults(); 291 scope.searching = true; 292 searchTimerComplete(scope.searchStr); 293 } 294 } 295 else if (which === KEY_ES) { 296 clearResults(); 297 scope.$apply(function() { 298 inputField.val(scope.searchStr); 299 }); 300 } 301 else { 302 if (minlength === 0 && !scope.searchStr) { 303 return; 304 } 305 306 if (!scope.searchStr || scope.searchStr === '') { 307 scope.showDropdown = false; 308 } else if (scope.searchStr.length >= minlength) { 309 initResults(); 310 311 if (searchTimer) { 312 $timeout.cancel(searchTimer); 313 } 314 315 scope.searching = true; 316 317 searchTimer = $timeout(function() { 318 searchTimerComplete(scope.searchStr); 319 }, scope.pause); 320 } 321 322 if (validState && validState !== scope.searchStr && !scope.clearSelected) { 323 scope.$apply(function() { 324 callOrAssign(); 325 }); 326 } 327 } 328 } 329 330 function handleOverrideSuggestions(event) { 331 if (scope.overrideSuggestions && 332 !(scope.selectedObject && scope.selectedObject.originalObject === scope.searchStr)) { 333 if (event) { 334 event.preventDefault(); 335 } 336 337 // cancel search timer 338 $timeout.cancel(searchTimer); 339 // cancel http request 340 cancelHttpRequest(); 341 342 setInputString(scope.searchStr); 343 } 344 } 345 346 function dropdownRowOffsetHeight(row) { 347 var css = getComputedStyle(row); 348 return row.offsetHeight + 349 parseInt(css.marginTop, 10) + parseInt(css.marginBottom, 10); 350 } 351 352 function dropdownHeight() { 353 return dd.getBoundingClientRect().top + 354 parseInt(getComputedStyle(dd).maxHeight, 10); 355 } 356 357 function dropdownRow() { 358 return elem[0].querySelectorAll('.angucomplete-row')[scope.currentIndex]; 359 } 360 361 function dropdownRowTop() { 362 return dropdownRow().getBoundingClientRect().top - 363 (dd.getBoundingClientRect().top + 364 parseInt(getComputedStyle(dd).paddingTop, 10)); 365 } 366 367 function dropdownScrollTopTo(offset) { 368 dd.scrollTop = dd.scrollTop + offset; 369 } 370 371 function updateInputField(){ 372 var current = scope.results[scope.currentIndex]; 373 if (scope.matchClass) { 374 inputField.val(extractTitle(current.originalObject)); 375 } 376 else { 377 inputField.val(current.title); 378 } 379 } 380 381 function keydownHandler(event) { 382 var which = ie8EventNormalizer(event); 383 var row = null; 384 var rowTop = null; 385 // CUSTOMIZED CODE 386 lastKeycode = which; 387 388 if (which === KEY_EN && scope.results) { 389 if (scope.currentIndex >= 0 && scope.currentIndex < scope.results.length) { 390 event.preventDefault(); 391 scope.selectResult(scope.results[scope.currentIndex]); 392 } else { 393 handleOverrideSuggestions(event); 394 clearResults(); 395 } 396 scope.$apply(); 397 } else if (which === KEY_DW && scope.results) { 398 event.preventDefault(); 399 if ((scope.currentIndex + 1) < scope.results.length && scope.showDropdown) { 400 scope.$apply(function() { 401 scope.currentIndex ++; 402 updateInputField(); 403 }); 404 405 if (isScrollOn) { 406 row = dropdownRow(); 407 if (dropdownHeight() < row.getBoundingClientRect().bottom) { 408 dropdownScrollTopTo(dropdownRowOffsetHeight(row)); 409 } 410 } 411 } 412 } else if (which === KEY_UP && scope.results) { 413 event.preventDefault(); 414 if (scope.currentIndex >= 1) { 415 scope.$apply(function() { 416 scope.currentIndex --; 417 updateInputField(); 418 }); 419 420 if (isScrollOn) { 421 rowTop = dropdownRowTop(); 422 if (rowTop < 0) { 423 dropdownScrollTopTo(rowTop - 1); 424 } 425 } 426 } 427 else if (scope.currentIndex === 0) { 428 scope.$apply(function() { 429 scope.currentIndex = -1; 430 inputField.val(scope.searchStr); 431 }); 432 } 433 } else if (which === KEY_TAB) { 434 if (scope.results && scope.results.length > 0 && scope.showDropdown) { 435 if (scope.currentIndex === -1 && scope.overrideSuggestions) { 436 // intentionally not sending event so that it does not 437 // prevent default tab behavior 438 handleOverrideSuggestions(); 439 } 440 else { 441 if (scope.currentIndex === -1) { 442 scope.currentIndex = 0; 443 } 444 scope.selectResult(scope.results[scope.currentIndex]); 445 scope.$digest(); 446 } 447 } 448 else { 449 // no results 450 // intentionally not sending event so that it does not 451 // prevent default tab behavior 452 if (scope.searchStr && scope.searchStr.length > 0) { 453 handleOverrideSuggestions(); 454 } 455 } 456 } else if (which === KEY_ES) { 457 // This is very specific to IE10/11 #272 458 // without this, IE clears the input text 459 event.preventDefault(); 460 } 461 } 462 463 function httpSuccessCallbackGen(str) { 464 return function(responseData, status, headers, config) { 465 // normalize return obejct from promise 466 if (!status && !headers && !config && responseData.data) { 467 responseData = responseData.data; 468 } 469 scope.searching = false; 470 processResults( 471 extractValue(responseFormatter(responseData), scope.remoteUrlDataField), 472 str); 473 }; 474 } 475 476 function httpErrorCallback(errorRes, status, headers, config) { 477 scope.searching = httpCallInProgress; 478 479 // normalize return obejct from promise 480 if (!status && !headers && !config) { 481 status = errorRes.status; 482 } 483 484 // cancelled/aborted 485 if (status === 0 || status === -1) { return; } 486 if (scope.remoteUrlErrorCallback) { 487 scope.remoteUrlErrorCallback(errorRes, status, headers, config); 488 } 489 else { 490 if (console && console.error) { 491 console.error('http error'); 492 } 493 } 494 } 495 496 function cancelHttpRequest() { 497 if (httpCanceller) { 498 httpCanceller.resolve(); 499 } 500 } 501 502 function getRemoteResults(str) { 503 var params = {}, 504 url = scope.remoteUrl + encodeURIComponent(str); 505 if (scope.remoteUrlRequestFormatter) { 506 params = {params: scope.remoteUrlRequestFormatter(str)}; 507 url = scope.remoteUrl; 508 } 509 if (!!scope.remoteUrlRequestWithCredentials) { 510 params.withCredentials = true; 511 } 512 cancelHttpRequest(); 513 httpCanceller = $q.defer(); 514 params.timeout = httpCanceller.promise; 515 httpCallInProgress = true; 516 // CUSTOMIZED CODE 517 // $http.get(url, params) 518 // .success(httpSuccessCallbackGen(str)) 519 // .error(httpErrorCallback) 520 // .finally(function(){httpCallInProgress=false;}); 521 $http.get(url, params) 522 .then( 523 function onSuccess(response) { 524 var data = response.data; 525 httpSuccessCallbackGen(data) 526 }, 527 function onError() { 528 httpErrorCallback() 529 } 530 ).finally( 531 function(){ httpCallInProgress=false; } 532 ); 533 } 534 535 function getRemoteResultsWithCustomHandler(str) { 536 cancelHttpRequest(); 537 538 httpCanceller = $q.defer(); 539 540 scope.remoteApiHandler(str, httpCanceller.promise) 541 .then(httpSuccessCallbackGen(str)) 542 .catch(httpErrorCallback); 543 544 /* IE8 compatible 545 scope.remoteApiHandler(str, httpCanceller.promise) 546 ['then'](httpSuccessCallbackGen(str)) 547 ['catch'](httpErrorCallback); 548 */ 549 } 550 551 function clearResults() { 552 scope.showDropdown = false; 553 scope.results = []; 554 if (dd) { 555 dd.scrollTop = 0; 556 } 557 } 558 559 function initResults() { 560 scope.showDropdown = displaySearching;
561 scope.currentIndex = scope.focusFirst ? 0 : -1; 562 scope.results = []; 563 } 564 // CUSTOMIZED CODE 565 // This function has been modified to only search the beginnings of words. Original code left in comments 566 // In case we need to update in the future, for comparison. 567 function getLocalResults(str) { 568 // var i, match, s, value, 569 // searchFields = scope.searchFields.split(','), 570 // matches = []; 571 var i, match, s, value, v, matchLater, 572 searchFields = scope.searchFields.split(','), 573 matches = [], 574 matchesLaterInString = []; 575 576 if (typeof scope.parseInput() !== 'undefined') { 577 str = scope.parseInput()(str); 578 } 579 for (i = 0; i < scope.localData.length; i++) { 580 match = false; 581 matchLater = false; 582 583 // CUSTOMIZED CODE 584 585 // for (s = 0; s < searchFields.length; s++) { 586 // value = extractValue(scope.localData[i], searchFields[s]) || ''; 587 // match = match || (value.toString().toLowerCase().indexOf(str.toString().toLowerCase()) >= 0); 588 // } 589 // 590 // if (match) { 591 // matches[matches.length] = scope.localData[i]; 592 // } 593 594 for (s = 0; s < searchFields.length; s++) { 595 value = extractValue(scope.localData[i], searchFields[s]) || ''; 596 var fullStringWithParentheses = value; 597 // Remove parentheses from string 598 value = value.replace(/[()]/g, ""); 599 var fullString = value; 600 // separate value by space 601 var separatedValueArray = value.split(" "); 602 separatedValueArray.push(fullString, fullStringWithParentheses); 603 604 // If the entered value matches the option, verify if the match is at the beginning of the string or elsewhere 605 match = match || (separatedValueArray[0].toString().toLowerCase().indexOf(str.toString().toLowerCase()) === 0); 606 for(v = 1; v < separatedValueArray.length; v++) { 607 matchLater = matchLater || (separatedValueArray[v].toString().toLowerCase().indexOf(str.toString().toLowerCase()) === 0); 608 } 609 610 // Check if the option matches, if so - determine if it should go in the "matches at the beginning" or "matches elsewhere in the string" arrays 611 if (match) { 612 matches[matches.length] = scope.localData[i]; 613 } else if (matchLater) { 614 matchesLaterInString[matchesLaterInString.length] = scope.localData[i]; 615 } 616 } 617 } 618 // return matches; 619 620 // Combine the two "matching" arrays 621 matches = matches.concat(matchesLaterInString); 622 return matches; 623 } 624 // CUSTOMIZED CODE 625 // function checkExactMatch(result, obj, str){ 626 function checkExactMatch(result, str){ 627 if (!str) { return false; } 628 // CUSTOMIZED CODE 629 // Don't automatch if the last key pressed was backspace, so that users can clear the input field 630 if (lastKeycode == 8 || lastKeycode == 46 || androidDelete) { return false; } 631 632 // CUSTOMIZED CODE 633 // for(var key in obj){ 634 // if(obj[key].toLowerCase() === str.toLowerCase()){ 635 // scope.selectResult(result); 636 // return true; 637 // } 638 // } 639 for(var i = 0; i < result.length; i++) { 640 if (result[i].originalObject.description && result[i].originalObject.description.toLowerCase() === str.toLowerCase()) { 641 scope.selectResult(result[i]); 642 return true; 643 } else if (result[i].originalObject.name) { 644 if (result[i].originalObject.name.toLowerCase() === str.toLowerCase()) { 645 scope.selectResult(result[i]); 646 return true; 647 } 648 // CUSTOMIZED CODE 649 else { 650 // check if value matches individual values inside "state" field **can add other fields later if necessary 651 var fullString = result[i].originalObject.name.toLowerCase(); 652 var valWithOutParen = fullString.replace(/[()]/g,""); 653 var separatedValueArray = valWithOutParen.split(" "); 654 // remove abbreviation from the array 655 // **some abbreviations will cause the user to select a state they 656 // were not meaning to, for example "AL" when the user wants Alaska, not Alabama 657 separatedValueArray.pop(); 658 // add full state name, with space, if the state name has two words 659 if (separatedValueArray.length > 1) { 660 separatedValueArray.push(separatedValueArray[0] + " " + separatedValueArray[1]); 661 } 662 // add full value back into the array 663 separatedValueArray.push(fullString); 664 for(var j = 0; j < separatedValueArray.length; j++) { 665 if (separatedValueArray[j] === str.toLowerCase() && isNotACommonStateNamePart(str)) { 666 scope.selectResult(result[i]); 667 return true; 668 } 669 } 670 } 671 } 672 } 673 674 return false; 675 } 676 677 function isNotACommonStateNamePart(str) { 678 var strLower = str.toLowerCase(); 679 return (strLower != "north" && strLower != "south" && strLower != "new"); 680 } 681 682 function searchTimerComplete(str) { 683 // Begin the search 684 if (!str || str.length < minlength) { 685 return; 686 } 687 if (scope.localData) { 688 scope.$apply(function() { 689 var matches; 690 if (typeof scope.localSearch() !== 'undefined') { 691 matches = scope.localSearch()(str, scope.localData); 692 } else { 693 matches = getLocalResults(str); 694 } 695 scope.searching = false; 696 processResults(matches, str); 697 }); 698 } 699 else if (scope.remoteApiHandler) { 700 getRemoteResultsWithCustomHandler(str); 701 } else { 702 getRemoteResults(str); 703 } 704 } 705 706 function processResults(responseData, str) { 707 var i, description, image, text, formattedText, formattedDesc; 708 709 if (responseData && responseData.length > 0) { 710 scope.results = []; 711 712 for (i = 0; i < responseData.length; i++) { 713 if (scope.titleField && scope.titleField !== '') { 714 text = formattedText = extractTitle(responseData[i]); 715 } 716 717 description = ''; 718 if (scope.descriptionField) { 719 description = formattedDesc = extractValue(responseData[i], scope.descriptionField); 720 } 721 722 image = ''; 723 if (scope.imageField) { 724 image = extractValue(responseData[i], scope.imageField); 725 } 726 727 if (scope.matchClass) { 728 formattedText = findMatchString(text, str); 729 formattedDesc = findMatchString(description, str); 730 } 731 732 scope.results[scope.results.length] = { 733 title: formattedText, 734 description: formattedDesc, 735 image: image, 736 originalObject: responseData[i] 737 }; 738 } 739 740 } else { 741 scope.results = []; 742 } 743 // CUSTOMIZED CODE 744 // if (scope.autoMatch && scope.results.length === 1 && 745 // checkExactMatch(scope.results[0], 746 // {title: text, desc: description || ''}, scope.searchStr)) { 747 if (scope.autoMatch && 748 checkExactMatch(scope.results, scope.searchStr)) { 749 scope.showDropdown = false; 750 } else if (scope.results.length === 0 && !displayNoResults) { 751 scope.showDropdown = false; 752 } else { 753 scope.showDropdown = true; 754 } 755 } 756 757 function showAll() { 758 if (scope.localData) { 759 scope.searching = false; 760 processResults(scope.localData, ''); 761 } 762 else if (scope.remoteApiHandler) { 763 scope.searching = true; 764 getRemoteResultsWithCustomHandler(''); 765 } 766 else { 767 scope.searching = true; 768 getRemoteResults(''); 769 } 770 } 771 772 scope.onFocusHandler = function() {
773 if (scope.focusIn) { 774 scope.focusIn(); 775 } 776 if (minlength === 0 && (!scope.searchStr || scope.searchStr.length === 0)) { 777 scope.currentIndex = scope.focusFirst ? 0 : scope.currentIndex; 778 scope.showDropdown = true; 779 showAll(); 780 } 781 }; 782 783 scope.hideResults = function() { 784 if (mousedownOn && 785 (mousedownOn === scope.id + '_dropdown' || 786 mousedownOn.indexOf('angucomplete') >= 0)) { 787 mousedownOn = null; 788 } 789 else { 790 hideTimer = $timeout(function() { 791 clearResults(); 792 scope.$apply(function() { 793 if (scope.searchStr && scope.searchStr.length > 0) { 794 // CUSTOMIZED CODE 795 // inputField.val(scope.searchStr); 796 797 // This will autopopulate the field when filled by a browser 798 var matches; 799 if (typeof scope.localSearch() !== 'undefined') { 800 matches = scope.localSearch()(scope.searchStr, scope.localData); 801 console.log("matches:", matches); 802 } else { 803 matches = getLocalResults(scope.searchStr); 804 console.log("matches from local results:", matches); 805 } 806 scope.searching = false; 807 processResults(matches, scope.searchStr); 808 if (scope.results.length == 1) { 809 console.log("results length = 1"); 810 scope.selectResult(scope.results[0]); 811 } else { 812 console.log("results length > 1"); 813 } 814 inputField.val(scope.searchStr); 815 scope.showDropdown = false; 816 } 817 }); 818 }, BLUR_TIMEOUT); 819 cancelHttpRequest(); 820 821 if (scope.focusOut) { 822 scope.focusOut(); 823 } 824 825 if (scope.overrideSuggestions) { 826 if (scope.searchStr && scope.searchStr.length > 0 && scope.currentIndex === -1) { 827 handleOverrideSuggestions(); 828 } 829 } 830 } 831 }; 832 833 scope.resetHideResults = function() { 834 if (hideTimer) { 835 $timeout.cancel(hideTimer); 836 } 837 }; 838 839 scope.hoverRow = function(index) { 840 scope.currentIndex = index; 841 }; 842 843 scope.selectResult = function(result) { 844 // Restore original values 845 if (scope.matchClass) { 846 result.title = extractTitle(result.originalObject); 847 result.description = extractValue(result.originalObject, scope.descriptionField); 848 } 849 850 if (scope.clearSelected) { 851 scope.searchStr = null; 852 } 853 else { 854 scope.searchStr = result.title; 855 } 856 callOrAssign(result); 857 clearResults(); 858 }; 859 860 scope.inputChangeHandler = function(str) { 861 if (str.length < minlength) { 862 cancelHttpRequest(); 863 clearResults(); 864 } 865 else if (str.length === 0 && minlength === 0) { 866 showAll(); 867 } 868 869 if (scope.inputChanged) { 870 str = scope.inputChanged(str); 871 } 872 return str; 873 }; 874 875 // check required 876 if (scope.fieldRequiredClass && scope.fieldRequiredClass !== '') { 877 requiredClassName = scope.fieldRequiredClass; 878 } 879 880 // check min length 881 if (scope.minlength && scope.minlength !== '') { 882 minlength = parseInt(scope.minlength, 10); 883 } 884 885 // check pause time 886 if (!scope.pause) { 887 scope.pause = PAUSE; 888 } 889 890 // check clearSelected 891 if (!scope.clearSelected) { 892 scope.clearSelected = false; 893 } 894 895 // check override suggestions 896 if (!scope.overrideSuggestions) { 897 scope.overrideSuggestions = false; 898 } 899 900 // check required field 901 if (scope.fieldRequired && ctrl) { 902 // check initial value, if given, set validitity to true 903 if (scope.initialValue) { 904 handleRequired(true); 905 } 906 else { 907 handleRequired(false); 908 } 909 } 910 911 scope.inputType = attrs.type ? attrs.type : 'text'; 912 913 // set strings for "Searching..." and "No results" 914 scope.textSearching = attrs.textSearching ? attrs.textSearching : TEXT_SEARCHING; 915 scope.textNoResults = attrs.textNoResults ? attrs.textNoResults : TEXT_NORESULTS; 916 displaySearching = scope.textSearching === 'false' ? false : true; 917 displayNoResults = scope.textNoResults === 'false' ? false : true; 918 919 // set max length (default to maxlength deault from html 920 scope.maxlength = attrs.maxlength ? attrs.maxlength : MAX_LENGTH; 921 922 // register events 923 inputField.on('keydown', keydownHandler); 924 inputField.on('keyup compositionend', keyupHandler); 925 926 // set response formatter 927 responseFormatter = callFunctionOrIdentity('remoteUrlResponseFormatter'); 928 929 // set isScrollOn 930 $timeout(function() { 931 var css = getComputedStyle(dd); 932 isScrollOn = css.maxHeight && css.overflowY === 'auto'; 933 }); 934 } 935 936 return { 937 restrict: 'EA', 938 require: '^form', 939 scope: { 940 selectedObject: '=', 941 selectedObjectData: '=', 942 disableInput: '=', 943 initialValue: '=', 944 localData: '=', 945 localSearch: '&', 946 remoteUrlRequestFormatter: '=', 947 remoteUrlRequestWithCredentials: '@', 948 remoteUrlResponseFormatter: '=', 949 remoteUrlErrorCallback: '=', 950 remoteApiHandler: '=', 951 id: '@', 952 type: '@', 953 placeholder: '@', 954 textSearching: '@', 955 textNoResults: '@', 956 remoteUrl: '@', 957 remoteUrlDataField: '@', 958 titleField: '@', 959 descriptionField: '@', 960 imageField: '@', 961 inputClass: '@', 962 pause: '@', 963 searchFields: '@', 964 minlength: '@', 965 matchClass: '@', 966 clearSelected: '@', 967 overrideSuggestions: '@', 968 fieldRequired: '=', 969 fieldRequiredClass: '@', 970 inputChanged: '=', 971 autoMatch: '@', 972 focusOut: '&', 973 focusIn: '&', 974 fieldTabindex: '@', 975 inputName: '@', 976 focusFirst: '@', 977 parseInput: '&' 978 }, 979 templateUrl: function(element, attrs) {
980 return attrs.templateUrl || TEMPLATE_URL; 981 }, 982 compile: function(tElement) { 983 var startSym = $interpolate.startSymbol(); 984 var endSym = $interpolate.endSymbol(); 985 if (!(startSym === '{{' && endSym === '}}')) { 986 var interpolatedHtml = tElement.html() 987 .replace(/\{\{/g, startSym) 988 .replace(/\}\}/g, endSym); 989 tElement.html(interpolatedHtml); 990 } 991 return link; 992 } 993 }; 994 }]); 995 996}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.