blob: 80fb896ab99afaadb80358506ea4df4c9d36d5cd [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) {
Sigurd Schneider53f33522020-10-08 15:00:4946 this.element = /** @type {!HTMLElement} */ (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;
Sigurd Schneider3fe3c212020-10-06 05:14:41372 if (!range || (!selection || selection.isCollapsed) || !this.element.hasSelection()) {
Blink Reformat4c46d092018-04-07 15:32:37373 this._headSelection = null;
374 this._anchorSelection = null;
375 return false;
376 }
377
Sigurd Schneider3fe3c212020-10-06 05:14:41378 let firstSelectedIndex = Number.MAX_VALUE;
379 let lastSelectedIndex = -1;
Blink Reformat4c46d092018-04-07 15:32:37380
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;
Sigurd Schneider3fe3c212020-10-06 05:14:41385 firstSelectedIndex = Math.min(firstSelectedIndex, index);
386 lastSelectedIndex = Math.max(lastSelectedIndex, index);
Blink Reformat4c46d092018-04-07 15:32:37387 hasVisibleSelection = true;
388 }
389 }
Sigurd Schneider3fe3c212020-10-06 05:14:41390
Blink Reformat4c46d092018-04-07 15:32:37391 const topOverlap = range.intersectsNode(this._topGapElement) && this._topGapElement._active;
392 const bottomOverlap = range.intersectsNode(this._bottomGapElement) && this._bottomGapElement._active;
393 if (!topOverlap && !bottomOverlap && !hasVisibleSelection) {
394 this._headSelection = null;
395 this._anchorSelection = null;
396 return false;
397 }
398
399 if (!this._anchorSelection || !this._headSelection) {
400 this._anchorSelection = this._createSelectionModel(0, this.element, 0);
401 this._headSelection = this._createSelectionModel(this._itemCount - 1, this.element, this.element.children.length);
402 this._selectionIsBackward = false;
403 }
404
405 const isBackward = this._isSelectionBackwards(selection);
406 const startSelection = this._selectionIsBackward ? this._headSelection : this._anchorSelection;
407 const endSelection = this._selectionIsBackward ? this._anchorSelection : this._headSelection;
Sigurd Schneider3fe3c212020-10-06 05:14:41408 let firstSelected = null;
409 let lastSelected = null;
410 if (hasVisibleSelection) {
411 firstSelected = this._createSelectionModel(
412 firstSelectedIndex, /** @type {!Node} */ (range.startContainer), range.startOffset);
413 lastSelected =
414 this._createSelectionModel(lastSelectedIndex, /** @type {!Node} */ (range.endContainer), range.endOffset);
415 }
Blink Reformat4c46d092018-04-07 15:32:37416 if (topOverlap && bottomOverlap && hasVisibleSelection) {
Sigurd Schneider3fe3c212020-10-06 05:14:41417 firstSelected = (firstSelected && firstSelected.item < startSelection.item) ? firstSelected : startSelection;
418 lastSelected = (lastSelected && lastSelected.item > endSelection.item) ? lastSelected : endSelection;
Blink Reformat4c46d092018-04-07 15:32:37419 } else if (!hasVisibleSelection) {
420 firstSelected = startSelection;
421 lastSelected = endSelection;
422 } else if (topOverlap) {
423 firstSelected = isBackward ? this._headSelection : this._anchorSelection;
424 } else if (bottomOverlap) {
425 lastSelected = isBackward ? this._anchorSelection : this._headSelection;
426 }
427
428 if (isBackward) {
429 this._anchorSelection = lastSelected;
430 this._headSelection = firstSelected;
431 } else {
432 this._anchorSelection = firstSelected;
433 this._headSelection = lastSelected;
434 }
435 this._selectionIsBackward = isBackward;
436 return true;
437 }
438
439 /**
440 * @param {?Selection} selection
441 */
442 _restoreSelection(selection) {
Sigurd Schneidera6388b02020-10-06 05:16:45443 if (!selection || !this._anchorSelection || !this._headSelection) {
444 return;
Blink Reformat4c46d092018-04-07 15:32:37445 }
446
Sigurd Schneidera6388b02020-10-06 05:16:45447 /**
448 * @param {!{item: number, node: !Node, offset: number}} selection
449 * @param {boolean} isSelectionBackwards
450 * @return {!{element: !Node, offset: number}}
451 */
452 const clampSelection = (selection, isSelectionBackwards) => {
453 if (this._firstActiveIndex <= selection.item && selection.item <= this._lastActiveIndex) {
454 return {element: selection.node, offset: selection.offset};
Tim van der Lippe1d6e57a2019-09-30 11:55:34455 }
Blink Reformat4c46d092018-04-07 15:32:37456
Sigurd Schneidera6388b02020-10-06 05:16:45457 const element = selection.item < this._firstActiveIndex ? this._topGapElement : this._bottomGapElement;
458 return {element, offset: isSelectionBackwards ? 1 : 0};
459 };
460
461 const {element: anchorElement, offset: anchorOffset} =
462 clampSelection(this._anchorSelection, !!this._selectionIsBackward);
463 const {element: headElement, offset: headOffset} = clampSelection(this._headSelection, !this._selectionIsBackward);
Blink Reformat4c46d092018-04-07 15:32:37464 selection.setBaseAndExtent(anchorElement, anchorOffset, headElement, headOffset);
465 }
466
Patrick Brosset874a7302020-08-11 13:52:22467 _selectionContainsTable() {
468 if (!this._anchorSelection || !this._headSelection) {
469 return false;
470 }
471
Patrick Brossetdc278772020-09-07 13:36:37472 const start = this._selectionIsBackward ? this._headSelection.item : this._anchorSelection.item;
473 const end = this._selectionIsBackward ? this._anchorSelection.item : this._headSelection.item;
474
475 for (let i = start; i <= end; i++) {
476 const element = /** @type {!ConsoleViewMessage} */ (this._providerElement(i));
477 if (element && element.consoleMessage().type === 'table') {
Patrick Brosset874a7302020-08-11 13:52:22478 return true;
479 }
480 }
481
482 return false;
483 }
484
Blink Reformat4c46d092018-04-07 15:32:37485 refresh() {
486 this._observer.disconnect();
487 this._innerRefresh();
Tim van der Lippe1d6e57a2019-09-30 11:55:34488 if (this._stickToBottom) {
Blink Reformat4c46d092018-04-07 15:32:37489 this._observer.observe(this._contentElement, this._observerConfig);
Tim van der Lippe1d6e57a2019-09-30 11:55:34490 }
Blink Reformat4c46d092018-04-07 15:32:37491 }
492
493 _innerRefresh() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34494 if (!this._visibleHeight()) {
495 return;
496 } // Do nothing for invisible controls.
Blink Reformat4c46d092018-04-07 15:32:37497
498 if (!this._itemCount) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34499 for (let i = 0; i < this._renderedItems.length; ++i) {
Blink Reformat4c46d092018-04-07 15:32:37500 this._renderedItems[i].willHide();
Tim van der Lippe1d6e57a2019-09-30 11:55:34501 }
Blink Reformat4c46d092018-04-07 15:32:37502 this._renderedItems = [];
503 this._contentElement.removeChildren();
504 this._topGapElement.style.height = '0px';
505 this._bottomGapElement.style.height = '0px';
506 this._firstActiveIndex = -1;
507 this._lastActiveIndex = -1;
Pavel Feldmandb310912019-01-30 00:31:20508 this._updateFocusedItem();
Blink Reformat4c46d092018-04-07 15:32:37509 return;
510 }
511
512 const selection = this.element.getComponentSelection();
513 const shouldRestoreSelection = this._updateSelectionModel(selection);
514
515 const visibleFrom = this.element.scrollTop;
516 const visibleHeight = this._visibleHeight();
517 const activeHeight = visibleHeight * 2;
518 this._rebuildCumulativeHeightsIfNeeded();
519
520 // When the viewport is scrolled to the bottom, using the cumulative heights estimate is not
521 // precise enough to determine next visible indices. This stickToBottom check avoids extra
522 // calls to refresh in those cases.
523 if (this._stickToBottom) {
524 this._firstActiveIndex =
525 Math.max(this._itemCount - Math.ceil(activeHeight / this._provider.minimumRowHeight()), 0);
526 this._lastActiveIndex = this._itemCount - 1;
527 } else {
528 this._firstActiveIndex =
529 Math.max(this._cumulativeHeights.lowerBound(visibleFrom + 1 - (activeHeight - visibleHeight) / 2), 0);
530 // Proactively render more rows in case some of them will be collapsed without triggering refresh. @see crbug.com/390169
531 this._lastActiveIndex = this._firstActiveIndex + Math.ceil(activeHeight / this._provider.minimumRowHeight()) - 1;
532 this._lastActiveIndex = Math.min(this._lastActiveIndex, this._itemCount - 1);
533 }
534
535 const topGapHeight = this._cumulativeHeights[this._firstActiveIndex - 1] || 0;
536 const bottomGapHeight =
537 this._cumulativeHeights[this._cumulativeHeights.length - 1] - this._cumulativeHeights[this._lastActiveIndex];
538
539 /**
Tim van der Lippeeaacb722020-01-10 12:16:00540 * @this {ConsoleViewport}
Blink Reformat4c46d092018-04-07 15:32:37541 */
542 function prepare() {
543 this._topGapElement.style.height = topGapHeight + 'px';
544 this._bottomGapElement.style.height = bottomGapHeight + 'px';
545 this._topGapElement._active = !!topGapHeight;
546 this._bottomGapElement._active = !!bottomGapHeight;
547 this._contentElement.style.setProperty('height', '10000000px');
548 }
549
550 this._partialViewportUpdate(prepare.bind(this));
551 this._contentElement.style.removeProperty('height');
552 // Should be the last call in the method as it might force layout.
Tim van der Lippe1d6e57a2019-09-30 11:55:34553 if (shouldRestoreSelection) {
Blink Reformat4c46d092018-04-07 15:32:37554 this._restoreSelection(selection);
Tim van der Lippe1d6e57a2019-09-30 11:55:34555 }
556 if (this._stickToBottom) {
Blink Reformat4c46d092018-04-07 15:32:37557 this.element.scrollTop = 10000000;
Tim van der Lippe1d6e57a2019-09-30 11:55:34558 }
Blink Reformat4c46d092018-04-07 15:32:37559 }
560
561 /**
562 * @param {function()} prepare
563 */
564 _partialViewportUpdate(prepare) {
565 const itemsToRender = new Set();
Tim van der Lippe1d6e57a2019-09-30 11:55:34566 for (let i = this._firstActiveIndex; i <= this._lastActiveIndex; ++i) {
Blink Reformat4c46d092018-04-07 15:32:37567 itemsToRender.add(this._providerElement(i));
Tim van der Lippe1d6e57a2019-09-30 11:55:34568 }
Blink Reformat4c46d092018-04-07 15:32:37569 const willBeHidden = this._renderedItems.filter(item => !itemsToRender.has(item));
Tim van der Lippe1d6e57a2019-09-30 11:55:34570 for (let i = 0; i < willBeHidden.length; ++i) {
Blink Reformat4c46d092018-04-07 15:32:37571 willBeHidden[i].willHide();
Tim van der Lippe1d6e57a2019-09-30 11:55:34572 }
Blink Reformat4c46d092018-04-07 15:32:37573 prepare();
Erik Luo39452ff2018-09-01 01:08:07574 let hadFocus = false;
575 for (let i = 0; i < willBeHidden.length; ++i) {
Pavel Feldmandb310912019-01-30 00:31:20576 hadFocus = hadFocus || willBeHidden[i].element().hasFocus();
Blink Reformat4c46d092018-04-07 15:32:37577 willBeHidden[i].element().remove();
Erik Luo39452ff2018-09-01 01:08:07578 }
Blink Reformat4c46d092018-04-07 15:32:37579
580 const wasShown = [];
581 let anchor = this._contentElement.firstChild;
582 for (const viewportElement of itemsToRender) {
583 const element = viewportElement.element();
584 if (element !== anchor) {
585 const shouldCallWasShown = !element.parentElement;
Tim van der Lippe1d6e57a2019-09-30 11:55:34586 if (shouldCallWasShown) {
Blink Reformat4c46d092018-04-07 15:32:37587 wasShown.push(viewportElement);
Tim van der Lippe1d6e57a2019-09-30 11:55:34588 }
Blink Reformat4c46d092018-04-07 15:32:37589 this._contentElement.insertBefore(element, anchor);
590 } else {
591 anchor = anchor.nextSibling;
592 }
593 }
Tim van der Lippe1d6e57a2019-09-30 11:55:34594 for (let i = 0; i < wasShown.length; ++i) {
Blink Reformat4c46d092018-04-07 15:32:37595 wasShown[i].wasShown();
Tim van der Lippe1d6e57a2019-09-30 11:55:34596 }
Blink Reformat4c46d092018-04-07 15:32:37597 this._renderedItems = Array.from(itemsToRender);
Erik Luo39452ff2018-09-01 01:08:07598
Tim van der Lippe1d6e57a2019-09-30 11:55:34599 if (hadFocus) {
Pavel Feldmandb310912019-01-30 00:31:20600 this._contentElement.focus();
Tim van der Lippe1d6e57a2019-09-30 11:55:34601 }
Pavel Feldmandb310912019-01-30 00:31:20602 this._updateFocusedItem();
Blink Reformat4c46d092018-04-07 15:32:37603 }
604
605 /**
606 * @return {?string}
607 */
608 _selectedText() {
609 this._updateSelectionModel(this.element.getComponentSelection());
Tim van der Lippe1d6e57a2019-09-30 11:55:34610 if (!this._headSelection || !this._anchorSelection) {
Blink Reformat4c46d092018-04-07 15:32:37611 return null;
Tim van der Lippe1d6e57a2019-09-30 11:55:34612 }
Blink Reformat4c46d092018-04-07 15:32:37613
614 let startSelection = null;
615 let endSelection = null;
616 if (this._selectionIsBackward) {
617 startSelection = this._headSelection;
618 endSelection = this._anchorSelection;
619 } else {
620 startSelection = this._anchorSelection;
621 endSelection = this._headSelection;
622 }
623
624 const textLines = [];
625 for (let i = startSelection.item; i <= endSelection.item; ++i) {
626 const element = this._providerElement(i).element();
Tim van der Lippe9b2f8712020-02-12 17:46:22627 const lineContent = element.childTextNodes().map(Components.Linkifier.Linkifier.untruncatedNodeText).join('');
Blink Reformat4c46d092018-04-07 15:32:37628 textLines.push(lineContent);
629 }
630
631 const endSelectionElement = this._providerElement(endSelection.item).element();
632 if (endSelection.node && endSelection.node.isSelfOrDescendant(endSelectionElement)) {
633 const itemTextOffset = this._textOffsetInNode(endSelectionElement, endSelection.node, endSelection.offset);
634 textLines[textLines.length - 1] = textLines.peekLast().substring(0, itemTextOffset);
635 }
636
637 const startSelectionElement = this._providerElement(startSelection.item).element();
638 if (startSelection.node && startSelection.node.isSelfOrDescendant(startSelectionElement)) {
639 const itemTextOffset = this._textOffsetInNode(startSelectionElement, startSelection.node, startSelection.offset);
640 textLines[0] = textLines[0].substring(itemTextOffset);
641 }
642
643 return textLines.join('\n');
644 }
645
646 /**
647 * @param {!Element} itemElement
648 * @param {!Node} selectionNode
649 * @param {number} offset
650 * @return {number}
651 */
652 _textOffsetInNode(itemElement, selectionNode, offset) {
653 // If the selectionNode is not a TextNode, we may need to convert a child offset into a character offset.
654 if (selectionNode.nodeType !== Node.TEXT_NODE) {
655 if (offset < selectionNode.childNodes.length) {
656 selectionNode = /** @type {!Node} */ (selectionNode.childNodes.item(offset));
657 offset = 0;
658 } else {
659 offset = selectionNode.textContent.length;
660 }
661 }
662
663 let chars = 0;
664 let node = itemElement;
665 while ((node = node.traverseNextNode(itemElement)) && node !== selectionNode) {
666 if (node.nodeType !== Node.TEXT_NODE || node.parentElement.nodeName === 'STYLE' ||
Tim van der Lippe1d6e57a2019-09-30 11:55:34667 node.parentElement.nodeName === 'SCRIPT') {
Blink Reformat4c46d092018-04-07 15:32:37668 continue;
Tim van der Lippe1d6e57a2019-09-30 11:55:34669 }
Tim van der Lippe9b2f8712020-02-12 17:46:22670 chars += Components.Linkifier.Linkifier.untruncatedNodeText(node).length;
Blink Reformat4c46d092018-04-07 15:32:37671 }
672 // If the selected node text was truncated, treat any non-zero offset as the full length.
Tim van der Lippe9b2f8712020-02-12 17:46:22673 const untruncatedContainerLength = Components.Linkifier.Linkifier.untruncatedNodeText(selectionNode).length;
Tim van der Lippe1d6e57a2019-09-30 11:55:34674 if (offset > 0 && untruncatedContainerLength !== selectionNode.textContent.length) {
Blink Reformat4c46d092018-04-07 15:32:37675 offset = untruncatedContainerLength;
Tim van der Lippe1d6e57a2019-09-30 11:55:34676 }
Blink Reformat4c46d092018-04-07 15:32:37677 return chars + offset;
678 }
679
680 /**
681 * @param {!Event} event
682 */
683 _onScroll(event) {
684 this.refresh();
685 }
686
687 /**
688 * @return {number}
689 */
690 firstVisibleIndex() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34691 if (!this._cumulativeHeights.length) {
Blink Reformat4c46d092018-04-07 15:32:37692 return -1;
Tim van der Lippe1d6e57a2019-09-30 11:55:34693 }
Blink Reformat4c46d092018-04-07 15:32:37694 this._rebuildCumulativeHeightsIfNeeded();
695 return this._cumulativeHeights.lowerBound(this.element.scrollTop + 1);
696 }
697
698 /**
699 * @return {number}
700 */
701 lastVisibleIndex() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34702 if (!this._cumulativeHeights.length) {
Blink Reformat4c46d092018-04-07 15:32:37703 return -1;
Tim van der Lippe1d6e57a2019-09-30 11:55:34704 }
Blink Reformat4c46d092018-04-07 15:32:37705 this._rebuildCumulativeHeightsIfNeeded();
706 const scrollBottom = this.element.scrollTop + this.element.clientHeight;
707 const right = this._itemCount - 1;
708 return this._cumulativeHeights.lowerBound(scrollBottom, undefined, undefined, right);
709 }
710
711 /**
Sigurd Schneider53f33522020-10-08 15:00:49712 * @return {?HTMLElement}
Blink Reformat4c46d092018-04-07 15:32:37713 */
714 renderedElementAt(index) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34715 if (index === -1 || index < this._firstActiveIndex || index > this._lastActiveIndex) {
Blink Reformat4c46d092018-04-07 15:32:37716 return null;
Tim van der Lippe1d6e57a2019-09-30 11:55:34717 }
Blink Reformat4c46d092018-04-07 15:32:37718 return this._renderedItems[index - this._firstActiveIndex].element();
719 }
720
721 /**
722 * @param {number} index
723 * @param {boolean=} makeLast
724 */
725 scrollItemIntoView(index, makeLast) {
726 const firstVisibleIndex = this.firstVisibleIndex();
727 const lastVisibleIndex = this.lastVisibleIndex();
Tim van der Lippe1d6e57a2019-09-30 11:55:34728 if (index > firstVisibleIndex && index < lastVisibleIndex) {
Blink Reformat4c46d092018-04-07 15:32:37729 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34730 }
Erik Luo4b002322018-07-30 21:23:31731 // If the prompt is visible, then the last item must be fully on screen.
Tim van der Lippe1d6e57a2019-09-30 11:55:34732 if (index === lastVisibleIndex &&
733 this._cumulativeHeights[index] <= this.element.scrollTop + this._visibleHeight()) {
Erik Luo4b002322018-07-30 21:23:31734 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34735 }
736 if (makeLast) {
Blink Reformat4c46d092018-04-07 15:32:37737 this.forceScrollItemToBeLast(index);
Tim van der Lippe1d6e57a2019-09-30 11:55:34738 } else if (index <= firstVisibleIndex) {
Blink Reformat4c46d092018-04-07 15:32:37739 this.forceScrollItemToBeFirst(index);
Tim van der Lippe1d6e57a2019-09-30 11:55:34740 } else if (index >= lastVisibleIndex) {
Blink Reformat4c46d092018-04-07 15:32:37741 this.forceScrollItemToBeLast(index);
Tim van der Lippe1d6e57a2019-09-30 11:55:34742 }
Blink Reformat4c46d092018-04-07 15:32:37743 }
744
745 /**
746 * @param {number} index
747 */
748 forceScrollItemToBeFirst(index) {
749 console.assert(index >= 0 && index < this._itemCount, 'Cannot scroll item at invalid index');
750 this.setStickToBottom(false);
751 this._rebuildCumulativeHeightsIfNeeded();
752 this.element.scrollTop = index > 0 ? this._cumulativeHeights[index - 1] : 0;
Sigurd Schneider7393b322020-10-05 14:25:40753 if (UI.UIUtils.isScrolledToBottom(this.element)) {
Blink Reformat4c46d092018-04-07 15:32:37754 this.setStickToBottom(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:34755 }
Blink Reformat4c46d092018-04-07 15:32:37756 this.refresh();
Erik Luo4b002322018-07-30 21:23:31757 // After refresh, the item is in DOM, but may not be visible (items above were larger than expected).
758 this.renderedElementAt(index).scrollIntoView(true /* alignTop */);
Blink Reformat4c46d092018-04-07 15:32:37759 }
760
761 /**
762 * @param {number} index
763 */
764 forceScrollItemToBeLast(index) {
765 console.assert(index >= 0 && index < this._itemCount, 'Cannot scroll item at invalid index');
766 this.setStickToBottom(false);
767 this._rebuildCumulativeHeightsIfNeeded();
768 this.element.scrollTop = this._cumulativeHeights[index] - this._visibleHeight();
Sigurd Schneider7393b322020-10-05 14:25:40769 if (UI.UIUtils.isScrolledToBottom(this.element)) {
Blink Reformat4c46d092018-04-07 15:32:37770 this.setStickToBottom(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:34771 }
Blink Reformat4c46d092018-04-07 15:32:37772 this.refresh();
Erik Luo4b002322018-07-30 21:23:31773 // After refresh, the item is in DOM, but may not be visible (items above were larger than expected).
774 this.renderedElementAt(index).scrollIntoView(false /* alignTop */);
Blink Reformat4c46d092018-04-07 15:32:37775 }
776
777 /**
778 * @return {number}
779 */
780 _visibleHeight() {
781 // Use offsetHeight instead of clientHeight to avoid being affected by horizontal scroll.
782 return this.element.offsetHeight;
783 }
Paul Lewisbf7aa3c2019-11-20 17:03:38784}
Blink Reformat4c46d092018-04-07 15:32:37785
786/**
787 * @interface
788 */
Tim van der Lippeeaacb722020-01-10 12:16:00789export class ConsoleViewportProvider {
Blink Reformat4c46d092018-04-07 15:32:37790 /**
791 * @param {number} index
792 * @return {number}
793 */
794 fastHeight(index) {
795 return 0;
Paul Lewisbf7aa3c2019-11-20 17:03:38796 }
Blink Reformat4c46d092018-04-07 15:32:37797
798 /**
799 * @return {number}
800 */
801 itemCount() {
802 return 0;
Paul Lewisbf7aa3c2019-11-20 17:03:38803 }
Blink Reformat4c46d092018-04-07 15:32:37804
805 /**
806 * @return {number}
807 */
808 minimumRowHeight() {
809 return 0;
Paul Lewisbf7aa3c2019-11-20 17:03:38810 }
Blink Reformat4c46d092018-04-07 15:32:37811
812 /**
813 * @param {number} index
Tim van der Lippeeaacb722020-01-10 12:16:00814 * @return {?ConsoleViewportElement}
Blink Reformat4c46d092018-04-07 15:32:37815 */
816 itemElement(index) {
817 return null;
818 }
Paul Lewisbf7aa3c2019-11-20 17:03:38819}
Blink Reformat4c46d092018-04-07 15:32:37820
821/**
822 * @interface
823 */
Paul Lewisbf7aa3c2019-11-20 17:03:38824export class ConsoleViewportElement {
825 willHide() {
826 }
Blink Reformat4c46d092018-04-07 15:32:37827
Paul Lewisbf7aa3c2019-11-20 17:03:38828 wasShown() {
829 }
Blink Reformat4c46d092018-04-07 15:32:37830
831 /**
Sigurd Schneider53f33522020-10-08 15:00:49832 * @return {!HTMLElement}
Blink Reformat4c46d092018-04-07 15:32:37833 */
Paul Lewisbf7aa3c2019-11-20 17:03:38834 element() {
Sigurd Schneider53f33522020-10-08 15:00:49835 throw new Error('Unimplemented method');
836 }
837
838 focusLastChildOrSelf() {
Paul Lewisbf7aa3c2019-11-20 17:03:38839 }
840}