| /* |
| * Copyright (C) 2013 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. |
| */ |
| |
| WebInspector.FrameTreeElement = function(frame, representedObject) |
| { |
| console.assert(frame instanceof WebInspector.Frame); |
| |
| WebInspector.ResourceTreeElement.call(this, frame.mainResource, representedObject || frame); |
| |
| this._frame = frame; |
| this._newChildQueue = []; |
| |
| this._updateExpandedSetting(); |
| |
| frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this); |
| frame.addEventListener(WebInspector.Frame.Event.ResourceWasAdded, this._resourceWasAdded, this); |
| frame.addEventListener(WebInspector.Frame.Event.ResourceWasRemoved, this._resourceWasRemoved, this); |
| frame.addEventListener(WebInspector.Frame.Event.ChildFrameWasAdded, this._childFrameWasAdded, this); |
| frame.addEventListener(WebInspector.Frame.Event.ChildFrameWasRemoved, this._childFrameWasRemoved, this); |
| |
| frame.domTree.addEventListener(WebInspector.DOMTree.Event.ContentFlowWasAdded, this._childContentFlowWasAdded, this); |
| frame.domTree.addEventListener(WebInspector.DOMTree.Event.ContentFlowWasRemoved, this._childContentFlowWasRemoved, this); |
| frame.domTree.addEventListener(WebInspector.DOMTree.Event.RootDOMNodeInvalidated, this._rootDOMNodeInvalidated, this); |
| |
| if (this._frame.isMainFrame()) { |
| this._downloadingPage = false; |
| WebInspector.notifications.addEventListener(WebInspector.Notification.PageArchiveStarted, this._pageArchiveStarted, this); |
| WebInspector.notifications.addEventListener(WebInspector.Notification.PageArchiveEnded, this._pageArchiveEnded, this); |
| } |
| |
| this._updateParentStatus(); |
| this.shouldRefreshChildren = true; |
| }; |
| |
| WebInspector.FrameTreeElement.MediumChildCountThreshold = 5; |
| WebInspector.FrameTreeElement.LargeChildCountThreshold = 15; |
| WebInspector.FrameTreeElement.NumberOfMediumCategoriesThreshold = 2; |
| WebInspector.FrameTreeElement.NewChildQueueUpdateInterval = 500; |
| |
| WebInspector.FrameTreeElement.prototype = { |
| constructor: WebInspector.FrameTreeElement, |
| |
| // Public |
| |
| get frame() |
| { |
| return this._frame; |
| }, |
| |
| descendantResourceTreeElementTypeDidChange: function(resourceTreeElement, oldType) |
| { |
| // Called by descendant ResourceTreeElements. |
| |
| // Add the tree element again, which will move it to the new location |
| // based on sorting and possible folder changes. |
| this._addTreeElement(resourceTreeElement); |
| }, |
| |
| descendantResourceTreeElementMainTitleDidChange: function(resourceTreeElement, oldMainTitle) |
| { |
| // Called by descendant ResourceTreeElements. |
| |
| // Add the tree element again, which will move it to the new location |
| // based on sorting and possible folder changes. |
| this._addTreeElement(resourceTreeElement); |
| }, |
| |
| // Overrides from SourceCodeTreeElement. |
| |
| updateSourceMapResources: function() |
| { |
| // Frames handle their own SourceMapResources. |
| |
| if (!this.treeOutline || !this.treeOutline.includeSourceMapResourceChildren) |
| return; |
| |
| if (!this._frame) |
| return; |
| |
| this._updateParentStatus(); |
| |
| if (this.resource && this.resource.sourceMaps.length) |
| this.shouldRefreshChildren = true; |
| }, |
| |
| onattach: function() |
| { |
| // Frames handle their own SourceMapResources. |
| |
| WebInspector.GeneralTreeElement.prototype.onattach.call(this); |
| }, |
| |
| // Called from ResourceTreeElement. |
| |
| updateStatusForMainFrame: function() |
| { |
| function loadedImages() |
| { |
| if (!this._reloadButton || !this._downloadButton) |
| return; |
| |
| var fragment = document.createDocumentFragment("div"); |
| fragment.appendChild(this._downloadButton.element); |
| fragment.appendChild(this._reloadButton.element); |
| this.status = fragment; |
| |
| delete this._loadingMainFrameButtons; |
| } |
| |
| if (this._reloadButton && this._downloadButton) { |
| loadedImages.call(this); |
| return; |
| } |
| |
| if (!this._loadingMainFrameButtons) { |
| this._loadingMainFrameButtons = true; |
| |
| var tooltip = WebInspector.UIString("Reload page (%s)\nReload ignoring cache (%s)").format(WebInspector._reloadPageKeyboardShortcut.displayName, WebInspector._reloadPageIgnoringCacheKeyboardShortcut.displayName); |
| wrappedSVGDocument("Images/Reload.svg", null, tooltip, function(element) { |
| this._reloadButton = new WebInspector.TreeElementStatusButton(element); |
| this._reloadButton.addEventListener(WebInspector.TreeElementStatusButton.Event.Clicked, this._reloadPageClicked, this); |
| loadedImages.call(this); |
| }.bind(this)); |
| |
| wrappedSVGDocument("Images/DownloadArrow.svg", null, WebInspector.UIString("Download Web Archive"), function(element) { |
| this._downloadButton = new WebInspector.TreeElementStatusButton(element); |
| this._downloadButton.addEventListener(WebInspector.TreeElementStatusButton.Event.Clicked, this._downloadButtonClicked, this); |
| this._updateDownloadButton(); |
| loadedImages.call(this); |
| }.bind(this)); |
| } |
| }, |
| |
| // Overrides from TreeElement (Private). |
| |
| onpopulate: function() |
| { |
| if (this.children.length && !this.shouldRefreshChildren) |
| return; |
| |
| this.shouldRefreshChildren = false; |
| |
| this.removeChildren(); |
| this._clearNewChildQueue(); |
| |
| if (this._shouldGroupIntoFolders() && !this._groupedIntoFolders) |
| this._groupedIntoFolders = true; |
| |
| for (var i = 0; i < this._frame.childFrames.length; ++i) |
| this._addTreeElementForRepresentedObject(this._frame.childFrames[i]); |
| |
| for (var i = 0; i < this._frame.resources.length; ++i) |
| this._addTreeElementForRepresentedObject(this._frame.resources[i]); |
| |
| var sourceMaps = this.resource && this.resource.sourceMaps; |
| for (var i = 0; i < sourceMaps.length; ++i) { |
| var sourceMap = sourceMaps[i]; |
| for (var j = 0; j < sourceMap.resources.length; ++j) |
| this._addTreeElementForRepresentedObject(sourceMap.resources[j]); |
| } |
| |
| var flowMap = this._frame.domTree.flowMap; |
| for (var flowKey in flowMap) |
| this._addTreeElementForRepresentedObject(flowMap[flowKey]); |
| }, |
| |
| onexpand: function() |
| { |
| this._expandedSetting.value = true; |
| this._frame.domTree.requestContentFlowList(); |
| }, |
| |
| oncollapse: function() |
| { |
| // Only store the setting if we have children, since setting hasChildren to false will cause a collapse, |
| // and we only care about user triggered collapses. |
| if (this.hasChildren) |
| this._expandedSetting.value = false; |
| }, |
| |
| removeChildren: function() |
| { |
| TreeElement.prototype.removeChildren.call(this); |
| |
| if (this._framesFolderTreeElement) |
| this._framesFolderTreeElement.removeChildren(); |
| |
| for (var type in this._resourceFoldersTypeMap) |
| this._resourceFoldersTypeMap[type].removeChildren(); |
| |
| delete this._resourceFoldersTypeMap; |
| delete this._framesFolderTreeElement; |
| }, |
| |
| // Private |
| |
| _updateExpandedSetting: function() |
| { |
| this._expandedSetting = new WebInspector.Setting("frame-expanded-" + this._frame.url.hash, this._frame.isMainFrame() ? true : false); |
| if (this._expandedSetting.value) |
| this.expand(); |
| else |
| this.collapse(); |
| }, |
| |
| _updateParentStatus: function() |
| { |
| this.hasChildren = (this._frame.resources.length || this._frame.childFrames.length || (this.resource && this.resource.sourceMaps.length)); |
| if (!this.hasChildren) |
| this.removeChildren(); |
| }, |
| |
| _mainResourceDidChange: function(event) |
| { |
| this._updateResource(this._frame.mainResource); |
| this._updateParentStatus(); |
| |
| this._groupedIntoFolders = false; |
| |
| this._clearNewChildQueue(); |
| |
| this.removeChildren(); |
| |
| // Change the expanded setting since the frame URL has changed. Do this before setting shouldRefreshChildren, since |
| // shouldRefreshChildren will call onpopulate if expanded is true. |
| this._updateExpandedSetting(); |
| |
| if (this._frame.isMainFrame()) |
| this._updateDownloadButton(); |
| |
| this.shouldRefreshChildren = true; |
| }, |
| |
| _resourceWasAdded: function(event) |
| { |
| this._addRepresentedObjectToNewChildQueue(event.data.resource); |
| }, |
| |
| _resourceWasRemoved: function(event) |
| { |
| this._removeChildForRepresentedObject(event.data.resource); |
| }, |
| |
| _childFrameWasAdded: function(event) |
| { |
| this._addRepresentedObjectToNewChildQueue(event.data.childFrame); |
| }, |
| |
| _childFrameWasRemoved: function(event) |
| { |
| this._removeChildForRepresentedObject(event.data.childFrame); |
| }, |
| |
| _childContentFlowWasAdded: function(event) |
| { |
| this._addRepresentedObjectToNewChildQueue(event.data.flow); |
| }, |
| |
| _childContentFlowWasRemoved: function(event) |
| { |
| this._removeChildForRepresentedObject(event.data.flow); |
| }, |
| |
| _rootDOMNodeInvalidated: function() { |
| if (this.expanded) |
| this._frame.domTree.requestContentFlowList(); |
| }, |
| |
| _addRepresentedObjectToNewChildQueue: function(representedObject) |
| { |
| // This queue reduces flashing as resources load and change folders when their type becomes known. |
| |
| this._newChildQueue.push(representedObject); |
| if (!this._newChildQueueTimeoutIdentifier) |
| this._newChildQueueTimeoutIdentifier = setTimeout(this._populateFromNewChildQueue.bind(this), WebInspector.FrameTreeElement.NewChildQueueUpdateInterval); |
| }, |
| |
| _removeRepresentedObjectFromNewChildQueue: function(representedObject) |
| { |
| this._newChildQueue.remove(representedObject); |
| }, |
| |
| _populateFromNewChildQueue: function() |
| { |
| for (var i = 0; i < this._newChildQueue.length; ++i) |
| this._addChildForRepresentedObject(this._newChildQueue[i]); |
| |
| this._newChildQueue = []; |
| this._newChildQueueTimeoutIdentifier = null; |
| }, |
| |
| _clearNewChildQueue: function() |
| { |
| this._newChildQueue = []; |
| if (this._newChildQueueTimeoutIdentifier) { |
| clearTimeout(this._newChildQueueTimeoutIdentifier); |
| this._newChildQueueTimeoutIdentifier = null; |
| } |
| }, |
| |
| _addChildForRepresentedObject: function(representedObject) |
| { |
| console.assert(representedObject instanceof WebInspector.Resource || representedObject instanceof WebInspector.Frame || representedObject instanceof WebInspector.ContentFlow); |
| if (!(representedObject instanceof WebInspector.Resource || representedObject instanceof WebInspector.Frame || representedObject instanceof WebInspector.ContentFlow)) |
| return; |
| |
| this._updateParentStatus(); |
| |
| if (!this.treeOutline) { |
| // Just mark as needing to update to avoid doing work that might not be needed. |
| this.shouldRefreshChildren = true; |
| return; |
| } |
| |
| if (this._shouldGroupIntoFolders() && !this._groupedIntoFolders) { |
| // Mark as needing a refresh to rebuild the tree into folders. |
| this._groupedIntoFolders = true; |
| this.shouldRefreshChildren = true; |
| return; |
| } |
| |
| this._addTreeElementForRepresentedObject(representedObject); |
| }, |
| |
| _removeChildForRepresentedObject: function(representedObject) |
| { |
| console.assert(representedObject instanceof WebInspector.Resource || representedObject instanceof WebInspector.Frame || representedObject instanceof WebInspector.ContentFlow); |
| if (!(representedObject instanceof WebInspector.Resource || representedObject instanceof WebInspector.Frame || representedObject instanceof WebInspector.ContentFlow)) |
| return; |
| |
| this._removeRepresentedObjectFromNewChildQueue(representedObject); |
| |
| this._updateParentStatus(); |
| |
| if (!this.treeOutline) { |
| // Just mark as needing to update to avoid doing work that might not be needed. |
| this.shouldRefreshChildren = true; |
| return; |
| } |
| |
| // Find the tree element for the frame by using getCachedTreeElement |
| // to only get the item if it has been created already. |
| var childTreeElement = this.treeOutline.getCachedTreeElement(representedObject); |
| if (!childTreeElement || !childTreeElement.parent) |
| return; |
| |
| this._removeTreeElement(childTreeElement); |
| }, |
| |
| _addTreeElementForRepresentedObject: function(representedObject) |
| { |
| var childTreeElement = this.treeOutline.getCachedTreeElement(representedObject); |
| if (!childTreeElement) { |
| if (representedObject instanceof WebInspector.SourceMapResource) |
| childTreeElement = new WebInspector.SourceMapResourceTreeElement(representedObject); |
| else if (representedObject instanceof WebInspector.Resource) |
| childTreeElement = new WebInspector.ResourceTreeElement(representedObject); |
| else if (representedObject instanceof WebInspector.Frame) |
| childTreeElement = new WebInspector.FrameTreeElement(representedObject); |
| else if (representedObject instanceof WebInspector.ContentFlow) |
| childTreeElement = new WebInspector.ContentFlowTreeElement(representedObject); |
| } |
| |
| this._addTreeElement(childTreeElement); |
| }, |
| |
| _addTreeElement: function(childTreeElement) |
| { |
| console.assert(childTreeElement); |
| if (!childTreeElement) |
| return; |
| |
| var wasSelected = childTreeElement.selected; |
| |
| this._removeTreeElement(childTreeElement, true, true); |
| |
| var parentTreeElement = this._parentTreeElementForRepresentedObject(childTreeElement.representedObject); |
| if (parentTreeElement !== this && !parentTreeElement.parent) |
| this._insertFolderTreeElement(parentTreeElement); |
| |
| this._insertResourceTreeElement(parentTreeElement, childTreeElement); |
| |
| if (wasSelected) |
| childTreeElement.revealAndSelect(true, false, true, true); |
| }, |
| |
| _compareTreeElementsByMainTitle: function(a, b) |
| { |
| return a.mainTitle.localeCompare(b.mainTitle); |
| }, |
| |
| _insertFolderTreeElement: function(folderTreeElement) |
| { |
| console.assert(this._groupedIntoFolders); |
| console.assert(!folderTreeElement.parent); |
| this.insertChild(folderTreeElement, insertionIndexForObjectInListSortedByFunction(folderTreeElement, this.children, this._compareTreeElementsByMainTitle)); |
| }, |
| |
| _compareResourceTreeElements: function(a, b) |
| { |
| if (a === b) |
| return 0; |
| |
| var aIsResource = a instanceof WebInspector.ResourceTreeElement; |
| var bIsResource = b instanceof WebInspector.ResourceTreeElement; |
| |
| if (aIsResource && bIsResource) |
| return WebInspector.ResourceTreeElement.compareResourceTreeElements(a, b); |
| |
| if (!aIsResource && !bIsResource) { |
| // When both components are not resources then just compare the titles. |
| return a.mainTitle.localeCompare(b.mainTitle); |
| } |
| |
| // Non-resources should appear before the resources. |
| // FIXME: There should be a better way to group the elements by their type. |
| return aIsResource ? 1 : -1; |
| }, |
| |
| _insertResourceTreeElement: function(parentTreeElement, childTreeElement) |
| { |
| console.assert(!childTreeElement.parent); |
| parentTreeElement.insertChild(childTreeElement, insertionIndexForObjectInListSortedByFunction(childTreeElement, parentTreeElement.children, this._compareResourceTreeElements)); |
| }, |
| |
| _removeTreeElement: function(childTreeElement, suppressOnDeselect, suppressSelectSibling) |
| { |
| var oldParent = childTreeElement.parent; |
| if (!oldParent) |
| return; |
| |
| oldParent.removeChild(childTreeElement, suppressOnDeselect, suppressSelectSibling); |
| |
| if (oldParent === this) |
| return; |
| |
| console.assert(oldParent instanceof WebInspector.FolderTreeElement); |
| if (!(oldParent instanceof WebInspector.FolderTreeElement)) |
| return; |
| |
| // Remove the old parent folder if it is now empty. |
| if (!oldParent.children.length) |
| oldParent.parent.removeChild(oldParent); |
| }, |
| |
| _folderNameForResourceType: function(type) |
| { |
| return WebInspector.Resource.Type.displayName(type, true); |
| }, |
| |
| _parentTreeElementForRepresentedObject: function(representedObject) |
| { |
| if (!this._groupedIntoFolders) |
| return this; |
| |
| function createFolderTreeElement(type, displayName) |
| { |
| var folderTreeElement = new WebInspector.FolderTreeElement(displayName); |
| folderTreeElement._expandedSetting = new WebInspector.Setting(type + "-folder-expanded-" + this._frame.url.hash, false); |
| if (folderTreeElement._expandedSetting.value) |
| folderTreeElement.expand(); |
| folderTreeElement.onexpand = this._folderTreeElementExpandedStateChange.bind(this); |
| folderTreeElement.oncollapse = this._folderTreeElementExpandedStateChange.bind(this); |
| return folderTreeElement; |
| } |
| |
| if (representedObject instanceof WebInspector.Frame) { |
| if (!this._framesFolderTreeElement) |
| this._framesFolderTreeElement = createFolderTreeElement.call(this, "frames", WebInspector.UIString("Frames")); |
| return this._framesFolderTreeElement; |
| } |
| |
| if (representedObject instanceof WebInspector.ContentFlow) { |
| if (!this._flowsFolderTreeElement) |
| this._flowsFolderTreeElement = createFolderTreeElement.call(this, "flows", WebInspector.UIString("Flows")); |
| return this._flowsFolderTreeElement; |
| } |
| |
| if (representedObject instanceof WebInspector.Resource) { |
| var folderName = this._folderNameForResourceType(representedObject.type); |
| if (!folderName) |
| return this; |
| |
| if (!this._resourceFoldersTypeMap) |
| this._resourceFoldersTypeMap = {}; |
| if (!this._resourceFoldersTypeMap[representedObject.type]) |
| this._resourceFoldersTypeMap[representedObject.type] = createFolderTreeElement.call(this, representedObject.type, folderName); |
| return this._resourceFoldersTypeMap[representedObject.type]; |
| } |
| |
| console.error("Unknown representedObject: ", representedObject); |
| return this; |
| }, |
| |
| _folderTreeElementExpandedStateChange: function(folderTreeElement) |
| { |
| console.assert(folderTreeElement._expandedSetting); |
| folderTreeElement._expandedSetting.value = folderTreeElement.expanded; |
| }, |
| |
| _shouldGroupIntoFolders: function() |
| { |
| // Already grouped into folders, keep it that way. |
| if (this._groupedIntoFolders) |
| return true; |
| |
| // Resources and Frames are grouped into folders if one of two thresholds are met: |
| // 1) Once the number of medium categories passes NumberOfMediumCategoriesThreshold. |
| // 2) When there is a category that passes LargeChildCountThreshold and there are |
| // any resources in another category. |
| |
| // Folders are avoided when there is only one category or most categories are small. |
| |
| var numberOfSmallCategories = 0; |
| var numberOfMediumCategories = 0; |
| var foundLargeCategory = false; |
| var frame = this._frame; |
| |
| function pushResourceType(type) { |
| // There are some other properties on WebInspector.Resource.Type that we need to skip, like private data and functions |
| if (type.charAt(0) === "_") |
| return false; |
| |
| // Only care about the values that are strings, not functions, etc. |
| var typeValue = WebInspector.Resource.Type[type]; |
| if (typeof typeValue !== "string") |
| return false; |
| |
| return pushCategory(frame.resourcesWithType(typeValue).length); |
| } |
| |
| function pushCategory(resourceCount) |
| { |
| if (!resourceCount) |
| return false; |
| |
| // If this type has any resources and there is a known large category, make folders. |
| if (foundLargeCategory) |
| return true; |
| |
| // If there are lots of this resource type, then count it as a large category. |
| if (resourceCount >= WebInspector.FrameTreeElement.LargeChildCountThreshold) { |
| // If we already have other resources in other small or medium categories, make folders. |
| if (numberOfSmallCategories || numberOfMediumCategories) |
| return true; |
| |
| foundLargeCategory = true; |
| return false; |
| } |
| |
| // Check if this is a medium category. |
| if (resourceCount >= WebInspector.FrameTreeElement.MediumChildCountThreshold) { |
| // If this is the medium category that puts us over the maximum allowed, make folders. |
| return ++numberOfMediumCategories >= WebInspector.FrameTreeElement.NumberOfMediumCategoriesThreshold; |
| } |
| |
| // This is a small category. |
| ++numberOfSmallCategories; |
| return false; |
| } |
| |
| // Iterate over all the available resource types. |
| return pushCategory(frame.childFrames.length) || pushCategory(frame.domTree.flowsCount) || Object.keys(WebInspector.Resource.Type).some(pushResourceType); |
| }, |
| |
| _reloadPageClicked: function(event) |
| { |
| // Ignore cache when the shift key is pressed. |
| PageAgent.reload(event.data.shiftKey); |
| }, |
| |
| _downloadButtonClicked: function(event) |
| { |
| WebInspector.archiveMainFrame(); |
| }, |
| |
| _updateDownloadButton: function() |
| { |
| console.assert(this._frame.isMainFrame()); |
| if (!this._downloadButton) |
| return; |
| |
| if (!PageAgent.archive) { |
| this._downloadButton.hidden = true; |
| return; |
| } |
| |
| if (this._downloadingPage) { |
| this._downloadButton.enabled = false; |
| return; |
| } |
| |
| this._downloadButton.enabled = WebInspector.canArchiveMainFrame(); |
| }, |
| |
| _pageArchiveStarted: function(event) |
| { |
| this._downloadingPage = true; |
| this._updateDownloadButton(); |
| }, |
| |
| _pageArchiveEnded: function(event) |
| { |
| this._downloadingPage = false; |
| this._updateDownloadButton(); |
| } |
| }; |
| |
| WebInspector.FrameTreeElement.prototype.__proto__ = WebInspector.ResourceTreeElement.prototype; |