2 * Copyright (C) 2008 Apple Inc. All rights reserved.
3 * Copyright (C) 2011 Google Inc. All rights reserved.
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
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.
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.
32 * @extends {WebInspector.Object}
33 * @implements {WebInspector.SuggestBoxDelegate}
34 * @param {function(!Element, !Range, boolean, function(!Array.<string>, number=))} completions
35 * @param {string=} stopCharacters
37 WebInspector.TextPrompt = function(completions, stopCharacters)
40 * @type {!Element|undefined}
43 this._proxyElementDisplay = "inline-block";
44 this._loadCompletions = completions;
45 this._completionStopCharacters = stopCharacters || " =:[({;,!+-*/&|^<>.";
48 WebInspector.TextPrompt.Events = {
49 ItemApplied: "text-prompt-item-applied",
50 ItemAccepted: "text-prompt-item-accepted"
53 WebInspector.TextPrompt.prototype = {
56 return this._proxyElement;
60 * @param {boolean} suggestBoxEnabled
62 setSuggestBoxEnabled: function(suggestBoxEnabled)
64 this._suggestBoxEnabled = suggestBoxEnabled;
67 renderAsBlock: function()
69 this._proxyElementDisplay = "block";
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.
76 * @param {!Element} element
79 attach: function(element)
81 return this._attachInternal(element);
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.)
90 * @param {!Element} element
91 * @param {function(!Event)} blurListener
94 attachAndStartEditing: function(element, blurListener)
96 this._attachInternal(element);
97 this._startEditing(blurListener);
98 return this.proxyElement;
102 * @param {!Element} element
105 _attachInternal: function(element)
107 if (this.proxyElement)
108 throw "Cannot attach an attached TextPrompt";
109 this._element = element;
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);
127 if (this._suggestBoxEnabled)
128 this._suggestBox = new WebInspector.SuggestBox(this);
130 return this.proxyElement;
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);
148 return this._element.textContent;
156 this._removeSuggestionAids();
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");
162 this._element.textContent = x;
165 this.moveCaretToEndOfPrompt();
166 this._element.scrollIntoView();
169 _removeFromElement: function()
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);
178 if (this._suggestBox)
179 this._suggestBox.removeFromElement();
183 * @param {function(!Event)=} blurListener
185 _startEditing: function(blurListener)
187 this._isEditing = true;
188 this._element.classList.add("editing");
190 this._blurListener = blurListener;
191 this._element.addEventListener("blur", this._blurListener, false);
193 this._oldTabIndex = this._element.tabIndex;
194 if (this._element.tabIndex < 0)
195 this._element.tabIndex = 0;
196 WebInspector.setCurrentFocusElement(this._element);
198 this._updateAutoComplete();
201 _stopEditing: function()
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;
210 _removeSuggestionAids: function()
212 this.clearAutoComplete();
213 this.hideSuggestBox();
216 _selectStart: function()
218 if (this._selectionTimeout)
219 clearTimeout(this._selectionTimeout);
221 this._removeSuggestionAids();
224 * @this {WebInspector.TextPrompt}
226 function moveBackIfOutside()
228 delete this._selectionTimeout;
229 if (!this.isCaretInsidePrompt() && window.getSelection().isCollapsed) {
230 this.moveCaretToEndOfPrompt();
231 this.autoCompleteSoon();
235 this._selectionTimeout = setTimeout(moveBackIfOutside.bind(this), 100);
239 * @param {boolean=} force
241 _updateAutoComplete: function(force)
243 this.clearAutoComplete();
244 this.autoCompleteSoon(force);
248 * @param {!Event} event
250 onMouseWheel: function(event)
252 // Subclasses can implement.
256 * @param {!Event} event
258 onKeyDown: function(event)
261 delete this._needUpdateAutocomplete;
263 switch (event.keyIdentifier) {
264 case "U+0009": // Tab
265 handled = this.tabKeyPressed(event);
269 this._removeSuggestionAids();
273 if (this.isCaretAtEndOfPrompt())
274 handled = this.acceptAutoComplete();
276 this._removeSuggestionAids();
278 case "U+001B": // Esc
279 if (this.isSuggestBoxVisible()) {
280 this._removeSuggestionAids();
284 case "U+0020": // Space
285 if (event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
286 this._updateAutoComplete(true);
297 if (!handled && this.isSuggestBoxVisible())
298 handled = this._suggestBox.keyPressed(event);
301 this._needUpdateAutocomplete = true;
308 * @param {!Event} event
310 onInput: function(event)
312 if (this._needUpdateAutocomplete)
313 this._updateAutoComplete();
319 acceptAutoComplete: function()
322 if (this.isSuggestBoxVisible())
323 result = this._suggestBox.acceptSuggestion();
325 result = this._acceptSuggestionInternal();
331 * @param {boolean=} includeTimeout
333 clearAutoComplete: function(includeTimeout)
335 if (includeTimeout && this._completeTimeout) {
336 clearTimeout(this._completeTimeout);
337 delete this._completeTimeout;
339 delete this._waitingForCompletions;
341 if (!this.autoCompleteElement)
344 this.autoCompleteElement.remove();
345 delete this.autoCompleteElement;
346 delete this._userEnteredRange;
347 delete this._userEnteredText;
351 * @param {boolean=} force
353 autoCompleteSoon: function(force)
355 var immediately = this.isSuggestBoxVisible() || force;
356 if (!this._completeTimeout)
357 this._completeTimeout = setTimeout(this.complete.bind(this, force), immediately ? 0 : 250);
361 * @param {boolean=} force
362 * @param {boolean=} reverse
364 complete: function(force, reverse)
366 this.clearAutoComplete(true);
367 var selection = window.getSelection();
368 if (!selection.rangeCount)
371 var selectionRange = selection.getRangeAt(0);
374 if (!force && !this.isCaretAtEndOfPrompt() && !this.isSuggestBoxVisible())
376 else if (!selection.isCollapsed)
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)
385 this.hideSuggestBox();
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));
394 disableDefaultSuggestionForEmptyInput: function()
396 this._disableDefaultSuggestionForEmptyInput = true;
400 * @param {!Selection} selection
401 * @param {!Range} textRange
403 _boxForAnchorAtStart: function(selection, textRange)
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);
417 * @param {!Array.<string>} completions
418 * @param {number} wordPrefixLength
420 _buildCommonPrefix: function(completions, wordPrefixLength)
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);
437 * @param {!Selection} selection
438 * @param {!Range} originalWordPrefixRange
439 * @param {boolean} reverse
440 * @param {!Array.<string>} completions
441 * @param {number=} selectedIndex
443 _completionsReady: function(selection, originalWordPrefixRange, reverse, completions, selectedIndex)
445 if (!this._waitingForCompletions || !completions.length) {
446 this.hideSuggestBox();
449 delete this._waitingForCompletions;
451 var selectionRange = selection.getRangeAt(0);
453 var fullWordRange = document.createRange();
454 fullWordRange.setStart(originalWordPrefixRange.startContainer, originalWordPrefixRange.startOffset);
455 fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset);
457 if (originalWordPrefixRange.toString() + selectionRange.toString() !== fullWordRange.toString())
460 selectedIndex = (this._disableDefaultSuggestionForEmptyInput && !this.text) ? -1 : (selectedIndex || 0);
462 this._userEnteredRange = fullWordRange;
463 this._userEnteredText = fullWordRange.toString();
465 if (this._suggestBox)
466 this._suggestBox.updateSuggestions(this._boxForAnchorAtStart(selection, fullWordRange), completions, selectedIndex, !this.isCaretAtEndOfPrompt(), this._userEnteredText);
468 if (selectedIndex === -1)
471 var wordPrefixLength = originalWordPrefixRange.toString().length;
472 this._commonPrefix = this._buildCommonPrefix(completions, wordPrefixLength);
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();
482 var prefixTextNode = document.createTextNode(prefixText);
483 fullWordRange.insertNode(prefixTextNode);
485 this.autoCompleteElement = document.createElementWithClass("span", "auto-complete-text");
486 this.autoCompleteElement.textContent = suffixText;
488 prefixTextNode.parentNode.insertBefore(this.autoCompleteElement, prefixTextNode.nextSibling);
490 finalSelectionRange.setStart(prefixTextNode, wordPrefixLength);
491 finalSelectionRange.setEnd(prefixTextNode, wordPrefixLength);
492 selection.removeAllRanges();
493 selection.addRange(finalSelectionRange);
494 this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApplied);
498 _completeCommonPrefix: function()
500 if (!this.autoCompleteElement || !this._commonPrefix || !this._userEnteredText || !this._commonPrefix.startsWith(this._userEnteredText))
503 if (!this.isSuggestBoxVisible()) {
504 this.acceptAutoComplete();
508 this.autoCompleteElement.textContent = this._commonPrefix.substring(this._userEnteredText.length);
509 this._acceptSuggestionInternal(true);
513 * @param {string} completionText
514 * @param {boolean=} isIntermediateSuggestion
516 applySuggestion: function(completionText, isIntermediateSuggestion)
518 this._applySuggestion(completionText, isIntermediateSuggestion);
522 * @param {string} completionText
523 * @param {boolean=} isIntermediateSuggestion
524 * @param {!Range=} originalPrefixRange
526 _applySuggestion: function(completionText, isIntermediateSuggestion, originalPrefixRange)
528 var wordPrefixLength;
529 if (originalPrefixRange)
530 wordPrefixLength = originalPrefixRange.toString().length;
532 wordPrefixLength = this._userEnteredText ? this._userEnteredText.length : 0;
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;
544 if (isIntermediateSuggestion)
545 finalSelectionRange.setStart(completionTextNode, wordPrefixLength);
547 finalSelectionRange.setStart(completionTextNode, completionText.length);
549 finalSelectionRange.setEnd(completionTextNode, completionText.length);
551 var selection = window.getSelection();
552 selection.removeAllRanges();
553 selection.addRange(finalSelectionRange);
554 if (isIntermediateSuggestion)
555 this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApplied, { itemText: completionText });
561 acceptSuggestion: function()
563 this._acceptSuggestionInternal();
567 * @param {boolean=} prefixAccepted
570 _acceptSuggestionInternal: function(prefixAccepted)
572 if (this._isAcceptingSuggestion)
575 if (!this.autoCompleteElement || !this.autoCompleteElement.parentNode)
578 var text = this.autoCompleteElement.textContent;
579 var textNode = document.createTextNode(text);
580 this.autoCompleteElement.parentNode.replaceChild(textNode, this.autoCompleteElement);
581 delete this.autoCompleteElement;
583 var finalSelectionRange = document.createRange();
584 finalSelectionRange.setStart(textNode, text.length);
585 finalSelectionRange.setEnd(textNode, text.length);
587 var selection = window.getSelection();
588 selection.removeAllRanges();
589 selection.addRange(finalSelectionRange);
591 if (!prefixAccepted) {
592 this.hideSuggestBox();
593 this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemAccepted);
595 this.autoCompleteSoon(true);
600 hideSuggestBox: function()
602 if (this.isSuggestBoxVisible())
603 this._suggestBox.hide();
609 isSuggestBoxVisible: function()
611 return this._suggestBox && this._suggestBox.visible();
617 isCaretInsidePrompt: function()
619 return this._element.isInsertionCaretInside();
625 isCaretAtEndOfPrompt: function()
627 var selection = window.getSelection();
628 if (!selection.rangeCount || !selection.isCollapsed)
631 var selectionRange = selection.getRangeAt(0);
632 var node = selectionRange.startContainer;
633 if (!node.isSelfOrDescendant(this._element))
636 if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length)
639 var foundNextText = false;
641 if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) {
642 if (foundNextText && (!this.autoCompleteElement || !this.autoCompleteElement.isAncestor(node)))
644 foundNextText = true;
647 node = node.traverseNextNode(this._element);
656 isCaretOnFirstLine: function()
658 var selection = window.getSelection();
659 var focusNode = selection.focusNode;
660 if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element)
663 if (focusNode.textContent.substring(0, selection.focusOffset).indexOf("\n") !== -1)
665 focusNode = focusNode.previousSibling;
668 if (focusNode.nodeType !== Node.TEXT_NODE)
670 if (focusNode.textContent.indexOf("\n") !== -1)
672 focusNode = focusNode.previousSibling;
681 isCaretOnLastLine: function()
683 var selection = window.getSelection();
684 var focusNode = selection.focusNode;
685 if (!focusNode || focusNode.nodeType !== Node.TEXT_NODE || focusNode.parentNode !== this._element)
688 if (focusNode.textContent.substring(selection.focusOffset).indexOf("\n") !== -1)
690 focusNode = focusNode.nextSibling;
693 if (focusNode.nodeType !== Node.TEXT_NODE)
695 if (focusNode.textContent.indexOf("\n") !== -1)
697 focusNode = focusNode.nextSibling;
703 moveCaretToEndOfPrompt: function()
705 var selection = window.getSelection();
706 var selectionRange = document.createRange();
708 var offset = this._element.childNodes.length;
709 selectionRange.setStart(this._element, offset);
710 selectionRange.setEnd(this._element, offset);
712 selection.removeAllRanges();
713 selection.addRange(selectionRange);
717 * @param {!Event} event
720 tabKeyPressed: function(event)
722 this._completeCommonPrefix();
728 __proto__: WebInspector.Object.prototype
734 * @extends {WebInspector.TextPrompt}
735 * @param {function(!Element, !Range, boolean, function(!Array.<string>, number=))} completions
736 * @param {string=} stopCharacters
738 WebInspector.TextPromptWithHistory = function(completions, stopCharacters)
740 WebInspector.TextPrompt.call(this, completions, stopCharacters);
743 * @type {!Array.<string>}
748 * 1-based entry in the history stack.
751 this._historyOffset = 1;
754 * Whether to coalesce duplicate items in the history, default is true.
757 this._coalesceHistoryDupes = true;
760 WebInspector.TextPromptWithHistory.prototype = {
762 * @return {!Array.<string>}
766 // FIXME: do we need to copy this?
773 setCoalesceHistoryDupes: function(x)
775 this._coalesceHistoryDupes = x;
779 * @param {!Array.<string>} data
781 setHistoryData: function(data)
783 this._data = [].concat(data);
784 this._historyOffset = 1;
788 * Pushes a committed text into the history.
789 * @param {string} text
791 pushHistoryItem: function(text)
793 if (this._uncommittedIsTop) {
795 delete this._uncommittedIsTop;
798 this._historyOffset = 1;
799 if (this._coalesceHistoryDupes && text === this._currentHistoryItem())
801 this._data.push(text);
805 * Pushes the current (uncommitted) text into the history.
807 _pushCurrentText: function()
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);
817 * @return {string|undefined}
819 _previous: function()
821 if (this._historyOffset > this._data.length)
823 if (this._historyOffset === 1)
824 this._pushCurrentText();
825 ++this._historyOffset;
826 return this._currentHistoryItem();
830 * @return {string|undefined}
834 if (this._historyOffset === 1)
836 --this._historyOffset;
837 return this._currentHistoryItem();
841 * @return {string|undefined}
843 _currentHistoryItem: function()
845 return this._data[this._data.length - this._historyOffset];
851 onKeyDown: function(event)
856 switch (event.keyIdentifier) {
858 if (!this.isCaretOnFirstLine() || this.isSuggestBoxVisible())
860 newText = this._previous();
864 if (!this.isCaretOnLastLine() || this.isSuggestBoxVisible())
866 newText = this._next();
868 case "U+0050": // Ctrl+P = Previous
869 if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
870 newText = this._previous();
874 case "U+004E": // Ctrl+N = Next
875 if (WebInspector.isMac() && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey)
876 newText = this._next();
880 if (newText !== undefined) {
885 var firstNewlineIndex = this.text.indexOf("\n");
886 if (firstNewlineIndex === -1)
887 this.moveCaretToEndOfPrompt();
889 var selection = window.getSelection();
890 var selectionRange = document.createRange();
892 selectionRange.setStart(this._element.firstChild, firstNewlineIndex);
893 selectionRange.setEnd(this._element.firstChild, firstNewlineIndex);
895 selection.removeAllRanges();
896 selection.addRange(selectionRange);
903 WebInspector.TextPrompt.prototype.onKeyDown.apply(this, arguments);
906 __proto__: WebInspector.TextPrompt.prototype