Bug 920141 - Add support for inspecting anonymous content. r=pbrosset

This commit is contained in:
Brian Grinstead
2014-09-29 09:29:00 +02:00
parent fbbe7d1812
commit 8cb3638a5d
35 changed files with 1532 additions and 496 deletions

View File

@@ -22,6 +22,7 @@ const {HTMLEditor} = require("devtools/markupview/html-editor");
const promise = require("devtools/toolkit/deprecated-sync-thenables");
const {Tooltip} = require("devtools/shared/widgets/Tooltip");
const EventEmitter = require("devtools/toolkit/event-emitter");
const Heritage = require("sdk/core/heritage");
Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
Cu.import("resource://gre/modules/devtools/Templater.jsm");
@@ -40,6 +41,9 @@ loader.lazyGetter(this, "AutocompletePopup", () => {
*
* MarkupContainer - the structure that holds an editor and its
* immediate children in the markup panel.
* - MarkupElementContainer: markup container for element nodes
* - MarkupTextContainer: markup container for text / comment nodes
* - MarkupReadonlyContainer: markup container for other nodes
* Node - A content node.
* object.elt - A UI element in the markup panel.
*/
@@ -186,7 +190,7 @@ MarkupView.prototype = {
parentNode = parentNode.parentNode;
}
if (container) {
if (container instanceof MarkupElementContainer) {
// With the newly found container, delegate the tooltip content creation
// and decision to show or not the tooltip
container._buildEventTooltipContent(event.target, this.tooltip);
@@ -301,7 +305,7 @@ MarkupView.prototype = {
* tooltip.
* Delegates the actual decision to the corresponding MarkupContainer instance
* if one is found.
* @return the promise returned by MarkupContainer._isImagePreviewTarget
* @return the promise returned by MarkupElementContainer._isImagePreviewTarget
*/
_isImagePreviewTarget: function(target) {
// From the target passed here, let's find the parent MarkupContainer
@@ -315,10 +319,10 @@ MarkupView.prototype = {
parent = parent.parentNode;
}
if (container) {
if (container instanceof MarkupElementContainer) {
// With the newly found container, delegate the tooltip content creation
// and decision to show or not the tooltip
return container._isImagePreviewTarget(target, this.tooltip);
return container.isImagePreviewTarget(target, this.tooltip);
}
},
@@ -507,7 +511,8 @@ MarkupView.prototype = {
*/
deleteNode: function(aNode) {
if (aNode.isDocumentElement ||
aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) {
aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE ||
aNode.isAnonymous) {
return;
}
@@ -568,7 +573,7 @@ MarkupView.prototype = {
/**
* Make sure a node is included in the markup tool.
*
* @param DOMNode aNode
* @param NodeFront aNode
* The node in the content document.
* @param boolean aFlashNode
* Whether the newly imported node should be flashed
@@ -583,15 +588,23 @@ MarkupView.prototype = {
return this.getContainer(aNode);
}
let container;
let {nodeType, isPseudoElement} = aNode;
if (aNode === this.walker.rootNode) {
var container = new RootContainer(this, aNode);
container = new RootContainer(this, aNode);
this._elt.appendChild(container.elt);
this._rootNode = aNode;
} else if (nodeType == Ci.nsIDOMNode.ELEMENT_NODE && !isPseudoElement) {
container = new MarkupElementContainer(this, aNode, this._inspector);
} else if (nodeType == Ci.nsIDOMNode.COMMENT_NODE ||
nodeType == Ci.nsIDOMNode.TEXT_NODE) {
container = new MarkupTextContainer(this, aNode, this._inspector);
} else {
var container = new MarkupContainer(this, aNode, this._inspector);
if (aFlashNode) {
container.flashMutation();
}
container = new MarkupReadOnlyContainer(this, aNode, this._inspector);
}
if (aFlashNode) {
container.flashMutation();
}
this._containers.set(aNode, container);
@@ -961,7 +974,7 @@ MarkupView.prototype = {
let parentContainer = this.getContainer(parent);
if (parentContainer) {
parentContainer.childrenDirty = true;
this._updateChildren(parentContainer, {expand: node});
this._updateChildren(parentContainer, {expand: true});
}
}
@@ -1306,74 +1319,69 @@ MarkupView.prototype = {
}
};
/**
* The main structure for storing a document node in the markup
* tree. Manages creation of the editor for the node and
* a <ul> for placing child elements, and expansion/collapsing
* of the element.
*
* @param MarkupView aMarkupView
* The markup view that owns this container.
* @param DOMNode aNode
* The node to display.
* @param Inspector aInspector
* The inspector tool container the markup-view
* This should not be instantiated directly, instead use one of:
* MarkupReadOnlyContainer
* MarkupTextContainer
* MarkupElementContainer
*/
function MarkupContainer(aMarkupView, aNode, aInspector) {
this.markup = aMarkupView;
this.doc = this.markup.doc;
this.undo = this.markup.undo;
this.node = aNode;
this._inspector = aInspector;
if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE) {
this.editor = new TextEditor(this, aNode, "text");
} else if (aNode.nodeType == Ci.nsIDOMNode.COMMENT_NODE) {
this.editor = new TextEditor(this, aNode, "comment");
} else if (aNode.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
this.editor = new ElementEditor(this, aNode);
} else if (aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) {
this.editor = new DoctypeEditor(this, aNode);
} else {
this.editor = new GenericEditor(this, aNode);
}
// The template will fill the following properties
this.elt = null;
this.expander = null;
this.tagState = null;
this.tagLine = null;
this.children = null;
this.markup.template("container", this);
this.elt.container = this;
this.children.container = this;
// Expanding/collapsing the node on dblclick of the whole tag-line element
this._onToggle = this._onToggle.bind(this);
this.elt.addEventListener("dblclick", this._onToggle, false);
this.expander.addEventListener("click", this._onToggle, false);
// Appending the editor element and attaching event listeners
this.tagLine.appendChild(this.editor.elt);
this._onMouseDown = this._onMouseDown.bind(this);
this.elt.addEventListener("mousedown", this._onMouseDown, false);
// Prepare the image preview tooltip data if any
this._prepareImagePreview();
// Marking the node as shown or hidden
this.isDisplayed = this.node.isDisplayed;
}
function MarkupContainer() { }
MarkupContainer.prototype = {
/*
* Initialize the MarkupContainer. Should be called while one
* of the other contain classes is instantiated.
*
* @param MarkupView markupView
* The markup view that owns this container.
* @param NodeFront node
* The node to display.
* @param string templateID
* Which template to render for this container
*/
initialize: function(markupView, node, templateID) {
this.markup = markupView;
this.node = node;
this.undo = this.markup.undo;
// The template will fill the following properties
this.elt = null;
this.expander = null;
this.tagState = null;
this.tagLine = null;
this.children = null;
this.markup.template(templateID, this);
this.elt.container = this;
// Binding event listeners
this._onMouseDown = this._onMouseDown.bind(this);
this.elt.addEventListener("mousedown", this._onMouseDown, false);
this._onToggle = this._onToggle.bind(this);
// Expanding/collapsing the node on dblclick of the whole tag-line element
this.elt.addEventListener("dblclick", this._onToggle, false);
if (this.expander) {
this.expander.addEventListener("click", this._onToggle, false);
}
// Marking the node as shown or hidden
this.isDisplayed = this.node.isDisplayed;
},
toString: function() {
return "[MarkupContainer for " + this.node + "]";
},
isPreviewable: function() {
if (this.node.tagName) {
if (this.node.tagName && !this.node.isPseudoElement) {
let tagName = this.node.tagName.toLowerCase();
let srcAttr = this.editor.getAttributeElement("src");
let isImage = tagName === "img" && srcAttr;
@@ -1385,60 +1393,6 @@ MarkupContainer.prototype = {
}
},
/**
* If the node is an image or canvas (@see isPreviewable), then get the
* image data uri from the server so that it can then later be previewed in
* a tooltip if needed.
* Stores a promise in this.tooltipData.data that resolves when the data has
* been retrieved
*/
_prepareImagePreview: function() {
if (this.isPreviewable()) {
// Get the image data for later so that when the user actually hovers over
// the element, the tooltip does contain the image
let def = promise.defer();
this.tooltipData = {
target: this.editor.getAttributeElement("src") || this.editor.tag,
data: def.promise
};
let maxDim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize");
this.node.getImageData(maxDim).then(data => {
data.data.string().then(str => {
let res = {data: str, size: data.size};
// Resolving the data promise and, to always keep tooltipData.data
// as a promise, create a new one that resolves immediately
def.resolve(res);
this.tooltipData.data = promise.resolve(res);
});
}, () => {
this.tooltipData.data = promise.reject();
});
}
},
/**
* Executed by MarkupView._isImagePreviewTarget which is itself called when the
* mouse hovers over a target in the markup-view.
* Checks if the target is indeed something we want to have an image tooltip
* preview over and, if so, inserts content into the tooltip.
* @return a promise that resolves when the content has been inserted or
* rejects if no preview is required. This promise is then used by Tooltip.js
* to decide if/when to show the tooltip
*/
_isImagePreviewTarget: function(target, tooltip) {
if (!this.tooltipData || this.tooltipData.target !== target) {
return promise.reject();
}
return this.tooltipData.data.then(({data, size}) => {
tooltip.setImageContent(data, size);
}, () => {
tooltip.setBrokenImageContent();
});
},
/**
* Show the element has displayed or not
*/
@@ -1449,37 +1403,6 @@ MarkupContainer.prototype = {
}
},
copyImageDataUri: function() {
// We need to send again a request to gettooltipData even if one was sent for
// the tooltip, because we want the full-size image
this.node.getImageData().then(data => {
data.data.string().then(str => {
clipboardHelper.copyString(str, this.markup.doc);
});
});
},
_buildEventTooltipContent: function(target, tooltip) {
if (target.hasAttribute("data-event")) {
tooltip.hide(target);
this.node.getEventListenerInfo().then(listenerInfo => {
tooltip.setEventContent({
eventListenerInfos: listenerInfo,
toolbox: this._inspector.toolbox
});
this.markup._makeTooltipPersistent(true);
tooltip.once("hidden", () => {
this.markup._makeTooltipPersistent(false);
});
tooltip.show(target);
});
return true;
}
},
/**
* True if the current node has children. The MarkupView
* will set this attribute for the MarkupContainer.
@@ -1492,6 +1415,10 @@ MarkupContainer.prototype = {
set hasChildren(aValue) {
this._hasChildren = aValue;
if (!this.expander) {
return;
}
if (aValue) {
this.expander.style.visibility = "visible";
} else {
@@ -1499,10 +1426,6 @@ MarkupContainer.prototype = {
}
},
parentContainer: function() {
return this.elt.parentNode ? this.elt.parentNode.container : null;
},
/**
* True if the node has been visually expanded in the tree.
*/
@@ -1511,32 +1434,35 @@ MarkupContainer.prototype = {
},
set expanded(aValue) {
if (!this.expander) {
return;
}
if (aValue && this.elt.classList.contains("collapsed")) {
// Expanding a node means cloning its "inline" closing tag into a new
// tag-line that the user can interact with and showing the children.
if (this.editor instanceof ElementEditor) {
let closingTag = this.elt.querySelector(".close");
if (closingTag) {
if (!this.closeTagLine) {
let line = this.markup.doc.createElement("div");
line.classList.add("tag-line");
let closingTag = this.elt.querySelector(".close");
if (closingTag) {
if (!this.closeTagLine) {
let line = this.markup.doc.createElement("div");
line.classList.add("tag-line");
let tagState = this.markup.doc.createElement("div");
tagState.classList.add("tag-state");
line.appendChild(tagState);
let tagState = this.markup.doc.createElement("div");
tagState.classList.add("tag-state");
line.appendChild(tagState);
line.appendChild(closingTag.cloneNode(true));
line.appendChild(closingTag.cloneNode(true));
this.closeTagLine = line;
}
this.elt.appendChild(this.closeTagLine);
this.closeTagLine = line;
}
this.elt.appendChild(this.closeTagLine);
}
this.elt.classList.remove("collapsed");
this.expander.setAttribute("open", "");
this.hovered = false;
} else if (!aValue) {
if (this.editor instanceof ElementEditor && this.closeTagLine) {
if (this.closeTagLine) {
this.elt.removeChild(this.closeTagLine);
}
this.elt.classList.add("collapsed");
@@ -1544,12 +1470,8 @@ MarkupContainer.prototype = {
}
},
_onToggle: function(event) {
this.markup.navigate(this);
if(this.hasChildren) {
this.markup.setNodeExpanded(this.node, !this.expanded, event.altKey);
}
event.stopPropagation();
parentContainer: function() {
return this.elt.parentNode ? this.elt.parentNode.container : null;
},
_onMouseDown: function(event) {
@@ -1695,11 +1617,27 @@ MarkupContainer.prototype = {
}
},
_onToggle: function(event) {
this.markup.navigate(this);
if (this.hasChildren) {
this.markup.setNodeExpanded(this.node, !this.expanded, event.altKey);
}
event.stopPropagation();
},
/**
* Get rid of event listeners and references, when the container is no longer
* needed
*/
destroy: function() {
// Remove event listeners
this.elt.removeEventListener("mousedown", this._onMouseDown, false);
this.elt.removeEventListener("dblclick", this._onToggle, false);
if (this.expander) {
this.expander.removeEventListener("click", this._onToggle, false);
}
// Recursively destroy children containers
let firstChild;
while (firstChild = this.children.firstChild) {
@@ -1711,16 +1649,168 @@ MarkupContainer.prototype = {
this.children.removeChild(firstChild);
}
// Remove event listeners
this.elt.removeEventListener("dblclick", this._onToggle, false);
this.elt.removeEventListener("mousedown", this._onMouseDown, false);
this.expander.removeEventListener("click", this._onToggle, false);
// Destroy my editor
this.editor.destroy();
}
};
/**
* An implementation of MarkupContainer for Pseudo Elements,
* Doctype nodes, or any other type generic node that doesn't
* fit for other editors.
* Does not allow any editing, just viewing / selecting.
*
* @param MarkupView markupView
* The markup view that owns this container.
* @param NodeFront node
* The node to display.
*/
function MarkupReadOnlyContainer(markupView, node) {
MarkupContainer.prototype.initialize.call(this, markupView, node, "readonlycontainer");
this.editor = new GenericEditor(this, node);
this.tagLine.appendChild(this.editor.elt);
}
MarkupReadOnlyContainer.prototype = Heritage.extend(MarkupContainer.prototype, {});
/**
* An implementation of MarkupContainer for text node and comment nodes.
* Allows basic text editing in a textarea.
*
* @param MarkupView aMarkupView
* The markup view that owns this container.
* @param NodeFront aNode
* The node to display.
* @param Inspector aInspector
* The inspector tool container the markup-view
*/
function MarkupTextContainer(markupView, node) {
MarkupContainer.prototype.initialize.call(this, markupView, node, "textcontainer");
if (node.nodeType == Ci.nsIDOMNode.TEXT_NODE) {
this.editor = new TextEditor(this, node, "text");
} else if (node.nodeType == Ci.nsIDOMNode.COMMENT_NODE) {
this.editor = new TextEditor(this, node, "comment");
} else {
throw "Invalid node for MarkupTextContainer";
}
this.tagLine.appendChild(this.editor.elt);
}
MarkupTextContainer.prototype = Heritage.extend(MarkupContainer.prototype, {});
/**
* An implementation of MarkupContainer for Elements that can contain
* child nodes.
* Allows editing of tag name, attributes, expanding / collapsing.
*
* @param MarkupView markupView
* The markup view that owns this container.
* @param NodeFront node
* The node to display.
*/
function MarkupElementContainer(markupView, node) {
MarkupContainer.prototype.initialize.call(this, markupView, node, "elementcontainer");
if (node.nodeType === Ci.nsIDOMNode.ELEMENT_NODE) {
this.editor = new ElementEditor(this, node);
} else {
throw "Invalid node for MarkupElementContainer";
}
this.tagLine.appendChild(this.editor.elt);
// Prepare the image preview tooltip data if any
this._prepareImagePreview();
}
MarkupElementContainer.prototype = Heritage.extend(MarkupContainer.prototype, {
_buildEventTooltipContent: function(target, tooltip) {
if (target.hasAttribute("data-event")) {
tooltip.hide(target);
this.node.getEventListenerInfo().then(listenerInfo => {
tooltip.setEventContent({
eventListenerInfos: listenerInfo,
toolbox: this.markup._inspector.toolbox
});
this.markup._makeTooltipPersistent(true);
tooltip.once("hidden", () => {
this.markup._makeTooltipPersistent(false);
});
tooltip.show(target);
});
return true;
}
},
/**
* If the node is an image or canvas (@see isPreviewable), then get the
* image data uri from the server so that it can then later be previewed in
* a tooltip if needed.
* Stores a promise in this.tooltipData.data that resolves when the data has
* been retrieved
*/
_prepareImagePreview: function() {
if (this.isPreviewable()) {
// Get the image data for later so that when the user actually hovers over
// the element, the tooltip does contain the image
let def = promise.defer();
this.tooltipData = {
target: this.editor.getAttributeElement("src") || this.editor.tag,
data: def.promise
};
let maxDim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize");
this.node.getImageData(maxDim).then(data => {
data.data.string().then(str => {
let res = {data: str, size: data.size};
// Resolving the data promise and, to always keep tooltipData.data
// as a promise, create a new one that resolves immediately
def.resolve(res);
this.tooltipData.data = promise.resolve(res);
});
}, () => {
this.tooltipData.data = promise.reject();
});
}
},
/**
* Executed by MarkupView._isImagePreviewTarget which is itself called when the
* mouse hovers over a target in the markup-view.
* Checks if the target is indeed something we want to have an image tooltip
* preview over and, if so, inserts content into the tooltip.
* @return a promise that resolves when the content has been inserted or
* rejects if no preview is required. This promise is then used by Tooltip.js
* to decide if/when to show the tooltip
*/
isImagePreviewTarget: function(target, tooltip) {
if (!this.tooltipData || this.tooltipData.target !== target) {
return promise.reject();
}
return this.tooltipData.data.then(({data, size}) => {
tooltip.setImageContent(data, size);
}, () => {
tooltip.setBrokenImageContent();
});
},
copyImageDataUri: function() {
// We need to send again a request to gettooltipData even if one was sent for
// the tooltip, because we want the full-size image
this.node.getImageData().then(data => {
data.data.string().then(str => {
clipboardHelper.copyString(str, this.markup.doc);
});
});
}
});
/**
* Dummy container node used for the root document element.
@@ -1742,35 +1832,33 @@ RootContainer.prototype = {
};
/**
* Creates an editor for simple nodes.
* Creates an editor for non-editable nodes.
*/
function GenericEditor(aContainer, aNode) {
this.elt = aContainer.doc.createElement("span");
this.elt.className = "editor";
this.elt.textContent = aNode.nodeName;
this.container = aContainer;
this.markup = this.container.markup;
this.template = this.markup.template.bind(this.markup);
this.elt = null;
this.template("generic", this);
if (aNode.isPseudoElement) {
this.tag.classList.add("theme-fg-color5");
this.tag.textContent = aNode.isBeforePseudoElement ? "::before" : "::after";
} else if (aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) {
this.elt.classList.add("comment");
this.tag.textContent = '<!DOCTYPE ' + aNode.name +
(aNode.publicId ? ' PUBLIC "' + aNode.publicId + '"': '') +
(aNode.systemId ? ' "' + aNode.systemId + '"' : '') +
'>';
} else {
this.tag.textContent = aNode.nodeName;
}
}
GenericEditor.prototype = {
destroy: function() {}
};
/**
* Creates an editor for a DOCTYPE node.
*
* @param MarkupContainer aContainer The container owning this editor.
* @param DOMNode aNode The node being edited.
*/
function DoctypeEditor(aContainer, aNode) {
this.elt = aContainer.doc.createElement("span");
this.elt.className = "editor comment";
this.elt.textContent = '<!DOCTYPE ' + aNode.name +
(aNode.publicId ? ' PUBLIC "' + aNode.publicId + '"': '') +
(aNode.systemId ? ' "' + aNode.systemId + '"' : '') +
'>';
}
DoctypeEditor.prototype = {
destroy: function() {}
destroy: function() {
this.elt.remove();
}
};
/**
@@ -1782,10 +1870,13 @@ DoctypeEditor.prototype = {
* @param string aTemplate The template id to use to build the editor.
*/
function TextEditor(aContainer, aNode, aTemplate) {
this.container = aContainer;
this.markup = this.container.markup;
this.node = aNode;
this.template = this.markup.template.bind(aTemplate);
this._selected = false;
aContainer.markup.template(aTemplate, this);
this.markup.template(aTemplate, this);
editableField({
element: this.value,
@@ -1800,13 +1891,13 @@ function TextEditor(aContainer, aNode, aTemplate) {
longstr.string().then(oldValue => {
longstr.release().then(null, console.error);
aContainer.undo.do(() => {
this.container.undo.do(() => {
this.node.setNodeValue(aVal).then(() => {
aContainer.markup.nodeChanged(this.node);
this.markup.nodeChanged(this.node);
});
}, () => {
this.node.setNodeValue(oldValue).then(() => {
aContainer.markup.nodeChanged(this.node);
this.markup.nodeChanged(this.node);
})
});
});
@@ -1859,12 +1950,11 @@ TextEditor.prototype = {
* @param Element aNode The node being edited.
*/
function ElementEditor(aContainer, aNode) {
this.doc = aContainer.doc;
this.undo = aContainer.undo;
this.template = aContainer.markup.template.bind(aContainer.markup);
this.container = aContainer;
this.markup = this.container.markup;
this.node = aNode;
this.markup = this.container.markup;
this.template = this.markup.template.bind(this.markup);
this.doc = this.markup.doc;
this.attrs = {};
@@ -1911,7 +2001,7 @@ function ElementEditor(aContainer, aNode) {
let doMods = this._startModifyingAttributes();
let undoMods = this._startModifyingAttributes();
this._applyAttributes(aVal, null, doMods, undoMods);
this.undo.do(() => {
this.container.undo.do(() => {
doMods.apply();
}, function() {
undoMods.apply();
@@ -2039,7 +2129,7 @@ ElementEditor.prototype = {
this._saveAttribute(aAttr.name, undoMods);
doMods.removeAttribute(aAttr.name);
this._applyAttributes(aVal, attr, doMods, undoMods);
this.undo.do(() => {
this.container.undo.do(() => {
doMods.apply();
}, () => {
undoMods.apply();
@@ -2154,7 +2244,7 @@ ElementEditor.prototype = {
aOld.parentNode.removeChild(aOld);
}
this.undo.do(() => {
this.container.undo.do(() => {
swapNodes(this.rawNode, newElt);
this.markup.setNodeExpanded(newFront, this.container.expanded);
if (this.container.selected) {