Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(328)

Side by Side Diff: third_party/WebKit/Source/devtools/front_end/ui/TextPrompt.js

Issue 2466123002: DevTools: reformat front-end code to match chromium style. (Closed)
Patch Set: all done Created 4 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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 };
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698