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