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 19 matching lines...) Expand all Loading... | |
30 /** | 30 /** |
31 * @param {?T} from | 31 * @param {?T} from |
32 * @param {?T} to | 32 * @param {?T} to |
33 * @param {?Element} fromElement | 33 * @param {?Element} fromElement |
34 * @param {?Element} toElement | 34 * @param {?Element} toElement |
35 */ | 35 */ |
36 selectedItemChanged(from, to, fromElement, toElement) {}, | 36 selectedItemChanged(from, to, fromElement, toElement) {}, |
37 }; | 37 }; |
38 | 38 |
39 /** @enum {symbol} */ | 39 /** @enum {symbol} */ |
40 UI.ListHeightMode = { | 40 UI.ListMode = { |
41 Fixed: Symbol('UI.ListHeightMode.Fixed'), | 41 Grow: Symbol('UI.ListMode.Grow'), |
42 Measured: Symbol('UI.ListHeightMode.Measured'), | 42 ViewportFixedItems: Symbol('UI.ListMode.ViewportFixedItems'), |
43 Variable: Symbol('UI.ListHeightMode.Variable') | 43 ViewportFixedItemsMeasured: Symbol('UI.ListMode.ViewportFixedItemsMeasured'), |
44 ViewportVariableItems: Symbol('UI.ListMode.ViewportVariableItems') | |
44 }; | 45 }; |
45 | 46 |
46 /** | 47 /** |
47 * @template T | 48 * @template T |
48 */ | 49 */ |
49 UI.ListControl = class { | 50 UI.ListControl = class { |
50 /** | 51 /** |
51 * @param {!UI.ListDelegate<T>} delegate | 52 * @param {!UI.ListDelegate<T>} delegate |
52 */ | 53 */ |
53 constructor(delegate) { | 54 constructor(delegate) { |
54 this.element = createElement('div'); | 55 this.element = createElement('div'); |
55 this.element.style.overflow = 'auto'; | 56 this.element.style.overflow = 'auto'; |
56 this._topElement = this.element.createChild('div'); | 57 this._topElement = this.element.createChild('div'); |
57 this._bottomElement = this.element.createChild('div'); | 58 this._bottomElement = this.element.createChild('div'); |
58 this._firstIndex = 0; | 59 this._firstIndex = 0; |
59 this._lastIndex = 0; | 60 this._lastIndex = 0; |
60 this._renderedHeight = 0; | 61 this._renderedHeight = 0; |
61 this._topHeight = 0; | 62 this._topHeight = 0; |
62 this._bottomHeight = 0; | 63 this._bottomHeight = 0; |
63 this._clearViewport(); | |
64 | 64 |
65 /** @type {!Array<T>} */ | 65 /** @type {!Array<T>} */ |
66 this._items = []; | 66 this._items = []; |
67 /** @type {!Map<T, !Element>} */ | 67 /** @type {!Map<T, !Element>} */ |
68 this._itemToElement = new Map(); | 68 this._itemToElement = new Map(); |
69 this._selectedIndex = -1; | 69 this._selectedIndex = -1; |
70 | 70 |
71 this._boundKeyDown = event => { | 71 this._boundKeyDown = event => { |
72 if (this.onKeyDown(event)) | 72 if (this.onKeyDown(event)) |
73 event.consume(true); | 73 event.consume(true); |
74 }; | 74 }; |
75 this._boundClick = event => { | 75 this._boundClick = event => { |
76 if (this.onClick(event)) | 76 if (this.onClick(event)) |
77 event.consume(true); | 77 event.consume(true); |
78 }; | 78 }; |
79 this._boundScroll = event => { | |
80 this._updateViewport(this.element.scrollTop, this.element.offsetHeight); | |
81 }; | |
79 | 82 |
80 this._delegate = delegate; | 83 this._delegate = delegate; |
81 this._heightMode = UI.ListHeightMode.Measured; | 84 this._mode = UI.ListMode.ViewportFixedItemsMeasured; |
82 this._fixedHeight = 0; | 85 this._fixedHeight = 0; |
83 this._variableOffsets = new Int32Array(0); | 86 this._variableOffsets = new Int32Array(0); |
84 | 87 this._clearViewport(); |
85 this.element.addEventListener('scroll', this._onScroll.bind(this), false); | 88 this.element.addEventListener('scroll', this._boundScroll, false); |
86 } | 89 } |
87 | 90 |
88 /** | 91 /** |
89 * @param {!UI.ListHeightMode} mode | 92 * @param {!UI.ListMode} mode |
90 */ | 93 */ |
91 setHeightMode(mode) { | 94 setMode(mode) { |
caseq
2016/12/29 19:25:31
Should we even allow changing mode on the fly? Per
dgozman
2016/12/29 20:38:44
Done.
| |
92 this._heightMode = mode; | 95 if (mode === UI.ListMode.Grow) { |
93 this._fixedHeight = 0; | 96 if (this._mode !== UI.ListMode.Grow) |
94 if (this._items.length) { | 97 this.element.removeEventListener('scroll', this._boundScroll, false); |
caseq
2016/12/29 19:25:31
do it unconditionally just in case?
| |
95 this._itemToElement.clear(); | 98 this._mode = mode; |
caseq
2016/12/29 19:25:30
this would go before if then
| |
96 this._invalidate(0, this._items.length, this._items.length); | 99 if (this._items.length) { |
100 this._itemToElement.clear(); | |
101 this._clearContents(); | |
102 this._invalidateGrowMode(0, 0, this._items.length); | |
103 } | |
104 } else { | |
105 if (this._mode === UI.ListMode.Grow) | |
106 this.element.addEventListener('scroll', this._boundScroll, false); | |
107 this._mode = mode; | |
108 this._fixedHeight = 0; | |
109 if (this._items.length) { | |
110 this._itemToElement.clear(); | |
111 if (this._mode === UI.ListMode.Grow) | |
caseq
2016/12/29 19:25:30
this can't be true :-)
| |
112 this._clearViewport(); | |
113 this._invalidate(0, this._items.length, this._items.length); | |
114 } | |
97 } | 115 } |
98 } | 116 } |
99 | 117 |
100 /** | 118 /** |
101 * @param {boolean} handleInput | 119 * @param {boolean} handleInput |
102 */ | 120 */ |
103 setHandleInput(handleInput) { | 121 setHandleInput(handleInput) { |
104 if (handleInput) { | 122 if (handleInput) { |
105 this.element.addEventListener('keydown', this._boundKeyDown, false); | 123 this.element.addEventListener('keydown', this._boundKeyDown, false); |
106 this.element.addEventListener('click', this._boundClick, false); | 124 this.element.addEventListener('click', this._boundClick, false); |
(...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
174 // Splice may fail with too many arguments. | 192 // Splice may fail with too many arguments. |
175 var before = this._items.slice(0, from); | 193 var before = this._items.slice(0, from); |
176 var after = this._items.slice(to); | 194 var after = this._items.slice(to); |
177 this._items = [].concat(before, items, after); | 195 this._items = [].concat(before, items, after); |
178 } | 196 } |
179 this._invalidate(from, to, items.length); | 197 this._invalidate(from, to, items.length); |
180 | 198 |
181 if (this._selectedIndex >= to) { | 199 if (this._selectedIndex >= to) { |
182 this._selectedIndex += items.length - (to - from); | 200 this._selectedIndex += items.length - (to - from); |
183 } else if (this._selectedIndex >= from) { | 201 } else if (this._selectedIndex >= from) { |
184 var index = this._findClosestSelectable(from + items.length, +1, 0, false) ; | 202 var index = this._findFirstSelectable(from + items.length, +1, false); |
185 if (index === -1) | 203 if (index === -1) |
186 index = this._findClosestSelectable(from - 1, -1, 0, false); | 204 index = this._findFirstSelectable(from - 1, -1, false); |
187 this._select(index, oldSelectedItem, oldSelectedElement); | 205 this._select(index, oldSelectedItem, oldSelectedElement); |
188 } | 206 } |
189 } | 207 } |
190 | 208 |
191 /** | 209 /** |
192 * @param {!Array<T>} items | 210 * @param {!Array<T>} items |
193 */ | 211 */ |
194 replaceAllItems(items) { | 212 replaceAllItems(items) { |
195 this.replaceItemsInRange(0, this._items.length, items); | 213 this.replaceItemsInRange(0, this._items.length, items); |
196 } | 214 } |
197 | 215 |
198 /** | 216 /** |
199 * @param {number} from | 217 * @param {number} from |
200 * @param {number} to | 218 * @param {number} to |
201 */ | 219 */ |
202 invalidateRange(from, to) { | 220 invalidateRange(from, to) { |
203 this._invalidate(from, to, to - from); | 221 this._invalidate(from, to, to - from); |
204 } | 222 } |
205 | 223 |
206 viewportResized() { | 224 viewportResized() { |
207 // TODO(dgozman): try to keep the visible scrollTop the same | 225 if (this._mode === UI.ListMode.Grow) |
208 // when invalidating after firstIndex but before first visible element. | 226 return; |
227 // TODO(dgozman): try to keep visible scrollTop the same. | |
209 var scrollTop = this.element.scrollTop; | 228 var scrollTop = this.element.scrollTop; |
210 var viewportHeight = this.element.offsetHeight; | 229 var viewportHeight = this.element.offsetHeight; |
211 this._clearViewport(); | 230 this._clearViewport(); |
212 this._updateViewport(Number.constrain(scrollTop, 0, this._totalHeight() - vi ewportHeight), viewportHeight); | 231 this._updateViewport(Number.constrain(scrollTop, 0, this._totalHeight() - vi ewportHeight), viewportHeight); |
213 } | 232 } |
214 | 233 |
215 /** | 234 /** |
216 * @param {number} index | 235 * @param {number} index |
217 */ | 236 */ |
218 scrollItemAtIndexIntoView(index) { | 237 scrollItemAtIndexIntoView(index) { |
238 if (this._mode === UI.ListMode.Grow) { | |
239 this._elementAtIndex(index).scrollIntoViewIfNeeded(false); | |
240 return; | |
241 } | |
219 var top = this._offsetAtIndex(index); | 242 var top = this._offsetAtIndex(index); |
220 var bottom = this._offsetAtIndex(index + 1); | 243 var bottom = this._offsetAtIndex(index + 1); |
221 var scrollTop = this.element.scrollTop; | 244 var scrollTop = this.element.scrollTop; |
222 var viewportHeight = this.element.offsetHeight; | 245 var viewportHeight = this.element.offsetHeight; |
223 if (top < scrollTop) | 246 if (top < scrollTop) |
224 this._updateViewport(top, viewportHeight); | 247 this._updateViewport(top, viewportHeight); |
225 else if (bottom > scrollTop + viewportHeight) | 248 else if (bottom > scrollTop + viewportHeight) |
226 this._updateViewport(bottom - viewportHeight, viewportHeight); | 249 this._updateViewport(bottom - viewportHeight, viewportHeight); |
227 } | 250 } |
228 | 251 |
(...skipping 24 matching lines...) Expand all Loading... | |
253 } | 276 } |
254 | 277 |
255 /** | 278 /** |
256 * @param {!Event} event | 279 * @param {!Event} event |
257 * @return {boolean} | 280 * @return {boolean} |
258 */ | 281 */ |
259 onKeyDown(event) { | 282 onKeyDown(event) { |
260 var index = -1; | 283 var index = -1; |
261 switch (event.key) { | 284 switch (event.key) { |
262 case 'ArrowUp': | 285 case 'ArrowUp': |
263 index = this._selectedIndex === -1 ? this._findClosestSelectable(this._i tems.length - 1, -1, 0, true) : | 286 index = this._selectedIndex === -1 ? this._items.length - 1 : this._sele ctedIndex - 1; |
264 this._findClosestSelectable(this._s electedIndex, -1, 1, true); | 287 index = this._findFirstSelectable(index, -1, true); |
265 break; | 288 break; |
266 case 'ArrowDown': | 289 case 'ArrowDown': |
267 index = this._selectedIndex === -1 ? this._findClosestSelectable(0, +1, 0, true) : | 290 index = this._selectedIndex === -1 ? 0 : this._selectedIndex + 1; |
268 this._findClosestSelectable(this._s electedIndex, +1, 1, true); | 291 index = this._findFirstSelectable(index, +1, true); |
269 break; | 292 break; |
270 case 'PageUp': | 293 case 'PageUp': |
294 if (this._mode === UI.ListMode.Grow) | |
295 return false; | |
271 index = this._selectedIndex === -1 ? this._items.length - 1 : this._sele ctedIndex; | 296 index = this._selectedIndex === -1 ? this._items.length - 1 : this._sele ctedIndex; |
272 // Compensate for zoom rounding errors with -1. | 297 index = this._findPageSelectable(index, -1); |
273 index = this._findClosestSelectable(index, -1, this.element.offsetHeight - 1, false); | |
274 break; | 298 break; |
275 case 'PageDown': | 299 case 'PageDown': |
300 if (this._mode === UI.ListMode.Grow) | |
301 return false; | |
276 index = this._selectedIndex === -1 ? 0 : this._selectedIndex; | 302 index = this._selectedIndex === -1 ? 0 : this._selectedIndex; |
277 // Compensate for zoom rounding errors with -1. | 303 index = this._findPageSelectable(index, +1); |
278 index = this._findClosestSelectable(index, +1, this.element.offsetHeight - 1, false); | |
279 break; | 304 break; |
280 default: | 305 default: |
281 return false; | 306 return false; |
282 } | 307 } |
283 if (index !== -1) { | 308 if (index !== -1) { |
284 this.scrollItemAtIndexIntoView(index); | 309 this.scrollItemAtIndexIntoView(index); |
285 this._select(index); | 310 this._select(index); |
286 return true; | 311 return true; |
287 } | 312 } |
288 return false; | 313 return false; |
289 } | 314 } |
290 | 315 |
291 /** | 316 /** |
292 * @param {!Event} event | 317 * @param {!Event} event |
293 * @return {boolean} | 318 * @return {boolean} |
294 */ | 319 */ |
295 onClick(event) { | 320 onClick(event) { |
296 var node = event.target; | 321 var node = event.target; |
297 while (node && node.parentNodeOrShadowHost() !== this.element) | 322 while (node && node.parentNodeOrShadowHost() !== this.element) |
298 node = node.parentNodeOrShadowHost(); | 323 node = node.parentNodeOrShadowHost(); |
299 if (!node || node.nodeType !== Node.ELEMENT_NODE) | 324 if (!node || node.nodeType !== Node.ELEMENT_NODE) |
300 return false; | 325 return false; |
301 var offset = /** @type {!Element} */ (node).getBoundingClientRect().top; | 326 var index = -1; |
302 offset -= this.element.getBoundingClientRect().top; | 327 if (this._mode === UI.ListMode.Grow) { |
303 var index = this._indexAtOffset(offset + this.element.scrollTop); | 328 for (var i = 0; i < this._items.length; i++) { |
caseq
2016/12/29 19:25:31
use findIndex()?
dgozman
2016/12/29 20:38:44
Done.
| |
329 if (this._itemToElement.get(this._items[i]) === node) { | |
330 index = i; | |
331 break; | |
332 } | |
333 } | |
334 } else { | |
335 var offset = /** @type {!Element} */ (node).getBoundingClientRect().top; | |
336 offset -= this.element.getBoundingClientRect().top; | |
337 index = this._indexAtOffset(offset + this.element.scrollTop); | |
338 } | |
304 if (index === -1 || !this._delegate.isItemSelectable(this._items[index])) | 339 if (index === -1 || !this._delegate.isItemSelectable(this._items[index])) |
305 return false; | 340 return false; |
306 this._select(index); | 341 this._select(index); |
307 return true; | 342 return true; |
308 } | 343 } |
309 | 344 |
310 /** | 345 /** |
311 * @return {number} | 346 * @return {number} |
312 */ | 347 */ |
313 _totalHeight() { | 348 _totalHeight() { |
314 return this._offsetAtIndex(this._items.length); | 349 return this._offsetAtIndex(this._items.length); |
315 } | 350 } |
316 | 351 |
317 /** | 352 /** |
318 * @param {number} offset | 353 * @param {number} offset |
319 * @return {number} | 354 * @return {number} |
320 */ | 355 */ |
321 _indexAtOffset(offset) { | 356 _indexAtOffset(offset) { |
357 if (this._mode === UI.ListMode.Grow) | |
358 throw 'There should be no offset conversions in grow mode'; | |
322 if (!this._items.length || offset < 0) | 359 if (!this._items.length || offset < 0) |
323 return 0; | 360 return 0; |
324 if (this._heightMode === UI.ListHeightMode.Variable) { | 361 if (this._mode === UI.ListMode.ViewportVariableItems) { |
325 return Math.min( | 362 return Math.min( |
326 this._items.length - 1, this._variableOffsets.lowerBound(offset, undef ined, 0, this._items.length)); | 363 this._items.length - 1, this._variableOffsets.lowerBound(offset, undef ined, 0, this._items.length)); |
327 } | 364 } |
328 if (!this._fixedHeight) | 365 if (!this._fixedHeight) |
329 this._measureHeight(); | 366 this._measureHeight(); |
330 return Math.min(this._items.length - 1, Math.floor(offset / this._fixedHeigh t)); | 367 return Math.min(this._items.length - 1, Math.floor(offset / this._fixedHeigh t)); |
331 } | 368 } |
332 | 369 |
333 /** | 370 /** |
334 * @param {number} index | 371 * @param {number} index |
335 * @return {!Element} | 372 * @return {!Element} |
336 */ | 373 */ |
337 _elementAtIndex(index) { | 374 _elementAtIndex(index) { |
338 var item = this._items[index]; | 375 var item = this._items[index]; |
339 var element = this._itemToElement.get(item); | 376 var element = this._itemToElement.get(item); |
340 if (!element) { | 377 if (!element) { |
341 element = this._delegate.createElementForItem(item); | 378 element = this._delegate.createElementForItem(item); |
342 this._itemToElement.set(item, element); | 379 this._itemToElement.set(item, element); |
343 } | 380 } |
344 return element; | 381 return element; |
345 } | 382 } |
346 | 383 |
347 /** | 384 /** |
348 * @param {number} index | 385 * @param {number} index |
349 * @return {number} | 386 * @return {number} |
350 */ | 387 */ |
351 _offsetAtIndex(index) { | 388 _offsetAtIndex(index) { |
389 if (this._mode === UI.ListMode.Grow) | |
390 throw 'There should be no offset conversions in grow mode'; | |
352 if (!this._items.length) | 391 if (!this._items.length) |
353 return 0; | 392 return 0; |
354 if (this._heightMode === UI.ListHeightMode.Variable) | 393 if (this._mode === UI.ListMode.ViewportVariableItems) |
355 return this._variableOffsets[index]; | 394 return this._variableOffsets[index]; |
356 if (!this._fixedHeight) | 395 if (!this._fixedHeight) |
357 this._measureHeight(); | 396 this._measureHeight(); |
358 return index * this._fixedHeight; | 397 return index * this._fixedHeight; |
359 } | 398 } |
360 | 399 |
361 _measureHeight() { | 400 _measureHeight() { |
362 if (this._heightMode === UI.ListHeightMode.Measured) | 401 if (this._mode === UI.ListMode.ViewportFixedItemsMeasured) |
363 this._fixedHeight = UI.measurePreferredSize(this._elementAtIndex(0), this. element).height; | 402 this._fixedHeight = UI.measurePreferredSize(this._elementAtIndex(0), this. element).height; |
364 else | 403 else |
365 this._fixedHeight = this._delegate.heightForItem(this._items[0]); | 404 this._fixedHeight = this._delegate.heightForItem(this._items[0]); |
366 } | 405 } |
367 | 406 |
368 /** | 407 /** |
369 * @param {number} index | 408 * @param {number} index |
370 * @param {?T=} oldItem | 409 * @param {?T=} oldItem |
371 * @param {?Element=} oldElement | 410 * @param {?Element=} oldElement |
372 */ | 411 */ |
373 _select(index, oldItem, oldElement) { | 412 _select(index, oldItem, oldElement) { |
374 if (oldItem === undefined) | 413 if (oldItem === undefined) |
375 oldItem = this._selectedIndex !== -1 ? this._items[this._selectedIndex] : null; | 414 oldItem = this._selectedIndex !== -1 ? this._items[this._selectedIndex] : null; |
376 if (oldElement === undefined) | 415 if (oldElement === undefined) |
377 oldElement = this._itemToElement.get(oldItem) || null; | 416 oldElement = this._itemToElement.get(oldItem) || null; |
378 this._selectedIndex = index; | 417 this._selectedIndex = index; |
379 var newItem = this._selectedIndex !== -1 ? this._items[this._selectedIndex] : null; | 418 var newItem = this._selectedIndex !== -1 ? this._items[this._selectedIndex] : null; |
380 var newElement = this._itemToElement.get(newItem) || null; | 419 var newElement = this._itemToElement.get(newItem) || null; |
381 this._delegate.selectedItemChanged(oldItem, newItem, /** @type {?Element} */ (oldElement), newElement); | 420 this._delegate.selectedItemChanged(oldItem, newItem, /** @type {?Element} */ (oldElement), newElement); |
382 } | 421 } |
383 | 422 |
384 /** | 423 /** |
385 * @param {number} index | 424 * @param {number} index |
386 * @param {number} direction | 425 * @param {number} direction |
387 * @param {number} minSkippedHeight | |
388 * @param {boolean} canWrap | 426 * @param {boolean} canWrap |
389 * @return {number} | 427 * @return {number} |
390 */ | 428 */ |
391 _findClosestSelectable(index, direction, minSkippedHeight, canWrap) { | 429 _findFirstSelectable(index, direction, canWrap) { |
392 var length = this._items.length; | 430 var length = this._items.length; |
393 if (!length) | 431 if (!length) |
394 return -1; | 432 return -1; |
395 | 433 for (var step = 0; step <= length; step++) { |
396 var lastSelectable = -1; | |
397 var start = -1; | |
398 var startOffset = this._offsetAtIndex(index); | |
399 while (true) { | |
400 if (index < 0 || index >= length) { | 434 if (index < 0 || index >= length) { |
401 if (!canWrap) | 435 if (!canWrap) |
402 return lastSelectable; | 436 return -1; |
403 index = (index + length) % length; | 437 index = (index + length) % length; |
404 } | 438 } |
439 if (this._delegate.isItemSelectable(this._items[index])) | |
440 return index; | |
441 index += direction; | |
442 } | |
443 return -1; | |
444 } | |
405 | 445 |
406 // Handle full wrap-around. | 446 /** |
407 if (index === start) | 447 * @param {number} index |
408 return lastSelectable; | 448 * @param {number} direction |
409 if (start === -1) { | 449 * @return {number} |
410 start = index; | 450 */ |
411 startOffset = this._offsetAtIndex(index); | 451 _findPageSelectable(index, direction) { |
412 } | 452 var lastSelectable = -1; |
413 | 453 var startOffset = this._offsetAtIndex(index); |
454 // Compensate for zoom rounding errors with -1. | |
455 var viewportHeight = this.element.offsetHeight - 1; | |
456 while (index >= 0 && index < this._items.length) { | |
414 if (this._delegate.isItemSelectable(this._items[index])) { | 457 if (this._delegate.isItemSelectable(this._items[index])) { |
415 if (Math.abs(this._offsetAtIndex(index) - startOffset) >= minSkippedHeig ht) | 458 if (Math.abs(this._offsetAtIndex(index) - startOffset) >= viewportHeight ) |
416 return index; | 459 return index; |
417 lastSelectable = index; | 460 lastSelectable = index; |
418 } | 461 } |
419 | |
420 index += direction; | 462 index += direction; |
421 } | 463 } |
464 return lastSelectable; | |
422 } | 465 } |
423 | 466 |
424 /** | 467 /** |
425 * @param {number} length | 468 * @param {number} length |
426 * @param {number} copyTo | 469 * @param {number} copyTo |
427 */ | 470 */ |
428 _reallocateVariableOffsets(length, copyTo) { | 471 _reallocateVariableOffsets(length, copyTo) { |
429 if (this._variableOffsets.length < length) { | 472 if (this._variableOffsets.length < length) { |
430 var variableOffsets = new Int32Array(Math.max(length, this._variableOffset s.length * 2)); | 473 var variableOffsets = new Int32Array(Math.max(length, this._variableOffset s.length * 2)); |
431 variableOffsets.set(this._variableOffsets.slice(0, copyTo), 0); | 474 variableOffsets.set(this._variableOffsets.slice(0, copyTo), 0); |
432 this._variableOffsets = variableOffsets; | 475 this._variableOffsets = variableOffsets; |
433 } else if (this._variableOffsets.length >= 2 * length) { | 476 } else if (this._variableOffsets.length >= 2 * length) { |
434 var variableOffsets = new Int32Array(length); | 477 var variableOffsets = new Int32Array(length); |
435 variableOffsets.set(this._variableOffsets.slice(0, copyTo), 0); | 478 variableOffsets.set(this._variableOffsets.slice(0, copyTo), 0); |
436 this._variableOffsets = variableOffsets; | 479 this._variableOffsets = variableOffsets; |
437 } | 480 } |
438 } | 481 } |
439 | 482 |
440 /** | 483 /** |
441 * @param {number} from | 484 * @param {number} from |
442 * @param {number} to | 485 * @param {number} to |
443 * @param {number} inserted | 486 * @param {number} inserted |
444 */ | 487 */ |
445 _invalidate(from, to, inserted) { | 488 _invalidate(from, to, inserted) { |
446 if (this._heightMode === UI.ListHeightMode.Variable) { | 489 if (this._mode === UI.ListMode.ViewportVariableItems) { |
447 this._reallocateVariableOffsets(this._items.length + 1, from + 1); | 490 this._reallocateVariableOffsets(this._items.length + 1, from + 1); |
448 for (var i = from + 1; i <= this._items.length; i++) | 491 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]); | 492 this._variableOffsets[i] = this._variableOffsets[i - 1] + this._delegate .heightForItem(this._items[i - 1]); |
450 } | 493 } |
451 | 494 |
495 if (this._mode === UI.ListMode.Grow) { | |
caseq
2016/12/29 19:25:31
nit: move up
dgozman
2016/12/29 20:38:44
Done.
| |
496 this._invalidateGrowMode(from, to - from, inserted); | |
497 return; | |
498 } | |
499 | |
452 var viewportHeight = this.element.offsetHeight; | 500 var viewportHeight = this.element.offsetHeight; |
453 var totalHeight = this._totalHeight(); | 501 var totalHeight = this._totalHeight(); |
454 var scrollTop = this.element.scrollTop; | 502 var scrollTop = this.element.scrollTop; |
455 | 503 |
456 if (this._renderedHeight < viewportHeight || totalHeight < viewportHeight) { | 504 if (this._renderedHeight < viewportHeight || totalHeight < viewportHeight) { |
457 this._clearViewport(); | 505 this._clearViewport(); |
458 this._updateViewport(Number.constrain(scrollTop, 0, totalHeight - viewport Height), viewportHeight); | 506 this._updateViewport(Number.constrain(scrollTop, 0, totalHeight - viewport Height), viewportHeight); |
459 return; | 507 return; |
460 } | 508 } |
461 | 509 |
(...skipping 11 matching lines...) Expand all Loading... | |
473 } | 521 } |
474 | 522 |
475 if (from >= this._lastIndex) { | 523 if (from >= this._lastIndex) { |
476 var bottomHeight = this._bottomHeight + heightDelta; | 524 var bottomHeight = this._bottomHeight + heightDelta; |
477 this._bottomElement.style.height = bottomHeight + 'px'; | 525 this._bottomElement.style.height = bottomHeight + 'px'; |
478 this._bottomHeight = bottomHeight; | 526 this._bottomHeight = bottomHeight; |
479 this._renderedHeight = totalHeight; | 527 this._renderedHeight = totalHeight; |
480 return; | 528 return; |
481 } | 529 } |
482 | 530 |
483 // TODO(dgozman): try to keep the visible scrollTop the same | 531 // TODO(dgozman): try to keep visible scrollTop the same |
484 // when invalidating after firstIndex but before first visible element. | 532 // when invalidating after firstIndex but before first visible element. |
485 this._clearViewport(); | 533 this._clearViewport(); |
486 this._updateViewport(Number.constrain(scrollTop, 0, totalHeight - viewportHe ight), viewportHeight); | 534 this._updateViewport(Number.constrain(scrollTop, 0, totalHeight - viewportHe ight), viewportHeight); |
487 } | 535 } |
488 | 536 |
537 /** | |
538 * @param {number} start | |
539 * @param {number} remove | |
540 * @param {number} add | |
541 */ | |
542 _invalidateGrowMode(start, remove, add) { | |
543 var startElement = this._topElement; | |
544 for (var index = 0; index < start; index++) | |
545 startElement = startElement.nextSibling; | |
caseq
2016/12/29 19:25:31
nextElementSibling
dgozman
2016/12/29 20:38:44
Done.
| |
546 for (var index = 0; index < remove; index++) | |
caseq
2016/12/29 19:25:30
nit: while (remove--)
dgozman
2016/12/29 20:38:44
Done.
| |
547 startElement.nextSibling.remove(); | |
caseq
2016/12/29 19:25:31
ditto.
| |
548 for (var index = add - 1; index >= 0; index--) { | |
549 var element = this._elementAtIndex(start + index); | |
550 this.element.insertBefore(element, startElement.nextSibling); | |
551 } | |
552 } | |
553 | |
489 _clearViewport() { | 554 _clearViewport() { |
555 if (this._mode === UI.ListMode.Grow) | |
556 throw 'There should be no viewport updates in grow mode'; | |
490 this._firstIndex = 0; | 557 this._firstIndex = 0; |
491 this._lastIndex = 0; | 558 this._lastIndex = 0; |
492 this._renderedHeight = 0; | 559 this._renderedHeight = 0; |
493 this._topHeight = 0; | 560 this._topHeight = 0; |
494 this._bottomHeight = 0; | 561 this._bottomHeight = 0; |
562 this._clearContents(); | |
563 } | |
564 | |
565 _clearContents() { | |
566 // Note: this method should not force layout. Be careful. | |
495 this._topElement.style.height = '0'; | 567 this._topElement.style.height = '0'; |
496 this._bottomElement.style.height = '0'; | 568 this._bottomElement.style.height = '0'; |
497 this.element.removeChildren(); | 569 this.element.removeChildren(); |
498 this.element.appendChild(this._topElement); | 570 this.element.appendChild(this._topElement); |
499 this.element.appendChild(this._bottomElement); | 571 this.element.appendChild(this._bottomElement); |
500 } | 572 } |
501 | 573 |
502 _onScroll() { | |
503 this._updateViewport(this.element.scrollTop, this.element.offsetHeight); | |
504 } | |
505 | |
506 /** | 574 /** |
507 * @param {number} scrollTop | 575 * @param {number} scrollTop |
508 * @param {number} viewportHeight | 576 * @param {number} viewportHeight |
509 */ | 577 */ |
510 _updateViewport(scrollTop, viewportHeight) { | 578 _updateViewport(scrollTop, viewportHeight) { |
511 // Note: this method should not force layout. Be careful. | 579 // Note: this method should not force layout. Be careful. |
580 if (this._mode === UI.ListMode.Grow) | |
581 throw 'There should be no viewport updates in grow mode'; | |
512 | 582 |
513 var totalHeight = this._totalHeight(); | 583 var totalHeight = this._totalHeight(); |
514 if (!totalHeight) { | 584 if (!totalHeight) { |
515 this._firstIndex = 0; | 585 this._firstIndex = 0; |
516 this._lastIndex = 0; | 586 this._lastIndex = 0; |
517 this._topHeight = 0; | 587 this._topHeight = 0; |
518 this._bottomHeight = 0; | 588 this._bottomHeight = 0; |
519 this._renderedHeight = 0; | 589 this._renderedHeight = 0; |
520 this._topElement.style.height = '0'; | 590 this._topElement.style.height = '0'; |
521 this._bottomElement.style.height = '0'; | 591 this._bottomElement.style.height = '0'; |
(...skipping 26 matching lines...) Expand all Loading... | |
548 this._firstIndex = firstIndex; | 618 this._firstIndex = firstIndex; |
549 this._lastIndex = lastIndex; | 619 this._lastIndex = lastIndex; |
550 this._topHeight = this._offsetAtIndex(firstIndex); | 620 this._topHeight = this._offsetAtIndex(firstIndex); |
551 this._topElement.style.height = this._topHeight + 'px'; | 621 this._topElement.style.height = this._topHeight + 'px'; |
552 this._bottomHeight = totalHeight - this._offsetAtIndex(lastIndex); | 622 this._bottomHeight = totalHeight - this._offsetAtIndex(lastIndex); |
553 this._bottomElement.style.height = this._bottomHeight + 'px'; | 623 this._bottomElement.style.height = this._bottomHeight + 'px'; |
554 this._renderedHeight = totalHeight; | 624 this._renderedHeight = totalHeight; |
555 this.element.scrollTop = scrollTop; | 625 this.element.scrollTop = scrollTop; |
556 } | 626 } |
557 }; | 627 }; |
OLD | NEW |