| /* |
| * 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.ObjectTreeView = class ObjectTreeView extends WI.Object |
| { |
| constructor(object, mode, propertyPath, forceExpanding) |
| { |
| super(); |
| |
| console.assert(object instanceof WI.RemoteObject); |
| console.assert(!propertyPath || propertyPath instanceof WI.PropertyPath); |
| |
| var providedPropertyPath = propertyPath instanceof WI.PropertyPath; |
| |
| this._object = object; |
| this._mode = mode || WI.ObjectTreeView.defaultModeForObject(object); |
| this._propertyPath = propertyPath || new WI.PropertyPath(this._object, "this"); |
| this._expanded = false; |
| this._hasLosslessPreview = false; |
| this._includeProtoProperty = true; |
| |
| // If ObjectTree is used outside of the console, we do not know when to release |
| // WeakMap entries. Currently collapse would work. For the console, we can just |
| // listen for console clear events. Currently all ObjectTrees are in the console. |
| this._inConsole = true; |
| |
| this._fetchStart = 0; |
| this._fetchEnd = ObjectTreeView.showMoreFetchCount; |
| this._fetchEndIndex = 0; |
| |
| // Always force expanding for classes. |
| if (this._object.isClass()) |
| forceExpanding = true; |
| |
| this._element = document.createElement("div"); |
| this._element.__objectTree = this; |
| this._element.className = "object-tree"; |
| |
| if (this._object.preview) { |
| this._previewView = new WI.ObjectPreviewView(this._object, this._object.preview); |
| this._previewView.setOriginatingObjectInfo(this._object, providedPropertyPath ? propertyPath : null); |
| this._previewView.element.addEventListener("click", this._handlePreviewOrTitleElementClick.bind(this)); |
| this._element.appendChild(this._previewView.element); |
| |
| if (this._previewView.lossless && !this._propertyPath.parent && !forceExpanding) { |
| this._hasLosslessPreview = true; |
| this.element.classList.add("lossless-preview"); |
| } |
| } else { |
| this._titleElement = document.createElement("span"); |
| this._titleElement.className = "title"; |
| this._titleElement.appendChild(WI.FormattedValue.createElementForRemoteObject(this._object)); |
| this._titleElement.addEventListener("click", this._handlePreviewOrTitleElementClick.bind(this)); |
| this._element.appendChild(this._titleElement); |
| } |
| |
| this._outline = new WI.TreeOutline; |
| this._outline.compact = true; |
| this._outline.customIndent = true; |
| this._outline.element.classList.add("object"); |
| this._element.appendChild(this._outline.element); |
| |
| // FIXME: Support editable ObjectTrees. |
| } |
| |
| // Static |
| |
| static defaultModeForObject(object) |
| { |
| if (object.subtype === "class") |
| return WI.ObjectTreeView.Mode.ClassAPI; |
| |
| return WI.ObjectTreeView.Mode.Properties; |
| } |
| |
| static createEmptyMessageElement(message) |
| { |
| var emptyMessageElement = document.createElement("div"); |
| emptyMessageElement.className = "empty-message"; |
| emptyMessageElement.textContent = message; |
| return emptyMessageElement; |
| } |
| |
| static get showMoreFetchCount() { return 100; } |
| |
| static addShowMoreIfNeeded({resolvedValue, representation, parentTreeElement, handleShowMoreClicked, handleShowAllClicked}) |
| { |
| console.assert(resolvedValue instanceof WI.RemoteObject, resolvedValue); |
| console.assert(resolvedValue.isArray() || resolvedValue.isCollectionType()); |
| console.assert(typeof resolvedValue.size === "number" && resolvedValue.size > 0, resolvedValue); |
| console.assert("_fetchStart" in representation, representation); |
| console.assert("_fetchEnd" in representation, representation); |
| console.assert("_fetchEndIndex" in representation, representation); |
| console.assert(typeof handleShowMoreClicked === "function", handleShowMoreClicked); |
| console.assert(typeof handleShowAllClicked === "function", handleShowAllClicked); |
| |
| // COMPATIBILITY (iOS 13): Runtime.getProperties/Runtime.getDisplayableProperties didn't support `fetchStart`/`fetchCount` yet. |
| if (!InspectorBackend.hasCommand("Runtime.getProperties", "fetchStart")) |
| return; |
| |
| let remaining = resolvedValue.size - representation._fetchEnd; |
| if (remaining <= 0) |
| return; |
| |
| let treeElement = new WI.TreeElement(document.createElement("span")); |
| treeElement.selectable = false; |
| parentTreeElement.insertChild(treeElement, representation._fetchEndIndex); |
| |
| // FIXME: <https://webkit.org/b/201965> Web Inspector: allow examining items in the middle of an array/collection without having to show all items before |
| |
| let buttons = []; |
| let createButton = (textContent, handleClick) => { |
| let button = treeElement.title.appendChild(document.createElement("button")); |
| button.textContent = textContent; |
| button.addEventListener("click", (event) => { |
| event.stop(); |
| |
| representation.singleFireEventListener(ObjectTreeView.Event.Updated, function(event) { |
| // The `treeElement` may have already been removed by some other means (e.g. `removeChildren`). |
| if (treeElement.parent === parentTreeElement) |
| parentTreeElement.removeChild(treeElement); |
| }, button); |
| |
| for (let other of buttons) |
| other.disabled = true; |
| |
| let spinner = new WI.IndeterminateProgressSpinner; |
| buttons.lastValue.insertAdjacentElement("afterend", spinner.element); |
| |
| handleClick(); |
| }); |
| buttons.push(button); |
| }; |
| |
| if (!resolvedValue.isWeakCollection() && remaining > ObjectTreeView.showMoreFetchCount) { |
| createButton(WI.UIString("Show %d More").format(ObjectTreeView.showMoreFetchCount), () => { |
| representation._fetchStart = representation._fetchEnd; |
| representation._fetchEnd = representation._fetchStart + ObjectTreeView.showMoreFetchCount; |
| handleShowMoreClicked(); |
| }); |
| } |
| |
| createButton(WI.UIString("Show All (%d More)").format(remaining), () => { |
| representation._fetchStart = 0; |
| representation._fetchEnd = Infinity; |
| representation._fetchEndIndex = 0; |
| handleShowAllClicked(); |
| }); |
| } |
| |
| static comparePropertyDescriptors(propertyA, propertyB) |
| { |
| var a = propertyA.name; |
| var b = propertyB.name; |
| |
| // Put __proto__ at the bottom. |
| if (a === "__proto__") |
| return 1; |
| if (b === "__proto__") |
| return -1; |
| |
| // Put Internal properties at the top. |
| if (propertyA.isInternalProperty && !propertyB.isInternalProperty) |
| return -1; |
| if (propertyB.isInternalProperty && !propertyA.isInternalProperty) |
| return 1; |
| |
| // Put Symbol properties at the bottom. |
| if (propertyA.symbol && !propertyB.symbol) |
| return 1; |
| if (propertyB.symbol && !propertyA.symbol) |
| return -1; |
| |
| // Symbol properties may have the same description string but be different objects. |
| if (a === b) |
| return 0; |
| |
| var diff = 0; |
| var chunk = /^\d+|^\D+/; |
| var chunka, chunkb, anum, bnum; |
| while (diff === 0) { |
| if (!a && b) |
| return -1; |
| if (!b && a) |
| return 1; |
| chunka = a.match(chunk)[0]; |
| chunkb = b.match(chunk)[0]; |
| anum = !isNaN(chunka); |
| bnum = !isNaN(chunkb); |
| if (anum && !bnum) |
| return -1; |
| if (bnum && !anum) |
| return 1; |
| if (anum && bnum) { |
| diff = chunka - chunkb; |
| if (diff === 0 && chunka.length !== chunkb.length) { |
| if (!+chunka && !+chunkb) // chunks are strings of all 0s (special case) |
| return chunka.length - chunkb.length; |
| else |
| return chunkb.length - chunka.length; |
| } |
| } else if (chunka !== chunkb) |
| return chunka < chunkb ? -1 : 1; |
| a = a.substring(chunka.length); |
| b = b.substring(chunkb.length); |
| } |
| return diff; |
| } |
| |
| // Public |
| |
| get object() |
| { |
| return this._object; |
| } |
| |
| get element() |
| { |
| return this._element; |
| } |
| |
| get treeOutline() |
| { |
| return this._outline; |
| } |
| |
| get expanded() |
| { |
| return this._expanded; |
| } |
| |
| expand() |
| { |
| if (this._expanded) |
| return; |
| |
| if (this._hasLosslessPreview) |
| return; |
| |
| this._expanded = true; |
| this._element.classList.add("expanded"); |
| |
| if (this._previewView) |
| this._previewView.showTitle(); |
| |
| this._trackWeakEntries(); |
| |
| this.update(); |
| } |
| |
| collapse() |
| { |
| if (!this._expanded) |
| return; |
| |
| this._expanded = false; |
| this._element.classList.remove("expanded"); |
| |
| if (this._previewView) |
| this._previewView.showPreview(); |
| |
| this._untrackWeakEntries(); |
| } |
| |
| showOnlyProperties() |
| { |
| this._inConsole = false; |
| |
| this._element.classList.add("properties-only"); |
| |
| this._includeProtoProperty = false; |
| } |
| |
| showOnlyJSON() |
| { |
| console.assert(this._mode === WI.ObjectTreeView.Mode.Properties, this._mode); |
| console.assert(!this._hasLosslessPreview); |
| |
| this.showOnlyProperties(); |
| |
| this._element.classList.add("json-only"); |
| } |
| |
| appendTitleSuffix(suffixElement) |
| { |
| if (this._previewView) |
| this._previewView.element.appendChild(suffixElement); |
| else |
| this._titleElement.appendChild(suffixElement); |
| } |
| |
| appendExtraPropertyDescriptor(propertyDescriptor) |
| { |
| if (!this._extraProperties) |
| this._extraProperties = []; |
| |
| this._extraProperties.push(propertyDescriptor); |
| } |
| |
| setPrototypeNameOverride(override) |
| { |
| this._prototypeNameOverride = override; |
| } |
| |
| resetPropertyPath() |
| { |
| this._propertyPath.pathComponent = "this"; |
| } |
| |
| // Protected |
| |
| update() |
| { |
| this._fetchStart = 0; |
| this._fetchEndIndex = 0; |
| this._updateChildren(); |
| } |
| |
| // Private |
| |
| _updateChildren() |
| { |
| let options = { |
| ownProperties: true, |
| generatePreview: true, |
| }; |
| |
| if (isFinite(this._fetchEnd) && this._fetchEnd < this._object.size) { |
| options.fetchStart = this._fetchStart; |
| options.fetchCount = this._fetchEnd - this._fetchStart; |
| } |
| |
| let wrap = (handler) => (list) => { |
| if (this._fetchEndIndex === 0) |
| this._outline.removeChildren(); |
| |
| if (!list) { |
| let errorMessageElement = WI.ObjectTreeView.createEmptyMessageElement(WI.UIString("Could not fetch properties. Object may no longer exist.")); |
| this._outline.appendChild(new WI.TreeElement(errorMessageElement)); |
| return; |
| } |
| |
| handler.call(this, list, this._propertyPath); |
| |
| this.dispatchEventToListeners(ObjectTreeView.Event.Updated); |
| }; |
| |
| if (this._object.isCollectionType() && this._mode === ObjectTreeView.Mode.Properties) |
| this._object.getCollectionEntries(wrap(this._updateEntries), options); |
| else if (this._object.isClass()) |
| this._object.classPrototype.getDisplayablePropertyDescriptors(wrap(this._updateProperties), options); |
| else if (this._mode === ObjectTreeView.Mode.PureAPI) |
| this._object.getPropertyDescriptors(wrap(this._updateProperties), options); |
| else |
| this._object.getDisplayablePropertyDescriptors(wrap(this._updateProperties), options); |
| } |
| |
| _updateEntries(entries, propertyPath) |
| { |
| entries.forEach((entry, i) => { |
| if (entry.key) { |
| this._outline.insertChild(new WI.ObjectTreeMapKeyTreeElement(entry.key, propertyPath), this._fetchEndIndex++); |
| this._outline.insertChild(new WI.ObjectTreeMapValueTreeElement(entry.value, propertyPath, entry.key), this._fetchEndIndex++); |
| } else |
| this._outline.insertChild(new WI.ObjectTreeSetIndexTreeElement(entry.value, propertyPath), this._fetchEndIndex++); |
| }); |
| |
| if (!this._outline.children.length) { |
| let emptyMessageElement = ObjectTreeView.createEmptyMessageElement(WI.UIString("No Entries")); |
| this._outline.appendChild(new WI.TreeElement(emptyMessageElement)); |
| } else { |
| console.assert(this._mode === WI.ObjectTreeView.Mode.Properties); |
| ObjectTreeView.addShowMoreIfNeeded({ |
| resolvedValue: this._object, |
| representation: this, |
| parentTreeElement: this._outline, |
| handleShowMoreClicked: () => { |
| this._updateChildren(); |
| }, |
| handleShowAllClicked: () => { |
| this.update(); |
| }, |
| }); |
| } |
| |
| // Show the prototype so users can see the API, but only fetch it the first time. |
| if (this._fetchStart === 0) { |
| this._object.getOwnPropertyDescriptor("__proto__", (propertyDescriptor) => { |
| if (propertyDescriptor) |
| this._outline.appendChild(new WI.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, this._mode)); |
| }); |
| } |
| } |
| |
| _updateProperties(properties, propertyPath) |
| { |
| if (this._extraProperties) |
| properties.pushAll(this._extraProperties); |
| |
| properties.sort(WI.ObjectTreeView.comparePropertyDescriptors); |
| |
| var isArray = this._object.isArray(); |
| var isPropertyMode = this._mode === WI.ObjectTreeView.Mode.Properties; |
| |
| var hadProto = false; |
| for (var propertyDescriptor of properties) { |
| if (propertyDescriptor.name === "__proto__") { |
| if (!this._includeProtoProperty) |
| continue; |
| hadProto = true; |
| this._outline.appendChild(new WI.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, this._mode, this._prototypeNameOverride)); |
| continue; |
| } |
| |
| if (isArray && isPropertyMode) { |
| if (propertyDescriptor.isIndexProperty()) |
| this._outline.insertChild(new WI.ObjectTreeArrayIndexTreeElement(propertyDescriptor, propertyPath), this._fetchEndIndex++); |
| } else |
| this._outline.insertChild(new WI.ObjectTreePropertyTreeElement(propertyDescriptor, propertyPath, this._mode, this._prototypeNameOverride), this._fetchEndIndex++); |
| } |
| |
| if (!this._outline.children.length || (hadProto && this._outline.children.length === 1)) { |
| let emptyMessageElement = ObjectTreeView.createEmptyMessageElement(WI.UIString("No Properties")); |
| this._outline.insertChild(new WI.TreeElement(emptyMessageElement), 0); |
| } else if (isArray && isPropertyMode) { |
| ObjectTreeView.addShowMoreIfNeeded({ |
| resolvedValue: this._object, |
| representation: this, |
| parentTreeElement: this._outline, |
| handleShowMoreClicked: () => { |
| this._updateChildren(); |
| }, |
| handleShowAllClicked: () => { |
| this.update(); |
| }, |
| }); |
| } |
| } |
| |
| _handlePreviewOrTitleElementClick(event) |
| { |
| if (this._hasLosslessPreview) |
| return; |
| |
| if (!this._expanded) |
| this.expand(); |
| else |
| this.collapse(); |
| |
| event.stopPropagation(); |
| } |
| |
| _trackWeakEntries() |
| { |
| if (this._trackingEntries) |
| return; |
| |
| if (!this._object.isWeakCollection()) |
| return; |
| |
| this._trackingEntries = true; |
| |
| if (this._inConsole) { |
| WI.consoleManager.addEventListener(WI.ConsoleManager.Event.Cleared, this._untrackWeakEntries, this); |
| WI.consoleManager.addEventListener(WI.ConsoleManager.Event.SessionStarted, this._untrackWeakEntries, this); |
| } |
| } |
| |
| _untrackWeakEntries() |
| { |
| if (!this._trackingEntries) |
| return; |
| |
| if (!this._object.isWeakCollection()) |
| return; |
| |
| this._trackingEntries = false; |
| |
| this._object.releaseWeakCollectionEntries(); |
| |
| if (this._inConsole) { |
| WI.consoleManager.removeEventListener(WI.ConsoleManager.Event.Cleared, this._untrackWeakEntries, this); |
| WI.consoleManager.removeEventListener(WI.ConsoleManager.Event.SessionStarted, this._untrackWeakEntries, this); |
| } |
| |
| // FIXME: This only tries to release weak entries if this object was a WeakMap. |
| // If there was a WeakMap expanded in a sub-object, we will never release those values. |
| // Should we attempt walking the entire tree and release weak collections? |
| } |
| }; |
| |
| WI.ObjectTreeView.Mode = { |
| Properties: Symbol("object-tree-properties"), // Properties |
| PrototypeAPI: Symbol("object-tree-prototype-api"), // API view on a live object instance, so getters can be invoked. |
| ClassAPI: Symbol("object-tree-class-api"), // API view without an object instance, can not invoke getters. |
| PureAPI: Symbol("object-tree-pure-api"), // API view without special displayable property handling, just own properties. Getters can be invoked if the property path has a non-prototype object. |
| }; |
| |
| WI.ObjectTreeView.Event = { |
| Updated: "object-tree-updated", |
| }; |