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

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

Issue 2179123004: DevTools: fix stick to bottom in console viewport (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Visible ranges too Created 4 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 /* 1 /*
2 * Copyright (C) 2013 Google Inc. All rights reserved. 2 * Copyright (C) 2013 Google Inc. All rights reserved.
3 * 3 *
4 * Redistribution and use in source and binary forms, with or without 4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are 5 * modification, are permitted provided that the following conditions are
6 * met: 6 * met:
7 * 7 *
8 * * Redistributions of source code must retain the above copyright 8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer. 9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above 10 * * Redistributions in binary form must reproduce the above
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after
52 this._provider = provider; 52 this._provider = provider;
53 this.element.addEventListener("scroll", this._onScroll.bind(this), false); 53 this.element.addEventListener("scroll", this._onScroll.bind(this), false);
54 this.element.addEventListener("copy", this._onCopy.bind(this), false); 54 this.element.addEventListener("copy", this._onCopy.bind(this), false);
55 this.element.addEventListener("dragstart", this._onDragStart.bind(this), fal se); 55 this.element.addEventListener("dragstart", this._onDragStart.bind(this), fal se);
56 56
57 this._firstVisibleIndex = 0; 57 this._firstVisibleIndex = 0;
58 this._lastVisibleIndex = -1; 58 this._lastVisibleIndex = -1;
59 this._renderedItems = []; 59 this._renderedItems = [];
60 this._anchorSelection = null; 60 this._anchorSelection = null;
61 this._headSelection = null; 61 this._headSelection = null;
62 this._stickToBottom = false;
63 this._scrolledToBottom = true;
64 this._itemCount = 0; 62 this._itemCount = 0;
63
64 // Listen for any changes to descendants and trigger a refresh. This ensures
65 // that items updated asynchronously will not break stick-to-bottom behavior
66 // if they change the scroll height.
67 this._observer = new MutationObserver(this.refresh.bind(this));
68 this._observerConfig = { childList: true, subtree: true };
65 } 69 }
66 70
67 /** 71 /**
68 * @interface 72 * @interface
69 */ 73 */
70 WebInspector.ViewportControl.Provider = function() 74 WebInspector.ViewportControl.Provider = function()
71 { 75 {
72 } 76 }
73 77
74 WebInspector.ViewportControl.Provider.prototype = { 78 WebInspector.ViewportControl.Provider.prototype = {
(...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after
138 element: function() 142 element: function()
139 { 143 {
140 return this._element; 144 return this._element;
141 }, 145 },
142 } 146 }
143 147
144 WebInspector.ViewportControl.prototype = { 148 WebInspector.ViewportControl.prototype = {
145 /** 149 /**
146 * @return {boolean} 150 * @return {boolean}
147 */ 151 */
148 scrolledToBottom: function() 152 stickToBottom: function()
149 { 153 {
150 return this._scrolledToBottom; 154 return this._stickToBottom;
151 }, 155 },
152 156
153 /** 157 /**
154 * @param {boolean} value 158 * @param {boolean} value
155 */ 159 */
156 setStickToBottom: function(value) 160 setStickToBottom: function(value)
157 { 161 {
158 this._stickToBottom = value; 162 this._stickToBottom = value;
163 if (this._stickToBottom)
164 this._observer.observe(this._contentElement, this._observerConfig);
165 else
166 this._observer.disconnect();
159 }, 167 },
160 168
161 /** 169 /**
162 * @param {!Event} event 170 * @param {!Event} event
163 */ 171 */
164 _onCopy: function(event) 172 _onCopy: function(event)
165 { 173 {
166 var text = this._selectedText(); 174 var text = this._selectedText();
167 if (!text) 175 if (!text)
168 return; 176 return;
(...skipping 98 matching lines...) Expand 10 before | Expand all | Expand 10 after
267 _createSelectionModel: function(itemIndex, node, offset) 275 _createSelectionModel: function(itemIndex, node, offset)
268 { 276 {
269 return { 277 return {
270 item: itemIndex, 278 item: itemIndex,
271 node: node, 279 node: node,
272 offset: offset 280 offset: offset
273 }; 281 };
274 }, 282 },
275 283
276 /** 284 /**
285 * @return {?Range}
286 */
287 getVisibleRange: function()
288 {
289 var selection = this.element.getComponentSelection();
290 var range = selection && selection.rangeCount ? selection.getRangeAt(0) : null;
291 if (!range || selection.isCollapsed)
292 return null;
293
294 var firstSelected = Number.MAX_VALUE;
295 var lastSelected = -1;
296
297 var hasVisibleSelection = false;
298 for (var i = 0; i < this._renderedItems.length; ++i) {
299 if (range.intersectsNode(this._renderedItems[i].element())) {
300 var index = i + this._firstVisibleIndex;
301 firstSelected = Math.min(firstSelected, index);
302 lastSelected = Math.max(lastSelected, index);
303 hasVisibleSelection = true;
304 }
305 }
306 return hasVisibleSelection ? range : null;
307 },
308
309 /**
277 * @param {?Selection} selection 310 * @param {?Selection} selection
278 */ 311 */
279 _updateSelectionModel: function(selection) 312 _updateSelectionModel: function(selection)
280 { 313 {
281 var range = selection && selection.rangeCount ? selection.getRangeAt(0) : null; 314 var range = selection && selection.rangeCount ? selection.getRangeAt(0) : null;
282 if (!range || selection.isCollapsed) { 315 if (!range || selection.isCollapsed) {
283 this._headSelection = null; 316 this._headSelection = null;
284 this._anchorSelection = null; 317 this._anchorSelection = null;
285 return false; 318 return false;
286 } 319 }
(...skipping 82 matching lines...) Expand 10 before | Expand all | Expand 10 after
369 else if (this._headSelection.item > this._lastVisibleIndex) 402 else if (this._headSelection.item > this._lastVisibleIndex)
370 headElement = this._bottomGapElement; 403 headElement = this._bottomGapElement;
371 headOffset = this._selectionIsBackward ? 0 : 1; 404 headOffset = this._selectionIsBackward ? 0 : 1;
372 } 405 }
373 406
374 selection.setBaseAndExtent(anchorElement, anchorOffset, headElement, hea dOffset); 407 selection.setBaseAndExtent(anchorElement, anchorOffset, headElement, hea dOffset);
375 }, 408 },
376 409
377 refresh: function() 410 refresh: function()
378 { 411 {
412 this._observer.disconnect();
413 this._innerRefresh();
414 if (this._stickToBottom)
415 this._observer.observe(this._contentElement, this._observerConfig);
416 },
417
418 _innerRefresh: function()
419 {
379 if (!this._visibleHeight()) 420 if (!this._visibleHeight())
380 return; // Do nothing for invisible controls. 421 return; // Do nothing for invisible controls.
381 422
382 if (!this._itemCount) { 423 if (!this._itemCount) {
383 for (var i = 0; i < this._renderedItems.length; ++i) 424 for (var i = 0; i < this._renderedItems.length; ++i)
384 this._renderedItems[i].willHide(); 425 this._renderedItems[i].willHide();
385 this._renderedItems = []; 426 this._renderedItems = [];
386 this._contentElement.removeChildren(); 427 this._contentElement.removeChildren();
387 this._topGapElement.style.height = "0px"; 428 this._topGapElement.style.height = "0px";
388 this._bottomGapElement.style.height = "0px"; 429 this._bottomGapElement.style.height = "0px";
389 this._firstVisibleIndex = -1; 430 this._firstVisibleIndex = -1;
390 this._lastVisibleIndex = -1; 431 this._lastVisibleIndex = -1;
391 return; 432 return;
392 } 433 }
393 434
394 var selection = this.element.getComponentSelection(); 435 var selection = this.element.getComponentSelection();
395 var shouldRestoreSelection = this._updateSelectionModel(selection); 436 var shouldRestoreSelection = this._updateSelectionModel(selection);
396 437
397 var visibleFrom = this.element.scrollTop; 438 var visibleFrom = this.element.scrollTop;
398 var visibleHeight = this._visibleHeight(); 439 var visibleHeight = this._visibleHeight();
399 this._scrolledToBottom = this.element.isScrolledToBottom();
400 var isInvalidating = !this._cumulativeHeights; 440 var isInvalidating = !this._cumulativeHeights;
401 441
402 for (var i = 0; i < this._renderedItems.length; ++i) { 442 for (var i = 0; i < this._renderedItems.length; ++i) {
403 // Tolerate 1-pixel error due to double-to-integer rounding errors. 443 // Tolerate 1-pixel error due to double-to-integer rounding errors.
404 if (this._cumulativeHeights && Math.abs(this._cachedItemHeight(this. _firstVisibleIndex + i) - this._renderedItems[i].element().offsetHeight) > 1) 444 if (this._cumulativeHeights && Math.abs(this._cachedItemHeight(this. _firstVisibleIndex + i) - this._renderedItems[i].element().offsetHeight) > 1)
405 delete this._cumulativeHeights; 445 delete this._cumulativeHeights;
406 } 446 }
407 this._rebuildCumulativeHeightsIfNeeded(); 447 this._rebuildCumulativeHeightsIfNeeded();
408 var oldFirstVisibleIndex = this._firstVisibleIndex; 448 var oldFirstVisibleIndex = this._firstVisibleIndex;
409 var oldLastVisibleIndex = this._lastVisibleIndex; 449 var oldLastVisibleIndex = this._lastVisibleIndex;
410 450
411 var shouldStickToBottom = this._stickToBottom && this._scrolledToBottom; 451 // When the viewport is scrolled to the bottom, using the cumulative hei ghts estimate is not
412 452 // precise enough to determine next visible indices. This stickToBottom check avoids extra
413 if (shouldStickToBottom) { 453 // calls to refresh in those cases.
454 if (this._stickToBottom) {
455 this._firstVisibleIndex = Math.max(this._itemCount - Math.ceil(visib leHeight / this._provider.minimumRowHeight()), 0);
414 this._lastVisibleIndex = this._itemCount - 1; 456 this._lastVisibleIndex = this._itemCount - 1;
415 this._firstVisibleIndex = Math.max(this._itemCount - Math.ceil(visib leHeight / this._provider.minimumRowHeight()), 0);
416 } else { 457 } else {
417 this._firstVisibleIndex = Math.max(Array.prototype.lowerBound.call(t his._cumulativeHeights, visibleFrom + 1), 0); 458 this._firstVisibleIndex = Math.max(Array.prototype.lowerBound.call(t his._cumulativeHeights, visibleFrom + 1), 0);
418 // Proactively render more rows in case some of them will be collaps ed without triggering refresh. @see crbug.com/390169 459 // Proactively render more rows in case some of them will be collaps ed without triggering refresh. @see crbug.com/390169
419 this._lastVisibleIndex = this._firstVisibleIndex + Math.ceil(visible Height / this._provider.minimumRowHeight()) - 1; 460 this._lastVisibleIndex = this._firstVisibleIndex + Math.ceil(visible Height / this._provider.minimumRowHeight()) - 1;
420 this._lastVisibleIndex = Math.min(this._lastVisibleIndex, this._item Count - 1); 461 this._lastVisibleIndex = Math.min(this._lastVisibleIndex, this._item Count - 1);
421 } 462 }
463
422 var topGapHeight = this._cumulativeHeights[this._firstVisibleIndex - 1] || 0; 464 var topGapHeight = this._cumulativeHeights[this._firstVisibleIndex - 1] || 0;
423 var bottomGapHeight = this._cumulativeHeights[this._cumulativeHeights.le ngth - 1] - this._cumulativeHeights[this._lastVisibleIndex]; 465 var bottomGapHeight = this._cumulativeHeights[this._cumulativeHeights.le ngth - 1] - this._cumulativeHeights[this._lastVisibleIndex];
424 466
425 /** 467 /**
426 * @this {WebInspector.ViewportControl} 468 * @this {WebInspector.ViewportControl}
427 */ 469 */
428 function prepare() 470 function prepare()
429 { 471 {
430 this._topGapElement.style.height = topGapHeight + "px"; 472 this._topGapElement.style.height = topGapHeight + "px";
431 this._bottomGapElement.style.height = bottomGapHeight + "px"; 473 this._bottomGapElement.style.height = bottomGapHeight + "px";
432 this._topGapElement._active = !!topGapHeight; 474 this._topGapElement._active = !!topGapHeight;
433 this._bottomGapElement._active = !!bottomGapHeight; 475 this._bottomGapElement._active = !!bottomGapHeight;
434 this._contentElement.style.setProperty("height", "10000000px"); 476 this._contentElement.style.setProperty("height", "10000000px");
435 } 477 }
436 478
437 if (isInvalidating) 479 if (isInvalidating)
438 this._fullViewportUpdate(prepare.bind(this)); 480 this._fullViewportUpdate(prepare.bind(this));
439 else 481 else
440 this._partialViewportUpdate(oldFirstVisibleIndex, oldLastVisibleInde x, prepare.bind(this)); 482 this._partialViewportUpdate(oldFirstVisibleIndex, oldLastVisibleInde x, prepare.bind(this));
441 this._contentElement.style.removeProperty("height"); 483 this._contentElement.style.removeProperty("height");
442 // Should be the last call in the method as it might force layout. 484 // Should be the last call in the method as it might force layout.
443 if (shouldRestoreSelection) 485 if (shouldRestoreSelection)
444 this._restoreSelection(selection); 486 this._restoreSelection(selection);
445 if (shouldStickToBottom) 487 if (this._stickToBottom)
446 this.element.scrollTop = 10000000; 488 this.element.scrollTop = 10000000;
447 }, 489 },
448 490
449 /** 491 /**
450 * @param {function()} prepare 492 * @param {function()} prepare
451 */ 493 */
452 _fullViewportUpdate: function(prepare) 494 _fullViewportUpdate: function(prepare)
453 { 495 {
454 for (var i = 0; i < this._renderedItems.length; ++i) 496 for (var i = 0; i < this._renderedItems.length; ++i)
455 this._renderedItems[i].willHide(); 497 this._renderedItems[i].willHide();
(...skipping 149 matching lines...) Expand 10 before | Expand all | Expand 10 after
605 this.forceScrollItemToBeFirst(index); 647 this.forceScrollItemToBeFirst(index);
606 else if (index >= this._lastVisibleIndex) 648 else if (index >= this._lastVisibleIndex)
607 this.forceScrollItemToBeLast(index); 649 this.forceScrollItemToBeLast(index);
608 }, 650 },
609 651
610 /** 652 /**
611 * @param {number} index 653 * @param {number} index
612 */ 654 */
613 forceScrollItemToBeFirst: function(index) 655 forceScrollItemToBeFirst: function(index)
614 { 656 {
657 this.setStickToBottom(false);
615 this._rebuildCumulativeHeightsIfNeeded(); 658 this._rebuildCumulativeHeightsIfNeeded();
616 this.element.scrollTop = index > 0 ? this._cumulativeHeights[index - 1] : 0; 659 this.element.scrollTop = index > 0 ? this._cumulativeHeights[index - 1] : 0;
660 if (this.element.isScrolledToBottom())
661 this.setStickToBottom(true);
617 this.refresh(); 662 this.refresh();
618 }, 663 },
619 664
620 /** 665 /**
621 * @param {number} index 666 * @param {number} index
622 */ 667 */
623 forceScrollItemToBeLast: function(index) 668 forceScrollItemToBeLast: function(index)
624 { 669 {
670 this.setStickToBottom(false);
625 this._rebuildCumulativeHeightsIfNeeded(); 671 this._rebuildCumulativeHeightsIfNeeded();
626 this.element.scrollTop = this._cumulativeHeights[index] - this._visibleH eight(); 672 this.element.scrollTop = this._cumulativeHeights[index] - this._visibleH eight();
673 if (this.element.isScrolledToBottom())
674 this.setStickToBottom(true);
627 this.refresh(); 675 this.refresh();
628 }, 676 },
629 677
630 /** 678 /**
631 * @return {number} 679 * @return {number}
632 */ 680 */
633 _visibleHeight: function() 681 _visibleHeight: function()
634 { 682 {
635 // Use offsetHeight instead of clientHeight to avoid being affected by h orizontal scroll. 683 // Use offsetHeight instead of clientHeight to avoid being affected by h orizontal scroll.
636 return this.element.offsetHeight; 684 return this.element.offsetHeight;
637 } 685 }
638 } 686 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698