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