|  | /** | 
|  | * @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'; | 
|  |  | 
|  | // Possible static search options for auto complete, without negations. | 
|  | const SEARCH_OPERATORS = [ | 
|  | 'added:', | 
|  | 'age:', | 
|  | 'age:1week', // Give an example age | 
|  | 'assignee:', | 
|  | 'author:', | 
|  | 'branch:', | 
|  | 'bug:', | 
|  | 'cc:', | 
|  | 'cc:self', | 
|  | 'change:', | 
|  | 'comment:', | 
|  | 'commentby:', | 
|  | 'commit:', | 
|  | 'committer:', | 
|  | 'conflicts:', | 
|  | 'deleted:', | 
|  | 'delta:', | 
|  | 'dir:', | 
|  | 'directory:', | 
|  | 'ext:', | 
|  | 'extension:', | 
|  | 'file:', | 
|  | 'footer:', | 
|  | 'from:', | 
|  | 'has:', | 
|  | 'has:draft', | 
|  | 'has:edit', | 
|  | 'has:star', | 
|  | 'has:stars', | 
|  | 'has:unresolved', | 
|  | 'hashtag:', | 
|  | 'intopic:', | 
|  | 'is:', | 
|  | 'is:abandoned', | 
|  | 'is:assigned', | 
|  | 'is:closed', | 
|  | 'is:ignored', | 
|  | 'is:mergeable', | 
|  | 'is:merged', | 
|  | 'is:open', | 
|  | 'is:owner', | 
|  | 'is:pending', | 
|  | 'is:private', | 
|  | 'is:reviewed', | 
|  | 'is:reviewer', | 
|  | 'is:starred', | 
|  | 'is:submittable', | 
|  | 'is:watched', | 
|  | 'is:wip', | 
|  | 'label:', | 
|  | 'message:', | 
|  | 'onlyexts:', | 
|  | 'onlyextensions:', | 
|  | 'owner:', | 
|  | 'ownerin:', | 
|  | 'parentproject:', | 
|  | 'project:', | 
|  | 'projects:', | 
|  | 'query:', | 
|  | 'ref:', | 
|  | 'reviewedby:', | 
|  | 'reviewer:', | 
|  | 'reviewer:self', | 
|  | 'reviewerin:', | 
|  | 'size:', | 
|  | 'star:', | 
|  | 'status:', | 
|  | 'status:abandoned', | 
|  | 'status:closed', | 
|  | 'status:merged', | 
|  | 'status:open', | 
|  | 'status:pending', | 
|  | 'status:reviewed', | 
|  | 'topic:', | 
|  | 'tr:', | 
|  | ]; | 
|  |  | 
|  | // All of the ops, with corresponding negations. | 
|  | const SEARCH_OPERATORS_WITH_NEGATIONS = | 
|  | SEARCH_OPERATORS.concat(SEARCH_OPERATORS.map(op => `-${op}`)); | 
|  |  | 
|  | const MAX_AUTOCOMPLETE_RESULTS = 10; | 
|  |  | 
|  | const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g; | 
|  |  | 
|  | Polymer({ | 
|  | is: 'gr-search-bar', | 
|  |  | 
|  | /** | 
|  | * Fired when a search is committed | 
|  | * | 
|  | * @event handle-search | 
|  | */ | 
|  |  | 
|  | behaviors: [ | 
|  | Gerrit.KeyboardShortcutBehavior, | 
|  | Gerrit.URLEncodingBehavior, | 
|  | ], | 
|  |  | 
|  | properties: { | 
|  | value: { | 
|  | type: String, | 
|  | value: '', | 
|  | notify: true, | 
|  | observer: '_valueChanged', | 
|  | }, | 
|  | keyEventTarget: { | 
|  | type: Object, | 
|  | value() { return document.body; }, | 
|  | }, | 
|  | query: { | 
|  | type: Function, | 
|  | value() { | 
|  | return this._getSearchSuggestions.bind(this); | 
|  | }, | 
|  | }, | 
|  | projectSuggestions: { | 
|  | type: Function, | 
|  | value() { | 
|  | return () => Promise.resolve([]); | 
|  | }, | 
|  | }, | 
|  | groupSuggestions: { | 
|  | type: Function, | 
|  | value() { | 
|  | return () => Promise.resolve([]); | 
|  | }, | 
|  | }, | 
|  | accountSuggestions: { | 
|  | type: Function, | 
|  | value() { | 
|  | return () => Promise.resolve([]); | 
|  | }, | 
|  | }, | 
|  | _inputVal: String, | 
|  | _threshold: { | 
|  | type: Number, | 
|  | value: 1, | 
|  | }, | 
|  | }, | 
|  |  | 
|  | keyboardShortcuts() { | 
|  | return { | 
|  | [this.Shortcut.SEARCH]: '_handleSearch', | 
|  | }; | 
|  | }, | 
|  |  | 
|  | _valueChanged(value) { | 
|  | this._inputVal = value; | 
|  | }, | 
|  |  | 
|  | _handleInputCommit(e) { | 
|  | this._preventDefaultAndNavigateToInputVal(e); | 
|  | }, | 
|  |  | 
|  | /** | 
|  | * This function is called in a few different cases: | 
|  | *   - e.target is the search button | 
|  | *   - e.target is the gr-autocomplete widget (#searchInput) | 
|  | *   - e.target is the input element wrapped within #searchInput | 
|  | * | 
|  | * @param {!Event} e | 
|  | */ | 
|  | _preventDefaultAndNavigateToInputVal(e) { | 
|  | e.preventDefault(); | 
|  | const target = Polymer.dom(e).rootTarget; | 
|  | // If the target is the #searchInput or has a sub-input component, that | 
|  | // is what holds the focus as opposed to the target from the DOM event. | 
|  | if (target.$.input) { | 
|  | target.$.input.blur(); | 
|  | } else { | 
|  | target.blur(); | 
|  | } | 
|  | if (this._inputVal) { | 
|  | this.dispatchEvent(new CustomEvent('handle-search', { | 
|  | detail: {inputVal: this._inputVal}, | 
|  | })); | 
|  | } | 
|  | }, | 
|  |  | 
|  | /** | 
|  | * Determine what array of possible suggestions should be provided | 
|  | *     to _getSearchSuggestions. | 
|  | * @param {string} input - The full search term, in lowercase. | 
|  | * @return {!Promise} This returns a promise that resolves to an array of | 
|  | *     suggestion objects. | 
|  | */ | 
|  | _fetchSuggestions(input) { | 
|  | // Split the input on colon to get a two part predicate/expression. | 
|  | const splitInput = input.split(':'); | 
|  | const predicate = splitInput[0]; | 
|  | const expression = splitInput[1] || ''; | 
|  | // Switch on the predicate to determine what to autocomplete. | 
|  | switch (predicate) { | 
|  | case 'ownerin': | 
|  | case 'reviewerin': | 
|  | // Fetch groups. | 
|  | return this.groupSuggestions(predicate, expression); | 
|  |  | 
|  | case 'parentproject': | 
|  | case 'project': | 
|  | // Fetch projects. | 
|  | return this.projectSuggestions(predicate, expression); | 
|  |  | 
|  | case 'author': | 
|  | case 'cc': | 
|  | case 'commentby': | 
|  | case 'committer': | 
|  | case 'from': | 
|  | case 'owner': | 
|  | case 'reviewedby': | 
|  | case 'reviewer': | 
|  | // Fetch accounts. | 
|  | return this.accountSuggestions(predicate, expression); | 
|  |  | 
|  | default: | 
|  | return Promise.resolve(SEARCH_OPERATORS_WITH_NEGATIONS | 
|  | .filter(operator => operator.includes(input)) | 
|  | .map(operator => ({text: operator}))); | 
|  | } | 
|  | }, | 
|  |  | 
|  | /** | 
|  | * Get the sorted, pruned list of suggestions for the current search query. | 
|  | * @param {string} input - The complete search query. | 
|  | * @return {!Promise} This returns a promise that resolves to an array of | 
|  | *     suggestions. | 
|  | */ | 
|  | _getSearchSuggestions(input) { | 
|  | // Allow spaces within quoted terms. | 
|  | const tokens = input.match(TOKENIZE_REGEX); | 
|  | const trimmedInput = tokens[tokens.length - 1].toLowerCase(); | 
|  |  | 
|  | return this._fetchSuggestions(trimmedInput) | 
|  | .then(suggestions => { | 
|  | if (!suggestions || !suggestions.length) { return []; } | 
|  | return suggestions | 
|  | // Prioritize results that start with the input. | 
|  | .sort((a, b) => { | 
|  | const aContains = a.text.toLowerCase().indexOf(trimmedInput); | 
|  | const bContains = b.text.toLowerCase().indexOf(trimmedInput); | 
|  | if (aContains === bContains) { | 
|  | return a.text.localeCompare(b.text); | 
|  | } | 
|  | if (aContains === -1) { | 
|  | return 1; | 
|  | } | 
|  | if (bContains === -1) { | 
|  | return -1; | 
|  | } | 
|  | return aContains - bContains; | 
|  | }) | 
|  | // Return only the first {MAX_AUTOCOMPLETE_RESULTS} results. | 
|  | .slice(0, MAX_AUTOCOMPLETE_RESULTS - 1) | 
|  | // Map to an object to play nice with gr-autocomplete. | 
|  | .map(({text, label}) => { | 
|  | return { | 
|  | name: text, | 
|  | value: text, | 
|  | label, | 
|  | }; | 
|  | }); | 
|  | }); | 
|  | }, | 
|  |  | 
|  | _handleSearch(e) { | 
|  | const keyboardEvent = this.getKeyboardEvent(e); | 
|  | if (this.shouldSuppressKeyboardShortcut(e) || | 
|  | (this.modifierPressed(e) && !keyboardEvent.shiftKey)) { return; } | 
|  |  | 
|  | e.preventDefault(); | 
|  | this.$.searchInput.focus(); | 
|  | this.$.searchInput.selectAll(); | 
|  | }, | 
|  | }); | 
|  | })(); |