/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript 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 {Cc, Cu, Ci} = require("chrome");
const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
const ENSURE_SELECTION_VISIBLE_DELAY = 50; // ms
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/devtools/DOMHelpers.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data;
const MAX_LABEL_LENGTH = 40;
let promise = require("resource://gre/modules/Promise.jsm").Promise;
const LOW_PRIORITY_ELEMENTS = {
"HEAD": true,
"BASE": true,
"BASEFONT": true,
"ISINDEX": true,
"LINK": true,
"META": true,
"SCRIPT": true,
"STYLE": true,
"TITLE": true,
};
function resolveNextTick(value) {
let deferred = promise.defer();
Services.tm.mainThread.dispatch(() => {
try {
deferred.resolve(value);
} catch(ex) {
console.error(ex);
}
}, Ci.nsIThread.DISPATCH_NORMAL);
return deferred.promise;
}
///////////////////////////////////////////////////////////////////////////
//// HTML Breadcrumbs
/**
* Display the ancestors of the current node and its children.
* Only one "branch" of children are displayed (only one line).
*
* FIXME: Bug 822388 - Use the BreadcrumbsWidget in the Inspector.
*
* Mechanism:
* . If no nodes displayed yet:
* then display the ancestor of the selected node and the selected node;
* else select the node;
* . If the selected node is the last node displayed, append its first (if any).
*/
function HTMLBreadcrumbs(aInspector)
{
this.inspector = aInspector;
this.selection = this.inspector.selection;
this.chromeWin = this.inspector.panelWin;
this.chromeDoc = this.inspector.panelDoc;
this.DOMHelpers = new DOMHelpers(this.chromeWin);
this._init();
}
exports.HTMLBreadcrumbs = HTMLBreadcrumbs;
HTMLBreadcrumbs.prototype = {
get walker() this.inspector.walker,
_init: function BC__init()
{
this.container = this.chromeDoc.getElementById("inspector-breadcrumbs");
// These separators are used for CSS purposes only, and are positioned
// off screen, but displayed with -moz-element.
this.separators = this.chromeDoc.createElement("box");
this.separators.className = "breadcrumb-separator-container";
this.separators.innerHTML =
"" +
"" +
"";
this.container.parentNode.appendChild(this.separators);
this.container.addEventListener("mousedown", this, true);
this.container.addEventListener("keypress", this, true);
this.container.addEventListener("mouseover", this, true);
this.container.addEventListener("mouseleave", this, true);
// We will save a list of already displayed nodes in this array.
this.nodeHierarchy = [];
// Last selected node in nodeHierarchy.
this.currentIndex = -1;
// By default, hide the arrows. We let the show them
// in case of overflow.
this.container.removeAttribute("overflows");
this.container._scrollButtonUp.collapsed = true;
this.container._scrollButtonDown.collapsed = true;
this.onscrollboxreflow = () => {
if (this.container._scrollButtonDown.collapsed)
this.container.removeAttribute("overflows");
else
this.container.setAttribute("overflows", true);
};
this.container.addEventListener("underflow", this.onscrollboxreflow, false);
this.container.addEventListener("overflow", this.onscrollboxreflow, false);
this.update = this.update.bind(this);
this.updateSelectors = this.updateSelectors.bind(this);
this.selection.on("new-node-front", this.update);
this.selection.on("pseudoclass", this.updateSelectors);
this.selection.on("attribute-changed", this.updateSelectors);
this.inspector.on("markupmutation", this.update);
this.update();
},
/**
* Include in a promise's then() chain to reject the chain
* when the breadcrumbs' selection has changed while the promise
* was outstanding.
*/
selectionGuard: function() {
let selection = this.selection.nodeFront;
return (result) => {
if (selection != this.selection.nodeFront) {
return promise.reject("selection-changed");
}
return result;
}
},
/**
* Warn if rejection was caused by selection change, print an error otherwise.
*/
selectionGuardEnd: function(err) {
if (err === "selection-changed") {
console.warn("Asynchronous operation was aborted as selection changed.");
} else {
console.error(err);
}
},
/**
* Build a string that represents the node: tagName#id.class1.class2.
*
* @param aNode The node to pretty-print
* @returns a string
*/
prettyPrintNodeAsText: function BC_prettyPrintNodeText(aNode)
{
let text = aNode.tagName.toLowerCase();
if (aNode.isPseudoElement) {
text = aNode.isBeforePseudoElement ? "::before" : "::after";
}
if (aNode.id) {
text += "#" + aNode.id;
}
if (aNode.className) {
let classList = aNode.className.split(/\s+/);
for (let i = 0; i < classList.length; i++) {
text += "." + classList[i];
}
}
for (let pseudo of aNode.pseudoClassLocks) {
text += pseudo;
}
return text;
},
/**
* Build