GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Code Duplication    Length = 531-531 lines in 2 locations

third-party/angularjs-modules-plugins/UI-Bootstrap/ui-bootstrap-tpls-1.3.2.js 1 location

@@ 6269-6799 (lines=531) @@
6266
  }])
6267
6268
  .controller('UibTypeaheadController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$$debounce', '$uibPosition', 'uibTypeaheadParser',
6269
    function(originalScope, element, attrs, $compile, $parse, $q, $timeout, $document, $window, $rootScope, $$debounce, $position, typeaheadParser) {
6270
    var HOT_KEYS = [9, 13, 27, 38, 40];
6271
    var eventDebounceTime = 200;
6272
    var modelCtrl, ngModelOptions;
6273
    //SUPPORTED ATTRIBUTES (OPTIONS)
6274
6275
    //minimal no of characters that needs to be entered before typeahead kicks-in
6276
    var minLength = originalScope.$eval(attrs.typeaheadMinLength);
6277
    if (!minLength && minLength !== 0) {
6278
      minLength = 1;
6279
    }
6280
6281
    originalScope.$watch(attrs.typeaheadMinLength, function (newVal) {
6282
        minLength = !newVal && newVal !== 0 ? 1 : newVal;
6283
    });
6284
    
6285
    //minimal wait time after last character typed before typeahead kicks-in
6286
    var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
6287
6288
    //should it restrict model values to the ones selected from the popup only?
6289
    var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
6290
    originalScope.$watch(attrs.typeaheadEditable, function (newVal) {
6291
      isEditable = newVal !== false;
6292
    });
6293
6294
    //binding to a variable that indicates if matches are being retrieved asynchronously
6295
    var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
6296
6297
    //a callback executed when a match is selected
6298
    var onSelectCallback = $parse(attrs.typeaheadOnSelect);
6299
6300
    //should it select highlighted popup value when losing focus?
6301
    var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false;
6302
6303
    //binding to a variable that indicates if there were no results after the query is completed
6304
    var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop;
6305
6306
    var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
6307
6308
    var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false;
6309
6310
    var appendTo = attrs.typeaheadAppendTo ?
6311
      originalScope.$eval(attrs.typeaheadAppendTo) : null;
6312
6313
    var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false;
6314
6315
    //If input matches an item of the list exactly, select it automatically
6316
    var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false;
6317
6318
    //binding to a variable that indicates if dropdown is open
6319
    var isOpenSetter = $parse(attrs.typeaheadIsOpen).assign || angular.noop;
6320
6321
    var showHint = originalScope.$eval(attrs.typeaheadShowHint) || false;
6322
6323
    //INTERNAL VARIABLES
6324
6325
    //model setter executed upon match selection
6326
    var parsedModel = $parse(attrs.ngModel);
6327
    var invokeModelSetter = $parse(attrs.ngModel + '($$$p)');
6328
    var $setModelValue = function(scope, newValue) {
6329
      if (angular.isFunction(parsedModel(originalScope)) &&
6330
        ngModelOptions && ngModelOptions.$options && ngModelOptions.$options.getterSetter) {
6331
        return invokeModelSetter(scope, {$$$p: newValue});
6332
      }
6333
6334
      return parsedModel.assign(scope, newValue);
6335
    };
6336
6337
    //expressions used by typeahead
6338
    var parserResult = typeaheadParser.parse(attrs.uibTypeahead);
6339
6340
    var hasFocus;
6341
6342
    //Used to avoid bug in iOS webview where iOS keyboard does not fire
6343
    //mousedown & mouseup events
6344
    //Issue #3699
6345
    var selected;
6346
6347
    //create a child scope for the typeahead directive so we are not polluting original scope
6348
    //with typeahead-specific data (matches, query etc.)
6349
    var scope = originalScope.$new();
6350
    var offDestroy = originalScope.$on('$destroy', function() {
6351
      scope.$destroy();
6352
    });
6353
    scope.$on('$destroy', offDestroy);
6354
6355
    // WAI-ARIA
6356
    var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
6357
    element.attr({
6358
      'aria-autocomplete': 'list',
6359
      'aria-expanded': false,
6360
      'aria-owns': popupId
6361
    });
6362
6363
    var inputsContainer, hintInputElem;
6364
    //add read-only input to show hint
6365
    if (showHint) {
6366
      inputsContainer = angular.element('<div></div>');
6367
      inputsContainer.css('position', 'relative');
6368
      element.after(inputsContainer);
6369
      hintInputElem = element.clone();
6370
      hintInputElem.attr('placeholder', '');
6371
      hintInputElem.attr('tabindex', '-1');
6372
      hintInputElem.val('');
6373
      hintInputElem.css({
6374
        'position': 'absolute',
6375
        'top': '0px',
6376
        'left': '0px',
6377
        'border-color': 'transparent',
6378
        'box-shadow': 'none',
6379
        'opacity': 1,
6380
        'background': 'none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)',
6381
        'color': '#999'
6382
      });
6383
      element.css({
6384
        'position': 'relative',
6385
        'vertical-align': 'top',
6386
        'background-color': 'transparent'
6387
      });
6388
      inputsContainer.append(hintInputElem);
6389
      hintInputElem.after(element);
6390
    }
6391
6392
    //pop-up element used to display matches
6393
    var popUpEl = angular.element('<div uib-typeahead-popup></div>');
6394
    popUpEl.attr({
6395
      id: popupId,
6396
      matches: 'matches',
6397
      active: 'activeIdx',
6398
      select: 'select(activeIdx, evt)',
6399
      'move-in-progress': 'moveInProgress',
6400
      query: 'query',
6401
      position: 'position',
6402
      'assign-is-open': 'assignIsOpen(isOpen)',
6403
      debounce: 'debounceUpdate'
6404
    });
6405
    //custom item template
6406
    if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
6407
      popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
6408
    }
6409
6410
    if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) {
6411
      popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl);
6412
    }
6413
6414
    var resetHint = function() {
6415
      if (showHint) {
6416
        hintInputElem.val('');
6417
      }
6418
    };
6419
6420
    var resetMatches = function() {
6421
      scope.matches = [];
6422
      scope.activeIdx = -1;
6423
      element.attr('aria-expanded', false);
6424
      resetHint();
6425
    };
6426
6427
    var getMatchId = function(index) {
6428
      return popupId + '-option-' + index;
6429
    };
6430
6431
    // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead.
6432
    // This attribute is added or removed automatically when the `activeIdx` changes.
6433
    scope.$watch('activeIdx', function(index) {
6434
      if (index < 0) {
6435
        element.removeAttr('aria-activedescendant');
6436
      } else {
6437
        element.attr('aria-activedescendant', getMatchId(index));
6438
      }
6439
    });
6440
6441
    var inputIsExactMatch = function(inputValue, index) {
6442
      if (scope.matches.length > index && inputValue) {
6443
        return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase();
6444
      }
6445
6446
      return false;
6447
    };
6448
6449
    var getMatchesAsync = function(inputValue, evt) {
6450
      var locals = {$viewValue: inputValue};
6451
      isLoadingSetter(originalScope, true);
6452
      isNoResultsSetter(originalScope, false);
6453
      $q.when(parserResult.source(originalScope, locals)).then(function(matches) {
6454
        //it might happen that several async queries were in progress if a user were typing fast
6455
        //but we are interested only in responses that correspond to the current view value
6456
        var onCurrentRequest = inputValue === modelCtrl.$viewValue;
6457
        if (onCurrentRequest && hasFocus) {
6458
          if (matches && matches.length > 0) {
6459
            scope.activeIdx = focusFirst ? 0 : -1;
6460
            isNoResultsSetter(originalScope, false);
6461
            scope.matches.length = 0;
6462
6463
            //transform labels
6464
            for (var i = 0; i < matches.length; i++) {
6465
              locals[parserResult.itemName] = matches[i];
6466
              scope.matches.push({
6467
                id: getMatchId(i),
6468
                label: parserResult.viewMapper(scope, locals),
6469
                model: matches[i]
6470
              });
6471
            }
6472
6473
            scope.query = inputValue;
6474
            //position pop-up with matches - we need to re-calculate its position each time we are opening a window
6475
            //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
6476
            //due to other elements being rendered
6477
            recalculatePosition();
6478
6479
            element.attr('aria-expanded', true);
6480
6481
            //Select the single remaining option if user input matches
6482
            if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) {
6483
              if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) {
6484
                $$debounce(function() {
6485
                  scope.select(0, evt);
6486
                }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']);
6487
              } else {
6488
                scope.select(0, evt);
6489
              }
6490
            }
6491
6492
            if (showHint) {
6493
              var firstLabel = scope.matches[0].label;
6494
              if (angular.isString(inputValue) &&
6495
                inputValue.length > 0 &&
6496
                firstLabel.slice(0, inputValue.length).toUpperCase() === inputValue.toUpperCase()) {
6497
                hintInputElem.val(inputValue + firstLabel.slice(inputValue.length));
6498
              } else {
6499
                hintInputElem.val('');
6500
              }
6501
            }
6502
          } else {
6503
            resetMatches();
6504
            isNoResultsSetter(originalScope, true);
6505
          }
6506
        }
6507
        if (onCurrentRequest) {
6508
          isLoadingSetter(originalScope, false);
6509
        }
6510
      }, function() {
6511
        resetMatches();
6512
        isLoadingSetter(originalScope, false);
6513
        isNoResultsSetter(originalScope, true);
6514
      });
6515
    };
6516
6517
    // bind events only if appendToBody params exist - performance feature
6518
    if (appendToBody) {
6519
      angular.element($window).on('resize', fireRecalculating);
6520
      $document.find('body').on('scroll', fireRecalculating);
6521
    }
6522
6523
    // Declare the debounced function outside recalculating for
6524
    // proper debouncing
6525
    var debouncedRecalculate = $$debounce(function() {
6526
      // if popup is visible
6527
      if (scope.matches.length) {
6528
        recalculatePosition();
6529
      }
6530
6531
      scope.moveInProgress = false;
6532
    }, eventDebounceTime);
6533
6534
    // Default progress type
6535
    scope.moveInProgress = false;
6536
6537
    function fireRecalculating() {
6538
      if (!scope.moveInProgress) {
6539
        scope.moveInProgress = true;
6540
        scope.$digest();
6541
      }
6542
6543
      debouncedRecalculate();
6544
    }
6545
6546
    // recalculate actual position and set new values to scope
6547
    // after digest loop is popup in right position
6548
    function recalculatePosition() {
6549
      scope.position = appendToBody ? $position.offset(element) : $position.position(element);
6550
      scope.position.top += element.prop('offsetHeight');
6551
    }
6552
6553
    //we need to propagate user's query so we can higlight matches
6554
    scope.query = undefined;
6555
6556
    //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
6557
    var timeoutPromise;
6558
6559
    var scheduleSearchWithTimeout = function(inputValue) {
6560
      timeoutPromise = $timeout(function() {
6561
        getMatchesAsync(inputValue);
6562
      }, waitTime);
6563
    };
6564
6565
    var cancelPreviousTimeout = function() {
6566
      if (timeoutPromise) {
6567
        $timeout.cancel(timeoutPromise);
6568
      }
6569
    };
6570
6571
    resetMatches();
6572
6573
    scope.assignIsOpen = function (isOpen) {
6574
      isOpenSetter(originalScope, isOpen);
6575
    };
6576
6577
    scope.select = function(activeIdx, evt) {
6578
      //called from within the $digest() cycle
6579
      var locals = {};
6580
      var model, item;
6581
6582
      selected = true;
6583
      locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
6584
      model = parserResult.modelMapper(originalScope, locals);
6585
      $setModelValue(originalScope, model);
6586
      modelCtrl.$setValidity('editable', true);
6587
      modelCtrl.$setValidity('parse', true);
6588
6589
      onSelectCallback(originalScope, {
6590
        $item: item,
6591
        $model: model,
6592
        $label: parserResult.viewMapper(originalScope, locals),
6593
        $event: evt
6594
      });
6595
6596
      resetMatches();
6597
6598
      //return focus to the input element if a match was selected via a mouse click event
6599
      // use timeout to avoid $rootScope:inprog error
6600
      if (scope.$eval(attrs.typeaheadFocusOnSelect) !== false) {
6601
        $timeout(function() { element[0].focus(); }, 0, false);
6602
      }
6603
    };
6604
6605
    //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
6606
    element.on('keydown', function(evt) {
6607
      //typeahead is open and an "interesting" key was pressed
6608
      if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
6609
        return;
6610
      }
6611
6612
      /**
6613
       * if there's nothing selected (i.e. focusFirst) and enter or tab is hit
6614
       * or
6615
       * shift + tab is pressed to bring focus to the previous element
6616
       * then clear the results
6617
       */
6618
      if (scope.activeIdx === -1 && (evt.which === 9 || evt.which === 13) || evt.which === 9 && !!evt.shiftKey) {
6619
        resetMatches();
6620
        scope.$digest();
6621
        return;
6622
      }
6623
6624
      evt.preventDefault();
6625
      var target;
6626
      switch (evt.which) {
6627
        case 9:
6628
        case 13:
6629
          scope.$apply(function () {
6630
            if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) {
6631
              $$debounce(function() {
6632
                scope.select(scope.activeIdx, evt);
6633
              }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']);
6634
            } else {
6635
              scope.select(scope.activeIdx, evt);
6636
            }
6637
          });
6638
          break;
6639
        case 27:
6640
          evt.stopPropagation();
6641
6642
          resetMatches();
6643
          originalScope.$digest();
6644
          break;
6645
        case 38:
6646
          scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1;
6647
          scope.$digest();
6648
          target = popUpEl.find('li')[scope.activeIdx];
6649
          target.parentNode.scrollTop = target.offsetTop;
6650
          break;
6651
        case 40:
6652
          scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
6653
          scope.$digest();
6654
          target = popUpEl.find('li')[scope.activeIdx];
6655
          target.parentNode.scrollTop = target.offsetTop;
6656
          break;
6657
      }
6658
    });
6659
6660
    element.bind('focus', function (evt) {
6661
      hasFocus = true;
6662
      if (minLength === 0 && !modelCtrl.$viewValue) {
6663
        $timeout(function() {
6664
          getMatchesAsync(modelCtrl.$viewValue, evt);
6665
        }, 0);
6666
      }
6667
    });
6668
6669
    element.bind('blur', function(evt) {
6670
      if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) {
6671
        selected = true;
6672
        scope.$apply(function() {
6673
          if (angular.isObject(scope.debounceUpdate) && angular.isNumber(scope.debounceUpdate.blur)) {
6674
            $$debounce(function() {
6675
              scope.select(scope.activeIdx, evt);
6676
            }, scope.debounceUpdate.blur);
6677
          } else {
6678
            scope.select(scope.activeIdx, evt);
6679
          }
6680
        });
6681
      }
6682
      if (!isEditable && modelCtrl.$error.editable) {
6683
        modelCtrl.$setViewValue();
6684
        // Reset validity as we are clearing
6685
        modelCtrl.$setValidity('editable', true);
6686
        modelCtrl.$setValidity('parse', true);
6687
        element.val('');
6688
      }
6689
      hasFocus = false;
6690
      selected = false;
6691
    });
6692
6693
    // Keep reference to click handler to unbind it.
6694
    var dismissClickHandler = function(evt) {
6695
      // Issue #3973
6696
      // Firefox treats right click as a click on document
6697
      if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) {
6698
        resetMatches();
6699
        if (!$rootScope.$$phase) {
6700
          originalScope.$digest();
6701
        }
6702
      }
6703
    };
6704
6705
    $document.on('click', dismissClickHandler);
6706
6707
    originalScope.$on('$destroy', function() {
6708
      $document.off('click', dismissClickHandler);
6709
      if (appendToBody || appendTo) {
6710
        $popup.remove();
6711
      }
6712
6713
      if (appendToBody) {
6714
        angular.element($window).off('resize', fireRecalculating);
6715
        $document.find('body').off('scroll', fireRecalculating);
6716
      }
6717
      // Prevent jQuery cache memory leak
6718
      popUpEl.remove();
6719
6720
      if (showHint) {
6721
          inputsContainer.remove();
6722
      }
6723
    });
6724
6725
    var $popup = $compile(popUpEl)(scope);
6726
6727
    if (appendToBody) {
6728
      $document.find('body').append($popup);
6729
    } else if (appendTo) {
6730
      angular.element(appendTo).eq(0).append($popup);
6731
    } else {
6732
      element.after($popup);
6733
    }
6734
6735
    this.init = function(_modelCtrl, _ngModelOptions) {
6736
      modelCtrl = _modelCtrl;
6737
      ngModelOptions = _ngModelOptions;
6738
6739
      scope.debounceUpdate = modelCtrl.$options && $parse(modelCtrl.$options.debounce)(originalScope);
6740
6741
      //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
6742
      //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
6743
      modelCtrl.$parsers.unshift(function(inputValue) {
6744
        hasFocus = true;
6745
6746
        if (minLength === 0 || inputValue && inputValue.length >= minLength) {
6747
          if (waitTime > 0) {
6748
            cancelPreviousTimeout();
6749
            scheduleSearchWithTimeout(inputValue);
6750
          } else {
6751
            getMatchesAsync(inputValue);
6752
          }
6753
        } else {
6754
          isLoadingSetter(originalScope, false);
6755
          cancelPreviousTimeout();
6756
          resetMatches();
6757
        }
6758
6759
        if (isEditable) {
6760
          return inputValue;
6761
        }
6762
6763
        if (!inputValue) {
6764
          // Reset in case user had typed something previously.
6765
          modelCtrl.$setValidity('editable', true);
6766
          return null;
6767
        }
6768
6769
        modelCtrl.$setValidity('editable', false);
6770
        return undefined;
6771
      });
6772
6773
      modelCtrl.$formatters.push(function(modelValue) {
6774
        var candidateViewValue, emptyViewValue;
6775
        var locals = {};
6776
6777
        // The validity may be set to false via $parsers (see above) if
6778
        // the model is restricted to selected values. If the model
6779
        // is set manually it is considered to be valid.
6780
        if (!isEditable) {
6781
          modelCtrl.$setValidity('editable', true);
6782
        }
6783
6784
        if (inputFormatter) {
6785
          locals.$model = modelValue;
6786
          return inputFormatter(originalScope, locals);
6787
        }
6788
6789
        //it might happen that we don't have enough info to properly render input value
6790
        //we need to check for this situation and simply return model value if we can't apply custom formatting
6791
        locals[parserResult.itemName] = modelValue;
6792
        candidateViewValue = parserResult.viewMapper(originalScope, locals);
6793
        locals[parserResult.itemName] = undefined;
6794
        emptyViewValue = parserResult.viewMapper(originalScope, locals);
6795
6796
        return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue;
6797
      });
6798
    };
6799
  }])
6800
6801
  .directive('uibTypeahead', function() {
6802
    return {

third-party/angularjs-modules-plugins/UI-Bootstrap/ui-bootstrap-1.3.2.js 1 location

@@ 6268-6798 (lines=531) @@
6265
  }])
6266
6267
  .controller('UibTypeaheadController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$$debounce', '$uibPosition', 'uibTypeaheadParser',
6268
    function(originalScope, element, attrs, $compile, $parse, $q, $timeout, $document, $window, $rootScope, $$debounce, $position, typeaheadParser) {
6269
    var HOT_KEYS = [9, 13, 27, 38, 40];
6270
    var eventDebounceTime = 200;
6271
    var modelCtrl, ngModelOptions;
6272
    //SUPPORTED ATTRIBUTES (OPTIONS)
6273
6274
    //minimal no of characters that needs to be entered before typeahead kicks-in
6275
    var minLength = originalScope.$eval(attrs.typeaheadMinLength);
6276
    if (!minLength && minLength !== 0) {
6277
      minLength = 1;
6278
    }
6279
6280
    originalScope.$watch(attrs.typeaheadMinLength, function (newVal) {
6281
        minLength = !newVal && newVal !== 0 ? 1 : newVal;
6282
    });
6283
    
6284
    //minimal wait time after last character typed before typeahead kicks-in
6285
    var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
6286
6287
    //should it restrict model values to the ones selected from the popup only?
6288
    var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
6289
    originalScope.$watch(attrs.typeaheadEditable, function (newVal) {
6290
      isEditable = newVal !== false;
6291
    });
6292
6293
    //binding to a variable that indicates if matches are being retrieved asynchronously
6294
    var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
6295
6296
    //a callback executed when a match is selected
6297
    var onSelectCallback = $parse(attrs.typeaheadOnSelect);
6298
6299
    //should it select highlighted popup value when losing focus?
6300
    var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false;
6301
6302
    //binding to a variable that indicates if there were no results after the query is completed
6303
    var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop;
6304
6305
    var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
6306
6307
    var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false;
6308
6309
    var appendTo = attrs.typeaheadAppendTo ?
6310
      originalScope.$eval(attrs.typeaheadAppendTo) : null;
6311
6312
    var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false;
6313
6314
    //If input matches an item of the list exactly, select it automatically
6315
    var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false;
6316
6317
    //binding to a variable that indicates if dropdown is open
6318
    var isOpenSetter = $parse(attrs.typeaheadIsOpen).assign || angular.noop;
6319
6320
    var showHint = originalScope.$eval(attrs.typeaheadShowHint) || false;
6321
6322
    //INTERNAL VARIABLES
6323
6324
    //model setter executed upon match selection
6325
    var parsedModel = $parse(attrs.ngModel);
6326
    var invokeModelSetter = $parse(attrs.ngModel + '($$$p)');
6327
    var $setModelValue = function(scope, newValue) {
6328
      if (angular.isFunction(parsedModel(originalScope)) &&
6329
        ngModelOptions && ngModelOptions.$options && ngModelOptions.$options.getterSetter) {
6330
        return invokeModelSetter(scope, {$$$p: newValue});
6331
      }
6332
6333
      return parsedModel.assign(scope, newValue);
6334
    };
6335
6336
    //expressions used by typeahead
6337
    var parserResult = typeaheadParser.parse(attrs.uibTypeahead);
6338
6339
    var hasFocus;
6340
6341
    //Used to avoid bug in iOS webview where iOS keyboard does not fire
6342
    //mousedown & mouseup events
6343
    //Issue #3699
6344
    var selected;
6345
6346
    //create a child scope for the typeahead directive so we are not polluting original scope
6347
    //with typeahead-specific data (matches, query etc.)
6348
    var scope = originalScope.$new();
6349
    var offDestroy = originalScope.$on('$destroy', function() {
6350
      scope.$destroy();
6351
    });
6352
    scope.$on('$destroy', offDestroy);
6353
6354
    // WAI-ARIA
6355
    var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
6356
    element.attr({
6357
      'aria-autocomplete': 'list',
6358
      'aria-expanded': false,
6359
      'aria-owns': popupId
6360
    });
6361
6362
    var inputsContainer, hintInputElem;
6363
    //add read-only input to show hint
6364
    if (showHint) {
6365
      inputsContainer = angular.element('<div></div>');
6366
      inputsContainer.css('position', 'relative');
6367
      element.after(inputsContainer);
6368
      hintInputElem = element.clone();
6369
      hintInputElem.attr('placeholder', '');
6370
      hintInputElem.attr('tabindex', '-1');
6371
      hintInputElem.val('');
6372
      hintInputElem.css({
6373
        'position': 'absolute',
6374
        'top': '0px',
6375
        'left': '0px',
6376
        'border-color': 'transparent',
6377
        'box-shadow': 'none',
6378
        'opacity': 1,
6379
        'background': 'none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)',
6380
        'color': '#999'
6381
      });
6382
      element.css({
6383
        'position': 'relative',
6384
        'vertical-align': 'top',
6385
        'background-color': 'transparent'
6386
      });
6387
      inputsContainer.append(hintInputElem);
6388
      hintInputElem.after(element);
6389
    }
6390
6391
    //pop-up element used to display matches
6392
    var popUpEl = angular.element('<div uib-typeahead-popup></div>');
6393
    popUpEl.attr({
6394
      id: popupId,
6395
      matches: 'matches',
6396
      active: 'activeIdx',
6397
      select: 'select(activeIdx, evt)',
6398
      'move-in-progress': 'moveInProgress',
6399
      query: 'query',
6400
      position: 'position',
6401
      'assign-is-open': 'assignIsOpen(isOpen)',
6402
      debounce: 'debounceUpdate'
6403
    });
6404
    //custom item template
6405
    if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
6406
      popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
6407
    }
6408
6409
    if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) {
6410
      popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl);
6411
    }
6412
6413
    var resetHint = function() {
6414
      if (showHint) {
6415
        hintInputElem.val('');
6416
      }
6417
    };
6418
6419
    var resetMatches = function() {
6420
      scope.matches = [];
6421
      scope.activeIdx = -1;
6422
      element.attr('aria-expanded', false);
6423
      resetHint();
6424
    };
6425
6426
    var getMatchId = function(index) {
6427
      return popupId + '-option-' + index;
6428
    };
6429
6430
    // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead.
6431
    // This attribute is added or removed automatically when the `activeIdx` changes.
6432
    scope.$watch('activeIdx', function(index) {
6433
      if (index < 0) {
6434
        element.removeAttr('aria-activedescendant');
6435
      } else {
6436
        element.attr('aria-activedescendant', getMatchId(index));
6437
      }
6438
    });
6439
6440
    var inputIsExactMatch = function(inputValue, index) {
6441
      if (scope.matches.length > index && inputValue) {
6442
        return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase();
6443
      }
6444
6445
      return false;
6446
    };
6447
6448
    var getMatchesAsync = function(inputValue, evt) {
6449
      var locals = {$viewValue: inputValue};
6450
      isLoadingSetter(originalScope, true);
6451
      isNoResultsSetter(originalScope, false);
6452
      $q.when(parserResult.source(originalScope, locals)).then(function(matches) {
6453
        //it might happen that several async queries were in progress if a user were typing fast
6454
        //but we are interested only in responses that correspond to the current view value
6455
        var onCurrentRequest = inputValue === modelCtrl.$viewValue;
6456
        if (onCurrentRequest && hasFocus) {
6457
          if (matches && matches.length > 0) {
6458
            scope.activeIdx = focusFirst ? 0 : -1;
6459
            isNoResultsSetter(originalScope, false);
6460
            scope.matches.length = 0;
6461
6462
            //transform labels
6463
            for (var i = 0; i < matches.length; i++) {
6464
              locals[parserResult.itemName] = matches[i];
6465
              scope.matches.push({
6466
                id: getMatchId(i),
6467
                label: parserResult.viewMapper(scope, locals),
6468
                model: matches[i]
6469
              });
6470
            }
6471
6472
            scope.query = inputValue;
6473
            //position pop-up with matches - we need to re-calculate its position each time we are opening a window
6474
            //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
6475
            //due to other elements being rendered
6476
            recalculatePosition();
6477
6478
            element.attr('aria-expanded', true);
6479
6480
            //Select the single remaining option if user input matches
6481
            if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) {
6482
              if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) {
6483
                $$debounce(function() {
6484
                  scope.select(0, evt);
6485
                }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']);
6486
              } else {
6487
                scope.select(0, evt);
6488
              }
6489
            }
6490
6491
            if (showHint) {
6492
              var firstLabel = scope.matches[0].label;
6493
              if (angular.isString(inputValue) &&
6494
                inputValue.length > 0 &&
6495
                firstLabel.slice(0, inputValue.length).toUpperCase() === inputValue.toUpperCase()) {
6496
                hintInputElem.val(inputValue + firstLabel.slice(inputValue.length));
6497
              } else {
6498
                hintInputElem.val('');
6499
              }
6500
            }
6501
          } else {
6502
            resetMatches();
6503
            isNoResultsSetter(originalScope, true);
6504
          }
6505
        }
6506
        if (onCurrentRequest) {
6507
          isLoadingSetter(originalScope, false);
6508
        }
6509
      }, function() {
6510
        resetMatches();
6511
        isLoadingSetter(originalScope, false);
6512
        isNoResultsSetter(originalScope, true);
6513
      });
6514
    };
6515
6516
    // bind events only if appendToBody params exist - performance feature
6517
    if (appendToBody) {
6518
      angular.element($window).on('resize', fireRecalculating);
6519
      $document.find('body').on('scroll', fireRecalculating);
6520
    }
6521
6522
    // Declare the debounced function outside recalculating for
6523
    // proper debouncing
6524
    var debouncedRecalculate = $$debounce(function() {
6525
      // if popup is visible
6526
      if (scope.matches.length) {
6527
        recalculatePosition();
6528
      }
6529
6530
      scope.moveInProgress = false;
6531
    }, eventDebounceTime);
6532
6533
    // Default progress type
6534
    scope.moveInProgress = false;
6535
6536
    function fireRecalculating() {
6537
      if (!scope.moveInProgress) {
6538
        scope.moveInProgress = true;
6539
        scope.$digest();
6540
      }
6541
6542
      debouncedRecalculate();
6543
    }
6544
6545
    // recalculate actual position and set new values to scope
6546
    // after digest loop is popup in right position
6547
    function recalculatePosition() {
6548
      scope.position = appendToBody ? $position.offset(element) : $position.position(element);
6549
      scope.position.top += element.prop('offsetHeight');
6550
    }
6551
6552
    //we need to propagate user's query so we can higlight matches
6553
    scope.query = undefined;
6554
6555
    //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
6556
    var timeoutPromise;
6557
6558
    var scheduleSearchWithTimeout = function(inputValue) {
6559
      timeoutPromise = $timeout(function() {
6560
        getMatchesAsync(inputValue);
6561
      }, waitTime);
6562
    };
6563
6564
    var cancelPreviousTimeout = function() {
6565
      if (timeoutPromise) {
6566
        $timeout.cancel(timeoutPromise);
6567
      }
6568
    };
6569
6570
    resetMatches();
6571
6572
    scope.assignIsOpen = function (isOpen) {
6573
      isOpenSetter(originalScope, isOpen);
6574
    };
6575
6576
    scope.select = function(activeIdx, evt) {
6577
      //called from within the $digest() cycle
6578
      var locals = {};
6579
      var model, item;
6580
6581
      selected = true;
6582
      locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
6583
      model = parserResult.modelMapper(originalScope, locals);
6584
      $setModelValue(originalScope, model);
6585
      modelCtrl.$setValidity('editable', true);
6586
      modelCtrl.$setValidity('parse', true);
6587
6588
      onSelectCallback(originalScope, {
6589
        $item: item,
6590
        $model: model,
6591
        $label: parserResult.viewMapper(originalScope, locals),
6592
        $event: evt
6593
      });
6594
6595
      resetMatches();
6596
6597
      //return focus to the input element if a match was selected via a mouse click event
6598
      // use timeout to avoid $rootScope:inprog error
6599
      if (scope.$eval(attrs.typeaheadFocusOnSelect) !== false) {
6600
        $timeout(function() { element[0].focus(); }, 0, false);
6601
      }
6602
    };
6603
6604
    //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
6605
    element.on('keydown', function(evt) {
6606
      //typeahead is open and an "interesting" key was pressed
6607
      if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
6608
        return;
6609
      }
6610
6611
      /**
6612
       * if there's nothing selected (i.e. focusFirst) and enter or tab is hit
6613
       * or
6614
       * shift + tab is pressed to bring focus to the previous element
6615
       * then clear the results
6616
       */
6617
      if (scope.activeIdx === -1 && (evt.which === 9 || evt.which === 13) || evt.which === 9 && !!evt.shiftKey) {
6618
        resetMatches();
6619
        scope.$digest();
6620
        return;
6621
      }
6622
6623
      evt.preventDefault();
6624
      var target;
6625
      switch (evt.which) {
6626
        case 9:
6627
        case 13:
6628
          scope.$apply(function () {
6629
            if (angular.isNumber(scope.debounceUpdate) || angular.isObject(scope.debounceUpdate)) {
6630
              $$debounce(function() {
6631
                scope.select(scope.activeIdx, evt);
6632
              }, angular.isNumber(scope.debounceUpdate) ? scope.debounceUpdate : scope.debounceUpdate['default']);
6633
            } else {
6634
              scope.select(scope.activeIdx, evt);
6635
            }
6636
          });
6637
          break;
6638
        case 27:
6639
          evt.stopPropagation();
6640
6641
          resetMatches();
6642
          originalScope.$digest();
6643
          break;
6644
        case 38:
6645
          scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1;
6646
          scope.$digest();
6647
          target = popUpEl.find('li')[scope.activeIdx];
6648
          target.parentNode.scrollTop = target.offsetTop;
6649
          break;
6650
        case 40:
6651
          scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
6652
          scope.$digest();
6653
          target = popUpEl.find('li')[scope.activeIdx];
6654
          target.parentNode.scrollTop = target.offsetTop;
6655
          break;
6656
      }
6657
    });
6658
6659
    element.bind('focus', function (evt) {
6660
      hasFocus = true;
6661
      if (minLength === 0 && !modelCtrl.$viewValue) {
6662
        $timeout(function() {
6663
          getMatchesAsync(modelCtrl.$viewValue, evt);
6664
        }, 0);
6665
      }
6666
    });
6667
6668
    element.bind('blur', function(evt) {
6669
      if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) {
6670
        selected = true;
6671
        scope.$apply(function() {
6672
          if (angular.isObject(scope.debounceUpdate) && angular.isNumber(scope.debounceUpdate.blur)) {
6673
            $$debounce(function() {
6674
              scope.select(scope.activeIdx, evt);
6675
            }, scope.debounceUpdate.blur);
6676
          } else {
6677
            scope.select(scope.activeIdx, evt);
6678
          }
6679
        });
6680
      }
6681
      if (!isEditable && modelCtrl.$error.editable) {
6682
        modelCtrl.$setViewValue();
6683
        // Reset validity as we are clearing
6684
        modelCtrl.$setValidity('editable', true);
6685
        modelCtrl.$setValidity('parse', true);
6686
        element.val('');
6687
      }
6688
      hasFocus = false;
6689
      selected = false;
6690
    });
6691
6692
    // Keep reference to click handler to unbind it.
6693
    var dismissClickHandler = function(evt) {
6694
      // Issue #3973
6695
      // Firefox treats right click as a click on document
6696
      if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) {
6697
        resetMatches();
6698
        if (!$rootScope.$$phase) {
6699
          originalScope.$digest();
6700
        }
6701
      }
6702
    };
6703
6704
    $document.on('click', dismissClickHandler);
6705
6706
    originalScope.$on('$destroy', function() {
6707
      $document.off('click', dismissClickHandler);
6708
      if (appendToBody || appendTo) {
6709
        $popup.remove();
6710
      }
6711
6712
      if (appendToBody) {
6713
        angular.element($window).off('resize', fireRecalculating);
6714
        $document.find('body').off('scroll', fireRecalculating);
6715
      }
6716
      // Prevent jQuery cache memory leak
6717
      popUpEl.remove();
6718
6719
      if (showHint) {
6720
          inputsContainer.remove();
6721
      }
6722
    });
6723
6724
    var $popup = $compile(popUpEl)(scope);
6725
6726
    if (appendToBody) {
6727
      $document.find('body').append($popup);
6728
    } else if (appendTo) {
6729
      angular.element(appendTo).eq(0).append($popup);
6730
    } else {
6731
      element.after($popup);
6732
    }
6733
6734
    this.init = function(_modelCtrl, _ngModelOptions) {
6735
      modelCtrl = _modelCtrl;
6736
      ngModelOptions = _ngModelOptions;
6737
6738
      scope.debounceUpdate = modelCtrl.$options && $parse(modelCtrl.$options.debounce)(originalScope);
6739
6740
      //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
6741
      //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
6742
      modelCtrl.$parsers.unshift(function(inputValue) {
6743
        hasFocus = true;
6744
6745
        if (minLength === 0 || inputValue && inputValue.length >= minLength) {
6746
          if (waitTime > 0) {
6747
            cancelPreviousTimeout();
6748
            scheduleSearchWithTimeout(inputValue);
6749
          } else {
6750
            getMatchesAsync(inputValue);
6751
          }
6752
        } else {
6753
          isLoadingSetter(originalScope, false);
6754
          cancelPreviousTimeout();
6755
          resetMatches();
6756
        }
6757
6758
        if (isEditable) {
6759
          return inputValue;
6760
        }
6761
6762
        if (!inputValue) {
6763
          // Reset in case user had typed something previously.
6764
          modelCtrl.$setValidity('editable', true);
6765
          return null;
6766
        }
6767
6768
        modelCtrl.$setValidity('editable', false);
6769
        return undefined;
6770
      });
6771
6772
      modelCtrl.$formatters.push(function(modelValue) {
6773
        var candidateViewValue, emptyViewValue;
6774
        var locals = {};
6775
6776
        // The validity may be set to false via $parsers (see above) if
6777
        // the model is restricted to selected values. If the model
6778
        // is set manually it is considered to be valid.
6779
        if (!isEditable) {
6780
          modelCtrl.$setValidity('editable', true);
6781
        }
6782
6783
        if (inputFormatter) {
6784
          locals.$model = modelValue;
6785
          return inputFormatter(originalScope, locals);
6786
        }
6787
6788
        //it might happen that we don't have enough info to properly render input value
6789
        //we need to check for this situation and simply return model value if we can't apply custom formatting
6790
        locals[parserResult.itemName] = modelValue;
6791
        candidateViewValue = parserResult.viewMapper(originalScope, locals);
6792
        locals[parserResult.itemName] = undefined;
6793
        emptyViewValue = parserResult.viewMapper(originalScope, locals);
6794
6795
        return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue;
6796
      });
6797
    };
6798
  }])
6799
6800
  .directive('uibTypeahead', function() {
6801
    return {