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
26WebInspector.ObjectPropertiesSection = function(object, title, subtitle, emptyPlaceholder, ignoreHasOwnProperty, extraProperties, treeElementConstructor)
27{
28    this.emptyPlaceholder = (emptyPlaceholder || WebInspector.UIString("No Properties"));
29    this.object = object;
30    this.ignoreHasOwnProperty = ignoreHasOwnProperty;
31    this.extraProperties = extraProperties;
32    this.treeElementConstructor = treeElementConstructor || WebInspector.ObjectPropertyTreeElement;
33    this.editable = true;
34
35    WebInspector.PropertiesSection.call(this, title, subtitle);
36}
37
38WebInspector.ObjectPropertiesSection.prototype = {
39    onpopulate: function()
40    {
41        this.update();
42    },
43
44    update: function()
45    {
46        var self = this;
47        function callback(properties)
48        {
49            if (!properties)
50                return;
51            self.updateProperties(properties);
52        }
53        if (this.ignoreHasOwnProperty)
54            this.object.getAllProperties(callback);
55        else
56            this.object.getOwnProperties(callback);
57    },
58
59    updateProperties: function(properties, rootTreeElementConstructor, rootPropertyComparer)
60    {
61        if (!rootTreeElementConstructor)
62            rootTreeElementConstructor = this.treeElementConstructor;
63
64        if (!rootPropertyComparer)
65            rootPropertyComparer = WebInspector.ObjectPropertiesSection.CompareProperties;
66
67        if (this.extraProperties)
68            for (var i = 0; i < this.extraProperties.length; ++i)
69                properties.push(this.extraProperties[i]);
70
71        properties.sort(rootPropertyComparer);
72
73        this.propertiesTreeOutline.removeChildren();
74
75        for (var i = 0; i < properties.length; ++i) {
76            properties[i].parentObject = this.object;
77            this.propertiesTreeOutline.appendChild(new rootTreeElementConstructor(properties[i]));
78        }
79
80        if (!this.propertiesTreeOutline.children.length) {
81            var title = document.createElement("div");
82            title.className = "info";
83            title.textContent = this.emptyPlaceholder;
84            var infoElement = new TreeElement(title, null, false);
85            this.propertiesTreeOutline.appendChild(infoElement);
86        }
87        this.propertiesForTest = properties;
88
89        this.dispatchEventToListeners(WebInspector.Section.Event.VisibleContentDidChange);
90    }
91}
92
93WebInspector.ObjectPropertiesSection.prototype.__proto__ = WebInspector.PropertiesSection.prototype;
94
95WebInspector.ObjectPropertiesSection.CompareProperties = function(propertyA, propertyB)
96{
97    var a = propertyA.name;
98    var b = propertyB.name;
99    if (a === "__proto__")
100        return 1;
101    if (b === "__proto__")
102        return -1;
103
104    // if used elsewhere make sure to
105    //  - convert a and b to strings (not needed here, properties are all strings)
106    //  - check if a == b (not needed here, no two properties can be the same)
107
108    var diff = 0;
109    var chunk = /^\d+|^\D+/;
110    var chunka, chunkb, anum, bnum;
111    while (diff === 0) {
112        if (!a && b)
113            return -1;
114        if (!b && a)
115            return 1;
116        chunka = a.match(chunk)[0];
117        chunkb = b.match(chunk)[0];
118        anum = !isNaN(chunka);
119        bnum = !isNaN(chunkb);
120        if (anum && !bnum)
121            return -1;
122        if (bnum && !anum)
123            return 1;
124        if (anum && bnum) {
125            diff = chunka - chunkb;
126            if (diff === 0 && chunka.length !== chunkb.length) {
127                if (!+chunka && !+chunkb) // chunks are strings of all 0s (special case)
128                    return chunka.length - chunkb.length;
129                else
130                    return chunkb.length - chunka.length;
131            }
132        } else if (chunka !== chunkb)
133            return (chunka < chunkb) ? -1 : 1;
134        a = a.substring(chunka.length);
135        b = b.substring(chunkb.length);
136    }
137    return diff;
138}
139
140WebInspector.ObjectPropertyTreeElement = function(property)
141{
142    this.property = property;
143
144    // Pass an empty title, the title gets made later in onattach.
145    TreeElement.call(this, "", null, false);
146    this.toggleOnClick = true;
147    this.selectable = false;
148}
149
150WebInspector.ObjectPropertyTreeElement.prototype = {
151    onpopulate: function()
152    {
153        if (this.children.length && !this.shouldRefreshChildren)
154            return;
155
156        var callback = function(properties) {
157            this.removeChildren();
158            if (!properties)
159                return;
160
161            properties.sort(WebInspector.ObjectPropertiesSection.CompareProperties);
162            for (var i = 0; i < properties.length; ++i) {
163                this.appendChild(new this.treeOutline.section.treeElementConstructor(properties[i]));
164            }
165        };
166        this.property.value.getOwnProperties(callback.bind(this));
167    },
168
169    ondblclick: function(event)
170    {
171        if (this.property.writable)
172            this.startEditing();
173    },
174
175    onattach: function()
176    {
177        this.update();
178    },
179
180    update: function()
181    {
182        this.nameElement = document.createElement("span");
183        this.nameElement.className = "name";
184        this.nameElement.textContent = this.property.name;
185        if (!this.property.enumerable && (!this.parent.root || !this.treeOutline.section.dontHighlightNonEnumerablePropertiesAtTopLevel))
186            this.nameElement.classList.add("dimmed");
187
188        var separatorElement = document.createElement("span");
189        separatorElement.className = "separator";
190        separatorElement.textContent = ": ";
191
192        this.valueElement = document.createElement("span");
193        this.valueElement.className = "value";
194
195        var description = this.property.value.description;
196        // Render \n as a nice unicode cr symbol.
197        if (this.property.wasThrown)
198            this.valueElement.textContent = "[Exception: " + description + "]";
199        else if (this.property.value.type === "string" && typeof description === "string") {
200            this.valueElement.textContent = "\"" + description.replace(/\n/g, "\u21B5") + "\"";
201            this.valueElement._originalTextContent = "\"" + description + "\"";
202        } else if (this.property.value.type === "function" && typeof description === "string") {
203            this.valueElement.textContent = /.*/.exec(description)[0].replace(/ +$/g, "");
204            this.valueElement._originalTextContent = description;
205        } else
206            this.valueElement.textContent = description;
207
208        if (this.property.value.type === "function")
209            this.valueElement.addEventListener("contextmenu", this._functionContextMenuEventFired.bind(this), false);
210
211        if (this.property.wasThrown)
212            this.valueElement.classList.add("error");
213        if (this.property.value.subtype)
214            this.valueElement.classList.add("console-formatted-" + this.property.value.subtype);
215        else if (this.property.value.type)
216            this.valueElement.classList.add("console-formatted-" + this.property.value.type);
217        if (this.property.value.subtype === "node")
218            this.valueElement.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), false);
219
220        this.listItemElement.removeChildren();
221
222        this.listItemElement.appendChild(this.nameElement);
223        this.listItemElement.appendChild(separatorElement);
224        this.listItemElement.appendChild(this.valueElement);
225        this.hasChildren = this.property.value.hasChildren && !this.property.wasThrown;
226    },
227
228    _contextMenuEventFired: function(event)
229    {
230        function selectNode(nodeId)
231        {
232            if (nodeId)
233                WebInspector.domTreeManager.inspectElement(nodeId);
234        }
235
236        function revealElement()
237        {
238            this.property.value.pushNodeToFrontend(selectNode);
239        }
240
241        var contextMenu = new WebInspector.ContextMenu(event);
242        contextMenu.appendItem(WebInspector.UIString("Reveal in DOM Tree"), revealElement.bind(this));
243        contextMenu.show();
244    },
245
246    _functionContextMenuEventFired: function(event)
247    {
248        function didGetLocation(error, response)
249        {
250            if (error) {
251                console.error(error);
252                return;
253            }
254            WebInspector.panels.scripts.showFunctionDefinition(response);
255        }
256
257        function revealFunction()
258        {
259            DebuggerAgent.getFunctionLocation(this.property.value.objectId, didGetLocation.bind(this));
260        }
261
262        var contextMenu = new WebInspector.ContextMenu(event);
263        contextMenu.appendItem(WebInspector.UIString("Show function definition"), revealFunction.bind(this));
264        contextMenu.show();
265    },
266
267    updateSiblings: function()
268    {
269        if (this.parent.root)
270            this.treeOutline.section.update();
271        else
272            this.parent.shouldRefreshChildren = true;
273    },
274
275    startEditing: function()
276    {
277        if (WebInspector.isBeingEdited(this.valueElement) || !this.treeOutline.section.editable)
278            return;
279
280        var context = { expanded: this.expanded };
281
282        // Lie about our children to prevent expanding on double click and to collapse subproperties.
283        this.hasChildren = false;
284
285        this.listItemElement.classList.add("editing-sub-part");
286
287        // Edit original source.
288        if (typeof this.valueElement._originalTextContent === "string")
289            this.valueElement.textContent = this.valueElement._originalTextContent;
290
291        var config = new WebInspector.EditingConfig(this.editingCommitted.bind(this), this.editingCancelled.bind(this), context);
292        WebInspector.startEditing(this.valueElement, config);
293    },
294
295    editingEnded: function(context)
296    {
297        this.listItemElement.scrollLeft = 0;
298        this.listItemElement.classList.remove("editing-sub-part");
299        if (context.expanded)
300            this.expand();
301    },
302
303    editingCancelled: function(element, context)
304    {
305        this.update();
306        this.editingEnded(context);
307    },
308
309    editingCommitted: function(element, userInput, previousContent, context)
310    {
311        if (userInput === previousContent)
312            return this.editingCancelled(element, context); // nothing changed, so cancel
313
314        this.applyExpression(userInput, true);
315
316        this.editingEnded(context);
317    },
318
319    applyExpression: function(expression, updateInterface)
320    {
321        expression = expression.trim();
322        var expressionLength = expression.length;
323        function callback(error)
324        {
325            if (!updateInterface)
326                return;
327
328            if (error)
329                this.update();
330
331            if (!expressionLength) {
332                // The property was deleted, so remove this tree element.
333                this.parent.removeChild(this);
334            } else {
335                // Call updateSiblings since their value might be based on the value that just changed.
336                this.updateSiblings();
337            }
338        };
339        this.property.parentObject.setPropertyValue(this.property.name, expression.trim(), callback.bind(this));
340    }
341}
342
343WebInspector.ObjectPropertyTreeElement.prototype.__proto__ = TreeElement.prototype;
344