Chromium Code Reviews| 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 |