Web Inspector: REGRESSION(r238599): Multiple Selection: restoring selection when...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / TreeOutline.js
1 /*
2  * Copyright (C) 2007-2018 Apple Inc.  All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer.
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution.
13  * 3.  Neither the name of Apple Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission.
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 WI.TreeOutline = class TreeOutline extends WI.Object
30 {
31     constructor(selectable = true)
32     {
33         super();
34
35         this.element = document.createElement("ol");
36         this.element.classList.add(WI.TreeOutline.ElementStyleClassName);
37         this.element.addEventListener("contextmenu", this._handleContextmenu.bind(this));
38
39         this.children = [];
40         this._childrenListNode = this.element;
41         this._childrenListNode.removeChildren();
42         this._knownTreeElements = [];
43         this._treeElementsExpandedState = [];
44         this.allowsRepeatSelection = false;
45         this.root = true;
46         this.hasChildren = false;
47         this.expanded = true;
48         this.selected = false;
49         this.treeOutline = this;
50         this._hidden = false;
51         this._compact = false;
52         this._large = false;
53         this._disclosureButtons = true;
54         this._customIndent = false;
55         this._selectable = selectable;
56
57         this._cachedNumberOfDescendents = 0;
58         this._selectionController = new WI.SelectionController(this);
59
60         this._itemWasSelectedByUser = false;
61         this._processingSelectionChange = false;
62         this._suppressNextSelectionDidChangeEvent = false;
63
64         this._virtualizedVisibleTreeElements = null;
65         this._virtualizedAttachedTreeElements = null;
66         this._virtualizedScrollContainer = null;
67         this._virtualizedTreeItemHeight = NaN;
68         this._virtualizedTopSpacer = null;
69         this._virtualizedBottomSpacer = null;
70
71         this._childrenListNode.tabIndex = 0;
72         this._childrenListNode.addEventListener("keydown", this._treeKeyDown.bind(this), true);
73         this._childrenListNode.addEventListener("mousedown", this._handleMouseDown.bind(this));
74
75         WI.TreeOutline._generateStyleRulesIfNeeded();
76
77         if (!this._selectable)
78             this.element.classList.add("non-selectable");
79     }
80
81     // Public
82
83     get allowsEmptySelection()
84     {
85         return this._selectionController.allowsEmptySelection;
86     }
87
88     set allowsEmptySelection(flag)
89     {
90         this._selectionController.allowsEmptySelection = flag;
91     }
92
93     get allowsMultipleSelection()
94     {
95         return this._selectionController.allowsMultipleSelection;
96     }
97
98     set allowsMultipleSelection(flag)
99     {
100         this._selectionController.allowsMultipleSelection = flag;
101     }
102
103     get selectedTreeElement()
104     {
105         let selectedIndex = this._selectionController.lastSelectedItem;
106         return this._treeElementAtIndex(selectedIndex) || null;
107     }
108
109     set selectedTreeElement(treeElement)
110     {
111         if (treeElement) {
112             let index = this._indexOfTreeElement(treeElement);
113             this._selectionController.selectItem(index);
114         } else
115             this._selectionController.deselectAll();
116     }
117
118     get selectedTreeElements()
119     {
120         if (this.allowsMultipleSelection) {
121             let treeElements = [];
122             for (let index of this._selectionController.selectedItems)
123                 treeElements.push(this._treeElementAtIndex(index));
124             return treeElements;
125         }
126
127         let selectedTreeElement = this.selectedTreeElement;
128         if (selectedTreeElement)
129             return [selectedTreeElement];
130
131         return [];
132     }
133
134     get processingSelectionChange() { return this._processingSelectionChange; }
135
136     get hidden()
137     {
138         return this._hidden;
139     }
140
141     set hidden(x)
142     {
143         if (this._hidden === x)
144             return;
145
146         this._hidden = x;
147         this.element.hidden = this._hidden;
148     }
149
150     get compact()
151     {
152         return this._compact;
153     }
154
155     set compact(x)
156     {
157         if (this._compact === x)
158             return;
159
160         this._compact = x;
161         if (this._compact)
162             this.large = false;
163
164         this.element.classList.toggle("compact", this._compact);
165     }
166
167     get large()
168     {
169         return this._large;
170     }
171
172     set large(x)
173     {
174         if (this._large === x)
175             return;
176
177         this._large = x;
178         if (this._large)
179             this.compact = false;
180
181         this.element.classList.toggle("large", this._large);
182     }
183
184     get disclosureButtons()
185     {
186         return this._disclosureButtons;
187     }
188
189     set disclosureButtons(x)
190     {
191         if (this._disclosureButtons === x)
192             return;
193
194         this._disclosureButtons = x;
195         this.element.classList.toggle("hide-disclosure-buttons", !this._disclosureButtons);
196     }
197
198     get customIndent()
199     {
200         return this._customIndent;
201     }
202
203     set customIndent(x)
204     {
205         if (this._customIndent === x)
206             return;
207
208         this._customIndent = x;
209         this.element.classList.toggle(WI.TreeOutline.CustomIndentStyleClassName, this._customIndent);
210     }
211
212     get selectable() { return this._selectable; }
213
214     appendChild(child)
215     {
216         console.assert(child);
217         if (!child)
218             return;
219
220         var lastChild = this.children[this.children.length - 1];
221         if (lastChild) {
222             lastChild.nextSibling = child;
223             child.previousSibling = lastChild;
224         } else {
225             child.previousSibling = null;
226             child.nextSibling = null;
227         }
228
229         var isFirstChild = !this.children.length;
230
231         this.children.push(child);
232         this.hasChildren = true;
233         child.parent = this;
234         child.treeOutline = this.treeOutline;
235         child.treeOutline._rememberTreeElement(child);
236
237         var current = child.children[0];
238         while (current) {
239             current.treeOutline = this.treeOutline;
240             current.treeOutline._rememberTreeElement(current);
241             current = current.traverseNextTreeElement(false, child, true);
242         }
243
244         if (child.hasChildren && child.treeOutline._treeElementsExpandedState[child.identifier] !== undefined)
245             child.expanded = child.treeOutline._treeElementsExpandedState[child.identifier];
246
247         if (this._childrenListNode)
248             child._attach();
249
250         if (this.treeOutline)
251             this.treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementAdded, {element: child});
252
253         if (isFirstChild && this.expanded)
254             this.expand();
255     }
256
257     insertChild(child, index)
258     {
259         console.assert(child);
260         if (!child)
261             return;
262
263         var previousChild = index > 0 ? this.children[index - 1] : null;
264         if (previousChild) {
265             previousChild.nextSibling = child;
266             child.previousSibling = previousChild;
267         } else {
268             child.previousSibling = null;
269         }
270
271         var nextChild = this.children[index];
272         if (nextChild) {
273             nextChild.previousSibling = child;
274             child.nextSibling = nextChild;
275         } else {
276             child.nextSibling = null;
277         }
278
279         var isFirstChild = !this.children.length;
280
281         this.children.splice(index, 0, child);
282         this.hasChildren = true;
283         child.parent = this;
284         child.treeOutline = this.treeOutline;
285         child.treeOutline._rememberTreeElement(child);
286
287         var current = child.children[0];
288         while (current) {
289             current.treeOutline = this.treeOutline;
290             current.treeOutline._rememberTreeElement(current);
291             current = current.traverseNextTreeElement(false, child, true);
292         }
293
294         if (child.hasChildren && child.treeOutline._treeElementsExpandedState[child.identifier] !== undefined)
295             child.expanded = child.treeOutline._treeElementsExpandedState[child.identifier];
296
297         // Update the SelectionController before attaching the TreeElement.
298         // Attaching the TreeElement can cause it to become selected, and
299         // added to the SelectionController.
300         let insertionIndex = this.treeOutline._indexOfTreeElement(child) || 0;
301         this.treeOutline._selectionController.didInsertItem(insertionIndex);
302
303         if (this._childrenListNode)
304             child._attach();
305
306         if (this.treeOutline)
307             this.treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementAdded, {element: child});
308
309         if (isFirstChild && this.expanded)
310             this.expand();
311     }
312
313     removeChildAtIndex(childIndex, suppressOnDeselect, suppressSelectSibling)
314     {
315         console.assert(childIndex >= 0 && childIndex < this.children.length);
316         if (childIndex < 0 || childIndex >= this.children.length)
317             return;
318
319         let child = this.children[childIndex];
320         this.children.splice(childIndex, 1);
321
322         let parent = child.parent;
323         if (child.deselect(suppressOnDeselect)) {
324             if (child.previousSibling && !suppressSelectSibling)
325                 child.previousSibling.select(true, false);
326             else if (child.nextSibling && !suppressSelectSibling)
327                 child.nextSibling.select(true, false);
328             else if (!suppressSelectSibling)
329                 parent.select(true, false);
330         }
331
332         if (child.previousSibling)
333             child.previousSibling.nextSibling = child.nextSibling;
334         if (child.nextSibling)
335             child.nextSibling.previousSibling = child.previousSibling;
336
337         let treeOutline = child.treeOutline;
338         if (treeOutline) {
339             treeOutline._forgetTreeElement(child);
340             treeOutline._forgetChildrenRecursive(child);
341             treeOutline._selectionController.didRemoveItem(childIndex);
342         }
343
344         child._detach();
345         child.treeOutline = null;
346         child.parent = null;
347         child.nextSibling = null;
348         child.previousSibling = null;
349
350         if (treeOutline)
351             treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementRemoved, {element: child});
352     }
353
354     removeChild(child, suppressOnDeselect, suppressSelectSibling)
355     {
356         console.assert(child);
357         if (!child)
358             return;
359
360         var childIndex = this.children.indexOf(child);
361         console.assert(childIndex !== -1);
362         if (childIndex === -1)
363             return;
364
365         this.removeChildAtIndex(childIndex, suppressOnDeselect, suppressSelectSibling);
366
367         if (!this.children.length) {
368             if (this._listItemNode)
369                 this._listItemNode.classList.remove("parent");
370             this.hasChildren = false;
371         }
372     }
373
374     removeChildren(suppressOnDeselect)
375     {
376         for (let child of this.children) {
377             child.deselect(suppressOnDeselect);
378
379             let treeOutline = child.treeOutline;
380             if (treeOutline) {
381                 treeOutline._forgetTreeElement(child);
382                 treeOutline._forgetChildrenRecursive(child);
383             }
384
385             child._detach();
386             child.treeOutline = null;
387             child.parent = null;
388             child.nextSibling = null;
389             child.previousSibling = null;
390
391             if (treeOutline)
392                 treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementRemoved, {element: child});
393         }
394
395         this.children = [];
396     }
397
398     _rememberTreeElement(element)
399     {
400         if (!this._knownTreeElements[element.identifier])
401             this._knownTreeElements[element.identifier] = [];
402
403         // check if the element is already known
404         var elements = this._knownTreeElements[element.identifier];
405         if (elements.indexOf(element) !== -1)
406             return;
407
408         // add the element
409         elements.push(element);
410         this._cachedNumberOfDescendents++;
411     }
412
413     _forgetTreeElement(element)
414     {
415         if (this.selectedTreeElement === element) {
416             element.deselect(true);
417             this.selectedTreeElement = null;
418         }
419         if (this._knownTreeElements[element.identifier]) {
420             this._knownTreeElements[element.identifier].remove(element, true);
421             this._cachedNumberOfDescendents--;
422         }
423     }
424
425     _forgetChildrenRecursive(parentElement)
426     {
427         var child = parentElement.children[0];
428         while (child) {
429             this._forgetTreeElement(child);
430             child = child.traverseNextTreeElement(false, parentElement, true);
431         }
432     }
433
434     getCachedTreeElement(representedObject)
435     {
436         if (!representedObject)
437             return null;
438
439         if (representedObject.__treeElementIdentifier) {
440             // If this representedObject has a tree element identifier, and it is a known TreeElement
441             // in our tree we can just return that tree element.
442             var elements = this._knownTreeElements[representedObject.__treeElementIdentifier];
443             if (elements) {
444                 for (var i = 0; i < elements.length; ++i)
445                     if (elements[i].representedObject === representedObject)
446                         return elements[i];
447             }
448         }
449         return null;
450     }
451
452     selfOrDescendant(predicate)
453     {
454         let treeElements = [this];
455         while (treeElements.length) {
456             let treeElement = treeElements.shift();
457             if (predicate(treeElement))
458                 return treeElement;
459
460             treeElements = treeElements.concat(treeElement.children);
461         }
462
463         return false;
464     }
465
466     findTreeElement(representedObject, isAncestor, getParent)
467     {
468         if (!representedObject)
469             return null;
470
471         var cachedElement = this.getCachedTreeElement(representedObject);
472         if (cachedElement)
473             return cachedElement;
474
475         // The representedObject isn't known, so we start at the top of the tree and work down to find the first
476         // tree element that represents representedObject or one of its ancestors.
477         var item;
478         var found = false;
479         for (var i = 0; i < this.children.length; ++i) {
480             item = this.children[i];
481             if (item.representedObject === representedObject || (isAncestor && isAncestor(item.representedObject, representedObject))) {
482                 found = true;
483                 break;
484             }
485         }
486
487         if (!found)
488             return null;
489
490         // Make sure the item that we found is connected to the root of the tree.
491         // Build up a list of representedObject's ancestors that aren't already in our tree.
492         var ancestors = [];
493         var currentObject = representedObject;
494         while (currentObject) {
495             ancestors.unshift(currentObject);
496             if (currentObject === item.representedObject)
497                 break;
498             currentObject = getParent(currentObject);
499         }
500
501         // For each of those ancestors we populate them to fill in the tree.
502         for (var i = 0; i < ancestors.length; ++i) {
503             // Make sure we don't call findTreeElement with the same representedObject
504             // again, to prevent infinite recursion.
505             if (ancestors[i] === representedObject)
506                 continue;
507
508             // FIXME: we could do something faster than findTreeElement since we will know the next
509             // ancestor exists in the tree.
510             item = this.findTreeElement(ancestors[i], isAncestor, getParent);
511             if (item)
512                 item.onpopulate();
513         }
514
515         return this.getCachedTreeElement(representedObject);
516     }
517
518     _treeElementDidChange(treeElement)
519     {
520         if (treeElement.treeOutline !== this)
521             return;
522
523         this.dispatchEventToListeners(WI.TreeOutline.Event.ElementDidChange, {element: treeElement});
524     }
525
526     treeElementFromNode(node)
527     {
528         var listNode = node.enclosingNodeOrSelfWithNodeNameInArray(["ol", "li"]);
529         if (listNode)
530             return listNode.parentTreeElement || listNode.treeElement;
531         return null;
532     }
533
534     treeElementFromPoint(x, y)
535     {
536         var node = this._childrenListNode.ownerDocument.elementFromPoint(x, y);
537         if (!node)
538             return null;
539
540         return this.treeElementFromNode(node);
541     }
542
543     _treeKeyDown(event)
544     {
545         if (event.target !== this._childrenListNode)
546             return;
547
548         if (!this.selectedTreeElement || event.commandOrControlKey)
549             return;
550
551         let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL;
552
553         var handled = false;
554         var nextSelectedElement;
555
556         if ((!isRTL && event.keyIdentifier === "Left") || (isRTL && event.keyIdentifier === "Right")) {
557             if (this.selectedTreeElement.expanded) {
558                 if (event.altKey)
559                     this.selectedTreeElement.collapseRecursively();
560                 else
561                     this.selectedTreeElement.collapse();
562                 handled = true;
563             } else if (this.selectedTreeElement.parent && !this.selectedTreeElement.parent.root) {
564                 handled = true;
565                 if (this.selectedTreeElement.parent.selectable) {
566                     nextSelectedElement = this.selectedTreeElement.parent;
567                     while (nextSelectedElement && !nextSelectedElement.selectable)
568                         nextSelectedElement = nextSelectedElement.parent;
569                     handled = nextSelectedElement ? true : false;
570                 } else if (this.selectedTreeElement.parent)
571                     this.selectedTreeElement.parent.collapse();
572             }
573         } else if ((!isRTL && event.keyIdentifier === "Right") || (isRTL && event.keyIdentifier === "Left")) {
574             if (!this.selectedTreeElement.revealed()) {
575                 this.selectedTreeElement.reveal();
576                 handled = true;
577             } else if (this.selectedTreeElement.hasChildren) {
578                 handled = true;
579                 if (this.selectedTreeElement.expanded) {
580                     nextSelectedElement = this.selectedTreeElement.children[0];
581                     while (nextSelectedElement && !nextSelectedElement.selectable)
582                         nextSelectedElement = nextSelectedElement.nextSibling;
583                     handled = nextSelectedElement ? true : false;
584                 } else {
585                     if (event.altKey)
586                         this.selectedTreeElement.expandRecursively();
587                     else
588                         this.selectedTreeElement.expand();
589                 }
590             }
591         } else if (event.keyCode === 8 /* Backspace */ || event.keyCode === 46 /* Delete */) {
592             for (let treeElement of this.selectedTreeElements) {
593                 if (treeElement.ondelete && treeElement.ondelete())
594                     handled = true;
595             }
596             if (!handled && this.treeOutline.ondelete)
597                 handled = this.treeOutline.ondelete(this.selectedTreeElement);
598         } else if (isEnterKey(event)) {
599             if (this.selectedTreeElement.onenter)
600                 handled = this.selectedTreeElement.onenter();
601             if (!handled && this.treeOutline.onenter)
602                 handled = this.treeOutline.onenter(this.selectedTreeElement);
603         } else if (event.keyIdentifier === "U+0020" /* Space */) {
604             if (this.selectedTreeElement.onspace)
605                 handled = this.selectedTreeElement.onspace();
606             if (!handled && this.treeOutline.onspace)
607                 handled = this.treeOutline.onspace(this.selectedTreeElement);
608         }
609
610         if (!handled)
611             handled = this._selectionController.handleKeyDown(event);
612
613         if (nextSelectedElement) {
614             nextSelectedElement.reveal();
615             nextSelectedElement.select(false, true);
616         }
617
618         if (handled) {
619             event.preventDefault();
620             event.stopPropagation();
621         }
622     }
623
624     expand()
625     {
626         // this is the root, do nothing
627     }
628
629     collapse()
630     {
631         // this is the root, do nothing
632     }
633
634     revealed()
635     {
636         return true;
637     }
638
639     reveal()
640     {
641         // this is the root, do nothing
642     }
643
644     select()
645     {
646         // this is the root, do nothing
647     }
648
649     revealAndSelect(omitFocus)
650     {
651         // this is the root, do nothing
652     }
653
654     get selectedTreeElementIndex()
655     {
656         if (!this.hasChildren || !this.selectedTreeElement)
657             return;
658
659         for (var i = 0; i < this.children.length; ++i) {
660             if (this.children[i] === this.selectedTreeElement)
661                 return i;
662         }
663
664         return false;
665     }
666
667     get virtualized()
668     {
669         return this._virtualizedScrollContainer && !isNaN(this._virtualizedTreeItemHeight);
670     }
671
672     registerScrollVirtualizer(scrollContainer, treeItemHeight)
673     {
674         console.assert(!isNaN(treeItemHeight));
675
676         this._virtualizedVisibleTreeElements = new Set;
677         this._virtualizedAttachedTreeElements = new Set;
678         this._virtualizedScrollContainer = scrollContainer;
679         this._virtualizedTreeItemHeight = treeItemHeight;
680         this._virtualizedTopSpacer = document.createElement("div");
681         this._virtualizedBottomSpacer = document.createElement("div");
682
683         let throttler = this.throttle(1000 / 16);
684         this._virtualizedScrollContainer.addEventListener("scroll", (event) => {
685             throttler.updateVirtualizedElements();
686         });
687     }
688
689     updateVirtualizedElements(focusedTreeElement)
690     {
691         if (!this.virtualized)
692             return;
693
694         function walk(parent, callback, count = 0) {
695             let shouldReturn = false;
696             for (let child of parent.children) {
697                 if (!child.revealed(false))
698                     continue;
699
700                 shouldReturn = callback(child, count);
701                 if (shouldReturn)
702                     break;
703
704                 ++count;
705                 if (child.expanded) {
706                     let result = walk(child, callback, count);
707                     count = result.count;
708                     if (result.shouldReturn)
709                         break;
710                 }
711             }
712             return {count, shouldReturn};
713         }
714
715         let {numberVisible, extraRows, firstItem, lastItem} = this._calculateVirtualizedValues();
716
717         let shouldScroll = false;
718         if (focusedTreeElement && focusedTreeElement.revealed(false)) {
719             let index = walk(this, (treeElement) => treeElement === focusedTreeElement).count;
720             if (index < firstItem) {
721                 firstItem = index - extraRows;
722                 lastItem = index + numberVisible + extraRows;
723             } else if (index > lastItem) {
724                 firstItem = index - numberVisible - extraRows;
725                 lastItem = index + extraRows;
726             }
727
728             // Only scroll if the `focusedTreeElement` is outside the visible items, not including
729             // the added buffer `extraRows`.
730             shouldScroll = (index < firstItem + extraRows) || (index > lastItem - extraRows);
731         }
732
733         console.assert(firstItem < lastItem);
734
735         let visibleTreeElements = new Set;
736         let treeElementsToAttach = new Set;
737         let treeElementsToDetach = new Set;
738         let totalItems = walk(this, (treeElement, count) => {
739             if (count >= firstItem && count <= lastItem) {
740                 treeElementsToAttach.add(treeElement);
741                 if (count >= firstItem + extraRows && count <= lastItem - extraRows)
742                     visibleTreeElements.add(treeElement);
743             } else if (treeElement.element.parentNode)
744                 treeElementsToDetach.add(treeElement);
745
746             return false;
747         }).count;
748
749         // Redraw if we are about to scroll.
750         if (!shouldScroll) {
751             // Redraw if all of the previously centered `WI.TreeElement` are no longer centered.
752             if (visibleTreeElements.intersects(this._virtualizedVisibleTreeElements)) {
753                 // Redraw if there is a `WI.TreeElement` that should be shown that isn't attached.
754                 if (visibleTreeElements.isSubsetOf(this._virtualizedAttachedTreeElements))
755                     return;
756             }
757         }
758
759         this._virtualizedVisibleTreeElements = visibleTreeElements;
760         this._virtualizedAttachedTreeElements = treeElementsToAttach;
761
762         for (let treeElement of treeElementsToDetach)
763             treeElement.element.remove();
764
765         for (let treeElement of treeElementsToAttach) {
766             treeElement.parent._childrenListNode.appendChild(treeElement.element);
767             if (treeElement._childrenListNode)
768                 treeElement.parent._childrenListNode.appendChild(treeElement._childrenListNode);
769         }
770
771         this._virtualizedTopSpacer.style.height = (Math.max(firstItem, 0) * this._virtualizedTreeItemHeight) + "px";
772         if (this.element.previousElementSibling !== this._virtualizedTopSpacer)
773             this.element.parentNode.insertBefore(this._virtualizedTopSpacer, this.element);
774
775         this._virtualizedBottomSpacer.style.height = (Math.max(totalItems - lastItem, 0) * this._virtualizedTreeItemHeight) + "px";
776         if (this.element.nextElementSibling !== this._virtualizedBottomSpacer)
777             this.element.parentNode.insertBefore(this._virtualizedBottomSpacer, this.element.nextElementSibling);
778
779         if (shouldScroll)
780             this._virtualizedScrollContainer.scrollTop = (firstItem + extraRows) * this._virtualizedTreeItemHeight;
781     }
782
783     // SelectionController delegate
784
785     selectionControllerNumberOfItems(controller)
786     {
787         return this._cachedNumberOfDescendents;
788     }
789
790     selectionControllerSelectionDidChange(controller, deselectedItems, selectedItems)
791     {
792         this._processingSelectionChange = true;
793
794         for (let index of deselectedItems) {
795             let treeElement = this._treeElementAtIndex(index);
796             console.assert(treeElement, "Missing TreeElement for deselected index " + index);
797             if (treeElement) {
798                 treeElement.listItemElement.classList.remove("selected");
799                 if (!this._suppressNextSelectionDidChangeEvent)
800                     treeElement.deselect();
801             }
802         }
803
804         for (let index of selectedItems) {
805             let treeElement = this._treeElementAtIndex(index);
806             console.assert(treeElement, "Missing TreeElement for selected index " + index);
807             if (treeElement) {
808                 treeElement.listItemElement.classList.add("selected");
809                 if (!this._suppressNextSelectionDidChangeEvent)
810                     treeElement.select();
811             }
812         }
813
814         this._dispatchSelectionDidChangeEvent();
815
816         this._processingSelectionChange = false;
817     }
818
819     selectionControllerNextSelectableIndex(controller, index)
820     {
821         let treeElement = this._treeElementAtIndex(index);
822         if (!treeElement)
823             return NaN;
824
825         const skipUnrevealed = true;
826         const stayWithin = null;
827         const dontPopulate = true;
828
829         while (treeElement = treeElement.traverseNextTreeElement(skipUnrevealed, stayWithin, dontPopulate)) {
830             if (treeElement.selectable)
831                 return this._indexOfTreeElement(treeElement);
832         }
833
834         return NaN;
835     }
836
837     selectionControllerPreviousSelectableIndex(controller, index)
838     {
839         let treeElement = this._treeElementAtIndex(index);
840         if (!treeElement)
841             return NaN;
842
843         const skipUnrevealed = true;
844         const stayWithin = null;
845         const dontPopulate = true;
846
847         while (treeElement = treeElement.traversePreviousTreeElement(skipUnrevealed, stayWithin, dontPopulate)) {
848             if (treeElement.selectable)
849                 return this._indexOfTreeElement(treeElement);
850         }
851
852         return NaN;
853     }
854
855     // Protected
856
857     selectTreeElementInternal(treeElement, suppressNotification = false, selectedByUser = false)
858     {
859         if (this._processingSelectionChange)
860             return;
861
862         this._itemWasSelectedByUser = selectedByUser;
863         this._suppressNextSelectionDidChangeEvent = suppressNotification;
864
865         if (this.allowsRepeatSelection && this.selectedTreeElement === treeElement) {
866             this._dispatchSelectionDidChangeEvent();
867             return;
868         }
869
870         this.selectedTreeElement = treeElement;
871     }
872
873     treeElementFromEvent(event)
874     {
875         let scrollContainer = this.element.parentElement;
876
877         // We choose this X coordinate based on the knowledge that our list
878         // items extend at least to the trailing edge of the outer <ol> container.
879         // In the no-word-wrap mode the outer <ol> may be wider than the tree container
880         // (and partially hidden), in which case we are left to use only its trailing boundary.
881         // This adjustment is useful in order to find the inner-most tree element that
882         // lines up horizontally with the location of the event. If the mouse event
883         // happened in the space preceding a nested tree element (in the leading indentated
884         // space) we use this adjustment to get the nested tree element and not a tree element
885         // from a parent / outer tree outline / tree element.
886         //
887         // NOTE: This can fail if there is floating content over the trailing edge of
888         // the <li> content, since the element from point could hit that.
889         let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL;
890         let trailingEdgeOffset = isRTL ? 36 : (scrollContainer.offsetWidth - 36);
891         let x = scrollContainer.totalOffsetLeft + trailingEdgeOffset;
892         let y = event.pageY;
893
894         // Our list items have 1-pixel cracks between them vertically. We avoid
895         // the cracks by checking slightly above and slightly below the mouse
896         // and seeing if we hit the same element each time.
897         let elementUnderMouse = this.treeElementFromPoint(x, y);
898         let elementAboveMouse = this.treeElementFromPoint(x, y - 2);
899         let element = null;
900         if (elementUnderMouse === elementAboveMouse)
901             element = elementUnderMouse;
902         else
903             element = this.treeElementFromPoint(x, y + 2);
904
905         return element;
906     }
907
908     populateContextMenu(contextMenu, event, treeElement)
909     {
910         treeElement.populateContextMenu(contextMenu, event);
911     }
912
913     // Private
914
915     static _generateStyleRulesIfNeeded()
916     {
917         if (WI.TreeOutline._styleElement)
918            return;
919
920         WI.TreeOutline._styleElement = document.createElement("style");
921
922         let maximumTreeDepth = 32;
923         let baseLeftPadding = 5; // Matches the padding in TreeOutline.css for the item class. Keep in sync.
924         let depthPadding = 10;
925
926         let styleText = "";
927         let childrenSubstring = "";
928         for (let i = 1; i <= maximumTreeDepth; ++i) {
929             // Keep all the elements at the same depth once the maximum is reached.
930             childrenSubstring += i === maximumTreeDepth ? " .children" : " > .children";
931             styleText += `.${WI.TreeOutline.ElementStyleClassName}:not(.${WI.TreeOutline.CustomIndentStyleClassName})${childrenSubstring} > .item { `;
932
933             if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL)
934                 styleText += "padding-right: ";
935             else
936                 styleText += "padding-left: ";
937
938             styleText += (baseLeftPadding + (depthPadding * i)) + "px; }\n";
939         }
940
941         WI.TreeOutline._styleElement.textContent = styleText;
942
943         document.head.appendChild(WI.TreeOutline._styleElement);
944     }
945
946     _calculateVirtualizedValues()
947     {
948         let numberVisible = Math.ceil(this._virtualizedScrollContainer.offsetHeight / this._virtualizedTreeItemHeight);
949         let extraRows = Math.max(numberVisible * 5, 50);
950         let firstItem = Math.floor(this._virtualizedScrollContainer.scrollTop / this._virtualizedTreeItemHeight) - extraRows;
951         let lastItem = firstItem + numberVisible + (extraRows * 2);
952         return {
953             numberVisible,
954             extraRows,
955             firstItem,
956             lastItem,
957         };
958     }
959
960     _handleContextmenu(event)
961     {
962         let treeElement = this.treeElementFromEvent(event);
963         if (!treeElement)
964             return;
965
966         let contextMenu = WI.ContextMenu.createFromEvent(event);
967         this.populateContextMenu(contextMenu, event, treeElement);
968     }
969
970     _handleMouseDown(event)
971     {
972         let treeElement = this.treeElementFromEvent(event);
973         if (!treeElement || !treeElement.selectable)
974             return;
975
976         if (treeElement.isEventWithinDisclosureTriangle(event)) {
977             event.preventDefault();
978             return;
979         }
980
981         if (!treeElement.canSelectOnMouseDown(event)) {
982             event.preventDefault();
983             return;
984         }
985
986         let index = this._indexOfTreeElement(treeElement);
987         if (isNaN(index))
988             return;
989
990         this._selectionController.handleItemMouseDown(index, event);
991     }
992
993     _indexOfTreeElement(treeElement)
994     {
995         const skipUnrevealed = false;
996         const stayWithin = null;
997         const dontPopulate = true;
998
999         let index = 0;
1000         let current = this.children[0];
1001         while (current) {
1002             if (treeElement === current)
1003                 return index;
1004
1005             current = current.traverseNextTreeElement(skipUnrevealed, stayWithin, dontPopulate);
1006             ++index;
1007         }
1008
1009         console.assert(false, "Unable to get index for tree element.", treeElement, treeOutline);
1010         return NaN;
1011     }
1012
1013     _treeElementAtIndex(index)
1014     {
1015         const skipUnrevealed = false;
1016         const stayWithin = null;
1017         const dontPopulate = true;
1018
1019         let current = 0;
1020         let treeElement = this.children[0];
1021         while (treeElement) {
1022             if (current === index)
1023                 return treeElement;
1024
1025             treeElement = treeElement.traverseNextTreeElement(skipUnrevealed, stayWithin, dontPopulate);
1026             ++current;
1027         }
1028
1029         return null;
1030     }
1031
1032     _dispatchSelectionDidChangeEvent()
1033     {
1034         let selectedByUser = this._itemWasSelectedByUser;
1035         this._itemWasSelectedByUser = false;
1036
1037         if (this._suppressNextSelectionDidChangeEvent) {
1038             this._suppressNextSelectionDidChangeEvent = false;
1039             return;
1040         }
1041
1042         this.dispatchEventToListeners(WI.TreeOutline.Event.SelectionDidChange, {selectedByUser});
1043     }
1044 };
1045
1046 WI.TreeOutline._styleElement = null;
1047
1048 WI.TreeOutline.ElementStyleClassName = "tree-outline";
1049 WI.TreeOutline.CustomIndentStyleClassName = "custom-indent";
1050
1051 WI.TreeOutline.Event = {
1052     ElementAdded: Symbol("element-added"),
1053     ElementDidChange: Symbol("element-did-change"),
1054     ElementRemoved: Symbol("element-removed"),
1055     ElementClicked: Symbol("element-clicked"),
1056     ElementDisclosureDidChanged: Symbol("element-disclosure-did-change"),
1057     ElementVisibilityDidChange: Symbol("element-visbility-did-change"),
1058     SelectionDidChange: Symbol("selection-did-change")
1059 };
1060
1061 WI.TreeOutline._knownTreeElementNextIdentifier = 1;