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

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

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

Powered by Google App Engine
This is Rietveld 408576698