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