Web Inspector: Debugger Popovers and Probes should use FormattedValue/ObjectTreeView...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / ObjectPropertiesSection.js
1 /*
2  * Copyright (C) 2013 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.ObjectPropertiesSection = function(object, title, subtitle, emptyPlaceholder, getAllProperties, extraProperties, treeElementConstructor)
27 {
28     this.emptyPlaceholder = (emptyPlaceholder || WebInspector.UIString("No Properties"));
29     this.object = object;
30     this.getAllProperties = getAllProperties;
31     this.extraProperties = extraProperties;
32     this.treeElementConstructor = treeElementConstructor || WebInspector.ObjectPropertyTreeElement;
33     this.editable = true;
34
35     WebInspector.PropertiesSection.call(this, title, subtitle);
36 };
37
38 WebInspector.ObjectPropertiesSection.prototype = {
39     onpopulate: function()
40     {
41         this.update();
42     },
43
44     update: function()
45     {
46         function callback(properties)
47         {
48             if (!properties)
49                 return;
50
51             this.updateProperties(properties);
52         }
53
54         if (this.getAllProperties)
55             this.object.deprecatedGetAllProperties(callback.bind(this));
56         else
57             this.object.deprecatedGetDisplayableProperties(callback.bind(this));
58     },
59
60     updateProperties: function(properties, rootTreeElementConstructor, rootPropertyComparer)
61     {
62         if (!rootTreeElementConstructor)
63             rootTreeElementConstructor = this.treeElementConstructor;
64
65         if (!rootPropertyComparer)
66             rootPropertyComparer = WebInspector.ObjectPropertiesSection.CompareProperties;
67
68         if (this.extraProperties)
69             for (var i = 0; i < this.extraProperties.length; ++i)
70                 properties.push(this.extraProperties[i]);
71
72         properties.sort(rootPropertyComparer);
73
74         this.propertiesTreeOutline.removeChildren();
75
76         for (var i = 0; i < properties.length; ++i) {
77             properties[i].parentObject = this.object;
78             this.propertiesTreeOutline.appendChild(new rootTreeElementConstructor(properties[i]));
79         }
80
81         if (!this.propertiesTreeOutline.children.length) {
82             var title = document.createElement("div");
83             title.className = "info";
84             title.textContent = this.emptyPlaceholder;
85             var infoElement = new TreeElement(title, null, false);
86             this.propertiesTreeOutline.appendChild(infoElement);
87         }
88         this.propertiesForTest = properties;
89
90         if (this.object.isCollectionType())
91             this.propertiesTreeOutline.appendChild(new WebInspector.CollectionEntriesMainTreeElement(this.object));
92
93         this.dispatchEventToListeners(WebInspector.Section.Event.VisibleContentDidChange);
94     }
95 };
96
97 WebInspector.ObjectPropertiesSection.prototype.__proto__ = WebInspector.PropertiesSection.prototype;
98
99 WebInspector.ObjectPropertiesSection.CompareProperties = function(propertyA, propertyB)
100 {
101     var a = propertyA.name;
102     var b = propertyB.name;
103     if (a === "__proto__")
104         return 1;
105     if (b === "__proto__")
106         return -1;
107
108     // if used elsewhere make sure to
109     //  - convert a and b to strings (not needed here, properties are all strings)
110     //  - check if a == b (not needed here, no two properties can be the same)
111
112     var diff = 0;
113     var chunk = /^\d+|^\D+/;
114     var chunka, chunkb, anum, bnum;
115     while (diff === 0) {
116         if (!a && b)
117             return -1;
118         if (!b && a)
119             return 1;
120         chunka = a.match(chunk)[0];
121         chunkb = b.match(chunk)[0];
122         anum = !isNaN(chunka);
123         bnum = !isNaN(chunkb);
124         if (anum && !bnum)
125             return -1;
126         if (bnum && !anum)
127             return 1;
128         if (anum && bnum) {
129             diff = chunka - chunkb;
130             if (diff === 0 && chunka.length !== chunkb.length) {
131                 if (!+chunka && !+chunkb) // chunks are strings of all 0s (special case)
132                     return chunka.length - chunkb.length;
133                 else
134                     return chunkb.length - chunka.length;
135             }
136         } else if (chunka !== chunkb)
137             return (chunka < chunkb) ? -1 : 1;
138         a = a.substring(chunka.length);
139         b = b.substring(chunkb.length);
140     }
141     return diff;
142 };
143
144 WebInspector.ObjectPropertyTreeElement = function(property)
145 {
146     this.property = property;
147
148     // Pass an empty title, the title gets made later in onattach.
149     TreeElement.call(this, "", null, false);
150     this.toggleOnClick = true;
151     this.selectable = false;
152 };
153
154 WebInspector.ObjectPropertyTreeElement.prototype = {
155     onpopulate: function()
156     {
157         if (this.children.length && !this.shouldRefreshChildren)
158             return;
159
160         function callback(properties) {
161             this.removeChildren();
162             if (!properties)
163                 return;
164
165             properties.sort(WebInspector.ObjectPropertiesSection.CompareProperties);
166             for (var i = 0; i < properties.length; ++i)
167                 this.appendChild(new this.treeOutline.section.treeElementConstructor(properties[i]));
168
169             if (this.property.value.isCollectionType())
170                 this.appendChild(new WebInspector.CollectionEntriesMainTreeElement(this.property.value));
171         };
172
173         if (this.property.name === "__proto__")
174             this.property.value.deprecatedGetOwnProperties(callback.bind(this));
175         else
176             this.property.value.deprecatedGetDisplayableProperties(callback.bind(this));
177     },
178
179     ondblclick: function(event)
180     {
181         if (this.property.writable)
182             this.startEditing();
183     },
184
185     onattach: function()
186     {
187         this.update();
188     },
189
190     update: function()
191     {
192         this.nameElement = document.createElement("span");
193         this.nameElement.className = "name";
194         this.nameElement.textContent = this.property.name;
195         if (!this.property.enumerable && (!this.parent.root || !this.treeOutline.section.dontHighlightNonEnumerablePropertiesAtTopLevel))
196             this.nameElement.classList.add("dimmed");
197
198         var separatorElement = document.createElement("span");
199         separatorElement.className = "separator";
200         separatorElement.textContent = ": ";
201
202         this.valueElement = document.createElement("span");
203         this.valueElement.className = "value";
204
205         var description = this.property.value.description;
206         // Render \n as a nice unicode cr symbol.
207         if (this.property.wasThrown)
208             this.valueElement.textContent = "[Exception: " + description + "]";
209         else if (this.property.value.type === "string" && typeof description === "string") {
210             this.valueElement.textContent = "\"" + description.replace(/\n/g, "\u21B5").replace(/"/g, "\\\"") + "\"";
211             this.valueElement._originalTextContent = "\"" + description + "\"";
212         } else if (this.property.value.type === "function" && typeof description === "string") {
213             this.valueElement.textContent = /.*/.exec(description)[0].replace(/ +$/g, "");
214             this.valueElement._originalTextContent = description;
215         } else
216             this.valueElement.textContent = description;
217
218         if (this.property.value.type === "function")
219             this.valueElement.addEventListener("contextmenu", this._functionContextMenuEventFired.bind(this), false);
220
221         if (this.property.wasThrown)
222             this.valueElement.classList.add("error");
223         if (this.property.value.subtype)
224             this.valueElement.classList.add("formatted-" + this.property.value.subtype);
225         else if (this.property.value.type)
226             this.valueElement.classList.add("formatted-" + this.property.value.type);
227         if (this.property.value.subtype === "node")
228             this.valueElement.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), false);
229
230         this.listItemElement.removeChildren();
231
232         this.listItemElement.appendChild(this.nameElement);
233         this.listItemElement.appendChild(separatorElement);
234         this.listItemElement.appendChild(this.valueElement);
235         this.hasChildren = this.property.value.hasChildren && !this.property.wasThrown;
236     },
237
238     _contextMenuEventFired: function(event)
239     {
240         function selectNode(nodeId)
241         {
242             if (nodeId)
243                 WebInspector.domTreeManager.inspectElement(nodeId);
244         }
245
246         function revealElement()
247         {
248             this.property.value.pushNodeToFrontend(selectNode);
249         }
250
251         var contextMenu = new WebInspector.ContextMenu(event);
252         contextMenu.appendItem(WebInspector.UIString("Reveal in DOM Tree"), revealElement.bind(this));
253         contextMenu.show();
254     },
255
256     _functionContextMenuEventFired: function(event)
257     {
258         function didGetLocation(error, response)
259         {
260             if (error)
261                 return;
262
263             var location = response.location;
264             var sourceCode = WebInspector.debuggerManager.scriptForIdentifier(location.scriptId);
265             if (!sourceCode)
266                 return;
267
268             var sourceCodeLocation = sourceCode.createSourceCodeLocation(location.lineNumber, location.columnNumber || 0);
269             WebInspector.resourceSidebarPanel.showSourceCodeLocation(sourceCodeLocation);
270         }
271
272         function revealFunction()
273         {
274             DebuggerAgent.getFunctionDetails(this.property.value.objectId, didGetLocation);
275         }
276
277         var contextMenu = new WebInspector.ContextMenu(event);
278         contextMenu.appendItem(WebInspector.UIString("Jump to Definition"), revealFunction.bind(this));
279         contextMenu.show();
280     },
281
282     updateSiblings: function()
283     {
284         if (this.parent.root)
285             this.treeOutline.section.update();
286         else
287             this.parent.shouldRefreshChildren = true;
288     },
289
290     startEditing: function()
291     {
292         if (WebInspector.isBeingEdited(this.valueElement) || !this.treeOutline.section.editable)
293             return;
294
295         var context = { expanded: this.expanded };
296
297         // Lie about our children to prevent expanding on double click and to collapse subproperties.
298         this.hasChildren = false;
299
300         this.listItemElement.classList.add("editing-sub-part");
301
302         // Edit original source.
303         if (typeof this.valueElement._originalTextContent === "string")
304             this.valueElement.textContent = this.valueElement._originalTextContent;
305
306         var config = new WebInspector.EditingConfig(this.editingCommitted.bind(this), this.editingCancelled.bind(this), context);
307         WebInspector.startEditing(this.valueElement, config);
308     },
309
310     editingEnded: function(context)
311     {
312         this.listItemElement.scrollLeft = 0;
313         this.listItemElement.classList.remove("editing-sub-part");
314         if (context.expanded)
315             this.expand();
316     },
317
318     editingCancelled: function(element, context)
319     {
320         this.update();
321         this.editingEnded(context);
322     },
323
324     editingCommitted: function(element, userInput, previousContent, context)
325     {
326         if (userInput === previousContent)
327             return this.editingCancelled(element, context); // nothing changed, so cancel
328
329         this.applyExpression(userInput, true);
330
331         this.editingEnded(context);
332     },
333
334     applyExpression: function(expression, updateInterface)
335     {
336         expression = expression.trim();
337         var expressionLength = expression.length;
338         function callback(error)
339         {
340             if (!updateInterface)
341                 return;
342
343             if (error)
344                 this.update();
345
346             if (!expressionLength) {
347                 // The property was deleted, so remove this tree element.
348                 this.parent.removeChild(this);
349             } else {
350                 // Call updateSiblings since their value might be based on the value that just changed.
351                 this.updateSiblings();
352             }
353         }
354         this.property.parentObject.setPropertyValue(this.property.name, expression.trim(), callback.bind(this));
355     }
356 };
357
358 WebInspector.ObjectPropertyTreeElement.prototype.__proto__ = TreeElement.prototype;
359
360 WebInspector.CollectionEntriesMainTreeElement = function(remoteObject)
361 {
362     TreeElement.call(this, "<entries>", null, false);
363
364     console.assert(remoteObject);
365
366     this._remoteObject = remoteObject;
367     this._requestingEntries = false;
368     this._trackingEntries = false;
369
370     this.toggleOnClick = true;
371     this.selectable = false;
372     this.hasChildren = true;
373     this.expand();
374
375     // FIXME: When a parent TreeElement is collapsed, we do not get a chance
376     // to releaseWeakCollectionEntries. We should.
377 }
378
379 WebInspector.CollectionEntriesMainTreeElement.prototype = {
380     constructor: WebInspector.CollectionEntriesMainTreeElement,
381     __proto__: TreeElement.prototype,
382
383     onexpand: function()
384     {
385         if (this.children.length && !this.shouldRefreshChildren)
386             return;
387
388         if (this._requestingEntries)
389             return;
390
391         this._requestingEntries = true;
392
393         function callback(entries) {
394             this._requestingEntries = false;
395
396             this.removeChildren();
397
398             if (!entries || !entries.length) {
399                 this.appendChild(new WebInspector.EmptyCollectionTreeElement);
400                 return;
401             }
402
403             this._trackWeakEntries();
404
405             for (var i = 0; i < entries.length; ++i) {
406                 var entry = entries[i];
407                 if (entry.key)
408                     this.appendChild(new WebInspector.CollectionEntryTreeElement(entry, i));
409                 else {
410                     this.appendChild(new WebInspector.ObjectPropertyTreeElement({
411                         name: "" + i,
412                         value: entry.value,
413                         enumerable: true,
414                         writable: false,
415                     }));
416                 }
417             }
418         }
419         
420         this._remoteObject.getCollectionEntries(0, 100, callback.bind(this));
421     },
422
423     oncollapse: function()
424     {
425         this._untrackWeakEntries();
426     },
427
428     ondetach: function()
429     {
430         this._untrackWeakEntries();
431     },
432
433     // Private.
434
435     _trackWeakEntries: function()
436     {
437         if (!this._remoteObject.isWeakCollection())
438             return;
439
440         if (this._trackingEntries)
441             return;
442
443         this._trackingEntries = true;
444
445         WebInspector.logManager.addEventListener(WebInspector.LogManager.Event.Cleared, this._untrackWeakEntries, this);
446         WebInspector.logManager.addEventListener(WebInspector.LogManager.Event.ActiveLogCleared, this._untrackWeakEntries, this);
447         WebInspector.logManager.addEventListener(WebInspector.LogManager.Event.SessionStarted, this._untrackWeakEntries, this);
448     },
449
450     _untrackWeakEntries: function()
451     {
452         if (!this._remoteObject.isWeakCollection())
453             return;
454
455         if (!this._trackingEntries)
456             return;
457
458         this._trackingEntries = false;
459
460         this._remoteObject.releaseWeakCollectionEntries();
461
462         WebInspector.logManager.removeEventListener(WebInspector.LogManager.Event.Cleared, this._untrackWeakEntries, this);
463         WebInspector.logManager.removeEventListener(WebInspector.LogManager.Event.ActiveLogCleared, this._untrackWeakEntries, this);
464         WebInspector.logManager.removeEventListener(WebInspector.LogManager.Event.SessionStarted, this._untrackWeakEntries, this);
465
466         this.removeChildren();
467
468         if (this.expanded)
469             this.collapse();
470     },
471 }
472
473 WebInspector.CollectionEntryTreeElement = function(entry, index)
474 {
475     TreeElement.call(this, "", null, false);
476
477     console.assert(entry);
478
479     this._name = "" + index;
480     this._key = entry.key;
481     this._value = entry.value;
482
483     this.toggleOnClick = true;
484     this.selectable = false;
485     this.hasChildren = true;
486 }
487
488 WebInspector.CollectionEntryTreeElement.prototype = {
489     constructor: WebInspector.CollectionEntryTreeElement,
490     __proto__: TreeElement.prototype,
491
492     onpopulate: function()
493     {
494         if (this.children.length && !this.shouldRefreshChildren)
495             return;
496
497         this.appendChild(new WebInspector.ObjectPropertyTreeElement({
498             name: "key",
499             value: this._key,
500             enumerable: true,
501             writable: false,
502         }));
503
504         this.appendChild(new WebInspector.ObjectPropertyTreeElement({
505             name: "value",
506             value: this._value,
507             enumerable: true,
508             writable: false,
509         }));
510     },
511
512     onattach: function()
513     {
514         var nameElement = document.createElement("span");
515         nameElement.className = "name";
516         nameElement.textContent = "" + this._name;
517
518         var separatorElement = document.createElement("span");
519         separatorElement.className = "separator";
520         separatorElement.textContent = ": ";
521
522         var valueElement = document.createElement("span");
523         valueElement.className = "value";
524         valueElement.textContent = "{" + this._key.description + " => " + this._value.description + "}";
525
526         this.listItemElement.removeChildren();
527         this.listItemElement.appendChild(nameElement);
528         this.listItemElement.appendChild(separatorElement);
529         this.listItemElement.appendChild(valueElement);
530     }
531 }
532
533 WebInspector.EmptyCollectionTreeElement = function()
534 {
535     TreeElement.call(this, WebInspector.UIString("Empty Collection"), null, false);
536
537     this.selectable = false;
538 }
539
540 WebInspector.EmptyCollectionTreeElement.prototype = {
541     constructor: WebInspector.EmptyCollectionTreeElement,
542     __proto__: TreeElement.prototype
543 }