88304c8634238c907f47c590ee10a04fd982a6e8
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / DataGridNode.js
1 /*
2  * Copyright (C) 2008, 2013-2017 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  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.DataGridNode = class DataGridNode extends WebInspector.Object
27 {
28     constructor(data, hasChildren)
29     {
30         super();
31
32         this._expanded = false;
33         this._hidden = false;
34         this._selected = false;
35         this._copyable = true;
36         this._shouldRefreshChildren = true;
37         this._data = data || {};
38         this.hasChildren = hasChildren || false;
39         this.children = [];
40         this.dataGrid = null;
41         this.parent = null;
42         this.previousSibling = null;
43         this.nextSibling = null;
44         this.disclosureToggleWidth = 10;
45     }
46
47     get hidden()
48     {
49         return this._hidden;
50     }
51
52     set hidden(x)
53     {
54         x = !!x;
55
56         if (this._hidden === x)
57             return;
58
59         this._hidden = x;
60         if (this._element)
61             this._element.classList.toggle("hidden", this._hidden);
62
63         if (this.dataGrid)
64             this.dataGrid._noteRowsChanged();
65     }
66
67     get selectable()
68     {
69         return this._element && !this._hidden;
70     }
71
72     get copyable()
73     {
74         return this._copyable;
75     }
76
77     set copyable(x)
78     {
79         this._copyable = x;
80     }
81
82     get element()
83     {
84         if (this._element)
85             return this._element;
86
87         if (!this.dataGrid)
88             return null;
89
90         this._element = document.createElement("tr");
91         this._element._dataGridNode = this;
92
93         if (this.hasChildren)
94             this._element.classList.add("parent");
95         if (this.expanded)
96             this._element.classList.add("expanded");
97         if (this.selected)
98             this._element.classList.add("selected");
99         if (this.revealed)
100             this._element.classList.add("revealed");
101         if (this._hidden)
102             this._element.classList.add("hidden");
103
104         this.createCells();
105         return this._element;
106     }
107
108     createCells()
109     {
110         for (var columnIdentifier of this.dataGrid.orderedColumns)
111             this._element.appendChild(this.createCell(columnIdentifier));
112     }
113
114     refreshIfNeeded()
115     {
116         if (!this._needsRefresh)
117             return;
118
119         this._needsRefresh = false;
120
121         this.refresh();
122     }
123
124     needsRefresh()
125     {
126         this._needsRefresh = true;
127
128         if (!this._revealed)
129             return;
130
131         if (this._scheduledRefreshIdentifier)
132             return;
133
134         this._scheduledRefreshIdentifier = requestAnimationFrame(this.refresh.bind(this));
135     }
136
137     get data()
138     {
139         return this._data;
140     }
141
142     set data(x)
143     {
144         console.assert(typeof x === "object", "Data should be an object.");
145
146         x = x || {};
147
148         if (Object.shallowEqual(this._data, x))
149             return;
150
151         this._data = x;
152         this.needsRefresh();
153     }
154
155     get filterableData()
156     {
157         if (this._cachedFilterableData)
158             return this._cachedFilterableData;
159
160         this._cachedFilterableData = [];
161
162         for (let column of this.dataGrid.columns.values()) {
163             if (column.hidden)
164                 continue;
165
166             let value = this.filterableDataForColumn(column.columnIdentifier);
167             if (!value)
168                 continue;
169
170             if (!(value instanceof Array))
171                 value = [value];
172
173             if (!value.length)
174                 continue;
175
176             this._cachedFilterableData = this._cachedFilterableData.concat(value);
177         }
178
179         return this._cachedFilterableData;
180     }
181
182     get revealed()
183     {
184         if ("_revealed" in this)
185             return this._revealed;
186
187         var currentAncestor = this.parent;
188         while (currentAncestor && !currentAncestor.root) {
189             if (!currentAncestor.expanded) {
190                 this._revealed = false;
191                 return false;
192             }
193
194             currentAncestor = currentAncestor.parent;
195         }
196
197         this._revealed = true;
198         return true;
199     }
200
201     set hasChildren(x)
202     {
203         if (this._hasChildren === x)
204             return;
205
206         this._hasChildren = x;
207
208         if (!this._element)
209             return;
210
211         if (this._hasChildren) {
212             this._element.classList.add("parent");
213             if (this.expanded)
214                 this._element.classList.add("expanded");
215         } else
216             this._element.classList.remove("parent", "expanded");
217     }
218
219     get hasChildren()
220     {
221         return this._hasChildren;
222     }
223
224     set revealed(x)
225     {
226         if (this._revealed === x)
227             return;
228
229         this._revealed = x;
230
231         if (this._element) {
232             if (this._revealed)
233                 this._element.classList.add("revealed");
234             else
235                 this._element.classList.remove("revealed");
236         }
237
238         this.refreshIfNeeded();
239
240         for (var i = 0; i < this.children.length; ++i)
241             this.children[i].revealed = x && this.expanded;
242     }
243
244     get depth()
245     {
246         if ("_depth" in this)
247             return this._depth;
248         if (this.parent && !this.parent.root)
249             this._depth = this.parent.depth + 1;
250         else
251             this._depth = 0;
252         return this._depth;
253     }
254
255     get indentPadding()
256     {
257         if (typeof this._indentPadding === "number")
258             return this._indentPadding;
259
260         this._indentPadding = this.depth * this.dataGrid.indentWidth;
261         return this._indentPadding;
262     }
263
264     get shouldRefreshChildren()
265     {
266         return this._shouldRefreshChildren;
267     }
268
269     set shouldRefreshChildren(x)
270     {
271         this._shouldRefreshChildren = x;
272         if (x && this.expanded)
273             this.expand();
274     }
275
276     get selected()
277     {
278         return this._selected;
279     }
280
281     set selected(x)
282     {
283         if (x)
284             this.select();
285         else
286             this.deselect();
287     }
288
289     get expanded()
290     {
291         return this._expanded;
292     }
293
294     set expanded(x)
295     {
296         if (x)
297             this.expand();
298         else
299             this.collapse();
300     }
301
302     hasAncestor(ancestor)
303     {
304         if (!ancestor)
305             return false;
306
307         let currentAncestor = this.parent;
308         while (currentAncestor) {
309             if (ancestor === currentAncestor)
310                 return true;
311
312             currentAncestor = currentAncestor.parent;
313         }
314
315         return false;
316     }
317
318     refresh()
319     {
320         if (!this._element || !this.dataGrid)
321             return;
322
323         if (this._scheduledRefreshIdentifier) {
324             cancelAnimationFrame(this._scheduledRefreshIdentifier);
325             this._scheduledRefreshIdentifier = undefined;
326         }
327
328         this._cachedFilterableData = null;
329         this._needsRefresh = false;
330
331         this._element.removeChildren();
332         this.createCells();
333     }
334
335     refreshRecursively()
336     {
337         this.refresh();
338         this.forEachChildInSubtree((node) => node.refresh());
339     }
340
341     updateLayout()
342     {
343         // Implemented by subclasses if needed.
344     }
345
346     createCell(columnIdentifier)
347     {
348         var cellElement = document.createElement("td");
349         cellElement.className = columnIdentifier + "-column";
350         cellElement.__columnIdentifier = columnIdentifier;
351
352         var div = cellElement.createChild("div", "cell-content");
353         var content = this.createCellContent(columnIdentifier, cellElement);
354         div.append(content);
355
356         let column = this.dataGrid.columns.get(columnIdentifier);
357         if (column) {
358             if (column["aligned"])
359                 cellElement.classList.add(column["aligned"]);
360
361             if (column["group"])
362                 cellElement.classList.add("column-group-" + column["group"]);
363
364             if (column["icon"]) {
365                 let iconElement = document.createElement("div");
366                 iconElement.classList.add("icon");
367                 div.insertBefore(iconElement, div.firstChild);
368             }
369         }
370
371         if (columnIdentifier === this.dataGrid.disclosureColumnIdentifier) {
372             cellElement.classList.add("disclosure");
373             if (this.indentPadding) {
374                 if (WebInspector.resolvedLayoutDirection() === WebInspector.LayoutDirection.RTL)
375                     cellElement.style.setProperty("padding-right", `${this.indentPadding}px`);
376                 else
377                     cellElement.style.setProperty("padding-left", `${this.indentPadding}px`);
378             }
379         }
380
381         return cellElement;
382     }
383
384     createCellContent(columnIdentifier)
385     {
386         let data = this.data[columnIdentifier];
387         if (!data)
388             return zeroWidthSpace; // Zero width space to keep the cell from collapsing.
389
390         return (typeof data === "number") ? data.maxDecimals(2).toLocaleString() : data;
391     }
392
393     elementWithColumnIdentifier(columnIdentifier)
394     {
395         if (!this.dataGrid)
396             return null;
397
398         let index = this.dataGrid.orderedColumns.indexOf(columnIdentifier);
399         if (index === -1)
400             return null;
401
402         return this.element.children[index];
403     }
404
405     // Share these functions with DataGrid. They are written to work with a DataGridNode this object.
406     appendChild() { return WebInspector.DataGrid.prototype.appendChild.apply(this, arguments); }
407     insertChild() { return WebInspector.DataGrid.prototype.insertChild.apply(this, arguments); }
408     removeChild() { return WebInspector.DataGrid.prototype.removeChild.apply(this, arguments); }
409     removeChildren() { return WebInspector.DataGrid.prototype.removeChildren.apply(this, arguments); }
410     removeChildrenRecursive() { return WebInspector.DataGrid.prototype.removeChildrenRecursive.apply(this, arguments); }
411
412     _recalculateSiblings(myIndex)
413     {
414         if (!this.parent)
415             return;
416
417         var previousChild = myIndex > 0 ? this.parent.children[myIndex - 1] : null;
418
419         if (previousChild) {
420             previousChild.nextSibling = this;
421             this.previousSibling = previousChild;
422         } else
423             this.previousSibling = null;
424
425         var nextChild = this.parent.children[myIndex + 1];
426
427         if (nextChild) {
428             nextChild.previousSibling = this;
429             this.nextSibling = nextChild;
430         } else
431             this.nextSibling = null;
432     }
433
434     collapse()
435     {
436         if (this._element)
437             this._element.classList.remove("expanded");
438
439         this._expanded = false;
440
441         for (var i = 0; i < this.children.length; ++i)
442             this.children[i].revealed = false;
443
444         this.dispatchEventToListeners("collapsed");
445
446         if (this.dataGrid) {
447             this.dataGrid.dispatchEventToListeners(WebInspector.DataGrid.Event.CollapsedNode, {dataGridNode: this});
448             this.dataGrid._noteRowsChanged();
449         }
450     }
451
452     collapseRecursively()
453     {
454         var item = this;
455         while (item) {
456             if (item.expanded)
457                 item.collapse();
458             item = item.traverseNextNode(false, this, true);
459         }
460     }
461
462     expand()
463     {
464         if (!this.hasChildren || this.expanded)
465             return;
466
467         if (this.revealed && !this._shouldRefreshChildren)
468             for (var i = 0; i < this.children.length; ++i)
469                 this.children[i].revealed = true;
470
471         if (this._shouldRefreshChildren) {
472             for (var i = 0; i < this.children.length; ++i)
473                 this.children[i]._detach();
474
475             this.dispatchEventToListeners("populate");
476
477             if (this._attached) {
478                 for (var i = 0; i < this.children.length; ++i) {
479                     var child = this.children[i];
480                     if (this.revealed)
481                         child.revealed = true;
482                     child._attach();
483                 }
484             }
485
486             this._shouldRefreshChildren = false;
487         }
488
489         if (this._element)
490             this._element.classList.add("expanded");
491
492         this._expanded = true;
493
494         this.dispatchEventToListeners("expanded");
495
496         if (this.dataGrid) {
497             this.dataGrid.dispatchEventToListeners(WebInspector.DataGrid.Event.ExpandedNode, {dataGridNode: this});
498             this.dataGrid._noteRowsChanged();
499         }
500     }
501
502     expandRecursively()
503     {
504         var item = this;
505         while (item) {
506             item.expand();
507             item = item.traverseNextNode(false, this);
508         }
509     }
510
511     forEachImmediateChild(callback)
512     {
513         for (let node of this.children)
514             callback(node);
515     }
516
517     forEachChildInSubtree(callback)
518     {
519         let node = this.traverseNextNode(false, this, true);
520         while (node) {
521             callback(node);
522             node = node.traverseNextNode(false, this, true);
523         }
524     }
525
526     isInSubtreeOfNode(baseNode)
527     {
528         let node = baseNode;
529         while (node) {
530             if (node === this)
531                 return true;
532             node = node.traverseNextNode(false, baseNode, true);
533         }
534         return false;
535     }
536
537     reveal()
538     {
539         var currentAncestor = this.parent;
540         while (currentAncestor && !currentAncestor.root) {
541             if (!currentAncestor.expanded)
542                 currentAncestor.expand();
543             currentAncestor = currentAncestor.parent;
544         }
545
546         this.element.scrollIntoViewIfNeeded(false);
547
548         this.dispatchEventToListeners("revealed");
549     }
550
551     select(suppressSelectedEvent)
552     {
553         if (!this.dataGrid || !this.selectable || this.selected)
554             return;
555
556         let oldSelectedNode = this.dataGrid.selectedNode;
557         if (oldSelectedNode)
558             oldSelectedNode.deselect(true);
559
560         this._selected = true;
561         this.dataGrid.selectedNode = this;
562
563         if (this._element)
564             this._element.classList.add("selected");
565
566         if (!suppressSelectedEvent)
567             this.dataGrid.dispatchEventToListeners(WebInspector.DataGrid.Event.SelectedNodeChanged, {oldSelectedNode});
568     }
569
570     revealAndSelect()
571     {
572         this.reveal();
573         this.select();
574     }
575
576     deselect(suppressDeselectedEvent)
577     {
578         if (!this.dataGrid || this.dataGrid.selectedNode !== this || !this.selected)
579             return;
580
581         this._selected = false;
582         this.dataGrid.selectedNode = null;
583
584         if (this._element)
585             this._element.classList.remove("selected");
586
587         if (!suppressDeselectedEvent)
588             this.dataGrid.dispatchEventToListeners(WebInspector.DataGrid.Event.SelectedNodeChanged, {oldSelectedNode: this});
589     }
590
591     traverseNextNode(skipHidden, stayWithin, dontPopulate, info)
592     {
593         if (!dontPopulate && this.hasChildren)
594             this.dispatchEventToListeners("populate");
595
596         if (info)
597             info.depthChange = 0;
598
599         var node = (!skipHidden || this.revealed) ? this.children[0] : null;
600         if (node && (!skipHidden || this.expanded)) {
601             if (info)
602                 info.depthChange = 1;
603             return node;
604         }
605
606         if (this === stayWithin)
607             return null;
608
609         node = (!skipHidden || this.revealed) ? this.nextSibling : null;
610         if (node)
611             return node;
612
613         node = this;
614         while (node && !node.root && !((!skipHidden || node.revealed) ? node.nextSibling : null) && node.parent !== stayWithin) {
615             if (info)
616                 info.depthChange -= 1;
617             node = node.parent;
618         }
619
620         if (!node)
621             return null;
622
623         return (!skipHidden || node.revealed) ? node.nextSibling : null;
624     }
625
626     traversePreviousNode(skipHidden, dontPopulate)
627     {
628         var node = (!skipHidden || this.revealed) ? this.previousSibling : null;
629         if (!dontPopulate && node && node.hasChildren)
630             node.dispatchEventToListeners("populate");
631
632         while (node && ((!skipHidden || (node.revealed && node.expanded)) ? node.children.lastValue : null)) {
633             if (!dontPopulate && node.hasChildren)
634                 node.dispatchEventToListeners("populate");
635             node = ((!skipHidden || (node.revealed && node.expanded)) ? node.children.lastValue : null);
636         }
637
638         if (node)
639             return node;
640
641         if (!this.parent || this.parent.root)
642             return null;
643
644         return this.parent;
645     }
646
647     isEventWithinDisclosureTriangle(event)
648     {
649         if (!this.hasChildren)
650             return false;
651
652         let cell = event.target.enclosingNodeOrSelfWithNodeName("td");
653         if (!cell || !cell.classList.contains("disclosure"))
654             return false;
655
656         let computedStyle = window.getComputedStyle(cell);
657         let start = 0;
658         if (WebInspector.resolvedLayoutDirection() === WebInspector.LayoutDirection.RTL)
659             start += cell.totalOffsetRight - computedStyle.getPropertyCSSValue("padding-right").getFloatValue(CSSPrimitiveValue.CSS_PX) - this.disclosureToggleWidth;
660         else
661             start += cell.totalOffsetLeft + computedStyle.getPropertyCSSValue("padding-left").getFloatValue(CSSPrimitiveValue.CSS_PX);
662         return event.pageX >= start && event.pageX <= start + this.disclosureToggleWidth;
663     }
664
665     _attach()
666     {
667         if (!this.dataGrid || this._attached)
668             return;
669
670         this._attached = true;
671
672         let insertionIndex = -1;
673
674         if (!this.isPlaceholderNode) {
675             var previousGridNode = this.traversePreviousNode(true, true);
676             insertionIndex = this.dataGrid._rows.indexOf(previousGridNode);
677             if (insertionIndex === -1)
678                 insertionIndex = 0;
679             else
680                 insertionIndex++;
681         }
682
683         if (insertionIndex === -1)
684             this.dataGrid._rows.push(this);
685         else
686             this.dataGrid._rows.insertAtIndex(this, insertionIndex);
687
688         this.dataGrid._noteRowsChanged();
689
690         if (this.expanded) {
691             for (var i = 0; i < this.children.length; ++i)
692                 this.children[i]._attach();
693         }
694     }
695
696     _detach()
697     {
698         if (!this._attached)
699             return;
700
701         this._attached = false;
702
703         this.dataGrid._rows.remove(this, true);
704         this.dataGrid._noteRowRemoved(this);
705
706         for (var i = 0; i < this.children.length; ++i)
707             this.children[i]._detach();
708     }
709
710     savePosition()
711     {
712         if (this._savedPosition)
713             return;
714
715         console.assert(this.parent);
716         if (!this.parent)
717             return;
718
719         this._savedPosition = {
720             parent: this.parent,
721             index: this.parent.children.indexOf(this)
722         };
723     }
724
725     restorePosition()
726     {
727         if (!this._savedPosition)
728             return;
729
730         if (this.parent !== this._savedPosition.parent)
731             this._savedPosition.parent.insertChild(this, this._savedPosition.index);
732
733         this._savedPosition = null;
734     }
735
736     appendContextMenuItems(contextMenu)
737     {
738         // Subclasses may override
739         return null;
740     }
741
742     // Protected
743
744     filterableDataForColumn(columnIdentifier)
745     {
746         let value = this.data[columnIdentifier];
747         return typeof value === "string" ? value : null;
748     }
749
750     didResizeColumn(columnIdentifier)
751     {
752         // Override by subclasses.
753     }
754 };
755
756 // Used to create a new table row when entering new data by editing cells.
757 WebInspector.PlaceholderDataGridNode = class PlaceholderDataGridNode extends WebInspector.DataGridNode
758 {
759     constructor(data)
760     {
761         super(data, false);
762         this.isPlaceholderNode = true;
763     }
764
765     makeNormal()
766     {
767         this.isPlaceholderNode = false;
768     }
769 };