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