Index: third_party/WebKit/Source/devtools/front_end/ui/TextPrompt.js |
diff --git a/third_party/WebKit/Source/devtools/front_end/ui/TextPrompt.js b/third_party/WebKit/Source/devtools/front_end/ui/TextPrompt.js |
index 5bccf6fa1966d3b9bcf28da25e82a5408e03df8a..4866d067f770a1ad358e81b5c6a4a224ab8d1a97 100644 |
--- a/third_party/WebKit/Source/devtools/front_end/ui/TextPrompt.js |
+++ b/third_party/WebKit/Source/devtools/front_end/ui/TextPrompt.js |
@@ -26,645 +26,620 @@ |
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
*/ |
- |
/** |
- * @constructor |
- * @extends {WebInspector.Object} |
* @implements {WebInspector.SuggestBoxDelegate} |
+ * @unrestricted |
*/ |
-WebInspector.TextPrompt = function() |
-{ |
+WebInspector.TextPrompt = class extends WebInspector.Object { |
+ constructor() { |
+ super(); |
/** |
* @type {!Element|undefined} |
*/ |
this._proxyElement; |
- this._proxyElementDisplay = "inline-block"; |
+ this._proxyElementDisplay = 'inline-block'; |
this._autocompletionTimeout = WebInspector.TextPrompt.DefaultAutocompletionTimeout; |
- this._title = ""; |
+ this._title = ''; |
this._prefixRange = null; |
- this._previousText = ""; |
- this._currentSuggestion = ""; |
+ this._previousText = ''; |
+ this._currentSuggestion = ''; |
this._completionRequestId = 0; |
- this._ghostTextElement = createElementWithClass("span", "auto-complete-text"); |
+ this._ghostTextElement = createElementWithClass('span', 'auto-complete-text'); |
+ } |
+ |
+ /** |
+ * @param {function(!Element, !Range, boolean, function(!Array.<string>, number=))} completions |
+ * @param {string=} stopCharacters |
+ */ |
+ initialize(completions, stopCharacters) { |
+ this._loadCompletions = completions; |
+ this._completionStopCharacters = stopCharacters || ' =:[({;,!+-*/&|^<>.'; |
+ } |
+ |
+ /** |
+ * @param {number} timeout |
+ */ |
+ setAutocompletionTimeout(timeout) { |
+ this._autocompletionTimeout = timeout; |
+ } |
+ |
+ /** |
+ * @param {boolean} suggestBoxEnabled |
+ */ |
+ setSuggestBoxEnabled(suggestBoxEnabled) { |
+ this._suggestBoxEnabled = suggestBoxEnabled; |
+ } |
+ |
+ renderAsBlock() { |
+ this._proxyElementDisplay = 'block'; |
+ } |
+ |
+ /** |
+ * Clients should never attach any event listeners to the |element|. Instead, |
+ * they should use the result of this method to attach listeners for bubbling events. |
+ * |
+ * @param {!Element} element |
+ * @return {!Element} |
+ */ |
+ attach(element) { |
+ return this._attachInternal(element); |
+ } |
+ |
+ /** |
+ * Clients should never attach any event listeners to the |element|. Instead, |
+ * they should use the result of this method to attach listeners for bubbling events |
+ * or the |blurListener| parameter to register a "blur" event listener on the |element| |
+ * (since the "blur" event does not bubble.) |
+ * |
+ * @param {!Element} element |
+ * @param {function(!Event)} blurListener |
+ * @return {!Element} |
+ */ |
+ attachAndStartEditing(element, blurListener) { |
+ var proxyElement = this._attachInternal(element); |
+ this._startEditing(blurListener); |
+ return proxyElement; |
+ } |
+ |
+ /** |
+ * @param {!Element} element |
+ * @return {!Element} |
+ */ |
+ _attachInternal(element) { |
+ if (this._proxyElement) |
+ throw 'Cannot attach an attached TextPrompt'; |
+ this._element = element; |
+ |
+ this._boundOnKeyDown = this.onKeyDown.bind(this); |
+ this._boundOnInput = this.onInput.bind(this); |
+ this._boundOnMouseWheel = this.onMouseWheel.bind(this); |
+ this._boundClearAutocomplete = this.clearAutocomplete.bind(this); |
+ this._proxyElement = element.ownerDocument.createElement('span'); |
+ var shadowRoot = WebInspector.createShadowRootWithCoreStyles(this._proxyElement, 'ui/textPrompt.css'); |
+ this._contentElement = shadowRoot.createChild('div'); |
+ this._contentElement.createChild('content'); |
+ this._proxyElement.style.display = this._proxyElementDisplay; |
+ element.parentElement.insertBefore(this._proxyElement, element); |
+ this._proxyElement.appendChild(element); |
+ this._element.classList.add('text-prompt'); |
+ this._element.addEventListener('keydown', this._boundOnKeyDown, false); |
+ this._element.addEventListener('input', this._boundOnInput, false); |
+ this._element.addEventListener('mousewheel', this._boundOnMouseWheel, false); |
+ this._element.addEventListener('selectstart', this._boundClearAutocomplete, false); |
+ this._element.addEventListener('blur', this._boundClearAutocomplete, false); |
+ this._element.ownerDocument.defaultView.addEventListener('resize', this._boundClearAutocomplete, false); |
+ |
+ if (this._suggestBoxEnabled) |
+ this._suggestBox = new WebInspector.SuggestBox(this, 20, true); |
+ |
+ if (this._title) |
+ this._proxyElement.title = this._title; |
+ |
+ return this._proxyElement; |
+ } |
+ |
+ detach() { |
+ this._removeFromElement(); |
+ this._proxyElement.parentElement.insertBefore(this._element, this._proxyElement); |
+ this._proxyElement.remove(); |
+ delete this._proxyElement; |
+ this._element.classList.remove('text-prompt'); |
+ this._focusRestorer.restore(); |
+ } |
+ |
+ /** |
+ * @return {string} |
+ */ |
+ textWithCurrentSuggestion() { |
+ return this._element.textContent; |
+ } |
+ |
+ /** |
+ * @return {string} |
+ */ |
+ text() { |
+ var text = this.textWithCurrentSuggestion(); |
+ if (this._ghostTextElement.parentNode) { |
+ var addition = this._ghostTextElement.textContent; |
+ text = text.substring(0, text.length - addition.length); |
+ } |
+ return text; |
+ } |
+ |
+ /** |
+ * @param {string} x |
+ */ |
+ setText(x) { |
+ this.clearAutocomplete(); |
+ if (!x) { |
+ // Append a break element instead of setting textContent to make sure the selection is inside the prompt. |
+ this._element.removeChildren(); |
+ this._element.createChild('br'); |
+ } else { |
+ this._element.textContent = x; |
+ } |
+ this._previousText = this.text(); |
+ |
+ this.moveCaretToEndOfPrompt(); |
+ this._element.scrollIntoView(); |
+ } |
+ |
+ /** |
+ * @return {string} |
+ */ |
+ title() { |
+ return this._title; |
+ } |
+ |
+ /** |
+ * @param {string} title |
+ */ |
+ setTitle(title) { |
+ this._title = title; |
+ if (this._proxyElement) |
+ this._proxyElement.title = title; |
+ } |
+ |
+ _removeFromElement() { |
+ this.clearAutocomplete(); |
+ this._element.removeEventListener('keydown', this._boundOnKeyDown, false); |
+ this._element.removeEventListener('input', this._boundOnInput, false); |
+ this._element.removeEventListener('selectstart', this._boundClearAutocomplete, false); |
+ this._element.removeEventListener('blur', this._boundClearAutocomplete, false); |
+ this._element.ownerDocument.defaultView.removeEventListener('resize', this._boundClearAutocomplete, false); |
+ if (this._isEditing) |
+ this._stopEditing(); |
+ if (this._suggestBox) |
+ this._suggestBox.removeFromElement(); |
+ } |
+ |
+ /** |
+ * @param {function(!Event)=} blurListener |
+ */ |
+ _startEditing(blurListener) { |
+ this._isEditing = true; |
+ this._contentElement.classList.add('text-prompt-editing'); |
+ if (blurListener) { |
+ this._blurListener = blurListener; |
+ this._element.addEventListener('blur', this._blurListener, false); |
+ } |
+ this._oldTabIndex = this._element.tabIndex; |
+ if (this._element.tabIndex < 0) |
+ this._element.tabIndex = 0; |
+ this._focusRestorer = new WebInspector.ElementFocusRestorer(this._element); |
+ if (!this.text()) |
+ this.autoCompleteSoon(); |
+ } |
+ |
+ _stopEditing() { |
+ this._element.tabIndex = this._oldTabIndex; |
+ if (this._blurListener) |
+ this._element.removeEventListener('blur', this._blurListener, false); |
+ this._contentElement.classList.remove('text-prompt-editing'); |
+ delete this._isEditing; |
+ } |
+ |
+ /** |
+ * @param {!Event} event |
+ */ |
+ onMouseWheel(event) { |
+ // Subclasses can implement. |
+ } |
+ |
+ /** |
+ * @param {!Event} event |
+ */ |
+ onKeyDown(event) { |
+ var handled = false; |
+ |
+ switch (event.key) { |
+ case 'Tab': |
+ handled = this.tabKeyPressed(event); |
+ break; |
+ case 'ArrowLeft': |
+ case 'Home': |
+ this.clearAutocomplete(); |
+ break; |
+ case 'ArrowRight': |
+ case 'End': |
+ if (this._isCaretAtEndOfPrompt()) |
+ handled = this.acceptAutoComplete(); |
+ else |
+ this.clearAutocomplete(); |
+ break; |
+ case 'Escape': |
+ if (this._isSuggestBoxVisible()) { |
+ this.clearAutocomplete(); |
+ handled = true; |
+ } |
+ break; |
+ case ' ': // Space |
+ if (event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) { |
+ this.autoCompleteSoon(true); |
+ handled = true; |
+ } |
+ break; |
+ case 'Alt': |
+ case 'Meta': |
+ case 'Shift': |
+ case 'Control': |
+ break; |
+ } |
+ |
+ if (!handled && this._isSuggestBoxVisible()) |
+ handled = this._suggestBox.keyPressed(event); |
+ |
+ if (handled) |
+ event.consume(true); |
+ } |
+ |
+ /** |
+ * @param {!Event} event |
+ */ |
+ onInput(event) { |
+ var text = this.text(); |
+ var hasCommonPrefix = text.startsWith(this._previousText) || this._previousText.startsWith(text); |
+ if (this._prefixRange && hasCommonPrefix) |
+ this._prefixRange.endColumn += text.length - this._previousText.length; |
+ this._refreshGhostText(); |
+ this._previousText = text; |
+ |
+ this.autoCompleteSoon(); |
+ } |
+ |
+ /** |
+ * @return {boolean} |
+ */ |
+ acceptAutoComplete() { |
+ var result = false; |
+ if (this._isSuggestBoxVisible()) |
+ result = this._suggestBox.acceptSuggestion(); |
+ if (!result) |
+ result = this._acceptSuggestionInternal(); |
+ |
+ return result; |
+ } |
+ |
+ clearAutocomplete() { |
+ if (this._isSuggestBoxVisible()) |
+ this._suggestBox.hide(); |
+ this._clearAutocompleteTimeout(); |
+ this._prefixRange = null; |
+ this._refreshGhostText(); |
+ } |
+ |
+ _refreshGhostText() { |
+ if (this._prefixRange && this._isCaretAtEndOfPrompt()) { |
+ this._ghostTextElement.textContent = |
+ this._currentSuggestion.substring(this._prefixRange.endColumn - this._prefixRange.startColumn); |
+ this._element.appendChild(this._ghostTextElement); |
+ } else { |
+ this._ghostTextElement.remove(); |
+ } |
+ } |
+ |
+ _clearAutocompleteTimeout() { |
+ if (this._completeTimeout) { |
+ clearTimeout(this._completeTimeout); |
+ delete this._completeTimeout; |
+ } |
+ this._completionRequestId++; |
+ } |
+ |
+ /** |
+ * @param {boolean=} force |
+ */ |
+ autoCompleteSoon(force) { |
+ var immediately = this._isSuggestBoxVisible() || force; |
+ if (!this._completeTimeout) |
+ this._completeTimeout = |
+ setTimeout(this.complete.bind(this, force), immediately ? 0 : this._autocompletionTimeout); |
+ } |
+ |
+ /** |
+ * @param {boolean=} force |
+ * @param {boolean=} reverse |
+ */ |
+ complete(force, reverse) { |
+ this._clearAutocompleteTimeout(); |
+ var selection = this._element.getComponentSelection(); |
+ var selectionRange = selection && selection.rangeCount ? selection.getRangeAt(0) : null; |
+ if (!selectionRange) |
+ return; |
+ |
+ var shouldExit; |
+ |
+ if (!force && !this._isCaretAtEndOfPrompt() && !this._isSuggestBoxVisible()) |
+ shouldExit = true; |
+ else if (!selection.isCollapsed) |
+ shouldExit = true; |
+ else if (!force) { |
+ // BUG72018: Do not show suggest box if caret is followed by a non-stop character. |
+ var wordSuffixRange = selectionRange.startContainer.rangeOfWord( |
+ selectionRange.endOffset, this._completionStopCharacters, this._element, 'forward'); |
+ var autocompleteTextLength = this._ghostTextElement.parentNode ? this._ghostTextElement.textContent.length : 0; |
+ if (wordSuffixRange.toString().length !== autocompleteTextLength) |
+ shouldExit = true; |
+ } |
+ if (shouldExit) { |
+ this.clearAutocomplete(); |
+ return; |
+ } |
+ |
+ var wordPrefixRange = selectionRange.startContainer.rangeOfWord( |
+ selectionRange.startOffset, this._completionStopCharacters, this._element, 'backward'); |
+ this._loadCompletions( |
+ /** @type {!Element} */ (this._proxyElement), wordPrefixRange, force || false, |
+ this._completionsReady.bind(this, ++this._completionRequestId, selection, wordPrefixRange, !!reverse, !!force)); |
+ } |
+ |
+ disableDefaultSuggestionForEmptyInput() { |
+ this._disableDefaultSuggestionForEmptyInput = true; |
+ } |
+ |
+ /** |
+ * @param {!Selection} selection |
+ * @param {!Range} textRange |
+ */ |
+ _boxForAnchorAtStart(selection, textRange) { |
+ var rangeCopy = selection.getRangeAt(0).cloneRange(); |
+ var anchorElement = createElement('span'); |
+ anchorElement.textContent = '\u200B'; |
+ textRange.insertNode(anchorElement); |
+ var box = anchorElement.boxInWindow(window); |
+ anchorElement.remove(); |
+ selection.removeAllRanges(); |
+ selection.addRange(rangeCopy); |
+ return box; |
+ } |
+ |
+ /** |
+ * @return {?Range} |
+ * @suppressGlobalPropertiesCheck |
+ */ |
+ _createRange() { |
+ return document.createRange(); |
+ } |
+ |
+ /** |
+ * @param {string} prefix |
+ * @return {!WebInspector.SuggestBox.Suggestions} |
+ */ |
+ additionalCompletions(prefix) { |
+ return []; |
+ } |
+ |
+ /** |
+ * @param {number} completionRequestId |
+ * @param {!Selection} selection |
+ * @param {!Range} originalWordPrefixRange |
+ * @param {boolean} reverse |
+ * @param {boolean} force |
+ * @param {!Array.<string>} completions |
+ * @param {number=} selectedIndex |
+ */ |
+ _completionsReady( |
+ completionRequestId, |
+ selection, |
+ originalWordPrefixRange, |
+ reverse, |
+ force, |
+ completions, |
+ selectedIndex) { |
+ if (this._completionRequestId !== completionRequestId) |
+ return; |
+ |
+ var prefix = originalWordPrefixRange.toString(); |
+ |
+ // Filter out dupes. |
+ var store = new Set(); |
+ completions = completions.filter(item => !store.has(item) && !!store.add(item)); |
+ var annotatedCompletions = completions.map(item => ({title: item})); |
+ |
+ if (prefix || force) { |
+ if (prefix) |
+ annotatedCompletions = annotatedCompletions.concat(this.additionalCompletions(prefix)); |
+ else |
+ annotatedCompletions = this.additionalCompletions(prefix).concat(annotatedCompletions); |
+ } |
+ |
+ if (!annotatedCompletions.length) { |
+ this.clearAutocomplete(); |
+ return; |
+ } |
+ |
+ var selectionRange = selection.getRangeAt(0); |
+ |
+ var fullWordRange = this._createRange(); |
+ fullWordRange.setStart(originalWordPrefixRange.startContainer, originalWordPrefixRange.startOffset); |
+ fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset); |
+ |
+ if (prefix + selectionRange.toString() !== fullWordRange.toString()) |
+ return; |
+ |
+ selectedIndex = (this._disableDefaultSuggestionForEmptyInput && !this.text()) ? -1 : (selectedIndex || 0); |
+ |
+ if (this._suggestBox) |
+ this._suggestBox.updateSuggestions( |
+ this._boxForAnchorAtStart(selection, fullWordRange), annotatedCompletions, selectedIndex, |
+ !this._isCaretAtEndOfPrompt(), this.text()); |
+ |
+ var beforeRange = this._createRange(); |
+ beforeRange.setStart(this._element, 0); |
+ beforeRange.setEnd(fullWordRange.startContainer, fullWordRange.startOffset); |
+ this._prefixRange = new WebInspector.TextRange( |
+ 0, beforeRange.toString().length, 0, beforeRange.toString().length + fullWordRange.toString().length); |
+ |
+ if (selectedIndex === -1) |
+ return; |
+ this.applySuggestion(annotatedCompletions[selectedIndex].title, true); |
+ } |
+ |
+ /** |
+ * @override |
+ * @param {string} suggestion |
+ * @param {boolean=} isIntermediateSuggestion |
+ */ |
+ applySuggestion(suggestion, isIntermediateSuggestion) { |
+ if (!this._prefixRange) |
+ return; |
+ this._currentSuggestion = suggestion; |
+ this._refreshGhostText(); |
+ if (isIntermediateSuggestion) |
+ this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApplied); |
+ } |
+ |
+ /** |
+ * @override |
+ */ |
+ acceptSuggestion() { |
+ this._acceptSuggestionInternal(); |
+ } |
+ |
+ /** |
+ * @return {boolean} |
+ */ |
+ _acceptSuggestionInternal() { |
+ if (!this._prefixRange) |
+ return false; |
+ |
+ var text = this.text(); |
+ this._element.textContent = text.substring(0, this._prefixRange.startColumn) + this._currentSuggestion + |
+ text.substring(this._prefixRange.endColumn); |
+ this._setDOMSelection( |
+ this._prefixRange.startColumn + this._currentSuggestion.length, |
+ this._prefixRange.startColumn + this._currentSuggestion.length); |
+ |
+ this.clearAutocomplete(); |
+ this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemAccepted); |
+ |
+ return true; |
+ } |
+ |
+ /** |
+ * @param {number} startColumn |
+ * @param {number} endColumn |
+ */ |
+ _setDOMSelection(startColumn, endColumn) { |
+ this._element.normalize(); |
+ var node = this._element.childNodes[0]; |
+ if (!node || node === this._ghostTextElement) |
+ return; |
+ var range = this._createRange(); |
+ range.setStart(node, startColumn); |
+ range.setEnd(node, endColumn); |
+ var selection = this._element.getComponentSelection(); |
+ selection.removeAllRanges(); |
+ selection.addRange(range); |
+ } |
+ |
+ /** |
+ * @return {boolean} |
+ */ |
+ _isSuggestBoxVisible() { |
+ return this._suggestBox && this._suggestBox.visible(); |
+ } |
+ |
+ /** |
+ * @return {boolean} |
+ */ |
+ isCaretInsidePrompt() { |
+ var selection = this._element.getComponentSelection(); |
+ // @see crbug.com/602541 |
+ var selectionRange = selection && selection.rangeCount ? selection.getRangeAt(0) : null; |
+ if (!selectionRange || !selection.isCollapsed) |
+ return false; |
+ return selectionRange.startContainer.isSelfOrDescendant(this._element); |
+ } |
+ |
+ /** |
+ * @return {boolean} |
+ */ |
+ _isCaretAtEndOfPrompt() { |
+ var selection = this._element.getComponentSelection(); |
+ var selectionRange = selection && selection.rangeCount ? selection.getRangeAt(0) : null; |
+ if (!selectionRange || !selection.isCollapsed) |
+ return false; |
+ |
+ var node = selectionRange.startContainer; |
+ if (!node.isSelfOrDescendant(this._element)) |
+ return false; |
+ |
+ if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length) |
+ return false; |
+ |
+ var foundNextText = false; |
+ while (node) { |
+ if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) { |
+ if (foundNextText && !this._ghostTextElement.isAncestor(node)) |
+ return false; |
+ foundNextText = true; |
+ } |
+ |
+ node = node.traverseNextNode(this._element); |
+ } |
+ |
+ return true; |
+ } |
+ |
+ moveCaretToEndOfPrompt() { |
+ var selection = this._element.getComponentSelection(); |
+ var selectionRange = this._createRange(); |
+ |
+ var container = this._element; |
+ while (container.childNodes.length) |
+ container = container.lastChild; |
+ var offset = container.nodeType === Node.TEXT_NODE ? container.textContent.length : 0; |
+ selectionRange.setStart(container, offset); |
+ selectionRange.setEnd(container, offset); |
+ |
+ selection.removeAllRanges(); |
+ selection.addRange(selectionRange); |
+ } |
+ |
+ /** |
+ * @param {!Event} event |
+ * @return {boolean} |
+ */ |
+ tabKeyPressed(event) { |
+ this.acceptAutoComplete(); |
+ |
+ // Consume the key. |
+ return true; |
+ } |
+ |
+ /** |
+ * @return {?Element} |
+ */ |
+ proxyElementForTests() { |
+ return this._proxyElement || null; |
+ } |
}; |
WebInspector.TextPrompt.DefaultAutocompletionTimeout = 250; |
/** @enum {symbol} */ |
WebInspector.TextPrompt.Events = { |
- ItemApplied: Symbol("text-prompt-item-applied"), |
- ItemAccepted: Symbol("text-prompt-item-accepted") |
-}; |
- |
-WebInspector.TextPrompt.prototype = { |
- /** |
- * @param {function(!Element, !Range, boolean, function(!Array.<string>, number=))} completions |
- * @param {string=} stopCharacters |
- */ |
- initialize: function(completions, stopCharacters) |
- { |
- this._loadCompletions = completions; |
- this._completionStopCharacters = stopCharacters || " =:[({;,!+-*/&|^<>."; |
- }, |
- |
- /** |
- * @param {number} timeout |
- */ |
- setAutocompletionTimeout: function(timeout) |
- { |
- this._autocompletionTimeout = timeout; |
- }, |
- |
- /** |
- * @param {boolean} suggestBoxEnabled |
- */ |
- setSuggestBoxEnabled: function(suggestBoxEnabled) |
- { |
- this._suggestBoxEnabled = suggestBoxEnabled; |
- }, |
- |
- renderAsBlock: function() |
- { |
- this._proxyElementDisplay = "block"; |
- }, |
- |
- /** |
- * Clients should never attach any event listeners to the |element|. Instead, |
- * they should use the result of this method to attach listeners for bubbling events. |
- * |
- * @param {!Element} element |
- * @return {!Element} |
- */ |
- attach: function(element) |
- { |
- return this._attachInternal(element); |
- }, |
- |
- /** |
- * Clients should never attach any event listeners to the |element|. Instead, |
- * they should use the result of this method to attach listeners for bubbling events |
- * or the |blurListener| parameter to register a "blur" event listener on the |element| |
- * (since the "blur" event does not bubble.) |
- * |
- * @param {!Element} element |
- * @param {function(!Event)} blurListener |
- * @return {!Element} |
- */ |
- attachAndStartEditing: function(element, blurListener) |
- { |
- var proxyElement = this._attachInternal(element); |
- this._startEditing(blurListener); |
- return proxyElement; |
- }, |
- |
- /** |
- * @param {!Element} element |
- * @return {!Element} |
- */ |
- _attachInternal: function(element) |
- { |
- if (this._proxyElement) |
- throw "Cannot attach an attached TextPrompt"; |
- this._element = element; |
- |
- this._boundOnKeyDown = this.onKeyDown.bind(this); |
- this._boundOnInput = this.onInput.bind(this); |
- this._boundOnMouseWheel = this.onMouseWheel.bind(this); |
- this._boundClearAutocomplete = this.clearAutocomplete.bind(this); |
- this._proxyElement = element.ownerDocument.createElement("span"); |
- var shadowRoot = WebInspector.createShadowRootWithCoreStyles(this._proxyElement, "ui/textPrompt.css"); |
- this._contentElement = shadowRoot.createChild("div"); |
- this._contentElement.createChild("content"); |
- this._proxyElement.style.display = this._proxyElementDisplay; |
- element.parentElement.insertBefore(this._proxyElement, element); |
- this._proxyElement.appendChild(element); |
- this._element.classList.add("text-prompt"); |
- this._element.addEventListener("keydown", this._boundOnKeyDown, false); |
- this._element.addEventListener("input", this._boundOnInput, false); |
- this._element.addEventListener("mousewheel", this._boundOnMouseWheel, false); |
- this._element.addEventListener("selectstart", this._boundClearAutocomplete, false); |
- this._element.addEventListener("blur", this._boundClearAutocomplete, false); |
- this._element.ownerDocument.defaultView.addEventListener("resize", this._boundClearAutocomplete, false); |
- |
- if (this._suggestBoxEnabled) |
- this._suggestBox = new WebInspector.SuggestBox(this, 20, true); |
- |
- if (this._title) |
- this._proxyElement.title = this._title; |
- |
- return this._proxyElement; |
- }, |
- |
- detach: function() |
- { |
- this._removeFromElement(); |
- this._proxyElement.parentElement.insertBefore(this._element, this._proxyElement); |
- this._proxyElement.remove(); |
- delete this._proxyElement; |
- this._element.classList.remove("text-prompt"); |
- this._focusRestorer.restore(); |
- }, |
- |
- /** |
- * @return {string} |
- */ |
- textWithCurrentSuggestion: function() |
- { |
- return this._element.textContent; |
- }, |
- |
- /** |
- * @return {string} |
- */ |
- text: function() |
- { |
- var text = this.textWithCurrentSuggestion(); |
- if (this._ghostTextElement.parentNode) { |
- var addition = this._ghostTextElement.textContent; |
- text = text.substring(0, text.length - addition.length); |
- } |
- return text; |
- }, |
- |
- /** |
- * @param {string} x |
- */ |
- setText: function(x) |
- { |
- this.clearAutocomplete(); |
- if (!x) { |
- // Append a break element instead of setting textContent to make sure the selection is inside the prompt. |
- this._element.removeChildren(); |
- this._element.createChild("br"); |
- } else { |
- this._element.textContent = x; |
- } |
- this._previousText = this.text(); |
- |
- this.moveCaretToEndOfPrompt(); |
- this._element.scrollIntoView(); |
- }, |
- |
- /** |
- * @return {string} |
- */ |
- title: function() |
- { |
- return this._title; |
- }, |
- |
- /** |
- * @param {string} title |
- */ |
- setTitle: function(title) |
- { |
- this._title = title; |
- if (this._proxyElement) |
- this._proxyElement.title = title; |
- }, |
- |
- _removeFromElement: function() |
- { |
- this.clearAutocomplete(); |
- this._element.removeEventListener("keydown", this._boundOnKeyDown, false); |
- this._element.removeEventListener("input", this._boundOnInput, false); |
- this._element.removeEventListener("selectstart", this._boundClearAutocomplete, false); |
- this._element.removeEventListener("blur", this._boundClearAutocomplete, false); |
- this._element.ownerDocument.defaultView.removeEventListener("resize", this._boundClearAutocomplete, false); |
- if (this._isEditing) |
- this._stopEditing(); |
- if (this._suggestBox) |
- this._suggestBox.removeFromElement(); |
- }, |
- |
- /** |
- * @param {function(!Event)=} blurListener |
- */ |
- _startEditing: function(blurListener) |
- { |
- this._isEditing = true; |
- this._contentElement.classList.add("text-prompt-editing"); |
- if (blurListener) { |
- this._blurListener = blurListener; |
- this._element.addEventListener("blur", this._blurListener, false); |
- } |
- this._oldTabIndex = this._element.tabIndex; |
- if (this._element.tabIndex < 0) |
- this._element.tabIndex = 0; |
- this._focusRestorer = new WebInspector.ElementFocusRestorer(this._element); |
- if (!this.text()) |
- this.autoCompleteSoon(); |
- }, |
- |
- _stopEditing: function() |
- { |
- this._element.tabIndex = this._oldTabIndex; |
- if (this._blurListener) |
- this._element.removeEventListener("blur", this._blurListener, false); |
- this._contentElement.classList.remove("text-prompt-editing"); |
- delete this._isEditing; |
- }, |
- |
- /** |
- * @param {!Event} event |
- */ |
- onMouseWheel: function(event) |
- { |
- // Subclasses can implement. |
- }, |
- |
- /** |
- * @param {!Event} event |
- */ |
- onKeyDown: function(event) |
- { |
- var handled = false; |
- |
- switch (event.key) { |
- case "Tab": |
- handled = this.tabKeyPressed(event); |
- break; |
- case "ArrowLeft": |
- case "Home": |
- this.clearAutocomplete(); |
- break; |
- case "ArrowRight": |
- case "End": |
- if (this._isCaretAtEndOfPrompt()) |
- handled = this.acceptAutoComplete(); |
- else |
- this.clearAutocomplete(); |
- break; |
- case "Escape": |
- if (this._isSuggestBoxVisible()) { |
- this.clearAutocomplete(); |
- handled = true; |
- } |
- break; |
- case " ": // Space |
- if (event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) { |
- this.autoCompleteSoon(true); |
- handled = true; |
- } |
- break; |
- case "Alt": |
- case "Meta": |
- case "Shift": |
- case "Control": |
- break; |
- } |
- |
- if (!handled && this._isSuggestBoxVisible()) |
- handled = this._suggestBox.keyPressed(event); |
- |
- if (handled) |
- event.consume(true); |
- }, |
- |
- /** |
- * @param {!Event} event |
- */ |
- onInput: function(event) |
- { |
- var text = this.text(); |
- var hasCommonPrefix = text.startsWith(this._previousText) || this._previousText.startsWith(text); |
- if (this._prefixRange && hasCommonPrefix) |
- this._prefixRange.endColumn += text.length - this._previousText.length; |
- this._refreshGhostText(); |
- this._previousText = text; |
- |
- this.autoCompleteSoon(); |
- }, |
- |
- /** |
- * @return {boolean} |
- */ |
- acceptAutoComplete: function() |
- { |
- var result = false; |
- if (this._isSuggestBoxVisible()) |
- result = this._suggestBox.acceptSuggestion(); |
- if (!result) |
- result = this._acceptSuggestionInternal(); |
- |
- return result; |
- }, |
- |
- clearAutocomplete: function() |
- { |
- if (this._isSuggestBoxVisible()) |
- this._suggestBox.hide(); |
- this._clearAutocompleteTimeout(); |
- this._prefixRange = null; |
- this._refreshGhostText(); |
- }, |
- |
- _refreshGhostText: function() |
- { |
- if (this._prefixRange && this._isCaretAtEndOfPrompt()) { |
- this._ghostTextElement.textContent = this._currentSuggestion.substring(this._prefixRange.endColumn - this._prefixRange.startColumn); |
- this._element.appendChild(this._ghostTextElement); |
- } else { |
- this._ghostTextElement.remove(); |
- } |
- }, |
- |
- _clearAutocompleteTimeout: function() |
- { |
- if (this._completeTimeout) { |
- clearTimeout(this._completeTimeout); |
- delete this._completeTimeout; |
- } |
- this._completionRequestId++; |
- }, |
- |
- /** |
- * @param {boolean=} force |
- */ |
- autoCompleteSoon: function(force) |
- { |
- var immediately = this._isSuggestBoxVisible() || force; |
- if (!this._completeTimeout) |
- this._completeTimeout = setTimeout(this.complete.bind(this, force), immediately ? 0 : this._autocompletionTimeout); |
- }, |
- |
- /** |
- * @param {boolean=} force |
- * @param {boolean=} reverse |
- */ |
- complete: function(force, reverse) |
- { |
- this._clearAutocompleteTimeout(); |
- var selection = this._element.getComponentSelection(); |
- var selectionRange = selection && selection.rangeCount ? selection.getRangeAt(0) : null; |
- if (!selectionRange) |
- return; |
- |
- var shouldExit; |
- |
- if (!force && !this._isCaretAtEndOfPrompt() && !this._isSuggestBoxVisible()) |
- shouldExit = true; |
- else if (!selection.isCollapsed) |
- shouldExit = true; |
- else if (!force) { |
- // BUG72018: Do not show suggest box if caret is followed by a non-stop character. |
- var wordSuffixRange = selectionRange.startContainer.rangeOfWord(selectionRange.endOffset, this._completionStopCharacters, this._element, "forward"); |
- var autocompleteTextLength = this._ghostTextElement.parentNode ? this._ghostTextElement.textContent.length : 0; |
- if (wordSuffixRange.toString().length !== autocompleteTextLength) |
- shouldExit = true; |
- } |
- if (shouldExit) { |
- this.clearAutocomplete(); |
- return; |
- } |
- |
- var wordPrefixRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, this._completionStopCharacters, this._element, "backward"); |
- this._loadCompletions(/** @type {!Element} */ (this._proxyElement), wordPrefixRange, force || false, this._completionsReady.bind(this, ++this._completionRequestId, selection, wordPrefixRange, !!reverse, !!force)); |
- }, |
- |
- disableDefaultSuggestionForEmptyInput: function() |
- { |
- this._disableDefaultSuggestionForEmptyInput = true; |
- }, |
- |
- /** |
- * @param {!Selection} selection |
- * @param {!Range} textRange |
- */ |
- _boxForAnchorAtStart: function(selection, textRange) |
- { |
- var rangeCopy = selection.getRangeAt(0).cloneRange(); |
- var anchorElement = createElement("span"); |
- anchorElement.textContent = "\u200B"; |
- textRange.insertNode(anchorElement); |
- var box = anchorElement.boxInWindow(window); |
- anchorElement.remove(); |
- selection.removeAllRanges(); |
- selection.addRange(rangeCopy); |
- return box; |
- }, |
- |
- /** |
- * @return {?Range} |
- * @suppressGlobalPropertiesCheck |
- */ |
- _createRange: function() |
- { |
- return document.createRange(); |
- }, |
- |
- /** |
- * @param {string} prefix |
- * @return {!WebInspector.SuggestBox.Suggestions} |
- */ |
- additionalCompletions: function(prefix) |
- { |
- return []; |
- }, |
- |
- /** |
- * @param {number} completionRequestId |
- * @param {!Selection} selection |
- * @param {!Range} originalWordPrefixRange |
- * @param {boolean} reverse |
- * @param {boolean} force |
- * @param {!Array.<string>} completions |
- * @param {number=} selectedIndex |
- */ |
- _completionsReady: function(completionRequestId, selection, originalWordPrefixRange, reverse, force, completions, selectedIndex) |
- { |
- if (this._completionRequestId !== completionRequestId) |
- return; |
- |
- var prefix = originalWordPrefixRange.toString(); |
- |
- // Filter out dupes. |
- var store = new Set(); |
- completions = completions.filter(item => !store.has(item) && !!store.add(item)); |
- var annotatedCompletions = completions.map(item => ({title: item})); |
- |
- if (prefix || force) { |
- if (prefix) |
- annotatedCompletions = annotatedCompletions.concat(this.additionalCompletions(prefix)); |
- else |
- annotatedCompletions = this.additionalCompletions(prefix).concat(annotatedCompletions); |
- } |
- |
- if (!annotatedCompletions.length) { |
- this.clearAutocomplete(); |
- return; |
- } |
- |
- var selectionRange = selection.getRangeAt(0); |
- |
- var fullWordRange = this._createRange(); |
- fullWordRange.setStart(originalWordPrefixRange.startContainer, originalWordPrefixRange.startOffset); |
- fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset); |
- |
- if (prefix + selectionRange.toString() !== fullWordRange.toString()) |
- return; |
- |
- selectedIndex = (this._disableDefaultSuggestionForEmptyInput && !this.text()) ? -1 : (selectedIndex || 0); |
- |
- if (this._suggestBox) |
- this._suggestBox.updateSuggestions(this._boxForAnchorAtStart(selection, fullWordRange), annotatedCompletions, selectedIndex, !this._isCaretAtEndOfPrompt(), this.text()); |
- |
- var beforeRange = this._createRange(); |
- beforeRange.setStart(this._element, 0); |
- beforeRange.setEnd(fullWordRange.startContainer, fullWordRange.startOffset); |
- this._prefixRange = new WebInspector.TextRange(0, beforeRange.toString().length, 0, beforeRange.toString().length + fullWordRange.toString().length); |
- |
- if (selectedIndex === -1) |
- return; |
- this.applySuggestion(annotatedCompletions[selectedIndex].title, true); |
- }, |
- |
- /** |
- * @override |
- * @param {string} suggestion |
- * @param {boolean=} isIntermediateSuggestion |
- */ |
- applySuggestion: function(suggestion, isIntermediateSuggestion) |
- { |
- if (!this._prefixRange) |
- return; |
- this._currentSuggestion = suggestion; |
- this._refreshGhostText(); |
- if (isIntermediateSuggestion) |
- this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApplied); |
- }, |
- |
- /** |
- * @override |
- */ |
- acceptSuggestion: function() |
- { |
- this._acceptSuggestionInternal(); |
- }, |
- |
- /** |
- * @return {boolean} |
- */ |
- _acceptSuggestionInternal: function() |
- { |
- if (!this._prefixRange) |
- return false; |
- |
- var text = this.text(); |
- this._element.textContent = text.substring(0, this._prefixRange.startColumn) + this._currentSuggestion + text.substring(this._prefixRange.endColumn); |
- this._setDOMSelection(this._prefixRange.startColumn + this._currentSuggestion.length, this._prefixRange.startColumn + this._currentSuggestion.length); |
- |
- this.clearAutocomplete(); |
- this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemAccepted); |
- |
- return true; |
- }, |
- |
- /** |
- * @param {number} startColumn |
- * @param {number} endColumn |
- */ |
- _setDOMSelection: function(startColumn, endColumn) |
- { |
- this._element.normalize(); |
- var node = this._element.childNodes[0]; |
- if (!node || node === this._ghostTextElement) |
- return; |
- var range = this._createRange(); |
- range.setStart(node, startColumn); |
- range.setEnd(node, endColumn); |
- var selection = this._element.getComponentSelection(); |
- selection.removeAllRanges(); |
- selection.addRange(range); |
- }, |
- |
- /** |
- * @return {boolean} |
- */ |
- _isSuggestBoxVisible: function() |
- { |
- return this._suggestBox && this._suggestBox.visible(); |
- }, |
- |
- /** |
- * @return {boolean} |
- */ |
- isCaretInsidePrompt: function() |
- { |
- var selection = this._element.getComponentSelection(); |
- // @see crbug.com/602541 |
- var selectionRange = selection && selection.rangeCount ? selection.getRangeAt(0) : null; |
- if (!selectionRange || !selection.isCollapsed) |
- return false; |
- return selectionRange.startContainer.isSelfOrDescendant(this._element); |
- }, |
- |
- /** |
- * @return {boolean} |
- */ |
- _isCaretAtEndOfPrompt: function() |
- { |
- var selection = this._element.getComponentSelection(); |
- var selectionRange = selection && selection.rangeCount ? selection.getRangeAt(0) : null; |
- if (!selectionRange || !selection.isCollapsed) |
- return false; |
- |
- var node = selectionRange.startContainer; |
- if (!node.isSelfOrDescendant(this._element)) |
- return false; |
- |
- if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length) |
- return false; |
- |
- var foundNextText = false; |
- while (node) { |
- if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) { |
- if (foundNextText && !this._ghostTextElement.isAncestor(node)) |
- return false; |
- foundNextText = true; |
- } |
- |
- node = node.traverseNextNode(this._element); |
- } |
- |
- return true; |
- }, |
- |
- moveCaretToEndOfPrompt: function() |
- { |
- var selection = this._element.getComponentSelection(); |
- var selectionRange = this._createRange(); |
- |
- var container = this._element; |
- while (container.childNodes.length) |
- container = container.lastChild; |
- var offset = container.nodeType === Node.TEXT_NODE ? container.textContent.length : 0; |
- selectionRange.setStart(container, offset); |
- selectionRange.setEnd(container, offset); |
- |
- selection.removeAllRanges(); |
- selection.addRange(selectionRange); |
- }, |
- |
- /** |
- * @param {!Event} event |
- * @return {boolean} |
- */ |
- tabKeyPressed: function(event) |
- { |
- this.acceptAutoComplete(); |
- |
- // Consume the key. |
- return true; |
- }, |
- |
- /** |
- * @return {?Element} |
- */ |
- proxyElementForTests: function() |
- { |
- return this._proxyElement || null; |
- }, |
- |
- __proto__: WebInspector.Object.prototype |
+ ItemApplied: Symbol('text-prompt-item-applied'), |
+ ItemAccepted: Symbol('text-prompt-item-accepted') |
}; |