b3b613aa1c77e2cc9be2a7e9a0ca8f651d2bfea7
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / ObjectTreeView.js
1 /*
2  * Copyright (C) 2015 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.ObjectTreeView = function(object, mode, propertyPath, forceExpanding)
27 {
28     // FIXME: Convert this to a WebInspector.Object subclass, and call super().
29     // WebInspector.Object.call(this);
30
31     console.assert(object instanceof WebInspector.RemoteObject);
32     console.assert(!propertyPath || propertyPath instanceof WebInspector.PropertyPath);
33
34     this._object = object;
35     this._mode = mode || WebInspector.ObjectTreeView.Mode.Properties;
36     this._propertyPath = propertyPath || new WebInspector.PropertyPath(this._object, "this");
37     this._expanded = false;
38     this._hasLosslessPreview = false;
39
40     // If ObjectTree is used outside of the console, we do not know when to release
41     // WeakMap entries. Currently collapse would work. For the console, we can just
42     // listen for console clear events. Currently all ObjectTrees are in the console.
43     this._inConsole = true;
44
45     this._element = document.createElement("div");
46     this._element.className = "object-tree";
47
48     if (this._object.preview) {
49         this._previewView = new WebInspector.ObjectPreviewView(this._object.preview);
50         this._previewView.element.addEventListener("click", this._handlePreviewOrTitleElementClick.bind(this));
51         this._element.appendChild(this._previewView.element);
52
53         if (this._previewView.lossless && !this._propertyPath.parent && !forceExpanding) {
54             this._hasLosslessPreview = true;
55             this.element.classList.add("lossless-preview");
56         }
57     } else {
58         this._titleElement = document.createElement("span");
59         this._titleElement.className = "title";
60         this._titleElement.appendChild(WebInspector.FormattedValue.createElementForRemoteObject(this._object));
61         this._titleElement.addEventListener("click", this._handlePreviewOrTitleElementClick.bind(this));
62         this._element.appendChild(this._titleElement);
63     }
64
65     this._outlineElement = document.createElement("ol");
66     this._outlineElement.className = "object-tree-outline";
67     this._outline = new WebInspector.TreeOutline(this._outlineElement);
68     this._element.appendChild(this._outlineElement);
69
70     // FIXME: Support editable ObjectTrees.
71 };
72
73 WebInspector.ObjectTreeView.emptyMessageElement = function(message)
74 {
75     var emptyMessageElement = document.createElement("div");
76     emptyMessageElement.className = "empty-message";
77     emptyMessageElement.textContent = message;
78     return emptyMessageElement;
79 };
80
81 WebInspector.ObjectTreeView.Mode = {
82     Properties: Symbol("object-tree-properties"),
83     API: Symbol("object-tree-api"),
84 };
85
86 WebInspector.ObjectTreeView.ComparePropertyDescriptors = function(propertyA, propertyB)
87 {
88     var a = propertyA.name;
89     var b = propertyB.name;
90
91     // Put __proto__ at the bottom.
92     if (a === "__proto__")
93         return 1;
94     if (b === "__proto__")
95         return -1;
96
97     // Put internal properties at the top.
98     if (a.isInternalProperty && !b.isInternalProperty)
99         return -1;
100     if (b.isInternalProperty && !a.isInternalProperty)
101         return 1;
102
103     // if used elsewhere make sure to
104     //  - convert a and b to strings (not needed here, properties are all strings)
105     //  - check if a == b (not needed here, no two properties can be the same)
106
107     var diff = 0;
108     var chunk = /^\d+|^\D+/;
109     var chunka, chunkb, anum, bnum;
110     while (diff === 0) {
111         if (!a && b)
112             return -1;
113         if (!b && a)
114             return 1;
115         chunka = a.match(chunk)[0];
116         chunkb = b.match(chunk)[0];
117         anum = !isNaN(chunka);
118         bnum = !isNaN(chunkb);
119         if (anum && !bnum)
120             return -1;
121         if (bnum && !anum)
122             return 1;
123         if (anum && bnum) {
124             diff = chunka - chunkb;
125             if (diff === 0 && chunka.length !== chunkb.length) {
126                 if (!+chunka && !+chunkb) // chunks are strings of all 0s (special case)
127                     return chunka.length - chunkb.length;
128                 else
129                     return chunkb.length - chunka.length;
130             }
131         } else if (chunka !== chunkb)
132             return (chunka < chunkb) ? -1 : 1;
133         a = a.substring(chunka.length);
134         b = b.substring(chunkb.length);
135     }
136     return diff;
137 };
138
139 WebInspector.ObjectTreeView.prototype = {
140     constructor: WebInspector.ObjectTreeView,
141     __proto__: WebInspector.Object.prototype,
142
143     // Public
144
145     get object()
146     {
147         return this._object;
148     },
149
150     get element()
151     {
152         return this._element;
153     },
154
155     get treeOutline()
156     {
157         return this._outline;
158     },
159
160     get expanded()
161     {
162         return this._expanded;
163     },
164
165     expand()
166     {
167         if (this._expanded)
168             return;
169
170         this._expanded = true;
171         this._element.classList.add("expanded");
172
173         if (this._previewView)
174             this._previewView.showTitle();
175
176         this._trackWeakEntries();
177
178         this.update();
179     },
180
181     collapse()
182     {
183         if (!this._expanded)
184             return;
185
186         this._expanded = false;
187         this._element.classList.remove("expanded");
188
189         if (this._previewView)
190             this._previewView.showPreview();
191
192         this._untrackWeakEntries();
193     },
194
195     showOnlyProperties()
196     {
197         this._inConsole = false;
198
199         this._element.classList.add("properties-only");
200     },
201
202     appendTitleSuffix(suffixElement)
203     {
204         if (this._previewView)
205             this._previewView.element.appendChild(suffixElement);
206         else
207             this._titleElement.appendChild(suffixElement);
208     },
209
210     appendExtraPropertyDescriptor(propertyDescriptor)
211     {
212         if (!this._extraProperties)
213             this._extraProperties = [];
214
215         this._extraProperties.push(propertyDescriptor);
216     },
217
218     // Protected
219
220     update()
221     {
222         if (this._object.isCollectionType() && this._mode === WebInspector.ObjectTreeView.Mode.Properties)
223             this._object.getCollectionEntries(0, 100, this._updateChildren.bind(this, this._updateEntries));
224         else
225             this._object.getDisplayablePropertyDescriptors(this._updateChildren.bind(this, this._updateProperties));
226     },
227
228     // Private
229
230     _updateChildren(handler, list)
231     {
232         this._outline.removeChildren();
233
234         if (!list) {
235             var errorMessageElement = WebInspector.ObjectTreeView.emptyMessageElement(WebInspector.UIString("Could not fetch properties. Object may no longer exist."));
236             this._outline.appendChild(new WebInspector.TreeElement(errorMessageElement, null, false));
237             return;
238         }
239
240         handler.call(this, list, this._propertyPath);
241     },
242
243     _updateEntries(entries, propertyPath)
244     {
245         for (var entry of entries) {
246             if (entry.key) {
247                 this._outline.appendChild(new WebInspector.ObjectTreeMapKeyTreeElement(entry.key, propertyPath));
248                 this._outline.appendChild(new WebInspector.ObjectTreeMapValueTreeElement(entry.value, propertyPath, entry.key));
249             } else
250                 this._outline.appendChild(new WebInspector.ObjectTreeSetIndexTreeElement(entry.value, propertyPath));
251         }
252
253         if (!this._outline.children.length) {
254             var emptyMessageElement = WebInspector.ObjectTreeView.emptyMessageElement(WebInspector.UIString("No Entries."));
255             this._outline.appendChild(new WebInspector.TreeElement(emptyMessageElement, null, false));
256         }
257
258         // Show the prototype so users can see the API.
259         this._object.getOwnPropertyDescriptor("__proto__", function(propertyDescriptor) {
260             if (propertyDescriptor)
261                 this._outline.appendChild(new WebInspector.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, this._mode));
262         }.bind(this));
263     },
264
265     _updateProperties(properties, propertyPath)
266     {
267         if (this._extraProperties)
268             properties = properties.concat(this._extraProperties);
269
270         properties.sort(WebInspector.ObjectTreeView.ComparePropertyDescriptors);
271
272         var isArray = this._object.isArray();
273         var isPropertyMode = this._mode === WebInspector.ObjectTreeView.Mode.Properties;
274
275         for (var propertyDescriptor of properties) {
276             if (isArray && isPropertyMode) {
277                 if (propertyDescriptor.isIndexProperty())
278                     this._outline.appendChild(new WebInspector.ObjectTreeArrayIndexTreeElement(propertyDescriptor, propertyPath));
279                 else if (propertyDescriptor.name === "__proto__")
280                     this._outline.appendChild(new WebInspector.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, this._mode));
281             } else
282                 this._outline.appendChild(new WebInspector.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, this._mode));
283         }
284
285         if (!this._outline.children.length) {
286             var emptyMessageElement = WebInspector.ObjectTreeView.emptyMessageElement(WebInspector.UIString("No Properties."));
287             this._outline.appendChild(new WebInspector.TreeElement(emptyMessageElement, null, false));
288         }
289     },
290
291     _handlePreviewOrTitleElementClick(event)
292     {
293         if (this._hasLosslessPreview)
294             return;
295
296         if (!this._expanded)
297             this.expand();
298         else
299             this.collapse();
300
301         event.stopPropagation();
302     },
303
304     _trackWeakEntries()
305     {
306         if (this._trackingEntries)
307             return;
308
309         if (!this._object.isWeakCollection())
310             return;
311
312         this._trackingEntries = true;
313
314         if (this._inConsole) {
315             WebInspector.logManager.addEventListener(WebInspector.LogManager.Event.Cleared, this._untrackWeakEntries, this);
316             WebInspector.logManager.addEventListener(WebInspector.LogManager.Event.ActiveLogCleared, this._untrackWeakEntries, this);
317             WebInspector.logManager.addEventListener(WebInspector.LogManager.Event.SessionStarted, this._untrackWeakEntries, this);
318         }
319     },
320
321     _untrackWeakEntries()
322     {
323         if (!this._trackingEntries)
324             return;
325
326         if (!this._object.isWeakCollection())
327             return;
328
329         this._trackingEntries = false;
330
331         this._object.releaseWeakCollectionEntries();
332
333         if (this._inConsole) {
334             WebInspector.logManager.removeEventListener(WebInspector.LogManager.Event.Cleared, this._untrackWeakEntries, this);
335             WebInspector.logManager.removeEventListener(WebInspector.LogManager.Event.ActiveLogCleared, this._untrackWeakEntries, this);
336             WebInspector.logManager.removeEventListener(WebInspector.LogManager.Event.SessionStarted, this._untrackWeakEntries, this);
337         }
338
339         // FIXME: This only tries to release weak entries if this object was a WeakMap.
340         // If there was a WeakMap expanded in a sub-object, we will never release those values.
341         // Should we attempt walking the entire tree and release weak collections?
342     },
343 };