UNPKG

chrome-devtools-frontend

Version:
1,238 lines (1,117 loc) 69.2 kB
// Copyright 2020 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* * 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. */ /* eslint-disable rulesdir/no_underscored_properties */ import * as Common from '../common/common.js'; import * as Components from '../components/components.js'; // eslint-disable-line no-unused-vars import * as Host from '../host/host.js'; import * as i18n from '../i18n/i18n.js'; import * as LinearMemoryInspector from '../linear_memory_inspector/linear_memory_inspector.js'; import * as Platform from '../platform/platform.js'; import * as SDK from '../sdk/sdk.js'; import * as TextUtils from '../text_utils/text_utils.js'; import * as WebComponents from '../ui/components/components.js'; import * as UI from '../ui/ui.js'; import {CustomPreviewComponent} from './CustomPreviewComponent.js'; import {JavaScriptAutocomplete} from './JavaScriptAutocomplete.js'; import {JavaScriptREPL} from './JavaScriptREPL.js'; import {createSpansForNodeTitle, RemoteObjectPreviewFormatter} from './RemoteObjectPreviewFormatter.js'; export const UIStrings = { /** *@description Text in Object Properties Section *@example {function alert() [native code] } PH1 */ exceptionS: '[Exception: {PH1}]', /** *@description Text in Object Properties Section */ unknown: 'unknown', /** *@description Text to expand something recursively */ expandRecursively: 'Expand recursively', /** *@description Text to collapse children of a parent group */ collapseChildren: 'Collapse children', /** *@description Text in Object Properties Section */ noProperties: 'No properties', /** *@description Element text content in Object Properties Section */ dots: '(...)', /** *@description Element title in Object Properties Section */ invokePropertyGetter: 'Invoke property getter', /** *@description Show all text content in Show More Data Grid Node of a data grid *@example {50} PH1 */ showAllD: 'Show all {PH1}', /** *@description Value element text content in Object Properties Section. Shown when the developer is *viewing a JavaScript object, but one of the properties is not readable and therefore can't be *displayed. This string should be translated. */ unreadable: '<unreadable>', /** *@description Value element title in Object Properties Section */ noPropertyGetter: 'No property getter', /** *@description A context menu item in the Watch Expressions Sidebar Pane of the Sources panel and Network pane request. */ copyValue: 'Copy value', /** *@description A context menu item in the Object Properties Section */ copyPropertyPath: 'Copy property path', /** *@description Text in Object Properties Section */ stringIsTooLargeToEdit: '<string is too large to edit>', /** *@description Text of attribute value when text is too long *@example {30 MB} PH1 */ showMoreS: 'Show more ({PH1})', /** *@description Text of attribute value when text is too long *@example {30 MB} PH1 */ longTextWasTruncatedS: 'long text was truncated ({PH1})', /** *@description Text for copying */ copy: 'Copy', }; const str_ = i18n.i18n.registerUIStrings('object_ui/ObjectPropertiesSection.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const EXPANDABLE_MAX_LENGTH = 50; const parentMap = new WeakMap<SDK.RemoteObject.RemoteObjectProperty, SDK.RemoteObject.RemoteObject|null>(); const objectPropertiesSectionMap = new WeakMap<Element, ObjectPropertiesSection>(); export const getObjectPropertiesSectionFrom = (element: Element): ObjectPropertiesSection|undefined => { return objectPropertiesSectionMap.get(element); }; export class ObjectPropertiesSection extends UI.TreeOutline.TreeOutlineInShadow { _object: SDK.RemoteObject.RemoteObject; _editable: boolean; _objectTreeElement: RootElement; titleElement: Element; _skipProto?: boolean; constructor( object: SDK.RemoteObject.RemoteObject, title?: string|Element|null, linkifier?: Components.Linkifier.Linkifier, emptyPlaceholder?: string|null, ignoreHasOwnProperty?: boolean, extraProperties?: SDK.RemoteObject.RemoteObjectProperty[], showOverflow?: boolean) { 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 instanceof HTMLElement && !this.titleElement.hasAttribute('tabIndex')) { this.titleElement.tabIndex = -1; } objectPropertiesSectionMap.set(this.element, this); this.registerRequiredCSS('object_ui/objectValue.css', {enableLegacyPatching: true}); this.registerRequiredCSS('object_ui/objectPropertiesSection.css', {enableLegacyPatching: true}); this.rootElement().childrenListElement.classList.add('source-code', 'object-properties-section'); } static defaultObjectPresentation( object: SDK.RemoteObject.RemoteObject, linkifier?: Components.Linkifier.Linkifier, skipProto?: boolean, readOnly?: boolean): Element { const objectPropertiesSection = ObjectPropertiesSection.defaultObjectPropertiesSection(object, linkifier, skipProto, readOnly); if (!object.hasChildren) { return objectPropertiesSection.titleElement; } return objectPropertiesSection.element; } static defaultObjectPropertiesSection( object: SDK.RemoteObject.RemoteObject, linkifier?: Components.Linkifier.Linkifier, skipProto?: boolean, readOnly?: boolean): ObjectPropertiesSection { const titleElement = document.createElement('span'); titleElement.classList.add('source-code'); const shadowRoot = UI.Utils.createShadowRootWithCoreStyles( titleElement, {cssFile: 'object_ui/objectValue.css', enableLegacyPatching: true, delegatesFocus: undefined}); const propertyValue = ObjectPropertiesSection.createPropertyValue(object, /* wasThrown */ false, /* showPreview */ true); shadowRoot.appendChild(propertyValue.element); const objectPropertiesSection = new ObjectPropertiesSection(object, titleElement, linkifier); objectPropertiesSection._editable = false; if (skipProto) { objectPropertiesSection.skipProto(); } if (readOnly) { objectPropertiesSection.setEditable(false); } return objectPropertiesSection; } static compareProperties( propertyA: SDK.RemoteObject.RemoteObjectProperty, propertyB: SDK.RemoteObject.RemoteObjectProperty): number { 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 Platform.StringUtilities.naturalOrderComparator(a, b); } static createNameElement(name: string|null, isPrivate?: boolean): Element { if (name === null) { return UI.Fragment.html`<span class="name"></span>`; } if (/^\s|\s$|^$|\n/.test(name)) { return UI.Fragment.html`<span class="name">"${name.replace(/\n/g, '\u21B5')}"</span>`; } if (isPrivate) { return UI.Fragment.html`<span class="name"> <span class="private-property-hash">${name[0]}</span>${name.substring(1)} </span>`; } return UI.Fragment.html`<span class="name">${name}</span>`; } static valueElementForFunctionDescription(description?: string|null, includePreview?: boolean, defaultName?: string): Element { const valueElement = document.createElement('span'); valueElement.classList.add('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: string = 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: string = text; if (defaultName) { abbreviation = defaultName + '()'; } else if (text.length > maxArrowFunctionCharacterLength) { abbreviation = text.substring(0, firstArrowIndex + 2) + ' {…}'; } addElements('', text, abbreviation); } else { addElements('\u0192', text, nameAndArguments(text)); } UI.Tooltip.Tooltip.install(valueElement, Platform.StringUtilities.trimEndWithMaxLength(description, 500)); return valueElement; function nameAndArguments(contents: string): string { const startOfArgumentsIndex = contents.indexOf('('); const endOfArgumentsMatch = contents.match(/\)\s*{/); if (startOfArgumentsIndex !== -1 && endOfArgumentsMatch && endOfArgumentsMatch.index !== undefined && endOfArgumentsMatch.index > startOfArgumentsIndex) { const name = contents.substring(0, startOfArgumentsIndex).trim() || defaultName; const args = contents.substring(startOfArgumentsIndex, endOfArgumentsMatch.index + 1); return name + args; } return defaultName + '()'; } function addElements(prefix: string, body: string, abbreviation: string): void { const maxFunctionBodyLength = 200; if (prefix.length) { valueElement.createChild('span', 'object-value-function-prefix').textContent = prefix + ' '; } if (includePreview) { UI.UIUtils.createTextChild( valueElement, Platform.StringUtilities.trimEndWithMaxLength(body.trim(), maxFunctionBodyLength)); } else { UI.UIUtils.createTextChild(valueElement, abbreviation.replace(/\n/g, ' ')); } } } static createPropertyValueWithCustomSupport( value: SDK.RemoteObject.RemoteObject, wasThrown: boolean, showPreview: boolean, parentElement?: Element, linkifier?: Components.Linkifier.Linkifier): ObjectPropertyValue { if (value.customPreview()) { const result = (new CustomPreviewComponent(value)).element; result.classList.add('object-properties-section-custom-section'); return new ObjectPropertyValue(result); } return ObjectPropertiesSection.createPropertyValue(value, wasThrown, showPreview, parentElement, linkifier); } static appendMemoryIcon(element: Element, obj: SDK.RemoteObject.RemoteObject): void { // We show the memory icon only on ArrayBuffer and WebAssembly.Memory instances. // TypedArrays DataViews are also supported, but showing the icon next to their // previews is quite a significant visual overhead, and users can easily get to // their buffers and open the memory inspector from there. if (obj.type !== 'object' || (obj.subtype !== 'arraybuffer' && obj.subtype !== 'webassemblymemory')) { return; } const memoryIcon = new WebComponents.Icon.Icon(); memoryIcon.data = { iconName: 'ic_memory_16x16', color: 'var(--color-text-secondary)', width: '13px', height: '13px', }; memoryIcon.onclick = (event: MouseEvent): void => { LinearMemoryInspector.LinearMemoryInspectorController.LinearMemoryInspectorController.instance() .openInspectorView(obj, 0); event.stopPropagation(); }; UI.Tooltip.Tooltip.install(memoryIcon, 'Reveal in Memory Inspector panel'); element.appendChild(memoryIcon); } static createPropertyValue( value: SDK.RemoteObject.RemoteObject, wasThrown: boolean, showPreview: boolean, parentElement?: Element, linkifier?: Components.Linkifier.Linkifier): ObjectPropertyValue { let propertyValue; const type = value.type; const subtype = value.subtype; const description = value.description || ''; const className = value.className; if (type === 'object' && subtype === 'internal#location') { const rawLocation = value.debuggerModel().createRawLocationByScriptId( value.value.scriptId, value.value.lineNumber, value.value.columnNumber); if (rawLocation && linkifier) { return new ObjectPropertyValue(linkifier.linkifyRawLocation(rawLocation, '')); } propertyValue = new ObjectPropertyValue(createUnknownInternalLocationElement()); } else if (type === 'string' && typeof description === 'string') { propertyValue = createStringElement(); } else if (type === 'object' && subtype === 'trustedtype') { propertyValue = createTrustedTypeElement(); } else if (type === 'function') { propertyValue = new ObjectPropertyValue(ObjectPropertiesSection.valueElementForFunctionDescription(description)); } else if (type === 'object' && subtype === 'node' && description) { propertyValue = new ObjectPropertyValue(createNodeElement()); } else { const valueElement = document.createElement('span'); valueElement.classList.add('object-value-' + (subtype || type)); if (value.preview && showPreview) { const previewFormatter = new RemoteObjectPreviewFormatter(); previewFormatter.appendObjectPreview(valueElement, value.preview, false /* isEntry */); propertyValue = new ObjectPropertyValue(valueElement); UI.Tooltip.Tooltip.install(propertyValue.element, description || ''); } else if ( description.length > // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // eslint-disable-next-line @typescript-eslint/no-explicit-any ((self as any).ObjectUI.ObjectPropertiesSection._maxRenderableStringLength || maxRenderableStringLength)) { propertyValue = new ExpandableTextPropertyValue(valueElement, description, EXPANDABLE_MAX_LENGTH); } else { propertyValue = new ObjectPropertyValue(valueElement); propertyValue.element.textContent = description; UI.Tooltip.Tooltip.install(propertyValue.element, description); } this.appendMemoryIcon(valueElement, value); } if (wasThrown) { const wrapperElement = document.createElement('span'); wrapperElement.classList.add('error'); wrapperElement.classList.add('value'); wrapperElement.appendChild( i18n.i18n.getFormatLocalizedString(str_, UIStrings.exceptionS, {PH1: propertyValue.element})); propertyValue.element = wrapperElement; } propertyValue.element.classList.add('value'); return propertyValue; function createUnknownInternalLocationElement(): Element { const valueElement = document.createElement('span'); valueElement.textContent = '<' + i18nString(UIStrings.unknown) + '>'; UI.Tooltip.Tooltip.install(valueElement, description || ''); return valueElement; } function createStringElement(): ObjectPropertyValue { const valueElement = document.createElement('span'); valueElement.classList.add('object-value-string'); const text = JSON.stringify(description); let propertyValue; if (description.length > // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // eslint-disable-next-line @typescript-eslint/no-explicit-any ((self as any).ObjectUI.ObjectPropertiesSection._maxRenderableStringLength || maxRenderableStringLength)) { propertyValue = new ExpandableTextPropertyValue(valueElement, text, EXPANDABLE_MAX_LENGTH); } else { UI.UIUtils.createTextChild(valueElement, text); propertyValue = new ObjectPropertyValue(valueElement); UI.Tooltip.Tooltip.install(valueElement, description); } return propertyValue; } function createTrustedTypeElement(): ObjectPropertyValue { const valueElement = (document.createElement('span') as HTMLElement); valueElement.classList.add('object-value-trustedtype'); const text = `${className} "${description}"`; let propertyValue; if (text.length > // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // eslint-disable-next-line @typescript-eslint/no-explicit-any ((self as any).ObjectUI.ObjectPropertiesSection._maxRenderableStringLength || maxRenderableStringLength)) { propertyValue = new ExpandableTextPropertyValue(valueElement, text, EXPANDABLE_MAX_LENGTH); } else { const contentString = createStringElement(); UI.UIUtils.createTextChild(valueElement, `${className} `); valueElement.appendChild(contentString.element); propertyValue = new ObjectPropertyValue(valueElement); UI.Tooltip.Tooltip.install(valueElement, text); } return propertyValue; } function createNodeElement(): Element { const valueElement = document.createElement('span'); valueElement.classList.add('object-value-node'); createSpansForNodeTitle(valueElement, (description as string)); valueElement.addEventListener('click', event => { Common.Revealer.reveal(value); event.consume(true); }, false); valueElement.addEventListener( 'mousemove', () => SDK.OverlayModel.OverlayModel.highlightObjectAsDOMNode(value), false); valueElement.addEventListener('mouseleave', () => SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight(), false); return valueElement; } } static formatObjectAsFunction( func: SDK.RemoteObject.RemoteObject, element: Element, linkify: boolean, includePreview?: boolean): Promise<void> { return func.debuggerModel().functionDetailsPromise(func).then(didGetDetails); function didGetDetails(response: SDK.DebuggerModel.FunctionDetails|null): void { 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: string|('' | 'anonymous') = includePreview ? '' : 'anonymous'; if (response && response.functionName) { defaultName = response.functionName; } const valueElement = ObjectPropertiesSection.valueElementForFunctionDescription(func.description, includePreview, defaultName); element.appendChild(valueElement); } } static _isDisplayableProperty( property: SDK.RemoteObject.RemoteObjectProperty, parentProperty?: SDK.RemoteObject.RemoteObjectProperty): boolean { if (!parentProperty || !parentProperty.synthetic) { return true; } const name = property.name; const useless = (parentProperty.name === '[[Entries]]' && (name === 'length' || name === '__proto__')); return !useless; } skipProto(): void { this._skipProto = true; } expand(): void { this._objectTreeElement.expand(); } setEditable(value: boolean): void { this._editable = value; } objectTreeElement(): UI.TreeOutline.TreeElement { return this._objectTreeElement; } enableContextMenu(): void { this.element.addEventListener('contextmenu', this._contextMenuEventFired.bind(this), false); } _contextMenuEventFired(event: Event): void { const contextMenu = new UI.ContextMenu.ContextMenu(event); contextMenu.appendApplicableItems(this._object); if (this._object instanceof SDK.RemoteObject.LocalJSONObject) { contextMenu.viewSection().appendItem( i18nString(UIStrings.expandRecursively), this._objectTreeElement.expandRecursively.bind(this._objectTreeElement, Number.MAX_VALUE)); contextMenu.viewSection().appendItem( i18nString(UIStrings.collapseChildren), this._objectTreeElement.collapseChildren.bind(this._objectTreeElement)); } contextMenu.show(); } titleLessMode(): void { this._objectTreeElement.listItemElement.classList.add('hidden'); this._objectTreeElement.childrenListElement.classList.add('title-less-mode'); this._objectTreeElement.expand(); } } /** @const */ const ARRAY_LOAD_THRESHOLD = 100; /** @const */ export const maxRenderableStringLength = 10000; export class ObjectPropertiesSectionsTreeOutline extends UI.TreeOutline.TreeOutlineInShadow { _editable: boolean; constructor(options?: TreeOutlineOptions|null) { super(); this.registerRequiredCSS('object_ui/objectValue.css', {enableLegacyPatching: true}); this.registerRequiredCSS('object_ui/objectPropertiesSection.css', {enableLegacyPatching: true}); this._editable = !(options && options.readOnly); this.contentElement.classList.add('source-code'); this.contentElement.classList.add('object-properties-section'); this.hideOverflow(); } } export class RootElement extends UI.TreeOutline.TreeElement { _object: SDK.RemoteObject.RemoteObject; _extraProperties: SDK.RemoteObject.RemoteObjectProperty[]; _ignoreHasOwnProperty: boolean; _emptyPlaceholder: string|null|undefined; toggleOnClick: boolean; _linkifier: Components.Linkifier.Linkifier|undefined; constructor( object: SDK.RemoteObject.RemoteObject, linkifier?: Components.Linkifier.Linkifier, emptyPlaceholder?: string|null, ignoreHasOwnProperty?: boolean, extraProperties?: SDK.RemoteObject.RemoteObjectProperty[]) { const contentElement = document.createElement('slot'); super(contentElement); this._object = object; this._extraProperties = extraProperties || []; this._ignoreHasOwnProperty = Boolean(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; } onexpand(): void { if (this.treeOutline) { this.treeOutline.element.classList.add('expanded'); } } oncollapse(): void { if (this.treeOutline) { this.treeOutline.element.classList.remove('expanded'); } } ondblclick(_e: Event): boolean { return true; } async onpopulate(): Promise<void> { const treeOutline = (this.treeOutline as ObjectPropertiesSection | null); const skipProto = treeOutline ? Boolean(treeOutline._skipProto) : false; return ObjectPropertyTreeElement._populate( this, this._object, skipProto, this._linkifier, this._emptyPlaceholder, this._ignoreHasOwnProperty, this._extraProperties); } } // Number of initially visible children in an ObjectPropertyTreeElement. // Remaining children are shown as soon as requested via a show more properties button. export const InitialVisibleChildrenLimit = 200; export class ObjectPropertyTreeElement extends UI.TreeOutline.TreeElement { property: SDK.RemoteObject.RemoteObjectProperty; toggleOnClick: boolean; _highlightChanges: UI.UIUtils.HighlightChange[]; _linkifier: Components.Linkifier.Linkifier|undefined; _maxNumPropertiesToShow: number; nameElement!: HTMLElement; valueElement!: HTMLElement; _rowContainer!: HTMLElement; _readOnly!: boolean; _prompt!: ObjectPropertyPrompt|undefined; _editableDiv!: HTMLElement; propertyValue?: ObjectPropertyValue; expandedValueElement?: Element|null; constructor(property: SDK.RemoteObject.RemoteObjectProperty, linkifier?: Components.Linkifier.Linkifier) { // Pass an empty title, the title gets made later in onattach. super(); this.property = property; this.toggleOnClick = true; this._highlightChanges = []; this._linkifier = linkifier; this._maxNumPropertiesToShow = InitialVisibleChildrenLimit; this.listItemElement.addEventListener('contextmenu', this._contextMenuFired.bind(this), false); this.listItemElement.dataset.objectPropertyNameForTest = property.name; } static async _populate( treeElement: UI.TreeOutline.TreeElement, value: SDK.RemoteObject.RemoteObject, skipProto: boolean, linkifier?: Components.Linkifier.Linkifier, emptyPlaceholder?: string|null, flattenProtoChain?: boolean, extraProperties?: SDK.RemoteObject.RemoteObjectProperty[], targetValue?: SDK.RemoteObject.RemoteObject): Promise<void> { if (value.arrayLength() > ARRAY_LOAD_THRESHOLD) { 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.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); } static populateWithProperties( treeNode: UI.TreeOutline.TreeElement, properties: SDK.RemoteObject.RemoteObjectProperty[], internalProperties: SDK.RemoteObject.RemoteObjectProperty[]|null, skipProto: boolean, value: SDK.RemoteObject.RemoteObject|null, linkifier?: Components.Linkifier.Linkifier, emptyPlaceholder?: string|null): void { properties.sort(ObjectPropertiesSection.compareProperties); internalProperties = internalProperties || []; const entriesProperty = internalProperties.find(property => property.name === '[[Entries]]'); if (entriesProperty) { parentMap.set(entriesProperty, value); const treeElement = new ObjectPropertyTreeElement(entriesProperty, linkifier); treeElement.setExpandable(true); treeElement.expand(); treeNode.appendChild(treeElement); } const tailProperties = []; let protoProperty: SDK.RemoteObject.RemoteObjectProperty|null = null; for (let i = 0; i < properties.length; ++i) { const property = properties[i]; parentMap.set(property, value); // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // eslint-disable-next-line @typescript-eslint/no-explicit-any if (!ObjectPropertiesSection._isDisplayableProperty(property, (treeNode as any).property)) { continue; } if (property.name === '__proto__' && !property.isAccessorProperty()) { protoProperty = property; continue; } if (property.isOwn && property.getter) { const getterProperty = new SDK.RemoteObject.RemoteObjectProperty('get ' + property.name, property.getter, false); parentMap.set(getterProperty, value); tailProperties.push(getterProperty); } if (property.isOwn && property.setter) { const setterProperty = new SDK.RemoteObject.RemoteObjectProperty('set ' + property.name, property.setter, false); parentMap.set(setterProperty, 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) { parentMap.set(property, value); const treeElement = new ObjectPropertyTreeElement(property, linkifier); if (property.name === '[[Entries]]') { continue; } treeNode.appendChild(treeElement); } ObjectPropertyTreeElement._appendEmptyPlaceholderIfNeeded(treeNode, emptyPlaceholder); } static _appendEmptyPlaceholderIfNeeded(treeNode: UI.TreeOutline.TreeElement, emptyPlaceholder?: string|null): void { if (treeNode.childCount()) { return; } const title = document.createElement('div'); title.classList.add('gray-info-message'); title.textContent = emptyPlaceholder || i18nString(UIStrings.noProperties); const infoElement = new UI.TreeOutline.TreeElement(title); treeNode.appendChild(infoElement); } static createRemoteObjectAccessorPropertySpan( object: SDK.RemoteObject.RemoteObject|null, propertyPath: string[], callback: (arg0: SDK.RemoteObject.CallFunctionResult) => void): HTMLElement { const rootElement = (document.createElement('span') as HTMLElement); const element = rootElement.createChild('span'); element.textContent = i18nString(UIStrings.dots); if (!object) { return rootElement; } element.classList.add('object-value-calculate-value-button'); UI.Tooltip.Tooltip.install(element, i18nString(UIStrings.invokePropertyGetter)); element.addEventListener('click', onInvokeGetterClick, false); function onInvokeGetterClick(event: Event): void { event.consume(); if (object) { // The definition of callFunction expects an unknown, and setting to `any` causes Closure to fail. // However, leaving this as unknown also causes TypeScript to fail, so for now we leave this as unchecked. // @ts-ignore TODO(crbug.com/1011811): Fix after Closure is removed. object.callFunction(invokeGetter, [{value: JSON.stringify(propertyPath)}]).then(callback); } } function invokeGetter(this: Object, arrayStr: string): Object { let result: Object = this; const properties = JSON.parse(arrayStr); for (let i = 0, n = properties.length; i < n; ++i) { // @ts-ignore callFunction expects this to be a generic Object, so while this works we can't be more specific on types. result = result[properties[i]]; } return result; } return rootElement; } setSearchRegex(regex: RegExp, additionalCssClassName?: string): boolean { let cssClasses = UI.UIUtils.highlightedSearchResultClassName; if (additionalCssClassName) { cssClasses += ' ' + additionalCssClassName; } this.revertHighlightChanges(); this._applySearch(regex, this.nameElement, cssClasses); if (this.property.value) { const valueType = this.property.value.type; if (valueType !== 'object') { this._applySearch(regex, this.valueElement, cssClasses); } } return Boolean(this._highlightChanges.length); } _applySearch(regex: RegExp, element: Element, cssClassName: string): void { const ranges = []; const content = element.textContent || ''; regex.lastIndex = 0; let match = regex.exec(content); while (match) { ranges.push(new TextUtils.TextRange.SourceRange(match.index, match[0].length)); match = regex.exec(content); } if (ranges.length) { UI.UIUtils.highlightRangesWithStyleClass(element, ranges, cssClassName, this._highlightChanges); } } _showAllPropertiesElementSelected(element: UI.TreeOutline.TreeElement): boolean { this.removeChild(element); this.children().forEach(x => { x.hidden = false; }); return false; } _createShowAllPropertiesButton(): void { const element = document.createElement('div'); element.classList.add('object-value-calculate-value-button'); element.textContent = i18nString(UIStrings.dots); UI.Tooltip.Tooltip.install(element, i18nString(UIStrings.showAllD, {PH1: this.childCount()})); const children = this.children(); for (let i = this._maxNumPropertiesToShow; i < this.childCount(); ++i) { children[i].hidden = true; } const showAllPropertiesButton = new UI.TreeOutline.TreeElement(element); showAllPropertiesButton.onselect = this._showAllPropertiesElementSelected.bind(this, showAllPropertiesButton); this.appendChild(showAllPropertiesButton); } revertHighlightChanges(): void { UI.UIUtils.revertDomChanges(this._highlightChanges); this._highlightChanges = []; } async onpopulate(): Promise<void> { const propertyValue = (this.property.value as SDK.RemoteObject.RemoteObject); console.assert(typeof propertyValue !== 'undefined'); const treeOutline = (this.treeOutline as ObjectPropertiesSection | null); const skipProto = treeOutline ? Boolean(treeOutline._skipProto) : false; const targetValue = this.property.name !== '__proto__' ? propertyValue : parentMap.get(this.property); if (targetValue) { await ObjectPropertyTreeElement._populate( this, propertyValue, skipProto, this._linkifier, undefined, undefined, undefined, targetValue); if (this.childCount() > this._maxNumPropertiesToShow) { this._createShowAllPropertiesButton(); } } } ondblclick(event: Event): boolean { const target = (event.target as HTMLElement); const inEditableElement = target.isSelfOrDescendant(this.valueElement) || (this.expandedValueElement && target.isSelfOrDescendant(this.expandedValueElement)); if (this.property.value && !this.property.value.customPreview() && inEditableElement && (this.property.writable || this.property.setter)) { this._startEditing(); } return false; } onenter(): boolean { if (this.property.value && !this.property.value.customPreview() && (this.property.writable || this.property.setter)) { this._startEditing(); return true; } return false; } onattach(): void { this.update(); this._updateExpandable(); } onexpand(): void { this._showExpandedValueElement(true); } oncollapse(): void { this._showExpandedValueElement(false); } _showExpandedValueElement(value: boolean): void { if (!this.expandedValueElement) { return; } if (value) { this._rowContainer.replaceChild(this.expandedValueElement, this.valueElement); } else { this._rowContainer.replaceChild(this.valueElement, this.expandedValueElement); } } _createExpandedValueElement(value: SDK.RemoteObject.RemoteObject): Element|null { const needsAlternateValue = value.hasChildren && !value.customPreview() && value.subtype !== 'node' && value.type !== 'function' && (value.type !== 'object' || value.preview); if (!needsAlternateValue) { return null; } const valueElement = document.createElement('span'); valueElement.classList.add('value'); if (value.description === 'Object') { valueElement.textContent = ''; } else { valueElement.setTextContentTruncatedIfNeeded(value.description || ''); } valueElement.classList.add('object-value-' + (value.subtype || value.type)); UI.Tooltip.Tooltip.install(valueElement, value.description || ''); ObjectPropertiesSection.appendMemoryIcon(valueElement, value); return valueElement; } update(): void { this.nameElement = (ObjectPropertiesSection.createNameElement(this.property.name, this.property.private) as HTMLElement); 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 = (document.createElement('span') as HTMLElement); this.valueElement.classList.add('value'); } else if (this.property.value) { const showPreview = this.property.name !== '__proto__'; this.propertyValue = ObjectPropertiesSection.createPropertyValueWithCustomSupport( this.property.value, this.property.wasThrown, showPreview, this.listItemElement, this._linkifier); this.valueElement = (this.propertyValue.element as HTMLElement); } else if (this.property.getter) { this.valueElement = ObjectPropertyTreeElement.createRemoteObjectAccessorPropertySpan( (parentMap.get(this.property) as SDK.RemoteObject.RemoteObject), [this.property.name], this._onInvokeGetterClick.bind(this)); } else { this.valueElement = (document.createElement('span') as HTMLElement); this.valueElement.classList.add('object-value-undefined'); this.valueElement.textContent = i18nString(UIStrings.unreadable); UI.Tooltip.Tooltip.install(this.valueElement, i18nString(UIStrings.noPropertyGetter)); } const valueText = this.valueElement.textContent; if (this.property.value && valueText && !this.property.wasThrown) { this.expandedValueElement = this._createExpandedValueElement(this.property.value); } this.listItemElement.removeChildren(); let container: Element; if (isInternalEntries) { container = UI.Fragment.html`<span class='name-and-value'>${this.nameElement}</span>`; } else { container = UI.Fragment.html`<span class='name-and-value'>${this.nameElement}: ${this.valueElement}</span>`; } this._rowContainer = (container as HTMLElement); this.listItemElement.appendChild(this._rowContainer); } _updatePropertyPath(): void { if (UI.Tooltip.Tooltip.getContent(this.nameElement)) { return; } const name = this.property.name; if (this.property.synthetic) { UI.Tooltip.Tooltip.install(this.nameElement, name); return; } // https://tc39.es/ecma262/#prod-IdentifierName const useDotNotation = /^(?:[$_\p{ID_Start}])(?:[$_\u200C\u200D\p{ID_Continue}])*$/u; const isInteger = /^(?:0|[1-9]\d*)$/; const parentPath = (this.parent instanceof ObjectPropertyTreeElement && this.parent.nameElement && !this.parent.property.synthetic) ? UI.Tooltip.Tooltip.getContent(this.parent.nameElement) : ''; if (this.property.private || useDotNotation.test(name)) { UI.Tooltip.Tooltip.install(this.nameElement, parentPath ? `${parentPath}.${name}` : name); } else if (isInteger.test(name)) { UI.Tooltip.Tooltip.install(this.nameElement, `${parentPath}[${name}]`); } else { UI.Tooltip.Tooltip.install(this.nameElement, `${parentPath}[${JSON.stringify(name)}]`); } } _contextMenuFired(event: Event): void { const contextMenu = new UI.ContextMenu.ContextMenu(event); contextMenu.appendApplicableItems(this); if (this.property.symbol) { contextMenu.appendApplicableItems(this.property.symbol); } if (this.property.value) { contextMenu.appendApplicableItems(this.property.value); if (parentMap.get(this.property) instanceof SDK.RemoteObject.LocalJSONObject) { const {value: {value}} = this.property; const propertyValue = typeof value === 'object' ? JSON.stringify(value, null, 2) : value; const copyValueHandler = (): void => { Host.userMetrics.actionTaken(Host.UserMetrics.Action.NetworkPanelCopyValue); Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText((propertyValue as string | undefined)); }; contextMenu.clipboardSection().appendItem(i18nString(UIStrings.copyValue), copyValueHandler); } } if (!this.property.synthetic && this.nameElement && UI.Tooltip.Tooltip.getContent(this.nameElement)) { const copyPathHandler = Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText.bind( Host.InspectorFrontendHost.InspectorFrontendHostInstance, UI.Tooltip.Tooltip.getContent(this.nameElement)); contextMenu.clipboardSection().appendItem(i18nString(UIStrings.copyPropertyPath), copyPathHandler); } if (parentMap.get(this.property) instanceof SDK.RemoteObject.LocalJSONObject) { contextMenu.viewSection().appendItem( i18nString(UIStrings.expandRecursively), this.expandRecursively.bind(this, Number.MAX_VALUE)); contextMenu.viewSection().appendItem(i18nString(UIStrings.collapseChildren), this.collapseChildren.bind(this)); } if (this.propertyValue) { this.propertyValue.appendApplicableItems(event, contextMenu, {}); } contextMenu.show(); } _startEditing(): void { const treeOutline = (this.treeOutline as ObjectPropertiesSection | null); if (this._prompt || !treeOutline || !treeOutline._editable || this._readOnly) { return; } this._editableDiv = (this._rowContainer.createChild('span', 'editable-div') as HTMLElement); if (this.property.value) { let text: string|(string | undefined) = this.property.value.description; if (this.property.value.type === 'string' && typeof text === 'string') { text = `"${text}"`; } this._editableDiv.setTextContentTruncatedIfNeeded(text, i18nString(UIStrings.stringIsTooLargeToEdit)); } 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'); const selection = this.listItemElement.getComponentSelection(); if (selection) { selection.selectAllChildren(this._editableDiv); } proxyElement.addEventListener('keydown', this._promptKeyDown.bind(this, originalContent), false); } _editingEnded(): void { if (this._prompt) { 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(): void { this.valueElement.classList.remove('hidden'); this._editingEnded(); } async _editingCommitted(originalContent: string): Promise<void> { const userInput = this._prompt ? this._prompt.text() : ''; if (userInput === originalContent) { this._editingCancelled(); // nothing changed, so cancel return; } this._editingEnded(); await this._applyExpression(userInput); } _promptKeyDown(originalContent: string, event: Event): void { const keyboardEvent = (event as KeyboardEvent); if (keyboardEvent.key === 'Enter') { keyboardEvent.consume(); this._editingCommitted(originalContent); return; } if (keyboardEvent.key === 'Escape') { keyboardEvent.consume(); this._editingCancelled(); return; } } async _applyExpression(expression: string): Promise<void> { const property = SDK.RemoteObject.RemoteObject.toCallArgument(this.property.symbol || this.property.name); expression = 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; if (parent) { parent.invalidateChildren(); parent.onpopulate(); } } else { this.update(); } return; } const parentObject = (parentMap.get(this.property) as SDK.RemoteObject.RemoteObject); const errorPromise = expression ? parentObject.setPropertyValue(property, expression) : 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 && this.parent.removeChild(this); } else { // Call updateSiblings since their value might be based on the value that just changed. const parent = this.parent; if (parent) { parent.invalidateChildren(); parent.onpopulate(); } } } _onInvokeGetterClick(result: SDK.RemoteObject.CallFunctionResult): void { if (!result.object) { return; } this.property.value = result.object; this.property.wasThrown = result.wasThrown || false; this.update(); this.invalidateChildren(); this._updateExpandable(); } _updateExpandable(): void { if (this.property.value) { this.setExpandable( !this.property.value.customPreview() && this.property.value.hasChildren && !this.property.wasThrown); } else { this.setExpandable(false); } } path(): string { return UI.Tooltip.Tooltip.getContent(this.nameElement); } } export class ArrayGroupingTreeElement extends UI.TreeOutline.TreeElement { toggleOnClick: boolean; _fromIndex: number; _toIndex: number; _object: SDK.RemoteObject.RemoteObject; _readOnly: boolean; _propertyCount: number; _linkifier: Components.Linkifier.Linkifier|undefined; constructor( object: SDK.RemoteObject.RemoteObject, fromIndex: number, toIndex: number, propertyCount: number, linkifier?: Components.Linkifier.Linkifier) { super(Platform.StringUtilities.sprintf('[%d … %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; } static async _populateArray( treeNode: UI.TreeOutline.TreeElement, object: SDK.RemoteObject.RemoteObject, fromIndex: number, toIndex: number, linkifier?: Components.Linkifier.Linkifier): Promise<void> { await ArrayGroupingTreeElement._populateRanges(treeNode, object, fromIndex, toIndex, true, linkifier); } static async _populateRanges( treeNode: U