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 */
37WebInspector.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
48WebInspector.TextPrompt.Events = {
49    ItemApplied: "text-prompt-item-applied",
50    ItemAccepted: "text-prompt-item-accepted"
51};
52
53WebInspector.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.autoCompleteElement || !this.autoCompleteElement.parentNode)
573            return false;
574
575        var text = this.autoCompleteElement.textContent;
576        var textNode = document.createTextNode(text);
577        this.autoCompleteElement.parentNode.replaceChild(textNode, this.autoCompleteElement);
578        delete this.autoCompleteElement;
579
580        var finalSelectionRange = document.createRange();
581        finalSelectionRange.setStart(textNode, text.length);
582        finalSelectionRange.setEnd(textNode, text.length);
583
584        var selection = window.getSelection();
585        selection.removeAllRanges();
586        selection.addRange(finalSelectionRange);
587
588        if (!prefixAccepted) {
589            this.hideSuggestBox();
590            this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemAccepted);
591        } else
592            this.autoCompleteSoon(true);
593
594        return true;
595    },
596
597    hideSuggestBox: function()
598    {
599        if (this.isSuggestBoxVisible())
600            this._suggestBox.hide();
601    },
602
603    /**
604     * @return {boolean}
605     */
606    isSuggestBoxVisible: function()
607    {
608        return this._suggestBox && this._suggestBox.visible();
609    },
610
611    /**
612     * @return {boolean}
613     */
614    isCaretInsidePrompt: function()
615    {
616        return this._element.isInsertionCaretInside();
617    },
618
619    /**
620     * @return {boolean}
621     */
622    isCaretAtEndOfPrompt: function()
623    {
624        var selection = window.getSelection();
625        if (!selection.rangeCount || !selection.isCollapsed)
626            return false;
627
628        var selectionRange = selection.getRangeAt(0);
629        var node = selectionRange.startContainer;
630        if (!node.isSelfOrDescendant(this._element))
631            return false;
632
633        if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length)
634            return false;
635
636        var foundNextText = false;
637        while (node) {
638            if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) {
639                if (foundNextText && (!this.autoCompleteElement || !this.autoCompleteElement.isAncestor(node)))
640                    return false;
641                foundNextText = true;
642            }
643
644            node = node.traverseNextNode(this._element);
645        }
646
647        return true;
648    },
649
650    /**
651     * @return {boolean}
652     */
653    isCaretOnFirstLine: function()
654    {
655        var selection = window.getSelection();
656        var focusNode = selection.focusNode;
657        if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element)
658            return true;
659
660        if (focusNode.textContent.substring(0, selection.focusOffset).indexOf("\n") !== -1)
661            return false;
662        focusNode = focusNode.previousSibling;
663
664        while (focusNode) {
665            if (focusNode.nodeType !== Node.TEXT_NODE)
666                return true;
667            if (focusNode.textContent.indexOf("\n") !== -1)
668                return false;
669            focusNode = focusNode.previousSibling;
670        }
671
672        return true;
673    },
674
675    /**
676     * @return {boolean}
677     */
678    isCaretOnLastLine: function()
679    {
680        var selection = window.getSelection();
681        var focusNode = selection.focusNode;
682        if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element)
683            return true;
684
685        if (focusNode.textContent.substring(selection.focusOffset).indexOf("\n") !== -1)
686            return false;
687        focusNode = focusNode.nextSibling;
688
689        while (focusNode) {
690            if (focusNode.nodeType !== Node.TEXT_NODE)
691                return true;
692            if (focusNode.textContent.indexOf("\n") !== -1)
693                return false;
694            focusNode = focusNode.nextSibling;
695        }
696
697        return true;
698    },
699
700    moveCaretToEndOfPrompt: function()
701    {
702        var selection = window.getSelection();
703        var selectionRange = document.createRange();
704
705        var offset = this._element.childNodes.length;
706        selectionRange.setStart(this._element, offset);
707        selectionRange.setEnd(this._element, offset);
708
709        selection.removeAllRanges();
710        selection.addRange(selectionRange);
711    },
712
713    /**
714     * @param {!Event} event
715     * @return {boolean}
716     */
717    tabKeyPressed: function(event)
718    {
719        this._completeCommonPrefix();
720
721        // Consume the key.
722        return true;
723    },
724
725    __proto__: WebInspector.Object.prototype
726}
727
728
729/**
730 * @constructor
731 * @extends {WebInspector.TextPrompt}
732 * @param {function(!Element, !Range, boolean, function(!Array.<string>, number=))} completions
733 * @param {string=} stopCharacters
734 */
735WebInspector.TextPromptWithHistory = function(completions, stopCharacters)
736{
737    WebInspector.TextPrompt.call(this, completions, stopCharacters);
738
739    /**
740     * @type {!Array.<string>}
741     */
742    this._data = [];
743
744    /**
745     * 1-based entry in the history stack.
746     * @type {number}
747     */
748    this._historyOffset = 1;
749
750    /**
751     * Whether to coalesce duplicate items in the history, default is true.
752     * @type {boolean}
753     */
754    this._coalesceHistoryDupes = true;
755}
756
757WebInspector.TextPromptWithHistory.prototype = {
758    /**
759     * @return {!Array.<string>}
760     */
761    get historyData()
762    {
763        // FIXME: do we need to copy this?
764        return this._data;
765    },
766
767    /**
768     * @param {boolean} x
769     */
770    setCoalesceHistoryDupes: function(x)
771    {
772        this._coalesceHistoryDupes = x;
773    },
774
775    /**
776     * @param {!Array.<string>} data
777     */
778    setHistoryData: function(data)
779    {
780        this._data = [].concat(data);
781        this._historyOffset = 1;
782    },
783
784    /**
785     * Pushes a committed text into the history.
786     * @param {string} text
787     */
788    pushHistoryItem: function(text)
789    {
790        if (this._uncommittedIsTop) {
791            this._data.pop();
792            delete this._uncommittedIsTop;
793        }
794
795        this._historyOffset = 1;
796        if (this._coalesceHistoryDupes && text === this._currentHistoryItem())
797            return;
798        this._data.push(text);
799    },
800
801    /**
802     * Pushes the current (uncommitted) text into the history.
803     */
804    _pushCurrentText: function()
805    {
806        if (this._uncommittedIsTop)
807            this._data.pop(); // Throw away obsolete uncommitted text.
808        this._uncommittedIsTop = true;
809        this.clearAutoComplete(true);
810        this._data.push(this.text);
811    },
812
813    /**
814     * @return {string|undefined}
815     */
816    _previous: function()
817    {
818        if (this._historyOffset > this._data.length)
819            return undefined;
820        if (this._historyOffset === 1)
821            this._pushCurrentText();
822        ++this._historyOffset;
823        return this._currentHistoryItem();
824    },
825
826    /**
827     * @return {string|undefined}
828     */
829    _next: function()
830    {
831        if (this._historyOffset === 1)
832            return undefined;
833        --this._historyOffset;
834        return this._currentHistoryItem();
835    },
836
837    /**
838     * @return {string|undefined}
839     */
840    _currentHistoryItem: function()
841    {
842        return this._data[this._data.length - this._historyOffset];
843    },
844
845    /**
846     * @override
847     */
848    onKeyDown: function(event)
849    {
850        var newText;
851        var isPrevious;
852
853        switch (event.keyIdentifier) {
854        case "Up":
855            if (!this.isCaretOnFirstLine() || this.isSuggestBoxVisible())
856                break;
857            newText = this._previous();
858            isPrevious = true;
859            break;
860        case "Down":
861            if (!this.isCaretOnLastLine() || this.isSuggestBoxVisible())
862                break;
863            newText = this._next();
864            break;
865        case "U+0050": // Ctrl+P = Previous
866            if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
867                newText = this._previous();
868                isPrevious = true;
869            }
870            break;
871        case "U+004E": // Ctrl+N = Next
872            if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey)
873                newText = this._next();
874            break;
875        }
876
877        if (newText !== undefined) {
878            event.consume(true);
879            this.text = newText;
880
881            if (isPrevious) {
882                var firstNewlineIndex = this.text.indexOf("\n");
883                if (firstNewlineIndex === -1)
884                    this.moveCaretToEndOfPrompt();
885                else {
886                    var selection = window.getSelection();
887                    var selectionRange = document.createRange();
888
889                    selectionRange.setStart(this._element.firstChild, firstNewlineIndex);
890                    selectionRange.setEnd(this._element.firstChild, firstNewlineIndex);
891
892                    selection.removeAllRanges();
893                    selection.addRange(selectionRange);
894                }
895            }
896
897            return;
898        }
899
900        WebInspector.TextPrompt.prototype.onKeyDown.apply(this, arguments);
901    },
902
903    __proto__: WebInspector.TextPrompt.prototype
904}
905
906