| /* |
| * Copyright (C) 2015 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| WI.ObjectTreePropertyTreeElement = class ObjectTreePropertyTreeElement extends WI.ObjectTreeBaseTreeElement |
| { |
| constructor(property, propertyPath, mode, prototypeName) |
| { |
| super(property, propertyPath, property); |
| |
| this._mode = mode || WI.ObjectTreeView.Mode.Properties; |
| this._prototypeName = prototypeName; |
| |
| this._fetchStart = 0; |
| this._fetchEnd = WI.ObjectTreeView.showMoreFetchCount; |
| this._fetchEndIndex = 0; |
| |
| this.mainTitle = this._titleFragment(); |
| this.addClassName("object-tree-property"); |
| |
| if (this.property.hasValue()) { |
| this.addClassName(this.property.value.type); |
| if (this.property.value.subtype) |
| this.addClassName(this.property.value.subtype); |
| } else |
| this.addClassName("accessor"); |
| |
| if (this.property.wasThrown) |
| this.addClassName("had-error"); |
| if (this.property.name === "__proto__") |
| this.addClassName("prototype-property"); |
| |
| this._updateTooltips(); |
| this._updateHasChildren(); |
| } |
| |
| // Protected |
| |
| onpopulate() |
| { |
| if (this.children.length && !this.shouldRefreshChildren) |
| return; |
| |
| this._fetchStart = 0; |
| this._fetchEndIndex = 0; |
| this._updateChildren(); |
| } |
| |
| onexpand() |
| { |
| if (this._previewView) |
| this._previewView.showTitle(); |
| } |
| |
| oncollapse() |
| { |
| if (this._previewView) |
| this._previewView.showPreview(); |
| } |
| |
| invokedGetter() |
| { |
| this.mainTitle = this._titleFragment(); |
| |
| var resolvedValue = this.resolvedValue(); |
| this.addClassName(resolvedValue.type); |
| if (resolvedValue.subtype) |
| this.addClassName(resolvedValue.subtype); |
| if (this.hadError()) |
| this.addClassName("had-error"); |
| this.removeClassName("accessor"); |
| |
| this._updateHasChildren(); |
| } |
| |
| // Private |
| |
| _updateHasChildren() |
| { |
| var resolvedValue = this.resolvedValue(); |
| var valueHasChildren = resolvedValue && resolvedValue.hasChildren; |
| var wasThrown = this.hadError(); |
| |
| if (this._mode === WI.ObjectTreeView.Mode.Properties) |
| this.hasChildren = !wasThrown && valueHasChildren; |
| else |
| this.hasChildren = !wasThrown && valueHasChildren && (this.property.name === "__proto__" || this._alwaysDisplayAsProperty()); |
| } |
| |
| _updateTooltips() |
| { |
| var attributes = []; |
| |
| if (this.property.configurable) |
| attributes.push("configurable"); |
| if (this.property.enumerable) |
| attributes.push("enumerable"); |
| if (this.property.writable) |
| attributes.push("writable"); |
| |
| this.iconElement.title = attributes.join(" "); |
| } |
| |
| _titleFragment() |
| { |
| if (this.property.name === "__proto__") |
| return this._createTitlePrototype(); |
| |
| if (this._mode === WI.ObjectTreeView.Mode.Properties) |
| return this._createTitlePropertyStyle(); |
| else |
| return this._createTitleAPIStyle(); |
| } |
| |
| _createTitlePrototype() |
| { |
| console.assert(this.property.hasValue()); |
| console.assert(this.property.name === "__proto__"); |
| |
| var nameElement = document.createElement("span"); |
| nameElement.className = "prototype-name"; |
| nameElement.textContent = WI.UIString("%s Prototype").format(this._sanitizedPrototypeString(this.property.value)); |
| nameElement.title = this.propertyPathString(this.thisPropertyPath()); |
| return nameElement; |
| } |
| |
| _createTitlePropertyStyle() |
| { |
| var container = document.createDocumentFragment(); |
| |
| // Property name. |
| var nameElement = document.createElement("span"); |
| nameElement.className = "property-name"; |
| nameElement.textContent = this.property.name + ": "; |
| nameElement.title = this.propertyPathString(this.thisPropertyPath()); |
| |
| // Property attributes. |
| if (this._mode === WI.ObjectTreeView.Mode.Properties) { |
| if (!this.property.enumerable) |
| nameElement.classList.add("not-enumerable"); |
| } |
| |
| // Value / Getter Value / Getter. |
| var valueOrGetterElement; |
| var resolvedValue = this.resolvedValue(); |
| if (resolvedValue) { |
| if (resolvedValue.preview) { |
| this._previewView = new WI.ObjectPreviewView(resolvedValue, resolvedValue.preview); |
| valueOrGetterElement = this._previewView.element; |
| } else { |
| this._loadPreviewLazilyIfNeeded(); |
| |
| valueOrGetterElement = WI.FormattedValue.createElementForRemoteObject(resolvedValue, this.hadError()); |
| |
| // Special case a function property string. |
| if (resolvedValue.type === "function") |
| valueOrGetterElement.textContent = this._functionPropertyString(); |
| } |
| } else { |
| valueOrGetterElement = document.createElement("span"); |
| if (this.property.hasGetter()) |
| valueOrGetterElement.appendChild(this.createGetterElement(this._mode !== WI.ObjectTreeView.Mode.ClassAPI)); |
| if (this.property.hasSetter()) |
| valueOrGetterElement.appendChild(this.createSetterElement()); |
| } |
| |
| valueOrGetterElement.classList.add("value"); |
| if (this.hadError()) |
| valueOrGetterElement.classList.add("error"); |
| |
| container.appendChild(nameElement); |
| container.appendChild(valueOrGetterElement); |
| return container; |
| } |
| |
| _createTitleAPIStyle() |
| { |
| // Fixed values and special properties display like a property. |
| if (this._alwaysDisplayAsProperty()) |
| return this._createTitlePropertyStyle(); |
| |
| // No API to display. |
| var isFunction = this.property.hasValue() && this.property.value.type === "function"; |
| if (!isFunction && !this.property.hasGetter() && !this.property.hasSetter()) |
| return null; |
| |
| var container = document.createDocumentFragment(); |
| |
| // Function / Getter / Setter. |
| var nameElement = document.createElement("span"); |
| nameElement.className = "property-name"; |
| nameElement.textContent = this.property.name; |
| nameElement.title = this.propertyPathString(this.thisPropertyPath()); |
| container.appendChild(nameElement); |
| |
| if (isFunction) { |
| var paramElement = document.createElement("span"); |
| paramElement.className = "function-parameters"; |
| paramElement.textContent = this._functionParameterString(); |
| container.appendChild(paramElement); |
| } else { |
| var spacer = container.appendChild(document.createElement("span")); |
| spacer.className = "spacer"; |
| if (this.property.hasGetter()) |
| container.appendChild(this.createGetterElement(this._mode !== WI.ObjectTreeView.Mode.ClassAPI)); |
| if (this.property.hasSetter()) |
| container.appendChild(this.createSetterElement()); |
| } |
| |
| return container; |
| } |
| |
| _loadPreviewLazilyIfNeeded() |
| { |
| let resolvedValue = this.resolvedValue(); |
| if (!resolvedValue.canLoadPreview()) |
| return; |
| |
| resolvedValue.updatePreview((preview) => { |
| if (preview) { |
| this.mainTitle = this._titleFragment(); |
| |
| if (this.expanded) |
| this._previewView.showTitle(); |
| } |
| }); |
| } |
| |
| _alwaysDisplayAsProperty() |
| { |
| // Constructor, though a function, is often better treated as an expandable object. |
| if (this.property.name === "constructor") |
| return true; |
| |
| // Non-function objects are often better treated as properties. |
| if (this.property.hasValue() && this.property.value.type !== "function") |
| return true; |
| |
| // Fetched getter value. |
| if (this._getterValue) |
| return true; |
| |
| return false; |
| } |
| |
| _functionPropertyString() |
| { |
| return "function" + this._functionParameterString(); |
| } |
| |
| _functionParameterString() |
| { |
| var resolvedValue = this.resolvedValue(); |
| console.assert(resolvedValue.type === "function"); |
| |
| // For Native methods, the toString is poor. We try to provide good function parameter strings. |
| if (isFunctionStringNativeCode(resolvedValue.description)) { |
| // Native function on a prototype, likely "Foo.prototype.method". |
| if (this._prototypeName) { |
| if (WI.NativePrototypeFunctionParameters[this._prototypeName]) { |
| var params = WI.NativePrototypeFunctionParameters[this._prototypeName][this._property.name]; |
| return params ? "(" + params + ")" : "()"; |
| } |
| } |
| |
| var parentDescription = this._propertyPath.object.description; |
| |
| // Native function property on a native function is likely a "Foo.method". |
| if (isFunctionStringNativeCode(parentDescription)) { |
| var match = parentDescription.match(/^function\s+([^)]+?)\(/); |
| if (match) { |
| var name = match[1]; |
| if (WI.NativeConstructorFunctionParameters[name]) { |
| var params = WI.NativeConstructorFunctionParameters[name][this._property.name]; |
| return params ? "(" + params + ")" : "()"; |
| } |
| } |
| } |
| |
| // Native DOM constructor or on native objects that are not functions. |
| if (parentDescription.endsWith("Constructor") || ["Math", "JSON", "Reflect", "Console"].includes(parentDescription)) { |
| var name = parentDescription; |
| if (WI.NativeConstructorFunctionParameters[name]) { |
| var params = WI.NativeConstructorFunctionParameters[name][this._property.name]; |
| return params ? "(" + params + ")" : "()"; |
| } |
| } |
| } |
| |
| var match = resolvedValue.functionDescription.match(/^function.*?(\([^)]*?\))/); |
| return match ? match[1] : "()"; |
| } |
| |
| _sanitizedPrototypeString(value) |
| { |
| // FIXME: <https://webkit.org/b/141610> For many X, X.prototype is an X when it must be a plain object |
| if (value.type === "function") |
| return "Function"; |
| if (value.subtype === "date") |
| return "Date"; |
| if (value.subtype === "regexp") |
| return "RegExp"; |
| |
| return value.description.replace(/Prototype$/, ""); |
| } |
| |
| _updateChildren() |
| { |
| let resolvedValue = this.resolvedValue(); |
| |
| let wrap = (handler, mode) => (list) => { |
| if (this._fetchEndIndex === 0) |
| this.removeChildren(); |
| |
| if (!list) { |
| let errorMessageElement = WI.ObjectTreeView.createEmptyMessageElement(WI.UIString("Could not fetch properties. Object may no longer exist.")); |
| this.appendChild(new WI.TreeElement(errorMessageElement)); |
| return; |
| } |
| |
| handler.call(this, list, this.resolvedValuePropertyPath(), mode); |
| |
| this.dispatchEventToListeners(WI.ObjectTreeView.Event.Updated); |
| }; |
| |
| let options = { |
| ownProperties: true, |
| generatePreview: true, |
| }; |
| |
| if (isFinite(this._fetchEnd) && this._fetchEnd < resolvedValue.size) { |
| options.fetchStart = this._fetchStart; |
| options.fetchCount = this._fetchEnd - this._fetchStart; |
| } |
| |
| if (resolvedValue.isCollectionType() && this._mode === WI.ObjectTreeView.Mode.Properties) |
| resolvedValue.getCollectionEntries(wrap(this._updateEntries, this._mode), options); |
| else if (this._mode === WI.ObjectTreeView.Mode.ClassAPI || this._mode === WI.ObjectTreeView.Mode.PureAPI) |
| resolvedValue.getPropertyDescriptors(wrap(this._updateProperties, WI.ObjectTreeView.Mode.ClassAPI), options); |
| else if (this.property.name === "__proto__") |
| resolvedValue.getPropertyDescriptors(wrap(this._updateProperties, WI.ObjectTreeView.Mode.PrototypeAPI), options); |
| else |
| resolvedValue.getDisplayablePropertyDescriptors(wrap(this._updateProperties, this._mode), options); |
| } |
| |
| _updateEntries(entries, propertyPath, mode) |
| { |
| let resolvedValue = this.resolvedValue(); |
| |
| entries.forEach((entry, i) => { |
| if (entry.key) { |
| this.insertChild(new WI.ObjectTreeMapKeyTreeElement(entry.key, propertyPath), this._fetchEndIndex++); |
| this.insertChild(new WI.ObjectTreeMapValueTreeElement(entry.value, propertyPath, entry.key), this._fetchEndIndex++); |
| } else |
| this.insertChild(new WI.ObjectTreeSetIndexTreeElement(entry.value, propertyPath), this._fetchEndIndex++); |
| }); |
| |
| if (!this.children.length) { |
| let emptyMessageElement = WI.ObjectTreeView.createEmptyMessageElement(WI.UIString("No Entries")); |
| this.appendChild(new WI.TreeElement(emptyMessageElement)); |
| } else { |
| console.assert(mode === WI.ObjectTreeView.Mode.Properties); |
| WI.ObjectTreeView.addShowMoreIfNeeded({ |
| resolvedValue, |
| representation: this, |
| parentTreeElement: this, |
| handleShowMoreClicked: () => { |
| this._updateChildren(); |
| }, |
| handleShowAllClicked: () => { |
| this.shouldRefreshChildren = true; |
| }, |
| }); |
| } |
| |
| // Show the prototype so users can see the API, but only fetch it the first time. |
| if (this._fetchStart === 0) { |
| resolvedValue.getOwnPropertyDescriptor("__proto__", (propertyDescriptor) => { |
| if (propertyDescriptor) |
| this.appendChild(new WI.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, mode)); |
| }); |
| } |
| } |
| |
| _updateProperties(properties, propertyPath, mode) |
| { |
| properties.sort(WI.ObjectTreeView.comparePropertyDescriptors); |
| |
| var resolvedValue = this.resolvedValue(); |
| var isArray = resolvedValue.isArray(); |
| var isPropertyMode = mode === WI.ObjectTreeView.Mode.Properties || this._getterValue; |
| var isAPI = mode !== WI.ObjectTreeView.Mode.Properties; |
| |
| var prototypeName; |
| if (this.property.name === "__proto__") { |
| if (resolvedValue.description) |
| prototypeName = this._sanitizedPrototypeString(resolvedValue); |
| } |
| |
| var hadProto = false; |
| for (var propertyDescriptor of properties) { |
| // FIXME: If this is a pure API ObjectTree, we should show the native getters. |
| // For now, just skip native binding getters in API mode, since we likely |
| // already showed them in the Properties section. |
| if (isAPI && propertyDescriptor.nativeGetter) |
| continue; |
| |
| if (propertyDescriptor.name === "__proto__") { |
| // COMPATIBILITY (iOS 8): Sometimes __proto__ is not a value, but a get/set property. |
| // In those cases it is actually not useful to show. |
| if (!propertyDescriptor.hasValue()) |
| continue; |
| |
| hadProto = true; |
| this.appendChild(new WI.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, mode, prototypeName)); |
| continue; |
| } |
| |
| if (isArray && isPropertyMode) { |
| if (propertyDescriptor.isIndexProperty()) |
| this.insertChild(new WI.ObjectTreeArrayIndexTreeElement(propertyDescriptor, propertyPath), this._fetchEndIndex++); |
| } else |
| this.insertChild(new WI.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, mode, prototypeName), this._fetchEndIndex++); |
| } |
| |
| if (!this.children.length || (hadProto && this.children.length === 1)) { |
| let emptyMessageElement = WI.ObjectTreeView.createEmptyMessageElement(WI.UIString("No Properties")); |
| this.insertChild(new WI.TreeElement(emptyMessageElement), 0); |
| } else if (isArray && isPropertyMode) { |
| WI.ObjectTreeView.addShowMoreIfNeeded({ |
| resolvedValue, |
| representation: this, |
| parentTreeElement: this, |
| handleShowMoreClicked: () => { |
| this._updateChildren(); |
| }, |
| handleShowAllClicked: () => { |
| this.shouldRefreshChildren = true; |
| }, |
| }); |
| } |
| } |
| }; |