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