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

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

Issue 2592433003: [DevTools] Replace ViewportControl with ListControl. (Closed)
Patch Set: small fix Created 3 years, 12 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 * @return {?number}
14 */
15 fixedHeight() {},
16
17 /**
18 * @param {T} item
19 * @return {!Element}
20 */
21 createElementForItem(item) {},
22
23 /**
24 * @param {T} item
25 * @return {number}
26 */
27 heightForItem(item) {},
28
29 /**
30 * @param {T} item
31 * @return {boolean}
32 */
33 isItemSelectable(item) {},
34
35 /**
36 * @param {?T} from
37 * @param {?T} to
38 * @param {?Element} fromElement
39 * @param {?Element} toElement
40 */
41 selectedItemChanged(from, to, fromElement, toElement) {},
42 };
43
44 /**
45 * @unrestricted
46 * @template T
47 */
48 UI.ListControl = class {
49 /**
50 * @param {!UI.ListDelegate<T>} delegate
51 * @param {boolean} handleInput
52 */
53 constructor(delegate, handleInput) {
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
62 /** @type {!Array<T>} */
63 this._items = [];
64 /** @type {!Map<T, !Element>} */
65 this._itemToElement = new Map();
66 /** @type {!WeakMap<!Element, T>} */
67 this._elementToItem = new WeakMap();
68 this._selectedIndex = -1;
69
70 this._boundKeyDown = (event) => {
alph 2016/12/27 23:38:33 drop ()
dgozman 2016/12/28 06:15:21 Done.
71 if (this.onKeyDown(event))
72 event.consume(true);
73 };
74 this._boundClick = (event) => {
alph 2016/12/27 23:38:33 ditto
dgozman 2016/12/28 06:15:20 Done.
75 if (this.onClick(event))
76 event.consume(true);
77 };
78 this._setUp(delegate, handleInput);
79
80 this.element.addEventListener('scroll', this._onScroll.bind(this), false);
81 this._update(0, this.element.offsetHeight);
82 }
83
84 /**
85 * @return {number}
86 */
87 length() {
88 return this._items.length;
89 }
90
91 /**
92 * @param {number} index
93 * @return {T}
94 */
95 itemAtIndex(index) {
96 return this._items[index];
97 }
98
99 /**
100 * @param {T} item
101 */
102 pushItem(item) {
103 this.replaceItemsInRange(this._items.length, this._items.length, [item]);
104 }
105
106 /**
107 * @return {T}
108 */
109 popItem() {
alph 2016/12/27 23:38:33 return this.removeItemAtIndex(...)
dgozman 2016/12/28 06:15:21 Done.
110 var result = this._items[this._items.length - 1];
alph 2016/12/27 23:38:32 peekLast
111 this.replaceItemsInRange(this._items.length - 1, this._items.length, []);
112 return result;
113 }
114
115 /**
116 * @param {number} index
117 * @param {T} item
118 */
119 insertItemAtIndex(index, item) {
120 this.replaceItemsInRange(index, index, [item]);
121 }
122
123 /**
124 * @param {number} index
125 * @return {T}
126 */
127 removeItemAtIndex(index) {
128 var result = this._items[index];
129 this.replaceItemsInRange(index, index + 1, []);
130 return result;
131 }
132
133 /**
134 * @param {number} from
135 * @param {number} to
136 * @param {!Array<T>} items
137 */
138 replaceItemsInRange(from, to, items) {
139 var oldSelectedItem = this._selectedIndex !== -1 ? this._items[this._selecte dIndex] : null;
140 var oldSelectedElement = oldSelectedItem ? (this._itemToElement.get(oldSelec tedItem) || null) : null;
141
142 for (var i = from; i < to; i++)
143 this._itemToElement.delete(this._items[i]);
144 this._items.splice.bind(this._items, from, to - from).apply(null, items);
alph 2016/12/27 23:38:32 apply with too many items may fail.
dgozman 2016/12/28 06:15:21 Done.
145 this._invalidate(from, to);
146
147 if (this._selectedIndex >= to)
148 this._selectedIndex += items.length - (to - from);
149 else if (this._selectedIndex >= from)
150 this._select(this._selectClosest(from - 1, -1, false), oldSelectedItem, ol dSelectedElement);
alph 2016/12/27 23:38:33 s/-1/+1/ Also put selection on n-1 if the last one
dgozman 2016/12/28 06:15:21 Done.
151 }
152
153 /**
154 * @param {!Array<T>} items
155 */
156 replaceAllItems(items) {
157 this.replaceItemsInRange(0, this._items.length, items);
158 }
159
160 /**
161 * @param {number} from
162 * @param {number} to
163 */
164 invalidateRange(from, to) {
165 this._invalidate(from, to);
alph 2016/12/27 23:38:32 you can merge these two.
dgozman 2016/12/28 06:15:20 Acknowledged.
166 }
167
168 viewportResized() {
169 this._refresh();
170 }
171
172 /**
173 * @param {!UI.ListDelegate<T>} delegate
174 * @param {boolean} handleInput
175 */
176 reset(delegate, handleInput) {
177 this._selectedIndex = -1;
178 this._items = [];
179 this._itemToElement.clear();
180 this._elementToItem = new WeakMap();
181 this._setUp(delegate, handleInput);
182 this._refresh();
183 }
184
185 /**
186 * @param {number} index
187 */
188 scrollItemAtIndexIntoView(index) {
189 var top = this._offsetAtIndex(index);
190 var bottom = this._offsetAtIndex(index + 1);
191 var scrollTop = this.element.scrollTop;
192 var height = this.element.offsetHeight;
193 if (top < scrollTop)
194 this._update(top, height);
195 else if (bottom > scrollTop + height)
196 this._update(bottom - height, height);
197 }
198
199 /**
200 * @param {number} index
201 * @param {boolean} scrollIntoView
alph 2016/12/27 23:38:32 boolean=
dgozman 2016/12/28 06:15:21 Done.
202 */
203 selectItemAtIndex(index, scrollIntoView) {
204 if (index !== -1 && !this._delegate.isItemSelectable(this._items[index]))
205 throw 'Attempt to select non-selectable item';
206 this._select(index);
207 if (index !== -1 && scrollIntoView)
208 this.scrollItemAtIndexIntoView(index);
209 }
210
211 /**
212 * @return {number}
213 */
214 selectedIndex() {
215 return this._selectedIndex;
216 }
217
218 /**
219 * @return {?T}
220 */
221 selectedItem() {
222 return this._selectedIndex === -1 ? null : this._items[this._selectedIndex];
223 }
224
225 /**
226 * @param {!Event} event
227 * @return {boolean}
228 */
229 onKeyDown(event) {
230 var index = -1;
231 switch (event.key) {
232 case 'ArrowUp':
233 index = this._selectedIndex === -1 ? this._items.length - 1 : this._sele ctedIndex - 1;
234 index = this._selectClosest(index, -1, true);
235 break;
236 case 'ArrowDown':
237 index = this._selectedIndex === -1 ? 0 : this._selectedIndex + 1;
238 index = this._selectClosest(index, 1, true);
239 break;
240 case 'PageUp':
241 index = this._selectedIndex === -1 ?
242 this._firstIndex :
alph 2016/12/27 23:38:33 Use first index in viewport
dgozman 2016/12/28 06:15:21 Done.
243 this._indexAtOffset(this._offsetAtIndex(this._selectedIndex) - this. element.offsetHeight);
244 index = this._selectClosest(index, 1, false);
alph 2016/12/27 23:38:33 -1
dgozman 2016/12/28 06:15:20 Done.
245 break;
246 case 'PageDown':
247 index = this._selectedIndex === -1 ?
248 this._lastIndex :
249 this._indexAtOffset(this._offsetAtIndex(this._selectedIndex) + this. element.offsetHeight);
250 index = this._selectClosest(index, 1, false);
251 break;
252 default:
253 return false;
254 }
255 if (index !== -1) {
256 this.scrollItemAtIndexIntoView(index);
257 this._select(index);
258 return true;
259 }
260 return false;
261 }
262
263 /**
264 * @param {!Event} event
265 * @return {boolean}
266 */
267 onClick(event) {
268 var node = event.target;
269 while (node && node.parentNode !== this.element)
alph 2016/12/27 23:38:32 handle shadow DOM
dgozman 2016/12/28 06:15:21 Done.
270 node = node.parentNode;
271 if (!node)
272 return false;
273 var item = this._elementToItem.get(node);
274 if (!item)
275 return false;
276 this._select(item);
alph 2016/12/27 23:38:32 something's wrong here
dgozman 2016/12/28 06:15:21 Done.
277 return true;
278 }
279
280 /**
281 * @return {number}
282 */
283 _totalHeight() {
284 return this._items.length * this._fixedHeight;
285 }
286
287 /**
288 * @param {number} offset
289 * @return {number}
290 */
291 _indexAtOffset(offset) {
292 if (!this._items.length)
293 return 0;
294 if (offset < 0)
295 return 0;
296 var index = Math.floor(offset / this._fixedHeight);
297 if (index >= this._items.length)
298 return this._items.length - 1;
299 return index;
300 }
301
302 /**
303 * @param {number} index
304 * @return {!Element}
305 */
306 _elementAtIndex(index) {
307 var item = this._items[index];
308 var element = this._itemToElement.get(item);
309 if (!element) {
310 element = this._delegate.createElementForItem(item);
311 this._itemToElement.set(item, element);
312 this._elementToItem.set(element, item);
313 }
314 return element;
315 }
316
317 /**
318 * @param {number} index
319 * @return {number}
320 */
321 _offsetAtIndex(index) {
322 return index * this._fixedHeight;
323 }
324
325 /**
326 * @param {number} index
327 * @param {?T=} oldItem
328 * @param {?Element=} oldElement
329 */
330 _select(index, oldItem, oldElement) {
331 if (oldItem === undefined)
332 oldItem = this._selectedIndex !== -1 ? this._items[this._selectedIndex] : null;
333 if (oldElement === undefined)
334 oldElement = oldItem ? (this._itemToElement.get(oldItem) || null) : null;
alph 2016/12/27 23:38:32 you can drop ?:
dgozman 2016/12/28 06:15:21 Done.
335 this._selectedIndex = index;
336 var newItem = this._selectedIndex !== -1 ? this._items[this._selectedIndex] : null;
337 var newElement = newItem ? (this._itemToElement.get(newItem) || null) : null ;
alph 2016/12/27 23:38:33 ditto
dgozman 2016/12/28 06:15:21 Done.
338 this._delegate.selectedItemChanged(oldItem, newItem, oldElement, newElement) ;
339 }
340
341 /**
342 * @param {number} index
343 * @param {number} direction
344 * @param {boolean} canWrap
345 * @return {number}
346 */
347 _selectClosest(index, direction, canWrap) {
348 var length = this._items.length;
349 if (!length)
350 return -1;
351 var start = -1;
352 while (true) {
353 if (index < 0 || index >= length) {
354 if (!canWrap)
355 return -1;
alph 2016/12/27 23:38:32 When reaching top, it should return first selectab
dgozman 2016/12/28 06:15:21 Done.
356 index = (index + length) % length;
357 }
358
359 // Handle full wrap-around.
360 if (index === start)
361 return -1;
362 if (start === -1)
363 start = index;
364
365 if (this._delegate.isItemSelectable(this._items[index]))
366 return index;
367 index += direction;
368 }
369 }
370
371 /**
372 * @param {!UI.ListDelegate<T>} delegate
373 * @param {boolean} handleInput
374 */
375 _setUp(delegate, handleInput) {
376 this._delegate = delegate;
377 if (handleInput) {
378 this.element.addEventListener('keydown', this._boundKeyDown, false);
379 this.element.addEventListener('click', this._boundClick, false);
380 } else {
381 this.element.removeEventListener('keydown', this._boundKeyDown, false);
382 this.element.removeEventListener('click', this._boundClick, false);
383 }
384 this._fixedHeight = this._delegate.fixedHeight();
385 if (!this._fixedHeight)
386 throw 'Variable height is not supported';
387 }
388
389 /**
390 * @param {number} from
391 * @param {number} to
392 */
393 _invalidate(from, to) {
394 var availableHeight = this.element.offsetHeight;
alph 2016/12/27 23:38:33 viewportHeight
dgozman 2016/12/28 06:15:21 Done.
395 var totalHeight = this._totalHeight();
396 if (this._renderedHeight < availableHeight || totalHeight < availableHeight) {
397 this._refresh();
398 return;
399 }
400
401 var scrollTop = this.element.scrollTop;
402 var heightDelta = totalHeight - this._renderedHeight;
403 if (to <= this._firstIndex) {
404 var topHeight = this._topHeight + heightDelta;
405 this._topElement.style.height = topHeight + 'px';
406 this.element.scrollTop = scrollTop + heightDelta;
407 this._topHeight = topHeight;
408 this._renderedHeight = totalHeight;
alph 2016/12/27 23:38:32 update _firstIndex
dgozman 2016/12/28 06:15:21 Done.
409 return;
410 }
411
412 if (from >= this._lastIndex) {
413 var bottomHeight = this._bottomHeight + heightDelta;
414 this._bottomElement.style.height = bottomHeight + 'px';
415 this.element.scrollTop = scrollTop + heightDelta;
alph 2016/12/27 23:38:33 drop this
dgozman 2016/12/28 06:15:21 Done.
416 this._bottomHeight = bottomHeight;
417 this._renderedHeight = totalHeight;
418 return;
419 }
420
421 this._refresh();
422 }
423
424 _refresh() {
425 var height = this.element.offsetHeight;
alph 2016/12/27 23:38:33 viewportHeight
dgozman 2016/12/28 06:15:20 Done.
426 var scrollTop = Math.max(0, Math.min(this.element.scrollTop, this._totalHeig ht() - height));
alph 2016/12/27 23:38:32 If items are added before visible ones, I'd expect
dgozman 2016/12/28 06:15:21 Added TODO.
427 this._firstIndex = 0;
428 this._lastIndex = 0;
429 this._renderedHeight = 0;
430 this.element.removeChildren();
431 this.element.appendChild(this._topElement);
432 this.element.appendChild(this._bottomElement);
433 this._update(scrollTop, height);
434 }
435
436 _onScroll() {
437 this._update(this.element.scrollTop, this.element.offsetHeight);
438 }
439
440 /**
441 * @param {number} scrollTop
442 * @param {number} height
443 */
444 _update(scrollTop, height) {
445 // Note: this method should not force layout. Be careful.
446
447 var totalHeight = this._totalHeight();
448 if (!totalHeight) {
449 this._firstIndex = 0;
450 this._lastIndex = 0;
451 this._topHeight = 0;
452 this._bottomHeight = 0;
453 this._renderedHeight = 0;
454 this._topElement.style.height = '0px';
alph 2016/12/27 23:38:33 '0'
dgozman 2016/12/28 06:15:21 Done.
455 this._bottomElement.style.height = '0px';
456 return;
457 }
458
459 var firstIndex = this._indexAtOffset(Math.max(0, scrollTop - height));
460 var lastIndex = this._indexAtOffset(Math.min(totalHeight, scrollTop + 2 * he ight)) + 1;
461
462 for (var index = this._firstIndex; index < firstIndex; index++) {
alph 2016/12/27 23:38:32 while (this._firstIndex < Math.min(firstIndex, thi
dgozman 2016/12/28 06:15:21 Done.
463 var element = this._elementAtIndex(index);
464 element.remove();
465 this._firstIndex++;
466 }
467 for (var index = this._lastIndex - 1; index >= lastIndex; index--) {
alph 2016/12/27 23:38:33 ditto
dgozman 2016/12/28 06:15:20 Done.
468 var element = this._elementAtIndex(index);
469 element.remove();
470 this._lastIndex--;
471 }
472 this._firstIndex = Math.min(this._firstIndex, lastIndex);
473 this._lastIndex = Math.max(this._lastIndex, firstIndex);
474 for (var index = this._firstIndex - 1; index >= firstIndex; index--) {
475 var element = this._elementAtIndex(index);
476 this.element.insertBefore(element, this._topElement.nextSibling);
477 }
478 for (var index = this._lastIndex; index < lastIndex; index++) {
479 var element = this._elementAtIndex(index);
480 this.element.insertBefore(element, this._bottomElement);
481 }
482
483 this._firstIndex = firstIndex;
484 this._lastIndex = lastIndex;
485 this._topHeight = this._offsetAtIndex(firstIndex);
486 this._topElement.style.height = this._topHeight + 'px';
487 this._bottomHeight = (totalHeight - this._offsetAtIndex(lastIndex));
alph 2016/12/27 23:38:33 drop ()
dgozman 2016/12/28 06:15:20 Done.
488 this._bottomElement.style.height = this._bottomHeight + 'px';
489 this._renderedHeight = totalHeight;
490 this.element.scrollTop = scrollTop;
491 }
492 };
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698