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