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