diff --git a/src/TabComplete.js b/src/TabComplete.js new file mode 100644 index 0000000000..b84192e418 --- /dev/null +++ b/src/TabComplete.js @@ -0,0 +1,300 @@ +/* +Copyright 2015 OpenMarket Ltd + +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. +*/ +var Entry = require("./TabCompleteEntries").Entry; + +const DELAY_TIME_MS = 1000; +const KEY_TAB = 9; +const KEY_SHIFT = 16; +const KEY_WINDOWS = 91; + +// NB: DO NOT USE \b its "words" are roman alphabet only! +// +// Capturing group containing the start +// of line or a whitespace char +// \_______________ __________Capturing group of 1 or more non-whitespace chars +// _|__ _|_ followed by the end of line +// / \/ \ +const MATCH_REGEX = /(^|\s)(\S+)$/; + +class TabComplete { + + constructor(opts) { + opts.startingWordSuffix = opts.startingWordSuffix || ""; + opts.wordSuffix = opts.wordSuffix || ""; + opts.allowLooping = opts.allowLooping || false; + opts.autoEnterTabComplete = opts.autoEnterTabComplete || false; + opts.onClickCompletes = opts.onClickCompletes || false; + this.opts = opts; + this.completing = false; + this.list = []; // full set of tab-completable things + this.matchedList = []; // subset of completable things to loop over + this.currentIndex = 0; // index in matchedList currently + this.originalText = null; // original input text when tab was first hit + this.textArea = opts.textArea; // DOMElement + this.isFirstWord = false; // true if you tab-complete on the first word + this.enterTabCompleteTimerId = null; + this.inPassiveMode = false; + } + + /** + * @param {Entry[]} completeList + */ + setCompletionList(completeList) { + this.list = completeList; + if (this.opts.onClickCompletes) { + // assign onClick listeners for each entry to complete the text + this.list.forEach((l) => { + l.onClick = () => { + this.completeTo(l.getText()); + } + }); + } + } + + /** + * @param {DOMElement} + */ + setTextArea(textArea) { + this.textArea = textArea; + } + + /** + * @return {Boolean} + */ + isTabCompleting() { + // actually have things to tab over + return this.completing && this.matchedList.length > 1; + } + + stopTabCompleting() { + this.completing = false; + this.currentIndex = 0; + this._notifyStateChange(); + } + + startTabCompleting() { + this.completing = true; + this.currentIndex = 0; + this._calculateCompletions(); + } + + /** + * Do an auto-complete with the given word. This terminates the tab-complete. + * @param {string} someVal + */ + completeTo(someVal) { + this.textArea.value = this._replaceWith(someVal, true); + this.stopTabCompleting(); + // keep focus on the text area + this.textArea.focus(); + } + + /** + * @param {Number} numAheadToPeek Return *up to* this many elements. + * @return {Entry[]} + */ + peek(numAheadToPeek) { + if (this.matchedList.length === 0) { + return []; + } + var peekList = []; + + // return the current match item and then one with an index higher, and + // so on until we've reached the requested limit. If we hit the end of + // the list of options we're done. + for (var i = 0; i < numAheadToPeek; i++) { + var nextIndex; + if (this.opts.allowLooping) { + nextIndex = (this.currentIndex + i) % this.matchedList.length; + } + else { + nextIndex = this.currentIndex + i; + if (nextIndex === this.matchedList.length) { + break; + } + } + peekList.push(this.matchedList[nextIndex]); + } + // console.log("Peek list(%s): %s", numAheadToPeek, JSON.stringify(peekList)); + return peekList; + } + + handleTabPress(passive, shiftKey) { + var wasInPassiveMode = this.inPassiveMode && !passive; + this.inPassiveMode = passive; + + if (!this.completing) { + this.startTabCompleting(); + } + + if (shiftKey) { + this.nextMatchedEntry(-1); + } + else { + // if we were in passive mode we got out of sync by incrementing the + // index to show the peek view but not set the text area. Therefore, + // we want to set the *current* index rather than the *next* index. + this.nextMatchedEntry(wasInPassiveMode ? 0 : 1); + } + this._notifyStateChange(); + } + + /** + * @param {DOMEvent} e + */ + onKeyDown(ev) { + if (!this.textArea) { + console.error("onKeyDown called before a