TextPrompt.js 28 KB

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