| // Copyright 2014 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. | |
| /** | |
| * @unrestricted | |
| */ | |
| class CustomPreviewSection { | |
| /** | |
| * @param {!SDK.RemoteObject} object | |
| */ | |
| constructor(object) { | |
| this._sectionElement = createElementWithClass('span', 'custom-expandable-section'); | |
| this._object = object; | |
| this._expanded = false; | |
| this._cachedContent = null; | |
| const customPreview = object.customPreview(); | |
| let headerJSON; | |
| try { | |
| headerJSON = JSON.parse(customPreview.header); | |
| } catch (e) { | |
| Common.console.error('Broken formatter: header is invalid json ' + e); | |
| return; | |
| } | |
| this._header = this._renderJSONMLTag(headerJSON); | |
| if (this._header.nodeType === Node.TEXT_NODE) { | |
| Common.console.error('Broken formatter: header should be an element node.'); | |
| return; | |
| } | |
| if (customPreview.hasBody || customPreview.bodyGetterId) { | |
| this._header.classList.add('custom-expandable-section-header'); | |
| this._header.addEventListener('click', this._onClick.bind(this), false); | |
| this._expandIcon = UI.Icon.create('smallicon-triangle-right', 'custom-expand-icon'); | |
| this._header.insertBefore(this._expandIcon, this._header.firstChild); | |
| } | |
| this._sectionElement.appendChild(this._header); | |
| } | |
| /** | |
| * @return {!Element} | |
| */ | |
| element() { | |
| return this._sectionElement; | |
| } | |
| /** | |
| * @param {*} jsonML | |
| * @return {!Node} | |
| */ | |
| _renderJSONMLTag(jsonML) { | |
| if (!Array.isArray(jsonML)) { | |
| return createTextNode(jsonML + ''); | |
| } | |
| const array = /** @type {!Array.<*>} */ (jsonML); | |
| return array[0] === 'object' ? this._layoutObjectTag(array) : this._renderElement(array); | |
| } | |
| /** | |
| * | |
| * @param {!Array.<*>} object | |
| * @return {!Node} | |
| */ | |
| _renderElement(object) { | |
| const tagName = object.shift(); | |
| if (!CustomPreviewSection._tagsWhiteList.has(tagName)) { | |
| Common.console.error('Broken formatter: element ' + tagName + ' is not allowed!'); | |
| return createElement('span'); | |
| } | |
| const element = createElement(/** @type {string} */ (tagName)); | |
| if ((typeof object[0] === 'object') && !Array.isArray(object[0])) { | |
| const attributes = object.shift(); | |
| for (const key in attributes) { | |
| const value = attributes[key]; | |
| if ((key !== 'style') || (typeof value !== 'string')) { | |
| continue; | |
| } | |
| element.setAttribute(key, value); | |
| } | |
| } | |
| this._appendJsonMLTags(element, object); | |
| return element; | |
| } | |
| /** | |
| * @param {!Array.<*>} objectTag | |
| * @return {!Node} | |
| */ | |
| _layoutObjectTag(objectTag) { | |
| objectTag.shift(); | |
| const attributes = objectTag.shift(); | |
| const remoteObject = this._object.runtimeModel().createRemoteObject( | |
| /** @type {!Protocol.Runtime.RemoteObject} */ (attributes)); | |
| if (remoteObject.customPreview()) { | |
| return (new CustomPreviewSection(remoteObject)).element(); | |
| } | |
| const sectionElement = ObjectUI.ObjectPropertiesSection.defaultObjectPresentation(remoteObject); | |
| sectionElement.classList.toggle('custom-expandable-section-standard-section', remoteObject.hasChildren); | |
| return sectionElement; | |
| } | |
| /** | |
| * @param {!Node} parentElement | |
| * @param {!Array.<*>} jsonMLTags | |
| */ | |
| _appendJsonMLTags(parentElement, jsonMLTags) { | |
| for (let i = 0; i < jsonMLTags.length; ++i) { | |
| parentElement.appendChild(this._renderJSONMLTag(jsonMLTags[i])); | |
| } | |
| } | |
| /** | |
| * @param {!Event} event | |
| */ | |
| _onClick(event) { | |
| event.consume(true); | |
| if (this._cachedContent) { | |
| this._toggleExpand(); | |
| } else { | |
| this._loadBody(); | |
| } | |
| } | |
| _toggleExpand() { | |
| this._expanded = !this._expanded; | |
| this._header.classList.toggle('expanded', this._expanded); | |
| this._cachedContent.classList.toggle('hidden', !this._expanded); | |
| if (this._expanded) { | |
| this._expandIcon.setIconType('smallicon-triangle-down'); | |
| } else { | |
| this._expandIcon.setIconType('smallicon-triangle-right'); | |
| } | |
| } | |
| _loadBody() { | |
| /** | |
| * @suppressReceiverCheck | |
| * @suppressGlobalPropertiesCheck | |
| * @suppress {undefinedVars} | |
| * @this {Object} | |
| * @param {function(!Object, *):*} bindRemoteObject | |
| * @param {*=} formatter | |
| * @param {*=} config | |
| */ | |
| function load(bindRemoteObject, formatter, config) { | |
| /** | |
| * @param {*} jsonMLObject | |
| * @throws {string} error message | |
| */ | |
| function substituteObjectTagsInCustomPreview(jsonMLObject) { | |
| if (!jsonMLObject || (typeof jsonMLObject !== 'object') || (typeof jsonMLObject.splice !== 'function')) { | |
| return; | |
| } | |
| const obj = jsonMLObject.length; | |
| if (!(typeof obj === 'number' && obj >>> 0 === obj && (obj > 0 || 1 / obj > 0))) { | |
| return; | |
| } | |
| let startIndex = 1; | |
| if (jsonMLObject[0] === 'object') { | |
| const attributes = jsonMLObject[1]; | |
| const originObject = attributes['object']; | |
| const config = attributes['config']; | |
| if (typeof originObject === 'undefined') { | |
| throw 'Illegal format: obligatory attribute "object" isn\'t specified'; | |
| } | |
| jsonMLObject[1] = bindRemoteObject(originObject, config); | |
| startIndex = 2; | |
| } | |
| for (let i = startIndex; i < jsonMLObject.length; ++i) { | |
| substituteObjectTagsInCustomPreview(jsonMLObject[i]); | |
| } | |
| } | |
| try { | |
| const body = formatter.body(this, config); | |
| substituteObjectTagsInCustomPreview(body); | |
| return body; | |
| } catch (e) { | |
| console.error('Custom Formatter Failed: ' + e); | |
| return null; | |
| } | |
| } | |
| const customPreview = this._object.customPreview(); | |
| if (customPreview.bindRemoteObjectFunctionId && customPreview.formatterObjectId) { | |
| // Support for V8 version < 7.3. | |
| const args = [{objectId: customPreview.bindRemoteObjectFunctionId}, {objectId: customPreview.formatterObjectId}]; | |
| if (customPreview.configObjectId) { | |
| args.push({objectId: customPreview.configObjectId}); | |
| } | |
| this._object.callFunctionJSON(load, args).then(onBodyLoaded.bind(this)); | |
| } else if (customPreview.bodyGetterId) { | |
| this._object.callFunctionJSON(bodyGetter => bodyGetter(), [{objectId: customPreview.bodyGetterId}]) | |
| .then(onBodyLoaded.bind(this)); | |
| } | |
| /** | |
| * @param {*} bodyJsonML | |
| * @this {CustomPreviewSection} | |
| */ | |
| function onBodyLoaded(bodyJsonML) { | |
| if (!bodyJsonML) { | |
| return; | |
| } | |
| this._cachedContent = this._renderJSONMLTag(bodyJsonML); | |
| this._sectionElement.appendChild(this._cachedContent); | |
| this._toggleExpand(); | |
| } | |
| } | |
| } | |
| /** | |
| * @unrestricted | |
| */ | |
| export default class CustomPreviewComponent { | |
| /** | |
| * @param {!SDK.RemoteObject} object | |
| */ | |
| constructor(object) { | |
| this._object = object; | |
| this._customPreviewSection = new CustomPreviewSection(object); | |
| this.element = createElementWithClass('span', 'source-code'); | |
| const shadowRoot = UI.createShadowRootWithCoreStyles(this.element, 'object_ui/customPreviewComponent.css'); | |
| this.element.addEventListener('contextmenu', this._contextMenuEventFired.bind(this), false); | |
| shadowRoot.appendChild(this._customPreviewSection.element()); | |
| } | |
| expandIfPossible() { | |
| if ((this._object.customPreview().hasBody || this._object.customPreview().bodyGetterId) && | |
| this._customPreviewSection) { | |
| this._customPreviewSection._loadBody(); | |
| } | |
| } | |
| /** | |
| * @param {!Event} event | |
| */ | |
| _contextMenuEventFired(event) { | |
| const contextMenu = new UI.ContextMenu(event); | |
| if (this._customPreviewSection) { | |
| contextMenu.revealSection().appendItem( | |
| Common.UIString('Show as JavaScript object'), this._disassemble.bind(this)); | |
| } | |
| contextMenu.appendApplicableItems(this._object); | |
| contextMenu.show(); | |
| } | |
| _disassemble() { | |
| this.element.shadowRoot.textContent = ''; | |
| this._customPreviewSection = null; | |
| this.element.shadowRoot.appendChild(ObjectUI.ObjectPropertiesSection.defaultObjectPresentation(this._object)); | |
| } | |
| } | |
| CustomPreviewSection._tagsWhiteList = new Set(['span', 'div', 'ol', 'li', 'table', 'tr', 'td']); | |
| /* Legacy exported object */ | |
| self.ObjectUI = self.ObjectUI || {}; | |
| /* Legacy exported object */ | |
| ObjectUI = ObjectUI || {}; | |
| /** @constructor */ | |
| ObjectUI.CustomPreviewComponent = CustomPreviewComponent; |