Home | History | Annotate | Download | only in front_end
      1 /*
      2  * Copyright (C) 2008 Apple Inc.  All rights reserved.
      3  * Copyright (C) 2011 Google Inc.  All rights reserved.
      4  *
      5  * Redistribution and use in source and binary forms, with or without
      6  * modification, are permitted provided that the following conditions
      7  * are met:
      8  *
      9  * 1.  Redistributions of source code must retain the above copyright
     10  *     notice, this list of conditions and the following disclaimer.
     11  * 2.  Redistributions in binary form must reproduce the above copyright
     12  *     notice, this list of conditions and the following disclaimer in the
     13  *     documentation and/or other materials provided with the distribution.
     14  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
     15  *     its contributors may be used to endorse or promote products derived
     16  *     from this software without specific prior written permission.
     17  *
     18  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
     19  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
     20  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     21  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
     22  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
     23  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
     24  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
     25  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     26  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
     27  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     28  */
     29 
     30 /**
     31  * @constructor
     32  * @extends WebInspector.Object
     33  * @implements {WebInspector.SuggestBoxDelegate}
     34  * @param {function(Element, Range, boolean, function(!Array.<string>, number=))} completions
     35  * @param {string=} stopCharacters
     36  */
     37 WebInspector.TextPrompt = function(completions, stopCharacters)
     38 {
     39     /**
     40      * @type {Element|undefined}
     41      */
     42     this._proxyElement;
     43     this._proxyElementDisplay = "inline-block";
     44     this._loadCompletions = completions;
     45     this._completionStopCharacters = stopCharacters || " =:[({;,!+-*/&|^<>.";
     46     this._suggestForceable = true;
     47 }
     48 
     49 WebInspector.TextPrompt.Events = {
     50     ItemApplied: "text-prompt-item-applied",
     51     ItemAccepted: "text-prompt-item-accepted"
     52 };
     53 
     54 WebInspector.TextPrompt.prototype = {
     55     get proxyElement()
     56     {
     57         return this._proxyElement;
     58     },
     59 
     60     /**
     61      * @param {boolean} x
     62      */
     63     setSuggestForceable: function(x)
     64     {
     65         this._suggestForceable = x;
     66     },
     67 
     68     /**
     69      * @param {boolean} x
     70      */
     71     setShowSuggestForEmptyInput: function(x)
     72     {
     73         this._showSuggestForEmptyInput = x;
     74     },
     75 
     76     /**
     77      * @param {string} className
     78      */
     79     setSuggestBoxEnabled: function(className)
     80     {
     81         this._suggestBoxClassName = className;
     82     },
     83 
     84     renderAsBlock: function()
     85     {
     86         this._proxyElementDisplay = "block";
     87     },
     88 
     89     /**
     90      * Clients should never attach any event listeners to the |element|. Instead,
     91      * they should use the result of this method to attach listeners for bubbling events.
     92      *
     93      * @param {Element} element
     94      */
     95     attach: function(element)
     96     {
     97         return this._attachInternal(element);
     98     },
     99 
    100     /**
    101      * Clients should never attach any event listeners to the |element|. Instead,
    102      * they should use the result of this method to attach listeners for bubbling events
    103      * or the |blurListener| parameter to register a "blur" event listener on the |element|
    104      * (since the "blur" event does not bubble.)
    105      *
    106      * @param {Element} element
    107      * @param {function(Event)} blurListener
    108      */
    109     attachAndStartEditing: function(element, blurListener)
    110     {
    111         this._attachInternal(element);
    112         this._startEditing(blurListener);
    113         return this.proxyElement;
    114     },
    115 
    116     /**
    117      * @param {Element} element
    118      */
    119     _attachInternal: function(element)
    120     {
    121         if (this.proxyElement)
    122             throw "Cannot attach an attached TextPrompt";
    123         this._element = element;
    124 
    125         this._boundOnKeyDown = this.onKeyDown.bind(this);
    126         this._boundOnMouseWheel = this.onMouseWheel.bind(this);
    127         this._boundSelectStart = this._selectStart.bind(this);
    128         this._proxyElement = element.ownerDocument.createElement("span");
    129         this._proxyElement.style.display = this._proxyElementDisplay;
    130         element.parentElement.insertBefore(this.proxyElement, element);
    131         this.proxyElement.appendChild(element);
    132         this._element.addStyleClass("text-prompt");
    133         this._element.addEventListener("keydown", this._boundOnKeyDown, false);
    134         this._element.addEventListener("mousewheel", this._boundOnMouseWheel, false);
    135         this._element.addEventListener("selectstart", this._boundSelectStart, false);
    136 
    137         if (typeof this._suggestBoxClassName === "string")
    138             this._suggestBox = new WebInspector.SuggestBox(this, this._element, this._suggestBoxClassName);
    139 
    140         return this.proxyElement;
    141     },
    142 
    143     detach: function()
    144     {
    145         this._removeFromElement();
    146         this.proxyElement.parentElement.insertBefore(this._element, this.proxyElement);
    147         this.proxyElement.remove();
    148         delete this._proxyElement;
    149         this._element.removeStyleClass("text-prompt");
    150         this._element.removeEventListener("keydown", this._boundOnKeyDown, false);
    151         this._element.removeEventListener("mousewheel", this._boundOnMouseWheel, false);
    152         this._element.removeEventListener("selectstart", this._boundSelectStart, false);
    153         WebInspector.restoreFocusFromElement(this._element);
    154     },
    155 
    156     /**
    157      * @return string
    158      */
    159     get text()
    160     {
    161         return this._element.textContent;
    162     },
    163 
    164     /**
    165      * @param {string} x
    166      */
    167     set text(x)
    168     {
    169         this._removeSuggestionAids();
    170         if (!x) {
    171             // Append a break element instead of setting textContent to make sure the selection is inside the prompt.
    172             this._element.removeChildren();
    173             this._element.appendChild(document.createElement("br"));
    174         } else
    175             this._element.textContent = x;
    176 
    177         this.moveCaretToEndOfPrompt();
    178         this._element.scrollIntoView();
    179     },
    180 
    181     _removeFromElement: function()
    182     {
    183         this.clearAutoComplete(true);
    184         this._element.removeEventListener("keydown", this._boundOnKeyDown, false);
    185         this._element.removeEventListener("selectstart", this._boundSelectStart, false);
    186         if (this._isEditing)
    187             this._stopEditing();
    188         if (this._suggestBox)
    189             this._suggestBox.removeFromElement();
    190     },
    191 
    192     /**
    193      * @param {function(Event)=} blurListener
    194      */
    195     _startEditing: function(blurListener)
    196     {
    197         this._isEditing = true;
    198         this._element.addStyleClass("editing");
    199         if (blurListener) {
    200             this._blurListener = blurListener;
    201             this._element.addEventListener("blur", this._blurListener, false);
    202         }
    203         this._oldTabIndex = this._element.tabIndex;
    204         if (this._element.tabIndex < 0)
    205             this._element.tabIndex = 0;
    206         WebInspector.setCurrentFocusElement(this._element);
    207         if (!this.text)
    208             this._updateAutoComplete();
    209     },
    210 
    211     _stopEditing: function()
    212     {
    213         this._element.tabIndex = this._oldTabIndex;
    214         if (this._blurListener)
    215             this._element.removeEventListener("blur", this._blurListener, false);
    216         this._element.removeStyleClass("editing");
    217         delete this._isEditing;
    218     },
    219 
    220     _removeSuggestionAids: function()
    221     {
    222         this.clearAutoComplete();
    223         this.hideSuggestBox();
    224     },
    225 
    226     _selectStart: function()
    227     {
    228         if (this._selectionTimeout)
    229             clearTimeout(this._selectionTimeout);
    230 
    231         this._removeSuggestionAids();
    232 
    233         function moveBackIfOutside()
    234         {
    235             delete this._selectionTimeout;
    236             if (!this.isCaretInsidePrompt() && window.getSelection().isCollapsed) {
    237                 this.moveCaretToEndOfPrompt();
    238                 this.autoCompleteSoon();
    239             }
    240         }
    241 
    242         this._selectionTimeout = setTimeout(moveBackIfOutside.bind(this), 100);
    243     },
    244 
    245     /**
    246      * @param {boolean=} force
    247      * @return {boolean}
    248      */
    249     defaultKeyHandler: function(event, force)
    250     {
    251         this._updateAutoComplete(force);
    252         return false;
    253     },
    254 
    255     /**
    256      * @param {boolean=} force
    257      */
    258     _updateAutoComplete: function(force)
    259     {
    260         this.clearAutoComplete();
    261         this.autoCompleteSoon(force);
    262     },
    263 
    264     /**
    265      * @param {Event} event
    266      */
    267     onMouseWheel: function(event)
    268     {
    269         // Subclasses can implement.
    270     },
    271 
    272     /**
    273      * @param {Event} event
    274      */
    275     onKeyDown: function(event)
    276     {
    277         var handled = false;
    278         var invokeDefault = true;
    279 
    280         switch (event.keyIdentifier) {
    281         case "U+0009": // Tab
    282             handled = this.tabKeyPressed(event);
    283             break;
    284         case "Left":
    285         case "Home":
    286             this._removeSuggestionAids();
    287             invokeDefault = false;
    288             break;
    289         case "Right":
    290         case "End":
    291             if (this.isCaretAtEndOfPrompt())
    292                 handled = this.acceptAutoComplete();
    293             else
    294                 this._removeSuggestionAids();
    295             invokeDefault = false;
    296             break;
    297         case "U+001B": // Esc
    298             if (this.isSuggestBoxVisible()) {
    299                 this._removeSuggestionAids();
    300                 handled = true;
    301             }
    302             break;
    303         case "U+0020": // Space
    304             if (this._suggestForceable && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
    305                 this.defaultKeyHandler(event, true);
    306                 handled = true;
    307             }
    308             break;
    309         case "Alt":
    310         case "Meta":
    311         case "Shift":
    312         case "Control":
    313             invokeDefault = false;
    314             break;
    315         }
    316 
    317         if (!handled && this.isSuggestBoxVisible())
    318             handled = this._suggestBox.keyPressed(event);
    319 
    320         if (!handled && invokeDefault)
    321             handled = this.defaultKeyHandler(event);
    322 
    323         if (handled)
    324             event.consume(true);
    325 
    326         return handled;
    327     },
    328 
    329     /**
    330      * @return {boolean}
    331      */
    332     acceptAutoComplete: function()
    333     {
    334         var result = false;
    335         if (this.isSuggestBoxVisible())
    336             result = this._suggestBox.acceptSuggestion();
    337         if (!result)
    338             result = this.acceptSuggestion();
    339 
    340         return result;
    341     },
    342 
    343     /**
    344      * @param {boolean=} includeTimeout
    345      */
    346     clearAutoComplete: function(includeTimeout)
    347     {
    348         if (includeTimeout && this._completeTimeout) {
    349             clearTimeout(this._completeTimeout);
    350             delete this._completeTimeout;
    351         }
    352         delete this._waitingForCompletions;
    353 
    354         if (!this.autoCompleteElement)
    355             return;
    356 
    357         this.autoCompleteElement.remove();
    358         delete this.autoCompleteElement;
    359 
    360         if (!this._userEnteredRange || !this._userEnteredText)
    361             return;
    362 
    363         this._userEnteredRange.deleteContents();
    364         this._element.normalize();
    365 
    366         var userTextNode = document.createTextNode(this._userEnteredText);
    367         this._userEnteredRange.insertNode(userTextNode);
    368 
    369         var selectionRange = document.createRange();
    370         selectionRange.setStart(userTextNode, this._userEnteredText.length);
    371         selectionRange.setEnd(userTextNode, this._userEnteredText.length);
    372 
    373         var selection = window.getSelection();
    374         selection.removeAllRanges();
    375         selection.addRange(selectionRange);
    376 
    377         delete this._userEnteredRange;
    378         delete this._userEnteredText;
    379     },
    380 
    381     /**
    382      * @param {boolean=} force
    383      */
    384     autoCompleteSoon: function(force)
    385     {
    386         var immediately = this.isSuggestBoxVisible() || force;
    387         if (!this._completeTimeout)
    388             this._completeTimeout = setTimeout(this.complete.bind(this, force), immediately ? 0 : 250);
    389     },
    390 
    391     /**
    392      * @param {boolean=} reverse
    393      */
    394     complete: function(force, reverse)
    395     {
    396         this.clearAutoComplete(true);
    397         var selection = window.getSelection();
    398         if (!selection.rangeCount)
    399             return;
    400 
    401         var selectionRange = selection.getRangeAt(0);
    402         var shouldExit;
    403 
    404         if (!force && !this.isCaretAtEndOfPrompt() && !this.isSuggestBoxVisible())
    405             shouldExit = true;
    406         else if (!selection.isCollapsed)
    407             shouldExit = true;
    408         else if (!force) {
    409             // BUG72018: Do not show suggest box if caret is followed by a non-stop character.
    410             var wordSuffixRange = selectionRange.startContainer.rangeOfWord(selectionRange.endOffset, this._completionStopCharacters, this._element, "forward");
    411             if (wordSuffixRange.toString().length)
    412                 shouldExit = true;
    413         }
    414         if (shouldExit) {
    415             this.hideSuggestBox();
    416             return;
    417         }
    418 
    419         var wordPrefixRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, this._completionStopCharacters, this._element, "backward");
    420         this._waitingForCompletions = true;
    421         this._loadCompletions(this.proxyElement, wordPrefixRange, force, this._completionsReady.bind(this, selection, wordPrefixRange, !!reverse));
    422     },
    423 
    424     /**
    425      * @param {Selection} selection
    426      * @param {Range} textRange
    427      */
    428     _boxForAnchorAtStart: function(selection, textRange)
    429     {
    430         var rangeCopy = selection.getRangeAt(0).cloneRange();
    431         var anchorElement = document.createElement("span");
    432         anchorElement.textContent = "\u200B";
    433         textRange.insertNode(anchorElement);
    434         var box = anchorElement.boxInWindow(window);
    435         anchorElement.remove();
    436         selection.removeAllRanges();
    437         selection.addRange(rangeCopy);
    438         return box;
    439     },
    440 
    441     /**
    442      * @param {Array.<string>} completions
    443      * @param {number} wordPrefixLength
    444      */
    445     _buildCommonPrefix: function(completions, wordPrefixLength)
    446     {
    447         var commonPrefix = completions[0];
    448         for (var i = 0; i < completions.length; ++i) {
    449             var completion = completions[i];
    450             var lastIndex = Math.min(commonPrefix.length, completion.length);
    451             for (var j = wordPrefixLength; j < lastIndex; ++j) {
    452                 if (commonPrefix[j] !== completion[j]) {
    453                     commonPrefix = commonPrefix.substr(0, j);
    454                     break;
    455                 }
    456             }
    457         }
    458         return commonPrefix;
    459     },
    460 
    461     /**
    462      * @param {Selection} selection
    463      * @param {Range} originalWordPrefixRange
    464      * @param {boolean} reverse
    465      * @param {!Array.<string>} completions
    466      * @param {number=} selectedIndex
    467      */
    468     _completionsReady: function(selection, originalWordPrefixRange, reverse, completions, selectedIndex)
    469     {
    470         if (!this._waitingForCompletions || !completions.length) {
    471             this.hideSuggestBox();
    472             return;
    473         }
    474         delete this._waitingForCompletions;
    475 
    476         var selectionRange = selection.getRangeAt(0);
    477 
    478         var fullWordRange = document.createRange();
    479         fullWordRange.setStart(originalWordPrefixRange.startContainer, originalWordPrefixRange.startOffset);
    480         fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset);
    481 
    482         if (originalWordPrefixRange.toString() + selectionRange.toString() != fullWordRange.toString())
    483             return;
    484 
    485         selectedIndex = selectedIndex || 0;
    486 
    487         this._userEnteredRange = fullWordRange;
    488         this._userEnteredText = fullWordRange.toString();
    489 
    490         if (this._suggestBox)
    491             this._suggestBox.updateSuggestions(this._boxForAnchorAtStart(selection, fullWordRange), completions, selectedIndex, !this.isCaretAtEndOfPrompt(), this._userEnteredText);
    492 
    493         var wordPrefixLength = originalWordPrefixRange.toString().length;
    494         this._commonPrefix = this._buildCommonPrefix(completions, wordPrefixLength);
    495 
    496         if (this.isCaretAtEndOfPrompt()) {
    497             this._userEnteredRange.deleteContents();
    498             this._element.normalize();
    499             var finalSelectionRange = document.createRange();
    500             var completionText = completions[selectedIndex];
    501             var prefixText = completionText.substring(0, wordPrefixLength);
    502             var suffixText = completionText.substring(wordPrefixLength);
    503 
    504             var prefixTextNode = document.createTextNode(prefixText);
    505             fullWordRange.insertNode(prefixTextNode);
    506 
    507             this.autoCompleteElement = document.createElement("span");
    508             this.autoCompleteElement.className = "auto-complete-text";
    509             this.autoCompleteElement.textContent = suffixText;
    510 
    511             prefixTextNode.parentNode.insertBefore(this.autoCompleteElement, prefixTextNode.nextSibling);
    512 
    513             finalSelectionRange.setStart(prefixTextNode, wordPrefixLength);
    514             finalSelectionRange.setEnd(prefixTextNode, wordPrefixLength);
    515             selection.removeAllRanges();
    516             selection.addRange(finalSelectionRange);
    517         }
    518     },
    519 
    520     _completeCommonPrefix: function()
    521     {
    522         if (!this.autoCompleteElement || !this._commonPrefix || !this._userEnteredText || !this._commonPrefix.startsWith(this._userEnteredText))
    523             return;
    524 
    525         if (!this.isSuggestBoxVisible()) {
    526             this.acceptAutoComplete();
    527             return;
    528         }
    529 
    530         this.autoCompleteElement.textContent = this._commonPrefix.substring(this._userEnteredText.length);
    531         this.acceptSuggestion(true)
    532     },
    533 
    534     /**
    535      * @param {string} completionText
    536      * @param {boolean=} isIntermediateSuggestion
    537      */
    538     applySuggestion: function(completionText, isIntermediateSuggestion)
    539     {
    540         this._applySuggestion(completionText, isIntermediateSuggestion);
    541     },
    542 
    543     /**
    544      * @param {string} completionText
    545      * @param {boolean=} isIntermediateSuggestion
    546      * @param {Range=} originalPrefixRange
    547      */
    548     _applySuggestion: function(completionText, isIntermediateSuggestion, originalPrefixRange)
    549     {
    550         var wordPrefixLength;
    551         if (originalPrefixRange)
    552             wordPrefixLength = originalPrefixRange.toString().length;
    553         else
    554             wordPrefixLength = this._userEnteredText ? this._userEnteredText.length : 0;
    555 
    556         this._userEnteredRange.deleteContents();
    557         this._element.normalize();
    558         var finalSelectionRange = document.createRange();
    559         var completionTextNode = document.createTextNode(completionText);
    560         this._userEnteredRange.insertNode(completionTextNode);
    561         if (this.autoCompleteElement) {
    562             this.autoCompleteElement.remove();
    563             delete this.autoCompleteElement;
    564         }
    565 
    566         if (isIntermediateSuggestion)
    567             finalSelectionRange.setStart(completionTextNode, wordPrefixLength);
    568         else
    569             finalSelectionRange.setStart(completionTextNode, completionText.length);
    570 
    571         finalSelectionRange.setEnd(completionTextNode, completionText.length);
    572 
    573         var selection = window.getSelection();
    574         selection.removeAllRanges();
    575         selection.addRange(finalSelectionRange);
    576         if (isIntermediateSuggestion)
    577             this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApplied, { itemText: completionText });
    578     },
    579 
    580     /**
    581      * @param {boolean=} prefixAccepted
    582      */
    583     acceptSuggestion: function(prefixAccepted)
    584     {
    585         if (this._isAcceptingSuggestion)
    586             return false;
    587 
    588         if (!this.autoCompleteElement || !this.autoCompleteElement.parentNode)
    589             return false;
    590 
    591         var text = this.autoCompleteElement.textContent;
    592         var textNode = document.createTextNode(text);
    593         this.autoCompleteElement.parentNode.replaceChild(textNode, this.autoCompleteElement);
    594         delete this.autoCompleteElement;
    595 
    596         var finalSelectionRange = document.createRange();
    597         finalSelectionRange.setStart(textNode, text.length);
    598         finalSelectionRange.setEnd(textNode, text.length);
    599 
    600         var selection = window.getSelection();
    601         selection.removeAllRanges();
    602         selection.addRange(finalSelectionRange);
    603 
    604         if (!prefixAccepted) {
    605             this.hideSuggestBox();
    606             this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemAccepted);
    607         } else
    608             this.autoCompleteSoon(true);
    609 
    610         return true;
    611     },
    612 
    613     hideSuggestBox: function()
    614     {
    615         if (this.isSuggestBoxVisible())
    616             this._suggestBox.hide();
    617     },
    618 
    619     /**
    620      * @return {boolean}
    621      */
    622     isSuggestBoxVisible: function()
    623     {
    624         return this._suggestBox && this._suggestBox.visible();
    625     },
    626 
    627     /**
    628      * @return {boolean}
    629      */
    630     isCaretInsidePrompt: function()
    631     {
    632         return this._element.isInsertionCaretInside();
    633     },
    634 
    635     /**
    636      * @return {boolean}
    637      */
    638     isCaretAtEndOfPrompt: function()
    639     {
    640         var selection = window.getSelection();
    641         if (!selection.rangeCount || !selection.isCollapsed)
    642             return false;
    643 
    644         var selectionRange = selection.getRangeAt(0);
    645         var node = selectionRange.startContainer;
    646         if (!node.isSelfOrDescendant(this._element))
    647             return false;
    648 
    649         if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length)
    650             return false;
    651 
    652         var foundNextText = false;
    653         while (node) {
    654             if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) {
    655                 if (foundNextText && (!this.autoCompleteElement || !this.autoCompleteElement.isAncestor(node)))
    656                     return false;
    657                 foundNextText = true;
    658             }
    659 
    660             node = node.traverseNextNode(this._element);
    661         }
    662 
    663         return true;
    664     },
    665 
    666     /**
    667      * @return {boolean}
    668      */
    669     isCaretOnFirstLine: function()
    670     {
    671         var selection = window.getSelection();
    672         var focusNode = selection.focusNode;
    673         if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element)
    674             return true;
    675 
    676         if (focusNode.textContent.substring(0, selection.focusOffset).indexOf("\n") !== -1)
    677             return false;
    678         focusNode = focusNode.previousSibling;
    679 
    680         while (focusNode) {
    681             if (focusNode.nodeType !== Node.TEXT_NODE)
    682                 return true;
    683             if (focusNode.textContent.indexOf("\n") !== -1)
    684                 return false;
    685             focusNode = focusNode.previousSibling;
    686         }
    687 
    688         return true;
    689     },
    690 
    691     /**
    692      * @return {boolean}
    693      */
    694     isCaretOnLastLine: function()
    695     {
    696         var selection = window.getSelection();
    697         var focusNode = selection.focusNode;
    698         if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element)
    699             return true;
    700 
    701         if (focusNode.textContent.substring(selection.focusOffset).indexOf("\n") !== -1)
    702             return false;
    703         focusNode = focusNode.nextSibling;
    704 
    705         while (focusNode) {
    706             if (focusNode.nodeType !== Node.TEXT_NODE)
    707                 return true;
    708             if (focusNode.textContent.indexOf("\n") !== -1)
    709                 return false;
    710             focusNode = focusNode.nextSibling;
    711         }
    712 
    713         return true;
    714     },
    715 
    716     moveCaretToEndOfPrompt: function()
    717     {
    718         var selection = window.getSelection();
    719         var selectionRange = document.createRange();
    720 
    721         var offset = this._element.childNodes.length;
    722         selectionRange.setStart(this._element, offset);
    723         selectionRange.setEnd(this._element, offset);
    724 
    725         selection.removeAllRanges();
    726         selection.addRange(selectionRange);
    727     },
    728 
    729     /**
    730      * @param {Event} event
    731      * @return {boolean}
    732      */
    733     tabKeyPressed: function(event)
    734     {
    735         this._completeCommonPrefix();
    736 
    737         // Consume the key.
    738         return true;
    739     },
    740 
    741     __proto__: WebInspector.Object.prototype
    742 }
    743 
    744 
    745 /**
    746  * @constructor
    747  * @extends {WebInspector.TextPrompt}
    748  * @param {function(Element, Range, boolean, function(!Array.<string>, number=))} completions
    749  * @param {string=} stopCharacters
    750  */
    751 WebInspector.TextPromptWithHistory = function(completions, stopCharacters)
    752 {
    753     WebInspector.TextPrompt.call(this, completions, stopCharacters);
    754 
    755     /**
    756      * @type {Array.<string>}
    757      */
    758     this._data = [];
    759 
    760     /**
    761      * 1-based entry in the history stack.
    762      * @type {number}
    763      */
    764     this._historyOffset = 1;
    765 
    766     /**
    767      * Whether to coalesce duplicate items in the history, default is true.
    768      * @type {boolean}
    769      */
    770     this._coalesceHistoryDupes = true;
    771 }
    772 
    773 WebInspector.TextPromptWithHistory.prototype = {
    774     /**
    775      * @return {Array.<string>}
    776      */
    777     get historyData()
    778     {
    779         // FIXME: do we need to copy this?
    780         return this._data;
    781     },
    782 
    783     /**
    784      * @param {boolean} x
    785      */
    786     setCoalesceHistoryDupes: function(x)
    787     {
    788         this._coalesceHistoryDupes = x;
    789     },
    790 
    791     /**
    792      * @param {Array.<string>} data
    793      */
    794     setHistoryData: function(data)
    795     {
    796         this._data = [].concat(data);
    797         this._historyOffset = 1;
    798     },
    799 
    800     /**
    801      * Pushes a committed text into the history.
    802      * @param {string} text
    803      */
    804     pushHistoryItem: function(text)
    805     {
    806         if (this._uncommittedIsTop) {
    807             this._data.pop();
    808             delete this._uncommittedIsTop;
    809         }
    810 
    811         this._historyOffset = 1;
    812         if (this._coalesceHistoryDupes && text === this._currentHistoryItem())
    813             return;
    814         this._data.push(text);
    815     },
    816 
    817     /**
    818      * Pushes the current (uncommitted) text into the history.
    819      */
    820     _pushCurrentText: function()
    821     {
    822         if (this._uncommittedIsTop)
    823             this._data.pop(); // Throw away obsolete uncommitted text.
    824         this._uncommittedIsTop = true;
    825         this.clearAutoComplete(true);
    826         this._data.push(this.text);
    827     },
    828 
    829     /**
    830      * @return {string|undefined}
    831      */
    832     _previous: function()
    833     {
    834         if (this._historyOffset > this._data.length)
    835             return undefined;
    836         if (this._historyOffset === 1)
    837             this._pushCurrentText();
    838         ++this._historyOffset;
    839         return this._currentHistoryItem();
    840     },
    841 
    842     /**
    843      * @return {string|undefined}
    844      */
    845     _next: function()
    846     {
    847         if (this._historyOffset === 1)
    848             return undefined;
    849         --this._historyOffset;
    850         return this._currentHistoryItem();
    851     },
    852 
    853     /**
    854      * @return {string|undefined}
    855      */
    856     _currentHistoryItem: function()
    857     {
    858         return this._data[this._data.length - this._historyOffset];
    859     },
    860 
    861     /**
    862      * @override
    863      */
    864     defaultKeyHandler: function(event, force)
    865     {
    866         var newText;
    867         var isPrevious;
    868 
    869         switch (event.keyIdentifier) {
    870         case "Up":
    871             if (!this.isCaretOnFirstLine())
    872                 break;
    873             newText = this._previous();
    874             isPrevious = true;
    875             break;
    876         case "Down":
    877             if (!this.isCaretOnLastLine())
    878                 break;
    879             newText = this._next();
    880             break;
    881         case "U+0050": // Ctrl+P = Previous
    882             if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
    883                 newText = this._previous();
    884                 isPrevious = true;
    885             }
    886             break;
    887         case "U+004E": // Ctrl+N = Next
    888             if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey)
    889                 newText = this._next();
    890             break;
    891         }
    892 
    893         if (newText !== undefined) {
    894             event.consume(true);
    895             this.text = newText;
    896 
    897             if (isPrevious) {
    898                 var firstNewlineIndex = this.text.indexOf("\n");
    899                 if (firstNewlineIndex === -1)
    900                     this.moveCaretToEndOfPrompt();
    901                 else {
    902                     var selection = window.getSelection();
    903                     var selectionRange = document.createRange();
    904 
    905                     selectionRange.setStart(this._element.firstChild, firstNewlineIndex);
    906                     selectionRange.setEnd(this._element.firstChild, firstNewlineIndex);
    907 
    908                     selection.removeAllRanges();
    909                     selection.addRange(selectionRange);
    910                 }
    911             }
    912 
    913             return true;
    914         }
    915 
    916         return WebInspector.TextPrompt.prototype.defaultKeyHandler.apply(this, arguments);
    917     },
    918 
    919     __proto__: WebInspector.TextPrompt.prototype
    920 }
    921 
    922