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

--HG--
rename : browser/devtools/styleinspector/test/browser_ruleview_pseudo-element.js => browser/devtools/styleinspector/test/browser_ruleview_pseudo-element_01.js
This commit is contained in:
Brian Grinstead 2014-09-29 09:29:00 +02:00
parent 555152bf1f
commit 03ce1f7e86
35 changed files with 1532 additions and 496 deletions

View File

@ -93,7 +93,7 @@ FontInspector.prototype = {
// We don't get fonts for a node, but for a range
let rng = contentDocument.createRange();
rng.selectNode(node);
rng.selectNodeContents(node);
let fonts = DOMUtils.getUsedFontFaces(rng);
let fontsArray = [];
for (let i = 0; i < fonts.length; i++) {

View File

@ -8,6 +8,7 @@
const {Cu, Ci} = require("chrome");
let EventEmitter = require("devtools/toolkit/event-emitter");
Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
/**
* API
@ -225,11 +226,18 @@ Selection.prototype = {
if (rawNode) {
try {
let doc = this.document;
return (doc && doc.defaultView && doc.documentElement.contains(rawNode));
if (doc && doc.defaultView) {
let docEl = doc.documentElement;
let bindingParent = LayoutHelpers.getRootBindingParent(rawNode);
if (docEl.contains(bindingParent)) {
return true;
}
}
} catch (e) {
// "can't access dead object" error
return false;
}
return false;
}
while(node) {
@ -252,6 +260,14 @@ Selection.prototype = {
return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ELEMENT_NODE;
},
isPseudoElementNode: function() {
return this.isNode() && this.nodeFront.isPseudoElement;
},
isAnonymousNode: function() {
return this.isNode() && this.nodeFront.isAnonymous;
},
isAttributeNode: function() {
return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ATTRIBUTE_NODE;
},

View File

@ -156,6 +156,10 @@ HTMLBreadcrumbs.prototype = {
prettyPrintNodeAsText: function BC_prettyPrintNodeText(aNode)
{
let text = aNode.tagName.toLowerCase();
if (aNode.isPseudoElement) {
text = aNode.isBeforePseudoElement ? "::before" : "::after";
}
if (aNode.id) {
text += "#" + aNode.id;
}
@ -201,6 +205,9 @@ HTMLBreadcrumbs.prototype = {
pseudosLabel.className = "breadcrumbs-widget-item-pseudo-classes plain";
let tagText = aNode.tagName.toLowerCase();
if (aNode.isPseudoElement) {
tagText = aNode.isBeforePseudoElement ? "::before" : "::after";
}
let idText = aNode.id ? ("#" + aNode.id) : "";
let classesText = "";

View File

@ -584,7 +584,10 @@ InspectorPanel.prototype = {
* Disable the delete item if needed. Update the pseudo classes.
*/
_setupNodeMenu: function InspectorPanel_setupNodeMenu() {
let isSelectionElement = this.selection.isElementNode();
let isSelectionElement = this.selection.isElementNode() &&
!this.selection.isPseudoElementNode();
let isEditableElement = isSelectionElement &&
!this.selection.isAnonymousNode();
// Set the pseudo classes
for (let name of ["hover", "active", "focus"]) {
@ -601,10 +604,10 @@ InspectorPanel.prototype = {
// Disable delete item if needed
let deleteNode = this.panelDoc.getElementById("node-menu-delete");
if (this.selection.isRoot() || this.selection.isDocumentTypeNode()) {
deleteNode.setAttribute("disabled", "true");
} else {
if (isEditableElement) {
deleteNode.removeAttribute("disabled");
} else {
deleteNode.setAttribute("disabled", "true");
}
// Disable / enable "Copy Unique Selector", "Copy inner HTML" &
@ -625,7 +628,7 @@ InspectorPanel.prototype = {
// Enable the "edit HTML" item if the selection is an element and the root
// actor has the appropriate trait (isOuterHTMLEditable)
let editHTML = this.panelDoc.getElementById("node-menu-edithtml");
if (this.isOuterHTMLEditable && isSelectionElement) {
if (isEditableElement && this.isOuterHTMLEditable) {
editHTML.removeAttribute("disabled");
} else {
editHTML.setAttribute("disabled", "true");
@ -635,7 +638,7 @@ InspectorPanel.prototype = {
// the root actor has the appropriate trait (isOuterHTMLEditable) and if
// the clipbard content is appropriate.
let pasteOuterHTML = this.panelDoc.getElementById("node-menu-pasteouterhtml");
if (this.isOuterHTMLEditable && isSelectionElement &&
if (isEditableElement && this.isOuterHTMLEditable &&
this._getClipboardContentForOuterHTML()) {
pasteOuterHTML.removeAttribute("disabled");
} else {
@ -646,7 +649,7 @@ InspectorPanel.prototype = {
// which essentially checks if it's an image or canvas tag
let copyImageData = this.panelDoc.getElementById("node-menu-copyimagedatauri");
let markupContainer = this.markup.getContainer(this.selection.nodeFront);
if (markupContainer && markupContainer.isPreviewable()) {
if (isSelectionElement && markupContainer && markupContainer.isPreviewable()) {
copyImageData.removeAttribute("disabled");
} else {
copyImageData.setAttribute("disabled", "true");

View File

@ -51,4 +51,26 @@ let test = asyncTest(function*() {
is(labelId.textContent, "#" + id,
"Node #" + node.nodeId + ": selection matches");
}
yield testPseudoElements(inspector, container);
});
function *testPseudoElements(inspector, container) {
info ("Checking for pseudo elements");
let pseudoParent = getNodeFront(getNode("#pseudo-container"));
let children = yield inspector.walker.children(pseudoParent);
is (children.nodes.length, 2, "Pseudo children returned from walker");
let beforeElement = children.nodes[0];
let breadcrumbsUpdated = inspector.once("breadcrumbs-updated");
let nodeSelected = selectNode(beforeElement, inspector);
yield Promise.all([breadcrumbsUpdated, nodeSelected]);
is(container.childNodes[3].textContent, "::before", "::before shows up in breadcrumb");
let afterElement = children.nodes[1];
breadcrumbsUpdated = inspector.once("breadcrumbs-updated");
nodeSelected = selectNode(afterElement, inspector);
yield Promise.all([breadcrumbsUpdated, nodeSelected]);
is(container.childNodes[3].textContent, "::after", "::before shows up in breadcrumb");
}

View File

@ -7,6 +7,12 @@
border: 1px solid red;
margin: 10px;
}
#pseudo-container::before {
content: 'before';
}
#pseudo-container::after {
content: 'after';
}
</style>
</head>
<body>
@ -36,5 +42,6 @@
</div>
</div>
</article>
<div id='pseudo-container'></div>
</body>
</html>

View File

@ -155,7 +155,7 @@ function selectAndHighlightNode(nodeOrSelector, inspector) {
/**
* Set the inspector's current selection to a node or to the first match of the
* given css selector.
* @param {String|DOMNode} nodeOrSelector
* @param {String|DOMNode|NodeFront} nodeOrSelector
* @param {InspectorPanel} inspector
* The instance of InspectorPanel currently loaded in the toolbox
* @param {String} reason
@ -169,7 +169,11 @@ function selectNode(nodeOrSelector, inspector, reason="test") {
let node = getNode(nodeOrSelector);
let updated = inspector.once("inspector-updated");
inspector.selection.setNode(node, reason);
if (node._form) {
inspector.selection.setNodeFront(node, reason);
} else {
inspector.selection.setNode(node, reason);
}
return updated;
}

View File

@ -131,7 +131,7 @@
transition: background .5s;
}
.tag-line .open, .tag-line .close, .comment {
.tag-line {
cursor: default;
}

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) {

View File

@ -26,7 +26,20 @@
<div id="templates" style="display:none">
<ul class="children">
<li id="template-container" save="${elt}" class="child collapsed">
<li id="template-elementcontainer" save="${elt}" class="child collapsed">
<div save="${tagLine}" class="tag-line"><!--
--><span save="${tagState}" class="tag-state"></span><!--
--><span save="${expander}" class="theme-twisty expander"></span><!--
--></div>
<ul save="${children}" class="children"></ul>
</li>
<li id="template-textcontainer" save="${elt}" class="child collapsed">
<div save="${tagLine}" class="tag-line"><span save="${tagState}" class="tag-state"></span></div>
<ul save="${children}" class="children"></ul>
</li>
<li id="template-readonlycontainer" save="${elt}" class="child collapsed">
<div save="${tagLine}" class="tag-line"><!--
--><span save="${tagState}" class="tag-state"></span><!--
--><span save="${expander}" class="theme-twisty expander"></span><!--
@ -42,6 +55,8 @@
</li>
</ul>
<span id="template-generic" save="${elt}" class="editor"><span save="${tag}" class="tag"></span></span>
<span id="template-element" save="${elt}" class="editor"><!--
--><span class="open">&lt;<!--
--><span save="${tag}" class="tag theme-fg-color3" tabindex="0"></span><!--

View File

@ -1,6 +1,7 @@
[DEFAULT]
subsuite = devtools
support-files =
doc_markup_anonymous.html
doc_markup_edit.html
doc_markup_events.html
doc_markup_events_jquery.html
@ -29,6 +30,10 @@ support-files =
lib_jquery_1.11.1_min.js
lib_jquery_2.1.1_min.js
[browser_markupview_anonymous_01.js]
[browser_markupview_anonymous_02.js]
skip-if = e10s # scratchpad.xul is not loading in e10s window
[browser_markupview_anonymous_03.js]
[browser_markupview_copy_image_data.js]
[browser_markupview_css_completion_style_attribute.js]
[browser_markupview_events.js]

View File

@ -0,0 +1,30 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test native anonymous content in the markupview.
const TEST_URL = TEST_URL_ROOT + "doc_markup_anonymous.html";
let test = asyncTest(function*() {
let {inspector} = yield addTab(TEST_URL).then(openInspector);
let pseudo = yield getNodeFront("#pseudo", inspector);
// Markup looks like: <div><::before /><span /><::after /></div>
let children = yield inspector.walker.children(pseudo);
is (children.nodes.length, 3, "Children returned from walker");
info ("Checking the ::before pseudo element");
let before = children.nodes[0];
yield isEditingMenuDisabled(before, inspector);
info ("Checking the normal child element");
let span = children.nodes[1];
yield isEditingMenuEnabled(span, inspector);
info ("Checking the ::after pseudo element");
let after = children.nodes[2];
yield isEditingMenuDisabled(after, inspector);
});

View File

@ -0,0 +1,29 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test XBL anonymous content in the markupview
const TEST_URL = "chrome://browser/content/devtools/scratchpad.xul";
let test = asyncTest(function*() {
let {inspector} = yield addTab(TEST_URL).then(openInspector);
let toolbarbutton = yield getNodeFront("toolbarbutton", inspector);
let children = yield inspector.walker.children(toolbarbutton);
is(toolbarbutton.numChildren, 3, "Correct number of children");
is (children.nodes.length, 3, "Children returned from walker");
is(toolbarbutton.isAnonymous, false, "Toolbarbutton is not anonymous");
yield isEditingMenuEnabled(toolbarbutton, inspector);
for (let node of children.nodes) {
ok (node.isAnonymous, "Child is anonymous");
ok (node._form.isXBLAnonymous, "Child is XBL anonymous");
ok (!node._form.isShadowAnonymous, "Child is not shadow anonymous");
ok (!node._form.isNativeAnonymous, "Child is not native anonymous");
yield isEditingMenuDisabled(node, inspector);
}
});

View File

@ -0,0 +1,34 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test shadow DOM content in the markupview.
// Note that many features are not yet enabled, but basic listing
// of elements should be working.
const TEST_URL = TEST_URL_ROOT + "doc_markup_anonymous.html";
let test = asyncTest(function*() {
Services.prefs.setBoolPref("dom.webcomponents.enabled", true);
let {inspector} = yield addTab(TEST_URL).then(openInspector);
let shadow = yield getNodeFront("#shadow", inspector.markup);
let children = yield inspector.walker.children(shadow);
is (shadow.numChildren, 3, "Children of the shadow root are counted");
is (children.nodes.length, 3, "Children returned from walker");
info ("Checking the ::before pseudo element");
let before = children.nodes[0];
yield isEditingMenuDisabled(before, inspector);
info ("Checking the <h3> shadow element");
let shadowChild1 = children.nodes[1];
yield isEditingMenuDisabled(shadowChild1, inspector);
info ("Checking the <select> shadow element");
let shadowChild2 = children.nodes[2];
yield isEditingMenuDisabled(shadowChild2, inspector);
});

View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Anonymous content test</title>
<style type="text/css">
#pseudo::before {
content: "before";
}
#pseudo::after {
content: "after";
}
#shadow::before {
content: "Testing ::before on a shadow host";
}
</style>
</head>
<body>
<div id="pseudo"><span>middle</span></div>
<div id="shadow">light dom</div>
<script>
var host = document.querySelector('#shadow');
if (host.createShadowRoot) {
var root = host.createShadowRoot();
root.innerHTML = '<h3>Shadow DOM</h3><select multiple></select>';
}
</script>
</body>
</html>

View File

@ -8,6 +8,7 @@ let TargetFactory = devtools.TargetFactory;
let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
let promise = devtools.require("devtools/toolkit/deprecated-sync-thenables");
let {getInplaceEditorForSpan: inplaceEditor} = devtools.require("devtools/shared/inplace-editor");
let clipboard = devtools.require("sdk/clipboard");
// All test are asynchronous
waitForExplicitFinish();
@ -30,6 +31,7 @@ registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.inspector.activeSidebar");
Services.prefs.clearUserPref("devtools.dump.emit");
Services.prefs.clearUserPref("devtools.markup.pagesize");
Services.prefs.clearUserPref("dom.webcomponents.enabled");
});
// Auto close the toolbox and close the test tabs when the test ends
@ -143,12 +145,15 @@ function getNode(nodeOrSelector) {
/**
* Get the NodeFront for a given css selector, via the protocol
* @param {String} selector
* @param {String|NodeFront} selector
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @return {Promise} Resolves to the NodeFront instance
*/
function getNodeFront(selector, {walker}) {
if (selector._form) {
return selector;
}
return walker.querySelector(walker.rootNode, selector);
}
@ -173,7 +178,7 @@ function selectAndHighlightNode(nodeOrSelector, inspector) {
/**
* Set the inspector's current selection to the first match of the given css
* selector
* @param {String} selector
* @param {String|NodeFront} selector
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @param {String} reason Defaults to "test" which instructs the inspector not
@ -203,7 +208,7 @@ function getContainerForNodeFront(nodeFront, {markup}) {
/**
* Get the MarkupContainer object instance that corresponds to the given
* selector
* @param {String} selector
* @param {String|NodeFront} selector
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @return {MarkupContainer}
@ -236,7 +241,7 @@ function waitForChildrenUpdated({markup}) {
/**
* Simulate a mouse-over on the markup-container (a line in the markup-view)
* that corresponds to the selector passed.
* @param {String} selector
* @param {String|NodeFront} selector
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @return {Promise} Resolves when the container is hovered and the higlighter
@ -257,7 +262,7 @@ let hoverContainer = Task.async(function*(selector, inspector) {
/**
* Simulate a click on the markup-container (a line in the markup-view)
* that corresponds to the selector passed.
* @param {String} selector
* @param {String|NodeFront} selector
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @return {Promise} Resolves when the node has been selected.
@ -440,6 +445,124 @@ function wait(ms) {
return def.promise;
}
/**
* Wait for eventName on target.
* @param {Object} target An observable object that either supports on/off or
* addEventListener/removeEventListener
* @param {String} eventName
* @param {Boolean} useCapture Optional, for addEventListener/removeEventListener
* @return A promise that resolves when the event has been handled
*/
function once(target, eventName, useCapture=false) {
info("Waiting for event: '" + eventName + "' on " + target + ".");
let deferred = promise.defer();
for (let [add, remove] of [
["addEventListener", "removeEventListener"],
["addListener", "removeListener"],
["on", "off"]
]) {
if ((add in target) && (remove in target)) {
target[add](eventName, function onEvent(...aArgs) {
info("Got event: '" + eventName + "' on " + target + ".");
target[remove](eventName, onEvent, useCapture);
deferred.resolve.apply(deferred, aArgs);
}, useCapture);
break;
}
}
return deferred.promise;
}
/**
* Check to see if the inspector menu items for editing are disabled.
* Things like Edit As HTML, Delete Node, etc.
* @param {NodeFront} nodeFront
* @param {InspectorPanel} inspector
* @param {Boolean} assert Should this function run assertions inline.
* @return A promise that resolves with a boolean indicating whether
* the menu items are disabled once the menu has been checked.
*/
let isEditingMenuDisabled = Task.async(function*(nodeFront, inspector, assert=true) {
let deleteMenuItem = inspector.panelDoc.getElementById("node-menu-delete");
let editHTMLMenuItem = inspector.panelDoc.getElementById("node-menu-edithtml");
let pasteHTMLMenuItem = inspector.panelDoc.getElementById("node-menu-pasteouterhtml");
// To ensure clipboard contains something to paste.
clipboard.set("<p>test</p>", "html");
let menu = inspector.nodemenu;
yield selectNode(nodeFront, inspector);
yield reopenMenu(menu);
let isDeleteMenuDisabled = deleteMenuItem.hasAttribute("disabled");
let isEditHTMLMenuDisabled = editHTMLMenuItem.hasAttribute("disabled");
let isPasteHTMLMenuDisabled = pasteHTMLMenuItem.hasAttribute("disabled");
if (assert) {
ok(isDeleteMenuDisabled, "Delete menu item is disabled");
ok(isEditHTMLMenuDisabled, "Edit HTML menu item is disabled");
ok(isPasteHTMLMenuDisabled, "Paste HTML menu item is disabled");
}
return isDeleteMenuDisabled && isEditHTMLMenuDisabled && isPasteHTMLMenuDisabled;
});
/**
* Check to see if the inspector menu items for editing are enabled.
* Things like Edit As HTML, Delete Node, etc.
* @param {NodeFront} nodeFront
* @param {InspectorPanel} inspector
* @param {Boolean} assert Should this function run assertions inline.
* @return A promise that resolves with a boolean indicating whether
* the menu items are enabled once the menu has been checked.
*/
let isEditingMenuEnabled = Task.async(function*(nodeFront, inspector, assert=true) {
let deleteMenuItem = inspector.panelDoc.getElementById("node-menu-delete");
let editHTMLMenuItem = inspector.panelDoc.getElementById("node-menu-edithtml");
let pasteHTMLMenuItem = inspector.panelDoc.getElementById("node-menu-pasteouterhtml");
// To ensure clipboard contains something to paste.
clipboard.set("<p>test</p>", "html");
let menu = inspector.nodemenu;
yield selectNode(nodeFront, inspector);
yield reopenMenu(menu);
let isDeleteMenuDisabled = deleteMenuItem.hasAttribute("disabled");
let isEditHTMLMenuDisabled = editHTMLMenuItem.hasAttribute("disabled");
let isPasteHTMLMenuDisabled = pasteHTMLMenuItem.hasAttribute("disabled");
if (assert) {
ok(!isDeleteMenuDisabled, "Delete menu item is enabled");
ok(!isEditHTMLMenuDisabled, "Edit HTML menu item is enabled");
ok(!isPasteHTMLMenuDisabled, "Paste HTML menu item is enabled");
}
return !isDeleteMenuDisabled && !isEditHTMLMenuDisabled && !isPasteHTMLMenuDisabled;
});
/**
* Open a menu (closing it first if necessary).
* @param {DOMNode} menu A menu that implements hidePopup/openPopup
* @return a promise that resolves once the menu is opened.
*/
let reopenMenu = Task.async(function*(menu) {
// First close it is if it is already opened.
if (menu.state == "closing" || menu.state == "open") {
let popuphidden = once(menu, "popuphidden", true);
menu.hidePopup();
yield popuphidden;
}
// Then open it and return once
let popupshown = once(menu, "popupshown", true);
menu.openPopup();
yield popupshown;
});
/**
* Wait for all current promises to be resolved. See this as executeSoon that
* can be used with yield.

View File

@ -153,7 +153,9 @@ ElementStyle.prototype = {
// engine, we will set properties on a dummy element and observe
// how their .style attribute reflects them as computed values.
return this.dummyElementPromise = createDummyDocument().then(document => {
this.dummyElement = document.createElementNS(this.element.namespaceURI,
// ::before and ::after do not have a namespaceURI
let namespaceURI = this.element.namespaceURI || document.documentElement.namespaceURI;
this.dummyElement = document.createElementNS(namespaceURI,
this.element.tagName);
document.documentElement.appendChild(this.dummyElement);
return this.dummyElement;
@ -163,9 +165,7 @@ ElementStyle.prototype = {
destroy: function() {
this.dummyElement = null;
this.dummyElementPromise.then(dummyElement => {
if (dummyElement.parentNode) {
dummyElement.parentNode.removeChild(dummyElement);
}
dummyElement.remove();
this.dummyElementPromise = null;
}, console.error);
},
@ -1236,6 +1236,8 @@ CssRuleView.prototype = {
let accessKey = label + ".accessKey";
this.menuitemSources.setAttribute("accesskey",
_strings.GetStringFromName(accessKey));
this.menuitemAddRule.disabled = this.inspector.selection.isAnonymousNode();
},
/**
@ -1831,10 +1833,14 @@ function RuleEditor(aRuleView, aRule) {
RuleEditor.prototype = {
get isSelectorEditable() {
let toolbox = this.ruleView.inspector.toolbox;
return this.isEditable &&
let trait = this.isEditable &&
toolbox.target.client.traits.selectorEditable &&
this.rule.domRule.type !== ELEMENT_STYLE &&
this.rule.domRule.type !== Ci.nsIDOMCSSRule.KEYFRAME_RULE
this.rule.domRule.type !== Ci.nsIDOMCSSRule.KEYFRAME_RULE;
// Do not allow editing anonymousselectors until we can
// detect mutations on pseudo elements in Bug 1034110.
return trait && !this.rule.elementStyle.element.isAnonymous;
},
_create: function() {

View File

@ -34,6 +34,7 @@ support-files =
[browser_computedview_media-queries.js]
[browser_computedview_no-results-placeholder.js]
[browser_computedview_original-source-link.js]
[browser_computedview_pseudo-element_01.js]
[browser_computedview_refresh-on-style-change_01.js]
[browser_computedview_refresh-on-style-change_02.js]
[browser_computedview_search-filter.js]
@ -92,7 +93,8 @@ skip-if = (os == "win" && debug) || e10s # bug 963492: win. bug 1040653: e10s.
[browser_ruleview_multiple_properties_02.js]
[browser_ruleview_original-source-link.js]
[browser_ruleview_override.js]
[browser_ruleview_pseudo-element.js]
[browser_ruleview_pseudo-element_01.js]
[browser_ruleview_pseudo-element_02.js]
[browser_ruleview_refresh-on-attribute-change_01.js]
[browser_ruleview_refresh-on-attribute-change_02.js]
[browser_ruleview_refresh-on-style-change.js]

View File

@ -0,0 +1,41 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that pseudoelements are displayed correctly in the rule view
const TEST_URI = TEST_URL_ROOT + "doc_pseudoelement.html";
let test = asyncTest(function*() {
yield addTab(TEST_URI);
let {toolbox, inspector, view} = yield openComputedView();
yield testTopLeft(inspector, view);
});
function* testTopLeft(inspector, view) {
let node = yield getNodeFront("#topleft", inspector.markup);
yield selectNode(node, inspector);
let float = getComputedViewPropertyValue(view, "float");
is(float, "left", "The computed view shows the correct float");
let children = yield inspector.markup.walker.children(node);
is (children.nodes.length, 3, "Element has correct number of children");
let beforeElement = children.nodes[0];
yield selectNode(beforeElement, inspector);
let top = getComputedViewPropertyValue(view, "top");
is(top, "0px", "The computed view shows the correct top");
let left = getComputedViewPropertyValue(view, "left");
is(left, "0px", "The computed view shows the correct left");
let afterElement = children.nodes[children.nodes.length-1];
yield selectNode(afterElement, inspector);
top = getComputedViewPropertyValue(view, "top");
is(top, "50%", "The computed view shows the correct top");
left = getComputedViewPropertyValue(view, "left");
is(left, "50%", "The computed view shows the correct left");
}

View File

@ -5,15 +5,15 @@
"use strict";
// Testing selector inplace-editor behaviors in the rule-view with pseudo
// classes and elements
// classes.
let PAGE_CONTENT = [
'<style type="text/css">',
' .testclass {',
' text-align: center;',
' }',
' #testid3:after {',
' content: "+"',
' #testid3:first-letter {',
' text-decoration: "italic"',
' }',
'</style>',
'<div id="testid">Styled Node</div>',
@ -41,11 +41,11 @@ let test = asyncTest(function*() {
info("Selecting the test element");
yield selectNode("#testid3", inspector);
yield testEditSelector(view, ".testclass2:after");
yield testEditSelector(view, ".testclass2:first-letter");
info("Selecting the modified element");
yield selectNode(".testclass2", inspector);
yield checkModifiedElement(view, ".testclass2:after");
yield checkModifiedElement(view, ".testclass2:first-letter");
});
function* testEditSelector(view, name) {

View File

@ -28,10 +28,8 @@ function* testTopLeft(inspector, view) {
elementStyle
} = yield assertPseudoElementRulesNumbers(selector, inspector, view, {
elementRulesNb: 4,
afterRulesNb: 1,
beforeRulesNb: 2,
firstLineRulesNb: 0,
firstLetterRulesNb: 0,
firstLineRulesNb: 2,
firstLetterRulesNb: 1,
selectionRulesNb: 0
});
@ -50,74 +48,62 @@ function* testTopLeft(inspector, view) {
ok (!view.element.firstChild.classList.contains("show-expandable-container"), "Pseudo Elements are collapsed by dblclicking");
let defaultView = element.ownerDocument.defaultView;
let elementRule = rules.elementRules[0];
let elementRuleView = getRuleViewRuleEditor(view, 3);
let elementAfterRule = rules.afterRules[0];
let elementAfterRuleView = [].filter.call(view.element.children[1].children, (e) => {
return e._ruleEditor && e._ruleEditor.rule === elementAfterRule;
let elementFirstLineRule = rules.firstLineRules[0];
let elementFirstLineRuleView = [].filter.call(view.element.children[1].children, (e) => {
return e._ruleEditor && e._ruleEditor.rule === elementFirstLineRule;
})[0]._ruleEditor;
is
(
convertTextPropsToString(elementAfterRule.textProps),
"background: none repeat scroll 0% 0% red; content: \" \"; position: absolute; " +
"border-radius: 50%; height: 32px; width: 32px; top: 50%; left: 50%; margin-top: -16px; margin-left: -16px",
"TopLeft after properties are correct"
convertTextPropsToString(elementFirstLineRule.textProps),
"color: orange",
"TopLeft firstLine properties are correct"
);
let elementBeforeRule = rules.beforeRules[0];
let elementBeforeRuleView = [].filter.call(view.element.children[1].children, (e) => {
return e._ruleEditor && e._ruleEditor.rule === elementBeforeRule;
})[0]._ruleEditor;
let firstProp = elementFirstLineRuleView.addProperty("background-color", "rgb(0, 255, 0)", "");
let secondProp = elementFirstLineRuleView.addProperty("font-style", "italic", "");
is
(
convertTextPropsToString(elementBeforeRule.textProps),
"top: 0px; left: 0px",
"TopLeft before properties are correct"
);
let firstProp = elementAfterRuleView.addProperty("background-color", "rgb(0, 255, 0)", "");
let secondProp = elementAfterRuleView.addProperty("padding", "100px", "");
is (firstProp, elementAfterRule.textProps[elementAfterRule.textProps.length - 2],
is (firstProp, elementFirstLineRule.textProps[elementFirstLineRule.textProps.length - 2],
"First added property is on back of array");
is (secondProp, elementAfterRule.textProps[elementAfterRule.textProps.length - 1],
is (secondProp, elementFirstLineRule.textProps[elementFirstLineRule.textProps.length - 1],
"Second added property is on back of array");
yield elementAfterRule._applyingModifications;
yield elementFirstLineRule._applyingModifications;
is((yield getComputedStyleProperty(selector, ":after", "background-color")),
is((yield getComputedStyleProperty(selector, ":first-line", "background-color")),
"rgb(0, 255, 0)", "Added property should have been used.");
is((yield getComputedStyleProperty(selector, ":after", "padding-top")),
"100px", "Added property should have been used.");
is((yield getComputedStyleProperty(selector, null, "padding-top")),
"32px", "Added property should not apply to element");
is((yield getComputedStyleProperty(selector, ":first-line", "font-style")),
"italic", "Added property should have been used.");
is((yield getComputedStyleProperty(selector, null, "text-decoration")),
"none", "Added property should not apply to element");
secondProp.setEnabled(false);
yield elementAfterRule._applyingModifications;
firstProp.setEnabled(false);
yield elementFirstLineRule._applyingModifications;
is((yield getComputedStyleProperty(selector, ":after", "padding-top")), "0px",
"Disabled property should have been used.");
is((yield getComputedStyleProperty(selector, null, "padding-top")), "32px",
"Added property should not apply to element");
is((yield getComputedStyleProperty(selector, ":first-line", "background-color")),
"rgb(255, 0, 0)", "Disabled property should now have been used.");
is((yield getComputedStyleProperty(selector, null, "background-color")),
"rgb(221, 221, 221)", "Added property should not apply to element");
secondProp.setEnabled(true);
yield elementAfterRule._applyingModifications;
firstProp.setEnabled(true);
yield elementFirstLineRule._applyingModifications;
is((yield getComputedStyleProperty(selector, ":after", "padding-top")), "100px",
"Enabled property should have been used.");
is((yield getComputedStyleProperty(selector, null, "padding-top")), "32px",
"Added property should not apply to element");
is((yield getComputedStyleProperty(selector, ":first-line", "background-color")),
"rgb(0, 255, 0)", "Added property should have been used.");
is((yield getComputedStyleProperty(selector, null, "text-decoration")),
"none", "Added property should not apply to element");
firstProp = elementRuleView.addProperty("background-color", "rgb(0, 0, 255)", "");
yield elementRule._applyingModifications;
is((yield getComputedStyleProperty(selector, null, "background-color")), "rgb(0, 0, 255)",
"Added property should have been used.");
is((yield getComputedStyleProperty(selector, ":after", "background-color")), "rgb(0, 255, 0)",
"Added prop does not apply to pseudo");
is((yield getComputedStyleProperty(selector, null, "background-color")),
"rgb(0, 0, 255)", "Added property should have been used.");
is((yield getComputedStyleProperty(selector, ":first-line", "background-color")),
"rgb(0, 255, 0)", "Added prop does not apply to pseudo");
}
function* testTopRight(inspector, view) {
@ -127,10 +113,8 @@ function* testTopRight(inspector, view) {
elementStyle
} = yield assertPseudoElementRulesNumbers("#topright", inspector, view, {
elementRulesNb: 4,
afterRulesNb: 1,
beforeRulesNb: 2,
firstLineRulesNb: 0,
firstLetterRulesNb: 0,
firstLineRulesNb: 1,
firstLetterRulesNb: 1,
selectionRulesNb: 0
});
@ -146,10 +130,8 @@ function* testTopRight(inspector, view) {
function* testBottomRight(inspector, view) {
yield assertPseudoElementRulesNumbers("#bottomright", inspector, view, {
elementRulesNb: 4,
afterRulesNb: 1,
beforeRulesNb: 3,
firstLineRulesNb: 0,
firstLetterRulesNb: 0,
firstLineRulesNb: 1,
firstLetterRulesNb: 1,
selectionRulesNb: 0
});
}
@ -157,10 +139,8 @@ function* testBottomRight(inspector, view) {
function* testBottomLeft(inspector, view) {
yield assertPseudoElementRulesNumbers("#bottomleft", inspector, view, {
elementRulesNb: 4,
afterRulesNb: 1,
beforeRulesNb: 2,
firstLineRulesNb: 0,
firstLetterRulesNb: 0,
firstLineRulesNb: 1,
firstLetterRulesNb: 1,
selectionRulesNb: 0
});
}
@ -172,8 +152,6 @@ function* testParagraph(inspector, view) {
elementStyle
} = yield assertPseudoElementRulesNumbers("#bottomleft p", inspector, view, {
elementRulesNb: 3,
afterRulesNb: 0,
beforeRulesNb: 0,
firstLineRulesNb: 1,
firstLetterRulesNb: 1,
selectionRulesNb: 1
@ -241,8 +219,6 @@ function* assertPseudoElementRulesNumbers(selector, inspector, view, ruleNbs) {
let rules = {
elementRules: elementStyle.rules.filter(rule => !rule.pseudoElement),
afterRules: elementStyle.rules.filter(rule => rule.pseudoElement === ":after"),
beforeRules: elementStyle.rules.filter(rule => rule.pseudoElement === ":before"),
firstLineRules: elementStyle.rules.filter(rule => rule.pseudoElement === ":first-line"),
firstLetterRules: elementStyle.rules.filter(rule => rule.pseudoElement === ":first-letter"),
selectionRules: elementStyle.rules.filter(rule => rule.pseudoElement === ":-moz-selection")
@ -250,10 +226,6 @@ function* assertPseudoElementRulesNumbers(selector, inspector, view, ruleNbs) {
is(rules.elementRules.length, ruleNbs.elementRulesNb, selector +
" has the correct number of non pseudo element rules");
is(rules.afterRules.length, ruleNbs.afterRulesNb, selector +
" has the correct number of :after rules");
is(rules.beforeRules.length, ruleNbs.beforeRulesNb, selector +
" has the correct number of :before rules");
is(rules.firstLineRules.length, ruleNbs.firstLineRulesNb, selector +
" has the correct number of :first-line rules");
is(rules.firstLetterRules.length, ruleNbs.firstLetterRulesNb, selector +
@ -270,5 +242,6 @@ function assertGutters(view) {
is (gutters[0].textContent, "Pseudo-elements", "Gutter heading is correct");
is (gutters[1].textContent, "This Element", "Gutter heading is correct");
is (gutters[2].textContent, "Inherited from body", "Gutter heading is correct");
return gutters;
}
}

View File

@ -0,0 +1,32 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that pseudoelements are displayed correctly in the rule view
const TEST_URI = TEST_URL_ROOT + "doc_pseudoelement.html";
let test = asyncTest(function*() {
yield addTab(TEST_URI);
let {toolbox, inspector, view} = yield openRuleView();
yield testTopLeft(inspector, view);
});
function* testTopLeft(inspector, view) {
let node = inspector.markup.walker.frontForRawNode(getNode("#topleft"));
let children = yield inspector.markup.walker.children(node);
is (children.nodes.length, 3, "Element has correct number of children");
let beforeElement = children.nodes[0];
is (beforeElement.tagName, "_moz_generated_content_before", "tag name is correct");
yield selectNode(beforeElement, inspector);
let afterElement = children.nodes[children.nodes.length-1];
is (afterElement.tagName, "_moz_generated_content_after", "tag name is correct");
yield selectNode(afterElement, inspector);
}

View File

@ -18,6 +18,15 @@ body {
position:relative;
}
.box:first-line {
color: orange;
background: red;
}
.box:first-letter {
color: green;
}
* {
cursor: default;
}
@ -69,6 +78,13 @@ p:first-letter {
left:0;
}
.topleft:first-line {
color: orange;
}
.topleft::selection {
color: orange;
}
.topright:before {
top:0;
right:0;

View File

@ -172,19 +172,25 @@ let selectAndHighlightNode = Task.async(function*(selector, inspector) {
yield updated;
});
/**
* Set the inspector's current selection to a node that matches the given css
* selector.
* @param {String} selector
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
/*
* Set the inspector's current selection to a node or to the first match of the
* given css selector.
* @param {String|NodeFront}
* data The node to select
* @param {InspectorPanel} inspector
* The instance of InspectorPanel currently
* loaded in the toolbox
* @param {String} reason Defaults to "test" which instructs the inspector not
* to highlight the node upon selection
* @param {String} reason
* Defaults to "test" which instructs the inspector not
* to highlight the node upon selection
* @return {Promise} Resolves when the inspector is updated with the new node
*/
let selectNode = Task.async(function*(selector, inspector, reason="test") {
info("Selecting the node for '" + selector + "'");
let nodeFront = yield getNodeFront(selector, inspector);
let selectNode = Task.async(function*(data, inspector, reason="test") {
info("Selecting the node for '" + data + "'");
let nodeFront = data;
if (!data._form) {
nodeFront = yield getNodeFront(data, inspector);
}
let updated = inspector.once("inspector-updated");
inspector.selection.setNodeFront(nodeFront, reason);
yield updated;

View File

@ -163,7 +163,7 @@ function stringify(aThing, aAllowNewLines) {
function debugElement(aElement) {
return "<" + aElement.tagName +
(aElement.id ? "#" + aElement.id : "") +
(aElement.className ?
(aElement.className && aElement.className.split ?
"." + aElement.className.split(" ").join(" .") :
"") +
">";

View File

@ -498,3 +498,112 @@ LayoutHelpers.prototype = {
};
}
};
/**
* Traverse getBindingParent until arriving upon the bound element
* responsible for the generation of the specified node.
* See https://developer.mozilla.org/en-US/docs/XBL/XBL_1.0_Reference/DOM_Interfaces#getBindingParent.
*
* @param {DOMNode} node
* @return {DOMNode}
* If node is not anonymous, this will return node. Otherwise,
* it will return the bound element
*
*/
LayoutHelpers.getRootBindingParent = function(node) {
let parent;
let doc = node.ownerDocument;
if (!doc) {
return node;
}
while ((parent = doc.getBindingParent(node))) {
node = parent;
}
return node;
};
LayoutHelpers.getBindingParent = function(node) {
let doc = node.ownerDocument;
if (!doc) {
return false;
}
// If there is no binding parent then it is not anonymous.
let parent = doc.getBindingParent(node);
if (!parent) {
return false;
}
return parent;
}
/**
* Determine whether a node is anonymous by determining if there
* is a bindingParent.
*
* @param {DOMNode} node
* @return {Boolean}
*
*/
LayoutHelpers.isAnonymous = function(node) {
return LayoutHelpers.getRootBindingParent(node) !== node;
};
/**
* Determine whether a node is native anonymous content (as opposed
* to XBL anonymous or shadow DOM).
* Native anonymous content includes elements like internals to form
* controls and ::before/::after.
*
* @param {DOMNode} node
* @return {Boolean}
*
*/
LayoutHelpers.isNativeAnonymous = function(node) {
if (!LayoutHelpers.getBindingParent(node)) {
return false;
}
return !LayoutHelpers.isXBLAnonymous(node) &&
!LayoutHelpers.isShadowAnonymous(node);
};
/**
* Determine whether a node is XBL anonymous content (as opposed
* to native anonymous or shadow DOM).
* See https://developer.mozilla.org/en-US/docs/XBL/XBL_1.0_Reference/Anonymous_Content.
*
* @param {DOMNode} node
* @return {Boolean}
*
*/
LayoutHelpers.isXBLAnonymous = function(node) {
let parent = LayoutHelpers.getBindingParent(node);
if (!parent) {
return false;
}
// Shadow nodes also show up in getAnonymousNodes, so return false.
if (parent.shadowRoot && parent.shadowRoot.contains(node)) {
return false;
}
let anonNodes = [...node.ownerDocument.getAnonymousNodes(parent) || []];
return anonNodes.indexOf(node) > -1;
};
/**
* Determine whether a node is a child of a shadow root.
* See https://w3c.github.io/webcomponents/spec/shadow/
*
* @param {DOMNode} node
* @return {Boolean}
*/
LayoutHelpers.isShadowAnonymous = function(node) {
let parent = LayoutHelpers.getBindingParent(node);
if (!parent) {
return false;
}
// If there is a shadowRoot and this is part of it then this
// is not native anonymous
return parent.shadowRoot && parent.shadowRoot.contains(node);
};

View File

@ -11,6 +11,7 @@ const {Arg, Option, method} = protocol;
const events = require("sdk/event/core");
const Heritage = require("sdk/core/heritage");
const {CssLogic} = require("devtools/styleinspector/css-logic");
const EventEmitter = require("devtools/toolkit/event-emitter");
const GUIDE_STROKE_WIDTH = 1;
@ -831,8 +832,7 @@ BoxModelHighlighter.prototype = Heritage.extend(XULBasedHighlighter.prototype, {
}
if (!this._computedStyle) {
this._computedStyle =
this.currentNode.ownerDocument.defaultView.getComputedStyle(this.currentNode);
this._computedStyle = CssLogic.getComputedStyle(this.currentNode);
}
return this._computedStyle.getPropertyValue("display") !== "none";
@ -961,12 +961,13 @@ BoxModelHighlighter.prototype = Heritage.extend(XULBasedHighlighter.prototype, {
return;
}
let node = this.currentNode;
let info = this.nodeInfo;
let {bindingElement:node, pseudo} =
CssLogic.getBindingElementAndPseudo(this.currentNode);
// Update the tag, id, classes, pseudo-classes and dimensions only if they
// changed to avoid triggering paint events
let tagName = node.tagName;
if (info.tagNameLabel.textContent !== tagName) {
info.tagNameLabel.textContent = tagName;
@ -977,7 +978,7 @@ BoxModelHighlighter.prototype = Heritage.extend(XULBasedHighlighter.prototype, {
info.idLabel.textContent = id;
}
let classList = node.classList.length ? "." + [...node.classList].join(".") : "";
let classList = (node.classList || []).length ? "." + [...node.classList].join(".") : "";
if (info.classesBox.textContent !== classList) {
info.classesBox.textContent = classList;
}
@ -985,6 +986,12 @@ BoxModelHighlighter.prototype = Heritage.extend(XULBasedHighlighter.prototype, {
let pseudos = PSEUDO_CLASSES.filter(pseudo => {
return DOMUtils.hasPseudoClassLock(node, pseudo);
}, this).join("");
if (pseudo) {
// Display :after as ::after
pseudos += ":" + pseudo;
}
if (info.pseudoClassesBox.textContent !== pseudos) {
info.pseudoClassesBox.textContent = pseudos;
}
@ -1160,8 +1167,8 @@ CssTransformHighlighter.prototype = Heritage.extend(XULBasedHighlighter.prototyp
* Checks if the supplied node is transformed and not inline
*/
_isTransformed: function(node) {
let style = node.ownerDocument.defaultView.getComputedStyle(node);
return style.transform !== "none" && style.display !== "inline";
let style = CssLogic.getComputedStyle(node);
return style && (style.transform !== "none" && style.display !== "inline");
},
_setPolygonPoints: function(quad, poly) {
@ -1378,9 +1385,16 @@ function isNodeValid(node) {
return false;
}
// Is it connected to the document?
// Is the document inaccessible?
let doc = node.ownerDocument;
if (!doc || !doc.defaultView || !doc.documentElement.contains(node)) {
if (!doc || !doc.defaultView) {
return false;
}
// Is the node connected to the document? Using getBindingParent adds
// support for anonymous elements generated by a node in the document.
let bindingParent = LayoutHelpers.getRootBindingParent(node);
if (!doc.documentElement.contains(bindingParent)) {
return false;
}

View File

@ -76,6 +76,7 @@ const FONT_FAMILY_PREVIEW_TEXT_SIZE = 20;
const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
const XHTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
const IMAGE_FETCHING_TIMEOUT = 500;
// The possible completions to a ':' with added score to give certain values
// some preference.
@ -126,6 +127,8 @@ loader.lazyGetter(this, "eventListenerService", function() {
.getService(Ci.nsIEventListenerService);
});
loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic);
// XXX: A poor man's makeInfallible until we move it out of transport.js
// Which should be very soon.
function makeInfallible(handler) {
@ -210,14 +213,6 @@ var NodeActor = exports.NodeActor = protocol.ActorClass({
let parentNode = this.walker.parentNode(this);
// Estimate the number of children.
let numChildren = this.rawNode.childNodes.length;
if (numChildren === 0 &&
(this.rawNode.contentDocument || this.rawNode.getSVGDocument)) {
// This might be an iframe with virtual children.
numChildren = 1;
}
let form = {
actor: this.actorID,
baseURI: this.rawNode.baseURI,
@ -225,7 +220,7 @@ var NodeActor = exports.NodeActor = protocol.ActorClass({
nodeType: this.rawNode.nodeType,
namespaceURI: this.rawNode.namespaceURI,
nodeName: this.rawNode.nodeName,
numChildren: numChildren,
numChildren: this.numChildren,
// doctype attributes
name: this.rawNode.name,
@ -233,7 +228,12 @@ var NodeActor = exports.NodeActor = protocol.ActorClass({
systemId: this.rawNode.systemId,
attrs: this.writeAttrs(),
isBeforePseudoElement: this.isBeforePseudoElement,
isAfterPseudoElement: this.isAfterPseudoElement,
isAnonymous: LayoutHelpers.isAnonymous(this.rawNode),
isNativeAnonymous: LayoutHelpers.isNativeAnonymous(this.rawNode),
isXBLAnonymous: LayoutHelpers.isXBLAnonymous(this.rawNode),
isShadowAnonymous: LayoutHelpers.isShadowAnonymous(this.rawNode),
pseudoClassLocks: this.writePseudoClassLocks(),
isDisplayed: this.isDisplayed,
@ -259,14 +259,50 @@ var NodeActor = exports.NodeActor = protocol.ActorClass({
return form;
},
get computedStyle() {
if (Cu.isDeadWrapper(this.rawNode) ||
this.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE ||
!this.rawNode.ownerDocument ||
!this.rawNode.ownerDocument.defaultView) {
return null;
get isBeforePseudoElement() {
return this.rawNode.nodeName === "_moz_generated_content_before"
},
get isAfterPseudoElement() {
return this.rawNode.nodeName === "_moz_generated_content_after"
},
// Estimate the number of children that the walker will return without making
// a call to children() if possible.
get numChildren() {
// For pseudo elements, childNodes.length returns 1, but the walker
// will return 0.
if (this.isBeforePseudoElement || this.isAfterPseudoElement) {
return 0;
}
return this.rawNode.ownerDocument.defaultView.getComputedStyle(this.rawNode);
let numChildren = this.rawNode.childNodes.length;
if (numChildren === 0 &&
(this.rawNode.contentDocument || this.rawNode.getSVGDocument)) {
// This might be an iframe with virtual children.
numChildren = 1;
}
// Count any anonymous children
if (this.rawNode.nodeType === Ci.nsIDOMNode.ELEMENT_NODE) {
let anonChildren = this.rawNode.ownerDocument.getAnonymousNodes(this.rawNode);
if (anonChildren) {
numChildren += anonChildren.length;
}
}
// Normal counting misses ::before/::after, so we have to check to make sure
// we aren't missing anything
if (numChildren === 0) {
numChildren = this.walker.children(this).nodes.length;
}
return numChildren;
},
get computedStyle() {
return CssLogic.getComputedStyle(this.rawNode);
},
/**
@ -751,9 +787,13 @@ let NodeFront = protocol.FrontClass(NodeActor, {
get hasChildren() this._form.numChildren > 0,
get numChildren() this._form.numChildren,
get hasEventListeners() this._form.hasEventListeners,
get isBeforePseudoElement() this._form.isBeforePseudoElement,
get isAfterPseudoElement() this._form.isAfterPseudoElement,
get isPseudoElement() this.isBeforePseudoElement || this.isAfterPseudoElement,
get isAnonymous() this._form.isAnonymous,
get tagName() this.nodeType === Ci.nsIDOMNode.ELEMENT_NODE ? this.nodeName : null,
get shortValue() this._form.shortValue,
get incompleteValue() !!this._form.incompleteValue,
@ -1299,7 +1339,7 @@ var WalkerActor = protocol.ActorClass({
* document as the node.
*/
parents: method(function(node, options={}) {
let walker = documentWalker(node.rawNode, this.rootWin);
let walker = DocumentWalker(node.rawNode, this.rootWin);
let parents = [];
let cur;
while((cur = walker.parentNode())) {
@ -1320,7 +1360,7 @@ var WalkerActor = protocol.ActorClass({
}),
parentNode: function(node) {
let walker = documentWalker(node.rawNode, this.rootWin);
let walker = DocumentWalker(node.rawNode, this.rootWin);
let parent = walker.parentNode();
if (parent) {
return this._ref(parent);
@ -1381,7 +1421,7 @@ var WalkerActor = protocol.ActorClass({
this._retainedOrphans.delete(node);
}
let walker = documentWalker(node.rawNode, this.rootWin);
let walker = DocumentWalker(node.rawNode, this.rootWin);
let child = walker.firstChild();
while (child) {
@ -1408,7 +1448,7 @@ var WalkerActor = protocol.ActorClass({
if (!node) {
return newParents;
}
let walker = documentWalker(node.rawNode, this.rootWin);
let walker = DocumentWalker(node.rawNode, this.rootWin);
let cur;
while ((cur = walker.parentNode())) {
let parent = this._refMap.get(cur);
@ -1458,14 +1498,14 @@ var WalkerActor = protocol.ActorClass({
// We're going to create a few document walkers with the same filter,
// make it easier.
let filteredWalker = (node) => {
return documentWalker(node, this.rootWin, options.whatToShow);
};
let getFilteredWalker = (node) => {
return new DocumentWalker(node, this.rootWin, options.whatToShow);
}
// Need to know the first and last child.
let rawNode = node.rawNode;
let firstChild = filteredWalker(rawNode).firstChild();
let lastChild = filteredWalker(rawNode).lastChild();
let firstChild = getFilteredWalker(rawNode).firstChild();
let lastChild = getFilteredWalker(rawNode).lastChild();
if (!firstChild) {
// No children, we're done.
@ -1484,7 +1524,7 @@ var WalkerActor = protocol.ActorClass({
let nodes = [];
// Start by reading backward from the starting point if we're centering...
let backwardWalker = filteredWalker(start);
let backwardWalker = getFilteredWalker(start);
if (start != firstChild && options.center) {
backwardWalker.previousSibling();
let backwardCount = Math.floor(maxNodes / 2);
@ -1493,7 +1533,7 @@ var WalkerActor = protocol.ActorClass({
}
// Then read forward by any slack left in the max children...
let forwardWalker = filteredWalker(start);
let forwardWalker = getFilteredWalker(start);
let forwardCount = maxNodes - nodes.length;
nodes = nodes.concat(this._readForward(forwardWalker, forwardCount));
@ -1542,7 +1582,7 @@ var WalkerActor = protocol.ActorClass({
* nodes: Child nodes returned by the request.
*/
siblings: method(function(node, options={}) {
let parentNode = documentWalker(node.rawNode, this.rootWin).parentNode();
let parentNode = DocumentWalker(node.rawNode, this.rootWin).parentNode();
if (!parentNode) {
return {
hasFirst: true,
@ -1568,7 +1608,7 @@ var WalkerActor = protocol.ActorClass({
* https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
*/
nextSibling: method(function(node, options={}) {
let walker = documentWalker(node.rawNode, this.rootWin, options.whatToShow || Ci.nsIDOMNodeFilter.SHOW_ALL);
let walker = DocumentWalker(node.rawNode, this.rootWin, options.whatToShow || Ci.nsIDOMNodeFilter.SHOW_ALL);
let sibling = walker.nextSibling();
return sibling ? this._ref(sibling) : null;
}, traversalMethod),
@ -1583,7 +1623,7 @@ var WalkerActor = protocol.ActorClass({
* https://developer.mozilla.org/en-US/docs/Web/API/NodeFilter.
*/
previousSibling: method(function(node, options={}) {
let walker = documentWalker(node.rawNode, this.rootWin, options.whatToShow || Ci.nsIDOMNodeFilter.SHOW_ALL);
let walker = DocumentWalker(node.rawNode, this.rootWin, options.whatToShow || Ci.nsIDOMNodeFilter.SHOW_ALL);
let sibling = walker.previousSibling();
return sibling ? this._ref(sibling) : null;
}, traversalMethod),
@ -1821,7 +1861,7 @@ var WalkerActor = protocol.ActorClass({
return;
}
let walker = documentWalker(node.rawNode, this.rootWin);
let walker = DocumentWalker(node.rawNode, this.rootWin);
let cur;
while ((cur = walker.parentNode())) {
let curNode = this._ref(cur);
@ -1902,7 +1942,7 @@ var WalkerActor = protocol.ActorClass({
return;
}
let walker = documentWalker(node.rawNode, this.rootWin);
let walker = DocumentWalker(node.rawNode, this.rootWin);
let cur;
while ((cur = walker.parentNode())) {
let curNode = this._ref(cur);
@ -2175,7 +2215,8 @@ var WalkerActor = protocol.ActorClass({
let mutation = {
type: change.type,
target: targetActor.actorID,
}
numChildren: targetActor.numChildren
};
if (mutation.type === "attributes") {
mutation.attributeName = change.attributeName;
@ -2218,7 +2259,7 @@ var WalkerActor = protocol.ActorClass({
this._orphaned.delete(addedActor);
addedActors.push(addedActor.actorID);
}
mutation.numChildren = change.target.childNodes.length;
mutation.removed = removedActors;
mutation.added = addedActors;
}
@ -2307,7 +2348,7 @@ var WalkerActor = protocol.ActorClass({
target: documentActor.actorID
});
let walker = documentWalker(doc, this.rootWin);
let walker = DocumentWalker(doc, this.rootWin);
let parentNode = walker.parentNode();
if (parentNode) {
// Send a childList mutation on the frame so that clients know
@ -2332,7 +2373,7 @@ var WalkerActor = protocol.ActorClass({
* document fragment
*/
_isInDOMTree: function(rawNode) {
let walker = documentWalker(rawNode, this.rootWin);
let walker = DocumentWalker(rawNode, this.rootWin);
let current = walker.currentNode;
// Reaching the top of tree
@ -2645,7 +2686,13 @@ var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
// ids with front in the mutation record.
emittedMutation.added = addedFronts;
emittedMutation.removed = removedFronts;
targetFront._form.numChildren = change.numChildren;
// If this is coming from a DOM mutation, the actor's numChildren
// was passed in. Otherwise, it is simulated from a frame load or
// unload, so don't change the front's form.
if ('numChildren' in change) {
targetFront._form.numChildren = change.numChildren;
}
} else if (change.type === "frameLoad") {
// Nothing we need to do here, except verify that we don't have any
// document children, because we should have gotten a documentUnload
@ -2996,32 +3043,38 @@ var InspectorFront = exports.InspectorFront = protocol.FrontClass(InspectorActor
})
});
function documentWalker(node, rootWin, whatToShow=Ci.nsIDOMNodeFilter.SHOW_ALL) {
return new DocumentWalker(node, rootWin, whatToShow, whitespaceTextFilter, false);
}
// Exported for test purposes.
exports._documentWalker = documentWalker;
exports._documentWalker = DocumentWalker;
function nodeDocument(node) {
return node.ownerDocument || (node.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE ? node : null);
}
/**
* Similar to a TreeWalker, except will dig in to iframes and it doesn't
* implement the good methods like previousNode and nextNode.
* Wrapper for inDeepTreeWalker. Adds filtering to the traversal methods.
* See inDeepTreeWalker for more information about the methods.
*
* See TreeWalker documentation for explanations of the methods.
* @param {DOMNode} aNode
* @param {Window} aRootWin
* @param {Int} aShow See Ci.nsIDOMNodeFilter / inIDeepTreeWalker for options.
* @param {Function} aFilter A custom filter function Taking in a DOMNode
* and returning an Int. See nodeFilter for an example.
*/
function DocumentWalker(aNode, aRootWin, aShow, aFilter, aExpandEntityReferences) {
function DocumentWalker(aNode, aRootWin, aShow=Ci.nsIDOMNodeFilter.SHOW_ALL,
aFilter=nodeFilter) {
if (!(this instanceof DocumentWalker)) {
return new DocumentWalker(aNode, aRootWin, aShow, aFilter);
}
if (!aRootWin.location) {
throw new Error("Got an invalid root window in DocumentWalker");
}
let doc = nodeDocument(aNode);
this.layoutHelpers = new LayoutHelpers(aRootWin);
this.walker = doc.createTreeWalker(doc,
aShow, aFilter, aExpandEntityReferences);
this.walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].createInstance(Ci.inIDeepTreeWalker);
this.walker.showAnonymousContent = true;
this.walker.showSubDocuments = true;
this.walker.showDocumentsAsNodes = true;
this.walker.init(aRootWin.document, aShow);
this.walker.currentNode = aNode;
this.filter = aFilter;
}
@ -3029,85 +3082,88 @@ function DocumentWalker(aNode, aRootWin, aShow, aFilter, aExpandEntityReferences
DocumentWalker.prototype = {
get node() this.walker.node,
get whatToShow() this.walker.whatToShow,
get expandEntityReferences() this.walker.expandEntityReferences,
get currentNode() this.walker.currentNode,
set currentNode(aVal) this.walker.currentNode = aVal,
/**
* Called when the new node is in a different document than
* the current node, creates a new treewalker for the document we've
* run in to.
*/
_reparentWalker: function(aNewNode) {
if (!aNewNode) {
return null;
}
let doc = nodeDocument(aNewNode);
let walker = doc.createTreeWalker(doc,
this.whatToShow, this.filter, this.expandEntityReferences);
walker.currentNode = aNewNode;
this.walker = walker;
return aNewNode;
},
parentNode: function() {
let currentNode = this.walker.currentNode;
let parentNode = this.walker.parentNode();
if (!parentNode) {
if (currentNode && currentNode.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE
&& currentNode.defaultView) {
let window = currentNode.defaultView;
let frame = this.layoutHelpers.getFrameElement(window);
if (frame) {
return this._reparentWalker(frame);
}
}
return null;
}
return parentNode;
return this.walker.parentNode();
},
firstChild: function() {
let node = this.walker.currentNode;
if (!node)
return null;
if (node.contentDocument) {
return this._reparentWalker(node.contentDocument);
} else if (node.getSVGDocument && node.getSVGDocument()) {
return this._reparentWalker(node.getSVGDocument());
let firstChild = this.walker.firstChild();
while (firstChild && this.filter(firstChild) === Ci.nsIDOMNodeFilter.FILTER_SKIP) {
firstChild = this.walker.nextSibling();
}
return this.walker.firstChild();
return firstChild;
},
lastChild: function() {
let node = this.walker.currentNode;
if (!node)
return null;
if (node.contentDocument) {
return this._reparentWalker(node.contentDocument);
} else if (node.getSVGDocument && node.getSVGDocument()) {
return this._reparentWalker(node.getSVGDocument());
let lastChild = this.walker.lastChild();
while (lastChild && this.filter(lastChild) === Ci.nsIDOMNodeFilter.FILTER_SKIP) {
lastChild = this.walker.previousSibling();
}
return this.walker.lastChild();
return lastChild;
},
previousSibling: function DW_previousSibling() this.walker.previousSibling(),
nextSibling: function DW_nextSibling() this.walker.nextSibling()
previousSibling: function() {
let node = this.walker.previousSibling();
while (node && this.filter(node) === Ci.nsIDOMNodeFilter.FILTER_SKIP) {
node = this.walker.previousSibling();
}
return node;
},
nextSibling: function() {
let node = this.walker.nextSibling();
while (node && this.filter(node) === Ci.nsIDOMNodeFilter.FILTER_SKIP) {
node = this.walker.nextSibling();
}
return node;
}
};
function isXULDocument(doc) {
return doc &&
doc.documentElement &&
doc.documentElement.namespaceURI === XUL_NS;
}
/**
* A tree walker filter for avoiding empty whitespace text nodes.
*/
function whitespaceTextFilter(aNode) {
if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE &&
!/[^\s]/.exec(aNode.nodeValue)) {
return Ci.nsIDOMNodeFilter.FILTER_SKIP;
} else {
return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
}
function nodeFilter(aNode) {
// Ignore empty whitespace text nodes.
if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE &&
!/[^\s]/.exec(aNode.nodeValue)) {
return Ci.nsIDOMNodeFilter.FILTER_SKIP;
}
// Ignore all native anonymous content (like internals for form
// controls). Except for:
// 1) Anonymous content in a XUL document. This is needed for all
// elements within the Browser Toolbox to properly show up.
// 2) ::before/::after - we do want this to show in the walker so
// they can be inspected.
if (LayoutHelpers.isNativeAnonymous(aNode) &&
!isXULDocument(aNode.ownerDocument) &&
(
aNode.nodeName !== "_moz_generated_content_before" &&
aNode.nodeName !== "_moz_generated_content_after")
) {
return Ci.nsIDOMNodeFilter.FILTER_SKIP;
}
return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
}
/**

View File

@ -28,6 +28,12 @@ exports.ELEMENT_STYLE = ELEMENT_STYLE;
const PSEUDO_ELEMENTS = [":first-line", ":first-letter", ":before", ":after", ":-moz-selection"];
exports.PSEUDO_ELEMENTS = PSEUDO_ELEMENTS;
// When gathering rules to read for pseudo elements, we will skip
// :before and :after, which are handled as a special case.
const PSEUDO_ELEMENTS_TO_READ = PSEUDO_ELEMENTS.filter(pseudo => {
return pseudo !== ":before" && pseudo !== ":after";
});
// Predeclare the domnode actor type for use in requests.
types.addActorType("domnode");
@ -303,7 +309,7 @@ var PageStyleActor = protocol.ActorClass({
*/
getApplied: method(function(node, options) {
let entries = [];
this.addElementRules(node.rawNode, undefined, options, entries);
entries = entries.concat(this._getAllElementRules(node, undefined, options));
return this.getAppliedProps(node, entries, options);
}, {
request: {
@ -322,66 +328,122 @@ var PageStyleActor = protocol.ActorClass({
},
/**
* Helper function for getApplied, adds all the rules from a given
* element.
* Helper function for getApplied, gets all the rules from a given
* element. See getApplied for documentation on parameters.
* @param NodeActor node
* @param bool inherited
* @param object options
* @return Array The rules for a given element. Each item in the
* array has the following signature:
* - rule RuleActor
* - isSystem Boolean
* - inherited Boolean
* - pseudoElement String
*/
addElementRules: function(element, inherited, options, rules) {
if (!element.style) {
return;
_getAllElementRules: function(node, inherited, options) {
let {bindingElement, pseudo} = CssLogic.getBindingElementAndPseudo(node.rawNode);
let rules = [];
if (!bindingElement || !bindingElement.style) {
return rules;
}
let elementStyle = this._styleRef(element);
let elementStyle = this._styleRef(bindingElement);
let showElementStyles = !inherited && !pseudo;
let showInheritedStyles = inherited && this._hasInheritedProps(bindingElement.style);
if (!inherited || this._hasInheritedProps(element.style)) {
// First any inline styles
if (showElementStyles) {
rules.push({
rule: elementStyle,
inherited: inherited,
});
}
let pseudoElements = inherited ? [null] : [null, ...PSEUDO_ELEMENTS];
for (let pseudo of pseudoElements) {
// Now any inherited styles
if (showInheritedStyles) {
rules.push({
rule: elementStyle,
inherited: inherited
});
}
// Get the styles that apply to the element.
let domRules = DOMUtils.getCSSStyleRules(element, pseudo);
// Add normal rules. Typically this is passing in the node passed into the
// function, unless if that node was ::before/::after. In which case,
// it will pass in the parentNode along with "::before"/"::after".
this._getElementRules(bindingElement, pseudo, inherited, options).forEach((rule) => {
// The only case when there would be a pseudo here is ::before/::after,
// and in this case we want to tell the view that it belongs to the
// element (which is a _moz_generated_content native anonymous element).
rule.pseudoElement = null;
rules.push(rule);
});
if (!domRules) {
continue;
}
// getCSSStyleRules returns ordered from least-specific to
// most-specific.
for (let i = domRules.Count() - 1; i >= 0; i--) {
let domRule = domRules.GetElementAt(i);
let isSystem = !CssLogic.isContentStylesheet(domRule.parentStyleSheet);
if (isSystem && options.filter != CssLogic.FILTER.UA) {
continue;
}
if (inherited) {
// Don't include inherited rules if none of its properties
// are inheritable.
let hasInherited = Array.prototype.some.call(domRule.style, prop => {
return DOMUtils.isInheritedProperty(prop);
});
if (!hasInherited) {
continue;
}
}
let ruleActor = this._styleRef(domRule);
rules.push({
rule: ruleActor,
inherited: inherited,
pseudoElement: pseudo,
isSystem: isSystem
// Now any pseudos (except for ::before / ::after, which was handled as
// a 'normal rule' above.
if (showElementStyles) {
for (let pseudo of PSEUDO_ELEMENTS_TO_READ) {
this._getElementRules(bindingElement, pseudo, inherited, options).forEach((rule) => {
rules.push(rule);
});
}
}
return rules;
},
/**
* Helper function for _getAllElementRules, returns the rules from a given
* element. See getApplied for documentation on parameters.
* @param DOMNode node
* @param string pseudo
* @param DOMNode inherited
* @param object options
*
* @returns Array
*/
_getElementRules: function (node, pseudo, inherited, options) {
let domRules = DOMUtils.getCSSStyleRules(node, pseudo);
if (!domRules) {
return [];
}
let rules = [];
// getCSSStyleRules returns ordered from least-specific to
// most-specific.
for (let i = domRules.Count() - 1; i >= 0; i--) {
let domRule = domRules.GetElementAt(i);
let isSystem = !CssLogic.isContentStylesheet(domRule.parentStyleSheet);
if (isSystem && options.filter != CssLogic.FILTER.UA) {
continue;
}
if (inherited) {
// Don't include inherited rules if none of its properties
// are inheritable.
let hasInherited = [...domRule.style].some(
prop => DOMUtils.isInheritedProperty(prop)
);
if (!hasInherited) {
continue;
}
}
let ruleActor = this._styleRef(domRule);
rules.push({
rule: ruleActor,
inherited: inherited,
isSystem: isSystem,
pseudoElement: pseudo
});
}
return rules;
},
/**
* Helper function for getApplied and addNewRule that fetches a set of
* style properties that apply to the given node and associated rules
@ -407,7 +469,7 @@ var PageStyleActor = protocol.ActorClass({
if (options.inherited) {
let parent = this.walker.parentNode(node);
while (parent && parent.rawNode.nodeType != Ci.nsIDOMNode.DOCUMENT_NODE) {
this.addElementRules(parent.rawNode, parent, options, entries);
entries = entries.concat(this._getAllElementRules(parent, parent, options));
parent = this.walker.parentNode(parent);
}
}
@ -421,9 +483,11 @@ var PageStyleActor = protocol.ActorClass({
let domRule = entry.rule.rawRule;
let selectors = CssLogic.getSelectors(domRule);
let element = entry.inherited ? entry.inherited.rawNode : node.rawNode;
let {bindingElement,pseudo} = CssLogic.getBindingElementAndPseudo(element);
entry.matchedSelectors = [];
for (let i = 0; i < selectors.length; i++) {
if (DOMUtils.selectorMatchesElement(element, domRule, i)) {
if (DOMUtils.selectorMatchesElement(bindingElement, domRule, i, pseudo)) {
entry.matchedSelectors.push(selectors[i]);
}
}
@ -507,7 +571,7 @@ var PageStyleActor = protocol.ActorClass({
layout.height = Math.round(clientRect.height);
// We compute and update the values of margins & co.
let style = node.rawNode.ownerDocument.defaultView.getComputedStyle(node.rawNode);
let style = CssLogic.getComputedStyle(node.rawNode);
for (let prop of [
"position",
"margin-top",

View File

@ -49,6 +49,7 @@ skip-if = buildapp == 'mulet'
skip-if = buildapp == 'mulet'
[test_highlighter-selector_02.html]
skip-if = buildapp == 'mulet'
[test_inspector-anonymous.html]
[test_inspector-changeattrs.html]
[test_inspector-changevalue.html]
[test_inspector-hide.html]

View File

@ -3,6 +3,7 @@ var Cu = Components.utils;
Cu.import("resource://gre/modules/devtools/Loader.jsm");
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
Cu.import("resource://gre/modules/Task.jsm");
const Services = devtools.require("Services");
const {_documentWalker} = devtools.require("devtools/server/actors/inspector");
@ -290,6 +291,10 @@ function addTest(test) {
_tests.push(test);
}
function addAsyncTest(generator) {
_tests.push(() => Task.spawn(generator).then(null, ok.bind(null, false)));
}
function runNextTest() {
if (_tests.length == 0) {
SimpleTest.finish()

View File

@ -1,7 +1,31 @@
<html>
<head>
<meta charset="UTF-8">
<title>Inspector Traversal Test Data</title>
<style type="text/css">
#pseudo::before {
content: "before";
}
#pseudo::after {
content: "after";
}
#pseudo-empty::before {
content: "before an empty element";
}
#shadow::before {
content: "Testing ::before on a shadow host";
}
</style>
<script type="text/javascript">
window.onload = function() {
// Set up a basic shadow DOM
var host = document.querySelector('#shadow');
if (host.createShadowRoot) {
var root = host.createShadowRoot();
root.innerHTML = '<h3>Shadow <em>DOM</em></h3><select multiple></select>';
}
// Put a copy of the body in an iframe to test frame traversal.
var body = document.querySelector("body");
var data = "data:text/html,<html>" + body.outerHTML + "<html>";
@ -14,6 +38,7 @@
body.appendChild(iframe);
}
</script>
</head>
<body style="background-color:white">
<h1>Inspector Actor Tests</h1>
<span id="longstring">longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong</span>
@ -51,6 +76,13 @@
<div id="longlist-sibling-firstchild"></div>
</div>
<p id="edit-html"></p>
<select multiple><option>one</option><option>two</option></select>
<div id="pseudo"><span>middle</span></div>
<div id="pseudo-empty"></div>
<div id="shadow">light dom</div>
<object>
<div id="1"></div>
</object>

View File

@ -96,6 +96,46 @@ addTest(function findCssSelector() {
runNextTest();
});
addTest(function getComputedStyle() {
let node = document.querySelector("#computed-style");
is (CssLogic.getComputedStyle(node).getPropertyValue("width"),
"50px", "Computed style on a normal node works (width)");
is (CssLogic.getComputedStyle(node).getPropertyValue("height"),
"10px", "Computed style on a normal node works (height)");
let firstChild = _documentWalker(node, window).firstChild();
is (CssLogic.getComputedStyle(firstChild).getPropertyValue("content"),
"\"before\"", "Computed style on a ::before node works (content)");
let lastChild = _documentWalker(node, window).lastChild();
is (CssLogic.getComputedStyle(lastChild).getPropertyValue("content"),
"\"after\"", "Computed style on a ::after node works (content)");
runNextTest();
});
addTest(function getBindingElementAndPseudo() {
let node = document.querySelector("#computed-style");
var {bindingElement, pseudo} = CssLogic.getBindingElementAndPseudo(node);
is (bindingElement, node,
"Binding element is the node itself for a normal node");
ok (!pseudo, "Pseudo is null for a normal node");
let firstChild = _documentWalker(node, window).firstChild();
var {bindingElement, pseudo} = CssLogic.getBindingElementAndPseudo(firstChild);
is (bindingElement, node,
"Binding element is the parent for a pseudo node");
is (pseudo, ":before", "Pseudo is correct for a ::before node");
let lastChild = _documentWalker(node, window).lastChild();
var {bindingElement, pseudo} = CssLogic.getBindingElementAndPseudo(lastChild);
is (bindingElement, node,
"Binding element is the parent for a pseudo node");
is (pseudo, ":after", "Pseudo is correct for a ::after node");
runNextTest();
});
</script>
</head>
<body>
@ -121,5 +161,11 @@ addTest(function findCssSelector() {
<!-- Special characters -->
<div id="!, &quot;, #, $, %, &amp;, ', (, ), *, +, ,, -, ., /, :, ;, <, =, >, ?, @, [, \, ], ^, `, {, |, }, ~"></div>
</div>
<style type="text/css">
#computed-style { width: 50px; height: 10px; }
#computed-style::before { content: "before"; }
#computed-style::after { content: "after"; }
</style>
<div id="computed-style"></div>
</body>
</html>

View File

@ -0,0 +1,161 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=777674
-->
<head>
<meta charset="utf-8">
<title>Test for Bug 777674</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript;version=1.8" src="inspector-helpers.js"></script>
<script type="application/javascript;version=1.8">
window.onload = function() {
Components.utils.import("resource://gre/modules/devtools/Loader.jsm");
const {Promise: promise} =
Components.utils.import("resource://gre/modules/Promise.jsm", {});
const {InspectorFront} =
devtools.require("devtools/server/actors/inspector");
Services.prefs.setBoolPref("dom.webcomponents.enabled", true);
SimpleTest.waitForExplicitFinish();
SimpleTest.registerCleanupFunction(() => {
Services.prefs.clearUserPref("dom.webcomponents.enabled");
});
let gWalker = null;
let gClient = null;
addTest(function setup() {
info ("Setting up inspector and walker actors.");
let url = document.getElementById("inspectorContent").href;
attachURL(url, function(err, client, tab, doc) {
gInspectee = doc;
let inspector = InspectorFront(client, tab);
promiseDone(inspector.getWalker().then(walker => {
ok(walker, "getWalker() should return an actor.");
gClient = client;
gWalker = walker;
}).then(runNextTest));
});
});
addAsyncTest(function* testNativeAnonymous() {
info ("Testing native anonymous content with walker.");
let select = yield gWalker.querySelector(gWalker.rootNode, "select");
let children = yield gWalker.children(select);
is (select.numChildren, 2, "No native anon content for form control");
is (children.nodes.length, 2, "No native anon content for form control");
runNextTest();
});
addAsyncTest(function* testPseudoElements() {
info ("Testing pseudo elements with walker.");
// Markup looks like: <div><::before /><span /><::after /></div>
let pseudo = yield gWalker.querySelector(gWalker.rootNode, "#pseudo");
let children = yield gWalker.children(pseudo);
is (pseudo.numChildren, 1, "::before/::after are not counted if there is a child");
is (children.nodes.length, 3, "Correct number of children");
let before = children.nodes[0];
ok (before.isAnonymous, "Child is anonymous");
ok (!before._form.isXBLAnonymous, "Child is not XBL anonymous");
ok (!before._form.isShadowAnonymous, "Child is not shadow anonymous");
ok (before._form.isNativeAnonymous, "Child is native anonymous");
let span = children.nodes[1];
ok (!span.isAnonymous, "Child is not anonymous");
let after = children.nodes[2];
ok (after.isAnonymous, "Child is anonymous");
ok (!after._form.isXBLAnonymous, "Child is not XBL anonymous");
ok (!after._form.isShadowAnonymous, "Child is not shadow anonymous");
ok (after._form.isNativeAnonymous, "Child is native anonymous");
runNextTest();
});
addAsyncTest(function* testEmptyWithPseudo() {
info ("Testing elements with no childrent, except for pseudos.");
info ("Checking an element whose only child is a pseudo element");
let pseudo = yield gWalker.querySelector(gWalker.rootNode, "#pseudo-empty");
let children = yield gWalker.children(pseudo);
is (pseudo.numChildren, 1, "::before/::after are is counted if there are no other children");
is (children.nodes.length, 1, "Correct number of children");
let before = children.nodes[0];
ok (before.isAnonymous, "Child is anonymous");
ok (!before._form.isXBLAnonymous, "Child is not XBL anonymous");
ok (!before._form.isShadowAnonymous, "Child is not shadow anonymous");
ok (before._form.isNativeAnonymous, "Child is native anonymous");
runNextTest();
});
addAsyncTest(function* testShadowAnonymous() {
info ("Testing shadow DOM content.");
let shadow = yield gWalker.querySelector(gWalker.rootNode, "#shadow");
let children = yield gWalker.children(shadow);
is (shadow.numChildren, 3, "Children of the shadow root are counted");
is (children.nodes.length, 3, "Children returned from walker");
let before = children.nodes[0];
ok (before.isAnonymous, "Child is anonymous");
ok (!before._form.isXBLAnonymous, "Child is not XBL anonymous");
ok (!before._form.isShadowAnonymous, "Child is not shadow anonymous");
ok (before._form.isNativeAnonymous, "Child is native anonymous");
// <h3>Shadow <em>DOM</em></h3>
let shadowChild1 = children.nodes[1];
ok (shadowChild1.isAnonymous, "Child is anonymous");
ok (!shadowChild1._form.isXBLAnonymous, "Child is not XBL anonymous");
ok (shadowChild1._form.isShadowAnonymous, "Child is shadow anonymous");
ok (!shadowChild1._form.isNativeAnonymous, "Child is not native anonymous");
let shadowSubChildren = yield gWalker.children(children.nodes[1]);
is (shadowChild1.numChildren, 2, "Subchildren of the shadow root are counted");
is (shadowSubChildren.nodes.length, 2, "Subchildren are returned from walker");
// <em>DOM</em>
let shadowSubChild = children.nodes[1];
ok (shadowSubChild.isAnonymous, "Child is anonymous");
ok (!shadowSubChild._form.isXBLAnonymous, "Child is not XBL anonymous");
ok (shadowSubChild._form.isShadowAnonymous, "Child is shadow anonymous");
ok (!shadowSubChild._form.isNativeAnonymous, "Child is not native anonymous");
// <select multiple></select>
let shadowChild2 = children.nodes[2];
ok (shadowChild2.isAnonymous, "Child is anonymous");
ok (!shadowChild2._form.isXBLAnonymous, "Child is not XBL anonymous");
ok (shadowChild2._form.isShadowAnonymous, "Child is shadow anonymous");
ok (!shadowChild2._form.isNativeAnonymous, "Child is not native anonymous");
runNextTest();
});
runNextTest();
};
</script>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a>
<a id="inspectorContent" target="_blank" href="inspector-traversal-data.html">Test Document</a>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<pre id="test">
</pre>
</body>
</html>

View File

@ -54,6 +54,7 @@ const RX_PSEUDO = /\s*:?:([\w-]+)(\(?\)?)\s*/g;
// on the worker thread, where Cu is not available.
if (Cu) {
Cu.importGlobalProperties(['CSS']);
Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
}
function CssLogic()
@ -179,8 +180,7 @@ CssLogic.prototype = {
this._matchedRules = null;
this._matchedSelectors = null;
let win = this.viewedDocument.defaultView;
this._computedStyle = win.getComputedStyle(this.viewedElement, "");
this._computedStyle = CssLogic.getComputedStyle(this.viewedElement);
},
/**
@ -615,14 +615,19 @@ CssLogic.prototype = {
CssLogic.STATUS.MATCHED : CssLogic.STATUS.PARENT_MATCH;
try {
domRules = domUtils.getCSSStyleRules(element);
// Handle finding rules on pseudo by reading style rules
// on the parent node with proper pseudo arg to getCSSStyleRules.
let {bindingElement, pseudo} = CssLogic.getBindingElementAndPseudo(element);
domRules = domUtils.getCSSStyleRules(bindingElement, pseudo);
} catch (ex) {
Services.console.
logStringMessage("CL__buildMatchedRules error: " + ex);
continue;
}
for (let i = 0, n = domRules.Count(); i < n; i++) {
// getCSSStyleRules can return null with a shadow DOM element.
let numDomRules = domRules ? domRules.Count() : 0;
for (let i = 0; i < numDomRules; i++) {
let domRule = domRules.GetElementAt(i);
if (domRule.type !== Ci.nsIDOMCSSRule.STYLE_RULE) {
continue;
@ -754,6 +759,56 @@ CssLogic.getSelectors = function CssLogic_getSelectors(aDOMRule)
return selectors;
}
/**
* Given a node, check to see if it is a ::before or ::after element.
* If so, return the node that is accessible from within the document
* (the parent of the anonymous node), along with which pseudo element
* it was. Otherwise, return the node itself.
*
* @returns {Object}
* - {DOMNode} node The non-anonymous node
* - {string} pseudo One of ':before', ':after', or null.
*/
CssLogic.getBindingElementAndPseudo = function(node)
{
let bindingElement = node;
let pseudo = null;
if (node.nodeName == "_moz_generated_content_before") {
bindingElement = node.parentNode;
pseudo = ":before";
} else if (node.nodeName == "_moz_generated_content_after") {
bindingElement = node.parentNode;
pseudo = ":after";
}
return {
bindingElement: bindingElement,
pseudo: pseudo
};
};
/**
* Get the computed style on a node. Automatically handles reading
* computed styles on a ::before/::after element by reading on the
* parent node with the proper pseudo argument.
*
* @param {Node}
* @returns {CSSStyleDeclaration}
*/
CssLogic.getComputedStyle = function(node)
{
if (!node ||
Cu.isDeadWrapper(node) ||
node.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE ||
!node.ownerDocument ||
!node.ownerDocument.defaultView) {
return null;
}
let {bindingElement, pseudo} = CssLogic.getBindingElementAndPseudo(node);
return node.ownerDocument.defaultView.getComputedStyle(bindingElement, pseudo);
};
/**
* Memonized lookup of a l10n string from a string bundle.
* @param {string} aName The key to lookup.
@ -860,8 +915,9 @@ function positionInNodeList(element, nodeList) {
* and ele.ownerDocument.querySelectorAll(reply).length === 1
*/
CssLogic.findCssSelector = function CssLogic_findCssSelector(ele) {
ele = LayoutHelpers.getRootBindingParent(ele);
var document = ele.ownerDocument;
if (!document.contains(ele)) {
if (!document || !document.contains(ele)) {
throw new Error('findCssSelector received element not inside document');
}