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