Bug 707740 - Ability to lock in a pseudo class in the page inspector. r=dcamp

This commit is contained in:
Heather Arthur 2012-02-24 16:01:29 -08:00
parent e6c503006c
commit edb51b91d5
8 changed files with 399 additions and 14 deletions

View File

@ -43,7 +43,11 @@
* ***** END LICENSE BLOCK ***** */
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
var EXPORTED_SYMBOLS = ["Highlighter"];
@ -59,6 +63,9 @@ const INSPECTOR_INVISIBLE_ELEMENTS = {
"title": true,
};
const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
// add ":visited" and ":link" after bug 713106 is fixed
/**
* A highlighter mechanism.
*
@ -109,6 +116,8 @@ const INSPECTOR_INVISIBLE_ELEMENTS = {
* "highlighting" - Highlighter is highlighting
* "locked" - The selected node has been locked
* "unlocked" - The selected ndoe has been unlocked
* "pseudoclasstoggled" - A pseudo-class lock has changed on the selected node
*
* Structure:
*
@ -238,6 +247,17 @@ Highlighter.prototype = {
}
},
/**
* Notify that a pseudo-class lock was toggled on the highlighted element
*
* @param aPseudo - The pseudo-class to toggle, e.g. ":hover".
*/
pseudoClassLockToggled: function Highlighter_pseudoClassLockToggled(aPseudo)
{
this.emitEvent("pseudoclasstoggled", [aPseudo]);
this.updateInfobar();
},
/**
* Update the highlighter size and position.
*/
@ -446,29 +466,80 @@ Highlighter.prototype = {
let classesBox = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span");
classesBox.id = "highlighter-nodeinfobar-classes";
let pseudoClassesBox = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span");
pseudoClassesBox.id = "highlighter-nodeinfobar-pseudo-classes";
// Add some content to force a better boundingClientRect down below.
classesBox.textContent = " ";
pseudoClassesBox.textContent = " ";
nodeInfobar.appendChild(tagNameLabel);
nodeInfobar.appendChild(idLabel);
nodeInfobar.appendChild(classesBox);
nodeInfobar.appendChild(pseudoClassesBox);
container.appendChild(arrowBoxTop);
container.appendChild(nodeInfobar);
container.appendChild(arrowBoxBottom);
aParent.appendChild(container);
nodeInfobar.onclick = (function _onInfobarRightClick(aEvent) {
if (aEvent.button == 2) {
this.openPseudoClassMenu();
}
}).bind(this);
let barHeight = container.getBoundingClientRect().height;
this.nodeInfo = {
tagNameLabel: tagNameLabel,
idLabel: idLabel,
classesBox: classesBox,
pseudoClassesBox: pseudoClassesBox,
container: container,
barHeight: barHeight,
};
},
/**
* Open the infobar's pseudo-class context menu.
*/
openPseudoClassMenu: function Highlighter_openPseudoClassMenu()
{
let menu = this.chromeDoc.createElement("menupopup");
menu.id = "infobar-context-menu";
let popupSet = this.chromeDoc.getElementById("mainPopupSet");
popupSet.appendChild(menu);
let fragment = this.buildPseudoClassMenu();
menu.appendChild(fragment);
menu.openPopup(this.nodeInfo.pseudoClassesBox, "end_before", 0, 0, true, false);
},
/**
* Create the menuitems for toggling the selection's pseudo-class state
*
* @returns DocumentFragment. The menuitems for toggling pseudo-classes.
*/
buildPseudoClassMenu: function IUI_buildPseudoClassesMenu()
{
let fragment = this.chromeDoc.createDocumentFragment();
for (let i = 0; i < PSEUDO_CLASSES.length; i++) {
let pseudo = PSEUDO_CLASSES[i];
let item = this.chromeDoc.createElement("menuitem");
item.setAttribute("type", "checkbox");
item.setAttribute("label", pseudo);
item.addEventListener("command",
this.pseudoClassLockToggled.bind(this, pseudo), false);
item.setAttribute("checked", DOMUtils.hasPseudoClassLock(this.node,
pseudo));
fragment.appendChild(item);
}
return fragment;
},
/**
* Highlight a rectangular region.
*
@ -543,6 +614,14 @@ Highlighter.prototype = {
classes.textContent = this.node.classList.length ?
"." + Array.join(this.node.classList, ".") : "";
// Pseudo-classes
let pseudos = PSEUDO_CLASSES.filter(function(pseudo) {
return DOMUtils.hasPseudoClassLock(this.node, pseudo);
}, this);
let pseudoBox = this.nodeInfo.pseudoClassesBox;
pseudoBox.textContent = pseudos.join("");
},
/**
@ -617,8 +696,8 @@ Highlighter.prototype = {
*/
computeZoomFactor: function Highlighter_computeZoomFactor() {
this.zoom =
this.win.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindowUtils)
this.win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.screenPixelsPerCSSPixel;
},
@ -805,3 +884,6 @@ Highlighter.prototype = {
///////////////////////////////////////////////////////////////////////////
XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils)
});

View File

@ -83,6 +83,8 @@ const INSPECTOR_NOTIFICATIONS = {
EDITOR_SAVED: "inspector-editor-saved",
};
const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
///////////////////////////////////////////////////////////////////////////
//// InspectorUI
@ -362,6 +364,7 @@ InspectorUI.prototype = {
show: this.openRuleView,
hide: this.closeRuleView,
onSelect: this.selectInRuleView,
onChanged: this.changeInRuleView,
panel: null,
unregister: this.destroyRuleView,
sidebar: true,
@ -481,6 +484,7 @@ InspectorUI.prototype = {
if (!aKeepStore) {
this.store.deleteStore(this.winID);
this.win.removeEventListener("pagehide", this, true);
this.clearPseudoClassLocks();
} else {
// Update the store before closing.
if (this.selection) {
@ -566,7 +570,7 @@ InspectorUI.prototype = {
this.inspecting = false;
this.toolsDim(false);
if (this.highlighter.getNode()) {
this.select(this.highlighter.getNode(), true, true, !aPreventScroll);
this.select(this.highlighter.getNode(), true, !aPreventScroll);
} else {
this.select(null, true, true);
}
@ -574,15 +578,17 @@ InspectorUI.prototype = {
},
/**
* Select an object in the tree view.
* Select an object in the inspector.
* @param aNode
* node to inspect
* @param forceUpdate
* force an update?
* @param aScroll boolean
* scroll the tree panel?
* @param aFrom [optional] string
* which part of the UI the selection occured from
*/
select: function IUI_select(aNode, forceUpdate, aScroll)
select: function IUI_select(aNode, forceUpdate, aScroll, aFrom)
{
// if currently editing an attribute value, using the
// highlighter dismisses the editor
@ -593,6 +599,10 @@ InspectorUI.prototype = {
aNode = this.defaultSelection;
if (forceUpdate || aNode != this.selection) {
if (aFrom != "breadcrumbs") {
this.clearPseudoClassLocks();
}
this.selection = aNode;
if (!this.inspecting) {
this.highlighter.highlight(this.selection);
@ -605,6 +615,41 @@ InspectorUI.prototype = {
this.toolsSelect(aScroll);
},
/**
* Toggle the pseudo-class lock on the currently inspected element. If the
* pseudo-class is :hover or :active, that pseudo-class will also be toggled
* on every ancestor of the element, mirroring real :hover and :active
* behavior.
*
* @param aPseudo the pseudo-class lock to toggle, e.g. ":hover"
*/
togglePseudoClassLock: function IUI_togglePseudoClassLock(aPseudo)
{
if (DOMUtils.hasPseudoClassLock(this.selection, aPseudo)) {
this.breadcrumbs.nodeHierarchy.forEach(function(crumb) {
DOMUtils.removePseudoClassLock(crumb.node, aPseudo);
});
} else {
let hierarchical = aPseudo == ":hover" || aPseudo == ":active";
let node = this.selection;
do {
DOMUtils.addPseudoClassLock(node, aPseudo);
node = node.parentNode;
} while (hierarchical && node.parentNode)
}
this.nodeChanged();
},
/**
* Clear all pseudo-class locks applied to elements in the node hierarchy
*/
clearPseudoClassLocks: function IUI_clearPseudoClassLocks()
{
this.breadcrumbs.nodeHierarchy.forEach(function(crumb) {
DOMUtils.clearPseudoClassLocks(crumb.node);
});
},
/**
* Called when the highlighted node is changed by a tool.
@ -616,6 +661,7 @@ InspectorUI.prototype = {
nodeChanged: function IUI_nodeChanged(aUpdater)
{
this.highlighter.invalidateSize();
this.breadcrumbs.updateSelectors();
this.toolsOnChanged(aUpdater);
},
@ -641,6 +687,10 @@ InspectorUI.prototype = {
self.select(self.highlighter.getNode(), false, false);
});
this.highlighter.addListener("pseudoclasstoggled", function(aPseudo) {
self.togglePseudoClassLock(aPseudo);
});
if (this.store.getValue(this.winID, "inspecting")) {
this.startInspecting();
this.highlighter.unlock();
@ -911,6 +961,15 @@ InspectorUI.prototype = {
if (this.ruleView)
this.ruleView.highlight(aNode);
},
/**
* Update the rules for the current node in the Css Rule View.
*/
changeInRuleView: function IUI_selectInRuleView()
{
if (this.ruleView)
this.ruleView.nodeChanged();
},
ruleViewChanged: function IUI_ruleViewChanged()
{
@ -1336,7 +1395,7 @@ InspectorUI.prototype = {
toolsOnChanged: function IUI_toolsChanged(aUpdater)
{
this.toolsDo(function IUI_toolsOnChanged(aTool) {
if (aTool.isOpen && ("onChanged" in aTool) && aTool != aUpdater) {
if (("onChanged" in aTool) && aTool != aUpdater) {
aTool.onChanged.call(aTool.context);
}
});
@ -1730,6 +1789,13 @@ HTMLBreadcrumbs.prototype = {
for (let i = 0; i < aNode.classList.length; i++) {
text += "." + aNode.classList[i];
}
for (let i = 0; i < PSEUDO_CLASSES.length; i++) {
let pseudo = PSEUDO_CLASSES[i];
if (DOMUtils.hasPseudoClassLock(aNode, pseudo)) {
text += pseudo;
}
}
return text;
},
@ -1755,6 +1821,9 @@ HTMLBreadcrumbs.prototype = {
let classesLabel = this.IUI.chromeDoc.createElement("label");
classesLabel.className = "inspector-breadcrumbs-classes plain";
let pseudosLabel = this.IUI.chromeDoc.createElement("label");
pseudosLabel.className = "inspector-breadcrumbs-pseudo-classes plain";
tagLabel.textContent = aNode.tagName.toLowerCase();
idLabel.textContent = aNode.id ? ("#" + aNode.id) : "";
@ -1765,9 +1834,15 @@ HTMLBreadcrumbs.prototype = {
}
classesLabel.textContent = classesText;
let pseudos = PSEUDO_CLASSES.filter(function(pseudo) {
return DOMUtils.hasPseudoClassLock(aNode, pseudo);
}, this);
pseudosLabel.textContent = pseudos.join("");
fragment.appendChild(tagLabel);
fragment.appendChild(idLabel);
fragment.appendChild(classesLabel);
fragment.appendChild(pseudosLabel);
return fragment;
},
@ -1807,7 +1882,7 @@ HTMLBreadcrumbs.prototype = {
item.onmouseup = (function(aNode) {
return function() {
inspector.select(aNode, true, true);
inspector.select(aNode, true, true, "breadcrumbs");
}
})(nodes[i]);
@ -1961,7 +2036,7 @@ HTMLBreadcrumbs.prototype = {
button.onBreadcrumbsClick = function onBreadcrumbsClick() {
inspector.stopInspecting();
inspector.select(aNode, true, true);
inspector.select(aNode, true, true, "breadcrumbs");
};
button.onclick = (function _onBreadcrumbsRightClick(aEvent) {
@ -2076,6 +2151,20 @@ HTMLBreadcrumbs.prototype = {
let element = this.nodeHierarchy[this.currentIndex].button;
scrollbox.ensureElementIsVisible(element);
},
updateSelectors: function BC_updateSelectors()
{
for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) {
let crumb = this.nodeHierarchy[i];
let button = crumb.button;
while(button.hasChildNodes()) {
button.removeChild(button.firstChild);
}
button.appendChild(this.prettyPrintNodeAsXUL(crumb.node));
button.setAttribute("tooltiptext", this.prettyPrintNodeAsText(crumb.node));
}
},
/**
* Update the breadcrumbs display when a new node is selected.
@ -2117,6 +2206,8 @@ HTMLBreadcrumbs.prototype = {
// Make sure the selected node and its neighbours are visible.
this.scroll();
this.updateSelectors();
},
}
@ -2136,3 +2227,6 @@ XPCOMUtils.defineLazyGetter(this, "StyleInspector", function () {
return obj.StyleInspector;
});
XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
});

View File

@ -72,6 +72,7 @@ _BROWSER_FILES = \
browser_inspector_invalidate.js \
browser_inspector_sidebarstate.js \
browser_inspector_treePanel_menu.js \
browser_inspector_pseudoclass_lock.js \
head.js \
$(NULL)

View File

@ -0,0 +1,154 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
let DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
let doc;
let div;
let pseudo = ":hover";
function test()
{
waitForExplicitFinish();
ignoreAllUncaughtExceptions();
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function() {
gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
doc = content.document;
waitForFocus(createDocument, content);
}, true);
content.location = "data:text/html,pseudo-class lock tests";
}
function createDocument()
{
div = doc.createElement("div");
div.textContent = "test div";
let head = doc.getElementsByTagName('head')[0];
let style = doc.createElement('style');
let rules = doc.createTextNode('div { color: red; } div:hover { color: blue; }');
style.appendChild(rules);
head.appendChild(style);
doc.body.appendChild(div);
setupTests();
}
function setupTests()
{
Services.obs.addObserver(selectNode,
InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
InspectorUI.openInspectorUI();
}
function selectNode()
{
Services.obs.removeObserver(selectNode,
InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
executeSoon(function() {
InspectorUI.highlighter.addListener("nodeselected", openRuleView);
InspectorUI.inspectNode(div);
});
}
function openRuleView()
{
Services.obs.addObserver(performTests,
InspectorUI.INSPECTOR_NOTIFICATIONS.RULEVIEWREADY, false);
InspectorUI.showSidebar();
InspectorUI.openRuleView();
}
function performTests()
{
Services.obs.removeObserver(performTests,
InspectorUI.INSPECTOR_NOTIFICATIONS.RULEVIEWREADY);
InspectorUI.highlighter.removeListener("nodeselected", performTests);
// toggle the class
InspectorUI.highlighter.pseudoClassLockToggled(pseudo);
testAdded();
// toggle the lock off
InspectorUI.highlighter.pseudoClassLockToggled(pseudo);
testRemoved();
testRemovedFromUI();
// toggle it back on
InspectorUI.highlighter.pseudoClassLockToggled(pseudo);
// close the inspector
Services.obs.addObserver(testInspectorClosed,
InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false);
InspectorUI.closeInspectorUI();
}
function testAdded()
{
// lock is applied to it and ancestors
let node = div;
do {
is(DOMUtils.hasPseudoClassLock(node, pseudo), true,
"pseudo-class lock has been applied");
node = node.parentNode;
} while (node.parentNode)
// infobar selector contains pseudo-class
let pseudoClassesBox = document.getElementById("highlighter-nodeinfobar-pseudo-classes");
is(pseudoClassesBox.textContent, pseudo, "pseudo-class in infobar selector");
// ruleview contains pseudo-class rule
is(InspectorUI.ruleView.element.children.length, 3,
"rule view is showing 3 rules for pseudo-class locked div");
is(InspectorUI.ruleView.element.children[1]._ruleEditor.rule.selectorText,
"div:hover", "rule view is showing " + pseudo + " rule");
}
function testRemoved()
{
// lock removed from node and ancestors
let node = div;
do {
is(DOMUtils.hasPseudoClassLock(node, pseudo), false,
"pseudo-class lock has been removed");
node = node.parentNode;
} while (node.parentNode)
}
function testRemovedFromUI()
{
// infobar selector doesn't contain pseudo-class
let pseudoClassesBox = document.getElementById("highlighter-nodeinfobar-pseudo-classes");
is(pseudoClassesBox.textContent, "", "pseudo-class removed from infobar selector");
// ruleview no longer contains pseudo-class rule
is(InspectorUI.ruleView.element.children.length, 2,
"rule view is showing 2 rules after removing lock");
}
function testInspectorClosed()
{
Services.obs.removeObserver(testInspectorClosed,
InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED);
testRemoved();
finishUp();
}
function finishUp()
{
doc = div = null;
gBrowser.removeCurrentTab();
finish();
}

View File

@ -116,7 +116,7 @@ function ElementStyle(aElement, aStore)
// how their .style attribute reflects them as computed values.
this.dummyElement = doc.createElementNS(this.element.namespaceURI,
this.element.tagName);
this._populate();
this.populate();
}
// We're exporting _ElementStyle for unit tests.
var _ElementStyle = ElementStyle;
@ -147,7 +147,7 @@ ElementStyle.prototype = {
* Refresh the list of rules to be displayed for the active element.
* Upon completion, this.rules[] will hold a list of Rule objects.
*/
_populate: function ElementStyle_populate()
populate: function ElementStyle_populate()
{
this.rules = [];
@ -713,15 +713,33 @@ CssRuleView.prototype = {
this._createEditors();
},
/**
* Update the rules for the currently highlighted element.
*/
nodeChanged: function CssRuleView_nodeChanged()
{
this._clearRules();
this._elementStyle.populate();
this._createEditors();
},
/**
* Clear the rules.
*/
_clearRules: function CssRuleView_clearRules()
{
while (this.element.hasChildNodes()) {
this.element.removeChild(this.element.lastChild);
}
},
/**
* Clear the rule view.
*/
clear: function CssRuleView_clear()
{
while (this.element.hasChildNodes()) {
this.element.removeChild(this.element.lastChild);
}
this._clearRules();
this._viewedElement = null;
this._elementStyle = null;
},

View File

@ -2014,6 +2014,10 @@ html|*#highlighter-nodeinfobar-id {
color: hsl(90, 79%, 52%);
}
html|*#highlighter-nodeinfobar-pseudo-classes {
color: hsl(20, 100%, 70%);
}
/* Highlighter - Node Infobar - box & arrow */
#highlighter-nodeinfobar {
@ -2118,11 +2122,19 @@ html|*#highlighter-nodeinfobar-id {
color: hsl(205,100%,70%);
}
.inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-pseudo-classes {
color: hsl(20, 100%, 70%);
}
.inspector-breadcrumbs-id,
.inspector-breadcrumbs-classes {
color: #8d99a6;
}
.inspector-breadcrumbs-pseudo-classes {
color: hsl(20, 100%, 85%);
}
/* Highlighter toolbar - breadcrumbs - LTR */
.inspector-breadcrumbs-button:-moz-locale-dir(ltr):first-of-type {

View File

@ -2761,6 +2761,10 @@ html|*#highlighter-nodeinfobar-id {
color: hsl(90, 79%, 52%);
}
html|*#highlighter-nodeinfobar-pseudo-classes {
color: hsl(20, 100%, 70%);
}
/* Highlighter - Node Infobar - box & arrow */
#highlighter-nodeinfobar {
@ -2859,11 +2863,19 @@ html|*#highlighter-nodeinfobar-id {
color: hsl(205,100%,70%);
}
.inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-pseudo-classes {
color: hsl(20, 100%, 70%);
}
.inspector-breadcrumbs-id,
.inspector-breadcrumbs-classes {
color: #8d99a6;
}
.inspector-breadcrumbs-pseudo-classes {
color: hsl(20, 100%, 85%);
}
/* Highlighter toolbar - breadcrumbs - LTR */
.inspector-breadcrumbs-button:-moz-locale-dir(ltr):first-of-type {

View File

@ -2709,6 +2709,10 @@ html|*#highlighter-nodeinfobar-id {
color: hsl(90, 79%, 52%);
}
html|*#highlighter-nodeinfobar-pseudo-classes {
color: hsl(20, 100%, 70%);
}
/* Highlighter - Node Infobar - box & arrow */
#highlighter-nodeinfobar {
@ -2813,11 +2817,19 @@ html|*#highlighter-nodeinfobar-id {
color: hsl(200,100%,70%);
}
.inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-pseudo-classes {
color: hsl(20, 100%, 70%);
}
.inspector-breadcrumbs-id,
.inspector-breadcrumbs-classes {
color: #8d99a6;
}
.inspector-breadcrumbs-pseudo-classes {
color: hsl(20, 100%, 85%);
}
/* Highlighter toolbar - breadcrumbs - LTR */
.inspector-breadcrumbs-button:-moz-locale-dir(ltr):first-of-type {