michael@0: /* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0:
michael@0: const {Cc, Cu, Ci} = require("chrome");
michael@0:
michael@0: const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
michael@0: const ENSURE_SELECTION_VISIBLE_DELAY = 50; // ms
michael@0:
michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0: Cu.import("resource:///modules/devtools/DOMHelpers.jsm");
michael@0: Cu.import("resource://gre/modules/Services.jsm");
michael@0: Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
michael@0: const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data;
michael@0: const MAX_LABEL_LENGTH = 40;
michael@0:
michael@0: let promise = require("devtools/toolkit/deprecated-sync-thenables");
michael@0:
michael@0: const LOW_PRIORITY_ELEMENTS = {
michael@0: "HEAD": true,
michael@0: "BASE": true,
michael@0: "BASEFONT": true,
michael@0: "ISINDEX": true,
michael@0: "LINK": true,
michael@0: "META": true,
michael@0: "SCRIPT": true,
michael@0: "STYLE": true,
michael@0: "TITLE": true,
michael@0: };
michael@0:
michael@0: function resolveNextTick(value) {
michael@0: let deferred = promise.defer();
michael@0: Services.tm.mainThread.dispatch(() => {
michael@0: try {
michael@0: deferred.resolve(value);
michael@0: } catch(ex) {
michael@0: console.error(ex);
michael@0: }
michael@0: }, Ci.nsIThread.DISPATCH_NORMAL);
michael@0: return deferred.promise;
michael@0: }
michael@0:
michael@0: ///////////////////////////////////////////////////////////////////////////
michael@0: //// HTML Breadcrumbs
michael@0:
michael@0: /**
michael@0: * Display the ancestors of the current node and its children.
michael@0: * Only one "branch" of children are displayed (only one line).
michael@0: *
michael@0: * FIXME: Bug 822388 - Use the BreadcrumbsWidget in the Inspector.
michael@0: *
michael@0: * Mechanism:
michael@0: * . If no nodes displayed yet:
michael@0: * then display the ancestor of the selected node and the selected node;
michael@0: * else select the node;
michael@0: * . If the selected node is the last node displayed, append its first (if any).
michael@0: */
michael@0: function HTMLBreadcrumbs(aInspector)
michael@0: {
michael@0: this.inspector = aInspector;
michael@0: this.selection = this.inspector.selection;
michael@0: this.chromeWin = this.inspector.panelWin;
michael@0: this.chromeDoc = this.inspector.panelDoc;
michael@0: this.DOMHelpers = new DOMHelpers(this.chromeWin);
michael@0: this._init();
michael@0: }
michael@0:
michael@0: exports.HTMLBreadcrumbs = HTMLBreadcrumbs;
michael@0:
michael@0: HTMLBreadcrumbs.prototype = {
michael@0: get walker() this.inspector.walker,
michael@0:
michael@0: _init: function BC__init()
michael@0: {
michael@0: this.container = this.chromeDoc.getElementById("inspector-breadcrumbs");
michael@0:
michael@0: // These separators are used for CSS purposes only, and are positioned
michael@0: // off screen, but displayed with -moz-element.
michael@0: this.separators = this.chromeDoc.createElement("box");
michael@0: this.separators.className = "breadcrumb-separator-container";
michael@0: this.separators.innerHTML =
michael@0: "" +
michael@0: "" +
michael@0: "";
michael@0: this.container.parentNode.appendChild(this.separators);
michael@0:
michael@0: this.container.addEventListener("mousedown", this, true);
michael@0: this.container.addEventListener("keypress", this, true);
michael@0:
michael@0: // We will save a list of already displayed nodes in this array.
michael@0: this.nodeHierarchy = [];
michael@0:
michael@0: // Last selected node in nodeHierarchy.
michael@0: this.currentIndex = -1;
michael@0:
michael@0: // By default, hide the arrows. We let the show them
michael@0: // in case of overflow.
michael@0: this.container.removeAttribute("overflows");
michael@0: this.container._scrollButtonUp.collapsed = true;
michael@0: this.container._scrollButtonDown.collapsed = true;
michael@0:
michael@0: this.onscrollboxreflow = function() {
michael@0: if (this.container._scrollButtonDown.collapsed)
michael@0: this.container.removeAttribute("overflows");
michael@0: else
michael@0: this.container.setAttribute("overflows", true);
michael@0: }.bind(this);
michael@0:
michael@0: this.container.addEventListener("underflow", this.onscrollboxreflow, false);
michael@0: this.container.addEventListener("overflow", this.onscrollboxreflow, false);
michael@0:
michael@0: this.update = this.update.bind(this);
michael@0: this.updateSelectors = this.updateSelectors.bind(this);
michael@0: this.selection.on("new-node-front", this.update);
michael@0: this.selection.on("pseudoclass", this.updateSelectors);
michael@0: this.selection.on("attribute-changed", this.updateSelectors);
michael@0: this.inspector.on("markupmutation", this.update);
michael@0: this.update();
michael@0: },
michael@0:
michael@0: /**
michael@0: * Include in a promise's then() chain to reject the chain
michael@0: * when the breadcrumbs' selection has changed while the promise
michael@0: * was outstanding.
michael@0: */
michael@0: selectionGuard: function() {
michael@0: let selection = this.selection.nodeFront;
michael@0: return (result) => {
michael@0: if (selection != this.selection.nodeFront) {
michael@0: return promise.reject("selection-changed");
michael@0: }
michael@0: return result;
michael@0: }
michael@0: },
michael@0:
michael@0: /**
michael@0: * Print any errors (except selection guard errors).
michael@0: */
michael@0: selectionGuardEnd: function(err) {
michael@0: if (err != "selection-changed") {
michael@0: console.error(err);
michael@0: }
michael@0: promise.reject(err);
michael@0: },
michael@0:
michael@0: /**
michael@0: * Build a string that represents the node: tagName#id.class1.class2.
michael@0: *
michael@0: * @param aNode The node to pretty-print
michael@0: * @returns a string
michael@0: */
michael@0: prettyPrintNodeAsText: function BC_prettyPrintNodeText(aNode)
michael@0: {
michael@0: let text = aNode.tagName.toLowerCase();
michael@0: if (aNode.id) {
michael@0: text += "#" + aNode.id;
michael@0: }
michael@0:
michael@0: if (aNode.className) {
michael@0: let classList = aNode.className.split(/\s+/);
michael@0: for (let i = 0; i < classList.length; i++) {
michael@0: text += "." + classList[i];
michael@0: }
michael@0: }
michael@0:
michael@0: for (let pseudo of aNode.pseudoClassLocks) {
michael@0: text += pseudo;
michael@0: }
michael@0:
michael@0: return text;
michael@0: },
michael@0:
michael@0:
michael@0: /**
michael@0: * Build