/* | |
* 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) | |
{ | |
this.element = element; | |
this.completions = completions; | |
this.completionStopCharacters = stopCharacters; | |
this.history = []; | |
this.historyOffset = 0; | |
this.element.addEventListener("keydown", this._onKeyDown.bind(this), 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(); | |
}, | |
_onKeyDown: function(event) | |
{ | |
function defaultAction() | |
{ | |
this.clearAutoComplete(); | |
this.autoCompleteSoon(); | |
} | |
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 (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 (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) { | |
handled = true; | |
this._moveForwardInHistory(); | |
break; | |
} | |
defaultAction.call(this); | |
break; | |
default: | |
defaultAction.call(this); | |
break; | |
} | |
if (handled) { | |
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) | |
{ | |
this.clearAutoComplete(true); | |
var selection = window.getSelection(); | |
if (!selection.rangeCount) | |
return; | |
var selectionRange = selection.getRangeAt(0); | |
if (!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)); | |
}, | |
_completionsReady: function(selection, auto, originalWordPrefixRange, 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; | |
if (completions.length === 1 || selection.isCollapsed || auto) { | |
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; | |
if (foundIndex === null || (foundIndex + 1) >= completions.length) | |
var completionText = completions[0]; | |
else | |
var completionText = completions[foundIndex + 1]; | |
} | |
var wordPrefixLength = originalWordPrefixRange.toString().length; | |
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.preventDefault(); | |
event.stopPropagation(); | |
this.complete(); | |
}, | |
_upKeyPressed: function(event) | |
{ | |
if (!this.isCaretOnFirstLine()) | |
return; | |
event.preventDefault(); | |
event.stopPropagation(); | |
this._moveBackInHistory(); | |
}, | |
_downKeyPressed: function(event) | |
{ | |
if (!this.isCaretOnLastLine()) | |
return; | |
event.preventDefault(); | |
event.stopPropagation(); | |
this._moveForwardInHistory(); | |
}, | |
_moveBackInHistory: function() | |
{ | |
if (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.scrollIntoViewIfNeeded(); | |
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.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.scrollIntoViewIfNeeded(); | |
} | |
} |