OLD | NEW |
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 Loading... |
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 this._observer = new MutationObserver(this.refresh.bind(this)); |
| 64 this._observerConfig = { childList: true, subtree: true }; |
| 65 |
| 66 this.setStickToBottom(true); |
65 } | 67 } |
66 | 68 |
67 /** | 69 /** |
68 * @interface | 70 * @interface |
69 */ | 71 */ |
70 WebInspector.ViewportControl.Provider = function() | 72 WebInspector.ViewportControl.Provider = function() |
71 { | 73 { |
72 } | 74 } |
73 | 75 |
74 WebInspector.ViewportControl.Provider.prototype = { | 76 WebInspector.ViewportControl.Provider.prototype = { |
(...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
138 element: function() | 140 element: function() |
139 { | 141 { |
140 return this._element; | 142 return this._element; |
141 }, | 143 }, |
142 } | 144 } |
143 | 145 |
144 WebInspector.ViewportControl.prototype = { | 146 WebInspector.ViewportControl.prototype = { |
145 /** | 147 /** |
146 * @return {boolean} | 148 * @return {boolean} |
147 */ | 149 */ |
148 scrolledToBottom: function() | 150 stickToBottom: function() |
149 { | 151 { |
150 return this._scrolledToBottom; | 152 return this._stickToBottom; |
151 }, | 153 }, |
152 | 154 |
153 /** | 155 /** |
154 * @param {boolean} value | 156 * @param {boolean} value |
155 */ | 157 */ |
156 setStickToBottom: function(value) | 158 setStickToBottom: function(value) |
157 { | 159 { |
158 this._stickToBottom = value; | 160 this._stickToBottom = value; |
| 161 if (this._stickToBottom) |
| 162 this._observer.observe(this._contentElement, this._observerConfig); |
| 163 else |
| 164 this._observer.disconnect(); |
159 }, | 165 }, |
160 | 166 |
161 /** | 167 /** |
162 * @param {!Event} event | 168 * @param {!Event} event |
163 */ | 169 */ |
164 _onCopy: function(event) | 170 _onCopy: function(event) |
165 { | 171 { |
166 var text = this._selectedText(); | 172 var text = this._selectedText(); |
167 if (!text) | 173 if (!text) |
168 return; | 174 return; |
(...skipping 200 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
369 else if (this._headSelection.item > this._lastVisibleIndex) | 375 else if (this._headSelection.item > this._lastVisibleIndex) |
370 headElement = this._bottomGapElement; | 376 headElement = this._bottomGapElement; |
371 headOffset = this._selectionIsBackward ? 0 : 1; | 377 headOffset = this._selectionIsBackward ? 0 : 1; |
372 } | 378 } |
373 | 379 |
374 selection.setBaseAndExtent(anchorElement, anchorOffset, headElement, hea
dOffset); | 380 selection.setBaseAndExtent(anchorElement, anchorOffset, headElement, hea
dOffset); |
375 }, | 381 }, |
376 | 382 |
377 refresh: function() | 383 refresh: function() |
378 { | 384 { |
| 385 this._observer.disconnect(); |
| 386 this._innerRefresh(); |
| 387 if (this._stickToBottom) |
| 388 this._observer.observe(this._contentElement, this._observerConfig); |
| 389 }, |
| 390 |
| 391 _innerRefresh: function() |
| 392 { |
379 if (!this._visibleHeight()) | 393 if (!this._visibleHeight()) |
380 return; // Do nothing for invisible controls. | 394 return; // Do nothing for invisible controls. |
381 | 395 |
382 if (!this._itemCount) { | 396 if (!this._itemCount) { |
383 for (var i = 0; i < this._renderedItems.length; ++i) | 397 for (var i = 0; i < this._renderedItems.length; ++i) |
384 this._renderedItems[i].willHide(); | 398 this._renderedItems[i].willHide(); |
385 this._renderedItems = []; | 399 this._renderedItems = []; |
386 this._contentElement.removeChildren(); | 400 this._contentElement.removeChildren(); |
387 this._topGapElement.style.height = "0px"; | 401 this._topGapElement.style.height = "0px"; |
388 this._bottomGapElement.style.height = "0px"; | 402 this._bottomGapElement.style.height = "0px"; |
389 this._firstVisibleIndex = -1; | 403 this._firstVisibleIndex = -1; |
390 this._lastVisibleIndex = -1; | 404 this._lastVisibleIndex = -1; |
391 return; | 405 return; |
392 } | 406 } |
393 | 407 |
394 var selection = this.element.getComponentSelection(); | 408 var selection = this.element.getComponentSelection(); |
395 var shouldRestoreSelection = this._updateSelectionModel(selection); | 409 var shouldRestoreSelection = this._updateSelectionModel(selection); |
396 | 410 |
397 var visibleFrom = this.element.scrollTop; | 411 var visibleFrom = this.element.scrollTop; |
398 var visibleHeight = this._visibleHeight(); | 412 var visibleHeight = this._visibleHeight(); |
399 this._scrolledToBottom = this.element.isScrolledToBottom(); | |
400 var isInvalidating = !this._cumulativeHeights; | 413 var isInvalidating = !this._cumulativeHeights; |
401 | 414 |
402 for (var i = 0; i < this._renderedItems.length; ++i) { | 415 for (var i = 0; i < this._renderedItems.length; ++i) { |
403 // Tolerate 1-pixel error due to double-to-integer rounding errors. | 416 // 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) | 417 if (this._cumulativeHeights && Math.abs(this._cachedItemHeight(this.
_firstVisibleIndex + i) - this._renderedItems[i].element().offsetHeight) > 1) |
405 delete this._cumulativeHeights; | 418 delete this._cumulativeHeights; |
406 } | 419 } |
407 this._rebuildCumulativeHeightsIfNeeded(); | 420 this._rebuildCumulativeHeightsIfNeeded(); |
408 var oldFirstVisibleIndex = this._firstVisibleIndex; | 421 var oldFirstVisibleIndex = this._firstVisibleIndex; |
409 var oldLastVisibleIndex = this._lastVisibleIndex; | 422 var oldLastVisibleIndex = this._lastVisibleIndex; |
410 | 423 |
411 var shouldStickToBottom = this._stickToBottom && this._scrolledToBottom; | 424 this._firstVisibleIndex = Math.max(Array.prototype.lowerBound.call(this.
_cumulativeHeights, visibleFrom + 1), 0); |
| 425 // Proactively render more rows in case some of them will be collapsed w
ithout triggering refresh. @see crbug.com/390169 |
| 426 this._lastVisibleIndex = this._firstVisibleIndex + Math.ceil(visibleHeig
ht / this._provider.minimumRowHeight()) - 1; |
| 427 this._lastVisibleIndex = Math.min(this._lastVisibleIndex, this._itemCoun
t - 1); |
412 | 428 |
413 if (shouldStickToBottom) { | |
414 this._lastVisibleIndex = this._itemCount - 1; | |
415 this._firstVisibleIndex = Math.max(this._itemCount - Math.ceil(visib
leHeight / this._provider.minimumRowHeight()), 0); | |
416 } else { | |
417 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 | |
419 this._lastVisibleIndex = this._firstVisibleIndex + Math.ceil(visible
Height / this._provider.minimumRowHeight()) - 1; | |
420 this._lastVisibleIndex = Math.min(this._lastVisibleIndex, this._item
Count - 1); | |
421 } | |
422 var topGapHeight = this._cumulativeHeights[this._firstVisibleIndex - 1]
|| 0; | 429 var topGapHeight = this._cumulativeHeights[this._firstVisibleIndex - 1]
|| 0; |
423 var bottomGapHeight = this._cumulativeHeights[this._cumulativeHeights.le
ngth - 1] - this._cumulativeHeights[this._lastVisibleIndex]; | 430 var bottomGapHeight = this._cumulativeHeights[this._cumulativeHeights.le
ngth - 1] - this._cumulativeHeights[this._lastVisibleIndex]; |
424 | 431 |
425 /** | 432 /** |
426 * @this {WebInspector.ViewportControl} | 433 * @this {WebInspector.ViewportControl} |
427 */ | 434 */ |
428 function prepare() | 435 function prepare() |
429 { | 436 { |
430 this._topGapElement.style.height = topGapHeight + "px"; | 437 this._topGapElement.style.height = topGapHeight + "px"; |
431 this._bottomGapElement.style.height = bottomGapHeight + "px"; | 438 this._bottomGapElement.style.height = bottomGapHeight + "px"; |
432 this._topGapElement._active = !!topGapHeight; | 439 this._topGapElement._active = !!topGapHeight; |
433 this._bottomGapElement._active = !!bottomGapHeight; | 440 this._bottomGapElement._active = !!bottomGapHeight; |
434 this._contentElement.style.setProperty("height", "10000000px"); | 441 this._contentElement.style.setProperty("height", "10000000px"); |
435 } | 442 } |
436 | 443 |
437 if (isInvalidating) | 444 if (isInvalidating) |
438 this._fullViewportUpdate(prepare.bind(this)); | 445 this._fullViewportUpdate(prepare.bind(this)); |
439 else | 446 else |
440 this._partialViewportUpdate(oldFirstVisibleIndex, oldLastVisibleInde
x, prepare.bind(this)); | 447 this._partialViewportUpdate(oldFirstVisibleIndex, oldLastVisibleInde
x, prepare.bind(this)); |
441 this._contentElement.style.removeProperty("height"); | 448 this._contentElement.style.removeProperty("height"); |
442 // Should be the last call in the method as it might force layout. | 449 // Should be the last call in the method as it might force layout. |
443 if (shouldRestoreSelection) | 450 if (shouldRestoreSelection) |
444 this._restoreSelection(selection); | 451 this._restoreSelection(selection); |
445 if (shouldStickToBottom) | 452 if (this._stickToBottom) |
446 this.element.scrollTop = 10000000; | 453 this.element.scrollTop = 10000000; |
447 }, | 454 }, |
448 | 455 |
449 /** | 456 /** |
450 * @param {function()} prepare | 457 * @param {function()} prepare |
451 */ | 458 */ |
452 _fullViewportUpdate: function(prepare) | 459 _fullViewportUpdate: function(prepare) |
453 { | 460 { |
454 for (var i = 0; i < this._renderedItems.length; ++i) | 461 for (var i = 0; i < this._renderedItems.length; ++i) |
455 this._renderedItems[i].willHide(); | 462 this._renderedItems[i].willHide(); |
(...skipping 149 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
605 this.forceScrollItemToBeFirst(index); | 612 this.forceScrollItemToBeFirst(index); |
606 else if (index >= this._lastVisibleIndex) | 613 else if (index >= this._lastVisibleIndex) |
607 this.forceScrollItemToBeLast(index); | 614 this.forceScrollItemToBeLast(index); |
608 }, | 615 }, |
609 | 616 |
610 /** | 617 /** |
611 * @param {number} index | 618 * @param {number} index |
612 */ | 619 */ |
613 forceScrollItemToBeFirst: function(index) | 620 forceScrollItemToBeFirst: function(index) |
614 { | 621 { |
| 622 this.setStickToBottom(false); |
615 this._rebuildCumulativeHeightsIfNeeded(); | 623 this._rebuildCumulativeHeightsIfNeeded(); |
616 this.element.scrollTop = index > 0 ? this._cumulativeHeights[index - 1]
: 0; | 624 this.element.scrollTop = index > 0 ? this._cumulativeHeights[index - 1]
: 0; |
| 625 if (this.element.isScrolledToBottom()) |
| 626 this.setStickToBottom(true); |
617 this.refresh(); | 627 this.refresh(); |
618 }, | 628 }, |
619 | 629 |
620 /** | 630 /** |
621 * @param {number} index | 631 * @param {number} index |
622 */ | 632 */ |
623 forceScrollItemToBeLast: function(index) | 633 forceScrollItemToBeLast: function(index) |
624 { | 634 { |
| 635 this.setStickToBottom(false); |
625 this._rebuildCumulativeHeightsIfNeeded(); | 636 this._rebuildCumulativeHeightsIfNeeded(); |
626 this.element.scrollTop = this._cumulativeHeights[index] - this._visibleH
eight(); | 637 this.element.scrollTop = this._cumulativeHeights[index] - this._visibleH
eight(); |
| 638 if (this.element.isScrolledToBottom()) |
| 639 this.setStickToBottom(true); |
627 this.refresh(); | 640 this.refresh(); |
628 }, | 641 }, |
629 | 642 |
630 /** | 643 /** |
631 * @return {number} | 644 * @return {number} |
632 */ | 645 */ |
633 _visibleHeight: function() | 646 _visibleHeight: function() |
634 { | 647 { |
635 // Use offsetHeight instead of clientHeight to avoid being affected by h
orizontal scroll. | 648 // Use offsetHeight instead of clientHeight to avoid being affected by h
orizontal scroll. |
636 return this.element.offsetHeight; | 649 return this.element.offsetHeight; |
637 } | 650 } |
638 } | 651 } |
OLD | NEW |