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