/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
release-1.13
src/search/QuickSearchField.js
347 строк
15 KB
Saurabh Kathpalia
Add option for first highlight index in Quick Open and Search History (#13444)
15 июн 2017, 17:53
15 июн 2017, 17:53
038bcb6
Код
Авторство
О чём код?
/* * Copyright (c) 2013 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /* * Text field with attached dropdown list that is updated (based on a provider) whenever the text changes. * * For styling, the DOM structure of the popup is as follows: * body * ol.quick-search-container * li * li.highlight * li * And the text field is: * input * input.no-results */ define(function (require, exports, module) { "use strict"; var KeyEvent = require("utils/KeyEvent"); /** * Attaches to an existing <input> tag * * @constructor * * @param {!jQueryObject} $input * @param {!function(string):($.Promise|Array.<*>|{error:?string}} options.resultProvider * Given the current search text, returns an an array of result objects, an error object, or a * Promise that yields one of those. If the Promise is still outstanding when the query next * changes, resultProvider() will be called again (without waiting for the earlier Promise), and * the Promise's result will be ignored. * If the provider yields [], or a non-null error string, input is decorated with ".no-results"; if * the provider yields a null error string, input is not decorated. * * @param {!function(*, string):string} options.formatter * Converts one result object to a string of HTML text. Passed the item and the current query. The * outermost element must be <li>. The ".highlight" class can be ignored as it is applied automatically. * @param {!function(?*, string):void} options.onCommit * Called when an item is selected by clicking or pressing Enter. Passed the item and the current * query. If the current result list is not up to date with the query text at the time Enter is * pressed, waits until it is before running this callback. If Enter pressed with no results, passed * null. The popup remains open after this event. * @param {!function(*, string, boolean):void} options.onHighlight * Called when an item is highlighted in the list. Passed the item, the current query, and a flag that is * true if the item was highlighted explicitly (arrow keys), not simply due to a results list update. Since * the top item in the list is always initially highlighted, every time the list is updated onHighlight() * is called with the top item and with the explicit flag set to false. * @param {?number} options.maxResults * Maximum number of items from resultProvider() to display in the popup. * @param {?number} options.verticalAdjust * Number of pixels to position the popup below where $input is when constructor is called. Useful * if UI is going to animate position after construction, but QuickSearchField may receive input * before the animation is done. * @param {?number} options.firstHighlightIndex * Index of the result that is highlighted by default. null to not highlight any result. */ function QuickSearchField($input, options) { this.$input = $input; this.options = options; options.maxResults = options.maxResults || 10; this._handleInput = this._handleInput.bind(this); this._handleKeyDown = this._handleKeyDown.bind(this); if (options.highlightZeroResults !== undefined) { this._highlightZeroResults = options.highlightZeroResults; } else { this._highlightZeroResults = true; } $input.on("input", this._handleInput); $input.on("keydown", this._handleKeyDown); // For search History this value is set to null this._firstHighlightIndex = options.firstHighlightIndex; this._dropdownTop = $input.offset().top + $input.height() + (options.verticalAdjust || 0); } /** @type {!Object} */ QuickSearchField.prototype.options = null; /** @type {?$.Promise} Promise corresponding to latest resultProvider call. Any earlier promises ignored */ QuickSearchField.prototype._pending = null; /** @type {boolean} True if Enter already pressed & just waiting for results to arrive before committing */ QuickSearchField.prototype._commitPending = false; /** @type {?string} Value of $input corresponding to the _displayedResults list */ QuickSearchField.prototype._displayedQuery = null; /** @type {?Array.<*>} Latest resultProvider result */ QuickSearchField.prototype._displayedResults = null; /** @type {?number} */ QuickSearchField.prototype._highlightIndex = null; /** @type {?jQueryObject} Dropdown's <ol>, while open; null while closed */ QuickSearchField.prototype._$dropdown = null; /** @type {!jQueryObject} */ QuickSearchField.prototype.$input = null; /** When text field changes, update results list */ QuickSearchField.prototype._handleInput = function () { this._pending = null; // immediately invalidate any previous Promise var valueAtEvent = this.$input.val(); var self = this; // The timeout lets us skip over a backlog of multiple keyboard events when the provider is responding // so slowly that JS execution can't keep up. All the remaining input events are serviced before the // first timeout runs; then all the queued-up timeouts run in a row. All except the last one can no-op. setTimeout(function () { if (self.$input.val() === valueAtEvent) { self.updateResults(); } }, 0); }; /** Handle special keys: Enter, Up/Down */ QuickSearchField.prototype._handleKeyDown = function (event) { if (event.keyCode === KeyEvent.DOM_VK_RETURN) { // Enter should always act on the latest results. If input has changed and we're still waiting for // new results, just flag the 'commit' for later if (this._displayedQuery === this.$input.val()) { event.preventDefault(); // prevents keyup from going to someone else after we close this._doCommit(); } else { // Once the current wait resolves, _render() will run the commit this._commitPending = true; } } else if (event.keyCode === KeyEvent.DOM_VK_DOWN) { // Highlight changes are always done synchronously on the currently shown result list. If the list // later changes, the highlight is reset to the top if (this._displayedResults && this._displayedResults.length) { if (this._highlightIndex === null || this._highlightIndex === this._displayedResults.length - 1) { this._highlightIndex = 0; } else { this._highlightIndex++; } this._updateHighlight(true); } event.preventDefault(); // treated as Home key otherwise } else if (event.keyCode === KeyEvent.DOM_VK_UP) { if (this._displayedResults && this._displayedResults.length) { if (this._highlightIndex === null || this._highlightIndex === 0) { this._highlightIndex = this._displayedResults.length - 1; } else { this._highlightIndex--; } this._updateHighlight(true); } event.preventDefault(); // treated as End key otherwise } }; /** Call onCommit() immediately */ QuickSearchField.prototype._doCommit = function (index) { var item; if (this._displayedResults && this._displayedResults.length) { if (index >= 0) { item = this._displayedResults[index]; } else if (this._highlightIndex >= 0) { item = this._displayedResults[this._highlightIndex]; } } this.options.onCommit(item, this._displayedQuery); }; /** Update display to reflect value of _highlightIndex, & call onHighlight() */ QuickSearchField.prototype._updateHighlight = function (explicit) { if (this._$dropdown) { var $items = this._$dropdown.find("li"); $items.removeClass("highlight"); if (this._highlightIndex !== null) { $items.eq(this._highlightIndex).addClass("highlight"); this.options.onHighlight(this._displayedResults[this._highlightIndex], this.$input.val(), explicit); } } }; /** * Refresh the results dropdown, as if the user had changed the search text. Useful for providers that * want to show cached data initially, then update the results with fresher data once available. */ QuickSearchField.prototype.updateResults = function () { this._pending = null; // immediately invalidate any previous Promise var query = this.$input.val(); var results = this.options.resultProvider(query); if (results.done && results.fail) { // Provider returned an async result - mark it as the latest Promise and if it's still latest when // it resolves, render the results then this._pending = results; var self = this; this._pending.done(function (realResults) { if (self._pending === results) { self._render(realResults, query); this._pending = null; } }); if (this._pending) { this._pending.fail(function () { if (self._pending === results) { self._render([], query); this._pending = null; } }); } } else { // Synchronous result - render immediately this._render(results, query); } }; /** Close dropdown result list if visible */ QuickSearchField.prototype._closeDropdown = function () { if (this._$dropdown) { this._$dropdown.remove(); this._$dropdown = null; } }; /** * Open dropdown result list & populate with the given content * @param {!string} htmlContent */ QuickSearchField.prototype._openDropdown = function (htmlContent) { if (!this._$dropdown) { var self = this; this._$dropdown = $("<ol class='quick-search-container'/>").appendTo("body") .css({ position: "absolute", top: this._dropdownTop, left: this.$input.offset().left, width: this.$input.outerWidth() }) .click(function (event) { // Unlike the Enter key, where we wait to catch up with typing, clicking commits immediately var $item = $(event.target).closest("li"); if ($item.length) { self._doCommit($item.index()); } }); } this._$dropdown.html(htmlContent); }; /** * Given finished provider result, format it into HTML and show in dropdown, and update "no-results" style. * If an Enter key commit was pending from earlier, process it now. * @param {!Array.<*>} results * @param {!string} query */ QuickSearchField.prototype._render = function (results, query) { this._displayedQuery = query; this._displayedResults = results; if (this._firstHighlightIndex >= 0) { this._highlightIndex = this._firstHighlightIndex; } else { this._highlightIndex = null; } // TODO: fixup to match prev value's item if possible? if (results.error || results.length === 0) { this._closeDropdown(); if (this._highlightZeroResults) { this.$input.addClass("no-results"); } } else if (results.hasOwnProperty("error")) { // Error present but falsy - no results to show, but don't decorate with error style this._closeDropdown(); if (this._highlightZeroResults) { this.$input.removeClass("no-results"); } } else { if (this._highlightZeroResults) { this.$input.removeClass("no-results"); } var count = Math.min(results.length, this.options.maxResults), html = "", i; for (i = 0; i < count; i++) { html += this.options.formatter(results[i], query); } this._openDropdown(html); // Highlight top item and trigger highlight callback this._updateHighlight(false); } // If Enter key was pressed earlier, handle it now that we've gotten results back if (this._commitPending) { this._commitPending = false; this._doCommit(); } }; /** * Programmatically changes the search text and updates the results. * @param {!string} value */ QuickSearchField.prototype.setText = function (value) { this.$input.val(value); this.updateResults(); // programmatic changes don't trigger "input" event }; /** * Closes the dropdown, and discards any pending Promises. */ QuickSearchField.prototype.destroy = function () { this._pending = null; // immediately invalidate any pending Promise this._closeDropdown(); }; exports.QuickSearchField = QuickSearchField; });