Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(69)

Side by Side Diff: third_party/WebKit/Source/devtools/front_end/ui/ListControl.js

Issue 2592433003: [DevTools] Replace ViewportControl with ListControl. (Closed)
Patch Set: measure, small fixes Created 3 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
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
3 // found in the LICENSE file.
4
5 /**
6 * @template T
7 * @interface
8 */
9 UI.ListDelegate = function() {};
10
11 UI.ListDelegate.prototype = {
12 /**
13 * @param {T} item
14 * @return {!Element}
15 */
16 createElementForItem(item) {},
17
18 /**
19 * @param {T} item
20 * @return {number}
21 */
22 heightForItem(item) {},
23
24 /**
25 * @param {T} item
26 * @return {boolean}
27 */
28 isItemSelectable(item) {},
29
30 /**
31 * @param {?T} from
32 * @param {?T} to
33 * @param {?Element} fromElement
34 * @param {?Element} toElement
35 */
36 selectedItemChanged(from, to, fromElement, toElement) {},
37 };
38
39 /** @enum {string} */
caseq 2016/12/28 18:40:51 Make it an @enum {symbol}?
dgozman 2016/12/28 19:17:11 Done.
40 UI.ListHeightMode = {
41 Fixed: 'Fixed',
42 Measured: 'Measured',
43 Variable: 'Variable'
44 };
45
46 /**
47 * @unrestricted
caseq 2016/12/28 18:40:51 nit: remove?
dgozman 2016/12/28 19:17:11 Done.
48 * @template T
49 */
50 UI.ListControl = class {
51 /**
52 * @param {!UI.ListDelegate<T>} delegate
53 */
54 constructor(delegate) {
55 this.element = createElement('div');
56 this.element.style.overflow = 'auto';
57 this._topElement = this.element.createChild('div');
58 this._bottomElement = this.element.createChild('div');
59 this._firstIndex = 0;
60 this._lastIndex = 0;
61 this._renderedHeight = 0;
62 this._topHeight = 0;
63 this._topElement.style.height = '0';
64 this._bottomHeight = 0;
65 this._bottomElement.style.height = '0';
66
67 /** @type {!Array<T>} */
68 this._items = [];
69 /** @type {!Map<T, !Element>} */
70 this._itemToElement = new Map();
71 this._selectedIndex = -1;
72
73 this._boundKeyDown = event => {
74 if (this.onKeyDown(event))
75 event.consume(true);
76 };
77 this._boundClick = event => {
78 if (this.onClick(event))
79 event.consume(true);
80 };
81
82 this._delegate = delegate;
83 this._heightMode = UI.ListHeightMode.Measured;
84 this._fixedHeight = 0;
85
86 this.element.addEventListener('scroll', this._onScroll.bind(this), false);
caseq 2016/12/28 18:40:51 nit: 3rd param is optional now.
87 }
88
89 /**
90 * @param {!UI.ListHeightMode} mode
91 */
92 setHeightMode(mode) {
93 if (mode === UI.ListHeightMode.Variable)
94 throw 'Variable height is not supported (yet)';
95 this._heightMode = mode;
96 this._fixedHeight = 0;
97 if (this._items.length)
98 this._refresh();
99 }
100
101 /**
102 * @param {boolean} handleInput
103 */
104 setHandleInput(handleInput) {
caseq 2016/12/28 18:40:51 remove till used?
dgozman 2016/12/28 19:17:11 Acknowledged.
105 if (handleInput) {
106 this.element.addEventListener('keydown', this._boundKeyDown, false);
107 this.element.addEventListener('click', this._boundClick, false);
108 } else {
109 this.element.removeEventListener('keydown', this._boundKeyDown, false);
110 this.element.removeEventListener('click', this._boundClick, false);
111 }
112 }
113
114 /**
115 * @return {number}
116 */
117 length() {
118 return this._items.length;
119 }
120
121 /**
122 * @param {number} index
123 * @return {T}
124 */
125 itemAtIndex(index) {
126 return this._items[index];
127 }
128
129 /**
130 * @param {T} item
131 */
132 pushItem(item) {
133 this.replaceItemsInRange(this._items.length, this._items.length, [item]);
134 }
135
136 /**
137 * @return {T}
138 */
139 popItem() {
140 return this.removeItemAtIndex(this._items.length - 1);
141 }
142
143 /**
144 * @param {number} index
145 * @param {T} item
146 */
147 insertItemAtIndex(index, item) {
148 this.replaceItemsInRange(index, index, [item]);
149 }
150
151 /**
152 * @param {number} index
153 * @return {T}
154 */
155 removeItemAtIndex(index) {
156 var result = this._items[index];
157 this.replaceItemsInRange(index, index + 1, []);
158 return result;
159 }
160
161 /**
162 * @param {number} from
163 * @param {number} to
164 * @param {!Array<T>} items
165 */
166 replaceItemsInRange(from, to, items) {
167 var oldSelectedItem = this._selectedIndex !== -1 ? this._items[this._selecte dIndex] : null;
168 var oldSelectedElement = oldSelectedItem ? (this._itemToElement.get(oldSelec tedItem) || null) : null;
169
170 for (var i = from; i < to; i++)
171 this._itemToElement.delete(this._items[i]);
172 if (items.length < 10000) {
173 this._items.splice.bind(this._items, from, to - from).apply(null, items);
174 } else {
175 // Splice may fail with too many arguments.
176 var before = this._items.slice(0, from);
177 var after = this._items.slice(to);
178 this._items = before.concat(items).concat(after);
caseq 2016/12/28 18:40:51 before.concat(items, after) or [].concat(before, i
dgozman 2016/12/28 19:17:11 Done.
179 }
180 this._invalidate(from, to, items.length);
181
182 if (this._selectedIndex >= to) {
183 this._selectedIndex += items.length - (to - from);
184 } else if (this._selectedIndex >= from) {
185 var index = this._findClosestSelectable(from + items.length, +1, 0, false) ;
186 if (index === -1)
187 index = this._findClosestSelectable(from - 1, -1, 0, false);
188 this._select(index, oldSelectedItem, oldSelectedElement);
189 }
190 }
191
192 /**
193 * @param {!Array<T>} items
194 */
195 replaceAllItems(items) {
196 this.replaceItemsInRange(0, this._items.length, items);
197 }
198
199 /**
200 * @param {number} from
201 * @param {number} to
202 */
203 invalidateRange(from, to) {
204 this._invalidate(from, to, to - from);
205 }
206
207 viewportResized() {
208 this._refresh();
209 }
210
211 /**
212 * @param {number} index
213 */
214 scrollItemAtIndexIntoView(index) {
215 var top = this._offsetAtIndex(index);
216 var bottom = this._offsetAtIndex(index + 1);
217 var scrollTop = this.element.scrollTop;
218 var height = this.element.offsetHeight;
219 if (top < scrollTop)
220 this._update(top, height);
221 else if (bottom > scrollTop + height)
222 this._update(bottom - height, height);
223 }
224
225 /**
226 * @param {number} index
227 * @param {boolean=} scrollIntoView
228 */
229 selectItemAtIndex(index, scrollIntoView) {
230 if (index !== -1 && !this._delegate.isItemSelectable(this._items[index]))
231 throw 'Attempt to select non-selectable item';
232 this._select(index);
233 if (index !== -1 && !!scrollIntoView)
234 this.scrollItemAtIndexIntoView(index);
235 }
236
237 /**
238 * @return {number}
239 */
240 selectedIndex() {
241 return this._selectedIndex;
242 }
243
244 /**
245 * @return {?T}
246 */
247 selectedItem() {
248 return this._selectedIndex === -1 ? null : this._items[this._selectedIndex];
249 }
250
251 /**
252 * @param {!Event} event
253 * @return {boolean}
254 */
255 onKeyDown(event) {
256 var index = -1;
257 switch (event.key) {
258 case 'ArrowUp':
259 index = this._selectedIndex === -1 ? this._items.length - 1 : this._sele ctedIndex;
caseq 2016/12/28 18:40:50 This will select second from last when nothing is
dgozman 2016/12/28 19:17:11 Done.
260 index = this._findClosestSelectable(index, -1, 1, true);
261 break;
262 case 'ArrowDown':
263 index = this._selectedIndex === -1 ? 0 : this._selectedIndex;
caseq 2016/12/28 18:40:51 Similarly, this will result in second element bein
264 index = this._findClosestSelectable(index, +1, 1, true);
265 break;
266 case 'PageUp':
267 index = this._selectedIndex === -1 ? this._items.length - 1 : this._sele ctedIndex;
268 // Compensate for zoom rounding errors with -1.
269 index = this._findClosestSelectable(index, -1, this.element.offsetHeight - 1, false);
270 break;
271 case 'PageDown':
272 index = this._selectedIndex === -1 ? 0 : this._selectedIndex;
273 // Compensate for zoom rounding errors with -1.
274 index = this._findClosestSelectable(index, +1, this.element.offsetHeight - 1, false);
275 break;
276 default:
277 return false;
278 }
279 if (index !== -1) {
280 this.scrollItemAtIndexIntoView(index);
281 this._select(index);
282 return true;
283 }
284 return false;
285 }
286
287 /**
288 * @param {!Event} event
289 * @return {boolean}
290 */
291 onClick(event) {
292 var node = event.target;
293 while (node && node.parentNodeOrShadowHost() !== this.element)
294 node = node.parentNodeOrShadowHost();
295 if (!node || node.nodeType !== Node.ELEMENT_NODE)
296 return false;
297 var offset = /** @type {!Element} */ (node).getBoundingClientRect().top;
298 offset -= this.element.getBoundingClientRect().top;
299 var index = this._indexAtOffset(offset + this.element.scrollTop);
300 if (index === -1 || !this._delegate.isItemSelectable(this._items[index]))
301 return false;
302 this._select(index);
303 return true;
304 }
305
306 /**
307 * @return {number}
308 */
309 _totalHeight() {
310 return this._offsetAtIndex(this._items.length);
311 }
312
313 /**
314 * @param {number} offset
315 * @return {number}
316 */
317 _indexAtOffset(offset) {
318 if (!this._items.length || offset < 0)
319 return 0;
320 if (this._heightMode === UI.ListHeightMode.Variable)
321 throw 'Variable height is not supported (yet)';
322 if (!this._fixedHeight)
323 this._measureHeight();
324 return Math.min(this._items.length - 1, Math.floor(offset / this._fixedHeigh t));
325 }
326
327 /**
328 * @param {number} index
329 * @return {!Element}
330 */
331 _elementAtIndex(index) {
332 var item = this._items[index];
333 var element = this._itemToElement.get(item);
334 if (!element) {
335 element = this._delegate.createElementForItem(item);
336 this._itemToElement.set(item, element);
337 }
338 return element;
339 }
340
341 /**
342 * @param {number} index
343 * @return {number}
344 */
345 _offsetAtIndex(index) {
346 if (!this._items.length)
347 return 0;
348 if (this._heightMode === UI.ListHeightMode.Variable)
349 throw 'Variable height is not supported (yet)';
350 if (!this._fixedHeight)
351 this._measureHeight();
352 return index * this._fixedHeight;
353 }
354
355 _measureHeight() {
356 if (this._heightMode === UI.ListHeightMode.Measured)
357 this._fixedHeight = UI.measurePreferredSize(this._elementAtIndex(0), this. element).height;
358 else
359 this._fixedHeight = this._delegate.heightForItem(this._items[0]);
360 }
361
362 /**
363 * @param {number} index
364 * @param {?T=} oldItem
365 * @param {?Element=} oldElement
366 */
367 _select(index, oldItem, oldElement) {
368 if (oldItem === undefined)
369 oldItem = this._selectedIndex !== -1 ? this._items[this._selectedIndex] : null;
370 if (oldElement === undefined)
371 oldElement = this._itemToElement.get(oldItem) || null;
372 this._selectedIndex = index;
373 var newItem = this._selectedIndex !== -1 ? this._items[this._selectedIndex] : null;
374 var newElement = this._itemToElement.get(newItem) || null;
375 this._delegate.selectedItemChanged(oldItem, newItem, /** @type {?Element} */ (oldElement), newElement);
376 }
377
378 /**
379 * @param {number} index
380 * @param {number} direction
381 * @param {number} minSkippedHeight
382 * @param {boolean} canWrap
383 * @return {number}
384 */
385 _findClosestSelectable(index, direction, minSkippedHeight, canWrap) {
386 var length = this._items.length;
387 if (!length)
388 return -1;
389
390 var lastSelectable = -1;
391 var start = -1;
392 var startOffset = this._offsetAtIndex(index);
393 while (true) {
394 if (index < 0 || index >= length) {
395 if (!canWrap)
396 return lastSelectable;
397 index = (index + length) % length;
398 }
399
400 // Handle full wrap-around.
401 if (index === start)
402 return lastSelectable;
403 if (start === -1) {
404 start = index;
405 startOffset = this._offsetAtIndex(index);
406 }
407
408 if (this._delegate.isItemSelectable(this._items[index])) {
409 if (Math.abs(this._offsetAtIndex(index) - startOffset) >= minSkippedHeig ht)
410 return index;
411 lastSelectable = index;
412 }
413
414 index += direction;
415 }
416 }
417
418 /**
419 * @param {number} from
420 * @param {number} to
421 * @param {number} inserted
422 */
423 _invalidate(from, to, inserted) {
424 var viewportHeight = this.element.offsetHeight;
425 var totalHeight = this._totalHeight();
426 if (this._renderedHeight < viewportHeight || totalHeight < viewportHeight) {
427 this._refresh();
428 return;
429 }
430
431 var scrollTop = this.element.scrollTop;
432 var heightDelta = totalHeight - this._renderedHeight;
433 if (to <= this._firstIndex) {
434 var topHeight = this._topHeight + heightDelta;
435 this._topElement.style.height = topHeight + 'px';
436 this.element.scrollTop = scrollTop + heightDelta;
437 this._topHeight = topHeight;
438 this._renderedHeight = totalHeight;
439 var indexDelta = inserted - (to - from);
440 this._firstIndex += indexDelta;
441 this._lastIndex += indexDelta;
442 return;
443 }
444
445 if (from >= this._lastIndex) {
446 var bottomHeight = this._bottomHeight + heightDelta;
447 this._bottomElement.style.height = bottomHeight + 'px';
448 this._bottomHeight = bottomHeight;
449 this._renderedHeight = totalHeight;
450 return;
451 }
452
453 // TODO(dgozman): try to keep the visible scrollTop the same
454 // when invalidating after firstIndex but before first visible element.
455 this._refresh();
456 }
457
458 _refresh() {
459 var viewportHeight = this.element.offsetHeight;
460 var scrollTop = Math.max(0, Math.min(this.element.scrollTop, this._totalHeig ht() - viewportHeight));
caseq 2016/12/28 18:40:51 use Number.constrain()?
dgozman 2016/12/28 19:17:11 Done.
461 this._firstIndex = 0;
462 this._lastIndex = 0;
463 this._renderedHeight = 0;
464 this._topHeight = 0;
465 this._bottomHeight = 0;
466 this.element.removeChildren();
467 this.element.appendChild(this._topElement);
468 this.element.appendChild(this._bottomElement);
469 this._update(scrollTop, viewportHeight);
470 }
471
472 _onScroll() {
473 this._update(this.element.scrollTop, this.element.offsetHeight);
474 }
475
476 /**
477 * @param {number} scrollTop
478 * @param {number} viewportHeight
479 */
480 _update(scrollTop, viewportHeight) {
481 // Note: this method should not force layout. Be careful.
482
483 var totalHeight = this._totalHeight();
484 if (!totalHeight) {
485 this._firstIndex = 0;
486 this._lastIndex = 0;
487 this._topHeight = 0;
488 this._bottomHeight = 0;
489 this._renderedHeight = 0;
490 this._topElement.style.height = '0';
491 this._bottomElement.style.height = '0';
492 return;
493 }
494
495 var firstIndex = this._indexAtOffset(scrollTop - viewportHeight);
496 var lastIndex = this._indexAtOffset(scrollTop + 2 * viewportHeight) + 1;
497
498 while (this._firstIndex < Math.min(firstIndex, this._lastIndex)) {
499 this._elementAtIndex(this._firstIndex).remove();
500 this._firstIndex++;
501 }
502 while (this._lastIndex > Math.max(lastIndex, this._firstIndex)) {
503 this._elementAtIndex(this._lastIndex - 1).remove();
504 this._lastIndex--;
505 }
506
507 this._firstIndex = Math.min(this._firstIndex, lastIndex);
508 this._lastIndex = Math.max(this._lastIndex, firstIndex);
509 for (var index = this._firstIndex - 1; index >= firstIndex; index--) {
510 var element = this._elementAtIndex(index);
511 this.element.insertBefore(element, this._topElement.nextSibling);
512 }
513 for (var index = this._lastIndex; index < lastIndex; index++) {
514 var element = this._elementAtIndex(index);
515 this.element.insertBefore(element, this._bottomElement);
516 }
517
518 this._firstIndex = firstIndex;
519 this._lastIndex = lastIndex;
520 this._topHeight = this._offsetAtIndex(firstIndex);
521 this._topElement.style.height = this._topHeight + 'px';
522 this._bottomHeight = totalHeight - this._offsetAtIndex(lastIndex);
523 this._bottomElement.style.height = this._bottomHeight + 'px';
524 this._renderedHeight = totalHeight;
525 this.element.scrollTop = scrollTop;
526 }
527 };
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698