| 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 |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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 Loading... |
| 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 } |
| OLD | NEW |