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