mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
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:
parent
555152bf1f
commit
03ce1f7e86
@ -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++) {
|
||||
|
@ -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;
|
||||
},
|
||||
|
@ -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 = "";
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -131,7 +131,7 @@
|
||||
transition: background .5s;
|
||||
}
|
||||
|
||||
.tag-line .open, .tag-line .close, .comment {
|
||||
.tag-line {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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"><<!--
|
||||
--><span save="${tag}" class="tag theme-fg-color3" tabindex="0"></span><!--
|
||||
|
@ -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]
|
||||
|
@ -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);
|
||||
});
|
@ -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);
|
||||
}
|
||||
});
|
@ -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);
|
||||
});
|
31
browser/devtools/markupview/test/doc_markup_anonymous.html
Normal file
31
browser/devtools/markupview/test/doc_markup_anonymous.html
Normal 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>
|
@ -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.
|
||||
|
@ -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() {
|
||||
|
@ -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]
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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(" .") :
|
||||
"") +
|
||||
">";
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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",
|
||||
|
@ -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]
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
|
@ -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="!, ", #, $, %, &, ', (, ), *, +, ,, -, ., /, :, ;, <, =, >, ?, @, [, \, ], ^, `, {, |, }, ~"></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>
|
||||
|
@ -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>
|
@ -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');
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user