Web Inspector: ES6: Provide a better view for Classes in the console
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / ObjectTreePropertyTreeElement.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.ObjectTreePropertyTreeElement = class ObjectTreePropertyTreeElement extends WebInspector.ObjectTreeBaseTreeElement
27 {
28     constructor(property, propertyPath, mode, prototypeName)
29     {
30         super(property, propertyPath, property);
31
32         this._mode = mode || WebInspector.ObjectTreeView.Mode.Properties;
33         this._prototypeName = prototypeName;
34
35         this.mainTitle = this._titleFragment();
36         this.addClassName("object-tree-property");
37
38         if (this.property.hasValue()) {
39             this.addClassName(this.property.value.type);
40             if (this.property.value.subtype)
41                 this.addClassName(this.property.value.subtype);
42         } else
43             this.addClassName("accessor");
44
45         if (this.property.wasThrown)
46             this.addClassName("had-error");
47         if (this.property.name === "__proto__")
48             this.addClassName("prototype-property");
49
50         this._updateTooltips();
51         this._updateHasChildren();
52     }
53     
54     // Protected
55
56     onpopulate()
57     {
58         this._updateChildren();
59     }
60
61     onexpand()
62     {
63         if (this._previewView)
64             this._previewView.showTitle();
65     }
66
67     oncollapse()
68     {
69         if (this._previewView)
70             this._previewView.showPreview();
71     }
72
73     invokedGetter()
74     {
75         this.mainTitle = this._titleFragment();
76
77         var resolvedValue = this.resolvedValue();
78         this.addClassName(resolvedValue.type);
79         if (resolvedValue.subtype)
80             this.addClassName(resolvedValue.subtype);
81         if (this.hadError())
82             this.addClassName("had-error");
83         this.removeClassName("accessor");
84
85         this._updateHasChildren();
86     }
87
88     // Private
89
90     _updateHasChildren()
91     {
92         var resolvedValue = this.resolvedValue();
93         var valueHasChildren = (resolvedValue && resolvedValue.hasChildren);
94         var wasThrown = this.hadError();
95
96         if (this._mode === WebInspector.ObjectTreeView.Mode.Properties)
97             this.hasChildren = !wasThrown && valueHasChildren;
98         else
99             this.hasChildren = !wasThrown && valueHasChildren && (this.property.name === "__proto__" || this._alwaysDisplayAsProperty());
100     }
101
102     _updateTooltips()
103     {
104         var attributes = [];
105
106         if (this.property.configurable)
107             attributes.push("configurable");
108         if (this.property.enumerable)
109             attributes.push("enumerable");
110         if (this.property.writable)
111             attributes.push("writable");
112
113         this.iconElement.title = attributes.join(" ");
114     }
115
116     _titleFragment()
117     {
118         if (this.property.name === "__proto__")
119             return this._createTitlePrototype();
120
121         if (this._mode === WebInspector.ObjectTreeView.Mode.Properties)
122             return this._createTitlePropertyStyle();
123         else
124             return this._createTitleAPIStyle();
125     }
126
127     _createTitlePrototype()
128     {
129         console.assert(this.property.hasValue());
130         console.assert(this.property.name === "__proto__");
131
132         var nameElement = document.createElement("span");
133         nameElement.className = "prototype-name";
134         nameElement.textContent = WebInspector.UIString("%s Prototype").format(this._sanitizedPrototypeString(this.property.value));
135         nameElement.title = this.propertyPathString(this.thisPropertyPath());
136         return nameElement;
137     }
138
139     _createTitlePropertyStyle()
140     {
141         var container = document.createDocumentFragment();
142
143         // Property name.
144         var nameElement = document.createElement("span");
145         nameElement.className = "property-name";
146         nameElement.textContent = this.property.name + ": ";
147         nameElement.title = this.propertyPathString(this.thisPropertyPath());
148
149         // Property attributes.
150         if (this._mode === WebInspector.ObjectTreeView.Mode.Properties) {
151             if (!this.property.enumerable)
152                 nameElement.classList.add("not-enumerable");
153         }
154
155         // Value / Getter Value / Getter.
156         var valueOrGetterElement;
157         var resolvedValue = this.resolvedValue();
158         if (resolvedValue) {
159             if (resolvedValue.preview) {
160                 this._previewView = new WebInspector.ObjectPreviewView(resolvedValue.preview);
161                 valueOrGetterElement = this._previewView.element;
162             } else {
163                 valueOrGetterElement = WebInspector.FormattedValue.createElementForRemoteObject(resolvedValue, this.hadError());
164
165                 // Special case a function property string.
166                 if (resolvedValue.type === "function")
167                     valueOrGetterElement.textContent = this._functionPropertyString();
168             }
169         } else {
170             valueOrGetterElement = document.createElement("span");
171             if (this.property.hasGetter())
172                 valueOrGetterElement.appendChild(this.createInteractiveGetterElement(this._mode !== WebInspector.ObjectTreeView.Mode.ClassAPI));
173             if (!this.property.hasSetter())
174                 valueOrGetterElement.appendChild(this.createReadOnlyIconElement());
175             // FIXME: What if just a setter?
176         }
177
178         valueOrGetterElement.classList.add("value");
179         if (this.hadError())
180             valueOrGetterElement.classList.add("error");
181
182         container.appendChild(nameElement);
183         container.appendChild(valueOrGetterElement);
184         return container;
185     }
186
187     _createTitleAPIStyle()
188     {
189         // Fixed values and special properties display like a property.
190         if (this._alwaysDisplayAsProperty())
191             return this._createTitlePropertyStyle();
192
193         // No API to display.
194         var isFunction = this.property.hasValue() && this.property.value.type === "function";
195         if (!isFunction && !this.property.hasGetter() && !this.property.hasSetter())
196             return null;
197
198         var container = document.createDocumentFragment();
199
200         // Function / Getter / Setter.
201         var nameElement = document.createElement("span");
202         nameElement.className = "property-name";
203         nameElement.textContent = this.property.name;
204         nameElement.title = this.propertyPathString(this.thisPropertyPath());
205         container.appendChild(nameElement);
206
207         if (isFunction) {
208             var paramElement = document.createElement("span");
209             paramElement.className = "function-parameters";
210             paramElement.textContent = this._functionParameterString();
211             container.appendChild(paramElement);
212         } else {
213             if (this.property.hasGetter())
214                 container.appendChild(this.createInteractiveGetterElement(this._mode !== WebInspector.ObjectTreeView.Mode.ClassAPI));
215             if (!this.property.hasSetter())
216                 container.appendChild(this.createReadOnlyIconElement());
217             // FIXME: What if just a setter?
218         }
219
220         return container;
221     }
222
223     _alwaysDisplayAsProperty()
224     {
225         // Constructor, though a function, is often better treated as an expandable object.
226         if (this.property.name === "constructor")
227             return true;
228
229         // Non-function objects are often better treated as properties.
230         if (this.property.hasValue() && this.property.value.type !== "function")
231             return true;
232
233         // Fetched getter value.
234         if (this._getterValue)
235             return true;
236
237         return false;
238     }
239
240     _functionPropertyString()
241     {
242         return "function" + this._functionParameterString();
243     }
244
245     _functionParameterString()
246     {
247         var resolvedValue = this.resolvedValue();
248         console.assert(resolvedValue.type === "function");
249
250         // For Native methods, the toString is poor. We try to provide good function parameter strings.
251         if (isFunctionStringNativeCode(resolvedValue.description)) {
252             // Native function on a prototype, likely "Foo.prototype.method".
253             if (this._prototypeName) {
254                 if (WebInspector.NativePrototypeFunctionParameters[this._prototypeName]) {
255                     var params = WebInspector.NativePrototypeFunctionParameters[this._prototypeName][this._property.name];
256                     return params ? "(" + params + ")" : "()";
257                 }
258             }
259
260             // Native function property on a native function is likely a "Foo.method".
261             if (isFunctionStringNativeCode(this._propertyPath.object.description)) {
262                 var match = this._propertyPath.object.description.match(/^function\s+([^)]+?)\(/);
263                 if (match) {
264                     var name = match[1];
265                     if (WebInspector.NativeConstructorFunctionParameters[name]) {
266                         var params = WebInspector.NativeConstructorFunctionParameters[name][this._property.name];
267                         return params ? "(" + params + ")" : "()";
268                     }
269                 }
270             }
271
272             // Native DOM constructor.
273             if (this._propertyPath.object.description.endsWith("Constructor")) {
274                 var name = this._propertyPath.object.description;
275                 if (WebInspector.NativeConstructorFunctionParameters[name]) {
276                     var params = WebInspector.NativeConstructorFunctionParameters[name][this._property.name];
277                     return params ? "(" + params + ")" : "()";
278                 }
279             }
280         }
281
282         var match = resolvedValue.description.match(/^function.*?(\([^)]+?\))/);
283         return match ? match[1] : "()";
284     }
285
286     _sanitizedPrototypeString(value)
287     {
288         // FIXME: <https://webkit.org/b/141610> For many X, X.prototype is an X when it must be a plain object
289         if (value.type === "function")
290             return "Function";
291         if (value.subtype === "date")
292             return "Date";
293         if (value.subtype === "regexp")
294             return "RegExp";
295
296         return value.description.replace(/Prototype$/, "");
297     }
298
299     _updateChildren()
300     {
301         if (this.children.length && !this.shouldRefreshChildren)
302             return;
303
304         var resolvedValue = this.resolvedValue();
305         if (resolvedValue.isCollectionType() && this._mode === WebInspector.ObjectTreeView.Mode.Properties)
306             resolvedValue.getCollectionEntries(0, 100, this._updateChildrenInternal.bind(this, this._updateEntries, this._mode));
307         else if (this._mode === WebInspector.ObjectTreeView.Mode.ClassAPI)
308             resolvedValue.getOwnPropertyDescriptors(this._updateChildrenInternal.bind(this, this._updateProperties, WebInspector.ObjectTreeView.Mode.ClassAPI));
309         else if (this.property.name === "__proto__")
310             resolvedValue.getOwnPropertyDescriptors(this._updateChildrenInternal.bind(this, this._updateProperties, WebInspector.ObjectTreeView.Mode.PrototypeAPI));
311         else
312             resolvedValue.getDisplayablePropertyDescriptors(this._updateChildrenInternal.bind(this, this._updateProperties, this._mode));
313     }
314
315     _updateChildrenInternal(handler, mode, list)
316     {
317         this.removeChildren();
318
319         if (!list) {
320             var errorMessageElement = WebInspector.ObjectTreeView.createEmptyMessageElement(WebInspector.UIString("Could not fetch properties. Object may no longer exist."));
321             this.appendChild(new WebInspector.TreeElement(errorMessageElement, null, false));
322             return;
323         }
324
325         handler.call(this, list, this.resolvedValuePropertyPath(), mode);
326     }
327
328     _updateEntries(entries, propertyPath, mode)
329     {
330         for (var entry of entries) {
331             if (entry.key) {
332                 this.appendChild(new WebInspector.ObjectTreeMapKeyTreeElement(entry.key, propertyPath));
333                 this.appendChild(new WebInspector.ObjectTreeMapValueTreeElement(entry.value, propertyPath, entry.key));
334             } else
335                 this.appendChild(new WebInspector.ObjectTreeSetIndexTreeElement(entry.value, propertyPath));
336         }
337
338         if (!this.children.length) {
339             var emptyMessageElement = WebInspector.ObjectTreeView.createEmptyMessageElement(WebInspector.UIString("No Entries."));
340             this.appendChild(new WebInspector.TreeElement(emptyMessageElement, null, false));
341         }
342
343         // Show the prototype so users can see the API.
344         var resolvedValue = this.resolvedValue();
345         resolvedValue.getOwnPropertyDescriptor("__proto__", function(propertyDescriptor) {
346             if (propertyDescriptor)
347                 this.appendChild(new WebInspector.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, mode));
348         }.bind(this));
349     }
350
351     _updateProperties(properties, propertyPath, mode)
352     {
353         properties.sort(WebInspector.ObjectTreeView.comparePropertyDescriptors);
354
355         var resolvedValue = this.resolvedValue();
356         var isArray = resolvedValue.isArray();
357         var isPropertyMode = mode === WebInspector.ObjectTreeView.Mode.Properties || this._getterValue;
358         var isAPI = mode !== WebInspector.ObjectTreeView.Mode.Prototype;
359
360         var prototypeName = undefined;
361         if (this.property.name === "__proto__") {
362             if (resolvedValue.description)
363                 prototypeName = this._sanitizedPrototypeString(resolvedValue);
364         }
365
366         for (var propertyDescriptor of properties) {
367             // FIXME: If this is a pure API ObjectTree, we should show the native getters.
368             // For now, just skip native binding getters in API mode, since we likely
369             // already showed them in the Properties section.
370             if (isAPI && propertyDescriptor.nativeGetter)
371                 continue;
372
373             if (isArray && isPropertyMode) {
374                 if (propertyDescriptor.isIndexProperty())
375                     this.appendChild(new WebInspector.ObjectTreeArrayIndexTreeElement(propertyDescriptor, propertyPath));
376                 else if (propertyDescriptor.name === "__proto__")
377                     this.appendChild(new WebInspector.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, mode, prototypeName));
378             } else
379                 this.appendChild(new WebInspector.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, mode, prototypeName));
380         }
381
382         if (!this.children.length) {
383             var emptyMessageElement = WebInspector.ObjectTreeView.createEmptyMessageElement(WebInspector.UIString("No Properties."));
384             this.appendChild(new WebInspector.TreeElement(emptyMessageElement, null, false));
385         }
386     }
387 };