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