blob: 58b77d860e7b77b44a9f35094e472df249db4dab [file] [log] [blame]
Blink Reformat4c46d092018-04-07 15:32:371/*
2 * Copyright (C) 2013 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
Jan Schefflerc5bc69f2020-07-30 09:51:5231// @ts-nocheck
32// TODO(crbug.com/1011811): Enable TypeScript compiler checks
33
Tim van der Lippe9b2f8712020-02-12 17:46:2234import * as Components from '../components/components.js';
35import * as UI from '../ui/ui.js';
Patrick Brossete3474dc2020-09-29 12:38:2636import {ConsoleViewMessage} from './ConsoleViewMessage.js'; // eslint-disable-line no-unused-vars
Tim van der Lippe9b2f8712020-02-12 17:46:2237
Blink Reformat4c46d092018-04-07 15:32:3738/**
39 * @unrestricted
40 */
Tim van der Lippeeaacb722020-01-10 12:16:0041export class ConsoleViewport {
Blink Reformat4c46d092018-04-07 15:32:3742 /**
Tim van der Lippeeaacb722020-01-10 12:16:0043 * @param {!ConsoleViewportProvider} provider
Blink Reformat4c46d092018-04-07 15:32:3744 */
45 constructor(provider) {
Tim van der Lippe087966e2020-08-20 11:40:3246 this.element = document.createElement('div');
Blink Reformat4c46d092018-04-07 15:32:3747 this.element.style.overflow = 'auto';
48 this._topGapElement = this.element.createChild('div');
49 this._topGapElement.style.height = '0px';
50 this._topGapElement.style.color = 'transparent';
51 this._contentElement = this.element.createChild('div');
52 this._bottomGapElement = this.element.createChild('div');
53 this._bottomGapElement.style.height = '0px';
54 this._bottomGapElement.style.color = 'transparent';
55
56 // Text content needed for range intersection checks in _updateSelectionModel.
57 // Use Unicode ZERO WIDTH NO-BREAK SPACE, which avoids contributing any height to the element's layout overflow.
58 this._topGapElement.textContent = '\uFEFF';
59 this._bottomGapElement.textContent = '\uFEFF';
60
Joel Einbinderca8fa5a2018-08-24 23:30:2561 UI.ARIAUtils.markAsHidden(this._topGapElement);
62 UI.ARIAUtils.markAsHidden(this._bottomGapElement);
63
Blink Reformat4c46d092018-04-07 15:32:3764 this._provider = provider;
65 this.element.addEventListener('scroll', this._onScroll.bind(this), false);
66 this.element.addEventListener('copy', this._onCopy.bind(this), false);
67 this.element.addEventListener('dragstart', this._onDragStart.bind(this), false);
Pavel Feldmandb310912019-01-30 00:31:2068 this._contentElement.addEventListener('focusin', this._onFocusIn.bind(this), false);
69 this._contentElement.addEventListener('focusout', this._onFocusOut.bind(this), false);
70 this._contentElement.addEventListener('keydown', this._onKeyDown.bind(this), false);
Erik Luo39452ff2018-09-01 01:08:0771 this._virtualSelectedIndex = -1;
Erik Luob5bfff42018-09-20 02:52:3972 this._contentElement.tabIndex = -1;
Blink Reformat4c46d092018-04-07 15:32:3773
74 this._firstActiveIndex = -1;
75 this._lastActiveIndex = -1;
76 this._renderedItems = [];
77 this._anchorSelection = null;
78 this._headSelection = null;
79 this._itemCount = 0;
80 this._cumulativeHeights = new Int32Array(0);
81 this._muteCopyHandler = false;
82
83 // Listen for any changes to descendants and trigger a refresh. This ensures
84 // that items updated asynchronously will not break stick-to-bottom behavior
85 // if they change the scroll height.
86 this._observer = new MutationObserver(this.refresh.bind(this));
87 this._observerConfig = {childList: true, subtree: true};
88 }
89
90 /**
91 * @return {boolean}
92 */
93 stickToBottom() {
94 return this._stickToBottom;
95 }
96
97 /**
98 * @param {boolean} value
99 */
100 setStickToBottom(value) {
101 this._stickToBottom = value;
Tim van der Lippe1d6e57a2019-09-30 11:55:34102 if (this._stickToBottom) {
Blink Reformat4c46d092018-04-07 15:32:37103 this._observer.observe(this._contentElement, this._observerConfig);
Tim van der Lippe1d6e57a2019-09-30 11:55:34104 } else {
Blink Reformat4c46d092018-04-07 15:32:37105 this._observer.disconnect();
Tim van der Lippe1d6e57a2019-09-30 11:55:34106 }
Blink Reformat4c46d092018-04-07 15:32:37107 }
108
Erik Luoad6c91c2018-12-06 03:28:42109 /**
110 * @return {boolean}
111 */
112 hasVirtualSelection() {
113 return this._virtualSelectedIndex !== -1;
114 }
115
Blink Reformat4c46d092018-04-07 15:32:37116 copyWithStyles() {
117 this._muteCopyHandler = true;
118 this.element.ownerDocument.execCommand('copy');
119 this._muteCopyHandler = false;
120 }
121
122 /**
123 * @param {!Event} event
124 */
125 _onCopy(event) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34126 if (this._muteCopyHandler) {
Blink Reformat4c46d092018-04-07 15:32:37127 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34128 }
Patrick Brosset874a7302020-08-11 13:52:22129
Blink Reformat4c46d092018-04-07 15:32:37130 const text = this._selectedText();
Tim van der Lippe1d6e57a2019-09-30 11:55:34131 if (!text) {
Blink Reformat4c46d092018-04-07 15:32:37132 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34133 }
Patrick Brosset874a7302020-08-11 13:52:22134
Blink Reformat4c46d092018-04-07 15:32:37135 event.preventDefault();
Patrick Brosset874a7302020-08-11 13:52:22136
137 if (this._selectionContainsTable()) {
138 this.copyWithStyles();
139 } else {
140 event.clipboardData.setData('text/plain', text);
141 }
Blink Reformat4c46d092018-04-07 15:32:37142 }
143
144 /**
145 * @param {!Event} event
146 */
Erik Luo39452ff2018-09-01 01:08:07147 _onFocusIn(event) {
Erik Luob5bfff42018-09-20 02:52:39148 const renderedIndex = this._renderedItems.findIndex(item => item.element().isSelfOrAncestor(event.target));
Tim van der Lippe1d6e57a2019-09-30 11:55:34149 if (renderedIndex !== -1) {
Erik Luob5bfff42018-09-20 02:52:39150 this._virtualSelectedIndex = this._firstActiveIndex + renderedIndex;
Tim van der Lippe1d6e57a2019-09-30 11:55:34151 }
Erik Luofc6a6302018-11-02 06:48:52152 let focusLastChild = false;
Erik Luo39452ff2018-09-01 01:08:07153 // Make default selection when moving from external (e.g. prompt) to the container.
154 if (this._virtualSelectedIndex === -1 && this._isOutsideViewport(/** @type {?Element} */ (event.relatedTarget)) &&
Erik Luoad6c91c2018-12-06 03:28:42155 event.target === this._contentElement && this._itemCount) {
Erik Luofc6a6302018-11-02 06:48:52156 focusLastChild = true;
Erik Luo39452ff2018-09-01 01:08:07157 this._virtualSelectedIndex = this._itemCount - 1;
Erik Luo840be6b2018-12-03 20:54:27158
159 // Update stick to bottom before scrolling into view.
160 this.refresh();
161 this.scrollItemIntoView(this._virtualSelectedIndex);
Erik Luofc6a6302018-11-02 06:48:52162 }
163 this._updateFocusedItem(focusLastChild);
Erik Luo39452ff2018-09-01 01:08:07164 }
165
166 /**
167 * @param {!Event} event
168 */
169 _onFocusOut(event) {
170 // Remove selection when focus moves to external location (e.g. prompt).
Tim van der Lippe1d6e57a2019-09-30 11:55:34171 if (this._isOutsideViewport(/** @type {?Element} */ (event.relatedTarget))) {
Erik Luo39452ff2018-09-01 01:08:07172 this._virtualSelectedIndex = -1;
Tim van der Lippe1d6e57a2019-09-30 11:55:34173 }
Erik Luo39452ff2018-09-01 01:08:07174 this._updateFocusedItem();
175 }
176
177 /**
178 * @param {?Element} element
179 * @return {boolean}
180 */
181 _isOutsideViewport(element) {
Erik Luob5bfff42018-09-20 02:52:39182 return !!element && !element.isSelfOrDescendant(this._contentElement);
Erik Luo39452ff2018-09-01 01:08:07183 }
184
185 /**
186 * @param {!Event} event
187 */
Blink Reformat4c46d092018-04-07 15:32:37188 _onDragStart(event) {
189 const text = this._selectedText();
Tim van der Lippe1d6e57a2019-09-30 11:55:34190 if (!text) {
Blink Reformat4c46d092018-04-07 15:32:37191 return false;
Tim van der Lippe1d6e57a2019-09-30 11:55:34192 }
Blink Reformat4c46d092018-04-07 15:32:37193 event.dataTransfer.clearData();
194 event.dataTransfer.setData('text/plain', text);
195 event.dataTransfer.effectAllowed = 'copy';
196 return true;
197 }
198
199 /**
Erik Luo39452ff2018-09-01 01:08:07200 * @param {!Event} event
201 */
202 _onKeyDown(event) {
Tim van der Lippe9b2f8712020-02-12 17:46:22203 if (UI.UIUtils.isEditing() || !this._itemCount || event.shiftKey) {
Erik Luo39452ff2018-09-01 01:08:07204 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34205 }
Erik Luo0b8282e2018-10-08 20:37:46206 let isArrowUp = false;
Erik Luo39452ff2018-09-01 01:08:07207 switch (event.key) {
208 case 'ArrowUp':
Erik Luo0b8282e2018-10-08 20:37:46209 if (this._virtualSelectedIndex > 0) {
210 isArrowUp = true;
211 this._virtualSelectedIndex--;
212 } else {
213 return;
214 }
Erik Luo39452ff2018-09-01 01:08:07215 break;
216 case 'ArrowDown':
Tim van der Lippe1d6e57a2019-09-30 11:55:34217 if (this._virtualSelectedIndex < this._itemCount - 1) {
Erik Luo0b8282e2018-10-08 20:37:46218 this._virtualSelectedIndex++;
Tim van der Lippe1d6e57a2019-09-30 11:55:34219 } else {
Erik Luo0b8282e2018-10-08 20:37:46220 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34221 }
Erik Luo39452ff2018-09-01 01:08:07222 break;
223 case 'Home':
224 this._virtualSelectedIndex = 0;
225 break;
226 case 'End':
227 this._virtualSelectedIndex = this._itemCount - 1;
228 break;
229 default:
230 return;
231 }
232 event.consume(true);
233 this.scrollItemIntoView(this._virtualSelectedIndex);
Erik Luo0b8282e2018-10-08 20:37:46234 this._updateFocusedItem(isArrowUp);
Erik Luo39452ff2018-09-01 01:08:07235 }
236
Erik Luo0b8282e2018-10-08 20:37:46237 /**
238 * @param {boolean=} focusLastChild
239 */
240 _updateFocusedItem(focusLastChild) {
Erik Luo39452ff2018-09-01 01:08:07241 const selectedElement = this.renderedElementAt(this._virtualSelectedIndex);
242 const changed = this._lastSelectedElement !== selectedElement;
Erik Luob5bfff42018-09-20 02:52:39243 const containerHasFocus = this._contentElement === this.element.ownerDocument.deepActiveElement();
Tim van der Lippe1d6e57a2019-09-30 11:55:34244 if (this._lastSelectedElement && changed) {
Erik Luo39452ff2018-09-01 01:08:07245 this._lastSelectedElement.classList.remove('console-selected');
Tim van der Lippe1d6e57a2019-09-30 11:55:34246 }
Erik Luo840be6b2018-12-03 20:54:27247 if (selectedElement && (focusLastChild || changed || containerHasFocus) && this.element.hasFocus()) {
Erik Luo39452ff2018-09-01 01:08:07248 selectedElement.classList.add('console-selected');
Erik Luob5bfff42018-09-20 02:52:39249 // Do not focus the message if something within holds focus (e.g. object).
Erik Luo840be6b2018-12-03 20:54:27250 if (focusLastChild) {
251 this.setStickToBottom(false);
252 this._renderedItems[this._virtualSelectedIndex - this._firstActiveIndex].focusLastChildOrSelf();
253 } else if (!selectedElement.hasFocus()) {
Peter Marshall477ef992020-05-12 17:59:55254 selectedElement.focus({preventScroll: true});
Erik Luo0b8282e2018-10-08 20:37:46255 }
Erik Luo39452ff2018-09-01 01:08:07256 }
Tim van der Lippe1d6e57a2019-09-30 11:55:34257 if (this._itemCount && !this._contentElement.hasFocus()) {
Erik Luob5bfff42018-09-20 02:52:39258 this._contentElement.tabIndex = 0;
Tim van der Lippe1d6e57a2019-09-30 11:55:34259 } else {
Erik Luob5bfff42018-09-20 02:52:39260 this._contentElement.tabIndex = -1;
Tim van der Lippe1d6e57a2019-09-30 11:55:34261 }
Erik Luo39452ff2018-09-01 01:08:07262 this._lastSelectedElement = selectedElement;
Erik Luo39452ff2018-09-01 01:08:07263 }
264
265 /**
Blink Reformat4c46d092018-04-07 15:32:37266 * @return {!Element}
267 */
268 contentElement() {
269 return this._contentElement;
270 }
271
272 invalidate() {
273 delete this._cachedProviderElements;
274 this._itemCount = this._provider.itemCount();
Tim van der Lippe1d6e57a2019-09-30 11:55:34275 if (this._virtualSelectedIndex > this._itemCount - 1) {
Erik Luo39452ff2018-09-01 01:08:07276 this._virtualSelectedIndex = this._itemCount - 1;
Tim van der Lippe1d6e57a2019-09-30 11:55:34277 }
Blink Reformat4c46d092018-04-07 15:32:37278 this._rebuildCumulativeHeights();
279 this.refresh();
280 }
281
282 /**
283 * @param {number} index
Tim van der Lippeeaacb722020-01-10 12:16:00284 * @return {?ConsoleViewportElement}
Blink Reformat4c46d092018-04-07 15:32:37285 */
286 _providerElement(index) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34287 if (!this._cachedProviderElements) {
Blink Reformat4c46d092018-04-07 15:32:37288 this._cachedProviderElements = new Array(this._itemCount);
Tim van der Lippe1d6e57a2019-09-30 11:55:34289 }
Blink Reformat4c46d092018-04-07 15:32:37290 let element = this._cachedProviderElements[index];
291 if (!element) {
292 element = this._provider.itemElement(index);
293 this._cachedProviderElements[index] = element;
294 }
295 return element;
296 }
297
298 _rebuildCumulativeHeights() {
299 const firstActiveIndex = this._firstActiveIndex;
300 const lastActiveIndex = this._lastActiveIndex;
301 let height = 0;
302 this._cumulativeHeights = new Int32Array(this._itemCount);
303 for (let i = 0; i < this._itemCount; ++i) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34304 if (firstActiveIndex <= i && i - firstActiveIndex < this._renderedItems.length && i <= lastActiveIndex) {
Blink Reformat4c46d092018-04-07 15:32:37305 height += this._renderedItems[i - firstActiveIndex].element().offsetHeight;
Tim van der Lippe1d6e57a2019-09-30 11:55:34306 } else {
Blink Reformat4c46d092018-04-07 15:32:37307 height += this._provider.fastHeight(i);
Tim van der Lippe1d6e57a2019-09-30 11:55:34308 }
Blink Reformat4c46d092018-04-07 15:32:37309 this._cumulativeHeights[i] = height;
310 }
311 }
312
313 _rebuildCumulativeHeightsIfNeeded() {
Erik Luo4b002322018-07-30 21:23:31314 let totalCachedHeight = 0;
315 let totalMeasuredHeight = 0;
Blink Reformat4c46d092018-04-07 15:32:37316 // Check whether current items in DOM have changed heights. Tolerate 1-pixel
317 // error due to double-to-integer rounding errors.
318 for (let i = 0; i < this._renderedItems.length; ++i) {
319 const cachedItemHeight = this._cachedItemHeight(this._firstActiveIndex + i);
Erik Luo4b002322018-07-30 21:23:31320 const measuredHeight = this._renderedItems[i].element().offsetHeight;
321 if (Math.abs(cachedItemHeight - measuredHeight) > 1) {
Blink Reformat4c46d092018-04-07 15:32:37322 this._rebuildCumulativeHeights();
Erik Luo4b002322018-07-30 21:23:31323 return;
324 }
325 totalMeasuredHeight += measuredHeight;
326 totalCachedHeight += cachedItemHeight;
327 if (Math.abs(totalCachedHeight - totalMeasuredHeight) > 1) {
328 this._rebuildCumulativeHeights();
329 return;
Blink Reformat4c46d092018-04-07 15:32:37330 }
331 }
332 }
333
334 /**
335 * @param {number} index
336 * @return {number}
337 */
338 _cachedItemHeight(index) {
339 return index === 0 ? this._cumulativeHeights[0] :
340 this._cumulativeHeights[index] - this._cumulativeHeights[index - 1];
341 }
342
343 /**
344 * @param {?Selection} selection
345 * @suppressGlobalPropertiesCheck
346 */
347 _isSelectionBackwards(selection) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34348 if (!selection || !selection.rangeCount) {
Blink Reformat4c46d092018-04-07 15:32:37349 return false;
Tim van der Lippe1d6e57a2019-09-30 11:55:34350 }
Blink Reformat4c46d092018-04-07 15:32:37351 const range = document.createRange();
352 range.setStart(selection.anchorNode, selection.anchorOffset);
353 range.setEnd(selection.focusNode, selection.focusOffset);
354 return range.collapsed;
355 }
356
357 /**
358 * @param {number} itemIndex
359 * @param {!Node} node
360 * @param {number} offset
361 * @return {!{item: number, node: !Node, offset: number}}
362 */
363 _createSelectionModel(itemIndex, node, offset) {
364 return {item: itemIndex, node: node, offset: offset};
365 }
366
367 /**
368 * @param {?Selection} selection
369 */
370 _updateSelectionModel(selection) {
371 const range = selection && selection.rangeCount ? selection.getRangeAt(0) : null;
372 if (!range || selection.isCollapsed || !this.element.hasSelection()) {
373 this._headSelection = null;
374 this._anchorSelection = null;
375 return false;
376 }
377
378 let firstSelected = Number.MAX_VALUE;
379 let lastSelected = -1;
380
381 let hasVisibleSelection = false;
382 for (let i = 0; i < this._renderedItems.length; ++i) {
383 if (range.intersectsNode(this._renderedItems[i].element())) {
384 const index = i + this._firstActiveIndex;
385 firstSelected = Math.min(firstSelected, index);
386 lastSelected = Math.max(lastSelected, index);
387 hasVisibleSelection = true;
388 }
389 }
390 if (hasVisibleSelection) {
391 firstSelected =
392 this._createSelectionModel(firstSelected, /** @type {!Node} */ (range.startContainer), range.startOffset);
393 lastSelected =
394 this._createSelectionModel(lastSelected, /** @type {!Node} */ (range.endContainer), range.endOffset);
395 }
396 const topOverlap = range.intersectsNode(this._topGapElement) && this._topGapElement._active;
397 const bottomOverlap = range.intersectsNode(this._bottomGapElement) && this._bottomGapElement._active;
398 if (!topOverlap && !bottomOverlap && !hasVisibleSelection) {
399 this._headSelection = null;
400 this._anchorSelection = null;
401 return false;
402 }
403
404 if (!this._anchorSelection || !this._headSelection) {
405 this._anchorSelection = this._createSelectionModel(0, this.element, 0);
406 this._headSelection = this._createSelectionModel(this._itemCount - 1, this.element, this.element.children.length);
407 this._selectionIsBackward = false;
408 }
409
410 const isBackward = this._isSelectionBackwards(selection);
411 const startSelection = this._selectionIsBackward ? this._headSelection : this._anchorSelection;
412 const endSelection = this._selectionIsBackward ? this._anchorSelection : this._headSelection;
413 if (topOverlap && bottomOverlap && hasVisibleSelection) {
414 firstSelected = firstSelected.item < startSelection.item ? firstSelected : startSelection;
415 lastSelected = lastSelected.item > endSelection.item ? lastSelected : endSelection;
416 } else if (!hasVisibleSelection) {
417 firstSelected = startSelection;
418 lastSelected = endSelection;
419 } else if (topOverlap) {
420 firstSelected = isBackward ? this._headSelection : this._anchorSelection;
421 } else if (bottomOverlap) {
422 lastSelected = isBackward ? this._anchorSelection : this._headSelection;
423 }
424
425 if (isBackward) {
426 this._anchorSelection = lastSelected;
427 this._headSelection = firstSelected;
428 } else {
429 this._anchorSelection = firstSelected;
430 this._headSelection = lastSelected;
431 }
432 this._selectionIsBackward = isBackward;
433 return true;
434 }
435
436 /**
437 * @param {?Selection} selection
438 */
439 _restoreSelection(selection) {
440 let anchorElement = null;
441 let anchorOffset;
442 if (this._firstActiveIndex <= this._anchorSelection.item && this._anchorSelection.item <= this._lastActiveIndex) {
443 anchorElement = this._anchorSelection.node;
444 anchorOffset = this._anchorSelection.offset;
445 } else {
Tim van der Lippe1d6e57a2019-09-30 11:55:34446 if (this._anchorSelection.item < this._firstActiveIndex) {
Blink Reformat4c46d092018-04-07 15:32:37447 anchorElement = this._topGapElement;
Tim van der Lippe1d6e57a2019-09-30 11:55:34448 } else if (this._anchorSelection.item > this._lastActiveIndex) {
Blink Reformat4c46d092018-04-07 15:32:37449 anchorElement = this._bottomGapElement;
Tim van der Lippe1d6e57a2019-09-30 11:55:34450 }
Blink Reformat4c46d092018-04-07 15:32:37451 anchorOffset = this._selectionIsBackward ? 1 : 0;
452 }
453
454 let headElement = null;
455 let headOffset;
456 if (this._firstActiveIndex <= this._headSelection.item && this._headSelection.item <= this._lastActiveIndex) {
457 headElement = this._headSelection.node;
458 headOffset = this._headSelection.offset;
459 } else {
Tim van der Lippe1d6e57a2019-09-30 11:55:34460 if (this._headSelection.item < this._firstActiveIndex) {
Blink Reformat4c46d092018-04-07 15:32:37461 headElement = this._topGapElement;
Tim van der Lippe1d6e57a2019-09-30 11:55:34462 } else if (this._headSelection.item > this._lastActiveIndex) {
Blink Reformat4c46d092018-04-07 15:32:37463 headElement = this._bottomGapElement;
Tim van der Lippe1d6e57a2019-09-30 11:55:34464 }
Blink Reformat4c46d092018-04-07 15:32:37465 headOffset = this._selectionIsBackward ? 0 : 1;
466 }
467
468 selection.setBaseAndExtent(anchorElement, anchorOffset, headElement, headOffset);
469 }
470
Patrick Brosset874a7302020-08-11 13:52:22471 _selectionContainsTable() {
472 if (!this._anchorSelection || !this._headSelection) {
473 return false;
474 }
475
Patrick Brossetdc278772020-09-07 13:36:37476 const start = this._selectionIsBackward ? this._headSelection.item : this._anchorSelection.item;
477 const end = this._selectionIsBackward ? this._anchorSelection.item : this._headSelection.item;
478
479 for (let i = start; i <= end; i++) {
480 const element = /** @type {!ConsoleViewMessage} */ (this._providerElement(i));
481 if (element && element.consoleMessage().type === 'table') {
Patrick Brosset874a7302020-08-11 13:52:22482 return true;
483 }
484 }
485
486 return false;
487 }
488
Blink Reformat4c46d092018-04-07 15:32:37489 refresh() {
490 this._observer.disconnect();
491 this._innerRefresh();
Tim van der Lippe1d6e57a2019-09-30 11:55:34492 if (this._stickToBottom) {
Blink Reformat4c46d092018-04-07 15:32:37493 this._observer.observe(this._contentElement, this._observerConfig);
Tim van der Lippe1d6e57a2019-09-30 11:55:34494 }
Blink Reformat4c46d092018-04-07 15:32:37495 }
496
497 _innerRefresh() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34498 if (!this._visibleHeight()) {
499 return;
500 } // Do nothing for invisible controls.
Blink Reformat4c46d092018-04-07 15:32:37501
502 if (!this._itemCount) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34503 for (let i = 0; i < this._renderedItems.length; ++i) {
Blink Reformat4c46d092018-04-07 15:32:37504 this._renderedItems[i].willHide();
Tim van der Lippe1d6e57a2019-09-30 11:55:34505 }
Blink Reformat4c46d092018-04-07 15:32:37506 this._renderedItems = [];
507 this._contentElement.removeChildren();
508 this._topGapElement.style.height = '0px';
509 this._bottomGapElement.style.height = '0px';
510 this._firstActiveIndex = -1;
511 this._lastActiveIndex = -1;
Pavel Feldmandb310912019-01-30 00:31:20512 this._updateFocusedItem();
Blink Reformat4c46d092018-04-07 15:32:37513 return;
514 }
515
516 const selection = this.element.getComponentSelection();
517 const shouldRestoreSelection = this._updateSelectionModel(selection);
518
519 const visibleFrom = this.element.scrollTop;
520 const visibleHeight = this._visibleHeight();
521 const activeHeight = visibleHeight * 2;
522 this._rebuildCumulativeHeightsIfNeeded();
523
524 // When the viewport is scrolled to the bottom, using the cumulative heights estimate is not
525 // precise enough to determine next visible indices. This stickToBottom check avoids extra
526 // calls to refresh in those cases.
527 if (this._stickToBottom) {
528 this._firstActiveIndex =
529 Math.max(this._itemCount - Math.ceil(activeHeight / this._provider.minimumRowHeight()), 0);
530 this._lastActiveIndex = this._itemCount - 1;
531 } else {
532 this._firstActiveIndex =
533 Math.max(this._cumulativeHeights.lowerBound(visibleFrom + 1 - (activeHeight - visibleHeight) / 2), 0);
534 // Proactively render more rows in case some of them will be collapsed without triggering refresh. @see crbug.com/390169
535 this._lastActiveIndex = this._firstActiveIndex + Math.ceil(activeHeight / this._provider.minimumRowHeight()) - 1;
536 this._lastActiveIndex = Math.min(this._lastActiveIndex, this._itemCount - 1);
537 }
538
539 const topGapHeight = this._cumulativeHeights[this._firstActiveIndex - 1] || 0;
540 const bottomGapHeight =
541 this._cumulativeHeights[this._cumulativeHeights.length - 1] - this._cumulativeHeights[this._lastActiveIndex];
542
543 /**
Tim van der Lippeeaacb722020-01-10 12:16:00544 * @this {ConsoleViewport}
Blink Reformat4c46d092018-04-07 15:32:37545 */
546 function prepare() {
547 this._topGapElement.style.height = topGapHeight + 'px';
548 this._bottomGapElement.style.height = bottomGapHeight + 'px';
549 this._topGapElement._active = !!topGapHeight;
550 this._bottomGapElement._active = !!bottomGapHeight;
551 this._contentElement.style.setProperty('height', '10000000px');
552 }
553
554 this._partialViewportUpdate(prepare.bind(this));
555 this._contentElement.style.removeProperty('height');
556 // Should be the last call in the method as it might force layout.
Tim van der Lippe1d6e57a2019-09-30 11:55:34557 if (shouldRestoreSelection) {
Blink Reformat4c46d092018-04-07 15:32:37558 this._restoreSelection(selection);
Tim van der Lippe1d6e57a2019-09-30 11:55:34559 }
560 if (this._stickToBottom) {
Blink Reformat4c46d092018-04-07 15:32:37561 this.element.scrollTop = 10000000;
Tim van der Lippe1d6e57a2019-09-30 11:55:34562 }
Blink Reformat4c46d092018-04-07 15:32:37563 }
564
565 /**
566 * @param {function()} prepare
567 */
568 _partialViewportUpdate(prepare) {
569 const itemsToRender = new Set();
Tim van der Lippe1d6e57a2019-09-30 11:55:34570 for (let i = this._firstActiveIndex; i <= this._lastActiveIndex; ++i) {
Blink Reformat4c46d092018-04-07 15:32:37571 itemsToRender.add(this._providerElement(i));
Tim van der Lippe1d6e57a2019-09-30 11:55:34572 }
Blink Reformat4c46d092018-04-07 15:32:37573 const willBeHidden = this._renderedItems.filter(item => !itemsToRender.has(item));
Tim van der Lippe1d6e57a2019-09-30 11:55:34574 for (let i = 0; i < willBeHidden.length; ++i) {
Blink Reformat4c46d092018-04-07 15:32:37575 willBeHidden[i].willHide();
Tim van der Lippe1d6e57a2019-09-30 11:55:34576 }
Blink Reformat4c46d092018-04-07 15:32:37577 prepare();
Erik Luo39452ff2018-09-01 01:08:07578 let hadFocus = false;
579 for (let i = 0; i < willBeHidden.length; ++i) {
Pavel Feldmandb310912019-01-30 00:31:20580 hadFocus = hadFocus || willBeHidden[i].element().hasFocus();
Blink Reformat4c46d092018-04-07 15:32:37581 willBeHidden[i].element().remove();
Erik Luo39452ff2018-09-01 01:08:07582 }
Blink Reformat4c46d092018-04-07 15:32:37583
584 const wasShown = [];
585 let anchor = this._contentElement.firstChild;
586 for (const viewportElement of itemsToRender) {
587 const element = viewportElement.element();
588 if (element !== anchor) {
589 const shouldCallWasShown = !element.parentElement;
Tim van der Lippe1d6e57a2019-09-30 11:55:34590 if (shouldCallWasShown) {
Blink Reformat4c46d092018-04-07 15:32:37591 wasShown.push(viewportElement);
Tim van der Lippe1d6e57a2019-09-30 11:55:34592 }
Blink Reformat4c46d092018-04-07 15:32:37593 this._contentElement.insertBefore(element, anchor);
594 } else {
595 anchor = anchor.nextSibling;
596 }
597 }
Tim van der Lippe1d6e57a2019-09-30 11:55:34598 for (let i = 0; i < wasShown.length; ++i) {
Blink Reformat4c46d092018-04-07 15:32:37599 wasShown[i].wasShown();
Tim van der Lippe1d6e57a2019-09-30 11:55:34600 }
Blink Reformat4c46d092018-04-07 15:32:37601 this._renderedItems = Array.from(itemsToRender);
Erik Luo39452ff2018-09-01 01:08:07602
Tim van der Lippe1d6e57a2019-09-30 11:55:34603 if (hadFocus) {
Pavel Feldmandb310912019-01-30 00:31:20604 this._contentElement.focus();
Tim van der Lippe1d6e57a2019-09-30 11:55:34605 }
Pavel Feldmandb310912019-01-30 00:31:20606 this._updateFocusedItem();
Blink Reformat4c46d092018-04-07 15:32:37607 }
608
609 /**
610 * @return {?string}
611 */
612 _selectedText() {
613 this._updateSelectionModel(this.element.getComponentSelection());
Tim van der Lippe1d6e57a2019-09-30 11:55:34614 if (!this._headSelection || !this._anchorSelection) {
Blink Reformat4c46d092018-04-07 15:32:37615 return null;
Tim van der Lippe1d6e57a2019-09-30 11:55:34616 }
Blink Reformat4c46d092018-04-07 15:32:37617
618 let startSelection = null;
619 let endSelection = null;
620 if (this._selectionIsBackward) {
621 startSelection = this._headSelection;
622 endSelection = this._anchorSelection;
623 } else {
624 startSelection = this._anchorSelection;
625 endSelection = this._headSelection;
626 }
627
628 const textLines = [];
629 for (let i = startSelection.item; i <= endSelection.item; ++i) {
630 const element = this._providerElement(i).element();
Tim van der Lippe9b2f8712020-02-12 17:46:22631 const lineContent = element.childTextNodes().map(Components.Linkifier.Linkifier.untruncatedNodeText).join('');
Blink Reformat4c46d092018-04-07 15:32:37632 textLines.push(lineContent);
633 }
634
635 const endSelectionElement = this._providerElement(endSelection.item).element();
636 if (endSelection.node && endSelection.node.isSelfOrDescendant(endSelectionElement)) {
637 const itemTextOffset = this._textOffsetInNode(endSelectionElement, endSelection.node, endSelection.offset);
638 textLines[textLines.length - 1] = textLines.peekLast().substring(0, itemTextOffset);
639 }
640
641 const startSelectionElement = this._providerElement(startSelection.item).element();
642 if (startSelection.node && startSelection.node.isSelfOrDescendant(startSelectionElement)) {
643 const itemTextOffset = this._textOffsetInNode(startSelectionElement, startSelection.node, startSelection.offset);
644 textLines[0] = textLines[0].substring(itemTextOffset);
645 }
646
647 return textLines.join('\n');
648 }
649
650 /**
651 * @param {!Element} itemElement
652 * @param {!Node} selectionNode
653 * @param {number} offset
654 * @return {number}
655 */
656 _textOffsetInNode(itemElement, selectionNode, offset) {
657 // If the selectionNode is not a TextNode, we may need to convert a child offset into a character offset.
658 if (selectionNode.nodeType !== Node.TEXT_NODE) {
659 if (offset < selectionNode.childNodes.length) {
660 selectionNode = /** @type {!Node} */ (selectionNode.childNodes.item(offset));
661 offset = 0;
662 } else {
663 offset = selectionNode.textContent.length;
664 }
665 }
666
667 let chars = 0;
668 let node = itemElement;
669 while ((node = node.traverseNextNode(itemElement)) && node !== selectionNode) {
670 if (node.nodeType !== Node.TEXT_NODE || node.parentElement.nodeName === 'STYLE' ||
Tim van der Lippe1d6e57a2019-09-30 11:55:34671 node.parentElement.nodeName === 'SCRIPT') {
Blink Reformat4c46d092018-04-07 15:32:37672 continue;
Tim van der Lippe1d6e57a2019-09-30 11:55:34673 }
Tim van der Lippe9b2f8712020-02-12 17:46:22674 chars += Components.Linkifier.Linkifier.untruncatedNodeText(node).length;
Blink Reformat4c46d092018-04-07 15:32:37675 }
676 // If the selected node text was truncated, treat any non-zero offset as the full length.
Tim van der Lippe9b2f8712020-02-12 17:46:22677 const untruncatedContainerLength = Components.Linkifier.Linkifier.untruncatedNodeText(selectionNode).length;
Tim van der Lippe1d6e57a2019-09-30 11:55:34678 if (offset > 0 && untruncatedContainerLength !== selectionNode.textContent.length) {
Blink Reformat4c46d092018-04-07 15:32:37679 offset = untruncatedContainerLength;
Tim van der Lippe1d6e57a2019-09-30 11:55:34680 }
Blink Reformat4c46d092018-04-07 15:32:37681 return chars + offset;
682 }
683
684 /**
685 * @param {!Event} event
686 */
687 _onScroll(event) {
688 this.refresh();
689 }
690
691 /**
692 * @return {number}
693 */
694 firstVisibleIndex() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34695 if (!this._cumulativeHeights.length) {
Blink Reformat4c46d092018-04-07 15:32:37696 return -1;
Tim van der Lippe1d6e57a2019-09-30 11:55:34697 }
Blink Reformat4c46d092018-04-07 15:32:37698 this._rebuildCumulativeHeightsIfNeeded();
699 return this._cumulativeHeights.lowerBound(this.element.scrollTop + 1);
700 }
701
702 /**
703 * @return {number}
704 */
705 lastVisibleIndex() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34706 if (!this._cumulativeHeights.length) {
Blink Reformat4c46d092018-04-07 15:32:37707 return -1;
Tim van der Lippe1d6e57a2019-09-30 11:55:34708 }
Blink Reformat4c46d092018-04-07 15:32:37709 this._rebuildCumulativeHeightsIfNeeded();
710 const scrollBottom = this.element.scrollTop + this.element.clientHeight;
711 const right = this._itemCount - 1;
712 return this._cumulativeHeights.lowerBound(scrollBottom, undefined, undefined, right);
713 }
714
715 /**
716 * @return {?Element}
717 */
718 renderedElementAt(index) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34719 if (index === -1 || index < this._firstActiveIndex || index > this._lastActiveIndex) {
Blink Reformat4c46d092018-04-07 15:32:37720 return null;
Tim van der Lippe1d6e57a2019-09-30 11:55:34721 }
Blink Reformat4c46d092018-04-07 15:32:37722 return this._renderedItems[index - this._firstActiveIndex].element();
723 }
724
725 /**
726 * @param {number} index
727 * @param {boolean=} makeLast
728 */
729 scrollItemIntoView(index, makeLast) {
730 const firstVisibleIndex = this.firstVisibleIndex();
731 const lastVisibleIndex = this.lastVisibleIndex();
Tim van der Lippe1d6e57a2019-09-30 11:55:34732 if (index > firstVisibleIndex && index < lastVisibleIndex) {
Blink Reformat4c46d092018-04-07 15:32:37733 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34734 }
Erik Luo4b002322018-07-30 21:23:31735 // If the prompt is visible, then the last item must be fully on screen.
Tim van der Lippe1d6e57a2019-09-30 11:55:34736 if (index === lastVisibleIndex &&
737 this._cumulativeHeights[index] <= this.element.scrollTop + this._visibleHeight()) {
Erik Luo4b002322018-07-30 21:23:31738 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34739 }
740 if (makeLast) {
Blink Reformat4c46d092018-04-07 15:32:37741 this.forceScrollItemToBeLast(index);
Tim van der Lippe1d6e57a2019-09-30 11:55:34742 } else if (index <= firstVisibleIndex) {
Blink Reformat4c46d092018-04-07 15:32:37743 this.forceScrollItemToBeFirst(index);
Tim van der Lippe1d6e57a2019-09-30 11:55:34744 } else if (index >= lastVisibleIndex) {
Blink Reformat4c46d092018-04-07 15:32:37745 this.forceScrollItemToBeLast(index);
Tim van der Lippe1d6e57a2019-09-30 11:55:34746 }
Blink Reformat4c46d092018-04-07 15:32:37747 }
748
749 /**
750 * @param {number} index
751 */
752 forceScrollItemToBeFirst(index) {
753 console.assert(index >= 0 && index < this._itemCount, 'Cannot scroll item at invalid index');
754 this.setStickToBottom(false);
755 this._rebuildCumulativeHeightsIfNeeded();
756 this.element.scrollTop = index > 0 ? this._cumulativeHeights[index - 1] : 0;
Tim van der Lippe1d6e57a2019-09-30 11:55:34757 if (this.element.isScrolledToBottom()) {
Blink Reformat4c46d092018-04-07 15:32:37758 this.setStickToBottom(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:34759 }
Blink Reformat4c46d092018-04-07 15:32:37760 this.refresh();
Erik Luo4b002322018-07-30 21:23:31761 // After refresh, the item is in DOM, but may not be visible (items above were larger than expected).
762 this.renderedElementAt(index).scrollIntoView(true /* alignTop */);
Blink Reformat4c46d092018-04-07 15:32:37763 }
764
765 /**
766 * @param {number} index
767 */
768 forceScrollItemToBeLast(index) {
769 console.assert(index >= 0 && index < this._itemCount, 'Cannot scroll item at invalid index');
770 this.setStickToBottom(false);
771 this._rebuildCumulativeHeightsIfNeeded();
772 this.element.scrollTop = this._cumulativeHeights[index] - this._visibleHeight();
Tim van der Lippe1d6e57a2019-09-30 11:55:34773 if (this.element.isScrolledToBottom()) {
Blink Reformat4c46d092018-04-07 15:32:37774 this.setStickToBottom(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:34775 }
Blink Reformat4c46d092018-04-07 15:32:37776 this.refresh();
Erik Luo4b002322018-07-30 21:23:31777 // After refresh, the item is in DOM, but may not be visible (items above were larger than expected).
778 this.renderedElementAt(index).scrollIntoView(false /* alignTop */);
Blink Reformat4c46d092018-04-07 15:32:37779 }
780
781 /**
782 * @return {number}
783 */
784 _visibleHeight() {
785 // Use offsetHeight instead of clientHeight to avoid being affected by horizontal scroll.
786 return this.element.offsetHeight;
787 }
Paul Lewisbf7aa3c2019-11-20 17:03:38788}
Blink Reformat4c46d092018-04-07 15:32:37789
790/**
791 * @interface
792 */
Tim van der Lippeeaacb722020-01-10 12:16:00793export class ConsoleViewportProvider {
Blink Reformat4c46d092018-04-07 15:32:37794 /**
795 * @param {number} index
796 * @return {number}
797 */
798 fastHeight(index) {
799 return 0;
Paul Lewisbf7aa3c2019-11-20 17:03:38800 }
Blink Reformat4c46d092018-04-07 15:32:37801
802 /**
803 * @return {number}
804 */
805 itemCount() {
806 return 0;
Paul Lewisbf7aa3c2019-11-20 17:03:38807 }
Blink Reformat4c46d092018-04-07 15:32:37808
809 /**
810 * @return {number}
811 */
812 minimumRowHeight() {
813 return 0;
Paul Lewisbf7aa3c2019-11-20 17:03:38814 }
Blink Reformat4c46d092018-04-07 15:32:37815
816 /**
817 * @param {number} index
Tim van der Lippeeaacb722020-01-10 12:16:00818 * @return {?ConsoleViewportElement}
Blink Reformat4c46d092018-04-07 15:32:37819 */
820 itemElement(index) {
821 return null;
822 }
Paul Lewisbf7aa3c2019-11-20 17:03:38823}
Blink Reformat4c46d092018-04-07 15:32:37824
825/**
826 * @interface
827 */
Paul Lewisbf7aa3c2019-11-20 17:03:38828export class ConsoleViewportElement {
829 willHide() {
830 }
Blink Reformat4c46d092018-04-07 15:32:37831
Paul Lewisbf7aa3c2019-11-20 17:03:38832 wasShown() {
833 }
Blink Reformat4c46d092018-04-07 15:32:37834
835 /**
836 * @return {!Element}
837 */
Paul Lewisbf7aa3c2019-11-20 17:03:38838 element() {
839 }
840}