blob: 24d69f088aa709059e317ab432c37c05d15fd966 [file] [log] [blame]
/*
* 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;