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