/* * Copyright (C) 2013, 2015 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ WI.ResourceClusterContentView = class ResourceClusterContentView extends WI.ClusterContentView { constructor(resource) { super(resource); this._resource = resource; this._resource.addEventListener(WI.Resource.Event.TypeDidChange, this._resourceTypeDidChange, this); this._resource.addEventListener(WI.Resource.Event.LoadingDidFinish, this._resourceLoadingDidFinish, this); this._responsePathComponent = this._createPathComponent({ displayName: WI.UIString("Response"), identifier: ResourceClusterContentView.Identifier.Response, styleClassNames: ["response-icon"], }); if (this._canShowRequestContentView()) { this._requestPathComponent = this._createPathComponent({ displayName: WI.UIString("Request"), identifier: ResourceClusterContentView.Identifier.Request, styleClassNames: ["request-icon"], nextSibling: this._responsePathComponent, }); this._tryEnableCustomRequestContentViews(); } // FIXME: Since a custom response content view may only become available after a response is received // we need to figure out a way to restore / prefer the custom content view. For example if users // always want to prefer the JSON view to the normal Response text view. this._currentContentViewSetting = new WI.Setting("resource-current-view-" + this._resource.url.hash, ResourceClusterContentView.Identifier.Response); this._tryEnableCustomResponseContentViews(); } // Public get resource() { return this._resource; } get selectionPathComponents() { let currentContentView = this._contentViewContainer.currentContentView; if (!currentContentView) return []; if (!this._canShowRequestContentView() && !this._canShowCustomRequestContentView() && !this._canShowCustomResponseContentView()) return currentContentView.selectionPathComponents; // Append the current view's path components to the path component representing the current view. let components = [this._pathComponentForContentView(currentContentView)]; return components.concat(currentContentView.selectionPathComponents); } attached() { super.attached(); if (this._shownInitialContent) return; this._showContentViewForIdentifier(this._currentContentViewSetting.value); } closed() { super.closed(); this._shownInitialContent = false; } restoreFromCookie(cookie) { let contentView = this._showContentViewForIdentifier(cookie[WI.ResourceClusterContentView.ContentViewIdentifierCookieKey]); if (contentView.revealPosition) { let textRangeToSelect = null; if (!isNaN(cookie.startLine) && !isNaN(cookie.startColumn) && !isNaN(cookie.endLine) && !isNaN(cookie.endColumn)) textRangeToSelect = new WI.TextRange(cookie.startLine, cookie.startColumn, cookie.endLine, cookie.endColumn); let position = null; if (!isNaN(cookie.lineNumber) && !isNaN(cookie.columnNumber)) position = new WI.SourceCodePosition(cookie.lineNumber, cookie.columnNumber); else if (textRangeToSelect) position = textRangeToSelect.startPosition(); let scrollOffset = null; if (!isNaN(cookie.scrollOffsetX) && !isNaN(cookie.scrollOffsetY)) scrollOffset = new WI.Point(cookie.scrollOffsetX, cookie.scrollOffsetY); if (position) contentView.revealPosition(position, {...cookie, textRangeToSelect, scrollOffset}); } } showRequest() { this._shownInitialContent = true; return this._showContentViewForIdentifier(ResourceClusterContentView.Identifier.Request); } showResponse() { this._shownInitialContent = true; return this._showContentViewForIdentifier(ResourceClusterContentView.Identifier.Response); } // Private get requestContentView() { if (!this._canShowRequestContentView()) return null; if (this._requestContentView) return this._requestContentView; this._requestContentView = new WI.TextContentView(this._resource.requestData || "", this._resource.requestDataContentType); return this._requestContentView; } get responseContentView() { if (this._responseContentView) return this._responseContentView; this._responseContentView = this._contentViewForResourceType(this._resource.type); if (this._responseContentView) return this._responseContentView; let typeFromMIMEType = WI.Resource.typeFromMIMEType(this._resource.mimeType); this._responseContentView = this._contentViewForResourceType(typeFromMIMEType); if (this._responseContentView) return this._responseContentView; if (WI.shouldTreatMIMETypeAsText(this._resource.mimeType)) { this._responseContentView = new WI.TextResourceContentView(this._resource); return this._responseContentView; } this._responseContentView = new WI.GenericResourceContentView(this._resource); return this._responseContentView; } get customRequestDOMContentView() { if (!this._customRequestDOMContentView && this._customRequestDOMContentViewInitializer) this._customRequestDOMContentView = this._customRequestDOMContentViewInitializer(); return this._customRequestDOMContentView; } get customRequestJSONContentView() { if (!this._customRequestJSONContentView && this._customRequestJSONContentViewInitializer) this._customRequestJSONContentView = this._customRequestJSONContentViewInitializer(); return this._customRequestJSONContentView; } get customResponseDOMContentView() { if (!this._customResponseDOMContentView && this._customResponseDOMContentViewInitializer) this._customResponseDOMContentView = this._customResponseDOMContentViewInitializer(); return this._customResponseDOMContentView; } get customResponseJSONContentView() { if (!this._customResponseJSONContentView && this._customResponseJSONContentViewInitializer) this._customResponseJSONContentView = this._customResponseJSONContentViewInitializer(); return this._customResponseJSONContentView; } get customResponseTextContentView() { if (!this._customResponseTextContentView && this._customResponseTextContentViewInitializer) this._customResponseTextContentView = this._customResponseTextContentViewInitializer(); return this._customResponseTextContentView; } _createPathComponent({displayName, styleClassNames, identifier, previousSibling, nextSibling}) { const textOnly = false; const showSelectorArrows = true; let pathComponent = new WI.HierarchicalPathComponent(displayName, styleClassNames, identifier, textOnly, showSelectorArrows); pathComponent.comparisonData = this._resource; if (previousSibling) { previousSibling.nextSibling = pathComponent; pathComponent.previousSibling = previousSibling; } if (nextSibling) { nextSibling.previousSibling = pathComponent; pathComponent.nextSibling = nextSibling; } pathComponent.addEventListener(WI.HierarchicalPathComponent.Event.SiblingWasSelected, this._pathComponentSelected, this); return pathComponent; } _canShowRequestContentView() { let requestData = this._resource.requestData; if (!requestData) return false; if (this._resource.hasRequestFormParameters()) return false; return true; } _canShowCustomRequestContentView() { return !!(this._customRequestDOMContentViewInitializer || this._customRequestJSONContentViewInitializer); } _canShowCustomResponseContentView() { return !!(this._customResponseDOMContentViewInitializer || this._customResponseJSONContentViewInitializer || this._customResponseTextContentViewInitializer); } _contentViewForResourceType(type) { switch (type) { case WI.Resource.Type.Document: case WI.Resource.Type.Script: case WI.Resource.Type.StyleSheet: return new WI.TextResourceContentView(this._resource); case WI.Resource.Type.Image: return new WI.ImageResourceContentView(this._resource); case WI.Resource.Type.Font: return new WI.FontResourceContentView(this._resource); case WI.Resource.Type.WebSocket: return new WI.WebSocketContentView(this._resource); default: return null; } } _pathComponentForContentView(contentView) { switch (contentView) { case this._requestContentView: return this._requestPathComponent; case this._customRequestDOMContentView: return this._customRequestDOMPathComponent; case this._customRequestJSONContentView: return this._customRequestJSONPathComponent; case this._responseContentView: return this._responsePathComponent; case this._customResponseDOMContentView: return this._customResponseDOMPathComponent; case this._customResponseJSONContentView: return this._customResponseJSONPathComponent; case this._customResponseTextContentView: return this._customResponseTextPathComponent; } console.error("Unknown contentView", contentView); return null; } _identifierForContentView(contentView) { console.assert(contentView); switch (contentView) { case this._requestContentView: return ResourceClusterContentView.Identifier.Request; case this._customRequestDOMContentView: return ResourceClusterContentView.Identifier.RequestDOM; case this._customRequestJSONContentView: return ResourceClusterContentView.Identifier.RequestJSON; case this._responseContentView: return ResourceClusterContentView.Identifier.Response; case this._customResponseDOMContentView: return ResourceClusterContentView.Identifier.ResponseDOM; case this._customResponseJSONContentView: return ResourceClusterContentView.Identifier.ResponseJSON; case this._customResponseTextContentView: return ResourceClusterContentView.Identifier.ResponseText; } console.error("Unknown contentView", contentView); return null; } _showContentViewForIdentifier(identifier) { let contentViewToShow = null; // This is expected to fall through all the way to the `default`. switch (identifier) { case ResourceClusterContentView.Identifier.RequestDOM: contentViewToShow = this.customRequestDOMContentView; if (contentViewToShow) break; // fallthrough case ResourceClusterContentView.Identifier.RequestJSON: contentViewToShow = this.customRequestJSONContentView; if (contentViewToShow) break; // fallthrough case ResourceClusterContentView.Identifier.Request: contentViewToShow = this.requestContentView; if (contentViewToShow) break; // fallthrough case ResourceClusterContentView.Identifier.ResponseDOM: contentViewToShow = this.customResponseDOMContentView; if (contentViewToShow) break; // fallthrough case ResourceClusterContentView.Identifier.ResponseJSON: contentViewToShow = this.customResponseJSONContentView; if (contentViewToShow) break; // fallthrough case ResourceClusterContentView.Identifier.ResponseText: contentViewToShow = this.customResponseTextContentView; if (contentViewToShow) break; // fallthrough case ResourceClusterContentView.Identifier.Response: default: contentViewToShow = this.responseContentView; break; } console.assert(contentViewToShow); this._currentContentViewSetting.value = this._identifierForContentView(contentViewToShow); return this.contentViewContainer.showContentView(contentViewToShow); } _pathComponentSelected(event) { this._showContentViewForIdentifier(event.data.pathComponent.representedObject); } _resourceTypeDidChange(event) { // Since resource views are based on the type, we need to make a new content view and tell the container to replace this // content view with the new one. Make a new ResourceContentView which will use the new resource type to make the correct // concrete ResourceContentView subclass. let currentResponseContentView = this._responseContentView; if (!currentResponseContentView) return; this._responseContentView = null; this.contentViewContainer.replaceContentView(currentResponseContentView, this.responseContentView); } _resourceLoadingDidFinish(event) { this._tryEnableCustomResponseContentViews(); } _canUseJSONContentViewForContent(content) { return typeof content === "string" && content.isJSON((json) => json && (typeof json === "object" || Array.isArray(json))); } _canUseDOMContentViewForContent(content, mimeType) { if (typeof content !== "string") return false; switch (mimeType) { case "text/html": return true; case "text/xml": case "application/xml": case "application/xhtml+xml": case "image/svg+xml": try { let dom = (new DOMParser).parseFromString(content, mimeType); return !dom.querySelector("parsererror"); } catch { } return false; } return false; } _normalizeMIMETypeForDOM(mimeType) { mimeType = parseMIMEType(mimeType).type; if (!mimeType) return mimeType; if (mimeType.endsWith("/html") || mimeType.endsWith("+html")) return "text/html"; if (mimeType.endsWith("/xml") || mimeType.endsWith("+xml")) { if (mimeType !== "application/xhtml+xml" && mimeType !== "image/svg+xml") return "application/xml"; } if (mimeType.endsWith("/xhtml") || mimeType.endsWith("+xhtml")) return "application/xhtml+xml"; if (mimeType.endsWith("/svg") || mimeType.endsWith("+svg")) return "image/svg+xml"; return mimeType; } _tryEnableCustomRequestContentViews() { let content = this._resource.requestData; if (this._canUseJSONContentViewForContent(content)) { this._customRequestJSONContentViewInitializer = () => new WI.LocalJSONContentView(content, this._resource); this._customRequestJSONPathComponent = this._createPathComponent({ displayName: WI.UIString("Request (Object Tree)"), styleClassNames: ["object-icon"], identifier: ResourceClusterContentView.Identifier.RequestJSON, previousSibling: this._requestPathComponent, nextSibling: this._responsePathComponent, }); this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange); return; } let mimeType = this._normalizeMIMETypeForDOM(this._resource.requestDataContentType); if (this._canUseDOMContentViewForContent(content, mimeType)) { this._customRequestDOMContentViewInitializer = () => new WI.LocalDOMContentView(content, mimeType, this._resource); this._customRequestDOMPathComponent = this._createPathComponent({ displayName: WI.UIString("Request (DOM Tree)"), styleClassNames: ["dom-document-icon"], identifier: ResourceClusterContentView.Identifier.RequestDOM, previousSibling: this._requestPathComponent, nextSibling: this._responsePathComponent, }); this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange); return; } } _tryEnableCustomResponseContentViews() { if (!this._resource.hasResponse()) return; // WebSocket resources already use a "custom" response content view. if (this._resource instanceof WI.WebSocketResource) return; this._resource.requestContent() .then(({error, content}) => { if (error || typeof content !== "string") return; if (this._canUseJSONContentViewForContent(content)) { this._customResponseJSONContentViewInitializer = () => new WI.LocalJSONContentView(content, this._resource); this._customResponseJSONPathComponent = this._createPathComponent({ displayName: WI.UIString("Response (Object Tree)"), styleClassNames: ["object-icon"], identifier: ResourceClusterContentView.Identifier.ResponseJSON, previousSibling: this._responsePathComponent, }); this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange); return; } let mimeType = this._normalizeMIMETypeForDOM(this._resource.mimeType); if (this._canUseDOMContentViewForContent(content, mimeType)) { if (mimeType === "image/svg+xml") { this._customResponseTextContentViewInitializer = () => new WI.TextContentView(content, mimeType, this._resource); this._customResponseTextPathComponent = this._createPathComponent({ displayName: WI.UIString("Response (Text)"), styleClassNames: ["source-icon"], identifier: ResourceClusterContentView.Identifier.ResponseText, previousSibling: this._responsePathComponent, }); } this._customResponseDOMContentViewInitializer = () => new WI.LocalDOMContentView(content, mimeType, this._resource); this._customResponseDOMPathComponent = this._createPathComponent({ displayName: WI.UIString("Response (DOM Tree)"), styleClassNames: ["dom-document-icon"], identifier: ResourceClusterContentView.Identifier.ResponseDOM, previousSibling: this._customResponseTextPathComponent || this._responsePathComponent, }); this.dispatchEventToListeners(WI.ContentView.Event.SelectionPathComponentsDidChange); return; } }); } }; WI.ResourceClusterContentView.ContentViewIdentifierCookieKey = "resource-cluster-content-view-identifier"; WI.ResourceClusterContentView.Identifier = { Request: "request", RequestDOM: "request-dom", RequestJSON: "request-json", Response: "response", ResponseDOM: "response-dom", ResponseJSON: "response-json", ResponseText: "response-text", };