OLD | NEW |
1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * @template T | 6 * @template T |
7 * @interface | 7 * @interface |
8 */ | 8 */ |
9 UI.ListDelegate = function() {}; | 9 UI.ListDelegate = function() {}; |
10 | 10 |
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
52 */ | 52 */ |
53 constructor(delegate) { | 53 constructor(delegate) { |
54 this.element = createElement('div'); | 54 this.element = createElement('div'); |
55 this.element.style.overflow = 'auto'; | 55 this.element.style.overflow = 'auto'; |
56 this._topElement = this.element.createChild('div'); | 56 this._topElement = this.element.createChild('div'); |
57 this._bottomElement = this.element.createChild('div'); | 57 this._bottomElement = this.element.createChild('div'); |
58 this._firstIndex = 0; | 58 this._firstIndex = 0; |
59 this._lastIndex = 0; | 59 this._lastIndex = 0; |
60 this._renderedHeight = 0; | 60 this._renderedHeight = 0; |
61 this._topHeight = 0; | 61 this._topHeight = 0; |
62 this._topElement.style.height = '0'; | |
63 this._bottomHeight = 0; | 62 this._bottomHeight = 0; |
64 this._bottomElement.style.height = '0'; | 63 this._clearViewport(); |
65 | 64 |
66 /** @type {!Array<T>} */ | 65 /** @type {!Array<T>} */ |
67 this._items = []; | 66 this._items = []; |
68 /** @type {!Map<T, !Element>} */ | 67 /** @type {!Map<T, !Element>} */ |
69 this._itemToElement = new Map(); | 68 this._itemToElement = new Map(); |
70 this._selectedIndex = -1; | 69 this._selectedIndex = -1; |
71 | 70 |
72 this._boundKeyDown = event => { | 71 this._boundKeyDown = event => { |
73 if (this.onKeyDown(event)) | 72 if (this.onKeyDown(event)) |
74 event.consume(true); | 73 event.consume(true); |
75 }; | 74 }; |
76 this._boundClick = event => { | 75 this._boundClick = event => { |
77 if (this.onClick(event)) | 76 if (this.onClick(event)) |
78 event.consume(true); | 77 event.consume(true); |
79 }; | 78 }; |
80 | 79 |
81 this._delegate = delegate; | 80 this._delegate = delegate; |
82 this._heightMode = UI.ListHeightMode.Measured; | 81 this._heightMode = UI.ListHeightMode.Measured; |
83 this._fixedHeight = 0; | 82 this._fixedHeight = 0; |
| 83 this._variableOffsets = new Int32Array(0); |
84 | 84 |
85 this.element.addEventListener('scroll', this._onScroll.bind(this), false); | 85 this.element.addEventListener('scroll', this._onScroll.bind(this), false); |
86 } | 86 } |
87 | 87 |
88 /** | 88 /** |
89 * @param {!UI.ListHeightMode} mode | 89 * @param {!UI.ListHeightMode} mode |
90 */ | 90 */ |
91 setHeightMode(mode) { | 91 setHeightMode(mode) { |
92 if (mode === UI.ListHeightMode.Variable) | |
93 throw 'Variable height is not supported (yet)'; | |
94 this._heightMode = mode; | 92 this._heightMode = mode; |
95 this._fixedHeight = 0; | 93 this._fixedHeight = 0; |
96 if (this._items.length) { | 94 if (this._items.length) { |
97 this._itemToElement.clear(); | 95 this._itemToElement.clear(); |
98 this._refresh(); | 96 this._invalidate(0, this._items.length, this._items.length); |
99 } | 97 } |
100 } | 98 } |
101 | 99 |
102 /** | 100 /** |
103 * @param {boolean} handleInput | 101 * @param {boolean} handleInput |
104 */ | 102 */ |
105 setHandleInput(handleInput) { | 103 setHandleInput(handleInput) { |
106 if (handleInput) { | 104 if (handleInput) { |
107 this.element.addEventListener('keydown', this._boundKeyDown, false); | 105 this.element.addEventListener('keydown', this._boundKeyDown, false); |
108 this.element.addEventListener('click', this._boundClick, false); | 106 this.element.addEventListener('click', this._boundClick, false); |
(...skipping 90 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
199 | 197 |
200 /** | 198 /** |
201 * @param {number} from | 199 * @param {number} from |
202 * @param {number} to | 200 * @param {number} to |
203 */ | 201 */ |
204 invalidateRange(from, to) { | 202 invalidateRange(from, to) { |
205 this._invalidate(from, to, to - from); | 203 this._invalidate(from, to, to - from); |
206 } | 204 } |
207 | 205 |
208 viewportResized() { | 206 viewportResized() { |
209 this._refresh(); | 207 // TODO(dgozman): try to keep the visible scrollTop the same |
| 208 // when invalidating after firstIndex but before first visible element. |
| 209 var scrollTop = this.element.scrollTop; |
| 210 var viewportHeight = this.element.offsetHeight; |
| 211 this._clearViewport(); |
| 212 this._updateViewport(Number.constrain(scrollTop, 0, this._totalHeight() - vi
ewportHeight), viewportHeight); |
210 } | 213 } |
211 | 214 |
212 /** | 215 /** |
213 * @param {number} index | 216 * @param {number} index |
214 */ | 217 */ |
215 scrollItemAtIndexIntoView(index) { | 218 scrollItemAtIndexIntoView(index) { |
216 var top = this._offsetAtIndex(index); | 219 var top = this._offsetAtIndex(index); |
217 var bottom = this._offsetAtIndex(index + 1); | 220 var bottom = this._offsetAtIndex(index + 1); |
218 var scrollTop = this.element.scrollTop; | 221 var scrollTop = this.element.scrollTop; |
219 var height = this.element.offsetHeight; | 222 var viewportHeight = this.element.offsetHeight; |
220 if (top < scrollTop) | 223 if (top < scrollTop) |
221 this._update(top, height); | 224 this._updateViewport(top, viewportHeight); |
222 else if (bottom > scrollTop + height) | 225 else if (bottom > scrollTop + viewportHeight) |
223 this._update(bottom - height, height); | 226 this._updateViewport(bottom - viewportHeight, viewportHeight); |
224 } | 227 } |
225 | 228 |
226 /** | 229 /** |
227 * @param {number} index | 230 * @param {number} index |
228 * @param {boolean=} scrollIntoView | 231 * @param {boolean=} scrollIntoView |
229 */ | 232 */ |
230 selectItemAtIndex(index, scrollIntoView) { | 233 selectItemAtIndex(index, scrollIntoView) { |
231 if (index !== -1 && !this._delegate.isItemSelectable(this._items[index])) | 234 if (index !== -1 && !this._delegate.isItemSelectable(this._items[index])) |
232 throw 'Attempt to select non-selectable item'; | 235 throw 'Attempt to select non-selectable item'; |
233 this._select(index); | 236 this._select(index); |
(...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
311 return this._offsetAtIndex(this._items.length); | 314 return this._offsetAtIndex(this._items.length); |
312 } | 315 } |
313 | 316 |
314 /** | 317 /** |
315 * @param {number} offset | 318 * @param {number} offset |
316 * @return {number} | 319 * @return {number} |
317 */ | 320 */ |
318 _indexAtOffset(offset) { | 321 _indexAtOffset(offset) { |
319 if (!this._items.length || offset < 0) | 322 if (!this._items.length || offset < 0) |
320 return 0; | 323 return 0; |
321 if (this._heightMode === UI.ListHeightMode.Variable) | 324 if (this._heightMode === UI.ListHeightMode.Variable) { |
322 throw 'Variable height is not supported (yet)'; | 325 return Math.min( |
| 326 this._items.length - 1, this._variableOffsets.lowerBound(offset, undef
ined, 0, this._items.length)); |
| 327 } |
323 if (!this._fixedHeight) | 328 if (!this._fixedHeight) |
324 this._measureHeight(); | 329 this._measureHeight(); |
325 return Math.min(this._items.length - 1, Math.floor(offset / this._fixedHeigh
t)); | 330 return Math.min(this._items.length - 1, Math.floor(offset / this._fixedHeigh
t)); |
326 } | 331 } |
327 | 332 |
328 /** | 333 /** |
329 * @param {number} index | 334 * @param {number} index |
330 * @return {!Element} | 335 * @return {!Element} |
331 */ | 336 */ |
332 _elementAtIndex(index) { | 337 _elementAtIndex(index) { |
333 var item = this._items[index]; | 338 var item = this._items[index]; |
334 var element = this._itemToElement.get(item); | 339 var element = this._itemToElement.get(item); |
335 if (!element) { | 340 if (!element) { |
336 element = this._delegate.createElementForItem(item); | 341 element = this._delegate.createElementForItem(item); |
337 this._itemToElement.set(item, element); | 342 this._itemToElement.set(item, element); |
338 } | 343 } |
339 return element; | 344 return element; |
340 } | 345 } |
341 | 346 |
342 /** | 347 /** |
343 * @param {number} index | 348 * @param {number} index |
344 * @return {number} | 349 * @return {number} |
345 */ | 350 */ |
346 _offsetAtIndex(index) { | 351 _offsetAtIndex(index) { |
347 if (!this._items.length) | 352 if (!this._items.length) |
348 return 0; | 353 return 0; |
349 if (this._heightMode === UI.ListHeightMode.Variable) | 354 if (this._heightMode === UI.ListHeightMode.Variable) |
350 throw 'Variable height is not supported (yet)'; | 355 return this._variableOffsets[index]; |
351 if (!this._fixedHeight) | 356 if (!this._fixedHeight) |
352 this._measureHeight(); | 357 this._measureHeight(); |
353 return index * this._fixedHeight; | 358 return index * this._fixedHeight; |
354 } | 359 } |
355 | 360 |
356 _measureHeight() { | 361 _measureHeight() { |
357 if (this._heightMode === UI.ListHeightMode.Measured) | 362 if (this._heightMode === UI.ListHeightMode.Measured) |
358 this._fixedHeight = UI.measurePreferredSize(this._elementAtIndex(0), this.
element).height; | 363 this._fixedHeight = UI.measurePreferredSize(this._elementAtIndex(0), this.
element).height; |
359 else | 364 else |
360 this._fixedHeight = this._delegate.heightForItem(this._items[0]); | 365 this._fixedHeight = this._delegate.heightForItem(this._items[0]); |
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
410 if (Math.abs(this._offsetAtIndex(index) - startOffset) >= minSkippedHeig
ht) | 415 if (Math.abs(this._offsetAtIndex(index) - startOffset) >= minSkippedHeig
ht) |
411 return index; | 416 return index; |
412 lastSelectable = index; | 417 lastSelectable = index; |
413 } | 418 } |
414 | 419 |
415 index += direction; | 420 index += direction; |
416 } | 421 } |
417 } | 422 } |
418 | 423 |
419 /** | 424 /** |
| 425 * @param {number} length |
| 426 * @param {number} copyTo |
| 427 */ |
| 428 _reallocateVariableOffsets(length, copyTo) { |
| 429 if (this._variableOffsets.length < length) { |
| 430 var variableOffsets = new Int32Array(Math.max(length, this._variableOffset
s.length * 2)); |
| 431 variableOffsets.set(this._variableOffsets.slice(0, copyTo), 0); |
| 432 this._variableOffsets = variableOffsets; |
| 433 } else if (this._variableOffsets.length >= 2 * length) { |
| 434 var variableOffsets = new Int32Array(length); |
| 435 variableOffsets.set(this._variableOffsets.slice(0, copyTo), 0); |
| 436 this._variableOffsets = variableOffsets; |
| 437 } |
| 438 } |
| 439 |
| 440 /** |
420 * @param {number} from | 441 * @param {number} from |
421 * @param {number} to | 442 * @param {number} to |
422 * @param {number} inserted | 443 * @param {number} inserted |
423 */ | 444 */ |
424 _invalidate(from, to, inserted) { | 445 _invalidate(from, to, inserted) { |
| 446 if (this._heightMode === UI.ListHeightMode.Variable) { |
| 447 this._reallocateVariableOffsets(this._items.length + 1, from + 1); |
| 448 for (var i = from + 1; i <= this._items.length; i++) |
| 449 this._variableOffsets[i] = this._variableOffsets[i - 1] + this._delegate
.heightForItem(this._items[i - 1]); |
| 450 } |
| 451 |
425 var viewportHeight = this.element.offsetHeight; | 452 var viewportHeight = this.element.offsetHeight; |
426 var totalHeight = this._totalHeight(); | 453 var totalHeight = this._totalHeight(); |
| 454 var scrollTop = this.element.scrollTop; |
| 455 |
427 if (this._renderedHeight < viewportHeight || totalHeight < viewportHeight) { | 456 if (this._renderedHeight < viewportHeight || totalHeight < viewportHeight) { |
428 this._refresh(); | 457 this._clearViewport(); |
| 458 this._updateViewport(Number.constrain(scrollTop, 0, totalHeight - viewport
Height), viewportHeight); |
429 return; | 459 return; |
430 } | 460 } |
431 | 461 |
432 var scrollTop = this.element.scrollTop; | |
433 var heightDelta = totalHeight - this._renderedHeight; | 462 var heightDelta = totalHeight - this._renderedHeight; |
434 if (to <= this._firstIndex) { | 463 if (to <= this._firstIndex) { |
435 var topHeight = this._topHeight + heightDelta; | 464 var topHeight = this._topHeight + heightDelta; |
436 this._topElement.style.height = topHeight + 'px'; | 465 this._topElement.style.height = topHeight + 'px'; |
437 this.element.scrollTop = scrollTop + heightDelta; | 466 this.element.scrollTop = scrollTop + heightDelta; |
438 this._topHeight = topHeight; | 467 this._topHeight = topHeight; |
439 this._renderedHeight = totalHeight; | 468 this._renderedHeight = totalHeight; |
440 var indexDelta = inserted - (to - from); | 469 var indexDelta = inserted - (to - from); |
441 this._firstIndex += indexDelta; | 470 this._firstIndex += indexDelta; |
442 this._lastIndex += indexDelta; | 471 this._lastIndex += indexDelta; |
443 return; | 472 return; |
444 } | 473 } |
445 | 474 |
446 if (from >= this._lastIndex) { | 475 if (from >= this._lastIndex) { |
447 var bottomHeight = this._bottomHeight + heightDelta; | 476 var bottomHeight = this._bottomHeight + heightDelta; |
448 this._bottomElement.style.height = bottomHeight + 'px'; | 477 this._bottomElement.style.height = bottomHeight + 'px'; |
449 this._bottomHeight = bottomHeight; | 478 this._bottomHeight = bottomHeight; |
450 this._renderedHeight = totalHeight; | 479 this._renderedHeight = totalHeight; |
451 return; | 480 return; |
452 } | 481 } |
453 | 482 |
454 // TODO(dgozman): try to keep the visible scrollTop the same | 483 // TODO(dgozman): try to keep the visible scrollTop the same |
455 // when invalidating after firstIndex but before first visible element. | 484 // when invalidating after firstIndex but before first visible element. |
456 this._refresh(); | 485 this._clearViewport(); |
| 486 this._updateViewport(Number.constrain(scrollTop, 0, totalHeight - viewportHe
ight), viewportHeight); |
457 } | 487 } |
458 | 488 |
459 _refresh() { | 489 _clearViewport() { |
460 var viewportHeight = this.element.offsetHeight; | |
461 var scrollTop = Number.constrain(this.element.scrollTop, 0, this._totalHeigh
t() - viewportHeight); | |
462 this._firstIndex = 0; | 490 this._firstIndex = 0; |
463 this._lastIndex = 0; | 491 this._lastIndex = 0; |
464 this._renderedHeight = 0; | 492 this._renderedHeight = 0; |
465 this._topHeight = 0; | 493 this._topHeight = 0; |
466 this._bottomHeight = 0; | 494 this._bottomHeight = 0; |
| 495 this._topElement.style.height = '0'; |
| 496 this._bottomElement.style.height = '0'; |
467 this.element.removeChildren(); | 497 this.element.removeChildren(); |
468 this.element.appendChild(this._topElement); | 498 this.element.appendChild(this._topElement); |
469 this.element.appendChild(this._bottomElement); | 499 this.element.appendChild(this._bottomElement); |
470 this._update(scrollTop, viewportHeight); | |
471 } | 500 } |
472 | 501 |
473 _onScroll() { | 502 _onScroll() { |
474 this._update(this.element.scrollTop, this.element.offsetHeight); | 503 this._updateViewport(this.element.scrollTop, this.element.offsetHeight); |
475 } | 504 } |
476 | 505 |
477 /** | 506 /** |
478 * @param {number} scrollTop | 507 * @param {number} scrollTop |
479 * @param {number} viewportHeight | 508 * @param {number} viewportHeight |
480 */ | 509 */ |
481 _update(scrollTop, viewportHeight) { | 510 _updateViewport(scrollTop, viewportHeight) { |
482 // Note: this method should not force layout. Be careful. | 511 // Note: this method should not force layout. Be careful. |
483 | 512 |
484 var totalHeight = this._totalHeight(); | 513 var totalHeight = this._totalHeight(); |
485 if (!totalHeight) { | 514 if (!totalHeight) { |
486 this._firstIndex = 0; | 515 this._firstIndex = 0; |
487 this._lastIndex = 0; | 516 this._lastIndex = 0; |
488 this._topHeight = 0; | 517 this._topHeight = 0; |
489 this._bottomHeight = 0; | 518 this._bottomHeight = 0; |
490 this._renderedHeight = 0; | 519 this._renderedHeight = 0; |
491 this._topElement.style.height = '0'; | 520 this._topElement.style.height = '0'; |
(...skipping 27 matching lines...) Expand all Loading... |
519 this._firstIndex = firstIndex; | 548 this._firstIndex = firstIndex; |
520 this._lastIndex = lastIndex; | 549 this._lastIndex = lastIndex; |
521 this._topHeight = this._offsetAtIndex(firstIndex); | 550 this._topHeight = this._offsetAtIndex(firstIndex); |
522 this._topElement.style.height = this._topHeight + 'px'; | 551 this._topElement.style.height = this._topHeight + 'px'; |
523 this._bottomHeight = totalHeight - this._offsetAtIndex(lastIndex); | 552 this._bottomHeight = totalHeight - this._offsetAtIndex(lastIndex); |
524 this._bottomElement.style.height = this._bottomHeight + 'px'; | 553 this._bottomElement.style.height = this._bottomHeight + 'px'; |
525 this._renderedHeight = totalHeight; | 554 this._renderedHeight = totalHeight; |
526 this.element.scrollTop = scrollTop; | 555 this.element.scrollTop = scrollTop; |
527 } | 556 } |
528 }; | 557 }; |
OLD | NEW |