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