blob: cb41774448e9c2ea172ccad96660057f5d059697 [file] [log] [blame]
Blink Reformat4c46d092018-04-07 15:32:371// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
Tim van der Lippeaa76aa22020-02-14 14:38:245import * as ARIAUtils from './ARIAUtils.js';
Paul Lewis9950e182019-12-16 16:06:076import {Events as ListModelEvents, ListModel} from './ListModel.js'; // eslint-disable-line no-unused-vars
7import {measurePreferredSize} from './UIUtils.js';
8
Blink Reformat4c46d092018-04-07 15:32:379/**
10 * @template T
11 * @interface
12 */
Tim van der Lippe0830b3d2019-10-03 13:20:0713export class ListDelegate {
Blink Reformat4c46d092018-04-07 15:32:3714 /**
15 * @param {T} item
16 * @return {!Element}
17 */
Tim van der Lippe0830b3d2019-10-03 13:20:0718 createElementForItem(item) {
19 }
Blink Reformat4c46d092018-04-07 15:32:3720
21 /**
22 * This method is not called in NonViewport mode.
23 * Return zero to make list measure the item (only works in SameHeight mode).
24 * @param {T} item
25 * @return {number}
26 */
Tim van der Lippe0830b3d2019-10-03 13:20:0727 heightForItem(item) {
28 }
Blink Reformat4c46d092018-04-07 15:32:3729
30 /**
31 * @param {T} item
32 * @return {boolean}
33 */
Tim van der Lippe0830b3d2019-10-03 13:20:0734 isItemSelectable(item) {
35 }
Blink Reformat4c46d092018-04-07 15:32:3736
37 /**
38 * @param {?T} from
39 * @param {?T} to
40 * @param {?Element} fromElement
41 * @param {?Element} toElement
42 */
Tim van der Lippe0830b3d2019-10-03 13:20:0743 selectedItemChanged(from, to, fromElement, toElement) {
44 }
Jack Lynch805641c2019-12-07 00:05:3945
46 /**
47 * @param {?Element} fromElement
48 * @param {?Element} toElement
49 * @return {boolean}
50 */
51 updateSelectedItemARIA(fromElement, toElement) {
52 }
Tim van der Lippe0830b3d2019-10-03 13:20:0753}
Blink Reformat4c46d092018-04-07 15:32:3754
55/** @enum {symbol} */
Tim van der Lippe0830b3d2019-10-03 13:20:0756export const ListMode = {
Blink Reformat4c46d092018-04-07 15:32:3757 NonViewport: Symbol('UI.ListMode.NonViewport'),
58 EqualHeightItems: Symbol('UI.ListMode.EqualHeightItems'),
59 VariousHeightItems: Symbol('UI.ListMode.VariousHeightItems')
60};
61
62/**
63 * @template T
64 */
Paul Lewis9950e182019-12-16 16:06:0765export class ListControl {
Blink Reformat4c46d092018-04-07 15:32:3766 /**
Paul Lewis9950e182019-12-16 16:06:0767 * @param {!ListModel<T>} model
Tim van der Lippe0830b3d2019-10-03 13:20:0768 * @param {!ListDelegate<T>} delegate
69 * @param {!ListMode=} mode
Blink Reformat4c46d092018-04-07 15:32:3770 */
71 constructor(model, delegate, mode) {
72 this.element = createElement('div');
73 this.element.style.overflowY = 'auto';
74 this._topElement = this.element.createChild('div');
75 this._bottomElement = this.element.createChild('div');
76 this._firstIndex = 0;
77 this._lastIndex = 0;
78 this._renderedHeight = 0;
79 this._topHeight = 0;
80 this._bottomHeight = 0;
81
82 this._model = model;
Paul Lewis9950e182019-12-16 16:06:0783 this._model.addEventListener(ListModelEvents.ItemsReplaced, this._replacedItemsInRange, this);
Blink Reformat4c46d092018-04-07 15:32:3784 /** @type {!Map<T, !Element>} */
85 this._itemToElement = new Map();
86 this._selectedIndex = -1;
87 /** @type {?T} */
88 this._selectedItem = null;
89
90 this.element.tabIndex = -1;
91 this.element.addEventListener('click', this._onClick.bind(this), false);
92 this.element.addEventListener('keydown', this._onKeyDown.bind(this), false);
Tim van der Lippeaa76aa22020-02-14 14:38:2493 ARIAUtils.markAsListBox(this.element);
Blink Reformat4c46d092018-04-07 15:32:3794
95 this._delegate = delegate;
Paul Lewis9950e182019-12-16 16:06:0796 this._mode = mode || ListMode.EqualHeightItems;
Blink Reformat4c46d092018-04-07 15:32:3797 this._fixedHeight = 0;
98 this._variableOffsets = new Int32Array(0);
99 this._clearContents();
100
Paul Lewis9950e182019-12-16 16:06:07101 if (this._mode !== ListMode.NonViewport) {
Blink Reformat4c46d092018-04-07 15:32:37102 this.element.addEventListener('scroll', () => {
103 this._updateViewport(this.element.scrollTop, this.element.offsetHeight);
104 }, false);
105 }
106 }
107
108 /**
Paul Lewis9950e182019-12-16 16:06:07109 * @param {!ListModel<T>} model
Blink Reformat4c46d092018-04-07 15:32:37110 */
111 setModel(model) {
112 this._itemToElement.clear();
113 const length = this._model.length;
Paul Lewis9950e182019-12-16 16:06:07114 this._model.removeEventListener(ListModelEvents.ItemsReplaced, this._replacedItemsInRange, this);
Blink Reformat4c46d092018-04-07 15:32:37115 this._model = model;
Paul Lewis9950e182019-12-16 16:06:07116 this._model.addEventListener(ListModelEvents.ItemsReplaced, this._replacedItemsInRange, this);
Blink Reformat4c46d092018-04-07 15:32:37117 this.invalidateRange(0, length);
118 }
119
120 /**
121 * @param {!Common.Event} event
122 */
123 _replacedItemsInRange(event) {
124 const data = /** @type {{index: number, removed: !Array<T>, inserted: number}} */ (event.data);
125 const from = data.index;
126 const to = from + data.removed.length;
127
128 const oldSelectedItem = this._selectedItem;
129 const oldSelectedElement = oldSelectedItem ? (this._itemToElement.get(oldSelectedItem) || null) : null;
Tim van der Lippe1d6e57a2019-09-30 11:55:34130 for (let i = 0; i < data.removed.length; i++) {
Blink Reformat4c46d092018-04-07 15:32:37131 this._itemToElement.delete(data.removed[i]);
Tim van der Lippe1d6e57a2019-09-30 11:55:34132 }
Blink Reformat4c46d092018-04-07 15:32:37133 this._invalidate(from, to, data.inserted);
134
135 if (this._selectedIndex >= to) {
136 this._selectedIndex += data.inserted - (to - from);
137 this._selectedItem = this._model.at(this._selectedIndex);
138 } else if (this._selectedIndex >= from) {
139 let index = this._findFirstSelectable(from + data.inserted, +1, false);
Tim van der Lippe1d6e57a2019-09-30 11:55:34140 if (index === -1) {
Blink Reformat4c46d092018-04-07 15:32:37141 index = this._findFirstSelectable(from - 1, -1, false);
Tim van der Lippe1d6e57a2019-09-30 11:55:34142 }
Blink Reformat4c46d092018-04-07 15:32:37143 this._select(index, oldSelectedItem, oldSelectedElement);
144 }
145 }
146
147 /**
148 * @param {T} item
149 */
150 refreshItem(item) {
151 const index = this._model.indexOf(item);
152 if (index === -1) {
153 console.error('Item to refresh is not present');
154 return;
155 }
Alexey Kozyatinskiyd0ece352018-08-14 02:04:17156 this.refreshItemByIndex(index);
157 }
158
159 /**
160 * @param {number} index
161 */
162 refreshItemByIndex(index) {
163 const item = this._model.at(index);
Blink Reformat4c46d092018-04-07 15:32:37164 this._itemToElement.delete(item);
165 this.invalidateRange(index, index + 1);
Tim van der Lippe1d6e57a2019-09-30 11:55:34166 if (this._selectedIndex !== -1) {
Blink Reformat4c46d092018-04-07 15:32:37167 this._select(this._selectedIndex, null, null);
Tim van der Lippe1d6e57a2019-09-30 11:55:34168 }
Blink Reformat4c46d092018-04-07 15:32:37169 }
170
171 /**
172 * @param {number} from
173 * @param {number} to
174 */
175 invalidateRange(from, to) {
176 this._invalidate(from, to, to - from);
177 }
178
179 viewportResized() {
Paul Lewis9950e182019-12-16 16:06:07180 if (this._mode === ListMode.NonViewport) {
Blink Reformat4c46d092018-04-07 15:32:37181 return;
Tim van der Lippe1d6e57a2019-09-30 11:55:34182 }
Blink Reformat4c46d092018-04-07 15:32:37183 // TODO(dgozman): try to keep visible scrollTop the same.
184 const scrollTop = this.element.scrollTop;
185 const viewportHeight = this.element.offsetHeight;
186 this._clearViewport();
187 this._updateViewport(Number.constrain(scrollTop, 0, this._totalHeight() - viewportHeight), viewportHeight);
188 }
189
190 invalidateItemHeight() {
Paul Lewis9950e182019-12-16 16:06:07191 if (this._mode !== ListMode.EqualHeightItems) {
Blink Reformat4c46d092018-04-07 15:32:37192 console.error('Only supported in equal height items mode');
193 return;
194 }
195 this._fixedHeight = 0;
196 if (this._model.length) {
197 this._itemToElement.clear();
198 this._invalidate(0, this._model.length, this._model.length);
199 }
200 }
201
202 /**
203 * @param {?Node} node
204 * @return {?T}
205 */
206 itemForNode(node) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34207 while (node && node.parentNodeOrShadowHost() !== this.element) {
Blink Reformat4c46d092018-04-07 15:32:37208 node = node.parentNodeOrShadowHost();
Tim van der Lippe1d6e57a2019-09-30 11:55:34209 }
210 if (!node) {
Blink Reformat4c46d092018-04-07 15:32:37211 return null;
Tim van der Lippe1d6e57a2019-09-30 11:55:34212 }
Blink Reformat4c46d092018-04-07 15:32:37213 const element = /** @type {!Element} */ (node);
214 const index = this._model.findIndex(item => this._itemToElement.get(item) === element);
215 return index !== -1 ? this._model.at(index) : null;
216 }
217
218 /**
219 * @param {T} item
220 * @param {boolean=} center
221 */
222 scrollItemIntoView(item, center) {
223 const index = this._model.indexOf(item);
224 if (index === -1) {
225 console.error('Attempt to scroll onto missing item');
226 return;
227 }
228 this._scrollIntoView(index, center);
229 }
230
231 /**
232 * @return {?T}
233 */
234 selectedItem() {
235 return this._selectedItem;
236 }
237
238 /**
239 * @return {number}
240 */
241 selectedIndex() {
242 return this._selectedIndex;
243 }
244
245 /**
246 * @param {?T} item
247 * @param {boolean=} center
248 * @param {boolean=} dontScroll
249 */
250 selectItem(item, center, dontScroll) {
251 let index = -1;
252 if (item !== null) {
253 index = this._model.indexOf(item);
254 if (index === -1) {
255 console.error('Attempt to select missing item');
256 return;
257 }
258 if (!this._delegate.isItemSelectable(item)) {
259 console.error('Attempt to select non-selectable item');
260 return;
261 }
262 }
Junyi Xiao27d18072019-07-27 01:10:10263 // Scrolling the item before selection ensures it is in the DOM.
Tim van der Lippe1d6e57a2019-09-30 11:55:34264 if (index !== -1 && !dontScroll) {
Blink Reformat4c46d092018-04-07 15:32:37265 this._scrollIntoView(index, center);
Tim van der Lippe1d6e57a2019-09-30 11:55:34266 }
267 if (this._selectedIndex !== index) {
Junyi Xiao27d18072019-07-27 01:10:10268 this._select(index);
Tim van der Lippe1d6e57a2019-09-30 11:55:34269 }
Blink Reformat4c46d092018-04-07 15:32:37270 }
271
272 /**
273 * @param {boolean=} canWrap
274 * @param {boolean=} center
275 * @return {boolean}
276 */
277 selectPreviousItem(canWrap, center) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34278 if (this._selectedIndex === -1 && !canWrap) {
Blink Reformat4c46d092018-04-07 15:32:37279 return false;
Tim van der Lippe1d6e57a2019-09-30 11:55:34280 }
Blink Reformat4c46d092018-04-07 15:32:37281 let index = this._selectedIndex === -1 ? this._model.length - 1 : this._selectedIndex - 1;
282 index = this._findFirstSelectable(index, -1, !!canWrap);
283 if (index !== -1) {
284 this._scrollIntoView(index, center);
285 this._select(index);
286 return true;
287 }
288 return false;
289 }
290
291 /**
292 * @param {boolean=} canWrap
293 * @param {boolean=} center
294 * @return {boolean}
295 */
296 selectNextItem(canWrap, center) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34297 if (this._selectedIndex === -1 && !canWrap) {
Blink Reformat4c46d092018-04-07 15:32:37298 return false;
Tim van der Lippe1d6e57a2019-09-30 11:55:34299 }
Blink Reformat4c46d092018-04-07 15:32:37300 let index = this._selectedIndex === -1 ? 0 : this._selectedIndex + 1;
301 index = this._findFirstSelectable(index, +1, !!canWrap);
302 if (index !== -1) {
303 this._scrollIntoView(index, center);
304 this._select(index);
305 return true;
306 }
307 return false;
308 }
309
310 /**
311 * @param {boolean=} center
312 * @return {boolean}
313 */
314 selectItemPreviousPage(center) {
Paul Lewis9950e182019-12-16 16:06:07315 if (this._mode === ListMode.NonViewport) {
Blink Reformat4c46d092018-04-07 15:32:37316 return false;
Tim van der Lippe1d6e57a2019-09-30 11:55:34317 }
Blink Reformat4c46d092018-04-07 15:32:37318 let index = this._selectedIndex === -1 ? this._model.length - 1 : this._selectedIndex;
319 index = this._findPageSelectable(index, -1);
320 if (index !== -1) {
321 this._scrollIntoView(index, center);
322 this._select(index);
323 return true;
324 }
325 return false;
326 }
327
328 /**
329 * @param {boolean=} center
330 * @return {boolean}
331 */
332 selectItemNextPage(center) {
Paul Lewis9950e182019-12-16 16:06:07333 if (this._mode === ListMode.NonViewport) {
Blink Reformat4c46d092018-04-07 15:32:37334 return false;
Tim van der Lippe1d6e57a2019-09-30 11:55:34335 }
Blink Reformat4c46d092018-04-07 15:32:37336 let index = this._selectedIndex === -1 ? 0 : this._selectedIndex;
337 index = this._findPageSelectable(index, +1);
338 if (index !== -1) {
339 this._scrollIntoView(index, center);
340 this._select(index);
341 return true;
342 }
343 return false;
344 }
345
346 /**
347 * @param {number} index
348 * @param {boolean=} center
349 */
350 _scrollIntoView(index, center) {
Paul Lewis9950e182019-12-16 16:06:07351 if (this._mode === ListMode.NonViewport) {
Blink Reformat4c46d092018-04-07 15:32:37352 this._elementAtIndex(index).scrollIntoViewIfNeeded(!!center);
353 return;
354 }
355
356 const top = this._offsetAtIndex(index);
357 const bottom = this._offsetAtIndex(index + 1);
358 const viewportHeight = this.element.offsetHeight;
359 if (center) {
360 const scrollTo = (top + bottom) / 2 - viewportHeight / 2;
361 this._updateViewport(Number.constrain(scrollTo, 0, this._totalHeight() - viewportHeight), viewportHeight);
362 return;
363 }
364
365 const scrollTop = this.element.scrollTop;
Tim van der Lippe1d6e57a2019-09-30 11:55:34366 if (top < scrollTop) {
Blink Reformat4c46d092018-04-07 15:32:37367 this._updateViewport(top, viewportHeight);
Tim van der Lippe1d6e57a2019-09-30 11:55:34368 } else if (bottom > scrollTop + viewportHeight) {
Blink Reformat4c46d092018-04-07 15:32:37369 this._updateViewport(bottom - viewportHeight, viewportHeight);
Tim van der Lippe1d6e57a2019-09-30 11:55:34370 }
Blink Reformat4c46d092018-04-07 15:32:37371 }
372
373 /**
374 * @param {!Event} event
375 */
376 _onClick(event) {
377 const item = this.itemForNode(/** @type {?Node} */ (event.target));
Tim van der Lippe1d6e57a2019-09-30 11:55:34378 if (item && this._delegate.isItemSelectable(item)) {
Blink Reformat4c46d092018-04-07 15:32:37379 this.selectItem(item);
Tim van der Lippe1d6e57a2019-09-30 11:55:34380 }
Blink Reformat4c46d092018-04-07 15:32:37381 }
382
383 /**
384 * @param {!Event} event
385 */
386 _onKeyDown(event) {
387 let selected = false;
388 switch (event.key) {
389 case 'ArrowUp':
390 selected = this.selectPreviousItem(true, false);
391 break;
392 case 'ArrowDown':
393 selected = this.selectNextItem(true, false);
394 break;
395 case 'PageUp':
396 selected = this.selectItemPreviousPage(false);
397 break;
398 case 'PageDown':
399 selected = this.selectItemNextPage(false);
400 break;
401 }
Tim van der Lippe1d6e57a2019-09-30 11:55:34402 if (selected) {
Jack Lynch805641c2019-12-07 00:05:39403 event.consume(true);
Tim van der Lippe1d6e57a2019-09-30 11:55:34404 }
Blink Reformat4c46d092018-04-07 15:32:37405 }
406
407 /**
408 * @return {number}
409 */
410 _totalHeight() {
411 return this._offsetAtIndex(this._model.length);
412 }
413
414 /**
415 * @param {number} offset
416 * @return {number}
417 */
418 _indexAtOffset(offset) {
Paul Lewis9950e182019-12-16 16:06:07419 if (this._mode === ListMode.NonViewport) {
Blink Reformat4c46d092018-04-07 15:32:37420 throw 'There should be no offset conversions in non-viewport mode';
Tim van der Lippe1d6e57a2019-09-30 11:55:34421 }
422 if (!this._model.length || offset < 0) {
Blink Reformat4c46d092018-04-07 15:32:37423 return 0;
Tim van der Lippe1d6e57a2019-09-30 11:55:34424 }
Paul Lewis9950e182019-12-16 16:06:07425 if (this._mode === ListMode.VariousHeightItems) {
Blink Reformat4c46d092018-04-07 15:32:37426 return Math.min(
427 this._model.length - 1, this._variableOffsets.lowerBound(offset, undefined, 0, this._model.length));
428 }
Tim van der Lippe1d6e57a2019-09-30 11:55:34429 if (!this._fixedHeight) {
Blink Reformat4c46d092018-04-07 15:32:37430 this._measureHeight();
Tim van der Lippe1d6e57a2019-09-30 11:55:34431 }
Blink Reformat4c46d092018-04-07 15:32:37432 return Math.min(this._model.length - 1, Math.floor(offset / this._fixedHeight));
433 }
434
435 /**
436 * @param {number} index
437 * @return {!Element}
438 */
439 _elementAtIndex(index) {
440 const item = this._model.at(index);
441 let element = this._itemToElement.get(item);
442 if (!element) {
443 element = this._delegate.createElementForItem(item);
Tim van der Lippeaa76aa22020-02-14 14:38:24444 if (!ARIAUtils.hasRole(element)) {
445 ARIAUtils.markAsOption(element);
Jack Lynch805641c2019-12-07 00:05:39446 }
Blink Reformat4c46d092018-04-07 15:32:37447 this._itemToElement.set(item, element);
448 }
449 return element;
450 }
451
452 /**
453 * @param {number} index
454 * @return {number}
455 */
456 _offsetAtIndex(index) {
Paul Lewis9950e182019-12-16 16:06:07457 if (this._mode === ListMode.NonViewport) {
Jack Lynch805641c2019-12-07 00:05:39458 throw new Error('There should be no offset conversions in non-viewport mode');
Tim van der Lippe1d6e57a2019-09-30 11:55:34459 }
460 if (!this._model.length) {
Blink Reformat4c46d092018-04-07 15:32:37461 return 0;
Tim van der Lippe1d6e57a2019-09-30 11:55:34462 }
Paul Lewis9950e182019-12-16 16:06:07463 if (this._mode === ListMode.VariousHeightItems) {
Blink Reformat4c46d092018-04-07 15:32:37464 return this._variableOffsets[index];
Tim van der Lippe1d6e57a2019-09-30 11:55:34465 }
466 if (!this._fixedHeight) {
Blink Reformat4c46d092018-04-07 15:32:37467 this._measureHeight();
Tim van der Lippe1d6e57a2019-09-30 11:55:34468 }
Blink Reformat4c46d092018-04-07 15:32:37469 return index * this._fixedHeight;
470 }
471
472 _measureHeight() {
473 this._fixedHeight = this._delegate.heightForItem(this._model.at(0));
Tim van der Lippe1d6e57a2019-09-30 11:55:34474 if (!this._fixedHeight) {
Paul Lewis9950e182019-12-16 16:06:07475 this._fixedHeight = measurePreferredSize(this._elementAtIndex(0), this.element).height;
Tim van der Lippe1d6e57a2019-09-30 11:55:34476 }
Blink Reformat4c46d092018-04-07 15:32:37477 }
478
479 /**
480 * @param {number} index
481 * @param {?T=} oldItem
482 * @param {?Element=} oldElement
483 */
484 _select(index, oldItem, oldElement) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34485 if (oldItem === undefined) {
Blink Reformat4c46d092018-04-07 15:32:37486 oldItem = this._selectedItem;
Tim van der Lippe1d6e57a2019-09-30 11:55:34487 }
488 if (oldElement === undefined) {
Blink Reformat4c46d092018-04-07 15:32:37489 oldElement = this._itemToElement.get(oldItem) || null;
Tim van der Lippe1d6e57a2019-09-30 11:55:34490 }
Blink Reformat4c46d092018-04-07 15:32:37491 this._selectedIndex = index;
492 this._selectedItem = index === -1 ? null : this._model.at(index);
493 const newItem = this._selectedItem;
494 const newElement = this._selectedIndex !== -1 ? this._elementAtIndex(index) : null;
Jack Lynch805641c2019-12-07 00:05:39495
Blink Reformat4c46d092018-04-07 15:32:37496 this._delegate.selectedItemChanged(oldItem, newItem, /** @type {?Element} */ (oldElement), newElement);
Jack Lynch805641c2019-12-07 00:05:39497 if (!this._delegate.updateSelectedItemARIA(/** @type {?Element} */ (oldElement), newElement)) {
498 if (oldElement) {
Tim van der Lippeaa76aa22020-02-14 14:38:24499 ARIAUtils.setSelected(oldElement, false);
Jack Lynch805641c2019-12-07 00:05:39500 }
501 if (newElement) {
Tim van der Lippeaa76aa22020-02-14 14:38:24502 ARIAUtils.setSelected(newElement, true);
Jack Lynch805641c2019-12-07 00:05:39503 }
Tim van der Lippeaa76aa22020-02-14 14:38:24504 ARIAUtils.setActiveDescendant(this.element, newElement);
Jack Lynch805641c2019-12-07 00:05:39505 }
Blink Reformat4c46d092018-04-07 15:32:37506 }
507
508 /**
509 * @param {number} index
510 * @param {number} direction
511 * @param {boolean} canWrap
512 * @return {number}
513 */
514 _findFirstSelectable(index, direction, canWrap) {
515 const length = this._model.length;
Tim van der Lippe1d6e57a2019-09-30 11:55:34516 if (!length) {
Blink Reformat4c46d092018-04-07 15:32:37517 return -1;
Tim van der Lippe1d6e57a2019-09-30 11:55:34518 }
Blink Reformat4c46d092018-04-07 15:32:37519 for (let step = 0; step <= length; step++) {
520 if (index < 0 || index >= length) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34521 if (!canWrap) {
Blink Reformat4c46d092018-04-07 15:32:37522 return -1;
Tim van der Lippe1d6e57a2019-09-30 11:55:34523 }
Blink Reformat4c46d092018-04-07 15:32:37524 index = (index + length) % length;
525 }
Tim van der Lippe1d6e57a2019-09-30 11:55:34526 if (this._delegate.isItemSelectable(this._model.at(index))) {
Blink Reformat4c46d092018-04-07 15:32:37527 return index;
Tim van der Lippe1d6e57a2019-09-30 11:55:34528 }
Blink Reformat4c46d092018-04-07 15:32:37529 index += direction;
530 }
531 return -1;
532 }
533
534 /**
535 * @param {number} index
536 * @param {number} direction
537 * @return {number}
538 */
539 _findPageSelectable(index, direction) {
540 let lastSelectable = -1;
541 const startOffset = this._offsetAtIndex(index);
542 // Compensate for zoom rounding errors with -1.
543 const viewportHeight = this.element.offsetHeight - 1;
544 while (index >= 0 && index < this._model.length) {
545 if (this._delegate.isItemSelectable(this._model.at(index))) {
Tim van der Lippe1d6e57a2019-09-30 11:55:34546 if (Math.abs(this._offsetAtIndex(index) - startOffset) >= viewportHeight) {
Blink Reformat4c46d092018-04-07 15:32:37547 return index;
Tim van der Lippe1d6e57a2019-09-30 11:55:34548 }
Blink Reformat4c46d092018-04-07 15:32:37549 lastSelectable = index;
550 }
551 index += direction;
552 }
553 return lastSelectable;
554 }
555
556 /**
557 * @param {number} length
558 * @param {number} copyTo
559 */
560 _reallocateVariableOffsets(length, copyTo) {
561 if (this._variableOffsets.length < length) {
562 const variableOffsets = new Int32Array(Math.max(length, this._variableOffsets.length * 2));
563 variableOffsets.set(this._variableOffsets.slice(0, copyTo), 0);
564 this._variableOffsets = variableOffsets;
565 } else if (this._variableOffsets.length >= 2 * length) {
566 const variableOffsets = new Int32Array(length);
567 variableOffsets.set(this._variableOffsets.slice(0, copyTo), 0);
568 this._variableOffsets = variableOffsets;
569 }
570 }
571
572 /**
573 * @param {number} from
574 * @param {number} to
575 * @param {number} inserted
576 */
577 _invalidate(from, to, inserted) {
Paul Lewis9950e182019-12-16 16:06:07578 if (this._mode === ListMode.NonViewport) {
Blink Reformat4c46d092018-04-07 15:32:37579 this._invalidateNonViewportMode(from, to - from, inserted);
580 return;
581 }
582
Paul Lewis9950e182019-12-16 16:06:07583 if (this._mode === ListMode.VariousHeightItems) {
Blink Reformat4c46d092018-04-07 15:32:37584 this._reallocateVariableOffsets(this._model.length + 1, from + 1);
Tim van der Lippe1d6e57a2019-09-30 11:55:34585 for (let i = from + 1; i <= this._model.length; i++) {
Blink Reformat4c46d092018-04-07 15:32:37586 this._variableOffsets[i] = this._variableOffsets[i - 1] + this._delegate.heightForItem(this._model.at(i - 1));
Tim van der Lippe1d6e57a2019-09-30 11:55:34587 }
Blink Reformat4c46d092018-04-07 15:32:37588 }
589
590 const viewportHeight = this.element.offsetHeight;
591 const totalHeight = this._totalHeight();
592 const scrollTop = this.element.scrollTop;
593
594 if (this._renderedHeight < viewportHeight || totalHeight < viewportHeight) {
595 this._clearViewport();
596 this._updateViewport(Number.constrain(scrollTop, 0, totalHeight - viewportHeight), viewportHeight);
597 return;
598 }
599
600 const heightDelta = totalHeight - this._renderedHeight;
601 if (to <= this._firstIndex) {
602 const topHeight = this._topHeight + heightDelta;
603 this._topElement.style.height = topHeight + 'px';
604 this.element.scrollTop = scrollTop + heightDelta;
605 this._topHeight = topHeight;
606 this._renderedHeight = totalHeight;
607 const indexDelta = inserted - (to - from);
608 this._firstIndex += indexDelta;
609 this._lastIndex += indexDelta;
610 return;
611 }
612
613 if (from >= this._lastIndex) {
614 const bottomHeight = this._bottomHeight + heightDelta;
615 this._bottomElement.style.height = bottomHeight + 'px';
616 this._bottomHeight = bottomHeight;
617 this._renderedHeight = totalHeight;
618 return;
619 }
620
621 // TODO(dgozman): try to keep visible scrollTop the same
622 // when invalidating after firstIndex but before first visible element.
623 this._clearViewport();
624 this._updateViewport(Number.constrain(scrollTop, 0, totalHeight - viewportHeight), viewportHeight);
625 }
626
627 /**
628 * @param {number} start
629 * @param {number} remove
630 * @param {number} add
631 */
632 _invalidateNonViewportMode(start, remove, add) {
633 let startElement = this._topElement;
Tim van der Lippe1d6e57a2019-09-30 11:55:34634 for (let index = 0; index < start; index++) {
Blink Reformat4c46d092018-04-07 15:32:37635 startElement = startElement.nextElementSibling;
Tim van der Lippe1d6e57a2019-09-30 11:55:34636 }
637 while (remove--) {
Blink Reformat4c46d092018-04-07 15:32:37638 startElement.nextElementSibling.remove();
Tim van der Lippe1d6e57a2019-09-30 11:55:34639 }
640 while (add--) {
Blink Reformat4c46d092018-04-07 15:32:37641 this.element.insertBefore(this._elementAtIndex(start + add), startElement.nextElementSibling);
Tim van der Lippe1d6e57a2019-09-30 11:55:34642 }
Blink Reformat4c46d092018-04-07 15:32:37643 }
644
645 _clearViewport() {
Paul Lewis9950e182019-12-16 16:06:07646 if (this._mode === ListMode.NonViewport) {
Blink Reformat4c46d092018-04-07 15:32:37647 console.error('There should be no viewport updates in non-viewport mode');
648 return;
649 }
650 this._firstIndex = 0;
651 this._lastIndex = 0;
652 this._renderedHeight = 0;
653 this._topHeight = 0;
654 this._bottomHeight = 0;
655 this._clearContents();
656 }
657
658 _clearContents() {
659 // Note: this method should not force layout. Be careful.
660 this._topElement.style.height = '0';
661 this._bottomElement.style.height = '0';
662 this.element.removeChildren();
663 this.element.appendChild(this._topElement);
664 this.element.appendChild(this._bottomElement);
665 }
666
667 /**
668 * @param {number} scrollTop
669 * @param {number} viewportHeight
670 */
671 _updateViewport(scrollTop, viewportHeight) {
672 // Note: this method should not force layout. Be careful.
Paul Lewis9950e182019-12-16 16:06:07673 if (this._mode === ListMode.NonViewport) {
Blink Reformat4c46d092018-04-07 15:32:37674 console.error('There should be no viewport updates in non-viewport mode');
675 return;
676 }
677 const totalHeight = this._totalHeight();
678 if (!totalHeight) {
679 this._firstIndex = 0;
680 this._lastIndex = 0;
681 this._topHeight = 0;
682 this._bottomHeight = 0;
683 this._renderedHeight = 0;
684 this._topElement.style.height = '0';
685 this._bottomElement.style.height = '0';
686 return;
687 }
688
689 const firstIndex = this._indexAtOffset(scrollTop - viewportHeight);
690 const lastIndex = this._indexAtOffset(scrollTop + 2 * viewportHeight) + 1;
691
692 while (this._firstIndex < Math.min(firstIndex, this._lastIndex)) {
693 this._elementAtIndex(this._firstIndex).remove();
694 this._firstIndex++;
695 }
696 while (this._lastIndex > Math.max(lastIndex, this._firstIndex)) {
697 this._elementAtIndex(this._lastIndex - 1).remove();
698 this._lastIndex--;
699 }
700
701 this._firstIndex = Math.min(this._firstIndex, lastIndex);
702 this._lastIndex = Math.max(this._lastIndex, firstIndex);
703 for (let index = this._firstIndex - 1; index >= firstIndex; index--) {
704 const element = this._elementAtIndex(index);
705 this.element.insertBefore(element, this._topElement.nextSibling);
706 }
707 for (let index = this._lastIndex; index < lastIndex; index++) {
708 const element = this._elementAtIndex(index);
709 this.element.insertBefore(element, this._bottomElement);
710 }
711
712 this._firstIndex = firstIndex;
713 this._lastIndex = lastIndex;
714 this._topHeight = this._offsetAtIndex(firstIndex);
715 this._topElement.style.height = this._topHeight + 'px';
716 this._bottomHeight = totalHeight - this._offsetAtIndex(lastIndex);
717 this._bottomElement.style.height = this._bottomHeight + 'px';
718 this._renderedHeight = totalHeight;
719 this.element.scrollTop = scrollTop;
720 }
Tim van der Lippe0830b3d2019-10-03 13:20:07721}