d2f7df039b7a59f9e3f721d96ee634474dca3605
[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     console.assert(property instanceof WebInspector.PropertyDescriptor);
29     console.assert(propertyPath instanceof WebInspector.PropertyPath);
30
31     this._property = property;
32     this._mode = mode || WebInspector.ObjectTreeView.Mode.Properties;
33     this._propertyPath = propertyPath;
34     this._prototypeName = prototypeName;
35
36     var classNames = ["object-tree-property"];
37
38     if (this._property.hasValue()) {
39         classNames.push(this._property.value.type);
40         if (this._property.value.subtype)
41             classNames.push(this._property.value.subtype);
42     } else
43         classNames.push("accessor");
44
45     if (this._property.wasThrown)
46         classNames.push("had-error");
47
48     if (this._property.name === "__proto__")
49         classNames.push("prototype-property");
50
51     WebInspector.GeneralTreeElement.call(this, classNames, this._titleFragment(), null, this._property, false);
52     this._updateTooltips();
53     this._updateHasChildren();
54
55     this.small = true;
56     this.toggleOnClick = true;
57     this.selectable = false;
58     this.tooltipHandledSeparately = true;
59 };
60
61 WebInspector.ObjectTreePropertyTreeElement.prototype = {
62     constructor: WebInspector.ObjectTreePropertyTreeElement,
63     __proto__: WebInspector.GeneralTreeElement.prototype,
64
65     // Public
66
67     get property()
68     {
69         return this._property;
70     },
71
72     // Protected
73
74     onpopulate: function()
75     {
76         this._updateChildren();
77     },
78
79     onexpand: function()
80     {
81         if (this._previewView)
82             this._previewView.showTitle();
83     },
84
85     oncollapse: function()
86     {
87         if (this._previewView)
88             this._previewView.showPreview();
89     },
90
91     // Private
92
93     _resolvedValue: function()
94     {
95         if (this._getterValue)
96             return this._getterValue;
97         if (this._property.hasValue())
98             return this._property.value;
99         return null;
100     },
101
102     _propertyPathType: function()
103     {
104         if (this._getterValue || this._property.hasValue())
105             return WebInspector.PropertyPath.Type.Value;
106         if (this._property.hasGetter())
107             return WebInspector.PropertyPath.Type.Getter;
108         if (this._property.hasSetter())
109             return WebInspector.PropertyPath.Type.Setter;
110         return WebInspector.PropertyPath.Type.Value;
111     },
112
113     _resolvedValuePropertyPath: function()
114     {
115         if (this._getterValue)
116             return this._propertyPath.appendPropertyDescriptor(this._getterValue, this._property, WebInspector.PropertyPath.Type.Value);
117         if (this._property.hasValue())
118             return this._propertyPath.appendPropertyDescriptor(this._property.value, this._property, WebInspector.PropertyPath.Type.Value);
119         return null;
120     },
121
122     _thisPropertyPath: function()
123     {
124         return this._propertyPath.appendPropertyDescriptor(null, this._property, this._propertyPathType());
125     },
126
127     _updateHasChildren: function()
128     {
129         var resolvedValue = this._resolvedValue();
130         var valueHasChildren = (resolvedValue && resolvedValue.hasChildren);
131         var wasThrown = this._property.wasThrown || this._getterHadError;
132
133         if (this._mode === WebInspector.ObjectTreeView.Mode.Properties)
134             this.hasChildren = !wasThrown && valueHasChildren;
135         else
136             this.hasChildren = !wasThrown && valueHasChildren && (this._property.name === "__proto__" || this._alwaysDisplayAsProperty());
137     },
138
139     _updateTooltips: function()
140     {
141         var attributes = [];
142
143         if (this._property.configurable)
144             attributes.push("configurable");
145         if (this._property.enumerable)
146             attributes.push("enumerable");
147         if (this._property.writable)
148             attributes.push("writable");
149
150         this.iconElement.title = attributes.join(" ");
151     },
152
153     _updateTitleAndIcon: function()
154     {
155         this.mainTitle = this._titleFragment();
156
157         if (this._getterValue) {
158             this.addClassName(this._getterValue.type);
159             if (this._getterValue.subtype)
160                 this.addClassName(this._getterValue.subtype);
161             if (this._getterHadError)
162                 this.addClassName("had-error");
163             this.removeClassName("accessor");
164         }
165
166         this._updateHasChildren();
167     },
168
169     _titleFragment: function()
170     {
171         if (this._property.name === "__proto__")
172             return this._createTitlePrototype();
173
174         if (this._mode === WebInspector.ObjectTreeView.Mode.Properties)
175             return this._createTitlePropertyStyle();
176         else
177             return this._createTitleAPIStyle();
178     },
179
180     _createTitlePrototype: function()
181     {
182         console.assert(this._property.hasValue());
183         console.assert(this._property.name === "__proto__");
184
185         var nameElement = document.createElement("span");
186         nameElement.className = "prototype-name";
187         nameElement.textContent = WebInspector.UIString("%s Prototype").format(this._sanitizedPrototypeString(this._property.value));
188         return nameElement;
189     },
190
191     _createTitlePropertyStyle: function()
192     {
193         var container = document.createDocumentFragment();
194
195         // Property name.
196         var nameElement = document.createElement("span");
197         nameElement.className = "property-name";
198         nameElement.textContent = this._property.name + ": ";
199         nameElement.title = this._propertyPathString(this._thisPropertyPath());
200
201         // Property attributes.
202         if (this._mode === WebInspector.ObjectTreeView.Mode.Properties) {
203             if (!this._property.enumerable)
204                 nameElement.classList.add("not-enumerable");
205         }
206
207         // Value / Getter Value / Getter.
208         var valueOrGetterElement;
209         var resolvedValue = this._resolvedValue();
210         if (resolvedValue) {
211             if (resolvedValue.preview) {
212                 this._previewView = new WebInspector.ObjectPreviewView(resolvedValue.preview);
213                 valueOrGetterElement = this._previewView.element;
214             } else {
215                 valueOrGetterElement = WebInspector.FormattedValue.createElementForRemoteObject(resolvedValue, this._property.wasThrown || this._getterHadError);
216
217                 // Special case a function property string.
218                 if (resolvedValue.type === "function")
219                     valueOrGetterElement.textContent = this._functionPropertyString();
220             }
221
222             // FIXME: Context Menu for Value. (See ObjectPropertiesSection).
223             // FIXME: Option+Click for Value.
224         } else {
225             valueOrGetterElement = document.createElement("span");
226             if (this._property.hasGetter())
227                 valueOrGetterElement.appendChild(this._createInteractiveGetterElement());
228             if (!this._property.hasSetter())
229                 valueOrGetterElement.appendChild(this._createReadOnlyIconElement());
230             // FIXME: What if just a setter?
231         }
232
233         valueOrGetterElement.classList.add("value");
234         if (this._property.wasThrown || this._getterHadError)
235             valueOrGetterElement.classList.add("error");
236
237         container.appendChild(nameElement);
238         container.appendChild(valueOrGetterElement);
239         return container;
240     },
241
242     _createTitleAPIStyle: function()
243     {
244         // Fixed values and special properties display like a property.
245         if (this._alwaysDisplayAsProperty())
246             return this._createTitlePropertyStyle();
247
248         // Fetched getter values should already have been shown as properties.
249         console.assert(!this._getterValue);
250
251         // No API to display.
252         var isFunction = this._property.hasValue() && this._property.value.type === "function";
253         if (!isFunction && !this._property.hasGetter() && !this._property.hasSetter())
254             return null;
255
256         var container = document.createDocumentFragment();
257
258         // Function / Getter / Setter.
259         var nameElement = document.createElement("span");
260         nameElement.className = "property-name";
261         nameElement.textContent = this._property.name;
262         nameElement.title = this._propertyPathString(this._thisPropertyPath());
263         container.appendChild(nameElement);
264
265         if (isFunction) {
266             var paramElement = document.createElement("span");
267             paramElement.className = "function-parameters";
268             paramElement.textContent = this._functionParameterString();
269             container.appendChild(paramElement);
270         } else {
271             if (this._property.hasGetter())
272                 container.appendChild(this._createInteractiveGetterElement());
273             if (!this._property.hasSetter())
274                 container.appendChild(this._createReadOnlyIconElement());
275             // FIXME: What if just a setter?
276         }
277
278         return container;
279     },
280
281     _createInteractiveGetterElement: function()
282     {
283         var getterElement = document.createElement("img");
284         getterElement.className = "getter";
285         getterElement.title = WebInspector.UIString("Invoke getter");
286
287         getterElement.addEventListener("click", function(event) {
288             event.stopPropagation();
289             var lastNonPrototypeObject = this._propertyPath.lastNonPrototypeObject;
290             var getterObject = this._property.get;
291             lastNonPrototypeObject.invokeGetter(getterObject, function(error, result, wasThrown) {
292                 this._getterHadError = !!(error || wasThrown);
293                 this._getterValue = result;
294                 this._updateTitleAndIcon();
295             }.bind(this));
296         }.bind(this));
297
298         return getterElement;
299     },
300
301     _createReadOnlyIconElement: function()
302     {
303         var readOnlyElement = document.createElement("img");
304         readOnlyElement.className = "read-only";
305         readOnlyElement.title = WebInspector.UIString("Read only");
306         return readOnlyElement;
307     },
308
309     _alwaysDisplayAsProperty: function()
310     {
311         // Constructor, though a function, is often better treated as an expandable object.
312         if (this._property.name === "constructor")
313             return true;
314
315         // Non-function objects are often better treated as properties.
316         if (this._property.hasValue() && this._property.value.type !== "function")
317             return true;
318
319         // Fetched getter value.
320         if (this._getterValue)
321             return true;
322
323         return false;
324     },
325
326     _functionPropertyString: function()
327     {
328         return "function" + this._functionParameterString();
329     },
330
331     _functionParameterString: function()
332     {
333         var resolvedValue = this._resolvedValue();
334         console.assert(resolvedValue.type === "function");
335
336         // For Native methods, the toString is poor. We try to provide good function parameter strings.
337         if (isFunctionStringNativeCode(resolvedValue.description)) {
338             // Native function on a prototype, likely "Foo.prototype.method".
339             if (this._prototypeName) {
340                 if (WebInspector.NativePrototypeFunctionParameters[this._prototypeName]) {
341                     var params = WebInspector.NativePrototypeFunctionParameters[this._prototypeName][this._property.name];
342                     return params ? "(" + params + ")" : "()";
343                 }
344             }
345
346             // Native function property on a native function is likely a "Foo.method".
347             if (isFunctionStringNativeCode(this._propertyPath.object.description)) {
348                 var match = this._propertyPath.object.description.match(/^function\s+([^)]+?)\(/);
349                 if (match) {
350                     var name = match[1];
351                     if (WebInspector.NativeConstructorFunctionParameters[name]) {
352                         var params = WebInspector.NativeConstructorFunctionParameters[name][this._property.name];
353                         return params ? "(" + params + ")" : "()";
354                     }
355                 }
356             }
357         }        
358
359         var match = resolvedValue.description.match(/^function.*?(\([^)]+?\))/);
360         return match ? match[1] : "()";
361     },
362
363     _sanitizedPrototypeString: function(value)
364     {
365         // FIXME: <https://webkit.org/b/141610> For many X, X.prototype is an X when it must be a plain object
366         if (value.type === "function")
367             return "Function";
368         if (value.subtype === "date")
369             return "Date";
370         if (value.subtype === "regexp")
371             return "RegExp";
372
373         return value.description.replace(/\[\d+\]$/, "").replace(/Prototype$/, "");
374     },
375
376     _propertyPathString: function(propertyPath)
377     {
378         if (propertyPath.isFullPathImpossible())
379             return WebInspector.UIString("Unable to determine path to property from root");
380
381         return propertyPath.displayPath(this._propertyPathType());
382     },
383
384     _updateChildren: function()
385     {
386         if (this.children.length && !this.shouldRefreshChildren)
387             return;
388
389         var resolvedValue = this._resolvedValue();
390         if (resolvedValue.isCollectionType() && this._mode === WebInspector.ObjectTreeView.Mode.Properties)
391             resolvedValue.getCollectionEntries(0, 100, this._updateChildrenInternal.bind(this, this._updateEntries, this._mode));
392         else if (this._property.name === "__proto__")
393             resolvedValue.getOwnPropertyDescriptors(this._updateChildrenInternal.bind(this, this._updateProperties, WebInspector.ObjectTreeView.Mode.API));
394         else
395             resolvedValue.getDisplayablePropertyDescriptors(this._updateChildrenInternal.bind(this, this._updateProperties, this._mode));
396     },
397
398     _updateChildrenInternal: function(handler, mode, list)
399     {
400         this.removeChildren();
401
402         if (!list) {
403             var errorMessageElement = WebInspector.ObjectTreeView.emptyMessageElement(WebInspector.UIString("Could not fetch properties. Object may no longer exist."));
404             this.appendChild(new TreeElement(errorMessageElement, null, false));
405             return;
406         }
407
408         handler.call(this, list, this._resolvedValuePropertyPath(), mode);
409     },
410
411     _updateEntries: function(entries, propertyPath, mode)
412     {
413         for (var entry of entries) {
414             if (entry.key) {
415                 this.appendChild(new WebInspector.ObjectTreeMapKeyTreeElement(entry.key, propertyPath));
416                 this.appendChild(new WebInspector.ObjectTreeMapValueTreeElement(entry.value, propertyPath, entry.key));
417             } else
418                 this.appendChild(new WebInspector.ObjectTreeSetIndexTreeElement(entry.value, propertyPath));
419         }
420
421         if (!this.children.length) {
422             var emptyMessageElement = WebInspector.ObjectTreeView.emptyMessageElement(WebInspector.UIString("No Entries."));
423             this.appendChild(new TreeElement(emptyMessageElement, null, false));
424         }
425
426         // Show the prototype so users can see the API.
427         var resolvedValue = this._resolvedValue();
428         resolvedValue.getOwnPropertyDescriptor("__proto__", function(propertyDescriptor) {
429             if (propertyDescriptor)
430                 this.appendChild(new WebInspector.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, mode));
431         }.bind(this));
432     },
433
434     _updateProperties: function(properties, propertyPath, mode)
435     {
436         properties.sort(WebInspector.ObjectTreeView.ComparePropertyDescriptors);
437
438         var resolvedValue = this._resolvedValue();
439         var isArray = resolvedValue.isArray();
440         var isPropertyMode = mode === WebInspector.ObjectTreeView.Mode.Properties || this._getterValue;
441         var isAPI = mode === WebInspector.ObjectTreeView.Mode.API;
442
443         var prototypeName = undefined;
444         if (this._property.name === "__proto__") {
445             if (resolvedValue.description)
446                 prototypeName = this._sanitizedPrototypeString(resolvedValue);
447         }
448
449         for (var propertyDescriptor of properties) {
450             // FIXME: If this is a pure API ObjectTree, we should show the native getters.
451             // For now, just skip native binding getters in API mode, since we likely
452             // already showed them in the Properties section.
453             if (isAPI && propertyDescriptor.nativeGetter)
454                 continue;
455             
456             if (isArray && isPropertyMode) {
457                 if (propertyDescriptor.isIndexProperty())
458                     this.appendChild(new WebInspector.ObjectTreeArrayIndexTreeElement(propertyDescriptor, propertyPath));
459                 else if (propertyDescriptor.name === "__proto__")
460                     this.appendChild(new WebInspector.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, mode, prototypeName));
461             } else
462                 this.appendChild(new WebInspector.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, mode, prototypeName));
463         }
464
465         if (!this.children.length) {
466             var emptyMessageElement = WebInspector.ObjectTreeView.emptyMessageElement(WebInspector.UIString("No Properties."));
467             this.appendChild(new TreeElement(emptyMessageElement, null, false));
468         }
469     }
470 };