Web Inspector: Create Separate Model and View Objects for RemoteObjects / ObjectPrevi...
[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.getAllProperties(callback.bind(this));
56         else
57             this.object.getOwnAndGetterProperties(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.getOwnProperties(callback.bind(this));
175         else
176             this.property.value.getOwnAndGetterProperties(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("console-formatted-" + this.property.value.subtype);
225         else if (this.property.value.type)
226             this.valueElement.classList.add("console-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                 console.error(error);
262                 return;
263             }
264             WebInspector.panels.scripts.showFunctionDefinition(response);
265         }
266
267         function revealFunction()
268         {
269             DebuggerAgent.getFunctionLocation(this.property.value.objectId, didGetLocation.bind(this));
270         }
271
272         var contextMenu = new WebInspector.ContextMenu(event);
273         contextMenu.appendItem(WebInspector.UIString("Show function definition"), revealFunction.bind(this));
274         contextMenu.show();
275     },
276
277     updateSiblings: function()
278     {
279         if (this.parent.root)
280             this.treeOutline.section.update();
281         else
282             this.parent.shouldRefreshChildren = true;
283     },
284
285     startEditing: function()
286     {
287         if (WebInspector.isBeingEdited(this.valueElement) || !this.treeOutline.section.editable)
288             return;
289
290         var context = { expanded: this.expanded };
291
292         // Lie about our children to prevent expanding on double click and to collapse subproperties.
293         this.hasChildren = false;
294
295         this.listItemElement.classList.add("editing-sub-part");
296
297         // Edit original source.
298         if (typeof this.valueElement._originalTextContent === "string")
299             this.valueElement.textContent = this.valueElement._originalTextContent;
300
301         var config = new WebInspector.EditingConfig(this.editingCommitted.bind(this), this.editingCancelled.bind(this), context);
302         WebInspector.startEditing(this.valueElement, config);
303     },
304
305     editingEnded: function(context)
306     {
307         this.listItemElement.scrollLeft = 0;
308         this.listItemElement.classList.remove("editing-sub-part");
309         if (context.expanded)
310             this.expand();
311     },
312
313     editingCancelled: function(element, context)
314     {
315         this.update();
316         this.editingEnded(context);
317     },
318
319     editingCommitted: function(element, userInput, previousContent, context)
320     {
321         if (userInput === previousContent)
322             return this.editingCancelled(element, context); // nothing changed, so cancel
323
324         this.applyExpression(userInput, true);
325
326         this.editingEnded(context);
327     },
328
329     applyExpression: function(expression, updateInterface)
330     {
331         expression = expression.trim();
332         var expressionLength = expression.length;
333         function callback(error)
334         {
335             if (!updateInterface)
336                 return;
337
338             if (error)
339                 this.update();
340
341             if (!expressionLength) {
342                 // The property was deleted, so remove this tree element.
343                 this.parent.removeChild(this);
344             } else {
345                 // Call updateSiblings since their value might be based on the value that just changed.
346                 this.updateSiblings();
347             }
348         }
349         this.property.parentObject.setPropertyValue(this.property.name, expression.trim(), callback.bind(this));
350     }
351 };
352
353 WebInspector.ObjectPropertyTreeElement.prototype.__proto__ = TreeElement.prototype;
354
355 WebInspector.CollectionEntriesMainTreeElement = function(remoteObject)
356 {
357     TreeElement.call(this, "<entries>", null, false);
358
359     console.assert(remoteObject);
360
361     this._remoteObject = remoteObject;
362     this._requestingEntries = false;
363     this._trackingEntries = false;
364
365     this.toggleOnClick = true;
366     this.selectable = false;
367     this.hasChildren = true;
368     this.expand();
369
370     // FIXME: When a parent TreeElement is collapsed, we do not get a chance
371     // to releaseWeakCollectionEntries. We should.
372 }
373
374 WebInspector.CollectionEntriesMainTreeElement.prototype = {
375     constructor: WebInspector.CollectionEntriesMainTreeElement,
376     __proto__: TreeElement.prototype,
377
378     onexpand: function()
379     {
380         if (this.children.length && !this.shouldRefreshChildren)
381             return;
382
383         if (this._requestingEntries)
384             return;
385
386         this._requestingEntries = true;
387
388         function callback(entries) {
389             this._requestingEntries = false;
390
391             this.removeChildren();
392
393             if (!entries || !entries.length) {
394                 this.appendChild(new WebInspector.EmptyCollectionTreeElement);
395                 return;
396             }
397
398             this._trackWeakEntries();
399
400             for (var i = 0; i < entries.length; ++i) {
401                 var entry = entries[i];
402                 if (entry.key)
403                     this.appendChild(new WebInspector.CollectionEntryTreeElement(entry, i));
404                 else {
405                     this.appendChild(new WebInspector.ObjectPropertyTreeElement({
406                         name: "" + i,
407                         value: entry.value,
408                         enumerable: true,
409                         writable: false,
410                     }));
411                 }
412             }
413         }
414         
415         this._remoteObject.getCollectionEntries(0, 100, callback.bind(this));
416     },
417
418     oncollapse: function()
419     {
420         this._untrackWeakEntries();
421     },
422
423     ondetach: function()
424     {
425         this._untrackWeakEntries();
426     },
427
428     // Private.
429
430     _trackWeakEntries: function()
431     {
432         if (!this._remoteObject.isWeakCollection())
433             return;
434
435         if (this._trackingEntries)
436             return;
437
438         this._trackingEntries = true;
439
440         WebInspector.logManager.addEventListener(WebInspector.LogManager.Event.Cleared, this._untrackWeakEntries, this);
441         WebInspector.logManager.addEventListener(WebInspector.LogManager.Event.ActiveLogCleared, this._untrackWeakEntries, this);
442         WebInspector.logManager.addEventListener(WebInspector.LogManager.Event.SessionStarted, this._untrackWeakEntries, this);
443     },
444
445     _untrackWeakEntries: function()
446     {
447         if (!this._remoteObject.isWeakCollection())
448             return;
449
450         if (!this._trackingEntries)
451             return;
452
453         this._trackingEntries = false;
454
455         this._remoteObject.releaseWeakCollectionEntries();
456
457         WebInspector.logManager.removeEventListener(WebInspector.LogManager.Event.Cleared, this._untrackWeakEntries, this);
458         WebInspector.logManager.removeEventListener(WebInspector.LogManager.Event.ActiveLogCleared, this._untrackWeakEntries, this);
459         WebInspector.logManager.removeEventListener(WebInspector.LogManager.Event.SessionStarted, this._untrackWeakEntries, this);
460
461         this.removeChildren();
462
463         if (this.expanded)
464             this.collapse();
465     },
466 }
467
468 WebInspector.CollectionEntryTreeElement = function(entry, index)
469 {
470     TreeElement.call(this, "", null, false);
471
472     console.assert(entry);
473
474     this._name = "" + index;
475     this._key = entry.key;
476     this._value = entry.value;
477
478     this.toggleOnClick = true;
479     this.selectable = false;
480     this.hasChildren = true;
481 }
482
483 WebInspector.CollectionEntryTreeElement.prototype = {
484     constructor: WebInspector.CollectionEntryTreeElement,
485     __proto__: TreeElement.prototype,
486
487     onpopulate: function()
488     {
489         if (this.children.length && !this.shouldRefreshChildren)
490             return;
491
492         this.appendChild(new WebInspector.ObjectPropertyTreeElement({
493             name: "key",
494             value: this._key,
495             enumerable: true,
496             writable: false,
497         }));
498
499         this.appendChild(new WebInspector.ObjectPropertyTreeElement({
500             name: "value",
501             value: this._value,
502             enumerable: true,
503             writable: false,
504         }));
505     },
506
507     onattach: function()
508     {
509         var nameElement = document.createElement("span");
510         nameElement.className = "name";
511         nameElement.textContent = "" + this._name;
512
513         var separatorElement = document.createElement("span");
514         separatorElement.className = "separator";
515         separatorElement.textContent = ": ";
516
517         var valueElement = document.createElement("span");
518         valueElement.className = "value";
519         valueElement.textContent = "{" + this._key.description + " => " + this._value.description + "}";
520
521         this.listItemElement.removeChildren();
522         this.listItemElement.appendChild(nameElement);
523         this.listItemElement.appendChild(separatorElement);
524         this.listItemElement.appendChild(valueElement);
525     }
526 }
527
528 WebInspector.EmptyCollectionTreeElement = function()
529 {
530     TreeElement.call(this, WebInspector.UIString("Empty Collection"), null, false);
531
532     this.selectable = false;
533 }
534
535 WebInspector.EmptyCollectionTreeElement.prototype = {
536     constructor: WebInspector.EmptyCollectionTreeElement,
537     __proto__: TreeElement.prototype
538 }