blob: 26abf6568c575e1aed984ff293b68c70c7263705 [file] [log] [blame]
/*
* 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;
},
});
}
}
};