blob: 78d2222fd4b6f86fd969acbe8aa232990d43b059 [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';
36
Blink Reformat4c46d092018-04-07 15:32:3737/**
38 * @unrestricted
39 */
Tim van der Lippeeaacb722020-01-10 12:16:0040export class ConsoleViewport {
Blink Reformat4c46d092018-04-07 15:32:3741 /**
Tim van der Lippeeaacb722020-01-10 12:16:0042 * @param {!ConsoleViewportProvider} provider
Blink Reformat4c46d092018-04-07 15:32:3743 */
44 constructor(provider) {
45 this.element = createElement('div');
46 this.element.style.overflow = 'auto';
47 this._topGapElement = this.element.createChild('div');
48 this._topGapElement.style.height = '0px';
49 this._topGapElement.style.color = 'transparent';
50 this._contentElement = this.element.createChild('div');
51 this._bottomGapElement = this.element.createChild('div');
52 this._bottomGapElement.style.height = '0px';
53 this._bottomGapElement.style.color = 'transparent';
54
55 // Text content needed for range intersection checks in _updateSelectionModel.
56 // Use Unicode ZERO WIDTH NO-BREAK SPACE, which avoids contributing any height to the element's layout overflow.
57 this._topGapElement.textContent = '\uFEFF';
58 this._bottomGapElement.textContent = '\uFEFF';
59
Joel Einbinderca8fa5a2018-08-24 23:30:2560 UI.ARIAUtils.markAsHidden(this._topGapElement);
61 UI.ARIAUtils.markAsHidden(this._bottomGapElement);
62
Blink Reformat4c46d092018-04-07 15:32:3763 this._provider = provider;
64 this.element.addEventListener('scroll', this._onScroll.bind(this), false);
65 this.element.addEventListener('copy', this._onCopy.bind(this), false);
66 this.element.addEventListener('dragstart', this._onDragStart.bind(this), false);
Pavel Feldmandb310912019-01-30 00:31:2067 this._contentElement.addEventListener('focusin', this._onFocusIn.bind(this), false);
68 this._contentElement.addEventListener('focusout', this._onFocusOut.bind(this), false);
69 this._contentElement.addEventListener('keydown', this._onKeyDown.bind(this), false);
Erik Luo39452ff2018-09-01 01:08:0770 this._virtualSelectedIndex = -1;
Erik Luob5bfff42018-09-20 02:52:3971 this._contentElement.tabIndex = -1;
Blink Reformat4c46d092018-04-07 15:32:3772
73 this._firstActiveIndex = -1;
74 this._lastActiveIndex = -1;
75 this._renderedItems = [];
76 this._anchorSelection = null;
77 this._headSelection = null;
78 this._itemCount = 0;
79 this._cumulativeHeights = new Int32Array(0);
80 this._muteCopyHandler = false;
81
82 // Listen for any changes to descendants and trigger a refresh. This ensures
83 // that items updated asynchronously will not break stick-to-bottom behavior
84 // if they change the scroll height.
85 this._observer = new MutationObserver(this.refresh.bind(this));
86 this._observerConfig = {childList: true, subtree: true};
87 }
88
89 /**
90 * @return {boolean}
91 */
92 stickToBottom() {
93 return this._stickToBottom;
94 }
95
96 /**
97 * @param {boolean} value
98 */
99 setStickToBottom(value) {
100 this._stickToBottom = value;
Tim van der Lippe1d6e57a2019-09-30 11:55:34101 if (this._stickToBottom) {
Blink Reformat4c46d092018-04-07 15:32:37102 this._observer.observe(this._contentElement, this._observerConfig);
Tim van der Lippe1d6e57a2019-09-30 11:55:34103 } else {
Blink Reformat4c46d092018-04-07 15:32:37104 this._observer.disconnect();
Tim van der Lippe1d6e57a2019-09-30 11:55:34105 }
Blink Reformat4c46d092018-04-07 15:32:37106 }
107
Erik Luoad6c91c2018-12-06 03:28:42108 /**
109 * @return {boolean}
110 */
111 hasVirtualSelection() {
112 return this._virtualSelectedIndex !== -1;
113 }
114
Blink Reformat4c46d092018-04-07 15:32:37115 copyWithStyles() {
116 this._muteCopyHandler = true;
117 this.element.ownerDocument.execCommand('copy');
118 this._muteCopyHandler = false;
119 }
120
121 /**
122 * @param {!Event} event
123 */
124 _onCopy(event) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34125 if (this._muteCopyHandler) {
Blink Reformat4c46d092018-04-07 15:32:37126 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34127 }
Blink Reformat4c46d092018-04-07 15:32:37128 const text = this._selectedText();
Tim van der Lippe1d6e57a2019-09-30 11:55:34129 if (!text) {
Blink Reformat4c46d092018-04-07 15:32:37130 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34131 }
Blink Reformat4c46d092018-04-07 15:32:37132 event.preventDefault();
133 event.clipboardData.setData('text/plain', text);
134 }
135
136 /**
137 * @param {!Event} event
138 */
Erik Luo39452ff2018-09-01 01:08:07139 _onFocusIn(event) {
Erik Luob5bfff42018-09-20 02:52:39140 const renderedIndex = this._renderedItems.findIndex(item => item.element().isSelfOrAncestor(event.target));
Tim van der Lippe1d6e57a2019-09-30 11:55:34141 if (renderedIndex !== -1) {
Erik Luob5bfff42018-09-20 02:52:39142 this._virtualSelectedIndex = this._firstActiveIndex + renderedIndex;
Tim van der Lippe1d6e57a2019-09-30 11:55:34143 }
Erik Luofc6a6302018-11-02 06:48:52144 let focusLastChild = false;
Erik Luo39452ff2018-09-01 01:08:07145 // Make default selection when moving from external (e.g. prompt) to the container.
146 if (this._virtualSelectedIndex === -1 && this._isOutsideViewport(/** @type {?Element} */ (event.relatedTarget)) &&
Erik Luoad6c91c2018-12-06 03:28:42147 event.target === this._contentElement && this._itemCount) {
Erik Luofc6a6302018-11-02 06:48:52148 focusLastChild = true;
Erik Luo39452ff2018-09-01 01:08:07149 this._virtualSelectedIndex = this._itemCount - 1;
Erik Luo840be6b2018-12-03 20:54:27150
151 // Update stick to bottom before scrolling into view.
152 this.refresh();
153 this.scrollItemIntoView(this._virtualSelectedIndex);
Erik Luofc6a6302018-11-02 06:48:52154 }
155 this._updateFocusedItem(focusLastChild);
Erik Luo39452ff2018-09-01 01:08:07156 }
157
158 /**
159 * @param {!Event} event
160 */
161 _onFocusOut(event) {
162 // Remove selection when focus moves to external location (e.g. prompt).
Tim van der Lippe1d6e57a2019-09-30 11:55:34163 if (this._isOutsideViewport(/** @type {?Element} */ (event.relatedTarget))) {
Erik Luo39452ff2018-09-01 01:08:07164 this._virtualSelectedIndex = -1;
Tim van der Lippe1d6e57a2019-09-30 11:55:34165 }
Erik Luo39452ff2018-09-01 01:08:07166 this._updateFocusedItem();
167 }
168
169 /**
170 * @param {?Element} element
171 * @return {boolean}
172 */
173 _isOutsideViewport(element) {
Erik Luob5bfff42018-09-20 02:52:39174 return !!element && !element.isSelfOrDescendant(this._contentElement);
Erik Luo39452ff2018-09-01 01:08:07175 }
176
177 /**
178 * @param {!Event} event
179 */
Blink Reformat4c46d092018-04-07 15:32:37180 _onDragStart(event) {
181 const text = this._selectedText();
Tim van der Lippe1d6e57a2019-09-30 11:55:34182 if (!text) {
Blink Reformat4c46d092018-04-07 15:32:37183 return false;
Tim van der Lippe1d6e57a2019-09-30 11:55:34184 }
Blink Reformat4c46d092018-04-07 15:32:37185 event.dataTransfer.clearData();
186 event.dataTransfer.setData('text/plain', text);
187 event.dataTransfer.effectAllowed = 'copy';
188 return true;
189 }
190
191 /**
Erik Luo39452ff2018-09-01 01:08:07192 * @param {!Event} event
193 */
194 _onKeyDown(event) {
Tim van der Lippe9b2f8712020-02-12 17:46:22195 if (UI.UIUtils.isEditing() || !this._itemCount || event.shiftKey) {
Erik Luo39452ff2018-09-01 01:08:07196 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34197 }
Erik Luo0b8282e2018-10-08 20:37:46198 let isArrowUp = false;
Erik Luo39452ff2018-09-01 01:08:07199 switch (event.key) {
200 case 'ArrowUp':
Erik Luo0b8282e2018-10-08 20:37:46201 if (this._virtualSelectedIndex > 0) {
202 isArrowUp = true;
203 this._virtualSelectedIndex--;
204 } else {
205 return;
206 }
Erik Luo39452ff2018-09-01 01:08:07207 break;
208 case 'ArrowDown':
Tim van der Lippe1d6e57a2019-09-30 11:55:34209 if (this._virtualSelectedIndex < this._itemCount - 1) {
Erik Luo0b8282e2018-10-08 20:37:46210 this._virtualSelectedIndex++;
Tim van der Lippe1d6e57a2019-09-30 11:55:34211 } else {
Erik Luo0b8282e2018-10-08 20:37:46212 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34213 }
Erik Luo39452ff2018-09-01 01:08:07214 break;
215 case 'Home':
216 this._virtualSelectedIndex = 0;
217 break;
218 case 'End':
219 this._virtualSelectedIndex = this._itemCount - 1;
220 break;
221 default:
222 return;
223 }
224 event.consume(true);
225 this.scrollItemIntoView(this._virtualSelectedIndex);
Erik Luo0b8282e2018-10-08 20:37:46226 this._updateFocusedItem(isArrowUp);
Erik Luo39452ff2018-09-01 01:08:07227 }
228
Erik Luo0b8282e2018-10-08 20:37:46229 /**
230 * @param {boolean=} focusLastChild
231 */
232 _updateFocusedItem(focusLastChild) {
Erik Luo39452ff2018-09-01 01:08:07233 const selectedElement = this.renderedElementAt(this._virtualSelectedIndex);
234 const changed = this._lastSelectedElement !== selectedElement;
Erik Luob5bfff42018-09-20 02:52:39235 const containerHasFocus = this._contentElement === this.element.ownerDocument.deepActiveElement();
Tim van der Lippe1d6e57a2019-09-30 11:55:34236 if (this._lastSelectedElement && changed) {
Erik Luo39452ff2018-09-01 01:08:07237 this._lastSelectedElement.classList.remove('console-selected');
Tim van der Lippe1d6e57a2019-09-30 11:55:34238 }
Erik Luo840be6b2018-12-03 20:54:27239 if (selectedElement && (focusLastChild || changed || containerHasFocus) && this.element.hasFocus()) {
Erik Luo39452ff2018-09-01 01:08:07240 selectedElement.classList.add('console-selected');
Erik Luob5bfff42018-09-20 02:52:39241 // Do not focus the message if something within holds focus (e.g. object).
Erik Luo840be6b2018-12-03 20:54:27242 if (focusLastChild) {
243 this.setStickToBottom(false);
244 this._renderedItems[this._virtualSelectedIndex - this._firstActiveIndex].focusLastChildOrSelf();
245 } else if (!selectedElement.hasFocus()) {
Peter Marshall477ef992020-05-12 17:59:55246 selectedElement.focus({preventScroll: true});
Erik Luo0b8282e2018-10-08 20:37:46247 }
Erik Luo39452ff2018-09-01 01:08:07248 }
Tim van der Lippe1d6e57a2019-09-30 11:55:34249 if (this._itemCount && !this._contentElement.hasFocus()) {
Erik Luob5bfff42018-09-20 02:52:39250 this._contentElement.tabIndex = 0;
Tim van der Lippe1d6e57a2019-09-30 11:55:34251 } else {
Erik Luob5bfff42018-09-20 02:52:39252 this._contentElement.tabIndex = -1;
Tim van der Lippe1d6e57a2019-09-30 11:55:34253 }
Erik Luo39452ff2018-09-01 01:08:07254 this._lastSelectedElement = selectedElement;
Erik Luo39452ff2018-09-01 01:08:07255 }
256
257 /**
Blink Reformat4c46d092018-04-07 15:32:37258 * @return {!Element}
259 */
260 contentElement() {
261 return this._contentElement;
262 }
263
264 invalidate() {
265 delete this._cachedProviderElements;
266 this._itemCount = this._provider.itemCount();
Tim van der Lippe1d6e57a2019-09-30 11:55:34267 if (this._virtualSelectedIndex > this._itemCount - 1) {
Erik Luo39452ff2018-09-01 01:08:07268 this._virtualSelectedIndex = this._itemCount - 1;
Tim van der Lippe1d6e57a2019-09-30 11:55:34269 }
Blink Reformat4c46d092018-04-07 15:32:37270 this._rebuildCumulativeHeights();
271 this.refresh();
272 }
273
274 /**
275 * @param {number} index
Tim van der Lippeeaacb722020-01-10 12:16:00276 * @return {?ConsoleViewportElement}
Blink Reformat4c46d092018-04-07 15:32:37277 */
278 _providerElement(index) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34279 if (!this._cachedProviderElements) {
Blink Reformat4c46d092018-04-07 15:32:37280 this._cachedProviderElements = new Array(this._itemCount);
Tim van der Lippe1d6e57a2019-09-30 11:55:34281 }
Blink Reformat4c46d092018-04-07 15:32:37282 let element = this._cachedProviderElements[index];
283 if (!element) {
284 element = this._provider.itemElement(index);
285 this._cachedProviderElements[index] = element;
286 }
287 return element;
288 }
289
290 _rebuildCumulativeHeights() {
291 const firstActiveIndex = this._firstActiveIndex;
292 const lastActiveIndex = this._lastActiveIndex;
293 let height = 0;
294 this._cumulativeHeights = new Int32Array(this._itemCount);
295 for (let i = 0; i < this._itemCount; ++i) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34296 if (firstActiveIndex <= i && i - firstActiveIndex < this._renderedItems.length && i <= lastActiveIndex) {
Blink Reformat4c46d092018-04-07 15:32:37297 height += this._renderedItems[i - firstActiveIndex].element().offsetHeight;
Tim van der Lippe1d6e57a2019-09-30 11:55:34298 } else {
Blink Reformat4c46d092018-04-07 15:32:37299 height += this._provider.fastHeight(i);
Tim van der Lippe1d6e57a2019-09-30 11:55:34300 }
Blink Reformat4c46d092018-04-07 15:32:37301 this._cumulativeHeights[i] = height;
302 }
303 }
304
305 _rebuildCumulativeHeightsIfNeeded() {
Erik Luo4b002322018-07-30 21:23:31306 let totalCachedHeight = 0;
307 let totalMeasuredHeight = 0;
Blink Reformat4c46d092018-04-07 15:32:37308 // Check whether current items in DOM have changed heights. Tolerate 1-pixel
309 // error due to double-to-integer rounding errors.
310 for (let i = 0; i < this._renderedItems.length; ++i) {
311 const cachedItemHeight = this._cachedItemHeight(this._firstActiveIndex + i);
Erik Luo4b002322018-07-30 21:23:31312 const measuredHeight = this._renderedItems[i].element().offsetHeight;
313 if (Math.abs(cachedItemHeight - measuredHeight) > 1) {
Blink Reformat4c46d092018-04-07 15:32:37314 this._rebuildCumulativeHeights();
Erik Luo4b002322018-07-30 21:23:31315 return;
316 }
317 totalMeasuredHeight += measuredHeight;
318 totalCachedHeight += cachedItemHeight;
319 if (Math.abs(totalCachedHeight - totalMeasuredHeight) > 1) {
320 this._rebuildCumulativeHeights();
321 return;
Blink Reformat4c46d092018-04-07 15:32:37322 }
323 }
324 }
325
326 /**
327 * @param {number} index
328 * @return {number}
329 */
330 _cachedItemHeight(index) {
331 return index === 0 ? this._cumulativeHeights[0] :
332 this._cumulativeHeights[index] - this._cumulativeHeights[index - 1];
333 }
334
335 /**
336 * @param {?Selection} selection
337 * @suppressGlobalPropertiesCheck
338 */
339 _isSelectionBackwards(selection) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34340 if (!selection || !selection.rangeCount) {
Blink Reformat4c46d092018-04-07 15:32:37341 return false;
Tim van der Lippe1d6e57a2019-09-30 11:55:34342 }
Blink Reformat4c46d092018-04-07 15:32:37343 const range = document.createRange();
344 range.setStart(selection.anchorNode, selection.anchorOffset);
345 range.setEnd(selection.focusNode, selection.focusOffset);
346 return range.collapsed;
347 }
348
349 /**
350 * @param {number} itemIndex
351 * @param {!Node} node
352 * @param {number} offset
353 * @return {!{item: number, node: !Node, offset: number}}
354 */
355 _createSelectionModel(itemIndex, node, offset) {
356 return {item: itemIndex, node: node, offset: offset};
357 }
358
359 /**
360 * @param {?Selection} selection
361 */
362 _updateSelectionModel(selection) {
363 const range = selection && selection.rangeCount ? selection.getRangeAt(0) : null;
364 if (!range || selection.isCollapsed || !this.element.hasSelection()) {
365 this._headSelection = null;
366 this._anchorSelection = null;
367 return false;
368 }
369
370 let firstSelected = Number.MAX_VALUE;
371 let lastSelected = -1;
372
373 let hasVisibleSelection = false;
374 for (let i = 0; i < this._renderedItems.length; ++i) {
375 if (range.intersectsNode(this._renderedItems[i].element())) {
376 const index = i + this._firstActiveIndex;
377 firstSelected = Math.min(firstSelected, index);
378 lastSelected = Math.max(lastSelected, index);
379 hasVisibleSelection = true;
380 }
381 }
382 if (hasVisibleSelection) {
383 firstSelected =
384 this._createSelectionModel(firstSelected, /** @type {!Node} */ (range.startContainer), range.startOffset);
385 lastSelected =
386 this._createSelectionModel(lastSelected, /** @type {!Node} */ (range.endContainer), range.endOffset);
387 }
388 const topOverlap = range.intersectsNode(this._topGapElement) && this._topGapElement._active;
389 const bottomOverlap = range.intersectsNode(this._bottomGapElement) && this._bottomGapElement._active;
390 if (!topOverlap && !bottomOverlap && !hasVisibleSelection) {
391 this._headSelection = null;
392 this._anchorSelection = null;
393 return false;
394 }
395
396 if (!this._anchorSelection || !this._headSelection) {
397 this._anchorSelection = this._createSelectionModel(0, this.element, 0);
398 this._headSelection = this._createSelectionModel(this._itemCount - 1, this.element, this.element.children.length);
399 this._selectionIsBackward = false;
400 }
401
402 const isBackward = this._isSelectionBackwards(selection);
403 const startSelection = this._selectionIsBackward ? this._headSelection : this._anchorSelection;
404 const endSelection = this._selectionIsBackward ? this._anchorSelection : this._headSelection;
405 if (topOverlap && bottomOverlap && hasVisibleSelection) {
406 firstSelected = firstSelected.item < startSelection.item ? firstSelected : startSelection;
407 lastSelected = lastSelected.item > endSelection.item ? lastSelected : endSelection;
408 } else if (!hasVisibleSelection) {
409 firstSelected = startSelection;
410 lastSelected = endSelection;
411 } else if (topOverlap) {
412 firstSelected = isBackward ? this._headSelection : this._anchorSelection;
413 } else if (bottomOverlap) {
414 lastSelected = isBackward ? this._anchorSelection : this._headSelection;
415 }
416
417 if (isBackward) {
418 this._anchorSelection = lastSelected;
419 this._headSelection = firstSelected;
420 } else {
421 this._anchorSelection = firstSelected;
422 this._headSelection = lastSelected;
423 }
424 this._selectionIsBackward = isBackward;
425 return true;
426 }
427
428 /**
429 * @param {?Selection} selection
430 */
431 _restoreSelection(selection) {
432 let anchorElement = null;
433 let anchorOffset;
434 if (this._firstActiveIndex <= this._anchorSelection.item && this._anchorSelection.item <= this._lastActiveIndex) {
435 anchorElement = this._anchorSelection.node;
436 anchorOffset = this._anchorSelection.offset;
437 } else {
Tim van der Lippe1d6e57a2019-09-30 11:55:34438 if (this._anchorSelection.item < this._firstActiveIndex) {
Blink Reformat4c46d092018-04-07 15:32:37439 anchorElement = this._topGapElement;
Tim van der Lippe1d6e57a2019-09-30 11:55:34440 } else if (this._anchorSelection.item > this._lastActiveIndex) {
Blink Reformat4c46d092018-04-07 15:32:37441 anchorElement = this._bottomGapElement;
Tim van der Lippe1d6e57a2019-09-30 11:55:34442 }
Blink Reformat4c46d092018-04-07 15:32:37443 anchorOffset = this._selectionIsBackward ? 1 : 0;
444 }
445
446 let headElement = null;
447 let headOffset;
448 if (this._firstActiveIndex <= this._headSelection.item && this._headSelection.item <= this._lastActiveIndex) {
449 headElement = this._headSelection.node;
450 headOffset = this._headSelection.offset;
451 } else {
Tim van der Lippe1d6e57a2019-09-30 11:55:34452 if (this._headSelection.item < this._firstActiveIndex) {
Blink Reformat4c46d092018-04-07 15:32:37453 headElement = this._topGapElement;
Tim van der Lippe1d6e57a2019-09-30 11:55:34454 } else if (this._headSelection.item > this._lastActiveIndex) {
Blink Reformat4c46d092018-04-07 15:32:37455 headElement = this._bottomGapElement;
Tim van der Lippe1d6e57a2019-09-30 11:55:34456 }
Blink Reformat4c46d092018-04-07 15:32:37457 headOffset = this._selectionIsBackward ? 0 : 1;
458 }
459
460 selection.setBaseAndExtent(anchorElement, anchorOffset, headElement, headOffset);
461 }
462
463 refresh() {
464 this._observer.disconnect();
465 this._innerRefresh();
Tim van der Lippe1d6e57a2019-09-30 11:55:34466 if (this._stickToBottom) {
Blink Reformat4c46d092018-04-07 15:32:37467 this._observer.observe(this._contentElement, this._observerConfig);
Tim van der Lippe1d6e57a2019-09-30 11:55:34468 }
Blink Reformat4c46d092018-04-07 15:32:37469 }
470
471 _innerRefresh() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34472 if (!this._visibleHeight()) {
473 return;
474 } // Do nothing for invisible controls.
Blink Reformat4c46d092018-04-07 15:32:37475
476 if (!this._itemCount) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34477 for (let i = 0; i < this._renderedItems.length; ++i) {
Blink Reformat4c46d092018-04-07 15:32:37478 this._renderedItems[i].willHide();
Tim van der Lippe1d6e57a2019-09-30 11:55:34479 }
Blink Reformat4c46d092018-04-07 15:32:37480 this._renderedItems = [];
481 this._contentElement.removeChildren();
482 this._topGapElement.style.height = '0px';
483 this._bottomGapElement.style.height = '0px';
484 this._firstActiveIndex = -1;
485 this._lastActiveIndex = -1;
Pavel Feldmandb310912019-01-30 00:31:20486 this._updateFocusedItem();
Blink Reformat4c46d092018-04-07 15:32:37487 return;
488 }
489
490 const selection = this.element.getComponentSelection();
491 const shouldRestoreSelection = this._updateSelectionModel(selection);
492
493 const visibleFrom = this.element.scrollTop;
494 const visibleHeight = this._visibleHeight();
495 const activeHeight = visibleHeight * 2;
496 this._rebuildCumulativeHeightsIfNeeded();
497
498 // When the viewport is scrolled to the bottom, using the cumulative heights estimate is not
499 // precise enough to determine next visible indices. This stickToBottom check avoids extra
500 // calls to refresh in those cases.
501 if (this._stickToBottom) {
502 this._firstActiveIndex =
503 Math.max(this._itemCount - Math.ceil(activeHeight / this._provider.minimumRowHeight()), 0);
504 this._lastActiveIndex = this._itemCount - 1;
505 } else {
506 this._firstActiveIndex =
507 Math.max(this._cumulativeHeights.lowerBound(visibleFrom + 1 - (activeHeight - visibleHeight) / 2), 0);
508 // Proactively render more rows in case some of them will be collapsed without triggering refresh. @see crbug.com/390169
509 this._lastActiveIndex = this._firstActiveIndex + Math.ceil(activeHeight / this._provider.minimumRowHeight()) - 1;
510 this._lastActiveIndex = Math.min(this._lastActiveIndex, this._itemCount - 1);
511 }
512
513 const topGapHeight = this._cumulativeHeights[this._firstActiveIndex - 1] || 0;
514 const bottomGapHeight =
515 this._cumulativeHeights[this._cumulativeHeights.length - 1] - this._cumulativeHeights[this._lastActiveIndex];
516
517 /**
Tim van der Lippeeaacb722020-01-10 12:16:00518 * @this {ConsoleViewport}
Blink Reformat4c46d092018-04-07 15:32:37519 */
520 function prepare() {
521 this._topGapElement.style.height = topGapHeight + 'px';
522 this._bottomGapElement.style.height = bottomGapHeight + 'px';
523 this._topGapElement._active = !!topGapHeight;
524 this._bottomGapElement._active = !!bottomGapHeight;
525 this._contentElement.style.setProperty('height', '10000000px');
526 }
527
528 this._partialViewportUpdate(prepare.bind(this));
529 this._contentElement.style.removeProperty('height');
530 // Should be the last call in the method as it might force layout.
Tim van der Lippe1d6e57a2019-09-30 11:55:34531 if (shouldRestoreSelection) {
Blink Reformat4c46d092018-04-07 15:32:37532 this._restoreSelection(selection);
Tim van der Lippe1d6e57a2019-09-30 11:55:34533 }
534 if (this._stickToBottom) {
Blink Reformat4c46d092018-04-07 15:32:37535 this.element.scrollTop = 10000000;
Tim van der Lippe1d6e57a2019-09-30 11:55:34536 }
Blink Reformat4c46d092018-04-07 15:32:37537 }
538
539 /**
540 * @param {function()} prepare
541 */
542 _partialViewportUpdate(prepare) {
543 const itemsToRender = new Set();
Tim van der Lippe1d6e57a2019-09-30 11:55:34544 for (let i = this._firstActiveIndex; i <= this._lastActiveIndex; ++i) {
Blink Reformat4c46d092018-04-07 15:32:37545 itemsToRender.add(this._providerElement(i));
Tim van der Lippe1d6e57a2019-09-30 11:55:34546 }
Blink Reformat4c46d092018-04-07 15:32:37547 const willBeHidden = this._renderedItems.filter(item => !itemsToRender.has(item));
Tim van der Lippe1d6e57a2019-09-30 11:55:34548 for (let i = 0; i < willBeHidden.length; ++i) {
Blink Reformat4c46d092018-04-07 15:32:37549 willBeHidden[i].willHide();
Tim van der Lippe1d6e57a2019-09-30 11:55:34550 }
Blink Reformat4c46d092018-04-07 15:32:37551 prepare();
Erik Luo39452ff2018-09-01 01:08:07552 let hadFocus = false;
553 for (let i = 0; i < willBeHidden.length; ++i) {
Pavel Feldmandb310912019-01-30 00:31:20554 hadFocus = hadFocus || willBeHidden[i].element().hasFocus();
Blink Reformat4c46d092018-04-07 15:32:37555 willBeHidden[i].element().remove();
Erik Luo39452ff2018-09-01 01:08:07556 }
Blink Reformat4c46d092018-04-07 15:32:37557
558 const wasShown = [];
559 let anchor = this._contentElement.firstChild;
560 for (const viewportElement of itemsToRender) {
561 const element = viewportElement.element();
562 if (element !== anchor) {
563 const shouldCallWasShown = !element.parentElement;
Tim van der Lippe1d6e57a2019-09-30 11:55:34564 if (shouldCallWasShown) {
Blink Reformat4c46d092018-04-07 15:32:37565 wasShown.push(viewportElement);
Tim van der Lippe1d6e57a2019-09-30 11:55:34566 }
Blink Reformat4c46d092018-04-07 15:32:37567 this._contentElement.insertBefore(element, anchor);
568 } else {
569 anchor = anchor.nextSibling;
570 }
571 }
Tim van der Lippe1d6e57a2019-09-30 11:55:34572 for (let i = 0; i < wasShown.length; ++i) {
Blink Reformat4c46d092018-04-07 15:32:37573 wasShown[i].wasShown();
Tim van der Lippe1d6e57a2019-09-30 11:55:34574 }
Blink Reformat4c46d092018-04-07 15:32:37575 this._renderedItems = Array.from(itemsToRender);
Erik Luo39452ff2018-09-01 01:08:07576
Tim van der Lippe1d6e57a2019-09-30 11:55:34577 if (hadFocus) {
Pavel Feldmandb310912019-01-30 00:31:20578 this._contentElement.focus();
Tim van der Lippe1d6e57a2019-09-30 11:55:34579 }
Pavel Feldmandb310912019-01-30 00:31:20580 this._updateFocusedItem();
Blink Reformat4c46d092018-04-07 15:32:37581 }
582
583 /**
584 * @return {?string}
585 */
586 _selectedText() {
587 this._updateSelectionModel(this.element.getComponentSelection());
Tim van der Lippe1d6e57a2019-09-30 11:55:34588 if (!this._headSelection || !this._anchorSelection) {
Blink Reformat4c46d092018-04-07 15:32:37589 return null;
Tim van der Lippe1d6e57a2019-09-30 11:55:34590 }
Blink Reformat4c46d092018-04-07 15:32:37591
592 let startSelection = null;
593 let endSelection = null;
594 if (this._selectionIsBackward) {
595 startSelection = this._headSelection;
596 endSelection = this._anchorSelection;
597 } else {
598 startSelection = this._anchorSelection;
599 endSelection = this._headSelection;
600 }
601
602 const textLines = [];
603 for (let i = startSelection.item; i <= endSelection.item; ++i) {
604 const element = this._providerElement(i).element();
Tim van der Lippe9b2f8712020-02-12 17:46:22605 const lineContent = element.childTextNodes().map(Components.Linkifier.Linkifier.untruncatedNodeText).join('');
Blink Reformat4c46d092018-04-07 15:32:37606 textLines.push(lineContent);
607 }
608
609 const endSelectionElement = this._providerElement(endSelection.item).element();
610 if (endSelection.node && endSelection.node.isSelfOrDescendant(endSelectionElement)) {
611 const itemTextOffset = this._textOffsetInNode(endSelectionElement, endSelection.node, endSelection.offset);
612 textLines[textLines.length - 1] = textLines.peekLast().substring(0, itemTextOffset);
613 }
614
615 const startSelectionElement = this._providerElement(startSelection.item).element();
616 if (startSelection.node && startSelection.node.isSelfOrDescendant(startSelectionElement)) {
617 const itemTextOffset = this._textOffsetInNode(startSelectionElement, startSelection.node, startSelection.offset);
618 textLines[0] = textLines[0].substring(itemTextOffset);
619 }
620
621 return textLines.join('\n');
622 }
623
624 /**
625 * @param {!Element} itemElement
626 * @param {!Node} selectionNode
627 * @param {number} offset
628 * @return {number}
629 */
630 _textOffsetInNode(itemElement, selectionNode, offset) {
631 // If the selectionNode is not a TextNode, we may need to convert a child offset into a character offset.
632 if (selectionNode.nodeType !== Node.TEXT_NODE) {
633 if (offset < selectionNode.childNodes.length) {
634 selectionNode = /** @type {!Node} */ (selectionNode.childNodes.item(offset));
635 offset = 0;
636 } else {
637 offset = selectionNode.textContent.length;
638 }
639 }
640
641 let chars = 0;
642 let node = itemElement;
643 while ((node = node.traverseNextNode(itemElement)) && node !== selectionNode) {
644 if (node.nodeType !== Node.TEXT_NODE || node.parentElement.nodeName === 'STYLE' ||
Tim van der Lippe1d6e57a2019-09-30 11:55:34645 node.parentElement.nodeName === 'SCRIPT') {
Blink Reformat4c46d092018-04-07 15:32:37646 continue;
Tim van der Lippe1d6e57a2019-09-30 11:55:34647 }
Tim van der Lippe9b2f8712020-02-12 17:46:22648 chars += Components.Linkifier.Linkifier.untruncatedNodeText(node).length;
Blink Reformat4c46d092018-04-07 15:32:37649 }
650 // If the selected node text was truncated, treat any non-zero offset as the full length.
Tim van der Lippe9b2f8712020-02-12 17:46:22651 const untruncatedContainerLength = Components.Linkifier.Linkifier.untruncatedNodeText(selectionNode).length;
Tim van der Lippe1d6e57a2019-09-30 11:55:34652 if (offset > 0 && untruncatedContainerLength !== selectionNode.textContent.length) {
Blink Reformat4c46d092018-04-07 15:32:37653 offset = untruncatedContainerLength;
Tim van der Lippe1d6e57a2019-09-30 11:55:34654 }
Blink Reformat4c46d092018-04-07 15:32:37655 return chars + offset;
656 }
657
658 /**
659 * @param {!Event} event
660 */
661 _onScroll(event) {
662 this.refresh();
663 }
664
665 /**
666 * @return {number}
667 */
668 firstVisibleIndex() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34669 if (!this._cumulativeHeights.length) {
Blink Reformat4c46d092018-04-07 15:32:37670 return -1;
Tim van der Lippe1d6e57a2019-09-30 11:55:34671 }
Blink Reformat4c46d092018-04-07 15:32:37672 this._rebuildCumulativeHeightsIfNeeded();
673 return this._cumulativeHeights.lowerBound(this.element.scrollTop + 1);
674 }
675
676 /**
677 * @return {number}
678 */
679 lastVisibleIndex() {
Tim van der Lippe1d6e57a2019-09-30 11:55:34680 if (!this._cumulativeHeights.length) {
Blink Reformat4c46d092018-04-07 15:32:37681 return -1;
Tim van der Lippe1d6e57a2019-09-30 11:55:34682 }
Blink Reformat4c46d092018-04-07 15:32:37683 this._rebuildCumulativeHeightsIfNeeded();
684 const scrollBottom = this.element.scrollTop + this.element.clientHeight;
685 const right = this._itemCount - 1;
686 return this._cumulativeHeights.lowerBound(scrollBottom, undefined, undefined, right);
687 }
688
689 /**
690 * @return {?Element}
691 */
692 renderedElementAt(index) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34693 if (index === -1 || index < this._firstActiveIndex || index > this._lastActiveIndex) {
Blink Reformat4c46d092018-04-07 15:32:37694 return null;
Tim van der Lippe1d6e57a2019-09-30 11:55:34695 }
Blink Reformat4c46d092018-04-07 15:32:37696 return this._renderedItems[index - this._firstActiveIndex].element();
697 }
698
699 /**
700 * @param {number} index
701 * @param {boolean=} makeLast
702 */
703 scrollItemIntoView(index, makeLast) {
704 const firstVisibleIndex = this.firstVisibleIndex();
705 const lastVisibleIndex = this.lastVisibleIndex();
Tim van der Lippe1d6e57a2019-09-30 11:55:34706 if (index > firstVisibleIndex && index < lastVisibleIndex) {
Blink Reformat4c46d092018-04-07 15:32:37707 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34708 }
Erik Luo4b002322018-07-30 21:23:31709 // If the prompt is visible, then the last item must be fully on screen.
Tim van der Lippe1d6e57a2019-09-30 11:55:34710 if (index === lastVisibleIndex &&
711 this._cumulativeHeights[index] <= this.element.scrollTop + this._visibleHeight()) {
Erik Luo4b002322018-07-30 21:23:31712 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34713 }
714 if (makeLast) {
Blink Reformat4c46d092018-04-07 15:32:37715 this.forceScrollItemToBeLast(index);
Tim van der Lippe1d6e57a2019-09-30 11:55:34716 } else if (index <= firstVisibleIndex) {
Blink Reformat4c46d092018-04-07 15:32:37717 this.forceScrollItemToBeFirst(index);
Tim van der Lippe1d6e57a2019-09-30 11:55:34718 } else if (index >= lastVisibleIndex) {
Blink Reformat4c46d092018-04-07 15:32:37719 this.forceScrollItemToBeLast(index);
Tim van der Lippe1d6e57a2019-09-30 11:55:34720 }
Blink Reformat4c46d092018-04-07 15:32:37721 }
722
723 /**
724 * @param {number} index
725 */
726 forceScrollItemToBeFirst(index) {
727 console.assert(index >= 0 && index < this._itemCount, 'Cannot scroll item at invalid index');
728 this.setStickToBottom(false);
729 this._rebuildCumulativeHeightsIfNeeded();
730 this.element.scrollTop = index > 0 ? this._cumulativeHeights[index - 1] : 0;
Tim van der Lippe1d6e57a2019-09-30 11:55:34731 if (this.element.isScrolledToBottom()) {
Blink Reformat4c46d092018-04-07 15:32:37732 this.setStickToBottom(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:34733 }
Blink Reformat4c46d092018-04-07 15:32:37734 this.refresh();
Erik Luo4b002322018-07-30 21:23:31735 // After refresh, the item is in DOM, but may not be visible (items above were larger than expected).
736 this.renderedElementAt(index).scrollIntoView(true /* alignTop */);
Blink Reformat4c46d092018-04-07 15:32:37737 }
738
739 /**
740 * @param {number} index
741 */
742 forceScrollItemToBeLast(index) {
743 console.assert(index >= 0 && index < this._itemCount, 'Cannot scroll item at invalid index');
744 this.setStickToBottom(false);
745 this._rebuildCumulativeHeightsIfNeeded();
746 this.element.scrollTop = this._cumulativeHeights[index] - this._visibleHeight();
Tim van der Lippe1d6e57a2019-09-30 11:55:34747 if (this.element.isScrolledToBottom()) {
Blink Reformat4c46d092018-04-07 15:32:37748 this.setStickToBottom(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:34749 }
Blink Reformat4c46d092018-04-07 15:32:37750 this.refresh();
Erik Luo4b002322018-07-30 21:23:31751 // After refresh, the item is in DOM, but may not be visible (items above were larger than expected).
752 this.renderedElementAt(index).scrollIntoView(false /* alignTop */);
Blink Reformat4c46d092018-04-07 15:32:37753 }
754
755 /**
756 * @return {number}
757 */
758 _visibleHeight() {
759 // Use offsetHeight instead of clientHeight to avoid being affected by horizontal scroll.
760 return this.element.offsetHeight;
761 }
Paul Lewisbf7aa3c2019-11-20 17:03:38762}
Blink Reformat4c46d092018-04-07 15:32:37763
764/**
765 * @interface
766 */
Tim van der Lippeeaacb722020-01-10 12:16:00767export class ConsoleViewportProvider {
Blink Reformat4c46d092018-04-07 15:32:37768 /**
769 * @param {number} index
770 * @return {number}
771 */
772 fastHeight(index) {
773 return 0;
Paul Lewisbf7aa3c2019-11-20 17:03:38774 }
Blink Reformat4c46d092018-04-07 15:32:37775
776 /**
777 * @return {number}
778 */
779 itemCount() {
780 return 0;
Paul Lewisbf7aa3c2019-11-20 17:03:38781 }
Blink Reformat4c46d092018-04-07 15:32:37782
783 /**
784 * @return {number}
785 */
786 minimumRowHeight() {
787 return 0;
Paul Lewisbf7aa3c2019-11-20 17:03:38788 }
Blink Reformat4c46d092018-04-07 15:32:37789
790 /**
791 * @param {number} index
Tim van der Lippeeaacb722020-01-10 12:16:00792 * @return {?ConsoleViewportElement}
Blink Reformat4c46d092018-04-07 15:32:37793 */
794 itemElement(index) {
795 return null;
796 }
Paul Lewisbf7aa3c2019-11-20 17:03:38797}
Blink Reformat4c46d092018-04-07 15:32:37798
799/**
800 * @interface
801 */
Paul Lewisbf7aa3c2019-11-20 17:03:38802export class ConsoleViewportElement {
803 willHide() {
804 }
Blink Reformat4c46d092018-04-07 15:32:37805
Paul Lewisbf7aa3c2019-11-20 17:03:38806 wasShown() {
807 }
Blink Reformat4c46d092018-04-07 15:32:37808
809 /**
810 * @return {!Element}
811 */
Paul Lewisbf7aa3c2019-11-20 17:03:38812 element() {
813 }
814}