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 |