| /* |
| * Copyright (C) 2008 Apple Inc. All Rights Reserved. |
| * Copyright (C) 2009 Joseph Pecoraro |
| * |
| * 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. ``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 |
| * 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. |
| */ |
| |
| /** |
| * @unrestricted |
| */ |
| export default class ObjectPropertiesSection extends UI.TreeOutlineInShadow { |
| /** |
| * @param {!SDK.RemoteObject} object |
| * @param {?string|!Element=} title |
| * @param {!Components.Linkifier=} linkifier |
| * @param {?string=} emptyPlaceholder |
| * @param {boolean=} ignoreHasOwnProperty |
| * @param {!Array.<!SDK.RemoteObjectProperty>=} extraProperties |
| * @param {boolean=} showOverflow |
| */ |
| constructor(object, title, linkifier, emptyPlaceholder, ignoreHasOwnProperty, extraProperties, showOverflow) { |
| super(); |
| this._object = object; |
| this._editable = true; |
| if (!showOverflow) { |
| this.hideOverflow(); |
| } |
| this.setFocusable(true); |
| this.setShowSelectionOnKeyboardFocus(true); |
| this._objectTreeElement = |
| new RootElement(object, linkifier, emptyPlaceholder, ignoreHasOwnProperty, extraProperties); |
| this.appendChild(this._objectTreeElement); |
| if (typeof title === 'string' || !title) { |
| this.titleElement = this.element.createChild('span'); |
| this.titleElement.textContent = title || ''; |
| } else { |
| this.titleElement = title; |
| this.element.appendChild(title); |
| } |
| if (!this.titleElement.hasAttribute('tabIndex')) { |
| this.titleElement.tabIndex = -1; |
| } |
| |
| this.element._section = this; |
| this.registerRequiredCSS('object_ui/objectValue.css'); |
| this.registerRequiredCSS('object_ui/objectPropertiesSection.css'); |
| this.rootElement().childrenListElement.classList.add('source-code', 'object-properties-section'); |
| } |
| |
| /** |
| * @param {!SDK.RemoteObject} object |
| * @param {!Components.Linkifier=} linkifier |
| * @param {boolean=} skipProto |
| * @param {boolean=} readOnly |
| * @return {!Element} |
| */ |
| static defaultObjectPresentation(object, linkifier, skipProto, readOnly) { |
| const objectPropertiesSection = |
| ObjectPropertiesSection.defaultObjectPropertiesSection(object, linkifier, skipProto, readOnly); |
| if (!object.hasChildren) { |
| return objectPropertiesSection.titleElement; |
| } else { |
| return objectPropertiesSection.element; |
| } |
| } |
| |
| /** |
| * @param {!SDK.RemoteObject} object |
| * @param {!Components.Linkifier=} linkifier |
| * @param {boolean=} skipProto |
| * @param {boolean=} readOnly |
| * @return {!ObjectPropertiesSection} |
| */ |
| static defaultObjectPropertiesSection(object, linkifier, skipProto, readOnly) { |
| const titleElement = createElementWithClass('span', 'source-code'); |
| const shadowRoot = UI.createShadowRootWithCoreStyles(titleElement, 'object_ui/objectValue.css'); |
| shadowRoot.appendChild( |
| ObjectPropertiesSection.createValueElement(object, /* wasThrown */ false, /* showPreview */ true)); |
| const objectPropertiesSection = new ObjectPropertiesSection(object, titleElement, linkifier); |
| objectPropertiesSection.editable = false; |
| if (skipProto) { |
| objectPropertiesSection.skipProto(); |
| } |
| if (readOnly) { |
| objectPropertiesSection.setEditable(false); |
| } |
| |
| return objectPropertiesSection; |
| } |
| |
| /** |
| * @param {!SDK.RemoteObjectProperty} propertyA |
| * @param {!SDK.RemoteObjectProperty} propertyB |
| * @return {number} |
| */ |
| static CompareProperties(propertyA, propertyB) { |
| const a = propertyA.name; |
| const b = propertyB.name; |
| if (a === '__proto__') { |
| return 1; |
| } |
| if (b === '__proto__') { |
| return -1; |
| } |
| if (!propertyA.enumerable && propertyB.enumerable) { |
| return 1; |
| } |
| if (!propertyB.enumerable && propertyA.enumerable) { |
| return -1; |
| } |
| if (a.startsWith('_') && !b.startsWith('_')) { |
| return 1; |
| } |
| if (b.startsWith('_') && !a.startsWith('_')) { |
| return -1; |
| } |
| if (propertyA.symbol && !propertyB.symbol) { |
| return 1; |
| } |
| if (propertyB.symbol && !propertyA.symbol) { |
| return -1; |
| } |
| if (propertyA.private && !propertyB.private) { |
| return 1; |
| } |
| if (propertyB.private && !propertyA.private) { |
| return -1; |
| } |
| return String.naturalOrderComparator(a, b); |
| } |
| |
| /** |
| * @param {?string} name |
| * @param {boolean=} isPrivate |
| * @return {!Element} |
| */ |
| static createNameElement(name, isPrivate) { |
| if (name === null) { |
| return UI.html`<span class="name"></span>`; |
| } |
| if (/^\s|\s$|^$|\n/.test(name)) { |
| return UI.html`<span class="name">"${name.replace(/\n/g, '\u21B5')}"</span>`; |
| } |
| if (isPrivate) { |
| return UI.html`<span class="name"> |
| <span class="private-property-hash">${name[0]}</span>${name.substring(1)} |
| </span>`; |
| } |
| return UI.html`<span class="name">${name}</span>`; |
| } |
| |
| /** |
| * @param {?string=} description |
| * @param {boolean=} includePreview |
| * @param {string=} defaultName |
| * @return {!Element} valueElement |
| */ |
| static valueElementForFunctionDescription(description, includePreview, defaultName) { |
| const valueElement = createElementWithClass('span', 'object-value-function'); |
| description = description || ''; |
| const text = description.replace(/^function [gs]et /, 'function ') |
| .replace(/^function [gs]et\(/, 'function\(') |
| .replace(/^[gs]et /, ''); |
| defaultName = defaultName || ''; |
| |
| // This set of best-effort regular expressions captures common function descriptions. |
| // Ideally, some parser would provide prefix, arguments, function body text separately. |
| const asyncMatch = text.match(/^(async\s+function)/); |
| const isGenerator = text.startsWith('function*'); |
| const isGeneratorShorthand = text.startsWith('*'); |
| const isBasic = !isGenerator && text.startsWith('function'); |
| const isClass = text.startsWith('class ') || text.startsWith('class{'); |
| const firstArrowIndex = text.indexOf('=>'); |
| const isArrow = !asyncMatch && !isGenerator && !isBasic && !isClass && firstArrowIndex > 0; |
| |
| let textAfterPrefix; |
| if (isClass) { |
| textAfterPrefix = text.substring('class'.length); |
| const classNameMatch = /^[^{\s]+/.exec(textAfterPrefix.trim()); |
| let className = defaultName; |
| if (classNameMatch) { |
| className = classNameMatch[0].trim() || defaultName; |
| } |
| addElements('class', textAfterPrefix, className); |
| } else if (asyncMatch) { |
| textAfterPrefix = text.substring(asyncMatch[1].length); |
| addElements('async \u0192', textAfterPrefix, nameAndArguments(textAfterPrefix)); |
| } else if (isGenerator) { |
| textAfterPrefix = text.substring('function*'.length); |
| addElements('\u0192*', textAfterPrefix, nameAndArguments(textAfterPrefix)); |
| } else if (isGeneratorShorthand) { |
| textAfterPrefix = text.substring('*'.length); |
| addElements('\u0192*', textAfterPrefix, nameAndArguments(textAfterPrefix)); |
| } else if (isBasic) { |
| textAfterPrefix = text.substring('function'.length); |
| addElements('\u0192', textAfterPrefix, nameAndArguments(textAfterPrefix)); |
| } else if (isArrow) { |
| const maxArrowFunctionCharacterLength = 60; |
| let abbreviation = text; |
| if (defaultName) { |
| abbreviation = defaultName + '()'; |
| } else if (text.length > maxArrowFunctionCharacterLength) { |
| abbreviation = text.substring(0, firstArrowIndex + 2) + ' {\u2026}'; |
| } |
| addElements('', text, abbreviation); |
| } else { |
| addElements('\u0192', text, nameAndArguments(text)); |
| } |
| valueElement.title = description.trimEndWithMaxLength(500); |
| return valueElement; |
| |
| /** |
| * @param {string} contents |
| * @return {string} |
| */ |
| function nameAndArguments(contents) { |
| const startOfArgumentsIndex = contents.indexOf('('); |
| const endOfArgumentsMatch = contents.match(/\)\s*{/); |
| if (startOfArgumentsIndex !== -1 && endOfArgumentsMatch && endOfArgumentsMatch.index > startOfArgumentsIndex) { |
| const name = contents.substring(0, startOfArgumentsIndex).trim() || defaultName; |
| const args = contents.substring(startOfArgumentsIndex, endOfArgumentsMatch.index + 1); |
| return name + args; |
| } |
| return defaultName + '()'; |
| } |
| |
| /** |
| * @param {string} prefix |
| * @param {string} body |
| * @param {string} abbreviation |
| */ |
| function addElements(prefix, body, abbreviation) { |
| const maxFunctionBodyLength = 200; |
| if (prefix.length) { |
| valueElement.createChild('span', 'object-value-function-prefix').textContent = prefix + ' '; |
| } |
| if (includePreview) { |
| valueElement.createTextChild(body.trim().trimEndWithMaxLength(maxFunctionBodyLength)); |
| } else { |
| valueElement.createTextChild(abbreviation.replace(/\n/g, ' ')); |
| } |
| } |
| } |
| |
| /** |
| * @param {!SDK.RemoteObject} value |
| * @param {boolean} wasThrown |
| * @param {boolean} showPreview |
| * @param {!Element=} parentElement |
| * @param {!Components.Linkifier=} linkifier |
| * @return {!Element} |
| */ |
| static createValueElementWithCustomSupport(value, wasThrown, showPreview, parentElement, linkifier) { |
| if (value.customPreview()) { |
| const result = (new ObjectUI.CustomPreviewComponent(value)).element; |
| result.classList.add('object-properties-section-custom-section'); |
| return result; |
| } |
| return ObjectPropertiesSection.createValueElement(value, wasThrown, showPreview, parentElement, linkifier); |
| } |
| |
| /** |
| * @param {!SDK.RemoteObject} value |
| * @param {boolean} wasThrown |
| * @param {boolean} showPreview |
| * @param {!Element=} parentElement |
| * @param {!Components.Linkifier=} linkifier |
| * @return {!Element} |
| */ |
| static createValueElement(value, wasThrown, showPreview, parentElement, linkifier) { |
| let valueElement; |
| const type = value.type; |
| const subtype = value.subtype; |
| const description = value.description; |
| if (type === 'object' && subtype === 'internal#location') { |
| const rawLocation = value.debuggerModel().createRawLocationByScriptId( |
| value.value.scriptId, value.value.lineNumber, value.value.columnNumber); |
| if (rawLocation && linkifier) { |
| return linkifier.linkifyRawLocation(rawLocation, ''); |
| } |
| valueElement = createUnknownInternalLocationElement(); |
| } else if (type === 'string' && typeof description === 'string') { |
| valueElement = createStringElement(); |
| } else if (type === 'function') { |
| valueElement = ObjectPropertiesSection.valueElementForFunctionDescription(description); |
| } else if (type === 'object' && subtype === 'node' && description) { |
| valueElement = createNodeElement(); |
| } else if (type === 'number' && description && description.indexOf('e') !== -1) { |
| valueElement = createNumberWithExponentElement(); |
| if (parentElement) // FIXME: do it in the caller. |
| { |
| parentElement.classList.add('hbox'); |
| } |
| } else { |
| valueElement = createElementWithClass('span', 'object-value-' + (subtype || type)); |
| valueElement.title = description || ''; |
| if (value.preview && showPreview) { |
| const previewFormatter = new ObjectUI.RemoteObjectPreviewFormatter(); |
| previewFormatter.appendObjectPreview(valueElement, value.preview, false /* isEntry */); |
| } else if (description.length > ObjectUI.ObjectPropertiesSection._maxRenderableStringLength) { |
| valueElement.appendChild(UI.createExpandableText(description, 50)); |
| } else { |
| valueElement.textContent = description; |
| } |
| } |
| |
| if (wasThrown) { |
| const wrapperElement = createElementWithClass('span', 'error value'); |
| wrapperElement.appendChild(UI.formatLocalized('[Exception: %s]', [valueElement])); |
| return wrapperElement; |
| } |
| valueElement.classList.add('value'); |
| return valueElement; |
| |
| /** |
| * @return {!Element} |
| */ |
| function createUnknownInternalLocationElement() { |
| const valueElement = createElementWithClass('span'); |
| valueElement.textContent = '<' + Common.UIString('unknown') + '>'; |
| valueElement.title = description || ''; |
| return valueElement; |
| } |
| |
| /** |
| * @return {!Element} |
| */ |
| function createStringElement() { |
| const valueElement = createElementWithClass('span', 'object-value-string'); |
| const text = description.replace(/\n/g, '\u21B5'); |
| valueElement.createChild('span', 'object-value-string-quote').textContent = '"'; |
| if (description.length > ObjectUI.ObjectPropertiesSection._maxRenderableStringLength) { |
| valueElement.appendChild(UI.createExpandableText(text, 50)); |
| } else { |
| valueElement.createTextChild(text); |
| } |
| valueElement.createChild('span', 'object-value-string-quote').textContent = '"'; |
| valueElement.title = description || ''; |
| return valueElement; |
| } |
| |
| /** |
| * @return {!Element} |
| */ |
| function createNodeElement() { |
| const valueElement = createElementWithClass('span', 'object-value-node'); |
| ObjectUI.RemoteObjectPreviewFormatter.createSpansForNodeTitle(valueElement, /** @type {string} */ (description)); |
| valueElement.addEventListener('click', event => { |
| Common.Revealer.reveal(value); |
| event.consume(true); |
| }, false); |
| valueElement.addEventListener('mousemove', () => SDK.OverlayModel.highlightObjectAsDOMNode(value), false); |
| valueElement.addEventListener('mouseleave', () => SDK.OverlayModel.hideDOMNodeHighlight(), false); |
| return valueElement; |
| } |
| |
| /** |
| * @return {!Element} |
| */ |
| function createNumberWithExponentElement() { |
| const valueElement = createElementWithClass('span', 'object-value-number'); |
| const numberParts = description.split('e'); |
| valueElement.createChild('span', 'object-value-scientific-notation-mantissa').textContent = numberParts[0]; |
| valueElement.createChild('span', 'object-value-scientific-notation-exponent').textContent = 'e' + numberParts[1]; |
| valueElement.classList.add('object-value-scientific-notation-number'); |
| valueElement.title = description || ''; |
| return valueElement; |
| } |
| } |
| |
| /** |
| * @param {!SDK.RemoteObject} func |
| * @param {!Element} element |
| * @param {boolean} linkify |
| * @param {boolean=} includePreview |
| * @return {!Promise} |
| */ |
| static formatObjectAsFunction(func, element, linkify, includePreview) { |
| return func.debuggerModel().functionDetailsPromise(func).then(didGetDetails); |
| |
| /** |
| * @param {?SDK.DebuggerModel.FunctionDetails} response |
| */ |
| function didGetDetails(response) { |
| if (linkify && response && response.location) { |
| element.classList.add('linkified'); |
| element.addEventListener('click', () => Common.Revealer.reveal(response.location) && false); |
| } |
| |
| // The includePreview flag is false for formats such as console.dir(). |
| let defaultName = includePreview ? '' : 'anonymous'; |
| if (response && response.functionName) { |
| defaultName = response.functionName; |
| } |
| const valueElement = |
| ObjectPropertiesSection.valueElementForFunctionDescription(func.description, includePreview, defaultName); |
| element.appendChild(valueElement); |
| } |
| } |
| |
| /** |
| * @param {!SDK.RemoteObjectProperty} property |
| * @param {!SDK.RemoteObjectProperty=} parentProperty |
| * @return {boolean} |
| */ |
| static _isDisplayableProperty(property, parentProperty) { |
| if (!parentProperty || !parentProperty.synthetic) { |
| return true; |
| } |
| const name = property.name; |
| const useless = (parentProperty.name === '[[Entries]]' && (name === 'length' || name === '__proto__')); |
| return !useless; |
| } |
| |
| skipProto() { |
| this._skipProto = true; |
| } |
| |
| expand() { |
| this._objectTreeElement.expand(); |
| } |
| |
| /** |
| * @param {boolean} value |
| */ |
| setEditable(value) { |
| this._editable = value; |
| } |
| |
| /** |
| * @return {!UI.TreeElement} |
| */ |
| objectTreeElement() { |
| return this._objectTreeElement; |
| } |
| |
| enableContextMenu() { |
| this.element.addEventListener('contextmenu', this._contextMenuEventFired.bind(this), false); |
| } |
| |
| _contextMenuEventFired(event) { |
| const contextMenu = new UI.ContextMenu(event); |
| contextMenu.appendApplicableItems(this._object); |
| if (this._object instanceof SDK.LocalJSONObject) { |
| contextMenu.viewSection().appendItem( |
| ls`Expand recursively`, |
| this._objectTreeElement.expandRecursively.bind(this._objectTreeElement, Number.MAX_VALUE)); |
| contextMenu.viewSection().appendItem( |
| ls`Collapse children`, this._objectTreeElement.collapseChildren.bind(this._objectTreeElement)); |
| } |
| contextMenu.show(); |
| } |
| |
| titleLessMode() { |
| this._objectTreeElement.listItemElement.classList.add('hidden'); |
| this._objectTreeElement.childrenListElement.classList.add('title-less-mode'); |
| this._objectTreeElement.expand(); |
| } |
| } |
| |
| /** @const */ |
| const _arrayLoadThreshold = 100; |
| |
| /** @const */ |
| export const _maxRenderableStringLength = 10000; |
| |
| export class ObjectPropertiesSectionsTreeOutline extends UI.TreeOutlineInShadow { |
| /** |
| * @param {?ObjectUI.ObjectPropertiesSectionsTreeOutlineOptions=} options |
| */ |
| constructor(options) { |
| super(); |
| this.registerRequiredCSS('object_ui/objectValue.css'); |
| this.registerRequiredCSS('object_ui/objectPropertiesSection.css'); |
| this._editable = !(options && options.readOnly); |
| this.contentElement.classList.add('source-code'); |
| this.contentElement.classList.add('object-properties-section'); |
| this.hideOverflow(); |
| } |
| } |
| |
| /** |
| * @unrestricted |
| */ |
| export class RootElement extends UI.TreeElement { |
| /** |
| * @param {!SDK.RemoteObject} object |
| * @param {!Components.Linkifier=} linkifier |
| * @param {?string=} emptyPlaceholder |
| * @param {boolean=} ignoreHasOwnProperty |
| * @param {!Array.<!SDK.RemoteObjectProperty>=} extraProperties |
| */ |
| constructor(object, linkifier, emptyPlaceholder, ignoreHasOwnProperty, extraProperties) { |
| const contentElement = createElement('slot'); |
| super(contentElement); |
| |
| this._object = object; |
| this._extraProperties = extraProperties || []; |
| this._ignoreHasOwnProperty = !!ignoreHasOwnProperty; |
| this._emptyPlaceholder = emptyPlaceholder; |
| |
| this.setExpandable(true); |
| this.selectable = true; |
| this.toggleOnClick = true; |
| this.listItemElement.classList.add('object-properties-section-root-element'); |
| this._linkifier = linkifier; |
| } |
| |
| /** |
| * @override |
| */ |
| onexpand() { |
| if (this.treeOutline) { |
| this.treeOutline.element.classList.add('expanded'); |
| } |
| } |
| |
| /** |
| * @override |
| */ |
| oncollapse() { |
| if (this.treeOutline) { |
| this.treeOutline.element.classList.remove('expanded'); |
| } |
| } |
| |
| /** |
| * @override |
| * @param {!Event} e |
| * @return {boolean} |
| */ |
| ondblclick(e) { |
| return true; |
| } |
| |
| /** |
| * @override |
| * @returns {!Promise} |
| */ |
| async onpopulate() { |
| return ObjectPropertyTreeElement._populate( |
| this, this._object, !!this.treeOutline._skipProto, this._linkifier, this._emptyPlaceholder, |
| this._ignoreHasOwnProperty, this._extraProperties); |
| } |
| } |
| |
| /** |
| * @unrestricted |
| */ |
| export class ObjectPropertyTreeElement extends UI.TreeElement { |
| /** |
| * @param {!SDK.RemoteObjectProperty} property |
| * @param {!Components.Linkifier=} linkifier |
| */ |
| constructor(property, linkifier) { |
| // Pass an empty title, the title gets made later in onattach. |
| super(); |
| |
| this.property = property; |
| this.toggleOnClick = true; |
| /** @type {!Array.<!Object>} */ |
| this._highlightChanges = []; |
| this._linkifier = linkifier; |
| this.listItemElement.addEventListener('contextmenu', this._contextMenuFired.bind(this), false); |
| } |
| |
| /** |
| * @param {!UI.TreeElement} treeElement |
| * @param {!SDK.RemoteObject} value |
| * @param {boolean} skipProto |
| * @param {!Components.Linkifier=} linkifier |
| * @param {?string=} emptyPlaceholder |
| * @param {boolean=} flattenProtoChain |
| * @param {!Array.<!SDK.RemoteObjectProperty>=} extraProperties |
| * @param {!SDK.RemoteObject=} targetValue |
| * @return {!Promise} |
| */ |
| static async _populate( |
| treeElement, value, skipProto, linkifier, emptyPlaceholder, flattenProtoChain, extraProperties, targetValue) { |
| if (value.arrayLength() > _arrayLoadThreshold) { |
| treeElement.removeChildren(); |
| ArrayGroupingTreeElement._populateArray(treeElement, value, 0, value.arrayLength() - 1, linkifier); |
| return; |
| } |
| |
| let allProperties; |
| if (flattenProtoChain) { |
| allProperties = await value.getAllProperties(false /* accessorPropertiesOnly */, true /* generatePreview */); |
| } else { |
| allProperties = await SDK.RemoteObject.loadFromObjectPerProto(value, true /* generatePreview */); |
| } |
| const properties = allProperties.properties; |
| const internalProperties = allProperties.internalProperties; |
| treeElement.removeChildren(); |
| if (!properties) { |
| return; |
| } |
| |
| extraProperties = extraProperties || []; |
| for (let i = 0; i < extraProperties.length; ++i) { |
| properties.push(extraProperties[i]); |
| } |
| |
| ObjectPropertyTreeElement.populateWithProperties( |
| treeElement, properties, internalProperties, skipProto, targetValue || value, linkifier, emptyPlaceholder); |
| } |
| |
| /** |
| * @param {!UI.TreeElement} treeNode |
| * @param {!Array.<!SDK.RemoteObjectProperty>} properties |
| * @param {?Array.<!SDK.RemoteObjectProperty>} internalProperties |
| * @param {boolean} skipProto |
| * @param {?SDK.RemoteObject} value |
| * @param {!Components.Linkifier=} linkifier |
| * @param {?string=} emptyPlaceholder |
| */ |
| static populateWithProperties( |
| treeNode, |
| properties, |
| internalProperties, |
| skipProto, |
| value, |
| linkifier, |
| emptyPlaceholder) { |
| internalProperties = internalProperties || []; |
| |
| const entriesProperty = internalProperties.find(property => property.name === '[[Entries]]'); |
| if (entriesProperty) { |
| entriesProperty.parentObject = value; |
| const treeElement = new ObjectPropertyTreeElement(entriesProperty, linkifier); |
| treeElement.setExpandable(true); |
| treeElement.expand(); |
| treeNode.appendChild(treeElement); |
| } |
| |
| const tailProperties = []; |
| let protoProperty = null; |
| for (let i = 0; i < properties.length; ++i) { |
| const property = properties[i]; |
| property.parentObject = value; |
| if (!ObjectPropertiesSection._isDisplayableProperty(property, treeNode.property)) { |
| continue; |
| } |
| if (property.name === '__proto__' && !property.isAccessorProperty()) { |
| protoProperty = property; |
| continue; |
| } |
| |
| if (property.isOwn && property.getter) { |
| const getterProperty = new SDK.RemoteObjectProperty('get ' + property.name, property.getter, false); |
| getterProperty.parentObject = value; |
| tailProperties.push(getterProperty); |
| } |
| if (property.isOwn && property.setter) { |
| const setterProperty = new SDK.RemoteObjectProperty('set ' + property.name, property.setter, false); |
| setterProperty.parentObject = value; |
| tailProperties.push(setterProperty); |
| } |
| const canShowProperty = property.getter || !property.isAccessorProperty(); |
| if (canShowProperty && property.name !== '__proto__') { |
| treeNode.appendChild(new ObjectPropertyTreeElement(property, linkifier)); |
| } |
| } |
| for (let i = 0; i < tailProperties.length; ++i) { |
| treeNode.appendChild(new ObjectPropertyTreeElement(tailProperties[i], linkifier)); |
| } |
| if (!skipProto && protoProperty) { |
| treeNode.appendChild(new ObjectPropertyTreeElement(protoProperty, linkifier)); |
| } |
| |
| for (const property of internalProperties) { |
| property.parentObject = value; |
| const treeElement = new ObjectPropertyTreeElement(property, linkifier); |
| if (property.name === '[[Entries]]') { |
| continue; |
| } |
| treeNode.appendChild(treeElement); |
| } |
| |
| ObjectPropertyTreeElement._appendEmptyPlaceholderIfNeeded(treeNode, emptyPlaceholder); |
| } |
| |
| /** |
| * @param {!UI.TreeElement} treeNode |
| * @param {?string=} emptyPlaceholder |
| */ |
| static _appendEmptyPlaceholderIfNeeded(treeNode, emptyPlaceholder) { |
| if (treeNode.childCount()) { |
| return; |
| } |
| const title = createElementWithClass('div', 'gray-info-message'); |
| title.textContent = emptyPlaceholder || Common.UIString('No properties'); |
| const infoElement = new UI.TreeElement(title); |
| treeNode.appendChild(infoElement); |
| } |
| |
| /** |
| * @param {?SDK.RemoteObject} object |
| * @param {!Array.<string>} propertyPath |
| * @param {function(!SDK.CallFunctionResult)} callback |
| * @return {!Element} |
| */ |
| static createRemoteObjectAccessorPropertySpan(object, propertyPath, callback) { |
| const rootElement = createElement('span'); |
| const element = rootElement.createChild('span'); |
| element.textContent = Common.UIString('(...)'); |
| if (!object) { |
| return rootElement; |
| } |
| element.classList.add('object-value-calculate-value-button'); |
| element.title = Common.UIString('Invoke property getter'); |
| element.addEventListener('click', onInvokeGetterClick, false); |
| |
| function onInvokeGetterClick(event) { |
| event.consume(); |
| object.callFunction(invokeGetter, [{value: JSON.stringify(propertyPath)}]).then(callback); |
| } |
| |
| /** |
| * @param {string} arrayStr |
| * @suppressReceiverCheck |
| * @this {Object} |
| */ |
| function invokeGetter(arrayStr) { |
| let result = this; |
| const properties = JSON.parse(arrayStr); |
| for (let i = 0, n = properties.length; i < n; ++i) { |
| result = result[properties[i]]; |
| } |
| return result; |
| } |
| |
| return rootElement; |
| } |
| |
| /** |
| * @param {!RegExp} regex |
| * @param {string=} additionalCssClassName |
| * @return {boolean} |
| */ |
| setSearchRegex(regex, additionalCssClassName) { |
| let cssClasses = UI.highlightedSearchResultClassName; |
| if (additionalCssClassName) { |
| cssClasses += ' ' + additionalCssClassName; |
| } |
| this.revertHighlightChanges(); |
| |
| this._applySearch(regex, this.nameElement, cssClasses); |
| const valueType = this.property.value.type; |
| if (valueType !== 'object') { |
| this._applySearch(regex, this.valueElement, cssClasses); |
| } |
| |
| return !!this._highlightChanges.length; |
| } |
| |
| /** |
| * @param {!RegExp} regex |
| * @param {!Element} element |
| * @param {string} cssClassName |
| */ |
| _applySearch(regex, element, cssClassName) { |
| const ranges = []; |
| const content = element.textContent; |
| regex.lastIndex = 0; |
| let match = regex.exec(content); |
| while (match) { |
| ranges.push(new TextUtils.SourceRange(match.index, match[0].length)); |
| match = regex.exec(content); |
| } |
| if (ranges.length) { |
| UI.highlightRangesWithStyleClass(element, ranges, cssClassName, this._highlightChanges); |
| } |
| } |
| |
| revertHighlightChanges() { |
| UI.revertDomChanges(this._highlightChanges); |
| this._highlightChanges = []; |
| } |
| |
| /** |
| * @override |
| * @returns {!Promise} |
| */ |
| async onpopulate() { |
| const propertyValue = /** @type {!SDK.RemoteObject} */ (this.property.value); |
| console.assert(propertyValue); |
| const skipProto = this.treeOutline ? this.treeOutline._skipProto : true; |
| const targetValue = this.property.name !== '__proto__' ? propertyValue : this.property.parentObject; |
| await ObjectPropertyTreeElement._populate( |
| this, propertyValue, skipProto, this._linkifier, undefined, undefined, undefined, targetValue); |
| } |
| |
| /** |
| * @override |
| * @return {boolean} |
| */ |
| ondblclick(event) { |
| const inEditableElement = event.target.isSelfOrDescendant(this.valueElement) || |
| (this.expandedValueElement && event.target.isSelfOrDescendant(this.expandedValueElement)); |
| if (!this.property.value.customPreview() && inEditableElement && (this.property.writable || this.property.setter)) { |
| this._startEditing(); |
| } |
| return false; |
| } |
| |
| /** |
| * @override |
| * @return {boolean} |
| */ |
| onenter() { |
| if (!this.property.value.customPreview() && (this.property.writable || this.property.setter)) { |
| this._startEditing(); |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * @override |
| */ |
| onattach() { |
| this.update(); |
| this._updateExpandable(); |
| } |
| |
| /** |
| * @override |
| */ |
| onexpand() { |
| this._showExpandedValueElement(true); |
| } |
| |
| /** |
| * @override |
| */ |
| oncollapse() { |
| this._showExpandedValueElement(false); |
| } |
| |
| /** |
| * @param {boolean} value |
| */ |
| _showExpandedValueElement(value) { |
| if (!this.expandedValueElement) { |
| return; |
| } |
| if (value) { |
| this._rowContainer.replaceChild(this.expandedValueElement, this.valueElement); |
| } else { |
| this._rowContainer.replaceChild(this.valueElement, this.expandedValueElement); |
| } |
| } |
| |
| /** |
| * @param {!SDK.RemoteObject} value |
| * @return {?Element} |
| */ |
| _createExpandedValueElement(value) { |
| const needsAlternateValue = value.hasChildren && !value.customPreview() && value.subtype !== 'node' && |
| value.type !== 'function' && (value.type !== 'object' || value.preview); |
| if (!needsAlternateValue) { |
| return null; |
| } |
| |
| const valueElement = createElementWithClass('span', 'value'); |
| if (value.description === 'Object') { |
| valueElement.textContent = ''; |
| } else { |
| valueElement.setTextContentTruncatedIfNeeded(value.description || ''); |
| } |
| valueElement.classList.add('object-value-' + (value.subtype || value.type)); |
| valueElement.title = value.description || ''; |
| return valueElement; |
| } |
| |
| update() { |
| this.nameElement = ObjectPropertiesSection.createNameElement(this.property.name, this.property.private); |
| if (!this.property.enumerable) { |
| this.nameElement.classList.add('object-properties-section-dimmed'); |
| } |
| if (this.property.synthetic) { |
| this.nameElement.classList.add('synthetic-property'); |
| } |
| |
| this._updatePropertyPath(); |
| |
| const isInternalEntries = this.property.synthetic && this.property.name === '[[Entries]]'; |
| if (isInternalEntries) { |
| this.valueElement = createElementWithClass('span', 'value'); |
| } else if (this.property.value) { |
| const showPreview = this.property.name !== '__proto__'; |
| this.valueElement = ObjectPropertiesSection.createValueElementWithCustomSupport( |
| this.property.value, this.property.wasThrown, showPreview, this.listItemElement, this._linkifier); |
| } else if (this.property.getter) { |
| this.valueElement = ObjectPropertyTreeElement.createRemoteObjectAccessorPropertySpan( |
| this.property.parentObject, [this.property.name], this._onInvokeGetterClick.bind(this)); |
| } else { |
| this.valueElement = createElementWithClass('span', 'object-value-undefined'); |
| this.valueElement.textContent = Common.UIString('<unreadable>'); |
| this.valueElement.title = Common.UIString('No property getter'); |
| } |
| |
| const valueText = this.valueElement.textContent; |
| if (this.property.value && valueText && !this.property.wasThrown) { |
| this.expandedValueElement = this._createExpandedValueElement(this.property.value); |
| } |
| |
| this.listItemElement.removeChildren(); |
| if (isInternalEntries) { |
| this._rowContainer = UI.html`<span class='name-and-value'>${this.nameElement}</span>`; |
| } else { |
| this._rowContainer = UI.html`<span class='name-and-value'>${this.nameElement}: ${this.valueElement}</span>`; |
| } |
| this.listItemElement.appendChild(this._rowContainer); |
| } |
| |
| _updatePropertyPath() { |
| if (this.nameElement.title) { |
| return; |
| } |
| |
| const name = this.property.name; |
| |
| if (this.property.synthetic) { |
| this.nameElement.title = name; |
| return; |
| } |
| |
| const useDotNotation = /^(_|\$|[A-Z])(_|\$|[A-Z]|\d)*$/i; |
| const isInteger = /^[1-9]\d*$/; |
| |
| const parentPath = |
| (this.parent.nameElement && !this.parent.property.synthetic) ? this.parent.nameElement.title : ''; |
| |
| if (this.property.private || useDotNotation.test(name)) { |
| this.nameElement.title = parentPath ? `${parentPath}.${name}` : name; |
| } else if (isInteger.test(name)) { |
| this.nameElement.title = parentPath + '[' + name + ']'; |
| } else { |
| this.nameElement.title = parentPath + '["' + JSON.stringify(name) + '"]'; |
| } |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| _contextMenuFired(event) { |
| const contextMenu = new UI.ContextMenu(event); |
| contextMenu.appendApplicableItems(this); |
| if (this.property.symbol) { |
| contextMenu.appendApplicableItems(this.property.symbol); |
| } |
| if (this.property.value) { |
| contextMenu.appendApplicableItems(this.property.value); |
| } |
| if (!this.property.synthetic && this.nameElement && this.nameElement.title) { |
| const copyPathHandler = |
| Host.InspectorFrontendHost.copyText.bind(Host.InspectorFrontendHost, this.nameElement.title); |
| contextMenu.clipboardSection().appendItem(ls`Copy property path`, copyPathHandler); |
| } |
| if (this.property.parentObject instanceof SDK.LocalJSONObject) { |
| contextMenu.viewSection().appendItem(ls`Expand recursively`, this.expandRecursively.bind(this, Number.MAX_VALUE)); |
| contextMenu.viewSection().appendItem(ls`Collapse children`, this.collapseChildren.bind(this)); |
| } |
| contextMenu.show(); |
| } |
| |
| _startEditing() { |
| if (this._prompt || !this.treeOutline._editable || this._readOnly) { |
| return; |
| } |
| |
| this._editableDiv = this._rowContainer.createChild('span', 'editable-div'); |
| |
| let text = this.property.value.description; |
| if (this.property.value.type === 'string' && typeof text === 'string') { |
| text = '"' + text + '"'; |
| } |
| |
| this._editableDiv.setTextContentTruncatedIfNeeded(text, Common.UIString('<string is too large to edit>')); |
| const originalContent = this._editableDiv.textContent; |
| |
| // Lie about our children to prevent expanding on double click and to collapse subproperties. |
| this.setExpandable(false); |
| this.listItemElement.classList.add('editing-sub-part'); |
| this.valueElement.classList.add('hidden'); |
| |
| this._prompt = new ObjectPropertyPrompt(); |
| |
| const proxyElement = |
| this._prompt.attachAndStartEditing(this._editableDiv, this._editingCommitted.bind(this, originalContent)); |
| proxyElement.classList.add('property-prompt'); |
| this.listItemElement.getComponentSelection().selectAllChildren(this._editableDiv); |
| proxyElement.addEventListener('keydown', this._promptKeyDown.bind(this, originalContent), false); |
| } |
| |
| _editingEnded() { |
| this._prompt.detach(); |
| delete this._prompt; |
| this._editableDiv.remove(); |
| this._updateExpandable(); |
| this.listItemElement.scrollLeft = 0; |
| this.listItemElement.classList.remove('editing-sub-part'); |
| this.select(); |
| } |
| |
| _editingCancelled() { |
| this.valueElement.classList.remove('hidden'); |
| this._editingEnded(); |
| } |
| |
| /** |
| * @param {string} originalContent |
| * @returns {!Promise} |
| */ |
| async _editingCommitted(originalContent) { |
| const userInput = this._prompt.text(); |
| if (userInput === originalContent) { |
| this._editingCancelled(); // nothing changed, so cancel |
| return; |
| } |
| |
| this._editingEnded(); |
| await this._applyExpression(userInput); |
| } |
| |
| /** |
| * @param {string} originalContent |
| * @param {!Event} event |
| */ |
| _promptKeyDown(originalContent, event) { |
| if (isEnterKey(event)) { |
| event.consume(); |
| this._editingCommitted(originalContent); |
| return; |
| } |
| if (event.key === 'Escape') { |
| event.consume(); |
| this._editingCancelled(); |
| return; |
| } |
| } |
| |
| /** |
| * @param {string} expression |
| */ |
| async _applyExpression(expression) { |
| const property = SDK.RemoteObject.toCallArgument(this.property.symbol || this.property.name); |
| expression = ObjectUI.JavaScriptREPL.wrapObjectLiteral(expression.trim()); |
| |
| if (this.property.synthetic) { |
| let invalidate = false; |
| if (expression) { |
| invalidate = await this.property.setSyntheticValue(expression); |
| } |
| if (invalidate) { |
| const parent = this.parent; |
| parent.invalidateChildren(); |
| parent.onpopulate(); |
| } else { |
| this.update(); |
| } |
| return; |
| } |
| |
| const errorPromise = expression ? this.property.parentObject.setPropertyValue(property, expression) : |
| this.property.parentObject.deleteProperty(property); |
| const error = await errorPromise; |
| if (error) { |
| this.update(); |
| return; |
| } |
| |
| if (!expression) { |
| // The property was deleted, so remove this tree element. |
| this.parent.removeChild(this); |
| } else { |
| // Call updateSiblings since their value might be based on the value that just changed. |
| const parent = this.parent; |
| parent.invalidateChildren(); |
| parent.onpopulate(); |
| } |
| } |
| |
| /** |
| * @param {!SDK.CallFunctionResult} result |
| */ |
| _onInvokeGetterClick(result) { |
| if (!result.object) { |
| return; |
| } |
| this.property.value = result.object; |
| this.property.wasThrown = result.wasThrown; |
| |
| this.update(); |
| this.invalidateChildren(); |
| this._updateExpandable(); |
| } |
| |
| _updateExpandable() { |
| if (this.property.value) { |
| this.setExpandable( |
| !this.property.value.customPreview() && this.property.value.hasChildren && !this.property.wasThrown); |
| } else { |
| this.setExpandable(false); |
| } |
| } |
| |
| /** |
| * @return {string} |
| */ |
| path() { |
| return this.nameElement.title; |
| } |
| } |
| |
| |
| /** |
| * @unrestricted |
| */ |
| class ArrayGroupingTreeElement extends UI.TreeElement { |
| /** |
| * @param {!SDK.RemoteObject} object |
| * @param {number} fromIndex |
| * @param {number} toIndex |
| * @param {number} propertyCount |
| * @param {!Components.Linkifier=} linkifier |
| */ |
| constructor(object, fromIndex, toIndex, propertyCount, linkifier) { |
| super(String.sprintf('[%d \u2026 %d]', fromIndex, toIndex), true); |
| this.toggleOnClick = true; |
| this._fromIndex = fromIndex; |
| this._toIndex = toIndex; |
| this._object = object; |
| this._readOnly = true; |
| this._propertyCount = propertyCount; |
| this._linkifier = linkifier; |
| } |
| |
| /** |
| * @param {!UI.TreeElement} treeNode |
| * @param {!SDK.RemoteObject} object |
| * @param {number} fromIndex |
| * @param {number} toIndex |
| * @param {!Components.Linkifier=} linkifier |
| * @returns {!Promise} |
| */ |
| static async _populateArray(treeNode, object, fromIndex, toIndex, linkifier) { |
| await ArrayGroupingTreeElement._populateRanges(treeNode, object, fromIndex, toIndex, true, linkifier); |
| } |
| |
| /** |
| * @param {!UI.TreeElement} treeNode |
| * @param {!SDK.RemoteObject} object |
| * @param {number} fromIndex |
| * @param {number} toIndex |
| * @param {boolean} topLevel |
| * @param {!Components.Linkifier=} linkifier |
| * @this {ArrayGroupingTreeElement} |
| * @returns {!Promise} |
| */ |
| static async _populateRanges(treeNode, object, fromIndex, toIndex, topLevel, linkifier) { |
| const jsonValue = await object.callFunctionJSON(packRanges, [ |
| {value: fromIndex}, {value: toIndex}, {value: ArrayGroupingTreeElement._bucketThreshold}, |
| {value: ArrayGroupingTreeElement._sparseIterationThreshold}, |
| {value: ArrayGroupingTreeElement._getOwnPropertyNamesThreshold} |
| ]); |
| |
| await callback(jsonValue); |
| |
| /** |
| * Note: must declare params as optional. |
| * @param {number=} fromIndex |
| * @param {number=} toIndex |
| * @param {number=} bucketThreshold |
| * @param {number=} sparseIterationThreshold |
| * @param {number=} getOwnPropertyNamesThreshold |
| * @suppressReceiverCheck |
| * @this {Object} |
| */ |
| function packRanges(fromIndex, toIndex, bucketThreshold, sparseIterationThreshold, getOwnPropertyNamesThreshold) { |
| let ownPropertyNames = null; |
| const consecutiveRange = (toIndex - fromIndex >= sparseIterationThreshold) && ArrayBuffer.isView(this); |
| const skipGetOwnPropertyNames = consecutiveRange && (toIndex - fromIndex >= getOwnPropertyNamesThreshold); |
| |
| function* arrayIndexes(object) { |
| if (toIndex - fromIndex < sparseIterationThreshold) { |
| for (let i = fromIndex; i <= toIndex; ++i) { |
| if (i in object) { |
| yield i; |
| } |
| } |
| } else { |
| ownPropertyNames = ownPropertyNames || Object.getOwnPropertyNames(object); |
| for (let i = 0; i < ownPropertyNames.length; ++i) { |
| const name = ownPropertyNames[i]; |
| const index = name >>> 0; |
| if (('' + index) === name && fromIndex <= index && index <= toIndex) { |
| yield index; |
| } |
| } |
| } |
| } |
| |
| let count = 0; |
| if (consecutiveRange) { |
| count = toIndex - fromIndex + 1; |
| } else { |
| for (const i of arrayIndexes(this)) // eslint-disable-line |
| ++count; |
| } |
| |
| let bucketSize = count; |
| if (count <= bucketThreshold) { |
| bucketSize = count; |
| } else { |
| bucketSize = Math.pow(bucketThreshold, Math.ceil(Math.log(count) / Math.log(bucketThreshold)) - 1); |
| } |
| |
| const ranges = []; |
| if (consecutiveRange) { |
| for (let i = fromIndex; i <= toIndex; i += bucketSize) { |
| const groupStart = i; |
| let groupEnd = groupStart + bucketSize - 1; |
| if (groupEnd > toIndex) { |
| groupEnd = toIndex; |
| } |
| ranges.push([groupStart, groupEnd, groupEnd - groupStart + 1]); |
| } |
| } else { |
| count = 0; |
| let groupStart = -1; |
| let groupEnd = 0; |
| for (const i of arrayIndexes(this)) { |
| if (groupStart === -1) { |
| groupStart = i; |
| } |
| groupEnd = i; |
| if (++count === bucketSize) { |
| ranges.push([groupStart, groupEnd, count]); |
| count = 0; |
| groupStart = -1; |
| } |
| } |
| if (count > 0) { |
| ranges.push([groupStart, groupEnd, count]); |
| } |
| } |
| |
| return {ranges: ranges, skipGetOwnPropertyNames: skipGetOwnPropertyNames}; |
| } |
| |
| async function callback(result) { |
| if (!result) { |
| return; |
| } |
| const ranges = /** @type {!Array.<!Array.<number>>} */ (result.ranges); |
| if (ranges.length === 1) { |
| await ArrayGroupingTreeElement._populateAsFragment(treeNode, object, ranges[0][0], ranges[0][1], linkifier); |
| } else { |
| for (let i = 0; i < ranges.length; ++i) { |
| const fromIndex = ranges[i][0]; |
| const toIndex = ranges[i][1]; |
| const count = ranges[i][2]; |
| if (fromIndex === toIndex) { |
| await ArrayGroupingTreeElement._populateAsFragment(treeNode, object, fromIndex, toIndex, linkifier); |
| } else { |
| treeNode.appendChild(new ArrayGroupingTreeElement(object, fromIndex, toIndex, count, linkifier)); |
| } |
| } |
| } |
| if (topLevel) { |
| await ArrayGroupingTreeElement._populateNonIndexProperties( |
| treeNode, object, result.skipGetOwnPropertyNames, linkifier); |
| } |
| } |
| } |
| |
| /** |
| * @param {!UI.TreeElement} treeNode |
| * @param {!SDK.RemoteObject} object |
| * @param {number} fromIndex |
| * @param {number} toIndex |
| * @param {!Components.Linkifier=} linkifier |
| * @return {!Promise} |
| * @this {ArrayGroupingTreeElement} |
| */ |
| static async _populateAsFragment(treeNode, object, fromIndex, toIndex, linkifier) { |
| const result = await object.callFunction( |
| buildArrayFragment, |
| [{value: fromIndex}, {value: toIndex}, {value: ArrayGroupingTreeElement._sparseIterationThreshold}]); |
| if (!result.object || result.wasThrown) { |
| return; |
| } |
| const arrayFragment = result.object; |
| const allProperties = |
| await arrayFragment.getAllProperties(false /* accessorPropertiesOnly */, true /* generatePreview */); |
| arrayFragment.release(); |
| const properties = allProperties.properties; |
| if (!properties) { |
| return; |
| } |
| properties.sort(ObjectPropertiesSection.CompareProperties); |
| for (let i = 0; i < properties.length; ++i) { |
| properties[i].parentObject = this._object; |
| const childTreeElement = new ObjectPropertyTreeElement(properties[i], linkifier); |
| childTreeElement._readOnly = true; |
| treeNode.appendChild(childTreeElement); |
| } |
| |
| /** |
| * @suppressReceiverCheck |
| * @this {Object} |
| * @param {number=} fromIndex // must declare optional |
| * @param {number=} toIndex // must declare optional |
| * @param {number=} sparseIterationThreshold // must declare optional |
| */ |
| function buildArrayFragment(fromIndex, toIndex, sparseIterationThreshold) { |
| const result = Object.create(null); |
| if (toIndex - fromIndex < sparseIterationThreshold) { |
| for (let i = fromIndex; i <= toIndex; ++i) { |
| if (i in this) { |
| result[i] = this[i]; |
| } |
| } |
| } else { |
| const ownPropertyNames = Object.getOwnPropertyNames(this); |
| for (let i = 0; i < ownPropertyNames.length; ++i) { |
| const name = ownPropertyNames[i]; |
| const index = name >>> 0; |
| if (String(index) === name && fromIndex <= index && index <= toIndex) { |
| result[index] = this[index]; |
| } |
| } |
| } |
| return result; |
| } |
| } |
| |
| /** |
| * @param {!UI.TreeElement} treeNode |
| * @param {!SDK.RemoteObject} object |
| * @param {boolean} skipGetOwnPropertyNames |
| * @param {!Components.Linkifier=} linkifier |
| * @return {!Promise<undefined>} |
| * @this {ArrayGroupingTreeElement} |
| */ |
| static async _populateNonIndexProperties(treeNode, object, skipGetOwnPropertyNames, linkifier) { |
| const result = await object.callFunction(buildObjectFragment, [{value: skipGetOwnPropertyNames}]); |
| if (!result.object || result.wasThrown) { |
| return; |
| } |
| const allProperties = await result.object.getOwnProperties(true /* generatePreview */); |
| result.object.release(); |
| if (!allProperties.properties) { |
| return; |
| } |
| const properties = allProperties.properties; |
| properties.sort(ObjectPropertiesSection.CompareProperties); |
| for (const property of properties) { |
| property.parentObject = this._object; |
| if (!ObjectPropertiesSection._isDisplayableProperty(property, treeNode.property)) { |
| continue; |
| } |
| const childTreeElement = new ObjectPropertyTreeElement(property, linkifier); |
| childTreeElement._readOnly = true; |
| treeNode.appendChild(childTreeElement); |
| } |
| |
| /** |
| * @param {boolean=} skipGetOwnPropertyNames |
| * @suppressReceiverCheck |
| * @this {Object} |
| */ |
| function buildObjectFragment(skipGetOwnPropertyNames) { |
| const result = {__proto__: this.__proto__}; |
| if (skipGetOwnPropertyNames) { |
| return result; |
| } |
| const names = Object.getOwnPropertyNames(this); |
| for (let i = 0; i < names.length; ++i) { |
| const name = names[i]; |
| // Array index check according to the ES5-15.4. |
| if (String(name >>> 0) === name && name >>> 0 !== 0xffffffff) { |
| continue; |
| } |
| const descriptor = Object.getOwnPropertyDescriptor(this, name); |
| if (descriptor) { |
| Object.defineProperty(result, name, descriptor); |
| } |
| } |
| return result; |
| } |
| } |
| |
| /** |
| * @override |
| * @returns {!Promise} |
| */ |
| async onpopulate() { |
| if (this._propertyCount >= ArrayGroupingTreeElement._bucketThreshold) { |
| await ArrayGroupingTreeElement._populateRanges( |
| this, this._object, this._fromIndex, this._toIndex, false, this._linkifier); |
| return; |
| } |
| await ArrayGroupingTreeElement._populateAsFragment( |
| this, this._object, this._fromIndex, this._toIndex, this._linkifier); |
| } |
| |
| /** |
| * @override |
| */ |
| onattach() { |
| this.listItemElement.classList.add('object-properties-section-name'); |
| } |
| } |
| |
| ArrayGroupingTreeElement._bucketThreshold = 100; |
| ArrayGroupingTreeElement._sparseIterationThreshold = 250000; |
| ArrayGroupingTreeElement._getOwnPropertyNamesThreshold = 500000; |
| |
| |
| /** |
| * @unrestricted |
| */ |
| export class ObjectPropertyPrompt extends UI.TextPrompt { |
| constructor() { |
| super(); |
| this.initialize( |
| ObjectUI.javaScriptAutocomplete.completionsForTextInCurrentContext.bind(ObjectUI.javaScriptAutocomplete)); |
| } |
| } |
| |
| /** |
| * @unrestricted |
| */ |
| export class ObjectPropertiesSectionsTreeExpandController { |
| /** |
| * @param {!UI.TreeOutline} treeOutline |
| */ |
| constructor(treeOutline) { |
| /** @type {!Set.<string>} */ |
| this._expandedProperties = new Set(); |
| treeOutline.addEventListener(UI.TreeOutline.Events.ElementAttached, this._elementAttached, this); |
| treeOutline.addEventListener(UI.TreeOutline.Events.ElementExpanded, this._elementExpanded, this); |
| treeOutline.addEventListener(UI.TreeOutline.Events.ElementCollapsed, this._elementCollapsed, this); |
| } |
| |
| /** |
| * @param {string} id |
| * @param {!RootElement} section |
| */ |
| watchSection(id, section) { |
| section[ObjectPropertiesSectionsTreeExpandController._treeOutlineId] = id; |
| |
| if (this._expandedProperties.has(id)) { |
| section.expand(); |
| } |
| } |
| |
| /** |
| * @param {string} id |
| */ |
| stopWatchSectionsWithId(id) { |
| for (const property of this._expandedProperties) { |
| if (property.startsWith(id + ':')) { |
| this._expandedProperties.delete(property); |
| } |
| } |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _elementAttached(event) { |
| const element = /** @type {!UI.TreeElement} */ (event.data); |
| if (element.isExpandable() && this._expandedProperties.has(this._propertyPath(element))) { |
| element.expand(); |
| } |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _elementExpanded(event) { |
| const element = /** @type {!UI.TreeElement} */ (event.data); |
| this._expandedProperties.add(this._propertyPath(element)); |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _elementCollapsed(event) { |
| const element = /** @type {!UI.TreeElement} */ (event.data); |
| this._expandedProperties.delete(this._propertyPath(element)); |
| } |
| |
| /** |
| * @param {!UI.TreeElement} treeElement |
| * @return {string} |
| */ |
| _propertyPath(treeElement) { |
| const cachedPropertyPath = treeElement[ObjectPropertiesSectionsTreeExpandController._cachedPathSymbol]; |
| if (cachedPropertyPath) { |
| return cachedPropertyPath; |
| } |
| |
| let current = treeElement; |
| let sectionRoot = current; |
| const rootElement = treeElement.treeOutline.rootElement(); |
| |
| let result; |
| |
| while (current !== rootElement) { |
| let currentName = ''; |
| if (current.property) { |
| currentName = current.property.name; |
| } else { |
| currentName = typeof current.title === 'string' ? current.title : current.title.textContent; |
| } |
| |
| result = currentName + (result ? '.' + result : ''); |
| sectionRoot = current; |
| current = current.parent; |
| } |
| const treeOutlineId = sectionRoot[ObjectPropertiesSectionsTreeExpandController._treeOutlineId]; |
| result = treeOutlineId + (result ? ':' + result : ''); |
| treeElement[ObjectPropertiesSectionsTreeExpandController._cachedPathSymbol] = result; |
| return result; |
| } |
| } |
| |
| ObjectPropertiesSectionsTreeExpandController._cachedPathSymbol = Symbol('cachedPath'); |
| ObjectPropertiesSectionsTreeExpandController._treeOutlineId = Symbol('treeOutlineId'); |
| |
| /** |
| * @implements {UI.Renderer} |
| */ |
| export class Renderer { |
| /** |
| * @override |
| * @param {!Object} object |
| * @param {!UI.Renderer.Options=} options |
| * @return {!Promise<?{node: !Node, tree: ?UI.TreeOutline}>} |
| */ |
| render(object, options) { |
| if (!(object instanceof SDK.RemoteObject)) { |
| return Promise.reject(new Error('Can\'t render ' + object)); |
| } |
| options = options || {}; |
| const title = options.title; |
| const section = new ObjectPropertiesSection(object, title); |
| if (!title) { |
| section.titleLessMode(); |
| } |
| section.editable = !!options.editable; |
| return Promise.resolve( |
| /** @type {?{node: !Node, tree: ?UI.TreeOutline}} */ ({node: section.element, tree: section})); |
| } |
| } |
| |
| /* Legacy exported object */ |
| self.ObjectUI = self.ObjectUI || {}; |
| |
| /* Legacy exported object */ |
| ObjectUI = ObjectUI || {}; |
| |
| ObjectUI.ArrayGroupingTreeElement = ArrayGroupingTreeElement; |
| |
| /** @constructor */ |
| ObjectUI.ObjectPropertiesSection = ObjectPropertiesSection; |
| |
| ObjectUI.ObjectPropertiesSection._maxRenderableStringLength = _maxRenderableStringLength; |
| |
| /** @constructor */ |
| ObjectUI.ObjectPropertiesSectionsTreeOutline = ObjectPropertiesSectionsTreeOutline; |
| |
| /** |
| * @constructor |
| */ |
| ObjectUI.ObjectPropertiesSection.RootElement = RootElement; |
| |
| /** |
| * @constructor |
| */ |
| ObjectUI.ObjectPropertiesSection.Renderer = Renderer; |
| |
| /** @constructor */ |
| ObjectUI.ObjectPropertyTreeElement = ObjectPropertyTreeElement; |
| |
| /** @constructor */ |
| ObjectUI.ObjectPropertyPrompt = ObjectPropertyPrompt; |
| |
| /** @constructor */ |
| ObjectUI.ObjectPropertiesSectionsTreeExpandController = ObjectPropertiesSectionsTreeExpandController; |
| |
| /** |
| * @typedef {{ |
| * readOnly: (boolean|undefined), |
| * }} |
| */ |
| ObjectUI.ObjectPropertiesSectionsTreeOutlineOptions; |