text: /**
   * @license
   * Copyright (C) 2016 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   * http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  (function() {
    'use strict';
  
    const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
    const DEBOUNCE_WAIT_MS = 200;
  
    /**
      * @appliesMixin Gerrit.FireMixin
      * @appliesMixin Gerrit.KeyboardShortcutMixin
      */
    class GrAutocomplete extends Polymer.mixinBehaviors( [
      Gerrit.FireBehavior,
      Gerrit.KeyboardShortcutBehavior,
    ], Polymer.GestureEventListeners(
        Polymer.LegacyElementMixin(
            Polymer.Element))) {
      static get is() { return 'gr-autocomplete'; }
      /**
       * Fired when a value is chosen.
       *
       * @event commit
       */
  
      /**
       * Fired when the user cancels.
       *
       * @event cancel
       */
  
      /**
       * Fired on keydown to allow for custom hooks into autocomplete textbox
       * behavior.
       *
       * @event input-keydown
       */
  
      static get properties() {
        return {
  
          /**
         * Query for requesting autocomplete suggestions. The function should
         * accept the input as a string parameter and return a promise. The
         * promise yields an array of suggestion objects with "name", "label",
         * "value" properties. The "name" property will be displayed in the
         * suggestion entry. The "label" property will, when specified, appear
         * next to the "name" as label text. The "value" property will be emitted
         * if that suggestion is selected.
         *
         * @type {function(string): Promise<?>}
         */
          query: {
            type: Function,
            value() {
              return function() {
                return Promise.resolve([]);
              };
            },
          },
  
          /**
           * The number of characters that must be typed before suggestions are
           * made. If threshold is zero, default suggestions are enabled.
           */
          threshold: {
            type: Number,
            value: 1,
          },
  
          allowNonSuggestedValues: Boolean,
          borderless: Boolean,
          disabled: Boolean,
          showSearchIcon: {
            type: Boolean,
            value: false,
          },
          /**
           * Vertical offset needed for an element with 20px line-height, 4px
           * padding and 1px border (30px height total). Plus 1px spacing between
           * input and dropdown. Inputs with different line-height or padding will
           * need to tweak vertical offset.
           */
          verticalOffset: {
            type: Number,
            value: 31,
          },
  
          text: {
            type: String,
            value: '',
            notify: true,
          },
  
          placeholder: String,
  
          clearOnCommit: {
            type: Boolean,
            value: false,
          },
  
          /**
           * When true, tab key autocompletes but does not fire the commit event.
           * When false, tab key not caught, and focus is removed from the element.
           * See Issue 4556, Issue 6645.
           */
          tabComplete: {
            type: Boolean,
            value: false,
          },
  
          value: {
            type: String,
            notify: true,
          },
  
          /**
         * Multi mode appends autocompleted entries to the value.
         * If false, autocompleted entries replace value.
         */
          multi: {
            type: Boolean,
            value: false,
          },
  
          /**
         * When true and uncommitted text is left in the autocomplete input after
         * blurring, the text will appear red.
         */
          warnUncommitted: {
            type: Boolean,
            value: false,
          },
  
          /**
         * When true, querying for suggestions is not debounced w/r/t keypresses
         */
          noDebounce: {
            type: Boolean,
            value: false,
          },
  
          /** @type {?} */
          _suggestions: {
            type: Array,
            value() { return []; },
          },
  
          _suggestionEls: {
            type: Array,
            value() { return []; },
          },
  
          _index: Number,
          _disableSuggestions: {
            type: Boolean,
            value: false,
          },
          _focused: {
            type: Boolean,
            value: false,
          },
  
          /** The DOM element of the selected suggestion. */
          _selected: Object,
        };
      }
  
      static get observers() {
        return [
          '_maybeOpenDropdown(_suggestions, _focused)',
          '_updateSuggestions(text, threshold, noDebounce)',
        ];
      }
  
      get _nativeInput() {
        // In Polymer 2 inputElement isn't nativeInput anymore
        return this.$.input.$.nativeInput || this.$.input.inputElement;
      }
  
      attached() {
        super.attached();
        this.listen(document.body, 'click', '_handleBodyClick');
      }
  
      detached() {
        super.detached();
        this.unlisten(document.body, 'click', '_handleBodyClick');
        this.cancelDebouncer('update-suggestions');
      }
  
      get focusStart() {
        return this.$.input;
      }
  
      focus() {
        this._nativeInput.focus();
      }
  
      selectAll() {
        const nativeInputElement = this._nativeInput;
        if (!this.$.input.value) { return; }
        nativeInputElement.setSelectionRange(0, this.$.input.value.length);
      }
  
      clear() {
        this.text = '';
      }
  
      _handleItemSelect(e) {
        // Let _handleKeydown deal with keyboard interaction.
        if (e.detail.trigger !== 'click') { return; }
        this._selected = e.detail.selected;
        this._commit();
      }
  
      get _inputElement() {
        // Polymer2: this.$ can be undefined when this is first evaluated.
        return this.$ && this.$.input;
      }
  
      /**
       * Set the text of the input without triggering the suggestion dropdown.
       * @param {string} text The new text for the input.
       */
      setText(text) {
        this._disableSuggestions = true;
        this.text = text;
        this._disableSuggestions = false;
      }
  
      _onInputFocus() {
        this._focused = true;
        this._updateSuggestions(this.text, this.threshold, this.noDebounce);
        this.$.input.classList.remove('warnUncommitted');
        // Needed so that --paper-input-container-input updated style is applied.
        this.updateStyles();
      }
  
      _onInputBlur() {
        this.$.input.classList.toggle('warnUncommitted',
            this.warnUncommitted && this.text.length && !this._focused);
        // Needed so that --paper-input-container-input updated style is applied.
        this.updateStyles();
      }
  
      _updateSuggestions(text, threshold, noDebounce) {
        // Polymer 2: check for undefined
        if ([text, threshold, noDebounce].some(arg => arg === undefined)) {
          return;
        }
  
        // Reset _suggestions for every update
        // This will also prevent from carrying over suggestions:
        // @see Issue 12039
        this._suggestions = [];
  
        // TODO(taoalpha): Also skip if text has not changed
  
        if (this._disableSuggestions) { return; }
        if (text.length < threshold) {
          this.value = '';
          return;
        }
  
        if (!this._focused) {
          return;
        }
  
        const update = () => {
          this.query(text).then(suggestions => {
            if (text !== this.text) {
              // Late response.
              return;
            }
            for (const suggestion of suggestions) {
              suggestion.text = suggestion.name;
            }
            this._suggestions = suggestions;
            Polymer.dom.flush();
            if (this._index === -1) {
              this.value = '';
            }
          });
        };
  
        if (noDebounce) {
          update();
        } else {
          this.debounce('update-suggestions', update, DEBOUNCE_WAIT_MS);
        }
      }
  
      _maybeOpenDropdown(suggestions, focused) {
        if (suggestions.length > 0 && focused) {
          return this.$.suggestions.open();
        }
        return this.$.suggestions.close();
      }
  
      _computeClass(borderless) {
        return borderless ? 'borderless' : '';
      }
  
      /**
       * _handleKeydown used for key handling in the this.$.input AND all child
       * autocomplete options.
       */
      _handleKeydown(e) {
        this._focused = true;
        switch (e.keyCode) {
          case 38: // Up
            e.preventDefault();
            this.$.suggestions.cursorUp();
            break;
          case 40: // Down
            e.preventDefault();
            this.$.suggestions.cursorDown();
            break;
          case 27: // Escape
            e.preventDefault();
            this._cancel();
            break;
          case 9: // Tab
            if (this._suggestions.length > 0 && this.tabComplete) {
              e.preventDefault();
              this._handleInputCommit(true);
              this.focus();
            } else {
              this._focused = false;
            }
            break;
          case 13: // Enter
            if (this.modifierPressed(e)) { break; }
            e.preventDefault();
            this._handleInputCommit();
            break;
          default:
            // For any normal keypress, return focus to the input to allow for
            // unbroken user input.
            this.focus();
  
            // Since this has been a normal keypress, the suggestions will have
            // been based on a previous input. Clear them. This prevents an
            // outdated suggestion from being used if the input keystroke is
            // immediately followed by a commit keystroke. @see Issue 8655
            this._suggestions = [];
        }
        this.fire('input-keydown', {keyCode: e.keyCode, input: this.$.input});
      }
  
      _cancel() {
        if (this._suggestions.length) {
          this.set('_suggestions', []);
        } else {
          this.fire('cancel');
        }
      }
  
      /**
       * @param {boolean=} opt_tabComplete
       */
      _handleInputCommit(opt_tabComplete) {
        // Nothing to do if the dropdown is not open.
        if (!this.allowNonSuggestedValues
            && this.$.suggestions.isHidden) { return; }
  
        this._selected = this.$.suggestions.getCursorTarget();
        this._commit(opt_tabComplete);
      }
  
      _updateValue(suggestion, suggestions) {
        if (!suggestion) { return; }
        const completed = suggestions[suggestion.dataset.index].value;
        if (this.multi) {
          // Append the completed text to the end of the string.
          // Allow spaces within quoted terms.
          const tokens = this.text.match(TOKENIZE_REGEX);
          tokens[tokens.length - 1] = completed;
          this.value = tokens.join(' ');
        } else {
          this.value = completed;
        }
      }
  
      _handleBodyClick(e) {
        const eventPath = Polymer.dom(e).path;
        for (let i = 0; i < eventPath.length; i++) {
          if (eventPath[i] === this) {
            return;
          }
        }
        this._focused = false;
      }
  
      _handleSuggestionTap(e) {
        e.stopPropagation();
        this.$.cursor.setCursor(e.target);
        this._commit();
      }
  
      /**
       * Commits the suggestion, optionally firing the commit event.
       *
       * @param {boolean=} opt_silent Allows for silent committing of an
       *     autocomplete suggestion in order to handle cases like tab-to-complete
       *     without firing the commit event.
       */
      _commit(opt_silent) {
        // Allow values that are not in suggestion list iff suggestions are empty.
        if (this._suggestions.length > 0) {
          this._updateValue(this._selected, this._suggestions);
        } else {
          this.value = this.text || '';
        }
  
        const value = this.value;
  
        // Value and text are mirrors of each other in multi mode.
        if (this.multi) {
          this.setText(this.value);
        } else {
          if (!this.clearOnCommit && this._selected) {
            this.setText(this._suggestions[this._selected.dataset.index].name);
          } else {
            this.clear();
          }
        }
  
        this._suggestions = [];
        if (!opt_silent) {
          this.fire('commit', {value});
        }
  
        this._textChangedSinceCommit = false;
      }
  
      _computeShowSearchIconClass(showSearchIcon) {
        return showSearchIcon ? 'showSearchIcon' : '';
      }
    }
  
    customElements.define(GrAutocomplete.is, GrAutocomplete);
  })();
  