| /* |
| * 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 |
| */ |
| Components.ObjectPropertiesSection = class extends TreeOutlineInShadow { |
| /** |
| * @param {!SDK.RemoteObject} object |
| * @param {?string|!Element=} title |
| * @param {!Components.Linkifier=} linkifier |
| * @param {?string=} emptyPlaceholder |
| * @param {boolean=} ignoreHasOwnProperty |
| * @param {!Array.<!SDK.RemoteObjectProperty>=} extraProperties |
| */ |
| constructor(object, title, linkifier, emptyPlaceholder, ignoreHasOwnProperty, extraProperties) { |
| super(); |
| this._object = object; |
| this._editable = true; |
| this.hideOverflow(); |
| this.setFocusable(false); |
| this._objectTreeElement = new Components.ObjectPropertiesSection.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 (object.description && Components.ObjectPropertiesSection._needsAlternateTitle(object)) { |
| this.expandedTitleElement = createElement('span'); |
| this.expandedTitleElement.createTextChild(object.description); |
| |
| var note = this.expandedTitleElement.createChild('span', 'object-state-note'); |
| note.classList.add('info-note'); |
| note.title = Common.UIString('Value below was evaluated just now.'); |
| } |
| |
| this.element._section = this; |
| this.registerRequiredCSS('components/objectValue.css'); |
| this.registerRequiredCSS('components/objectPropertiesSection.css'); |
| this.rootElement().childrenListElement.classList.add('source-code', 'object-properties-section'); |
| } |
| |
| /** |
| * @param {!SDK.RemoteObject} object |
| * @param {!Components.Linkifier=} linkifier |
| * @param {boolean=} skipProto |
| * @return {!Element} |
| */ |
| static defaultObjectPresentation(object, linkifier, skipProto) { |
| var componentRoot = createElementWithClass('span', 'source-code'); |
| var shadowRoot = UI.createShadowRootWithCoreStyles(componentRoot, 'components/objectValue.css'); |
| shadowRoot.appendChild(Components.ObjectPropertiesSection.createValueElement(object, false)); |
| if (!object.hasChildren) |
| return componentRoot; |
| |
| var objectPropertiesSection = new Components.ObjectPropertiesSection(object, componentRoot, linkifier); |
| objectPropertiesSection.editable = false; |
| if (skipProto) |
| objectPropertiesSection.skipProto(); |
| |
| return objectPropertiesSection.element; |
| } |
| |
| /** |
| * @param {!SDK.RemoteObjectProperty} propertyA |
| * @param {!SDK.RemoteObjectProperty} propertyB |
| * @return {number} |
| */ |
| static CompareProperties(propertyA, propertyB) { |
| var a = propertyA.name; |
| var b = propertyB.name; |
| if (a === '__proto__') |
| return 1; |
| if (b === '__proto__') |
| return -1; |
| if (propertyA.symbol && !propertyB.symbol) |
| return 1; |
| if (propertyB.symbol && !propertyA.symbol) |
| return -1; |
| return String.naturalOrderComparator(a, b); |
| } |
| |
| /** |
| * @param {?string} name |
| * @return {!Element} |
| */ |
| static createNameElement(name) { |
| var nameElement = createElementWithClass('span', 'name'); |
| if (/^\s|\s$|^$|\n/.test(name)) |
| nameElement.createTextChildren('"', name.replace(/\n/g, '\u21B5'), '"'); |
| else |
| nameElement.textContent = name; |
| return nameElement; |
| } |
| |
| /** |
| * @param {?string=} description |
| * @return {string} valueText |
| */ |
| static valueTextForFunctionDescription(description) { |
| var text = description.replace(/^function [gs]et /, 'function '); |
| var functionPrefixWithArguments = |
| new RegExp(Components.ObjectPropertiesSection._functionPrefixSource.source + '([^)]*)'); |
| var matches = functionPrefixWithArguments.exec(text); |
| if (!matches) { |
| // process shorthand methods |
| matches = /[^(]*(\([^)]*)/.exec(text); |
| } |
| var match = matches ? matches[1] : null; |
| return match ? match.replace(/\n/g, ' ') + ')' : (text || ''); |
| } |
| |
| /** |
| * @param {!SDK.RemoteObject} value |
| * @param {boolean} wasThrown |
| * @param {!Element=} parentElement |
| * @param {!Components.Linkifier=} linkifier |
| * @return {!Element} |
| */ |
| static createValueElementWithCustomSupport(value, wasThrown, parentElement, linkifier) { |
| if (value.customPreview()) { |
| var result = (new Components.CustomPreviewComponent(value)).element; |
| result.classList.add('object-properties-section-custom-section'); |
| return result; |
| } |
| return Components.ObjectPropertiesSection.createValueElement(value, wasThrown, parentElement, linkifier); |
| } |
| |
| /** |
| * @param {!SDK.RemoteObject} value |
| * @param {boolean} wasThrown |
| * @param {!Element=} parentElement |
| * @param {!Components.Linkifier=} linkifier |
| * @return {!Element} |
| */ |
| static createValueElement(value, wasThrown, parentElement, linkifier) { |
| var valueElement = createElementWithClass('span', 'value'); |
| var type = value.type; |
| var subtype = value.subtype; |
| var description = value.description; |
| var prefix; |
| var valueText; |
| var suffix; |
| if (wasThrown) { |
| prefix = '[Exception: '; |
| valueText = description; |
| suffix = ']'; |
| } else if (type === 'string' && typeof description === 'string') { |
| // Render \n as a nice unicode cr symbol. |
| prefix = '"'; |
| valueText = description.replace(/\n/g, '\u21B5'); |
| suffix = '"'; |
| } else if (type === 'function') { |
| valueText = Components.ObjectPropertiesSection.valueTextForFunctionDescription(description); |
| } else if (type !== 'object' || subtype !== 'node') { |
| valueText = description; |
| } |
| if (type !== 'number' || valueText.indexOf('e') === -1) { |
| valueElement.setTextContentTruncatedIfNeeded(valueText || ''); |
| if (prefix) |
| valueElement.insertBefore(createTextNode(prefix), valueElement.firstChild); |
| if (suffix) |
| valueElement.createTextChild(suffix); |
| } else { |
| var numberParts = valueText.split('e'); |
| var mantissa = valueElement.createChild('span', 'object-value-scientific-notation-mantissa'); |
| mantissa.textContent = numberParts[0]; |
| var exponent = valueElement.createChild('span', 'object-value-scientific-notation-exponent'); |
| exponent.textContent = 'e' + numberParts[1]; |
| valueElement.classList.add('object-value-scientific-notation-number'); |
| if (parentElement) // FIXME: do it in the caller. |
| parentElement.classList.add('hbox'); |
| } |
| |
| if (wasThrown) |
| valueElement.classList.add('error'); |
| if (subtype || type) |
| valueElement.classList.add('object-value-' + (subtype || type)); |
| |
| if (type === 'object' && subtype === 'node' && description) { |
| Components.DOMPresentationUtils.createSpansForNodeTitle(valueElement, description); |
| valueElement.addEventListener('click', mouseClick, false); |
| valueElement.addEventListener('mousemove', mouseMove, false); |
| valueElement.addEventListener('mouseleave', mouseLeave, false); |
| } else { |
| valueElement.title = description || ''; |
| } |
| |
| if (type === 'object' && subtype === 'internal#location') { |
| var rawLocation = value.debuggerModel().createRawLocationByScriptId( |
| value.value.scriptId, value.value.lineNumber, value.value.columnNumber); |
| if (rawLocation && linkifier) |
| return linkifier.linkifyRawLocation(rawLocation, ''); |
| valueElement.textContent = '<unknown>'; |
| } |
| |
| function mouseMove() { |
| SDK.DOMModel.highlightObjectAsDOMNode(value); |
| } |
| |
| function mouseLeave() { |
| SDK.DOMModel.hideDOMNodeHighlight(); |
| } |
| |
| /** |
| * @param {!Event} event |
| */ |
| function mouseClick(event) { |
| Common.Revealer.reveal(value); |
| event.consume(true); |
| } |
| |
| return valueElement; |
| } |
| |
| /** |
| * @param {!SDK.RemoteObject} object |
| * @return {boolean} |
| */ |
| static _needsAlternateTitle(object) { |
| return object && object.hasChildren && !object.customPreview() && object.subtype !== 'node' && |
| object.type !== 'function' && (object.type !== 'object' || object.preview); |
| } |
| |
| /** |
| * @param {!SDK.RemoteObject} func |
| * @param {!Element} element |
| * @param {boolean} linkify |
| * @param {boolean=} includePreview |
| */ |
| static formatObjectAsFunction(func, element, linkify, includePreview) { |
| func.debuggerModel().functionDetailsPromise(func).then(didGetDetails); |
| |
| /** |
| * @param {?SDK.DebuggerModel.FunctionDetails} response |
| */ |
| function didGetDetails(response) { |
| if (!response) { |
| var valueText = Components.ObjectPropertiesSection.valueTextForFunctionDescription(func.description); |
| element.createTextChild(valueText); |
| return; |
| } |
| |
| var matched = func.description.match(Components.ObjectPropertiesSection._functionPrefixSource); |
| if (matched) { |
| var prefix = createElementWithClass('span', 'object-value-function-prefix'); |
| prefix.textContent = matched[0]; |
| element.appendChild(prefix); |
| } |
| |
| if (linkify && response && response.location) { |
| var anchor = createElement('span'); |
| element.classList.add('linkified'); |
| element.appendChild(anchor); |
| element.addEventListener( |
| 'click', Common.Revealer.reveal.bind(Common.Revealer, response.location, undefined)); |
| element = anchor; |
| } |
| |
| var text = func.description.substring(0, 200); |
| if (includePreview) { |
| element.createTextChild( |
| text.replace(Components.ObjectPropertiesSection._functionPrefixSource, '') + |
| (func.description.length > 200 ? '\u2026' : '')); |
| return; |
| } |
| |
| // Now parse description and get the real params and title. |
| self.runtime.extension(Common.TokenizerFactory).instance().then(processTokens); |
| |
| var params = null; |
| var functionName = response ? response.functionName : ''; |
| |
| /** |
| * @param {!Common.TokenizerFactory} tokenizerFactory |
| */ |
| function processTokens(tokenizerFactory) { |
| var tokenize = tokenizerFactory.createTokenizer('text/javascript'); |
| tokenize(text, processToken); |
| element.createTextChild((functionName || 'anonymous') + '(' + (params || []).join(', ') + ')'); |
| } |
| |
| var doneProcessing = false; |
| |
| /** |
| * @param {string} token |
| * @param {?string} tokenType |
| * @param {number} column |
| * @param {number} newColumn |
| */ |
| function processToken(token, tokenType, column, newColumn) { |
| if (!params && tokenType === 'js-def' && !functionName) |
| functionName = token; |
| doneProcessing = doneProcessing || token === ')'; |
| if (doneProcessing) |
| return; |
| if (token === '(') { |
| params = []; |
| return; |
| } |
| if (params && tokenType === 'js-def') |
| params.push(token); |
| } |
| } |
| } |
| |
| skipProto() { |
| this._skipProto = true; |
| } |
| |
| expand() { |
| this._objectTreeElement.expand(); |
| } |
| |
| /** |
| * @param {boolean} value |
| */ |
| setEditable(value) { |
| this._editable = value; |
| } |
| |
| /** |
| * @return {!TreeElement} |
| */ |
| objectTreeElement() { |
| return this._objectTreeElement; |
| } |
| |
| enableContextMenu() { |
| this.element.addEventListener('contextmenu', this._contextMenuEventFired.bind(this), false); |
| } |
| |
| _contextMenuEventFired(event) { |
| var contextMenu = new UI.ContextMenu(event); |
| contextMenu.appendApplicableItems(this._object); |
| contextMenu.show(); |
| } |
| |
| titleLessMode() { |
| this._objectTreeElement.listItemElement.classList.add('hidden'); |
| this._objectTreeElement.childrenListElement.classList.add('title-less-mode'); |
| this._objectTreeElement.expand(); |
| } |
| }; |
| |
| /** @const */ |
| Components.ObjectPropertiesSection._arrayLoadThreshold = 100; |
| |
| |
| /** |
| * @unrestricted |
| */ |
| Components.ObjectPropertiesSection.RootElement = class extends 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) { |
| var contentElement = createElement('content'); |
| super(contentElement); |
| |
| this._object = object; |
| this._extraProperties = extraProperties || []; |
| this._ignoreHasOwnProperty = !!ignoreHasOwnProperty; |
| this._emptyPlaceholder = emptyPlaceholder; |
| |
| this.setExpandable(true); |
| this.selectable = false; |
| 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'); |
| this._showExpandedTitleElement(true); |
| } |
| } |
| |
| /** |
| * @override |
| */ |
| oncollapse() { |
| if (this.treeOutline) { |
| this.treeOutline.element.classList.remove('expanded'); |
| this._showExpandedTitleElement(false); |
| } |
| } |
| |
| /** |
| * @param {boolean} value |
| */ |
| _showExpandedTitleElement(value) { |
| if (!this.treeOutline.expandedTitleElement) |
| return; |
| if (value) |
| this.treeOutline.element.replaceChild(this.treeOutline.expandedTitleElement, this.treeOutline.titleElement); |
| else |
| this.treeOutline.element.replaceChild(this.treeOutline.titleElement, this.treeOutline.expandedTitleElement); |
| } |
| |
| /** |
| * @override |
| * @param {!Event} e |
| * @return {boolean} |
| */ |
| ondblclick(e) { |
| return true; |
| } |
| |
| /** |
| * @override |
| */ |
| onpopulate() { |
| Components.ObjectPropertyTreeElement._populate( |
| this, this._object, !!this.treeOutline._skipProto, this._linkifier, this._emptyPlaceholder, |
| this._ignoreHasOwnProperty, this._extraProperties); |
| } |
| }; |
| |
| /** |
| * @unrestricted |
| */ |
| Components.ObjectPropertyTreeElement = class extends 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; |
| this.selectable = false; |
| /** @type {!Array.<!Object>} */ |
| this._highlightChanges = []; |
| this._linkifier = linkifier; |
| } |
| |
| /** |
| * @param {!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 |
| */ |
| static _populate( |
| treeElement, |
| value, |
| skipProto, |
| linkifier, |
| emptyPlaceholder, |
| flattenProtoChain, |
| extraProperties, |
| targetValue) { |
| if (value.arrayLength() > Components.ObjectPropertiesSection._arrayLoadThreshold) { |
| treeElement.removeChildren(); |
| Components.ArrayGroupingTreeElement._populateArray(treeElement, value, 0, value.arrayLength() - 1, linkifier); |
| return; |
| } |
| |
| /** |
| * @param {?Array.<!SDK.RemoteObjectProperty>} properties |
| * @param {?Array.<!SDK.RemoteObjectProperty>} internalProperties |
| */ |
| function callback(properties, internalProperties) { |
| treeElement.removeChildren(); |
| if (!properties) |
| return; |
| |
| extraProperties = extraProperties || []; |
| for (var i = 0; i < extraProperties.length; ++i) |
| properties.push(extraProperties[i]); |
| |
| Components.ObjectPropertyTreeElement.populateWithProperties( |
| treeElement, properties, internalProperties, skipProto, targetValue || value, linkifier, emptyPlaceholder); |
| } |
| |
| if (flattenProtoChain) |
| value.getAllProperties(false, callback); |
| else |
| SDK.RemoteObject.loadFromObjectPerProto(value, callback); |
| } |
| |
| /** |
| * @param {!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) { |
| properties.sort(Components.ObjectPropertiesSection.CompareProperties); |
| |
| var tailProperties = []; |
| var protoProperty = null; |
| for (var i = 0; i < properties.length; ++i) { |
| var property = properties[i]; |
| property.parentObject = value; |
| if (property.name === '__proto__' && !property.isAccessorProperty()) { |
| protoProperty = property; |
| continue; |
| } |
| |
| if (property.isOwn && property.getter) { |
| var getterProperty = new SDK.RemoteObjectProperty('get ' + property.name, property.getter, false); |
| getterProperty.parentObject = value; |
| tailProperties.push(getterProperty); |
| } |
| if (property.isOwn && property.setter) { |
| var setterProperty = new SDK.RemoteObjectProperty('set ' + property.name, property.setter, false); |
| setterProperty.parentObject = value; |
| tailProperties.push(setterProperty); |
| } |
| var canShowProperty = property.getter || !property.isAccessorProperty(); |
| if (canShowProperty && property.name !== '__proto__') |
| treeNode.appendChild(new Components.ObjectPropertyTreeElement(property, linkifier)); |
| } |
| for (var i = 0; i < tailProperties.length; ++i) |
| treeNode.appendChild(new Components.ObjectPropertyTreeElement(tailProperties[i], linkifier)); |
| if (!skipProto && protoProperty) |
| treeNode.appendChild(new Components.ObjectPropertyTreeElement(protoProperty, linkifier)); |
| |
| if (internalProperties) { |
| for (var i = 0; i < internalProperties.length; i++) { |
| internalProperties[i].parentObject = value; |
| var treeElement = new Components.ObjectPropertyTreeElement(internalProperties[i], linkifier); |
| if (internalProperties[i].name === '[[Entries]]') { |
| treeElement.setExpandable(true); |
| treeElement.expand(); |
| } |
| treeNode.appendChild(treeElement); |
| } |
| } |
| |
| Components.ObjectPropertyTreeElement._appendEmptyPlaceholderIfNeeded(treeNode, emptyPlaceholder); |
| } |
| |
| /** |
| * @param {!TreeElement} treeNode |
| * @param {?string=} emptyPlaceholder |
| */ |
| static _appendEmptyPlaceholderIfNeeded(treeNode, emptyPlaceholder) { |
| if (treeNode.childCount()) |
| return; |
| var title = createElementWithClass('div', 'gray-info-message'); |
| title.textContent = emptyPlaceholder || Common.UIString('No Properties'); |
| var infoElement = new TreeElement(title); |
| treeNode.appendChild(infoElement); |
| } |
| |
| /** |
| * @param {?SDK.RemoteObject} object |
| * @param {!Array.<string>} propertyPath |
| * @param {function(?SDK.RemoteObject, boolean=)} callback |
| * @return {!Element} |
| */ |
| static createRemoteObjectAccessorPropertySpan(object, propertyPath, callback) { |
| var rootElement = createElement('span'); |
| var 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.getProperty(propertyPath, callback); |
| } |
| |
| return rootElement; |
| } |
| |
| /** |
| * @param {!RegExp} regex |
| * @param {string=} additionalCssClassName |
| * @return {boolean} |
| */ |
| setSearchRegex(regex, additionalCssClassName) { |
| var cssClasses = UI.highlightedSearchResultClassName; |
| if (additionalCssClassName) |
| cssClasses += ' ' + additionalCssClassName; |
| this.revertHighlightChanges(); |
| |
| this._applySearch(regex, this.nameElement, cssClasses); |
| var 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) { |
| var ranges = []; |
| var content = element.textContent; |
| regex.lastIndex = 0; |
| var match = regex.exec(content); |
| while (match) { |
| ranges.push(new Common.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 |
| */ |
| onpopulate() { |
| var propertyValue = /** @type {!SDK.RemoteObject} */ (this.property.value); |
| console.assert(propertyValue); |
| var skipProto = this.treeOutline ? this.treeOutline._skipProto : true; |
| var targetValue = this.property.name !== '__proto__' ? propertyValue : this.property.parentObject; |
| Components.ObjectPropertyTreeElement._populate( |
| this, propertyValue, skipProto, this._linkifier, undefined, undefined, undefined, targetValue); |
| } |
| |
| /** |
| * @override |
| * @return {boolean} |
| */ |
| ondblclick(event) { |
| var 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 |
| */ |
| 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.listItemElement.replaceChild(this.expandedValueElement, this.valueElement); |
| else |
| this.listItemElement.replaceChild(this.valueElement, this.expandedValueElement); |
| } |
| |
| /** |
| * @param {!SDK.RemoteObject} value |
| * @return {?Element} |
| */ |
| _createExpandedValueElement(value) { |
| if (!Components.ObjectPropertiesSection._needsAlternateTitle(value)) |
| return null; |
| |
| var valueElement = createElementWithClass('span', 'value'); |
| valueElement.setTextContentTruncatedIfNeeded(value.description || ''); |
| valueElement.classList.add('object-value-' + (value.subtype || value.type)); |
| valueElement.title = value.description || ''; |
| return valueElement; |
| } |
| |
| update() { |
| this.nameElement = Components.ObjectPropertiesSection.createNameElement(this.property.name); |
| if (!this.property.enumerable) |
| this.nameElement.classList.add('object-properties-section-dimmed'); |
| if (this.property.synthetic) |
| this.nameElement.classList.add('synthetic-property'); |
| |
| this._updatePropertyPath(); |
| this.nameElement.addEventListener('contextmenu', this._contextMenuFired.bind(this, this.property), false); |
| |
| var separatorElement = createElementWithClass('span', 'object-properties-section-separator'); |
| separatorElement.textContent = ': '; |
| |
| if (this.property.value) { |
| this.valueElement = Components.ObjectPropertiesSection.createValueElementWithCustomSupport( |
| this.property.value, this.property.wasThrown, this.listItemElement, this._linkifier); |
| this.valueElement.addEventListener('contextmenu', this._contextMenuFired.bind(this, this.property), false); |
| } else if (this.property.getter) { |
| this.valueElement = Components.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'); |
| } |
| |
| var valueText = this.valueElement.textContent; |
| if (this.property.value && valueText && !this.property.wasThrown) |
| this.expandedValueElement = this._createExpandedValueElement(this.property.value); |
| |
| this.listItemElement.removeChildren(); |
| this.listItemElement.appendChildren(this.nameElement, separatorElement, this.valueElement); |
| } |
| |
| _updatePropertyPath() { |
| if (this.nameElement.title) |
| return; |
| |
| var useDotNotation = /^(_|\$|[A-Z])(_|\$|[A-Z]|\d)*$/i; |
| var isInteger = /^[1-9]\d*$/; |
| var name = this.property.name; |
| var parentPath = this.parent.nameElement ? this.parent.nameElement.title : ''; |
| if (useDotNotation.test(name)) |
| this.nameElement.title = parentPath + '.' + name; |
| else if (isInteger.test(name)) |
| this.nameElement.title = parentPath + '[' + name + ']'; |
| else |
| this.nameElement.title = parentPath + '["' + name + '"]'; |
| } |
| |
| /** |
| * @param {!SDK.RemoteObjectProperty} property |
| * @param {!Event} event |
| */ |
| _contextMenuFired(property, event) { |
| var contextMenu = new UI.ContextMenu(event); |
| if (property.symbol) |
| contextMenu.appendApplicableItems(property.symbol); |
| if (property.value) |
| contextMenu.appendApplicableItems(property.value); |
| var copyPathHandler = InspectorFrontendHost.copyText.bind(InspectorFrontendHost, this.nameElement.title); |
| contextMenu.beforeShow(() => { |
| contextMenu.appendItem(Common.UIString.capitalize('Copy ^property ^path'), copyPathHandler); |
| }); |
| contextMenu.show(); |
| } |
| |
| _startEditing() { |
| if (this._prompt || !this.treeOutline._editable || this._readOnly) |
| return; |
| |
| this._editableDiv = this.listItemElement.createChild('span'); |
| |
| var 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>')); |
| var 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 Components.ObjectPropertyPrompt(); |
| |
| var proxyElement = |
| this._prompt.attachAndStartEditing(this._editableDiv, this._editingCommitted.bind(this, originalContent)); |
| this.listItemElement.getComponentSelection().setBaseAndExtent(this._editableDiv, 0, this._editableDiv, 1); |
| 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'); |
| } |
| |
| _editingCancelled() { |
| this.valueElement.classList.remove('hidden'); |
| this._editingEnded(); |
| } |
| |
| /** |
| * @param {string} originalContent |
| */ |
| _editingCommitted(originalContent) { |
| var userInput = this._prompt.text(); |
| if (userInput === originalContent) { |
| this._editingCancelled(); // nothing changed, so cancel |
| return; |
| } |
| |
| this._editingEnded(); |
| this._applyExpression(userInput); |
| } |
| |
| /** |
| * @param {string} originalContent |
| * @param {!Event} event |
| */ |
| _promptKeyDown(originalContent, event) { |
| if (isEnterKey(event)) { |
| event.consume(true); |
| this._editingCommitted(originalContent); |
| return; |
| } |
| if (event.key === 'Escape') { |
| event.consume(); |
| this._editingCancelled(); |
| return; |
| } |
| } |
| |
| /** |
| * @param {string} expression |
| */ |
| _applyExpression(expression) { |
| var property = SDK.RemoteObject.toCallArgument(this.property.symbol || this.property.name); |
| expression = expression.trim(); |
| if (expression) |
| this.property.parentObject.setPropertyValue(property, expression, callback.bind(this)); |
| else |
| this.property.parentObject.deleteProperty(property, callback.bind(this)); |
| |
| /** |
| * @param {?Protocol.Error} error |
| * @this {Components.ObjectPropertyTreeElement} |
| */ |
| function callback(error) { |
| 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. |
| var parent = this.parent; |
| parent.invalidateChildren(); |
| parent.onpopulate(); |
| } |
| } |
| } |
| |
| /** |
| * @param {?SDK.RemoteObject} result |
| * @param {boolean=} wasThrown |
| */ |
| _onInvokeGetterClick(result, wasThrown) { |
| if (!result) |
| return; |
| this.property.value = result; |
| this.property.wasThrown = 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); |
| } |
| }; |
| |
| |
| /** |
| * @unrestricted |
| */ |
| Components.ArrayGroupingTreeElement = class extends 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.selectable = false; |
| this._fromIndex = fromIndex; |
| this._toIndex = toIndex; |
| this._object = object; |
| this._readOnly = true; |
| this._propertyCount = propertyCount; |
| this._linkifier = linkifier; |
| } |
| |
| /** |
| * @param {!TreeElement} treeNode |
| * @param {!SDK.RemoteObject} object |
| * @param {number} fromIndex |
| * @param {number} toIndex |
| * @param {!Components.Linkifier=} linkifier |
| */ |
| static _populateArray(treeNode, object, fromIndex, toIndex, linkifier) { |
| Components.ArrayGroupingTreeElement._populateRanges(treeNode, object, fromIndex, toIndex, true, linkifier); |
| } |
| |
| /** |
| * @param {!TreeElement} treeNode |
| * @param {!SDK.RemoteObject} object |
| * @param {number} fromIndex |
| * @param {number} toIndex |
| * @param {boolean} topLevel |
| * @param {!Components.Linkifier=} linkifier |
| * @this {Components.ArrayGroupingTreeElement} |
| */ |
| static _populateRanges(treeNode, object, fromIndex, toIndex, topLevel, linkifier) { |
| object.callFunctionJSON( |
| packRanges, |
| [ |
| {value: fromIndex}, {value: toIndex}, {value: Components.ArrayGroupingTreeElement._bucketThreshold}, |
| {value: Components.ArrayGroupingTreeElement._sparseIterationThreshold}, |
| {value: Components.ArrayGroupingTreeElement._getOwnPropertyNamesThreshold} |
| ], |
| callback); |
| |
| /** |
| * 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) { |
| var ownPropertyNames = null; |
| var consecutiveRange = (toIndex - fromIndex >= sparseIterationThreshold) && ArrayBuffer.isView(this); |
| var skipGetOwnPropertyNames = consecutiveRange && (toIndex - fromIndex >= getOwnPropertyNamesThreshold); |
| |
| function* arrayIndexes(object) { |
| if (toIndex - fromIndex < sparseIterationThreshold) { |
| for (var i = fromIndex; i <= toIndex; ++i) { |
| if (i in object) |
| yield i; |
| } |
| } else { |
| ownPropertyNames = ownPropertyNames || Object.getOwnPropertyNames(object); |
| for (var i = 0; i < ownPropertyNames.length; ++i) { |
| var name = ownPropertyNames[i]; |
| var index = name >>> 0; |
| if (('' + index) === name && fromIndex <= index && index <= toIndex) |
| yield index; |
| } |
| } |
| } |
| |
| var count = 0; |
| if (consecutiveRange) { |
| count = toIndex - fromIndex + 1; |
| } else { |
| for (var i of arrayIndexes(this)) |
| ++count; |
| } |
| |
| var bucketSize = count; |
| if (count <= bucketThreshold) |
| bucketSize = count; |
| else |
| bucketSize = Math.pow(bucketThreshold, Math.ceil(Math.log(count) / Math.log(bucketThreshold)) - 1); |
| |
| var ranges = []; |
| if (consecutiveRange) { |
| for (var i = fromIndex; i <= toIndex; i += bucketSize) { |
| var groupStart = i; |
| var groupEnd = groupStart + bucketSize - 1; |
| if (groupEnd > toIndex) |
| groupEnd = toIndex; |
| ranges.push([groupStart, groupEnd, groupEnd - groupStart + 1]); |
| } |
| } else { |
| count = 0; |
| var groupStart = -1; |
| var groupEnd = 0; |
| for (var 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}; |
| } |
| |
| function callback(result) { |
| if (!result) |
| return; |
| var ranges = /** @type {!Array.<!Array.<number>>} */ (result.ranges); |
| if (ranges.length === 1) { |
| Components.ArrayGroupingTreeElement._populateAsFragment( |
| treeNode, object, ranges[0][0], ranges[0][1], linkifier); |
| } else { |
| for (var i = 0; i < ranges.length; ++i) { |
| var fromIndex = ranges[i][0]; |
| var toIndex = ranges[i][1]; |
| var count = ranges[i][2]; |
| if (fromIndex === toIndex) |
| Components.ArrayGroupingTreeElement._populateAsFragment(treeNode, object, fromIndex, toIndex, linkifier); |
| else |
| treeNode.appendChild( |
| new Components.ArrayGroupingTreeElement(object, fromIndex, toIndex, count, linkifier)); |
| } |
| } |
| if (topLevel) |
| Components.ArrayGroupingTreeElement._populateNonIndexProperties( |
| treeNode, object, result.skipGetOwnPropertyNames, linkifier); |
| } |
| } |
| |
| /** |
| * @param {!TreeElement} treeNode |
| * @param {!SDK.RemoteObject} object |
| * @param {number} fromIndex |
| * @param {number} toIndex |
| * @param {!Components.Linkifier=} linkifier |
| * @this {Components.ArrayGroupingTreeElement} |
| */ |
| static _populateAsFragment(treeNode, object, fromIndex, toIndex, linkifier) { |
| object.callFunction( |
| buildArrayFragment, |
| [ |
| {value: fromIndex}, {value: toIndex}, |
| {value: Components.ArrayGroupingTreeElement._sparseIterationThreshold} |
| ], |
| processArrayFragment.bind(this)); |
| |
| /** |
| * @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) { |
| var result = Object.create(null); |
| if (toIndex - fromIndex < sparseIterationThreshold) { |
| for (var i = fromIndex; i <= toIndex; ++i) { |
| if (i in this) |
| result[i] = this[i]; |
| } |
| } else { |
| var ownPropertyNames = Object.getOwnPropertyNames(this); |
| for (var i = 0; i < ownPropertyNames.length; ++i) { |
| var name = ownPropertyNames[i]; |
| var index = name >>> 0; |
| if (String(index) === name && fromIndex <= index && index <= toIndex) |
| result[index] = this[index]; |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * @param {?SDK.RemoteObject} arrayFragment |
| * @param {boolean=} wasThrown |
| * @this {Components.ArrayGroupingTreeElement} |
| */ |
| function processArrayFragment(arrayFragment, wasThrown) { |
| if (!arrayFragment || wasThrown) |
| return; |
| arrayFragment.getAllProperties(false, processProperties.bind(this)); |
| } |
| |
| /** @this {Components.ArrayGroupingTreeElement} */ |
| function processProperties(properties, internalProperties) { |
| if (!properties) |
| return; |
| |
| properties.sort(Components.ObjectPropertiesSection.CompareProperties); |
| for (var i = 0; i < properties.length; ++i) { |
| properties[i].parentObject = this._object; |
| var childTreeElement = new Components.ObjectPropertyTreeElement(properties[i], linkifier); |
| childTreeElement._readOnly = true; |
| treeNode.appendChild(childTreeElement); |
| } |
| } |
| } |
| |
| /** |
| * @param {!TreeElement} treeNode |
| * @param {!SDK.RemoteObject} object |
| * @param {boolean} skipGetOwnPropertyNames |
| * @param {!Components.Linkifier=} linkifier |
| * @this {Components.ArrayGroupingTreeElement} |
| */ |
| static _populateNonIndexProperties(treeNode, object, skipGetOwnPropertyNames, linkifier) { |
| object.callFunction(buildObjectFragment, [{value: skipGetOwnPropertyNames}], processObjectFragment.bind(this)); |
| |
| /** |
| * @param {boolean=} skipGetOwnPropertyNames |
| * @suppressReceiverCheck |
| * @this {Object} |
| */ |
| function buildObjectFragment(skipGetOwnPropertyNames) { |
| var result = {__proto__: this.__proto__}; |
| if (skipGetOwnPropertyNames) |
| return result; |
| var names = Object.getOwnPropertyNames(this); |
| for (var i = 0; i < names.length; ++i) { |
| var name = names[i]; |
| // Array index check according to the ES5-15.4. |
| if (String(name >>> 0) === name && name >>> 0 !== 0xffffffff) |
| continue; |
| var descriptor = Object.getOwnPropertyDescriptor(this, name); |
| if (descriptor) |
| Object.defineProperty(result, name, descriptor); |
| } |
| return result; |
| } |
| |
| /** |
| * @param {?SDK.RemoteObject} arrayFragment |
| * @param {boolean=} wasThrown |
| * @this {Components.ArrayGroupingTreeElement} |
| */ |
| function processObjectFragment(arrayFragment, wasThrown) { |
| if (!arrayFragment || wasThrown) |
| return; |
| arrayFragment.getOwnProperties(processProperties.bind(this)); |
| } |
| |
| /** |
| * @param {?Array.<!SDK.RemoteObjectProperty>} properties |
| * @param {?Array.<!SDK.RemoteObjectProperty>=} internalProperties |
| * @this {Components.ArrayGroupingTreeElement} |
| */ |
| function processProperties(properties, internalProperties) { |
| if (!properties) |
| return; |
| properties.sort(Components.ObjectPropertiesSection.CompareProperties); |
| for (var i = 0; i < properties.length; ++i) { |
| properties[i].parentObject = this._object; |
| var childTreeElement = new Components.ObjectPropertyTreeElement(properties[i], linkifier); |
| childTreeElement._readOnly = true; |
| treeNode.appendChild(childTreeElement); |
| } |
| } |
| } |
| |
| /** |
| * @override |
| */ |
| onpopulate() { |
| if (this._propertyCount >= Components.ArrayGroupingTreeElement._bucketThreshold) { |
| Components.ArrayGroupingTreeElement._populateRanges( |
| this, this._object, this._fromIndex, this._toIndex, false, this._linkifier); |
| return; |
| } |
| Components.ArrayGroupingTreeElement._populateAsFragment( |
| this, this._object, this._fromIndex, this._toIndex, this._linkifier); |
| } |
| |
| /** |
| * @override |
| */ |
| onattach() { |
| this.listItemElement.classList.add('object-properties-section-name'); |
| } |
| }; |
| |
| Components.ArrayGroupingTreeElement._bucketThreshold = 100; |
| Components.ArrayGroupingTreeElement._sparseIterationThreshold = 250000; |
| Components.ArrayGroupingTreeElement._getOwnPropertyNamesThreshold = 500000; |
| |
| |
| /** |
| * @unrestricted |
| */ |
| Components.ObjectPropertyPrompt = class extends UI.TextPrompt { |
| constructor() { |
| super(); |
| this.initialize(Components.JavaScriptAutocomplete.completionsForTextPromptInCurrentContext); |
| this.setSuggestBoxEnabled(true); |
| } |
| }; |
| |
| |
| Components.ObjectPropertiesSection._functionPrefixSource = /^(?:async\s)?function\*?\s/; |
| |
| |
| /** |
| * @unrestricted |
| */ |
| Components.ObjectPropertiesSectionExpandController = class { |
| constructor() { |
| /** @type {!Set.<string>} */ |
| this._expandedProperties = new Set(); |
| } |
| |
| /** |
| * @param {string} id |
| * @param {!Components.ObjectPropertiesSection} section |
| */ |
| watchSection(id, section) { |
| section.addEventListener(TreeOutline.Events.ElementAttached, this._elementAttached, this); |
| section.addEventListener(TreeOutline.Events.ElementExpanded, this._elementExpanded, this); |
| section.addEventListener(TreeOutline.Events.ElementCollapsed, this._elementCollapsed, this); |
| section[Components.ObjectPropertiesSectionExpandController._treeOutlineId] = id; |
| |
| if (this._expandedProperties.has(id)) |
| section.expand(); |
| } |
| |
| /** |
| * @param {string} id |
| */ |
| stopWatchSectionsWithId(id) { |
| for (var property of this._expandedProperties) { |
| if (property.startsWith(id + ':')) |
| this._expandedProperties.delete(property); |
| } |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _elementAttached(event) { |
| var element = /** @type {!TreeElement} */ (event.data); |
| if (element.isExpandable() && this._expandedProperties.has(this._propertyPath(element))) |
| element.expand(); |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _elementExpanded(event) { |
| var element = /** @type {!TreeElement} */ (event.data); |
| this._expandedProperties.add(this._propertyPath(element)); |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _elementCollapsed(event) { |
| var element = /** @type {!TreeElement} */ (event.data); |
| this._expandedProperties.delete(this._propertyPath(element)); |
| } |
| |
| /** |
| * @param {!TreeElement} treeElement |
| * @return {string} |
| */ |
| _propertyPath(treeElement) { |
| var cachedPropertyPath = treeElement[Components.ObjectPropertiesSectionExpandController._cachedPathSymbol]; |
| if (cachedPropertyPath) |
| return cachedPropertyPath; |
| |
| var current = treeElement; |
| var rootElement = treeElement.treeOutline.objectTreeElement(); |
| |
| var result; |
| |
| while (current !== rootElement) { |
| var currentName = ''; |
| if (current.property) |
| currentName = current.property.name; |
| else |
| currentName = typeof current.title === 'string' ? current.title : current.title.textContent; |
| |
| result = currentName + (result ? '.' + result : ''); |
| current = current.parent; |
| } |
| var treeOutlineId = treeElement.treeOutline[Components.ObjectPropertiesSectionExpandController._treeOutlineId]; |
| result = treeOutlineId + (result ? ':' + result : ''); |
| treeElement[Components.ObjectPropertiesSectionExpandController._cachedPathSymbol] = result; |
| return result; |
| } |
| }; |
| |
| Components.ObjectPropertiesSectionExpandController._cachedPathSymbol = Symbol('cachedPath'); |
| Components.ObjectPropertiesSectionExpandController._treeOutlineId = Symbol('treeOutlineId'); |