| OLD | NEW |
| 1 /* | 1 /* |
| 2 * Copyright (C) 2008 Apple Inc. All rights reserved. | 2 * Copyright (C) 2008 Apple Inc. All rights reserved. |
| 3 * Copyright (C) 2011 Google Inc. All rights reserved. | 3 * Copyright (C) 2011 Google Inc. All rights reserved. |
| 4 * | 4 * |
| 5 * Redistribution and use in source and binary forms, with or without | 5 * Redistribution and use in source and binary forms, with or without |
| 6 * modification, are permitted provided that the following conditions | 6 * modification, are permitted provided that the following conditions |
| 7 * are met: | 7 * are met: |
| 8 * | 8 * |
| 9 * 1. Redistributions of source code must retain the above copyright | 9 * 1. Redistributions of source code must retain the above copyright |
| 10 * notice, this list of conditions and the following disclaimer. | 10 * notice, this list of conditions and the following disclaimer. |
| 11 * 2. Redistributions in binary form must reproduce the above copyright | 11 * 2. Redistributions in binary form must reproduce the above copyright |
| 12 * notice, this list of conditions and the following disclaimer in the | 12 * notice, this list of conditions and the following disclaimer in the |
| 13 * documentation and/or other materials provided with the distribution. | 13 * documentation and/or other materials provided with the distribution. |
| 14 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of | 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 | 15 * its contributors may be used to endorse or promote products derived |
| 16 * from this software without specific prior written permission. | 16 * from this software without specific prior written permission. |
| 17 * | 17 * |
| 18 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY | 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 | 19 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| 20 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | 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 | 21 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| 22 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | 22 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| 23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | 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 | 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 | 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 | 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. | 27 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 28 */ | 28 */ |
| 29 | |
| 30 /** | 29 /** |
| 31 * @constructor | |
| 32 * @extends {WebInspector.Object} | |
| 33 * @implements {WebInspector.SuggestBoxDelegate} | 30 * @implements {WebInspector.SuggestBoxDelegate} |
| 31 * @unrestricted |
| 34 */ | 32 */ |
| 35 WebInspector.TextPrompt = function() | 33 WebInspector.TextPrompt = class extends WebInspector.Object { |
| 36 { | 34 constructor() { |
| 35 super(); |
| 37 /** | 36 /** |
| 38 * @type {!Element|undefined} | 37 * @type {!Element|undefined} |
| 39 */ | 38 */ |
| 40 this._proxyElement; | 39 this._proxyElement; |
| 41 this._proxyElementDisplay = "inline-block"; | 40 this._proxyElementDisplay = 'inline-block'; |
| 42 this._autocompletionTimeout = WebInspector.TextPrompt.DefaultAutocompletionT
imeout; | 41 this._autocompletionTimeout = WebInspector.TextPrompt.DefaultAutocompletionT
imeout; |
| 43 this._title = ""; | 42 this._title = ''; |
| 44 this._prefixRange = null; | 43 this._prefixRange = null; |
| 45 this._previousText = ""; | 44 this._previousText = ''; |
| 46 this._currentSuggestion = ""; | 45 this._currentSuggestion = ''; |
| 47 this._completionRequestId = 0; | 46 this._completionRequestId = 0; |
| 48 this._ghostTextElement = createElementWithClass("span", "auto-complete-text"
); | 47 this._ghostTextElement = createElementWithClass('span', 'auto-complete-text'
); |
| 48 } |
| 49 |
| 50 /** |
| 51 * @param {function(!Element, !Range, boolean, function(!Array.<string>, numbe
r=))} completions |
| 52 * @param {string=} stopCharacters |
| 53 */ |
| 54 initialize(completions, stopCharacters) { |
| 55 this._loadCompletions = completions; |
| 56 this._completionStopCharacters = stopCharacters || ' =:[({;,!+-*/&|^<>.'; |
| 57 } |
| 58 |
| 59 /** |
| 60 * @param {number} timeout |
| 61 */ |
| 62 setAutocompletionTimeout(timeout) { |
| 63 this._autocompletionTimeout = timeout; |
| 64 } |
| 65 |
| 66 /** |
| 67 * @param {boolean} suggestBoxEnabled |
| 68 */ |
| 69 setSuggestBoxEnabled(suggestBoxEnabled) { |
| 70 this._suggestBoxEnabled = suggestBoxEnabled; |
| 71 } |
| 72 |
| 73 renderAsBlock() { |
| 74 this._proxyElementDisplay = 'block'; |
| 75 } |
| 76 |
| 77 /** |
| 78 * Clients should never attach any event listeners to the |element|. Instead, |
| 79 * they should use the result of this method to attach listeners for bubbling
events. |
| 80 * |
| 81 * @param {!Element} element |
| 82 * @return {!Element} |
| 83 */ |
| 84 attach(element) { |
| 85 return this._attachInternal(element); |
| 86 } |
| 87 |
| 88 /** |
| 89 * Clients should never attach any event listeners to the |element|. Instead, |
| 90 * they should use the result of this method to attach listeners for bubbling
events |
| 91 * or the |blurListener| parameter to register a "blur" event listener on the
|element| |
| 92 * (since the "blur" event does not bubble.) |
| 93 * |
| 94 * @param {!Element} element |
| 95 * @param {function(!Event)} blurListener |
| 96 * @return {!Element} |
| 97 */ |
| 98 attachAndStartEditing(element, blurListener) { |
| 99 var proxyElement = this._attachInternal(element); |
| 100 this._startEditing(blurListener); |
| 101 return proxyElement; |
| 102 } |
| 103 |
| 104 /** |
| 105 * @param {!Element} element |
| 106 * @return {!Element} |
| 107 */ |
| 108 _attachInternal(element) { |
| 109 if (this._proxyElement) |
| 110 throw 'Cannot attach an attached TextPrompt'; |
| 111 this._element = element; |
| 112 |
| 113 this._boundOnKeyDown = this.onKeyDown.bind(this); |
| 114 this._boundOnInput = this.onInput.bind(this); |
| 115 this._boundOnMouseWheel = this.onMouseWheel.bind(this); |
| 116 this._boundClearAutocomplete = this.clearAutocomplete.bind(this); |
| 117 this._proxyElement = element.ownerDocument.createElement('span'); |
| 118 var shadowRoot = WebInspector.createShadowRootWithCoreStyles(this._proxyElem
ent, 'ui/textPrompt.css'); |
| 119 this._contentElement = shadowRoot.createChild('div'); |
| 120 this._contentElement.createChild('content'); |
| 121 this._proxyElement.style.display = this._proxyElementDisplay; |
| 122 element.parentElement.insertBefore(this._proxyElement, element); |
| 123 this._proxyElement.appendChild(element); |
| 124 this._element.classList.add('text-prompt'); |
| 125 this._element.addEventListener('keydown', this._boundOnKeyDown, false); |
| 126 this._element.addEventListener('input', this._boundOnInput, false); |
| 127 this._element.addEventListener('mousewheel', this._boundOnMouseWheel, false)
; |
| 128 this._element.addEventListener('selectstart', this._boundClearAutocomplete,
false); |
| 129 this._element.addEventListener('blur', this._boundClearAutocomplete, false); |
| 130 this._element.ownerDocument.defaultView.addEventListener('resize', this._bou
ndClearAutocomplete, false); |
| 131 |
| 132 if (this._suggestBoxEnabled) |
| 133 this._suggestBox = new WebInspector.SuggestBox(this, 20, true); |
| 134 |
| 135 if (this._title) |
| 136 this._proxyElement.title = this._title; |
| 137 |
| 138 return this._proxyElement; |
| 139 } |
| 140 |
| 141 detach() { |
| 142 this._removeFromElement(); |
| 143 this._proxyElement.parentElement.insertBefore(this._element, this._proxyElem
ent); |
| 144 this._proxyElement.remove(); |
| 145 delete this._proxyElement; |
| 146 this._element.classList.remove('text-prompt'); |
| 147 this._focusRestorer.restore(); |
| 148 } |
| 149 |
| 150 /** |
| 151 * @return {string} |
| 152 */ |
| 153 textWithCurrentSuggestion() { |
| 154 return this._element.textContent; |
| 155 } |
| 156 |
| 157 /** |
| 158 * @return {string} |
| 159 */ |
| 160 text() { |
| 161 var text = this.textWithCurrentSuggestion(); |
| 162 if (this._ghostTextElement.parentNode) { |
| 163 var addition = this._ghostTextElement.textContent; |
| 164 text = text.substring(0, text.length - addition.length); |
| 165 } |
| 166 return text; |
| 167 } |
| 168 |
| 169 /** |
| 170 * @param {string} x |
| 171 */ |
| 172 setText(x) { |
| 173 this.clearAutocomplete(); |
| 174 if (!x) { |
| 175 // Append a break element instead of setting textContent to make sure the
selection is inside the prompt. |
| 176 this._element.removeChildren(); |
| 177 this._element.createChild('br'); |
| 178 } else { |
| 179 this._element.textContent = x; |
| 180 } |
| 181 this._previousText = this.text(); |
| 182 |
| 183 this.moveCaretToEndOfPrompt(); |
| 184 this._element.scrollIntoView(); |
| 185 } |
| 186 |
| 187 /** |
| 188 * @return {string} |
| 189 */ |
| 190 title() { |
| 191 return this._title; |
| 192 } |
| 193 |
| 194 /** |
| 195 * @param {string} title |
| 196 */ |
| 197 setTitle(title) { |
| 198 this._title = title; |
| 199 if (this._proxyElement) |
| 200 this._proxyElement.title = title; |
| 201 } |
| 202 |
| 203 _removeFromElement() { |
| 204 this.clearAutocomplete(); |
| 205 this._element.removeEventListener('keydown', this._boundOnKeyDown, false); |
| 206 this._element.removeEventListener('input', this._boundOnInput, false); |
| 207 this._element.removeEventListener('selectstart', this._boundClearAutocomplet
e, false); |
| 208 this._element.removeEventListener('blur', this._boundClearAutocomplete, fals
e); |
| 209 this._element.ownerDocument.defaultView.removeEventListener('resize', this._
boundClearAutocomplete, false); |
| 210 if (this._isEditing) |
| 211 this._stopEditing(); |
| 212 if (this._suggestBox) |
| 213 this._suggestBox.removeFromElement(); |
| 214 } |
| 215 |
| 216 /** |
| 217 * @param {function(!Event)=} blurListener |
| 218 */ |
| 219 _startEditing(blurListener) { |
| 220 this._isEditing = true; |
| 221 this._contentElement.classList.add('text-prompt-editing'); |
| 222 if (blurListener) { |
| 223 this._blurListener = blurListener; |
| 224 this._element.addEventListener('blur', this._blurListener, false); |
| 225 } |
| 226 this._oldTabIndex = this._element.tabIndex; |
| 227 if (this._element.tabIndex < 0) |
| 228 this._element.tabIndex = 0; |
| 229 this._focusRestorer = new WebInspector.ElementFocusRestorer(this._element); |
| 230 if (!this.text()) |
| 231 this.autoCompleteSoon(); |
| 232 } |
| 233 |
| 234 _stopEditing() { |
| 235 this._element.tabIndex = this._oldTabIndex; |
| 236 if (this._blurListener) |
| 237 this._element.removeEventListener('blur', this._blurListener, false); |
| 238 this._contentElement.classList.remove('text-prompt-editing'); |
| 239 delete this._isEditing; |
| 240 } |
| 241 |
| 242 /** |
| 243 * @param {!Event} event |
| 244 */ |
| 245 onMouseWheel(event) { |
| 246 // Subclasses can implement. |
| 247 } |
| 248 |
| 249 /** |
| 250 * @param {!Event} event |
| 251 */ |
| 252 onKeyDown(event) { |
| 253 var handled = false; |
| 254 |
| 255 switch (event.key) { |
| 256 case 'Tab': |
| 257 handled = this.tabKeyPressed(event); |
| 258 break; |
| 259 case 'ArrowLeft': |
| 260 case 'Home': |
| 261 this.clearAutocomplete(); |
| 262 break; |
| 263 case 'ArrowRight': |
| 264 case 'End': |
| 265 if (this._isCaretAtEndOfPrompt()) |
| 266 handled = this.acceptAutoComplete(); |
| 267 else |
| 268 this.clearAutocomplete(); |
| 269 break; |
| 270 case 'Escape': |
| 271 if (this._isSuggestBoxVisible()) { |
| 272 this.clearAutocomplete(); |
| 273 handled = true; |
| 274 } |
| 275 break; |
| 276 case ' ': // Space |
| 277 if (event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey)
{ |
| 278 this.autoCompleteSoon(true); |
| 279 handled = true; |
| 280 } |
| 281 break; |
| 282 case 'Alt': |
| 283 case 'Meta': |
| 284 case 'Shift': |
| 285 case 'Control': |
| 286 break; |
| 287 } |
| 288 |
| 289 if (!handled && this._isSuggestBoxVisible()) |
| 290 handled = this._suggestBox.keyPressed(event); |
| 291 |
| 292 if (handled) |
| 293 event.consume(true); |
| 294 } |
| 295 |
| 296 /** |
| 297 * @param {!Event} event |
| 298 */ |
| 299 onInput(event) { |
| 300 var text = this.text(); |
| 301 var hasCommonPrefix = text.startsWith(this._previousText) || this._previousT
ext.startsWith(text); |
| 302 if (this._prefixRange && hasCommonPrefix) |
| 303 this._prefixRange.endColumn += text.length - this._previousText.length; |
| 304 this._refreshGhostText(); |
| 305 this._previousText = text; |
| 306 |
| 307 this.autoCompleteSoon(); |
| 308 } |
| 309 |
| 310 /** |
| 311 * @return {boolean} |
| 312 */ |
| 313 acceptAutoComplete() { |
| 314 var result = false; |
| 315 if (this._isSuggestBoxVisible()) |
| 316 result = this._suggestBox.acceptSuggestion(); |
| 317 if (!result) |
| 318 result = this._acceptSuggestionInternal(); |
| 319 |
| 320 return result; |
| 321 } |
| 322 |
| 323 clearAutocomplete() { |
| 324 if (this._isSuggestBoxVisible()) |
| 325 this._suggestBox.hide(); |
| 326 this._clearAutocompleteTimeout(); |
| 327 this._prefixRange = null; |
| 328 this._refreshGhostText(); |
| 329 } |
| 330 |
| 331 _refreshGhostText() { |
| 332 if (this._prefixRange && this._isCaretAtEndOfPrompt()) { |
| 333 this._ghostTextElement.textContent = |
| 334 this._currentSuggestion.substring(this._prefixRange.endColumn - this._
prefixRange.startColumn); |
| 335 this._element.appendChild(this._ghostTextElement); |
| 336 } else { |
| 337 this._ghostTextElement.remove(); |
| 338 } |
| 339 } |
| 340 |
| 341 _clearAutocompleteTimeout() { |
| 342 if (this._completeTimeout) { |
| 343 clearTimeout(this._completeTimeout); |
| 344 delete this._completeTimeout; |
| 345 } |
| 346 this._completionRequestId++; |
| 347 } |
| 348 |
| 349 /** |
| 350 * @param {boolean=} force |
| 351 */ |
| 352 autoCompleteSoon(force) { |
| 353 var immediately = this._isSuggestBoxVisible() || force; |
| 354 if (!this._completeTimeout) |
| 355 this._completeTimeout = |
| 356 setTimeout(this.complete.bind(this, force), immediately ? 0 : this._au
tocompletionTimeout); |
| 357 } |
| 358 |
| 359 /** |
| 360 * @param {boolean=} force |
| 361 * @param {boolean=} reverse |
| 362 */ |
| 363 complete(force, reverse) { |
| 364 this._clearAutocompleteTimeout(); |
| 365 var selection = this._element.getComponentSelection(); |
| 366 var selectionRange = selection && selection.rangeCount ? selection.getRangeA
t(0) : null; |
| 367 if (!selectionRange) |
| 368 return; |
| 369 |
| 370 var shouldExit; |
| 371 |
| 372 if (!force && !this._isCaretAtEndOfPrompt() && !this._isSuggestBoxVisible()) |
| 373 shouldExit = true; |
| 374 else if (!selection.isCollapsed) |
| 375 shouldExit = true; |
| 376 else if (!force) { |
| 377 // BUG72018: Do not show suggest box if caret is followed by a non-stop ch
aracter. |
| 378 var wordSuffixRange = selectionRange.startContainer.rangeOfWord( |
| 379 selectionRange.endOffset, this._completionStopCharacters, this._elemen
t, 'forward'); |
| 380 var autocompleteTextLength = this._ghostTextElement.parentNode ? this._gho
stTextElement.textContent.length : 0; |
| 381 if (wordSuffixRange.toString().length !== autocompleteTextLength) |
| 382 shouldExit = true; |
| 383 } |
| 384 if (shouldExit) { |
| 385 this.clearAutocomplete(); |
| 386 return; |
| 387 } |
| 388 |
| 389 var wordPrefixRange = selectionRange.startContainer.rangeOfWord( |
| 390 selectionRange.startOffset, this._completionStopCharacters, this._elemen
t, 'backward'); |
| 391 this._loadCompletions( |
| 392 /** @type {!Element} */ (this._proxyElement), wordPrefixRange, force ||
false, |
| 393 this._completionsReady.bind(this, ++this._completionRequestId, selection
, wordPrefixRange, !!reverse, !!force)); |
| 394 } |
| 395 |
| 396 disableDefaultSuggestionForEmptyInput() { |
| 397 this._disableDefaultSuggestionForEmptyInput = true; |
| 398 } |
| 399 |
| 400 /** |
| 401 * @param {!Selection} selection |
| 402 * @param {!Range} textRange |
| 403 */ |
| 404 _boxForAnchorAtStart(selection, textRange) { |
| 405 var rangeCopy = selection.getRangeAt(0).cloneRange(); |
| 406 var anchorElement = 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 * @return {?Range} |
| 418 * @suppressGlobalPropertiesCheck |
| 419 */ |
| 420 _createRange() { |
| 421 return document.createRange(); |
| 422 } |
| 423 |
| 424 /** |
| 425 * @param {string} prefix |
| 426 * @return {!WebInspector.SuggestBox.Suggestions} |
| 427 */ |
| 428 additionalCompletions(prefix) { |
| 429 return []; |
| 430 } |
| 431 |
| 432 /** |
| 433 * @param {number} completionRequestId |
| 434 * @param {!Selection} selection |
| 435 * @param {!Range} originalWordPrefixRange |
| 436 * @param {boolean} reverse |
| 437 * @param {boolean} force |
| 438 * @param {!Array.<string>} completions |
| 439 * @param {number=} selectedIndex |
| 440 */ |
| 441 _completionsReady( |
| 442 completionRequestId, |
| 443 selection, |
| 444 originalWordPrefixRange, |
| 445 reverse, |
| 446 force, |
| 447 completions, |
| 448 selectedIndex) { |
| 449 if (this._completionRequestId !== completionRequestId) |
| 450 return; |
| 451 |
| 452 var prefix = originalWordPrefixRange.toString(); |
| 453 |
| 454 // Filter out dupes. |
| 455 var store = new Set(); |
| 456 completions = completions.filter(item => !store.has(item) && !!store.add(ite
m)); |
| 457 var annotatedCompletions = completions.map(item => ({title: item})); |
| 458 |
| 459 if (prefix || force) { |
| 460 if (prefix) |
| 461 annotatedCompletions = annotatedCompletions.concat(this.additionalComple
tions(prefix)); |
| 462 else |
| 463 annotatedCompletions = this.additionalCompletions(prefix).concat(annotat
edCompletions); |
| 464 } |
| 465 |
| 466 if (!annotatedCompletions.length) { |
| 467 this.clearAutocomplete(); |
| 468 return; |
| 469 } |
| 470 |
| 471 var selectionRange = selection.getRangeAt(0); |
| 472 |
| 473 var fullWordRange = this._createRange(); |
| 474 fullWordRange.setStart(originalWordPrefixRange.startContainer, originalWordP
refixRange.startOffset); |
| 475 fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset); |
| 476 |
| 477 if (prefix + selectionRange.toString() !== fullWordRange.toString()) |
| 478 return; |
| 479 |
| 480 selectedIndex = (this._disableDefaultSuggestionForEmptyInput && !this.text()
) ? -1 : (selectedIndex || 0); |
| 481 |
| 482 if (this._suggestBox) |
| 483 this._suggestBox.updateSuggestions( |
| 484 this._boxForAnchorAtStart(selection, fullWordRange), annotatedCompleti
ons, selectedIndex, |
| 485 !this._isCaretAtEndOfPrompt(), this.text()); |
| 486 |
| 487 var beforeRange = this._createRange(); |
| 488 beforeRange.setStart(this._element, 0); |
| 489 beforeRange.setEnd(fullWordRange.startContainer, fullWordRange.startOffset); |
| 490 this._prefixRange = new WebInspector.TextRange( |
| 491 0, beforeRange.toString().length, 0, beforeRange.toString().length + ful
lWordRange.toString().length); |
| 492 |
| 493 if (selectedIndex === -1) |
| 494 return; |
| 495 this.applySuggestion(annotatedCompletions[selectedIndex].title, true); |
| 496 } |
| 497 |
| 498 /** |
| 499 * @override |
| 500 * @param {string} suggestion |
| 501 * @param {boolean=} isIntermediateSuggestion |
| 502 */ |
| 503 applySuggestion(suggestion, isIntermediateSuggestion) { |
| 504 if (!this._prefixRange) |
| 505 return; |
| 506 this._currentSuggestion = suggestion; |
| 507 this._refreshGhostText(); |
| 508 if (isIntermediateSuggestion) |
| 509 this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApplied); |
| 510 } |
| 511 |
| 512 /** |
| 513 * @override |
| 514 */ |
| 515 acceptSuggestion() { |
| 516 this._acceptSuggestionInternal(); |
| 517 } |
| 518 |
| 519 /** |
| 520 * @return {boolean} |
| 521 */ |
| 522 _acceptSuggestionInternal() { |
| 523 if (!this._prefixRange) |
| 524 return false; |
| 525 |
| 526 var text = this.text(); |
| 527 this._element.textContent = text.substring(0, this._prefixRange.startColumn)
+ this._currentSuggestion + |
| 528 text.substring(this._prefixRange.endColumn); |
| 529 this._setDOMSelection( |
| 530 this._prefixRange.startColumn + this._currentSuggestion.length, |
| 531 this._prefixRange.startColumn + this._currentSuggestion.length); |
| 532 |
| 533 this.clearAutocomplete(); |
| 534 this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemAccepted); |
| 535 |
| 536 return true; |
| 537 } |
| 538 |
| 539 /** |
| 540 * @param {number} startColumn |
| 541 * @param {number} endColumn |
| 542 */ |
| 543 _setDOMSelection(startColumn, endColumn) { |
| 544 this._element.normalize(); |
| 545 var node = this._element.childNodes[0]; |
| 546 if (!node || node === this._ghostTextElement) |
| 547 return; |
| 548 var range = this._createRange(); |
| 549 range.setStart(node, startColumn); |
| 550 range.setEnd(node, endColumn); |
| 551 var selection = this._element.getComponentSelection(); |
| 552 selection.removeAllRanges(); |
| 553 selection.addRange(range); |
| 554 } |
| 555 |
| 556 /** |
| 557 * @return {boolean} |
| 558 */ |
| 559 _isSuggestBoxVisible() { |
| 560 return this._suggestBox && this._suggestBox.visible(); |
| 561 } |
| 562 |
| 563 /** |
| 564 * @return {boolean} |
| 565 */ |
| 566 isCaretInsidePrompt() { |
| 567 var selection = this._element.getComponentSelection(); |
| 568 // @see crbug.com/602541 |
| 569 var selectionRange = selection && selection.rangeCount ? selection.getRangeA
t(0) : null; |
| 570 if (!selectionRange || !selection.isCollapsed) |
| 571 return false; |
| 572 return selectionRange.startContainer.isSelfOrDescendant(this._element); |
| 573 } |
| 574 |
| 575 /** |
| 576 * @return {boolean} |
| 577 */ |
| 578 _isCaretAtEndOfPrompt() { |
| 579 var selection = this._element.getComponentSelection(); |
| 580 var selectionRange = selection && selection.rangeCount ? selection.getRangeA
t(0) : null; |
| 581 if (!selectionRange || !selection.isCollapsed) |
| 582 return false; |
| 583 |
| 584 var node = selectionRange.startContainer; |
| 585 if (!node.isSelfOrDescendant(this._element)) |
| 586 return false; |
| 587 |
| 588 if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.no
deValue.length) |
| 589 return false; |
| 590 |
| 591 var foundNextText = false; |
| 592 while (node) { |
| 593 if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) { |
| 594 if (foundNextText && !this._ghostTextElement.isAncestor(node)) |
| 595 return false; |
| 596 foundNextText = true; |
| 597 } |
| 598 |
| 599 node = node.traverseNextNode(this._element); |
| 600 } |
| 601 |
| 602 return true; |
| 603 } |
| 604 |
| 605 moveCaretToEndOfPrompt() { |
| 606 var selection = this._element.getComponentSelection(); |
| 607 var selectionRange = this._createRange(); |
| 608 |
| 609 var container = this._element; |
| 610 while (container.childNodes.length) |
| 611 container = container.lastChild; |
| 612 var offset = container.nodeType === Node.TEXT_NODE ? container.textContent.l
ength : 0; |
| 613 selectionRange.setStart(container, offset); |
| 614 selectionRange.setEnd(container, offset); |
| 615 |
| 616 selection.removeAllRanges(); |
| 617 selection.addRange(selectionRange); |
| 618 } |
| 619 |
| 620 /** |
| 621 * @param {!Event} event |
| 622 * @return {boolean} |
| 623 */ |
| 624 tabKeyPressed(event) { |
| 625 this.acceptAutoComplete(); |
| 626 |
| 627 // Consume the key. |
| 628 return true; |
| 629 } |
| 630 |
| 631 /** |
| 632 * @return {?Element} |
| 633 */ |
| 634 proxyElementForTests() { |
| 635 return this._proxyElement || null; |
| 636 } |
| 49 }; | 637 }; |
| 50 | 638 |
| 51 WebInspector.TextPrompt.DefaultAutocompletionTimeout = 250; | 639 WebInspector.TextPrompt.DefaultAutocompletionTimeout = 250; |
| 52 | 640 |
| 53 /** @enum {symbol} */ | 641 /** @enum {symbol} */ |
| 54 WebInspector.TextPrompt.Events = { | 642 WebInspector.TextPrompt.Events = { |
| 55 ItemApplied: Symbol("text-prompt-item-applied"), | 643 ItemApplied: Symbol('text-prompt-item-applied'), |
| 56 ItemAccepted: Symbol("text-prompt-item-accepted") | 644 ItemAccepted: Symbol('text-prompt-item-accepted') |
| 57 }; | 645 }; |
| 58 | |
| 59 WebInspector.TextPrompt.prototype = { | |
| 60 /** | |
| 61 * @param {function(!Element, !Range, boolean, function(!Array.<string>, num
ber=))} completions | |
| 62 * @param {string=} stopCharacters | |
| 63 */ | |
| 64 initialize: function(completions, stopCharacters) | |
| 65 { | |
| 66 this._loadCompletions = completions; | |
| 67 this._completionStopCharacters = stopCharacters || " =:[({;,!+-*/&|^<>."
; | |
| 68 }, | |
| 69 | |
| 70 /** | |
| 71 * @param {number} timeout | |
| 72 */ | |
| 73 setAutocompletionTimeout: function(timeout) | |
| 74 { | |
| 75 this._autocompletionTimeout = timeout; | |
| 76 }, | |
| 77 | |
| 78 /** | |
| 79 * @param {boolean} suggestBoxEnabled | |
| 80 */ | |
| 81 setSuggestBoxEnabled: function(suggestBoxEnabled) | |
| 82 { | |
| 83 this._suggestBoxEnabled = suggestBoxEnabled; | |
| 84 }, | |
| 85 | |
| 86 renderAsBlock: function() | |
| 87 { | |
| 88 this._proxyElementDisplay = "block"; | |
| 89 }, | |
| 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 bubblin
g events. | |
| 94 * | |
| 95 * @param {!Element} element | |
| 96 * @return {!Element} | |
| 97 */ | |
| 98 attach: function(element) | |
| 99 { | |
| 100 return this._attachInternal(element); | |
| 101 }, | |
| 102 | |
| 103 /** | |
| 104 * Clients should never attach any event listeners to the |element|. Instead
, | |
| 105 * they should use the result of this method to attach listeners for bubblin
g events | |
| 106 * or the |blurListener| parameter to register a "blur" event listener on th
e |element| | |
| 107 * (since the "blur" event does not bubble.) | |
| 108 * | |
| 109 * @param {!Element} element | |
| 110 * @param {function(!Event)} blurListener | |
| 111 * @return {!Element} | |
| 112 */ | |
| 113 attachAndStartEditing: function(element, blurListener) | |
| 114 { | |
| 115 var proxyElement = this._attachInternal(element); | |
| 116 this._startEditing(blurListener); | |
| 117 return proxyElement; | |
| 118 }, | |
| 119 | |
| 120 /** | |
| 121 * @param {!Element} element | |
| 122 * @return {!Element} | |
| 123 */ | |
| 124 _attachInternal: function(element) | |
| 125 { | |
| 126 if (this._proxyElement) | |
| 127 throw "Cannot attach an attached TextPrompt"; | |
| 128 this._element = element; | |
| 129 | |
| 130 this._boundOnKeyDown = this.onKeyDown.bind(this); | |
| 131 this._boundOnInput = this.onInput.bind(this); | |
| 132 this._boundOnMouseWheel = this.onMouseWheel.bind(this); | |
| 133 this._boundClearAutocomplete = this.clearAutocomplete.bind(this); | |
| 134 this._proxyElement = element.ownerDocument.createElement("span"); | |
| 135 var shadowRoot = WebInspector.createShadowRootWithCoreStyles(this._proxy
Element, "ui/textPrompt.css"); | |
| 136 this._contentElement = shadowRoot.createChild("div"); | |
| 137 this._contentElement.createChild("content"); | |
| 138 this._proxyElement.style.display = this._proxyElementDisplay; | |
| 139 element.parentElement.insertBefore(this._proxyElement, element); | |
| 140 this._proxyElement.appendChild(element); | |
| 141 this._element.classList.add("text-prompt"); | |
| 142 this._element.addEventListener("keydown", this._boundOnKeyDown, false); | |
| 143 this._element.addEventListener("input", this._boundOnInput, false); | |
| 144 this._element.addEventListener("mousewheel", this._boundOnMouseWheel, fa
lse); | |
| 145 this._element.addEventListener("selectstart", this._boundClearAutocomple
te, false); | |
| 146 this._element.addEventListener("blur", this._boundClearAutocomplete, fal
se); | |
| 147 this._element.ownerDocument.defaultView.addEventListener("resize", this.
_boundClearAutocomplete, false); | |
| 148 | |
| 149 if (this._suggestBoxEnabled) | |
| 150 this._suggestBox = new WebInspector.SuggestBox(this, 20, true); | |
| 151 | |
| 152 if (this._title) | |
| 153 this._proxyElement.title = this._title; | |
| 154 | |
| 155 return this._proxyElement; | |
| 156 }, | |
| 157 | |
| 158 detach: function() | |
| 159 { | |
| 160 this._removeFromElement(); | |
| 161 this._proxyElement.parentElement.insertBefore(this._element, this._proxy
Element); | |
| 162 this._proxyElement.remove(); | |
| 163 delete this._proxyElement; | |
| 164 this._element.classList.remove("text-prompt"); | |
| 165 this._focusRestorer.restore(); | |
| 166 }, | |
| 167 | |
| 168 /** | |
| 169 * @return {string} | |
| 170 */ | |
| 171 textWithCurrentSuggestion: function() | |
| 172 { | |
| 173 return this._element.textContent; | |
| 174 }, | |
| 175 | |
| 176 /** | |
| 177 * @return {string} | |
| 178 */ | |
| 179 text: function() | |
| 180 { | |
| 181 var text = this.textWithCurrentSuggestion(); | |
| 182 if (this._ghostTextElement.parentNode) { | |
| 183 var addition = this._ghostTextElement.textContent; | |
| 184 text = text.substring(0, text.length - addition.length); | |
| 185 } | |
| 186 return text; | |
| 187 }, | |
| 188 | |
| 189 /** | |
| 190 * @param {string} x | |
| 191 */ | |
| 192 setText: function(x) | |
| 193 { | |
| 194 this.clearAutocomplete(); | |
| 195 if (!x) { | |
| 196 // Append a break element instead of setting textContent to make sur
e the selection is inside the prompt. | |
| 197 this._element.removeChildren(); | |
| 198 this._element.createChild("br"); | |
| 199 } else { | |
| 200 this._element.textContent = x; | |
| 201 } | |
| 202 this._previousText = this.text(); | |
| 203 | |
| 204 this.moveCaretToEndOfPrompt(); | |
| 205 this._element.scrollIntoView(); | |
| 206 }, | |
| 207 | |
| 208 /** | |
| 209 * @return {string} | |
| 210 */ | |
| 211 title: function() | |
| 212 { | |
| 213 return this._title; | |
| 214 }, | |
| 215 | |
| 216 /** | |
| 217 * @param {string} title | |
| 218 */ | |
| 219 setTitle: function(title) | |
| 220 { | |
| 221 this._title = title; | |
| 222 if (this._proxyElement) | |
| 223 this._proxyElement.title = title; | |
| 224 }, | |
| 225 | |
| 226 _removeFromElement: function() | |
| 227 { | |
| 228 this.clearAutocomplete(); | |
| 229 this._element.removeEventListener("keydown", this._boundOnKeyDown, false
); | |
| 230 this._element.removeEventListener("input", this._boundOnInput, false); | |
| 231 this._element.removeEventListener("selectstart", this._boundClearAutocom
plete, false); | |
| 232 this._element.removeEventListener("blur", this._boundClearAutocomplete,
false); | |
| 233 this._element.ownerDocument.defaultView.removeEventListener("resize", th
is._boundClearAutocomplete, false); | |
| 234 if (this._isEditing) | |
| 235 this._stopEditing(); | |
| 236 if (this._suggestBox) | |
| 237 this._suggestBox.removeFromElement(); | |
| 238 }, | |
| 239 | |
| 240 /** | |
| 241 * @param {function(!Event)=} blurListener | |
| 242 */ | |
| 243 _startEditing: function(blurListener) | |
| 244 { | |
| 245 this._isEditing = true; | |
| 246 this._contentElement.classList.add("text-prompt-editing"); | |
| 247 if (blurListener) { | |
| 248 this._blurListener = blurListener; | |
| 249 this._element.addEventListener("blur", this._blurListener, false); | |
| 250 } | |
| 251 this._oldTabIndex = this._element.tabIndex; | |
| 252 if (this._element.tabIndex < 0) | |
| 253 this._element.tabIndex = 0; | |
| 254 this._focusRestorer = new WebInspector.ElementFocusRestorer(this._elemen
t); | |
| 255 if (!this.text()) | |
| 256 this.autoCompleteSoon(); | |
| 257 }, | |
| 258 | |
| 259 _stopEditing: function() | |
| 260 { | |
| 261 this._element.tabIndex = this._oldTabIndex; | |
| 262 if (this._blurListener) | |
| 263 this._element.removeEventListener("blur", this._blurListener, false)
; | |
| 264 this._contentElement.classList.remove("text-prompt-editing"); | |
| 265 delete this._isEditing; | |
| 266 }, | |
| 267 | |
| 268 /** | |
| 269 * @param {!Event} event | |
| 270 */ | |
| 271 onMouseWheel: function(event) | |
| 272 { | |
| 273 // Subclasses can implement. | |
| 274 }, | |
| 275 | |
| 276 /** | |
| 277 * @param {!Event} event | |
| 278 */ | |
| 279 onKeyDown: function(event) | |
| 280 { | |
| 281 var handled = false; | |
| 282 | |
| 283 switch (event.key) { | |
| 284 case "Tab": | |
| 285 handled = this.tabKeyPressed(event); | |
| 286 break; | |
| 287 case "ArrowLeft": | |
| 288 case "Home": | |
| 289 this.clearAutocomplete(); | |
| 290 break; | |
| 291 case "ArrowRight": | |
| 292 case "End": | |
| 293 if (this._isCaretAtEndOfPrompt()) | |
| 294 handled = this.acceptAutoComplete(); | |
| 295 else | |
| 296 this.clearAutocomplete(); | |
| 297 break; | |
| 298 case "Escape": | |
| 299 if (this._isSuggestBoxVisible()) { | |
| 300 this.clearAutocomplete(); | |
| 301 handled = true; | |
| 302 } | |
| 303 break; | |
| 304 case " ": // Space | |
| 305 if (event.ctrlKey && !event.metaKey && !event.altKey && !event.shift
Key) { | |
| 306 this.autoCompleteSoon(true); | |
| 307 handled = true; | |
| 308 } | |
| 309 break; | |
| 310 case "Alt": | |
| 311 case "Meta": | |
| 312 case "Shift": | |
| 313 case "Control": | |
| 314 break; | |
| 315 } | |
| 316 | |
| 317 if (!handled && this._isSuggestBoxVisible()) | |
| 318 handled = this._suggestBox.keyPressed(event); | |
| 319 | |
| 320 if (handled) | |
| 321 event.consume(true); | |
| 322 }, | |
| 323 | |
| 324 /** | |
| 325 * @param {!Event} event | |
| 326 */ | |
| 327 onInput: function(event) | |
| 328 { | |
| 329 var text = this.text(); | |
| 330 var hasCommonPrefix = text.startsWith(this._previousText) || this._previ
ousText.startsWith(text); | |
| 331 if (this._prefixRange && hasCommonPrefix) | |
| 332 this._prefixRange.endColumn += text.length - this._previousText.leng
th; | |
| 333 this._refreshGhostText(); | |
| 334 this._previousText = text; | |
| 335 | |
| 336 this.autoCompleteSoon(); | |
| 337 }, | |
| 338 | |
| 339 /** | |
| 340 * @return {boolean} | |
| 341 */ | |
| 342 acceptAutoComplete: function() | |
| 343 { | |
| 344 var result = false; | |
| 345 if (this._isSuggestBoxVisible()) | |
| 346 result = this._suggestBox.acceptSuggestion(); | |
| 347 if (!result) | |
| 348 result = this._acceptSuggestionInternal(); | |
| 349 | |
| 350 return result; | |
| 351 }, | |
| 352 | |
| 353 clearAutocomplete: function() | |
| 354 { | |
| 355 if (this._isSuggestBoxVisible()) | |
| 356 this._suggestBox.hide(); | |
| 357 this._clearAutocompleteTimeout(); | |
| 358 this._prefixRange = null; | |
| 359 this._refreshGhostText(); | |
| 360 }, | |
| 361 | |
| 362 _refreshGhostText: function() | |
| 363 { | |
| 364 if (this._prefixRange && this._isCaretAtEndOfPrompt()) { | |
| 365 this._ghostTextElement.textContent = this._currentSuggestion.substri
ng(this._prefixRange.endColumn - this._prefixRange.startColumn); | |
| 366 this._element.appendChild(this._ghostTextElement); | |
| 367 } else { | |
| 368 this._ghostTextElement.remove(); | |
| 369 } | |
| 370 }, | |
| 371 | |
| 372 _clearAutocompleteTimeout: function() | |
| 373 { | |
| 374 if (this._completeTimeout) { | |
| 375 clearTimeout(this._completeTimeout); | |
| 376 delete this._completeTimeout; | |
| 377 } | |
| 378 this._completionRequestId++; | |
| 379 }, | |
| 380 | |
| 381 /** | |
| 382 * @param {boolean=} force | |
| 383 */ | |
| 384 autoCompleteSoon: function(force) | |
| 385 { | |
| 386 var immediately = this._isSuggestBoxVisible() || force; | |
| 387 if (!this._completeTimeout) | |
| 388 this._completeTimeout = setTimeout(this.complete.bind(this, force),
immediately ? 0 : this._autocompletionTimeout); | |
| 389 }, | |
| 390 | |
| 391 /** | |
| 392 * @param {boolean=} force | |
| 393 * @param {boolean=} reverse | |
| 394 */ | |
| 395 complete: function(force, reverse) | |
| 396 { | |
| 397 this._clearAutocompleteTimeout(); | |
| 398 var selection = this._element.getComponentSelection(); | |
| 399 var selectionRange = selection && selection.rangeCount ? selection.getRa
ngeAt(0) : null; | |
| 400 if (!selectionRange) | |
| 401 return; | |
| 402 | |
| 403 var shouldExit; | |
| 404 | |
| 405 if (!force && !this._isCaretAtEndOfPrompt() && !this._isSuggestBoxVisibl
e()) | |
| 406 shouldExit = true; | |
| 407 else if (!selection.isCollapsed) | |
| 408 shouldExit = true; | |
| 409 else if (!force) { | |
| 410 // BUG72018: Do not show suggest box if caret is followed by a non-s
top character. | |
| 411 var wordSuffixRange = selectionRange.startContainer.rangeOfWord(sele
ctionRange.endOffset, this._completionStopCharacters, this._element, "forward"); | |
| 412 var autocompleteTextLength = this._ghostTextElement.parentNode ? thi
s._ghostTextElement.textContent.length : 0; | |
| 413 if (wordSuffixRange.toString().length !== autocompleteTextLength) | |
| 414 shouldExit = true; | |
| 415 } | |
| 416 if (shouldExit) { | |
| 417 this.clearAutocomplete(); | |
| 418 return; | |
| 419 } | |
| 420 | |
| 421 var wordPrefixRange = selectionRange.startContainer.rangeOfWord(selectio
nRange.startOffset, this._completionStopCharacters, this._element, "backward"); | |
| 422 this._loadCompletions(/** @type {!Element} */ (this._proxyElement), word
PrefixRange, force || false, this._completionsReady.bind(this, ++this._completio
nRequestId, selection, wordPrefixRange, !!reverse, !!force)); | |
| 423 }, | |
| 424 | |
| 425 disableDefaultSuggestionForEmptyInput: function() | |
| 426 { | |
| 427 this._disableDefaultSuggestionForEmptyInput = true; | |
| 428 }, | |
| 429 | |
| 430 /** | |
| 431 * @param {!Selection} selection | |
| 432 * @param {!Range} textRange | |
| 433 */ | |
| 434 _boxForAnchorAtStart: function(selection, textRange) | |
| 435 { | |
| 436 var rangeCopy = selection.getRangeAt(0).cloneRange(); | |
| 437 var anchorElement = createElement("span"); | |
| 438 anchorElement.textContent = "\u200B"; | |
| 439 textRange.insertNode(anchorElement); | |
| 440 var box = anchorElement.boxInWindow(window); | |
| 441 anchorElement.remove(); | |
| 442 selection.removeAllRanges(); | |
| 443 selection.addRange(rangeCopy); | |
| 444 return box; | |
| 445 }, | |
| 446 | |
| 447 /** | |
| 448 * @return {?Range} | |
| 449 * @suppressGlobalPropertiesCheck | |
| 450 */ | |
| 451 _createRange: function() | |
| 452 { | |
| 453 return document.createRange(); | |
| 454 }, | |
| 455 | |
| 456 /** | |
| 457 * @param {string} prefix | |
| 458 * @return {!WebInspector.SuggestBox.Suggestions} | |
| 459 */ | |
| 460 additionalCompletions: function(prefix) | |
| 461 { | |
| 462 return []; | |
| 463 }, | |
| 464 | |
| 465 /** | |
| 466 * @param {number} completionRequestId | |
| 467 * @param {!Selection} selection | |
| 468 * @param {!Range} originalWordPrefixRange | |
| 469 * @param {boolean} reverse | |
| 470 * @param {boolean} force | |
| 471 * @param {!Array.<string>} completions | |
| 472 * @param {number=} selectedIndex | |
| 473 */ | |
| 474 _completionsReady: function(completionRequestId, selection, originalWordPref
ixRange, reverse, force, completions, selectedIndex) | |
| 475 { | |
| 476 if (this._completionRequestId !== completionRequestId) | |
| 477 return; | |
| 478 | |
| 479 var prefix = originalWordPrefixRange.toString(); | |
| 480 | |
| 481 // Filter out dupes. | |
| 482 var store = new Set(); | |
| 483 completions = completions.filter(item => !store.has(item) && !!store.add
(item)); | |
| 484 var annotatedCompletions = completions.map(item => ({title: item})); | |
| 485 | |
| 486 if (prefix || force) { | |
| 487 if (prefix) | |
| 488 annotatedCompletions = annotatedCompletions.concat(this.addition
alCompletions(prefix)); | |
| 489 else | |
| 490 annotatedCompletions = this.additionalCompletions(prefix).concat
(annotatedCompletions); | |
| 491 } | |
| 492 | |
| 493 if (!annotatedCompletions.length) { | |
| 494 this.clearAutocomplete(); | |
| 495 return; | |
| 496 } | |
| 497 | |
| 498 var selectionRange = selection.getRangeAt(0); | |
| 499 | |
| 500 var fullWordRange = this._createRange(); | |
| 501 fullWordRange.setStart(originalWordPrefixRange.startContainer, originalW
ordPrefixRange.startOffset); | |
| 502 fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffs
et); | |
| 503 | |
| 504 if (prefix + selectionRange.toString() !== fullWordRange.toString()) | |
| 505 return; | |
| 506 | |
| 507 selectedIndex = (this._disableDefaultSuggestionForEmptyInput && !this.te
xt()) ? -1 : (selectedIndex || 0); | |
| 508 | |
| 509 if (this._suggestBox) | |
| 510 this._suggestBox.updateSuggestions(this._boxForAnchorAtStart(selecti
on, fullWordRange), annotatedCompletions, selectedIndex, !this._isCaretAtEndOfPr
ompt(), this.text()); | |
| 511 | |
| 512 var beforeRange = this._createRange(); | |
| 513 beforeRange.setStart(this._element, 0); | |
| 514 beforeRange.setEnd(fullWordRange.startContainer, fullWordRange.startOffs
et); | |
| 515 this._prefixRange = new WebInspector.TextRange(0, beforeRange.toString()
.length, 0, beforeRange.toString().length + fullWordRange.toString().length); | |
| 516 | |
| 517 if (selectedIndex === -1) | |
| 518 return; | |
| 519 this.applySuggestion(annotatedCompletions[selectedIndex].title, true); | |
| 520 }, | |
| 521 | |
| 522 /** | |
| 523 * @override | |
| 524 * @param {string} suggestion | |
| 525 * @param {boolean=} isIntermediateSuggestion | |
| 526 */ | |
| 527 applySuggestion: function(suggestion, isIntermediateSuggestion) | |
| 528 { | |
| 529 if (!this._prefixRange) | |
| 530 return; | |
| 531 this._currentSuggestion = suggestion; | |
| 532 this._refreshGhostText(); | |
| 533 if (isIntermediateSuggestion) | |
| 534 this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemApp
lied); | |
| 535 }, | |
| 536 | |
| 537 /** | |
| 538 * @override | |
| 539 */ | |
| 540 acceptSuggestion: function() | |
| 541 { | |
| 542 this._acceptSuggestionInternal(); | |
| 543 }, | |
| 544 | |
| 545 /** | |
| 546 * @return {boolean} | |
| 547 */ | |
| 548 _acceptSuggestionInternal: function() | |
| 549 { | |
| 550 if (!this._prefixRange) | |
| 551 return false; | |
| 552 | |
| 553 var text = this.text(); | |
| 554 this._element.textContent = text.substring(0, this._prefixRange.startCol
umn) + this._currentSuggestion + text.substring(this._prefixRange.endColumn); | |
| 555 this._setDOMSelection(this._prefixRange.startColumn + this._currentSugge
stion.length, this._prefixRange.startColumn + this._currentSuggestion.length); | |
| 556 | |
| 557 this.clearAutocomplete(); | |
| 558 this.dispatchEventToListeners(WebInspector.TextPrompt.Events.ItemAccepte
d); | |
| 559 | |
| 560 return true; | |
| 561 }, | |
| 562 | |
| 563 /** | |
| 564 * @param {number} startColumn | |
| 565 * @param {number} endColumn | |
| 566 */ | |
| 567 _setDOMSelection: function(startColumn, endColumn) | |
| 568 { | |
| 569 this._element.normalize(); | |
| 570 var node = this._element.childNodes[0]; | |
| 571 if (!node || node === this._ghostTextElement) | |
| 572 return; | |
| 573 var range = this._createRange(); | |
| 574 range.setStart(node, startColumn); | |
| 575 range.setEnd(node, endColumn); | |
| 576 var selection = this._element.getComponentSelection(); | |
| 577 selection.removeAllRanges(); | |
| 578 selection.addRange(range); | |
| 579 }, | |
| 580 | |
| 581 /** | |
| 582 * @return {boolean} | |
| 583 */ | |
| 584 _isSuggestBoxVisible: function() | |
| 585 { | |
| 586 return this._suggestBox && this._suggestBox.visible(); | |
| 587 }, | |
| 588 | |
| 589 /** | |
| 590 * @return {boolean} | |
| 591 */ | |
| 592 isCaretInsidePrompt: function() | |
| 593 { | |
| 594 var selection = this._element.getComponentSelection(); | |
| 595 // @see crbug.com/602541 | |
| 596 var selectionRange = selection && selection.rangeCount ? selection.getRa
ngeAt(0) : null; | |
| 597 if (!selectionRange || !selection.isCollapsed) | |
| 598 return false; | |
| 599 return selectionRange.startContainer.isSelfOrDescendant(this._element); | |
| 600 }, | |
| 601 | |
| 602 /** | |
| 603 * @return {boolean} | |
| 604 */ | |
| 605 _isCaretAtEndOfPrompt: function() | |
| 606 { | |
| 607 var selection = this._element.getComponentSelection(); | |
| 608 var selectionRange = selection && selection.rangeCount ? selection.getRa
ngeAt(0) : null; | |
| 609 if (!selectionRange || !selection.isCollapsed) | |
| 610 return false; | |
| 611 | |
| 612 var node = selectionRange.startContainer; | |
| 613 if (!node.isSelfOrDescendant(this._element)) | |
| 614 return false; | |
| 615 | |
| 616 if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < nod
e.nodeValue.length) | |
| 617 return false; | |
| 618 | |
| 619 var foundNextText = false; | |
| 620 while (node) { | |
| 621 if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) { | |
| 622 if (foundNextText && !this._ghostTextElement.isAncestor(node)) | |
| 623 return false; | |
| 624 foundNextText = true; | |
| 625 } | |
| 626 | |
| 627 node = node.traverseNextNode(this._element); | |
| 628 } | |
| 629 | |
| 630 return true; | |
| 631 }, | |
| 632 | |
| 633 moveCaretToEndOfPrompt: function() | |
| 634 { | |
| 635 var selection = this._element.getComponentSelection(); | |
| 636 var selectionRange = this._createRange(); | |
| 637 | |
| 638 var container = this._element; | |
| 639 while (container.childNodes.length) | |
| 640 container = container.lastChild; | |
| 641 var offset = container.nodeType === Node.TEXT_NODE ? container.textConte
nt.length : 0; | |
| 642 selectionRange.setStart(container, offset); | |
| 643 selectionRange.setEnd(container, offset); | |
| 644 | |
| 645 selection.removeAllRanges(); | |
| 646 selection.addRange(selectionRange); | |
| 647 }, | |
| 648 | |
| 649 /** | |
| 650 * @param {!Event} event | |
| 651 * @return {boolean} | |
| 652 */ | |
| 653 tabKeyPressed: function(event) | |
| 654 { | |
| 655 this.acceptAutoComplete(); | |
| 656 | |
| 657 // Consume the key. | |
| 658 return true; | |
| 659 }, | |
| 660 | |
| 661 /** | |
| 662 * @return {?Element} | |
| 663 */ | |
| 664 proxyElementForTests: function() | |
| 665 { | |
| 666 return this._proxyElement || null; | |
| 667 }, | |
| 668 | |
| 669 __proto__: WebInspector.Object.prototype | |
| 670 }; | |
| OLD | NEW |