gecko/browser/devtools/styleinspector/CssHtmlTree.jsm
Ehsan Akhgari 45fe6d3ae2 Bug 722872 - Part 1: Add nsITransferable::Init(nsILoadContext*), enforce that it's called in debug builds, and add nsIDOMDocument* arguments to nsIClipboardHelper methods; r=roc
This patch does the following:

* It adds nsITransferable::Init(nsILoadContext*).  The load context
  might be null, which means that the transferable is non-private, but
  if it's non-null, we extract the boolean value for the privacy mode
  and store it in the transferable.
* It adds checks in debug builds to make sure that Init is always
  called, in form of fatal assertions.
* It adds nsIDOMDocument* agruments to nsIClipboardHelper methods which
  represent the document that the string is coming from.
  nsIClipboardHelper implementation internally gets the nsILoadContext
  from that and passes it on to the transferable upon creation.  The
  reason that I did this was that nsIClipboardHelper is supposed to be a
  high-level helper, and in most of its call sites, we have easy access
  to a document object.
* It modifies all of the call sites of the above interfaces according to
  this change.
* It adds a GetLoadContext helper to nsIDocument to help with changing
  the call sites.
2012-04-16 22:14:01 -04:00

1245 lines
37 KiB
JavaScript

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const FILTER_CHANGED_TIMEOUT = 300;
const HTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PluralForm.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/devtools/CssLogic.jsm");
Cu.import("resource:///modules/devtools/Templater.jsm");
var EXPORTED_SYMBOLS = ["CssHtmlTree", "PropertyView"];
/**
* Helper for long-running processes that should yield occasionally to
* the mainloop.
*
* @param {Window} aWin
* Timeouts will be set on this window when appropriate.
* @param {Generator} aGenerator
* Will iterate this generator.
* @param {object} aOptions
* Options for the update process:
* onItem {function} Will be called with the value of each iteration.
* onBatch {function} Will be called after each batch of iterations,
* before yielding to the main loop.
* onDone {function} Will be called when iteration is complete.
* onCancel {function} Will be called if the process is canceled.
* threshold {int} How long to process before yielding, in ms.
*
* @constructor
*/
function UpdateProcess(aWin, aGenerator, aOptions)
{
this.win = aWin;
this.iter = Iterator(aGenerator);
this.onItem = aOptions.onItem || function() {};
this.onBatch = aOptions.onBatch || function () {};
this.onDone = aOptions.onDone || function() {};
this.onCancel = aOptions.onCancel || function() {};
this.threshold = aOptions.threshold || 45;
this.canceled = false;
}
UpdateProcess.prototype = {
/**
* Schedule a new batch on the main loop.
*/
schedule: function UP_schedule()
{
if (this.cancelled) {
return;
}
this._timeout = this.win.setTimeout(this._timeoutHandler.bind(this), 0);
},
/**
* Cancel the running process. onItem will not be called again,
* and onCancel will be called.
*/
cancel: function UP_cancel()
{
if (this._timeout) {
this.win.clearTimeout(this._timeout);
this._timeout = 0;
}
this.canceled = true;
this.onCancel();
},
_timeoutHandler: function UP_timeoutHandler() {
this._timeout = null;
try {
this._runBatch();
this.schedule();
} catch(e) {
if (e instanceof StopIteration) {
this.onBatch();
this.onDone();
return;
}
throw e;
}
},
_runBatch: function Y_runBatch()
{
let time = Date.now();
while(!this.cancelled) {
// Continue until iter.next() throws...
let next = this.iter.next();
this.onItem(next[1]);
if ((Date.now() - time) > this.threshold) {
this.onBatch();
return;
}
}
}
};
/**
* CssHtmlTree is a panel that manages the display of a table sorted by style.
* There should be one instance of CssHtmlTree per style display (of which there
* will generally only be one).
*
* @params {StyleInspector} aStyleInspector The owner of this CssHtmlTree
* @constructor
*/
function CssHtmlTree(aStyleInspector)
{
this.styleWin = aStyleInspector.iframe;
this.styleInspector = aStyleInspector;
this.cssLogic = aStyleInspector.cssLogic;
this.doc = aStyleInspector.document;
this.win = aStyleInspector.window;
this.getRTLAttr = this.win.getComputedStyle(this.win.gBrowser).direction;
this.propertyViews = [];
// Create bound methods.
this.siBoundMenuUpdate = this.computedViewMenuUpdate.bind(this);
this.siBoundCopy = this.computedViewCopy.bind(this);
this.siBoundCopyDeclaration = this.computedViewCopyDeclaration.bind(this);
this.siBoundCopyProperty = this.computedViewCopyProperty.bind(this);
this.siBoundCopyPropertyValue = this.computedViewCopyPropertyValue.bind(this);
// The document in which we display the results (csshtmltree.xul).
this.styleDocument = this.styleWin.contentWindow.document;
this.styleDocument.addEventListener("copy", this.siBoundCopy);
// Nodes used in templating
this.root = this.styleDocument.getElementById("root");
this.templateRoot = this.styleDocument.getElementById("templateRoot");
this.propertyContainer = this.styleDocument.getElementById("propertyContainer");
this.panel = aStyleInspector.panel;
// No results text.
this.noResults = this.styleDocument.getElementById("noResults");
// The element that we're inspecting, and the document that it comes from.
this.viewedElement = null;
this.createStyleViews();
this.createContextMenu();
}
/**
* Memoized lookup of a l10n string from a string bundle.
* @param {string} aName The key to lookup.
* @returns A localized version of the given key.
*/
CssHtmlTree.l10n = function CssHtmlTree_l10n(aName)
{
try {
return CssHtmlTree._strings.GetStringFromName(aName);
} catch (ex) {
Services.console.logStringMessage("Error reading '" + aName + "'");
throw new Error("l10n error with " + aName);
}
};
/**
* Clone the given template node, and process it by resolving ${} references
* in the template.
*
* @param {nsIDOMElement} aTemplate the template note to use.
* @param {nsIDOMElement} aDestination the destination node where the
* processed nodes will be displayed.
* @param {object} aData the data to pass to the template.
* @param {Boolean} aPreserveDestination If true then the template will be
* appended to aDestination's content else aDestination.innerHTML will be
* cleared before the template is appended.
*/
CssHtmlTree.processTemplate = function CssHtmlTree_processTemplate(aTemplate,
aDestination, aData, aPreserveDestination)
{
if (!aPreserveDestination) {
aDestination.innerHTML = "";
}
// All the templater does is to populate a given DOM tree with the given
// values, so we need to clone the template first.
let duplicated = aTemplate.cloneNode(true);
// See https://github.com/mozilla/domtemplate/blob/master/README.md
// for docs on the template() function
template(duplicated, aData, { allowEval: true });
while (duplicated.firstChild) {
aDestination.appendChild(duplicated.firstChild);
}
};
XPCOMUtils.defineLazyGetter(CssHtmlTree, "_strings", function() Services.strings
.createBundle("chrome://browser/locale/devtools/styleinspector.properties"));
XPCOMUtils.defineLazyGetter(CssHtmlTree, "HELP_LINK_TITLE", function() {
return CssHtmlTree.HELP_LINK_TITLE = CssHtmlTree.l10n("helpLinkTitle");
});
XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
return Cc["@mozilla.org/widget/clipboardhelper;1"].
getService(Ci.nsIClipboardHelper);
});
CssHtmlTree.prototype = {
// Cache the list of properties that have matched and unmatched properties.
_matchedProperties: null,
_unmatchedProperties: null,
htmlComplete: false,
// Used for cancelling timeouts in the style filter.
_filterChangedTimeout: null,
// The search filter
searchField: null,
// Reference to the "Only user Styles" checkbox.
onlyUserStylesCheckbox: null,
// Holds the ID of the panelRefresh timeout.
_panelRefreshTimeout: null,
// Toggle for zebra striping
_darkStripe: true,
// Number of visible properties
numVisibleProperties: 0,
get showOnlyUserStyles()
{
return this.onlyUserStylesCheckbox.checked;
},
/**
* Update the highlighted element. The CssHtmlTree panel will show the style
* information for the given element.
* @param {nsIDOMElement} aElement The highlighted node to get styles for.
*/
highlight: function CssHtmlTree_highlight(aElement)
{
this.viewedElement = aElement;
this._unmatchedProperties = null;
this._matchedProperties = null;
if (this.htmlComplete) {
this.refreshSourceFilter();
this.refreshPanel();
} else {
if (this._refreshProcess) {
this._refreshProcess.cancel();
}
CssHtmlTree.processTemplate(this.templateRoot, this.root, this);
// Refresh source filter ... this must be done after templateRoot has been
// processed.
this.refreshSourceFilter();
this.numVisibleProperties = 0;
let fragment = this.doc.createDocumentFragment();
this._refreshProcess = new UpdateProcess(this.win, CssHtmlTree.propertyNames, {
onItem: function(aPropertyName) {
// Per-item callback.
let propView = new PropertyView(this, aPropertyName);
fragment.appendChild(propView.buildMain());
fragment.appendChild(propView.buildSelectorContainer());
if (propView.visible) {
this.numVisibleProperties++;
}
propView.refreshAllSelectors();
this.propertyViews.push(propView);
}.bind(this),
onDone: function() {
// Completed callback.
this.htmlComplete = true;
this.propertyContainer.appendChild(fragment);
this.noResults.hidden = this.numVisibleProperties > 0;
this._refreshProcess = null;
// If a refresh was scheduled during the building, complete it.
if (this._needsRefresh) {
delete this._needsRefresh;
this.refreshPanel();
} else {
Services.obs.notifyObservers(null, "StyleInspector-populated", null);
}
}.bind(this)});
this._refreshProcess.schedule();
}
},
/**
* Refresh the panel content.
*/
refreshPanel: function CssHtmlTree_refreshPanel()
{
// If we're still in the process of creating the initial layout,
// leave it alone.
if (!this.htmlComplete) {
if (this._refreshProcess) {
this._needsRefresh = true;
}
return;
}
if (this._refreshProcess) {
this._refreshProcess.cancel();
}
this.noResults.hidden = true;
// Reset visible property count
this.numVisibleProperties = 0;
// Reset zebra striping.
this._darkStripe = true;
let display = this.propertyContainer.style.display;
this._refreshProcess = new UpdateProcess(this.win, this.propertyViews, {
onItem: function(aPropView) {
aPropView.refresh();
}.bind(this),
onDone: function() {
this._refreshProcess = null;
this.noResults.hidden = this.numVisibleProperties > 0;
Services.obs.notifyObservers(null, "StyleInspector-populated", null);
}.bind(this)
});
this._refreshProcess.schedule();
},
/**
* Called when the user enters a search term.
*
* @param {Event} aEvent the DOM Event object.
*/
filterChanged: function CssHtmlTree_filterChanged(aEvent)
{
let win = this.styleWin.contentWindow;
if (this._filterChangedTimeout) {
win.clearTimeout(this._filterChangedTimeout);
}
this._filterChangedTimeout = win.setTimeout(function() {
this.refreshPanel();
this._filterChangeTimeout = null;
}.bind(this), FILTER_CHANGED_TIMEOUT);
},
/**
* The change event handler for the onlyUserStyles checkbox.
*
* @param {Event} aEvent the DOM Event object.
*/
onlyUserStylesChanged: function CssHtmltree_onlyUserStylesChanged(aEvent)
{
this.refreshSourceFilter();
this.refreshPanel();
},
/**
* When onlyUserStyles.checked is true we only display properties that have
* matched selectors and have been included by the document or one of the
* document's stylesheets. If .checked is false we display all properties
* including those that come from UA stylesheets.
*/
refreshSourceFilter: function CssHtmlTree_setSourceFilter()
{
this._matchedProperties = null;
this.cssLogic.sourceFilter = this.showOnlyUserStyles ?
CssLogic.FILTER.ALL :
CssLogic.FILTER.UA;
},
/**
* The CSS as displayed by the UI.
*/
createStyleViews: function CssHtmlTree_createStyleViews()
{
if (CssHtmlTree.propertyNames) {
return;
}
CssHtmlTree.propertyNames = [];
// Here we build and cache a list of css properties supported by the browser
// We could use any element but let's use the main document's root element
let styles = this.styleWin.contentWindow.getComputedStyle(this.styleDocument.documentElement);
let mozProps = [];
for (let i = 0, numStyles = styles.length; i < numStyles; i++) {
let prop = styles.item(i);
if (prop.charAt(0) == "-") {
mozProps.push(prop);
} else {
CssHtmlTree.propertyNames.push(prop);
}
}
CssHtmlTree.propertyNames.sort();
CssHtmlTree.propertyNames.push.apply(CssHtmlTree.propertyNames,
mozProps.sort());
},
/**
* Get a list of properties that have matched selectors.
*
* @return {object} the object maps property names (keys) to booleans (values)
* that tell if the given property has matched selectors or not.
*/
get matchedProperties()
{
if (!this._matchedProperties) {
this._matchedProperties =
this.cssLogic.hasMatchedSelectors(CssHtmlTree.propertyNames);
}
return this._matchedProperties;
},
/**
* Check if a property has unmatched selectors. Result is cached.
*
* @param {string} aProperty the name of the property you want to check.
* @return {boolean} true if the property has unmatched selectors, false
* otherwise.
*/
hasUnmatchedSelectors: function CssHtmlTree_hasUnmatchedSelectors(aProperty)
{
// Initially check all of the properties that return false for
// hasMatchedSelectors(). This speeds-up the UI.
if (!this._unmatchedProperties) {
let properties = [];
CssHtmlTree.propertyNames.forEach(function(aName) {
if (!this.matchedProperties[aName]) {
properties.push(aName);
}
}, this);
if (properties.indexOf(aProperty) == -1) {
properties.push(aProperty);
}
this._unmatchedProperties = this.cssLogic.hasUnmatchedSelectors(properties);
}
// Lazy-get the result for properties we do not have cached.
if (!(aProperty in this._unmatchedProperties)) {
let result = this.cssLogic.hasUnmatchedSelectors([aProperty]);
this._unmatchedProperties[aProperty] = result[aProperty];
}
return this._unmatchedProperties[aProperty];
},
/**
* Create a context menu.
*/
createContextMenu: function SI_createContextMenu()
{
let popupSet = this.doc.getElementById("mainPopupSet");
let menu = this.doc.createElement("menupopup");
menu.addEventListener("popupshowing", this.siBoundMenuUpdate);
menu.id = "computed-view-context-menu";
popupSet.appendChild(menu);
// Copy selection
let label = CssHtmlTree.l10n("style.contextmenu.copyselection");
let accessKey = CssHtmlTree.l10n("style.contextmenu.copyselection.accesskey");
let item = this.doc.createElement("menuitem");
item.id = "computed-view-copy";
item.setAttribute("label", label);
item.setAttribute("accesskey", accessKey);
item.addEventListener("command", this.siBoundCopy);
menu.appendChild(item);
// Copy declaration
label = CssHtmlTree.l10n("style.contextmenu.copydeclaration");
accessKey = CssHtmlTree.l10n("style.contextmenu.copydeclaration.accesskey");
item = this.doc.createElement("menuitem");
item.id = "computed-view-copy-declaration";
item.setAttribute("label", label);
item.setAttribute("accesskey", accessKey);
item.addEventListener("command", this.siBoundCopyDeclaration);
menu.appendChild(item);
// Copy property name
label = CssHtmlTree.l10n("style.contextmenu.copyproperty");
accessKey = CssHtmlTree.l10n("style.contextmenu.copyproperty.accesskey");
item = this.doc.createElement("menuitem");
item.id = "computed-view-copy-property";
item.setAttribute("label", label);
item.setAttribute("accesskey", accessKey);
item.addEventListener("command", this.siBoundCopyProperty);
menu.appendChild(item);
// Copy property value
label = CssHtmlTree.l10n("style.contextmenu.copypropertyvalue");
accessKey = CssHtmlTree.l10n("style.contextmenu.copypropertyvalue.accesskey");
item = this.doc.createElement("menuitem");
item.id = "computed-view-copy-property-value";
item.setAttribute("label", label);
item.setAttribute("accesskey", accessKey);
item.addEventListener("command", this.siBoundCopyPropertyValue);
menu.appendChild(item);
this.styleWin.setAttribute("context", menu.id);
},
/**
* Update the context menu by disabling irrelevant menuitems and enabling
* relevant ones.
*/
computedViewMenuUpdate: function si_computedViewMenuUpdate()
{
let win = this.styleDocument.defaultView;
let disable = win.getSelection().isCollapsed;
let menuitem = this.doc.querySelector("#computed-view-copy");
menuitem.disabled = disable;
let node = this.doc.popupNode;
if (!node) {
return;
}
if (!node.classList.contains("property-view")) {
while (node = node.parentElement) {
if (node.classList.contains("property-view")) {
break;
}
}
}
let disablePropertyItems = !node;
menuitem = this.doc.querySelector("#computed-view-copy-declaration");
menuitem.disabled = disablePropertyItems;
menuitem = this.doc.querySelector("#computed-view-copy-property");
menuitem.disabled = disablePropertyItems;
menuitem = this.doc.querySelector("#computed-view-copy-property-value");
menuitem.disabled = disablePropertyItems;
},
/**
* Copy selected text.
*
* @param aEvent The event object
*/
computedViewCopy: function si_computedViewCopy(aEvent)
{
let win = this.styleDocument.defaultView;
let text = win.getSelection().toString();
// Tidy up block headings by moving CSS property names and their values onto
// the same line and inserting a colon between them.
text = text.replace(/(.+)\r?\n\s+/g, "$1: ");
// Remove any MDN link titles
text = text.replace(CssHtmlTree.HELP_LINK_TITLE, "");
clipboardHelper.copyString(text, this.doc);
if (aEvent) {
aEvent.preventDefault();
}
},
/**
* Copy declaration.
*
* @param aEvent The event object
*/
computedViewCopyDeclaration: function si_computedViewCopyDeclaration(aEvent)
{
let node = this.doc.popupNode;
if (!node) {
return;
}
if (!node.classList.contains("property-view")) {
while (node = node.parentElement) {
if (node.classList.contains("property-view")) {
break;
}
}
}
if (node) {
let name = node.querySelector(".property-name").textContent;
let value = node.querySelector(".property-value").textContent;
clipboardHelper.copyString(name + ": " + value + ";", this.doc);
}
},
/**
* Copy property name.
*
* @param aEvent The event object
*/
computedViewCopyProperty: function si_computedViewCopyProperty(aEvent)
{
let node = this.doc.popupNode;
if (!node) {
return;
}
if (!node.classList.contains("property-view")) {
while (node = node.parentElement) {
if (node.classList.contains("property-view")) {
break;
}
}
}
if (node) {
node = node.querySelector(".property-name");
clipboardHelper.copyString(node.textContent, this.doc);
}
},
/**
* Copy property value.
*
* @param aEvent The event object
*/
computedViewCopyPropertyValue: function si_computedViewCopyPropertyValue(aEvent)
{
let node = this.doc.popupNode;
if (!node) {
return;
}
if (!node.classList.contains("property-view")) {
while (node = node.parentElement) {
if (node.classList.contains("property-view")) {
break;
}
}
}
if (node) {
node = node.querySelector(".property-value");
clipboardHelper.copyString(node.textContent, this.doc);
}
},
/**
* Destructor for CssHtmlTree.
*/
destroy: function CssHtmlTree_destroy()
{
delete this.viewedElement;
// Remove event listeners
this.onlyUserStylesCheckbox.removeEventListener("command",
this.onlyUserStylesChanged);
this.searchField.removeEventListener("command", this.filterChanged);
// Cancel tree construction
if (this._refreshProcess) {
this._refreshProcess.cancel();
}
// Remove context menu
let menu = this.doc.querySelector("#computed-view-context-menu");
if (menu) {
// Copy selected
let menuitem = this.doc.querySelector("#computed-view-copy");
menuitem.removeEventListener("command", this.siBoundCopy);
// Copy property
menuitem = this.doc.querySelector("#computed-view-copy-declaration");
menuitem.removeEventListener("command", this.siBoundCopyDeclaration);
// Copy property name
menuitem = this.doc.querySelector("#computed-view-copy-property");
menuitem.removeEventListener("command", this.siBoundCopyProperty);
// Copy property value
menuitem = this.doc.querySelector("#computed-view-copy-property-value");
menuitem.removeEventListener("command", this.siBoundCopyPropertyValue);
menu.removeEventListener("popupshowing", this.siBoundMenuUpdate);
menu.parentNode.removeChild(menu);
}
// Remove bound listeners
this.styleDocument.removeEventListener("copy", this.siBoundCopy);
// Nodes used in templating
delete this.root;
delete this.propertyContainer;
delete this.panel;
// The document in which we display the results (csshtmltree.xul).
delete this.styleDocument;
// The element that we're inspecting, and the document that it comes from.
delete this.propertyViews;
delete this.styleWin;
delete this.cssLogic;
delete this.doc;
delete this.win;
delete this.styleInspector;
},
};
/**
* A container to give easy access to property data from the template engine.
*
* @constructor
* @param {CssHtmlTree} aTree the CssHtmlTree instance we are working with.
* @param {string} aName the CSS property name for which this PropertyView
* instance will render the rules.
*/
function PropertyView(aTree, aName)
{
this.tree = aTree;
this.name = aName;
this.getRTLAttr = aTree.getRTLAttr;
this.link = "https://developer.mozilla.org/en/CSS/" + aName;
this.templateMatchedSelectors = aTree.styleDocument.getElementById("templateMatchedSelectors");
}
PropertyView.prototype = {
// The parent element which contains the open attribute
element: null,
// Property header node
propertyHeader: null,
// Destination for property names
nameNode: null,
// Destination for property values
valueNode: null,
// Are matched rules expanded?
matchedExpanded: false,
// Are unmatched rules expanded?
unmatchedExpanded: false,
// Unmatched selector table
unmatchedSelectorTable: null,
// Matched selector container
matchedSelectorsContainer: null,
// Matched selector expando
matchedExpander: null,
// Unmatched selector expando
unmatchedExpander: null,
// Unmatched selector container
unmatchedSelectorsContainer: null,
// Unmatched title block
unmatchedTitleBlock: null,
// Cache for matched selector views
_matchedSelectorViews: null,
// Cache for unmatched selector views
_unmatchedSelectorViews: null,
// The previously selected element used for the selector view caches
prevViewedElement: null,
/**
* Get the computed style for the current property.
*
* @return {string} the computed style for the current property of the
* currently highlighted element.
*/
get value()
{
return this.propertyInfo.value;
},
/**
* An easy way to access the CssPropertyInfo behind this PropertyView.
*/
get propertyInfo()
{
return this.tree.cssLogic.getPropertyInfo(this.name);
},
/**
* Does the property have any matched selectors?
*/
get hasMatchedSelectors()
{
return this.name in this.tree.matchedProperties;
},
/**
* Does the property have any unmatched selectors?
*/
get hasUnmatchedSelectors()
{
return this.name in this.tree.hasUnmatchedSelectors;
},
/**
* Should this property be visible?
*/
get visible()
{
if (this.tree.showOnlyUserStyles && !this.hasMatchedSelectors) {
return false;
}
let searchTerm = this.tree.searchField.value.toLowerCase();
if (searchTerm && this.name.toLowerCase().indexOf(searchTerm) == -1 &&
this.value.toLowerCase().indexOf(searchTerm) == -1) {
return false;
}
return true;
},
/**
* Returns the className that should be assigned to the propertyView.
*
* @return string
*/
get propertyHeaderClassName()
{
if (this.visible) {
this.tree._darkStripe = !this.tree._darkStripe;
let darkValue = this.tree._darkStripe ?
"property-view darkrow" : "property-view";
return darkValue;
}
return "property-view-hidden";
},
/**
* Returns the className that should be assigned to the propertyView content
* container.
* @return string
*/
get propertyContentClassName()
{
if (this.visible) {
let darkValue = this.tree._darkStripe ?
"property-content darkrow" : "property-content";
return darkValue;
}
return "property-content-hidden";
},
buildMain: function PropertyView_buildMain()
{
let doc = this.tree.doc;
this.element = doc.createElementNS(HTML_NS, "tr");
this.element.setAttribute("class", this.propertyHeaderClassName);
this.propertyHeader = doc.createElementNS(HTML_NS, "td");
this.element.appendChild(this.propertyHeader);
this.propertyHeader.setAttribute("class", "property-header");
this.matchedExpander = doc.createElementNS(HTML_NS, "div");
this.matchedExpander.setAttribute("class", "match expander");
this.matchedExpander.setAttribute("tabindex", "0");
this.matchedExpander.addEventListener("click",
this.matchedExpanderClick.bind(this), false);
this.matchedExpander.addEventListener("keydown", function(aEvent) {
let keyEvent = Ci.nsIDOMKeyEvent;
if (aEvent.keyCode == keyEvent.DOM_VK_F1) {
this.mdnLinkClick();
}
if (aEvent.keyCode == keyEvent.DOM_VK_RETURN ||
aEvent.keyCode == keyEvent.DOM_VK_SPACE) {
this.matchedExpanderClick(aEvent);
}
}.bind(this), false);
this.propertyHeader.appendChild(this.matchedExpander);
this.nameNode = doc.createElementNS(HTML_NS, "div");
this.propertyHeader.appendChild(this.nameNode);
this.nameNode.setAttribute("class", "property-name");
this.nameNode.textContent = this.name;
this.nameNode.addEventListener("click", function(aEvent) {
this.matchedExpander.focus();
}.bind(this), false);
let helpcontainer = doc.createElementNS(HTML_NS, "td");
this.element.appendChild(helpcontainer);
helpcontainer.setAttribute("class", "helplink-container");
let helplink = doc.createElementNS(HTML_NS, "a");
helpcontainer.appendChild(helplink);
helplink.setAttribute("class", "helplink");
helplink.setAttribute("title", CssHtmlTree.HELP_LINK_TITLE);
helplink.textContent = CssHtmlTree.HELP_LINK_TITLE;
helplink.addEventListener("click", this.mdnLinkClick.bind(this), false);
this.valueNode = doc.createElementNS(HTML_NS, "td");
this.element.appendChild(this.valueNode);
this.valueNode.setAttribute("class", "property-value");
this.valueNode.setAttribute("dir", "ltr");
this.valueNode.textContent = this.value;
return this.element;
},
buildSelectorContainer: function PropertyView_buildSelectorContainer()
{
let doc = this.tree.doc;
let element = doc.createElementNS(HTML_NS, "tr");
element.setAttribute("class", this.propertyContentClassName);
this.matchedSelectorsContainer = doc.createElementNS(HTML_NS, "td");
this.matchedSelectorsContainer.setAttribute("colspan", "0");
this.matchedSelectorsContainer.setAttribute("class", "rulelink");
element.appendChild(this.matchedSelectorsContainer);
return element;
},
/**
* Refresh the panel's CSS property value.
*/
refresh: function PropertyView_refresh()
{
this.element.className = this.propertyHeaderClassName;
this.element.nextElementSibling.className = this.propertyContentClassName;
if (this.prevViewedElement != this.tree.viewedElement) {
this._matchedSelectorViews = null;
this._unmatchedSelectorViews = null;
this.prevViewedElement = this.tree.viewedElement;
}
if (!this.tree.viewedElement || !this.visible) {
this.valueNode.innerHTML = "";
this.matchedSelectorsContainer.parentNode.hidden = true;
this.matchedSelectorsContainer.innerHTML = "";
this.matchedExpander.removeAttribute("open");
return;
}
this.tree.numVisibleProperties++;
this.valueNode.innerHTML = this.propertyInfo.value;
this.refreshAllSelectors();
},
/**
* Refresh the panel matched rules.
*/
refreshMatchedSelectors: function PropertyView_refreshMatchedSelectors()
{
let hasMatchedSelectors = this.hasMatchedSelectors;
this.matchedSelectorsContainer.parentNode.hidden = !hasMatchedSelectors;
if (hasMatchedSelectors) {
this.matchedExpander.classList.add("expandable");
} else {
this.matchedExpander.classList.remove("expandable");
}
if (this.matchedExpanded && hasMatchedSelectors) {
CssHtmlTree.processTemplate(this.templateMatchedSelectors,
this.matchedSelectorsContainer, this);
this.matchedExpander.setAttribute("open", "");
} else {
this.matchedSelectorsContainer.innerHTML = "";
this.matchedExpander.removeAttribute("open");
}
},
/**
* Refresh the panel unmatched rules.
*/
refreshUnmatchedSelectors: function PropertyView_refreshUnmatchedSelectors()
{
let hasMatchedSelectors = this.hasMatchedSelectors;
this.unmatchedSelectorTable.hidden = !this.unmatchedExpanded;
if (hasMatchedSelectors) {
this.unmatchedSelectorsContainer.hidden = !this.matchedExpanded ||
!this.hasUnmatchedSelectors;
this.unmatchedTitleBlock.hidden = false;
} else {
this.unmatchedSelectorsContainer.hidden = !this.unmatchedExpanded;
this.unmatchedTitleBlock.hidden = true;
}
if (this.unmatchedExpanded && this.hasUnmatchedSelectors) {
CssHtmlTree.processTemplate(this.templateUnmatchedSelectors,
this.unmatchedSelectorTable, this);
if (!hasMatchedSelectors) {
this.matchedExpander.setAttribute("open", "");
this.unmatchedSelectorTable.classList.add("only-unmatched");
} else {
this.unmatchedExpander.setAttribute("open", "");
this.unmatchedSelectorTable.classList.remove("only-unmatched");
}
} else {
if (!hasMatchedSelectors) {
this.matchedExpander.removeAttribute("open");
}
this.unmatchedExpander.removeAttribute("open");
this.unmatchedSelectorTable.innerHTML = "";
}
},
/**
* Refresh the panel matched and unmatched rules
*/
refreshAllSelectors: function PropertyView_refreshAllSelectors()
{
this.refreshMatchedSelectors();
},
/**
* Provide access to the matched SelectorViews that we are currently
* displaying.
*/
get matchedSelectorViews()
{
if (!this._matchedSelectorViews) {
this._matchedSelectorViews = [];
this.propertyInfo.matchedSelectors.forEach(
function matchedSelectorViews_convert(aSelectorInfo) {
this._matchedSelectorViews.push(new SelectorView(this.tree, aSelectorInfo));
}, this);
}
return this._matchedSelectorViews;
},
/**
* Provide access to the unmatched SelectorViews that we are currently
* displaying.
*/
get unmatchedSelectorViews()
{
if (!this._unmatchedSelectorViews) {
this._unmatchedSelectorViews = [];
this.propertyInfo.unmatchedSelectors.forEach(
function unmatchedSelectorViews_convert(aSelectorInfo) {
this._unmatchedSelectorViews.push(new SelectorView(this.tree, aSelectorInfo));
}, this);
}
return this._unmatchedSelectorViews;
},
/**
* The action when a user expands matched selectors.
*
* @param {Event} aEvent Used to determine the class name of the targets click
* event.
*/
matchedExpanderClick: function PropertyView_matchedExpanderClick(aEvent)
{
this.matchedExpanded = !this.matchedExpanded;
this.refreshAllSelectors();
aEvent.preventDefault();
},
/**
* The action when a user expands unmatched selectors.
*/
unmatchedSelectorsClick: function PropertyView_unmatchedSelectorsClick(aEvent)
{
this.unmatchedExpanded = !this.unmatchedExpanded;
this.refreshUnmatchedSelectors();
aEvent.preventDefault();
},
/**
* The action when a user clicks on the MDN help link for a property.
*/
mdnLinkClick: function PropertyView_mdnLinkClick(aEvent)
{
this.tree.win.openUILinkIn(this.link, "tab");
aEvent.preventDefault();
},
};
/**
* A container to view us easy access to display data from a CssRule
* @param CssHtmlTree aTree, the owning CssHtmlTree
* @param aSelectorInfo
*/
function SelectorView(aTree, aSelectorInfo)
{
this.tree = aTree;
this.selectorInfo = aSelectorInfo;
this._cacheStatusNames();
}
/**
* Decode for cssInfo.rule.status
* @see SelectorView.prototype._cacheStatusNames
* @see CssLogic.STATUS
*/
SelectorView.STATUS_NAMES = [
// "Unmatched", "Parent Match", "Matched", "Best Match"
];
SelectorView.CLASS_NAMES = [
"unmatched", "parentmatch", "matched", "bestmatch"
];
SelectorView.prototype = {
/**
* Cache localized status names.
*
* These statuses are localized inside the styleinspector.properties string
* bundle.
* @see CssLogic.jsm - the CssLogic.STATUS array.
*
* @return {void}
*/
_cacheStatusNames: function SelectorView_cacheStatusNames()
{
if (SelectorView.STATUS_NAMES.length) {
return;
}
for (let status in CssLogic.STATUS) {
let i = CssLogic.STATUS[status];
if (i > -1) {
let value = CssHtmlTree.l10n("rule.status." + status);
// Replace normal spaces with non-breaking spaces
SelectorView.STATUS_NAMES[i] = value.replace(/ /g, '\u00A0');
}
}
},
/**
* A localized version of cssRule.status
*/
get statusText()
{
return SelectorView.STATUS_NAMES[this.selectorInfo.status];
},
/**
* Get class name for selector depending on status
*/
get statusClass()
{
return SelectorView.CLASS_NAMES[this.selectorInfo.status];
},
/**
* A localized Get localized human readable info
*/
humanReadableText: function SelectorView_humanReadableText(aElement)
{
if (this.tree.getRTLAttr == "rtl") {
return this.selectorInfo.value + " \u2190 " + this.text(aElement);
} else {
return this.text(aElement) + " \u2192 " + this.selectorInfo.value;
}
},
text: function SelectorView_text(aElement) {
let result = this.selectorInfo.selector.text;
if (this.selectorInfo.elementStyle) {
let source = this.selectorInfo.sourceElement;
let IUI = this.tree.styleInspector.IUI;
if (IUI && IUI.selection == source) {
result = "this";
} else {
result = CssLogic.getShortName(source);
}
result += ".style";
}
return result;
},
maybeOpenStyleEditor: function(aEvent)
{
let keyEvent = Ci.nsIDOMKeyEvent;
if (aEvent.keyCode == keyEvent.DOM_VK_RETURN) {
this.openStyleEditor();
}
},
/**
* When a css link is clicked this method is called in order to either:
* 1. Open the link in view source (for element style attributes).
* 2. Open the link in the style editor.
*
* Like the style editor, we only view stylesheets contained in
* document.styleSheets inside the style editor.
*
* @param aEvent The click event
*/
openStyleEditor: function(aEvent)
{
let rule = this.selectorInfo.selector._cssRule;
let doc = this.tree.win.content.document;
let line = this.selectorInfo.ruleLine || 0;
let cssSheet = rule._cssSheet;
let contentSheet = false;
let styleSheet;
let styleSheets;
if (cssSheet) {
styleSheet = cssSheet.domSheet;
styleSheets = doc.styleSheets;
// Array.prototype.indexOf always returns -1 here so we loop through
// the styleSheets array instead.
for each (let sheet in styleSheets) {
if (sheet == styleSheet) {
contentSheet = true;
break;
}
}
}
if (contentSheet) {
this.tree.win.StyleEditor.openChrome(styleSheet, line);
} else {
let href = styleSheet ? styleSheet.href : "";
let viewSourceUtils = this.tree.win.gViewSourceUtils;
if (this.selectorInfo.sourceElement) {
href = this.selectorInfo.sourceElement.ownerDocument.location.href;
}
viewSourceUtils.viewSource(href, null, doc, line);
}
},
};