• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.appendChild(document.createElement("br"));
161        } else
162            this._element.textContent = x;
163
164        this.moveCaretToEndOfPrompt();
165        this._element.scrollIntoView();
166    },
167
168    _removeFromElement: function()
169    {
170        this.clearAutoComplete(true);
171        this._element.removeEventListener("keydown", this._boundOnKeyDown, false);
172        this._element.removeEventListener("input", this._boundOnInput, false);
173        this._element.removeEventListener("selectstart", this._boundSelectStart, false);
174        this._element.removeEventListener("blur", this._boundRemoveSuggestionAids, 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     */
240    _updateAutoComplete: function(force)
241    {
242        this.clearAutoComplete();
243        this.autoCompleteSoon(force);
244    },
245
246    /**
247     * @param {?Event} event
248     */
249    onMouseWheel: function(event)
250    {
251        // Subclasses can implement.
252    },
253
254    /**
255     * @param {?Event} event
256     */
257    onKeyDown: function(event)
258    {
259        var handled = false;
260        delete this._needUpdateAutocomplete;
261
262        switch (event.keyIdentifier) {
263        case "U+0009": // Tab
264            handled = this.tabKeyPressed(event);
265            break;
266        case "Left":
267        case "Home":
268            this._removeSuggestionAids();
269            break;
270        case "Right":
271        case "End":
272            if (this.isCaretAtEndOfPrompt())
273                handled = this.acceptAutoComplete();
274            else
275                this._removeSuggestionAids();
276            break;
277        case "U+001B": // Esc
278            if (this.isSuggestBoxVisible()) {
279                this._removeSuggestionAids();
280                handled = true;
281            }
282            break;
283        case "U+0020": // Space
284            if (event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
285                this._updateAutoComplete(true);
286                handled = true;
287            }
288            break;
289        case "Alt":
290        case "Meta":
291        case "Shift":
292        case "Control":
293            break;
294        }
295
296        if (!handled && this.isSuggestBoxVisible())
297            handled = this._suggestBox.keyPressed(event);
298
299        if (!handled)
300            this._needUpdateAutocomplete = true;
301
302        if (handled)
303            event.consume(true);
304    },
305
306    /**
307     * @param {?Event} event
308     */
309    onInput: function(event)
310    {
311        if (this._needUpdateAutocomplete)
312            this._updateAutoComplete();
313    },
314
315    /**
316     * @return {boolean}
317     */
318    acceptAutoComplete: function()
319    {
320        var result = false;
321        if (this.isSuggestBoxVisible())
322            result = this._suggestBox.acceptSuggestion();
323        if (!result)
324            result = this._acceptSuggestionInternal();
325
326        return result;
327    },
328
329    /**
330     * @param {boolean=} includeTimeout
331     */
332    clearAutoComplete: function(includeTimeout)
333    {
334        if (includeTimeout && this._completeTimeout) {
335            clearTimeout(this._completeTimeout);
336            delete this._completeTimeout;
337        }
338        delete this._waitingForCompletions;
339
340        if (!this.autoCompleteElement)
341            return;
342
343        this.autoCompleteElement.remove();
344        delete this.autoCompleteElement;
345        delete this._userEnteredRange;
346        delete this._userEnteredText;
347    },
348
349    /**
350     * @param {boolean=} force
351     */
352    autoCompleteSoon: function(force)
353    {
354        var immediately = this.isSuggestBoxVisible() || force;
355        if (!this._completeTimeout)
356            this._completeTimeout = setTimeout(this.complete.bind(this, force), immediately ? 0 : 250);
357    },
358
359    /**
360     * @param {boolean=} force
361     * @param {boolean=} reverse
362     */
363    complete: function(force, reverse)
364    {
365        this.clearAutoComplete(true);
366        var selection = window.getSelection();
367        if (!selection.rangeCount)
368            return;
369
370        var selectionRange = selection.getRangeAt(0);
371        var shouldExit;
372
373        if (!force && !this.isCaretAtEndOfPrompt() && !this.isSuggestBoxVisible())
374            shouldExit = true;
375        else if (!selection.isCollapsed)
376            shouldExit = true;
377        else if (!force) {
378            // BUG72018: Do not show suggest box if caret is followed by a non-stop character.
379            var wordSuffixRange = selectionRange.startContainer.rangeOfWord(selectionRange.endOffset, this._completionStopCharacters, this._element, "forward");
380            if (wordSuffixRange.toString().length)
381                shouldExit = true;
382        }
383        if (shouldExit) {
384            this.hideSuggestBox();
385            return;
386        }
387
388        var wordPrefixRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, this._completionStopCharacters, this._element, "backward");
389        this._waitingForCompletions = true;
390        this._loadCompletions(this.proxyElement, wordPrefixRange, force || false, this._completionsReady.bind(this, selection, wordPrefixRange, !!reverse));
391    },
392
393    disableDefaultSuggestionForEmptyInput: function()
394    {
395        this._disableDefaultSuggestionForEmptyInput = true;
396    },
397
398    /**
399     * @param {!Selection} selection
400     * @param {!Range} textRange
401     */
402    _boxForAnchorAtStart: function(selection, textRange)
403    {
404        var rangeCopy = selection.getRangeAt(0).cloneRange();
405        var anchorElement = document.createElement("span");
406        anchorElement.textContent = "\u200B";
407        textRange.insertNode(anchorElement);
408        var box = anchorElement.boxInWindow(window);
409        anchorElement.remove();
410        selection.removeAllRanges();
411        selection.addRange(rangeCopy);
412        return box;
413    },
414
415    /**
416     * @param {!Array.<string>} completions
417     * @param {number} wordPrefixLength
418     */
419    _buildCommonPrefix: function(completions, wordPrefixLength)
420    {
421        var commonPrefix = completions[0];
422        for (var i = 0; i < completions.length; ++i) {
423            var completion = completions[i];
424            var lastIndex = Math.min(commonPrefix.length, completion.length);
425            for (var j = wordPrefixLength; j < lastIndex; ++j) {
426                if (commonPrefix[j] !== completion[j]) {
427                    commonPrefix = commonPrefix.substr(0, j);
428                    break;
429                }
430            }
431        }
432        return commonPrefix;
433    },
434
435    /**
436     * @param {!Selection} selection
437     * @param {!Range} originalWordPrefixRange
438     * @param {boolean} reverse
439     * @param {!Array.<string>} completions
440     * @param {number=} selectedIndex
441     */
442    _completionsReady: function(selection, originalWordPrefixRange, reverse, completions, selectedIndex)
443    {
444        if (!this._waitingForCompletions || !completions.length) {
445            this.hideSuggestBox();
446            return;
447        }
448        delete this._waitingForCompletions;
449
450        var selectionRange = selection.getRangeAt(0);
451
452        var fullWordRange = document.createRange();
453        fullWordRange.setStart(originalWordPrefixRange.startContainer, originalWordPrefixRange.startOffset);
454        fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset);
455
456        if (originalWordPrefixRange.toString() + selectionRange.toString() !== fullWordRange.toString())
457            return;
458
459        selectedIndex = (this._disableDefaultSuggestionForEmptyInput && !this.text) ? -1 : (selectedIndex || 0);
460
461        this._userEnteredRange = fullWordRange;
462        this._userEnteredText = fullWordRange.toString();
463
464        if (this._suggestBox)
465            this._suggestBox.updateSuggestions(this._boxForAnchorAtStart(selection, fullWordRange), completions, selectedIndex, !this.isCaretAtEndOfPrompt(), this._userEnteredText);
466
467        if (selectedIndex === -1)
468            return;
469
470        var wordPrefixLength = originalWordPrefixRange.toString().length;
471        this._commonPrefix = this._buildCommonPrefix(completions, wordPrefixLength);
472
473        if (this.isCaretAtEndOfPrompt()) {
474            this._userEnteredRange.deleteContents();
475            this._element.normalize();
476            var finalSelectionRange = document.createRange();
477            var completionText = completions[selectedIndex];
478            var prefixText = completionText.substring(0, wordPrefixLength);
479            var suffixText = completionText.substring(wordPrefixLength);
480
481            var prefixTextNode = document.createTextNode(prefixText);
482            fullWordRange.insertNode(prefixTextNode);
483
484            this.autoCompleteElement = document.createElement("span");
485            this.autoCompleteElement.className = "auto-complete-text";
486            this.autoCompleteElement.textContent = suffixText;
487
488            prefixTextNode.parentNode.insertBefore(this.autoCompleteElement, prefixTextNode.nextSibling);
489
490            finalSelectionRange.setStart(prefixTextNode, wordPrefixLength);
491            finalSelectionRange.setEnd(prefixTextNode, wordPrefixLength);
492            selection.removeAllRanges();
493            selection.addRange(finalSelectionRange);
494            this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApplied);
495        }
496    },
497
498    _completeCommonPrefix: function()
499    {
500        if (!this.autoCompleteElement || !this._commonPrefix || !this._userEnteredText || !this._commonPrefix.startsWith(this._userEnteredText))
501            return;
502
503        if (!this.isSuggestBoxVisible()) {
504            this.acceptAutoComplete();
505            return;
506        }
507
508        this.autoCompleteElement.textContent = this._commonPrefix.substring(this._userEnteredText.length);
509        this._acceptSuggestionInternal(true);
510    },
511
512    /**
513     * @param {string} completionText
514     * @param {boolean=} isIntermediateSuggestion
515     */
516    applySuggestion: function(completionText, isIntermediateSuggestion)
517    {
518        this._applySuggestion(completionText, isIntermediateSuggestion);
519    },
520
521    /**
522     * @param {string} completionText
523     * @param {boolean=} isIntermediateSuggestion
524     * @param {!Range=} originalPrefixRange
525     */
526    _applySuggestion: function(completionText, isIntermediateSuggestion, originalPrefixRange)
527    {
528        var wordPrefixLength;
529        if (originalPrefixRange)
530            wordPrefixLength = originalPrefixRange.toString().length;
531        else
532            wordPrefixLength = this._userEnteredText ? this._userEnteredText.length : 0;
533
534        this._userEnteredRange.deleteContents();
535        this._element.normalize();
536        var finalSelectionRange = document.createRange();
537        var completionTextNode = document.createTextNode(completionText);
538        this._userEnteredRange.insertNode(completionTextNode);
539        if (this.autoCompleteElement) {
540            this.autoCompleteElement.remove();
541            delete this.autoCompleteElement;
542        }
543
544        if (isIntermediateSuggestion)
545            finalSelectionRange.setStart(completionTextNode, wordPrefixLength);
546        else
547            finalSelectionRange.setStart(completionTextNode, completionText.length);
548
549        finalSelectionRange.setEnd(completionTextNode, completionText.length);
550
551        var selection = window.getSelection();
552        selection.removeAllRanges();
553        selection.addRange(finalSelectionRange);
554        if (isIntermediateSuggestion)
555            this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApplied, { itemText: completionText });
556    },
557
558    /**
559     * @override
560     */
561    acceptSuggestion: function()
562    {
563        this._acceptSuggestionInternal();
564    },
565
566    /**
567     * @param {boolean=} prefixAccepted
568     * @return {boolean}
569     */
570    _acceptSuggestionInternal: function(prefixAccepted)
571    {
572        if (this._isAcceptingSuggestion)
573            return false;
574
575        if (!this.autoCompleteElement || !this.autoCompleteElement.parentNode)
576            return false;
577
578        var text = this.autoCompleteElement.textContent;
579        var textNode = document.createTextNode(text);
580        this.autoCompleteElement.parentNode.replaceChild(textNode, this.autoCompleteElement);
581        delete this.autoCompleteElement;
582
583        var finalSelectionRange = document.createRange();
584        finalSelectionRange.setStart(textNode, text.length);
585        finalSelectionRange.setEnd(textNode, text.length);
586
587        var selection = window.getSelection();
588        selection.removeAllRanges();
589        selection.addRange(finalSelectionRange);
590
591        if (!prefixAccepted) {
592            this.hideSuggestBox();
593            this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemAccepted);
594        } else
595            this.autoCompleteSoon(true);
596
597        return true;
598    },
599
600    hideSuggestBox: function()
601    {
602        if (this.isSuggestBoxVisible())
603            this._suggestBox.hide();
604    },
605
606    /**
607     * @return {boolean}
608     */
609    isSuggestBoxVisible: function()
610    {
611        return this._suggestBox && this._suggestBox.visible();
612    },
613
614    /**
615     * @return {boolean}
616     */
617    isCaretInsidePrompt: function()
618    {
619        return this._element.isInsertionCaretInside();
620    },
621
622    /**
623     * @return {boolean}
624     */
625    isCaretAtEndOfPrompt: function()
626    {
627        var selection = window.getSelection();
628        if (!selection.rangeCount || !selection.isCollapsed)
629            return false;
630
631        var selectionRange = selection.getRangeAt(0);
632        var node = selectionRange.startContainer;
633        if (!node.isSelfOrDescendant(this._element))
634            return false;
635
636        if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length)
637            return false;
638
639        var foundNextText = false;
640        while (node) {
641            if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) {
642                if (foundNextText && (!this.autoCompleteElement || !this.autoCompleteElement.isAncestor(node)))
643                    return false;
644                foundNextText = true;
645            }
646
647            node = node.traverseNextNode(this._element);
648        }
649
650        return true;
651    },
652
653    /**
654     * @return {boolean}
655     */
656    isCaretOnFirstLine: function()
657    {
658        var selection = window.getSelection();
659        var focusNode = selection.focusNode;
660        if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element)
661            return true;
662
663        if (focusNode.textContent.substring(0, selection.focusOffset).indexOf("\n") !== -1)
664            return false;
665        focusNode = focusNode.previousSibling;
666
667        while (focusNode) {
668            if (focusNode.nodeType !== Node.TEXT_NODE)
669                return true;
670            if (focusNode.textContent.indexOf("\n") !== -1)
671                return false;
672            focusNode = focusNode.previousSibling;
673        }
674
675        return true;
676    },
677
678    /**
679     * @return {boolean}
680     */
681    isCaretOnLastLine: function()
682    {
683        var selection = window.getSelection();
684        var focusNode = selection.focusNode;
685        if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element)
686            return true;
687
688        if (focusNode.textContent.substring(selection.focusOffset).indexOf("\n") !== -1)
689            return false;
690        focusNode = focusNode.nextSibling;
691
692        while (focusNode) {
693            if (focusNode.nodeType !== Node.TEXT_NODE)
694                return true;
695            if (focusNode.textContent.indexOf("\n") !== -1)
696                return false;
697            focusNode = focusNode.nextSibling;
698        }
699
700        return true;
701    },
702
703    moveCaretToEndOfPrompt: function()
704    {
705        var selection = window.getSelection();
706        var selectionRange = document.createRange();
707
708        var offset = this._element.childNodes.length;
709        selectionRange.setStart(this._element, offset);
710        selectionRange.setEnd(this._element, offset);
711
712        selection.removeAllRanges();
713        selection.addRange(selectionRange);
714    },
715
716    /**
717     * @param {!Event} event
718     * @return {boolean}
719     */
720    tabKeyPressed: function(event)
721    {
722        this._completeCommonPrefix();
723
724        // Consume the key.
725        return true;
726    },
727
728    __proto__: WebInspector.Object.prototype
729}
730
731
732/**
733 * @constructor
734 * @extends {WebInspector.TextPrompt}
735 * @param {function(!Element, !Range, boolean, function(!Array.<string>, number=))} completions
736 * @param {string=} stopCharacters
737 */
738WebInspector.TextPromptWithHistory = function(completions, stopCharacters)
739{
740    WebInspector.TextPrompt.call(this, completions, stopCharacters);
741
742    /**
743     * @type {!Array.<string>}
744     */
745    this._data = [];
746
747    /**
748     * 1-based entry in the history stack.
749     * @type {number}
750     */
751    this._historyOffset = 1;
752
753    /**
754     * Whether to coalesce duplicate items in the history, default is true.
755     * @type {boolean}
756     */
757    this._coalesceHistoryDupes = true;
758}
759
760WebInspector.TextPromptWithHistory.prototype = {
761    /**
762     * @return {!Array.<string>}
763     */
764    get historyData()
765    {
766        // FIXME: do we need to copy this?
767        return this._data;
768    },
769
770    /**
771     * @param {boolean} x
772     */
773    setCoalesceHistoryDupes: function(x)
774    {
775        this._coalesceHistoryDupes = x;
776    },
777
778    /**
779     * @param {!Array.<string>} data
780     */
781    setHistoryData: function(data)
782    {
783        this._data = [].concat(data);
784        this._historyOffset = 1;
785    },
786
787    /**
788     * Pushes a committed text into the history.
789     * @param {string} text
790     */
791    pushHistoryItem: function(text)
792    {
793        if (this._uncommittedIsTop) {
794            this._data.pop();
795            delete this._uncommittedIsTop;
796        }
797
798        this._historyOffset = 1;
799        if (this._coalesceHistoryDupes && text === this._currentHistoryItem())
800            return;
801        this._data.push(text);
802    },
803
804    /**
805     * Pushes the current (uncommitted) text into the history.
806     */
807    _pushCurrentText: function()
808    {
809        if (this._uncommittedIsTop)
810            this._data.pop(); // Throw away obsolete uncommitted text.
811        this._uncommittedIsTop = true;
812        this.clearAutoComplete(true);
813        this._data.push(this.text);
814    },
815
816    /**
817     * @return {string|undefined}
818     */
819    _previous: function()
820    {
821        if (this._historyOffset > this._data.length)
822            return undefined;
823        if (this._historyOffset === 1)
824            this._pushCurrentText();
825        ++this._historyOffset;
826        return this._currentHistoryItem();
827    },
828
829    /**
830     * @return {string|undefined}
831     */
832    _next: function()
833    {
834        if (this._historyOffset === 1)
835            return undefined;
836        --this._historyOffset;
837        return this._currentHistoryItem();
838    },
839
840    /**
841     * @return {string|undefined}
842     */
843    _currentHistoryItem: function()
844    {
845        return this._data[this._data.length - this._historyOffset];
846    },
847
848    /**
849     * @override
850     */
851    onKeyDown: function(event)
852    {
853        var newText;
854        var isPrevious;
855
856        switch (event.keyIdentifier) {
857        case "Up":
858            if (!this.isCaretOnFirstLine() || this.isSuggestBoxVisible())
859                break;
860            newText = this._previous();
861            isPrevious = true;
862            break;
863        case "Down":
864            if (!this.isCaretOnLastLine() || this.isSuggestBoxVisible())
865                break;
866            newText = this._next();
867            break;
868        case "U+0050": // Ctrl+P = Previous
869            if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
870                newText = this._previous();
871                isPrevious = true;
872            }
873            break;
874        case "U+004E": // Ctrl+N = Next
875            if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey)
876                newText = this._next();
877            break;
878        }
879
880        if (newText !== undefined) {
881            event.consume(true);
882            this.text = newText;
883
884            if (isPrevious) {
885                var firstNewlineIndex = this.text.indexOf("\n");
886                if (firstNewlineIndex === -1)
887                    this.moveCaretToEndOfPrompt();
888                else {
889                    var selection = window.getSelection();
890                    var selectionRange = document.createRange();
891
892                    selectionRange.setStart(this._element.firstChild, firstNewlineIndex);
893                    selectionRange.setEnd(this._element.firstChild, firstNewlineIndex);
894
895                    selection.removeAllRanges();
896                    selection.addRange(selectionRange);
897                }
898            }
899
900            return;
901        }
902
903        WebInspector.TextPrompt.prototype.onKeyDown.apply(this, arguments);
904    },
905
906    __proto__: WebInspector.TextPrompt.prototype
907}
908
909