| /* |
| * Copyright (C) 2008 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of |
| * its contributors may be used to endorse or promote products derived |
| * from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| WebInspector.TextPrompt = function(element, completions, stopCharacters, omitHistory) |
| { |
| this.element = element; |
| this.element.addStyleClass("text-prompt"); |
| this.completions = completions; |
| this.completionStopCharacters = stopCharacters; |
| if (!omitHistory) { |
| this.history = []; |
| this.historyOffset = 0; |
| } |
| this._boundOnKeyDown = this._onKeyDown.bind(this); |
| this.element.addEventListener("keydown", this._boundOnKeyDown, true); |
| } |
| |
| WebInspector.TextPrompt.prototype = { |
| get text() |
| { |
| return this.element.textContent; |
| }, |
| |
| set text(x) |
| { |
| if (!x) { |
| // Append a break element instead of setting textContent to make sure the selection is inside the prompt. |
| this.element.removeChildren(); |
| this.element.appendChild(document.createElement("br")); |
| } else |
| this.element.textContent = x; |
| |
| this.moveCaretToEndOfPrompt(); |
| }, |
| |
| removeFromElement: function() |
| { |
| this.clearAutoComplete(true); |
| this.element.removeEventListener("keydown", this._boundOnKeyDown, true); |
| }, |
| |
| _onKeyDown: function(event) |
| { |
| function defaultAction() |
| { |
| this.clearAutoComplete(); |
| this.autoCompleteSoon(); |
| } |
| |
| if (event.handled) |
| return; |
| |
| var handled = false; |
| |
| switch (event.keyIdentifier) { |
| case "Up": |
| this.upKeyPressed(event); |
| break; |
| case "Down": |
| this.downKeyPressed(event); |
| break; |
| case "U+0009": // Tab |
| this.tabKeyPressed(event); |
| break; |
| case "Right": |
| case "End": |
| if (!this.acceptAutoComplete()) |
| this.autoCompleteSoon(); |
| break; |
| case "Alt": |
| case "Meta": |
| case "Shift": |
| case "Control": |
| break; |
| case "U+0050": // Ctrl+P = Previous |
| if (this.history && WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) { |
| handled = true; |
| this._moveBackInHistory(); |
| break; |
| } |
| defaultAction.call(this); |
| break; |
| case "U+004E": // Ctrl+N = Next |
| if (this.history && WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) { |
| handled = true; |
| this._moveForwardInHistory(); |
| break; |
| } |
| defaultAction.call(this); |
| break; |
| default: |
| defaultAction.call(this); |
| break; |
| } |
| |
| handled |= event.handled; |
| if (handled) { |
| event.handled = true; |
| event.preventDefault(); |
| event.stopPropagation(); |
| } |
| }, |
| |
| acceptAutoComplete: function() |
| { |
| if (!this.autoCompleteElement || !this.autoCompleteElement.parentNode) |
| return false; |
| |
| var text = this.autoCompleteElement.textContent; |
| var textNode = document.createTextNode(text); |
| this.autoCompleteElement.parentNode.replaceChild(textNode, this.autoCompleteElement); |
| delete this.autoCompleteElement; |
| |
| var finalSelectionRange = document.createRange(); |
| finalSelectionRange.setStart(textNode, text.length); |
| finalSelectionRange.setEnd(textNode, text.length); |
| |
| var selection = window.getSelection(); |
| selection.removeAllRanges(); |
| selection.addRange(finalSelectionRange); |
| |
| return true; |
| }, |
| |
| clearAutoComplete: function(includeTimeout) |
| { |
| if (includeTimeout && "_completeTimeout" in this) { |
| clearTimeout(this._completeTimeout); |
| delete this._completeTimeout; |
| } |
| |
| if (!this.autoCompleteElement) |
| return; |
| |
| if (this.autoCompleteElement.parentNode) |
| this.autoCompleteElement.parentNode.removeChild(this.autoCompleteElement); |
| delete this.autoCompleteElement; |
| |
| if (!this._userEnteredRange || !this._userEnteredText) |
| return; |
| |
| this._userEnteredRange.deleteContents(); |
| this.element.pruneEmptyTextNodes(); |
| |
| var userTextNode = document.createTextNode(this._userEnteredText); |
| this._userEnteredRange.insertNode(userTextNode); |
| |
| var selectionRange = document.createRange(); |
| selectionRange.setStart(userTextNode, this._userEnteredText.length); |
| selectionRange.setEnd(userTextNode, this._userEnteredText.length); |
| |
| var selection = window.getSelection(); |
| selection.removeAllRanges(); |
| selection.addRange(selectionRange); |
| |
| delete this._userEnteredRange; |
| delete this._userEnteredText; |
| }, |
| |
| autoCompleteSoon: function() |
| { |
| if (!("_completeTimeout" in this)) |
| this._completeTimeout = setTimeout(this.complete.bind(this, true), 250); |
| }, |
| |
| complete: function(auto, reverse) |
| { |
| this.clearAutoComplete(true); |
| var selection = window.getSelection(); |
| if (!selection.rangeCount) |
| return; |
| |
| var selectionRange = selection.getRangeAt(0); |
| var isEmptyInput = selectionRange.commonAncestorContainer === this.element; // this.element has no child Text nodes. |
| |
| // Do not attempt to auto-complete an empty input in the auto mode (only on demand). |
| if (auto && isEmptyInput) |
| return; |
| if (!auto && !isEmptyInput && !selectionRange.commonAncestorContainer.isDescendant(this.element)) |
| return; |
| if (auto && !this.isCaretAtEndOfPrompt()) |
| return; |
| var wordPrefixRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, this.completionStopCharacters, this.element, "backward"); |
| this.completions(wordPrefixRange, auto, this._completionsReady.bind(this, selection, auto, wordPrefixRange, reverse)); |
| }, |
| |
| _completionsReady: function(selection, auto, originalWordPrefixRange, reverse, completions) |
| { |
| if (!completions || !completions.length) |
| return; |
| |
| var selectionRange = selection.getRangeAt(0); |
| |
| var fullWordRange = document.createRange(); |
| fullWordRange.setStart(originalWordPrefixRange.startContainer, originalWordPrefixRange.startOffset); |
| fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset); |
| |
| if (originalWordPrefixRange.toString() + selectionRange.toString() != fullWordRange.toString()) |
| return; |
| |
| var wordPrefixLength = originalWordPrefixRange.toString().length; |
| |
| if (auto) |
| var completionText = completions[0]; |
| else { |
| if (completions.length === 1) { |
| var completionText = completions[0]; |
| wordPrefixLength = completionText.length; |
| } else { |
| var commonPrefix = completions[0]; |
| for (var i = 0; i < completions.length; ++i) { |
| var completion = completions[i]; |
| var lastIndex = Math.min(commonPrefix.length, completion.length); |
| for (var j = wordPrefixLength; j < lastIndex; ++j) { |
| if (commonPrefix[j] !== completion[j]) { |
| commonPrefix = commonPrefix.substr(0, j); |
| break; |
| } |
| } |
| } |
| wordPrefixLength = commonPrefix.length; |
| |
| if (selection.isCollapsed) |
| var completionText = completions[0]; |
| else { |
| var currentText = fullWordRange.toString(); |
| |
| var foundIndex = null; |
| for (var i = 0; i < completions.length; ++i) { |
| if (completions[i] === currentText) |
| foundIndex = i; |
| } |
| |
| var nextIndex = foundIndex + (reverse ? -1 : 1); |
| if (foundIndex === null || nextIndex >= completions.length) |
| var completionText = completions[0]; |
| else if (nextIndex < 0) |
| var completionText = completions[completions.length - 1]; |
| else |
| var completionText = completions[nextIndex]; |
| } |
| } |
| } |
| |
| this._userEnteredRange = fullWordRange; |
| this._userEnteredText = fullWordRange.toString(); |
| |
| fullWordRange.deleteContents(); |
| this.element.pruneEmptyTextNodes(); |
| |
| var finalSelectionRange = document.createRange(); |
| |
| if (auto) { |
| var prefixText = completionText.substring(0, wordPrefixLength); |
| var suffixText = completionText.substring(wordPrefixLength); |
| |
| var prefixTextNode = document.createTextNode(prefixText); |
| fullWordRange.insertNode(prefixTextNode); |
| |
| this.autoCompleteElement = document.createElement("span"); |
| this.autoCompleteElement.className = "auto-complete-text"; |
| this.autoCompleteElement.textContent = suffixText; |
| |
| prefixTextNode.parentNode.insertBefore(this.autoCompleteElement, prefixTextNode.nextSibling); |
| |
| finalSelectionRange.setStart(prefixTextNode, wordPrefixLength); |
| finalSelectionRange.setEnd(prefixTextNode, wordPrefixLength); |
| } else { |
| var completionTextNode = document.createTextNode(completionText); |
| fullWordRange.insertNode(completionTextNode); |
| |
| if (completions.length > 1) |
| finalSelectionRange.setStart(completionTextNode, wordPrefixLength); |
| else |
| finalSelectionRange.setStart(completionTextNode, completionText.length); |
| |
| finalSelectionRange.setEnd(completionTextNode, completionText.length); |
| } |
| |
| selection.removeAllRanges(); |
| selection.addRange(finalSelectionRange); |
| }, |
| |
| isCaretInsidePrompt: function() |
| { |
| return this.element.isInsertionCaretInside(); |
| }, |
| |
| isCaretAtEndOfPrompt: function() |
| { |
| var selection = window.getSelection(); |
| if (!selection.rangeCount || !selection.isCollapsed) |
| return false; |
| |
| var selectionRange = selection.getRangeAt(0); |
| var node = selectionRange.startContainer; |
| if (node !== this.element && !node.isDescendant(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) |
| return false; |
| foundNextText = true; |
| } |
| |
| node = node.traverseNextNode(this.element); |
| } |
| |
| return true; |
| }, |
| |
| isCaretOnFirstLine: function() |
| { |
| var selection = window.getSelection(); |
| var focusNode = selection.focusNode; |
| if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this.element) |
| return true; |
| |
| if (focusNode.textContent.substring(0, selection.focusOffset).indexOf("\n") !== -1) |
| return false; |
| focusNode = focusNode.previousSibling; |
| |
| while (focusNode) { |
| if (focusNode.nodeType !== Node.TEXT_NODE) |
| return true; |
| if (focusNode.textContent.indexOf("\n") !== -1) |
| return false; |
| focusNode = focusNode.previousSibling; |
| } |
| |
| return true; |
| }, |
| |
| isCaretOnLastLine: function() |
| { |
| var selection = window.getSelection(); |
| var focusNode = selection.focusNode; |
| if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this.element) |
| return true; |
| |
| if (focusNode.textContent.substring(selection.focusOffset).indexOf("\n") !== -1) |
| return false; |
| focusNode = focusNode.nextSibling; |
| |
| while (focusNode) { |
| if (focusNode.nodeType !== Node.TEXT_NODE) |
| return true; |
| if (focusNode.textContent.indexOf("\n") !== -1) |
| return false; |
| focusNode = focusNode.nextSibling; |
| } |
| |
| return true; |
| }, |
| |
| moveCaretToEndOfPrompt: function() |
| { |
| var selection = window.getSelection(); |
| var selectionRange = document.createRange(); |
| |
| var offset = this.element.childNodes.length; |
| selectionRange.setStart(this.element, offset); |
| selectionRange.setEnd(this.element, offset); |
| |
| selection.removeAllRanges(); |
| selection.addRange(selectionRange); |
| }, |
| |
| tabKeyPressed: function(event) |
| { |
| event.handled = true; |
| this.complete(false, event.shiftKey); |
| }, |
| |
| upKeyPressed: function(event) |
| { |
| if (!this.isCaretOnFirstLine()) |
| return; |
| |
| event.handled = true; |
| this._moveBackInHistory(); |
| }, |
| |
| downKeyPressed: function(event) |
| { |
| if (!this.isCaretOnLastLine()) |
| return; |
| |
| event.handled = true; |
| this._moveForwardInHistory(); |
| }, |
| |
| _moveBackInHistory: function() |
| { |
| if (!this.history || this.historyOffset == this.history.length) |
| return; |
| |
| this.clearAutoComplete(true); |
| |
| if (this.historyOffset === 0) |
| this.tempSavedCommand = this.text; |
| |
| ++this.historyOffset; |
| this.text = this.history[this.history.length - this.historyOffset]; |
| |
| this.element.scrollIntoView(true); |
| var firstNewlineIndex = this.text.indexOf("\n"); |
| if (firstNewlineIndex === -1) |
| this.moveCaretToEndOfPrompt(); |
| else { |
| var selection = window.getSelection(); |
| var selectionRange = document.createRange(); |
| |
| selectionRange.setStart(this.element.firstChild, firstNewlineIndex); |
| selectionRange.setEnd(this.element.firstChild, firstNewlineIndex); |
| |
| selection.removeAllRanges(); |
| selection.addRange(selectionRange); |
| } |
| }, |
| |
| _moveForwardInHistory: function() |
| { |
| if (!this.history || this.historyOffset === 0) |
| return; |
| |
| this.clearAutoComplete(true); |
| |
| --this.historyOffset; |
| |
| if (this.historyOffset === 0) { |
| this.text = this.tempSavedCommand; |
| delete this.tempSavedCommand; |
| return; |
| } |
| |
| this.text = this.history[this.history.length - this.historyOffset]; |
| this.element.scrollIntoView(); |
| } |
| } |