| OLD | NEW |
| 1 /* | 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 * Copyright (C) 2013 Google Inc. All rights reserved. | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 * | 3 // found in the LICENSE file. |
| 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 | |
| 31 /** | 4 /** |
| 32 * @unrestricted | 5 * @unrestricted |
| 33 */ | 6 */ |
| 34 UI.ViewportControl = class { | 7 UI.ViewportControl = class { |
| 35 /** | 8 /** |
| 36 * @param {!UI.ViewportControl.Provider} provider | 9 * @param {!UI.ViewportControl.Provider} provider |
| 37 */ | 10 */ |
| 38 constructor(provider) { | 11 constructor(provider) { |
| 39 this.element = createElement('div'); | 12 this.element = createElement('div'); |
| 40 this.element.style.overflow = 'auto'; | 13 this.element.style.overflow = 'auto'; |
| 41 this._topGapElement = this.element.createChild('div'); | 14 this._innerElement = this.element.createChild('div'); |
| 42 this._topGapElement.style.height = '0px'; | 15 this._innerElement.style.height = '0px'; |
| 43 this._topGapElement.style.color = 'transparent'; | 16 this._innerElement.style.position = 'relative'; |
| 44 this._contentElement = this.element.createChild('div'); | 17 this._innerElement.style.overflow = 'hidden'; |
| 45 this._bottomGapElement = this.element.createChild('div'); | |
| 46 this._bottomGapElement.style.height = '0px'; | |
| 47 this._bottomGapElement.style.color = 'transparent'; | |
| 48 | |
| 49 // Text content needed for range intersection checks in _updateSelectionMode
l. | |
| 50 // Use Unicode ZERO WIDTH NO-BREAK SPACE, which avoids contributing any heig
ht to the element's layout overflow. | |
| 51 this._topGapElement.textContent = '\uFEFF'; | |
| 52 this._bottomGapElement.textContent = '\uFEFF'; | |
| 53 | 18 |
| 54 this._provider = provider; | 19 this._provider = provider; |
| 55 this.element.addEventListener('scroll', this._onScroll.bind(this), false); | 20 this.element.addEventListener('scroll', this._update.bind(this), false); |
| 56 this.element.addEventListener('copy', this._onCopy.bind(this), false); | |
| 57 this.element.addEventListener('dragstart', this._onDragStart.bind(this), fal
se); | |
| 58 | |
| 59 this._firstActiveIndex = 0; | |
| 60 this._lastActiveIndex = -1; | |
| 61 this._renderedItems = []; | |
| 62 this._anchorSelection = null; | |
| 63 this._headSelection = null; | |
| 64 this._itemCount = 0; | 21 this._itemCount = 0; |
| 65 | 22 this._indexSymbol = Symbol('UI.ViewportControl._indexSymbol'); |
| 66 // Listen for any changes to descendants and trigger a refresh. This ensures | |
| 67 // that items updated asynchronously will not break stick-to-bottom behavior | |
| 68 // if they change the scroll height. | |
| 69 this._observer = new MutationObserver(this.refresh.bind(this)); | |
| 70 this._observerConfig = {childList: true, subtree: true}; | |
| 71 } | 23 } |
| 72 | 24 |
| 73 /** | 25 refresh() { |
| 74 * @return {boolean} | 26 this._itemCount = this._provider.itemCount(); |
| 75 */ | 27 this._innerElement.removeChildren(); |
| 76 stickToBottom() { | 28 |
| 77 return this._stickToBottom; | 29 var height = 0; |
| 30 this._cumulativeHeights = new Int32Array(this._itemCount); |
| 31 for (var i = 0; i < this._itemCount; ++i) { |
| 32 height += this._provider.fastItemHeight(i); |
| 33 this._cumulativeHeights[i] = height; |
| 34 } |
| 35 this._innerElement.style.height = height + 'px'; |
| 36 |
| 37 this._update(); |
| 78 } | 38 } |
| 79 | 39 |
| 80 /** | 40 _update() { |
| 81 * @param {boolean} value | 41 if (!this._cumulativeHeights) { |
| 82 */ | 42 this.refresh(); |
| 83 setStickToBottom(value) { | 43 return; |
| 84 this._stickToBottom = value; | 44 } |
| 85 if (this._stickToBottom) | |
| 86 this._observer.observe(this._contentElement, this._observerConfig); | |
| 87 else | |
| 88 this._observer.disconnect(); | |
| 89 } | |
| 90 | 45 |
| 91 /** | 46 var visibleHeight = this._visibleHeight(); |
| 92 * @param {!Event} event | 47 var visibleFrom = this.element.scrollTop; |
| 93 */ | 48 var activeHeight = visibleHeight * 2; |
| 94 _onCopy(event) { | 49 var firstActiveIndex = Math.max( |
| 95 var text = this._selectedText(); | 50 Array.prototype.lowerBound.call(this._cumulativeHeights, visibleFrom + 1
- (activeHeight - visibleHeight) / 2), |
| 96 if (!text) | 51 0); |
| 97 return; | 52 var lastActiveIndex = Math.min( |
| 98 event.preventDefault(); | 53 Array.prototype.lowerBound.call( |
| 99 event.clipboardData.setData('text/plain', text); | 54 this._cumulativeHeights, visibleFrom + visibleHeight + (activeHeight
- visibleHeight) / 2), |
| 100 } | 55 this._itemCount - 1); |
| 101 | 56 |
| 102 /** | 57 var children = this._innerElement.children; |
| 103 * @param {!Event} event | 58 for (var i = children.length - 1; i >= 0; --i) { |
| 104 */ | 59 var element = children[i]; |
| 105 _onDragStart(event) { | 60 if (element[this._indexSymbol] < firstActiveIndex || element[this._indexSy
mbol] > lastActiveIndex) |
| 106 var text = this._selectedText(); | 61 element.remove(); |
| 107 if (!text) | 62 } |
| 108 return false; | |
| 109 event.dataTransfer.clearData(); | |
| 110 event.dataTransfer.setData('text/plain', text); | |
| 111 event.dataTransfer.effectAllowed = 'copy'; | |
| 112 return true; | |
| 113 } | |
| 114 | 63 |
| 115 /** | 64 for (var i = firstActiveIndex; i <= lastActiveIndex; ++i) |
| 116 * @return {!Element} | 65 this._insertElement(i); |
| 117 */ | |
| 118 contentElement() { | |
| 119 return this._contentElement; | |
| 120 } | |
| 121 | |
| 122 invalidate() { | |
| 123 delete this._cumulativeHeights; | |
| 124 delete this._cachedProviderElements; | |
| 125 this._itemCount = this._provider.itemCount(); | |
| 126 this.refresh(); | |
| 127 } | 66 } |
| 128 | 67 |
| 129 /** | 68 /** |
| 130 * @param {number} index | 69 * @param {number} index |
| 131 * @return {?UI.ViewportElement} | |
| 132 */ | 70 */ |
| 133 _providerElement(index) { | 71 _insertElement(index) { |
| 134 if (!this._cachedProviderElements) | 72 var element = this._provider.itemElement(index); |
| 135 this._cachedProviderElements = new Array(this._itemCount); | 73 if (!element || element.parentElement === this._innerElement) |
| 136 var element = this._cachedProviderElements[index]; | 74 return; |
| 137 if (!element) { | |
| 138 element = this._provider.itemElement(index); | |
| 139 this._cachedProviderElements[index] = element; | |
| 140 } | |
| 141 return element; | |
| 142 } | |
| 143 | 75 |
| 144 _rebuildCumulativeHeightsIfNeeded() { | 76 element.style.position = 'absolute'; |
| 145 if (this._cumulativeHeights) | 77 element.style.top = (this._cumulativeHeights[index - 1] || 0) + 'px'; |
| 146 return; | 78 element.style.left = '0'; |
| 147 if (!this._itemCount) | 79 element.style.right = '0'; |
| 148 return; | 80 element[this._indexSymbol] = index; |
| 149 var firstActiveIndex = this._firstActiveIndex; | 81 this._innerElement.appendChild(element); |
| 150 var lastActiveIndex = this._lastActiveIndex; | |
| 151 var height = 0; | |
| 152 this._cumulativeHeights = new Int32Array(this._itemCount); | |
| 153 for (var i = 0; i < this._itemCount; ++i) { | |
| 154 if (firstActiveIndex <= i && i <= lastActiveIndex) | |
| 155 height += this._renderedItems[i - firstActiveIndex].element().offsetHeig
ht; | |
| 156 else | |
| 157 height += this._provider.fastHeight(i); | |
| 158 this._cumulativeHeights[i] = height; | |
| 159 } | |
| 160 } | |
| 161 | |
| 162 /** | |
| 163 * @param {number} index | |
| 164 * @return {number} | |
| 165 */ | |
| 166 _cachedItemHeight(index) { | |
| 167 return index === 0 ? this._cumulativeHeights[0] : | |
| 168 this._cumulativeHeights[index] - this._cumulativeHeight
s[index - 1]; | |
| 169 } | |
| 170 | |
| 171 /** | |
| 172 * @param {?Selection} selection | |
| 173 * @suppressGlobalPropertiesCheck | |
| 174 */ | |
| 175 _isSelectionBackwards(selection) { | |
| 176 if (!selection || !selection.rangeCount) | |
| 177 return false; | |
| 178 var range = document.createRange(); | |
| 179 range.setStart(selection.anchorNode, selection.anchorOffset); | |
| 180 range.setEnd(selection.focusNode, selection.focusOffset); | |
| 181 return range.collapsed; | |
| 182 } | |
| 183 | |
| 184 /** | |
| 185 * @param {number} itemIndex | |
| 186 * @param {!Node} node | |
| 187 * @param {number} offset | |
| 188 * @return {!{item: number, node: !Node, offset: number}} | |
| 189 */ | |
| 190 _createSelectionModel(itemIndex, node, offset) { | |
| 191 return {item: itemIndex, node: node, offset: offset}; | |
| 192 } | |
| 193 | |
| 194 /** | |
| 195 * @param {?Selection} selection | |
| 196 */ | |
| 197 _updateSelectionModel(selection) { | |
| 198 var range = selection && selection.rangeCount ? selection.getRangeAt(0) : nu
ll; | |
| 199 if (!range || selection.isCollapsed || !this.element.hasSelection()) { | |
| 200 this._headSelection = null; | |
| 201 this._anchorSelection = null; | |
| 202 return false; | |
| 203 } | |
| 204 | |
| 205 var firstSelected = Number.MAX_VALUE; | |
| 206 var lastSelected = -1; | |
| 207 | |
| 208 var hasVisibleSelection = false; | |
| 209 for (var i = 0; i < this._renderedItems.length; ++i) { | |
| 210 if (range.intersectsNode(this._renderedItems[i].element())) { | |
| 211 var index = i + this._firstActiveIndex; | |
| 212 firstSelected = Math.min(firstSelected, index); | |
| 213 lastSelected = Math.max(lastSelected, index); | |
| 214 hasVisibleSelection = true; | |
| 215 } | |
| 216 } | |
| 217 if (hasVisibleSelection) { | |
| 218 firstSelected = | |
| 219 this._createSelectionModel(firstSelected, /** @type {!Node} */ (range.
startContainer), range.startOffset); | |
| 220 lastSelected = | |
| 221 this._createSelectionModel(lastSelected, /** @type {!Node} */ (range.e
ndContainer), range.endOffset); | |
| 222 } | |
| 223 var topOverlap = range.intersectsNode(this._topGapElement) && this._topGapEl
ement._active; | |
| 224 var bottomOverlap = range.intersectsNode(this._bottomGapElement) && this._bo
ttomGapElement._active; | |
| 225 if (!topOverlap && !bottomOverlap && !hasVisibleSelection) { | |
| 226 this._headSelection = null; | |
| 227 this._anchorSelection = null; | |
| 228 return false; | |
| 229 } | |
| 230 | |
| 231 if (!this._anchorSelection || !this._headSelection) { | |
| 232 this._anchorSelection = this._createSelectionModel(0, this.element, 0); | |
| 233 this._headSelection = this._createSelectionModel(this._itemCount - 1, this
.element, this.element.children.length); | |
| 234 this._selectionIsBackward = false; | |
| 235 } | |
| 236 | |
| 237 var isBackward = this._isSelectionBackwards(selection); | |
| 238 var startSelection = this._selectionIsBackward ? this._headSelection : this.
_anchorSelection; | |
| 239 var endSelection = this._selectionIsBackward ? this._anchorSelection : this.
_headSelection; | |
| 240 if (topOverlap && bottomOverlap && hasVisibleSelection) { | |
| 241 firstSelected = firstSelected.item < startSelection.item ? firstSelected :
startSelection; | |
| 242 lastSelected = lastSelected.item > endSelection.item ? lastSelected : endS
election; | |
| 243 } else if (!hasVisibleSelection) { | |
| 244 firstSelected = startSelection; | |
| 245 lastSelected = endSelection; | |
| 246 } else if (topOverlap) { | |
| 247 firstSelected = isBackward ? this._headSelection : this._anchorSelection; | |
| 248 } else if (bottomOverlap) { | |
| 249 lastSelected = isBackward ? this._anchorSelection : this._headSelection; | |
| 250 } | |
| 251 | |
| 252 if (isBackward) { | |
| 253 this._anchorSelection = lastSelected; | |
| 254 this._headSelection = firstSelected; | |
| 255 } else { | |
| 256 this._anchorSelection = firstSelected; | |
| 257 this._headSelection = lastSelected; | |
| 258 } | |
| 259 this._selectionIsBackward = isBackward; | |
| 260 return true; | |
| 261 } | |
| 262 | |
| 263 /** | |
| 264 * @param {?Selection} selection | |
| 265 */ | |
| 266 _restoreSelection(selection) { | |
| 267 var anchorElement = null; | |
| 268 var anchorOffset; | |
| 269 if (this._firstActiveIndex <= this._anchorSelection.item && this._anchorSele
ction.item <= this._lastActiveIndex) { | |
| 270 anchorElement = this._anchorSelection.node; | |
| 271 anchorOffset = this._anchorSelection.offset; | |
| 272 } else { | |
| 273 if (this._anchorSelection.item < this._firstActiveIndex) | |
| 274 anchorElement = this._topGapElement; | |
| 275 else if (this._anchorSelection.item > this._lastActiveIndex) | |
| 276 anchorElement = this._bottomGapElement; | |
| 277 anchorOffset = this._selectionIsBackward ? 1 : 0; | |
| 278 } | |
| 279 | |
| 280 var headElement = null; | |
| 281 var headOffset; | |
| 282 if (this._firstActiveIndex <= this._headSelection.item && this._headSelectio
n.item <= this._lastActiveIndex) { | |
| 283 headElement = this._headSelection.node; | |
| 284 headOffset = this._headSelection.offset; | |
| 285 } else { | |
| 286 if (this._headSelection.item < this._firstActiveIndex) | |
| 287 headElement = this._topGapElement; | |
| 288 else if (this._headSelection.item > this._lastActiveIndex) | |
| 289 headElement = this._bottomGapElement; | |
| 290 headOffset = this._selectionIsBackward ? 0 : 1; | |
| 291 } | |
| 292 | |
| 293 selection.setBaseAndExtent(anchorElement, anchorOffset, headElement, headOff
set); | |
| 294 } | |
| 295 | |
| 296 refresh() { | |
| 297 this._observer.disconnect(); | |
| 298 this._innerRefresh(); | |
| 299 if (this._stickToBottom) | |
| 300 this._observer.observe(this._contentElement, this._observerConfig); | |
| 301 } | |
| 302 | |
| 303 _innerRefresh() { | |
| 304 if (!this._visibleHeight()) | |
| 305 return; // Do nothing for invisible controls. | |
| 306 | |
| 307 if (!this._itemCount) { | |
| 308 for (var i = 0; i < this._renderedItems.length; ++i) | |
| 309 this._renderedItems[i].willHide(); | |
| 310 this._renderedItems = []; | |
| 311 this._contentElement.removeChildren(); | |
| 312 this._topGapElement.style.height = '0px'; | |
| 313 this._bottomGapElement.style.height = '0px'; | |
| 314 this._firstActiveIndex = -1; | |
| 315 this._lastActiveIndex = -1; | |
| 316 return; | |
| 317 } | |
| 318 | |
| 319 var selection = this.element.getComponentSelection(); | |
| 320 var shouldRestoreSelection = this._updateSelectionModel(selection); | |
| 321 | |
| 322 var visibleFrom = this.element.scrollTop; | |
| 323 var visibleHeight = this._visibleHeight(); | |
| 324 var isInvalidating = !this._cumulativeHeights; | |
| 325 | |
| 326 for (var i = 0; i < this._renderedItems.length; ++i) { | |
| 327 // Tolerate 1-pixel error due to double-to-integer rounding errors. | |
| 328 if (this._cumulativeHeights && | |
| 329 Math.abs(this._cachedItemHeight(this._firstActiveIndex + i) - this._re
nderedItems[i].element().offsetHeight) > | |
| 330 1) | |
| 331 delete this._cumulativeHeights; | |
| 332 } | |
| 333 this._rebuildCumulativeHeightsIfNeeded(); | |
| 334 var oldFirstActiveIndex = this._firstActiveIndex; | |
| 335 var oldLastActiveIndex = this._lastActiveIndex; | |
| 336 var activeHeight = visibleHeight * 2; | |
| 337 // When the viewport is scrolled to the bottom, using the cumulative heights
estimate is not | |
| 338 // precise enough to determine next visible indices. This stickToBottom chec
k avoids extra | |
| 339 // calls to refresh in those cases. | |
| 340 if (this._stickToBottom) { | |
| 341 this._firstActiveIndex = | |
| 342 Math.max(this._itemCount - Math.ceil(activeHeight / this._provider.min
imumRowHeight()), 0); | |
| 343 this._lastActiveIndex = this._itemCount - 1; | |
| 344 } else { | |
| 345 this._firstActiveIndex = Math.max( | |
| 346 Array.prototype.lowerBound.call( | |
| 347 this._cumulativeHeights, visibleFrom + 1 - (activeHeight - visible
Height) / 2), | |
| 348 0); | |
| 349 // Proactively render more rows in case some of them will be collapsed wit
hout triggering refresh. @see crbug.com/390169 | |
| 350 this._lastActiveIndex = this._firstActiveIndex + Math.ceil(activeHeight /
this._provider.minimumRowHeight()) - 1; | |
| 351 this._lastActiveIndex = Math.min(this._lastActiveIndex, this._itemCount -
1); | |
| 352 } | |
| 353 | |
| 354 var topGapHeight = this._cumulativeHeights[this._firstActiveIndex - 1] || 0; | |
| 355 var bottomGapHeight = | |
| 356 this._cumulativeHeights[this._cumulativeHeights.length - 1] - this._cumu
lativeHeights[this._lastActiveIndex]; | |
| 357 | |
| 358 /** | |
| 359 * @this {UI.ViewportControl} | |
| 360 */ | |
| 361 function prepare() { | |
| 362 this._topGapElement.style.height = topGapHeight + 'px'; | |
| 363 this._bottomGapElement.style.height = bottomGapHeight + 'px'; | |
| 364 this._topGapElement._active = !!topGapHeight; | |
| 365 this._bottomGapElement._active = !!bottomGapHeight; | |
| 366 this._contentElement.style.setProperty('height', '10000000px'); | |
| 367 } | |
| 368 | |
| 369 if (isInvalidating) | |
| 370 this._fullViewportUpdate(prepare.bind(this)); | |
| 371 else | |
| 372 this._partialViewportUpdate(oldFirstActiveIndex, oldLastActiveIndex, prepa
re.bind(this)); | |
| 373 this._contentElement.style.removeProperty('height'); | |
| 374 // Should be the last call in the method as it might force layout. | |
| 375 if (shouldRestoreSelection) | |
| 376 this._restoreSelection(selection); | |
| 377 if (this._stickToBottom) | |
| 378 this.element.scrollTop = 10000000; | |
| 379 } | |
| 380 | |
| 381 /** | |
| 382 * @param {function()} prepare | |
| 383 */ | |
| 384 _fullViewportUpdate(prepare) { | |
| 385 for (var i = 0; i < this._renderedItems.length; ++i) | |
| 386 this._renderedItems[i].willHide(); | |
| 387 prepare(); | |
| 388 this._renderedItems = []; | |
| 389 this._contentElement.removeChildren(); | |
| 390 for (var i = this._firstActiveIndex; i <= this._lastActiveIndex; ++i) { | |
| 391 var viewportElement = this._providerElement(i); | |
| 392 this._contentElement.appendChild(viewportElement.element()); | |
| 393 this._renderedItems.push(viewportElement); | |
| 394 } | |
| 395 for (var i = 0; i < this._renderedItems.length; ++i) | |
| 396 this._renderedItems[i].wasShown(); | |
| 397 } | |
| 398 | |
| 399 /** | |
| 400 * @param {number} oldFirstActiveIndex | |
| 401 * @param {number} oldLastActiveIndex | |
| 402 * @param {function()} prepare | |
| 403 */ | |
| 404 _partialViewportUpdate(oldFirstActiveIndex, oldLastActiveIndex, prepare) { | |
| 405 var willBeHidden = []; | |
| 406 for (var i = 0; i < this._renderedItems.length; ++i) { | |
| 407 var index = oldFirstActiveIndex + i; | |
| 408 if (index < this._firstActiveIndex || this._lastActiveIndex < index) | |
| 409 willBeHidden.push(this._renderedItems[i]); | |
| 410 } | |
| 411 for (var i = 0; i < willBeHidden.length; ++i) | |
| 412 willBeHidden[i].willHide(); | |
| 413 prepare(); | |
| 414 for (var i = 0; i < willBeHidden.length; ++i) | |
| 415 willBeHidden[i].element().remove(); | |
| 416 | |
| 417 this._renderedItems = []; | |
| 418 var anchor = this._contentElement.firstChild; | |
| 419 var wasShown = []; | |
| 420 for (var i = this._firstActiveIndex; i <= this._lastActiveIndex; ++i) { | |
| 421 var viewportElement = this._providerElement(i); | |
| 422 var element = viewportElement.element(); | |
| 423 if (element !== anchor) { | |
| 424 this._contentElement.insertBefore(element, anchor); | |
| 425 wasShown.push(viewportElement); | |
| 426 } else { | |
| 427 anchor = anchor.nextSibling; | |
| 428 } | |
| 429 this._renderedItems.push(viewportElement); | |
| 430 } | |
| 431 for (var i = 0; i < wasShown.length; ++i) | |
| 432 wasShown[i].wasShown(); | |
| 433 } | |
| 434 | |
| 435 /** | |
| 436 * @return {?string} | |
| 437 */ | |
| 438 _selectedText() { | |
| 439 this._updateSelectionModel(this.element.getComponentSelection()); | |
| 440 if (!this._headSelection || !this._anchorSelection) | |
| 441 return null; | |
| 442 | |
| 443 var startSelection = null; | |
| 444 var endSelection = null; | |
| 445 if (this._selectionIsBackward) { | |
| 446 startSelection = this._headSelection; | |
| 447 endSelection = this._anchorSelection; | |
| 448 } else { | |
| 449 startSelection = this._anchorSelection; | |
| 450 endSelection = this._headSelection; | |
| 451 } | |
| 452 | |
| 453 var textLines = []; | |
| 454 for (var i = startSelection.item; i <= endSelection.item; ++i) | |
| 455 textLines.push(this._providerElement(i).element().deepTextContent()); | |
| 456 | |
| 457 var endSelectionElement = this._providerElement(endSelection.item).element()
; | |
| 458 if (endSelection.node && endSelection.node.isSelfOrDescendant(endSelectionEl
ement)) { | |
| 459 var itemTextOffset = this._textOffsetInNode(endSelectionElement, endSelect
ion.node, endSelection.offset); | |
| 460 textLines[textLines.length - 1] = textLines.peekLast().substring(0, itemTe
xtOffset); | |
| 461 } | |
| 462 | |
| 463 var startSelectionElement = this._providerElement(startSelection.item).eleme
nt(); | |
| 464 if (startSelection.node && startSelection.node.isSelfOrDescendant(startSelec
tionElement)) { | |
| 465 var itemTextOffset = this._textOffsetInNode(startSelectionElement, startSe
lection.node, startSelection.offset); | |
| 466 textLines[0] = textLines[0].substring(itemTextOffset); | |
| 467 } | |
| 468 | |
| 469 return textLines.join('\n'); | |
| 470 } | |
| 471 | |
| 472 /** | |
| 473 * @param {!Element} itemElement | |
| 474 * @param {!Node} container | |
| 475 * @param {number} offset | |
| 476 * @return {number} | |
| 477 */ | |
| 478 _textOffsetInNode(itemElement, container, offset) { | |
| 479 var chars = 0; | |
| 480 var node = itemElement; | |
| 481 while ((node = node.traverseNextTextNode()) && !node.isSelfOrDescendant(cont
ainer)) | |
| 482 chars += node.textContent.length; | |
| 483 return chars + offset; | |
| 484 } | |
| 485 | |
| 486 /** | |
| 487 * @param {!Event} event | |
| 488 */ | |
| 489 _onScroll(event) { | |
| 490 this.refresh(); | |
| 491 } | 82 } |
| 492 | 83 |
| 493 /** | 84 /** |
| 494 * @return {number} | 85 * @return {number} |
| 495 */ | 86 */ |
| 496 firstVisibleIndex() { | 87 firstVisibleIndex() { |
| 497 var firstVisibleIndex = | 88 return Math.max(Array.prototype.lowerBound.call(this._cumulativeHeights, thi
s.element.scrollTop + 1), 0); |
| 498 Math.max(Array.prototype.lowerBound.call(this._cumulativeHeights, this.e
lement.scrollTop + 1), 0); | |
| 499 return Math.max(firstVisibleIndex, this._firstActiveIndex); | |
| 500 } | 89 } |
| 501 | 90 |
| 502 /** | 91 /** |
| 503 * @return {number} | 92 * @return {number} |
| 504 */ | 93 */ |
| 505 lastVisibleIndex() { | 94 lastVisibleIndex() { |
| 506 var lastVisibleIndex; | 95 return Math.min( |
| 507 if (this._stickToBottom) { | 96 Array.prototype.lowerBound.call(this._cumulativeHeights, this.element.sc
rollTop + this._visibleHeight()), |
| 508 lastVisibleIndex = this._itemCount - 1; | 97 this._itemCount); |
| 509 } else { | |
| 510 lastVisibleIndex = | |
| 511 this.firstVisibleIndex() + Math.ceil(this._visibleHeight() / this._pro
vider.minimumRowHeight()) - 1; | |
| 512 } | |
| 513 return Math.min(lastVisibleIndex, this._lastActiveIndex); | |
| 514 } | 98 } |
| 515 | 99 |
| 516 /** | 100 /** |
| 517 * @return {?Element} | |
| 518 */ | |
| 519 renderedElementAt(index) { | |
| 520 if (index < this._firstActiveIndex) | |
| 521 return null; | |
| 522 if (index > this._lastActiveIndex) | |
| 523 return null; | |
| 524 return this._renderedItems[index - this._firstActiveIndex].element(); | |
| 525 } | |
| 526 | |
| 527 /** | |
| 528 * @param {number} index | 101 * @param {number} index |
| 529 * @param {boolean=} makeLast | 102 * @param {boolean=} makeLast |
| 530 */ | 103 */ |
| 531 scrollItemIntoView(index, makeLast) { | 104 scrollItemIntoView(index, makeLast) { |
| 532 var firstVisibleIndex = this.firstVisibleIndex(); | 105 var firstVisibleIndex = this.firstVisibleIndex(); |
| 533 var lastVisibleIndex = this.lastVisibleIndex(); | 106 var lastVisibleIndex = this.lastVisibleIndex(); |
| 534 if (index > firstVisibleIndex && index < lastVisibleIndex) | 107 if (index > firstVisibleIndex && index < lastVisibleIndex) |
| 535 return; | 108 return; |
| 536 if (makeLast) | 109 if (makeLast) |
| 537 this.forceScrollItemToBeLast(index); | 110 this.forceScrollItemToBeLast(index); |
| 538 else if (index <= firstVisibleIndex) | 111 else if (index <= firstVisibleIndex) |
| 539 this.forceScrollItemToBeFirst(index); | 112 this.forceScrollItemToBeFirst(index); |
| 540 else if (index >= lastVisibleIndex) | 113 else if (index >= lastVisibleIndex) |
| 541 this.forceScrollItemToBeLast(index); | 114 this.forceScrollItemToBeLast(index); |
| 542 } | 115 } |
| 543 | 116 |
| 544 /** | 117 /** |
| 545 * @param {number} index | 118 * @param {number} index |
| 546 */ | 119 */ |
| 547 forceScrollItemToBeFirst(index) { | 120 forceScrollItemToBeFirst(index) { |
| 548 this.setStickToBottom(false); | |
| 549 this._rebuildCumulativeHeightsIfNeeded(); | |
| 550 this.element.scrollTop = index > 0 ? this._cumulativeHeights[index - 1] : 0; | 121 this.element.scrollTop = index > 0 ? this._cumulativeHeights[index - 1] : 0; |
| 551 if (this.element.isScrolledToBottom()) | 122 this._update(); |
| 552 this.setStickToBottom(true); | |
| 553 this.refresh(); | |
| 554 } | 123 } |
| 555 | 124 |
| 556 /** | 125 /** |
| 557 * @param {number} index | 126 * @param {number} index |
| 558 */ | 127 */ |
| 559 forceScrollItemToBeLast(index) { | 128 forceScrollItemToBeLast(index) { |
| 560 this.setStickToBottom(false); | |
| 561 this._rebuildCumulativeHeightsIfNeeded(); | |
| 562 this.element.scrollTop = this._cumulativeHeights[index] - this._visibleHeigh
t(); | 129 this.element.scrollTop = this._cumulativeHeights[index] - this._visibleHeigh
t(); |
| 563 if (this.element.isScrolledToBottom()) | 130 this._update(); |
| 564 this.setStickToBottom(true); | |
| 565 this.refresh(); | |
| 566 } | 131 } |
| 567 | 132 |
| 568 /** | 133 /** |
| 569 * @return {number} | 134 * @return {number} |
| 570 */ | 135 */ |
| 571 _visibleHeight() { | 136 _visibleHeight() { |
| 572 // Use offsetHeight instead of clientHeight to avoid being affected by horiz
ontal scroll. | |
| 573 return this.element.offsetHeight; | 137 return this.element.offsetHeight; |
| 574 } | 138 } |
| 575 }; | 139 }; |
| 576 | 140 |
| 577 /** | 141 /** |
| 578 * @interface | 142 * @interface |
| 579 */ | 143 */ |
| 580 UI.ViewportControl.Provider = function() {}; | 144 UI.ViewportControl.Provider = function() {}; |
| 581 | 145 |
| 582 UI.ViewportControl.Provider.prototype = { | 146 UI.ViewportControl.Provider.prototype = { |
| 583 /** | 147 /** |
| 584 * @param {number} index | 148 * @param {number} index |
| 585 * @return {number} | 149 * @return {number} |
| 586 */ | 150 */ |
| 587 fastHeight(index) { | 151 fastItemHeight(index) { |
| 588 return 0; | 152 return 0; |
| 589 }, | 153 }, |
| 590 | 154 |
| 591 /** | 155 /** |
| 592 * @return {number} | 156 * @return {number} |
| 593 */ | 157 */ |
| 594 itemCount() { | 158 itemCount() { |
| 595 return 0; | 159 return 0; |
| 596 }, | 160 }, |
| 597 | 161 |
| 598 /** | 162 /** |
| 599 * @return {number} | |
| 600 */ | |
| 601 minimumRowHeight() { | |
| 602 return 0; | |
| 603 }, | |
| 604 | |
| 605 /** | |
| 606 * @param {number} index | 163 * @param {number} index |
| 607 * @return {?UI.ViewportElement} | 164 * @return {?Element} |
| 608 */ | 165 */ |
| 609 itemElement(index) { | 166 itemElement(index) { |
| 610 return null; | 167 return null; |
| 611 } | 168 } |
| 612 }; | 169 }; |
| 613 | |
| 614 /** | |
| 615 * @interface | |
| 616 */ | |
| 617 UI.ViewportElement = function() {}; | |
| 618 UI.ViewportElement.prototype = { | |
| 619 willHide() {}, | |
| 620 | |
| 621 wasShown() {}, | |
| 622 | |
| 623 /** | |
| 624 * @return {!Element} | |
| 625 */ | |
| 626 element() {}, | |
| 627 }; | |
| 628 | |
| 629 /** | |
| 630 * @implements {UI.ViewportElement} | |
| 631 * @unrestricted | |
| 632 */ | |
| 633 UI.StaticViewportElement = class { | |
| 634 /** | |
| 635 * @param {!Element} element | |
| 636 */ | |
| 637 constructor(element) { | |
| 638 this._element = element; | |
| 639 } | |
| 640 | |
| 641 /** | |
| 642 * @override | |
| 643 */ | |
| 644 willHide() { | |
| 645 } | |
| 646 | |
| 647 /** | |
| 648 * @override | |
| 649 */ | |
| 650 wasShown() { | |
| 651 } | |
| 652 | |
| 653 /** | |
| 654 * @override | |
| 655 * @return {!Element} | |
| 656 */ | |
| 657 element() { | |
| 658 return this._element; | |
| 659 } | |
| 660 }; | |
| OLD | NEW |