| 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 |