2014-06-25 05:12:07 +00:00
|
|
|
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
2012-11-30 08:07:59 +00:00
|
|
|
/* 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/. */
|
|
|
|
|
2015-04-08 10:16:25 +00:00
|
|
|
"use strict";
|
2012-11-30 08:07:59 +00:00
|
|
|
|
2016-07-21 15:04:47 +00:00
|
|
|
/* eslint-disable mozilla/reject-some-requires */
|
2016-04-15 10:03:33 +00:00
|
|
|
const {Ci} = require("chrome");
|
2016-07-21 15:04:47 +00:00
|
|
|
/* eslint-enable mozilla/reject-some-requires */
|
2016-02-27 12:51:10 +00:00
|
|
|
const Services = require("Services");
|
2015-08-26 13:05:13 +00:00
|
|
|
const promise = require("promise");
|
2015-04-08 10:16:25 +00:00
|
|
|
|
2016-05-04 21:48:15 +00:00
|
|
|
const ELLIPSIS = Services.prefs.getComplexValue(
|
|
|
|
"intl.ellipsis",
|
|
|
|
Ci.nsIPrefLocalizedString).data;
|
2014-01-30 20:24:30 +00:00
|
|
|
const MAX_LABEL_LENGTH = 40;
|
2012-11-30 08:07:59 +00:00
|
|
|
|
2016-05-31 19:29:50 +00:00
|
|
|
const NS_XHTML = "http://www.w3.org/1999/xhtml";
|
|
|
|
const SCROLL_REPEAT_MS = 100;
|
|
|
|
|
2016-08-02 15:14:29 +00:00
|
|
|
const EventEmitter = require("devtools/shared/event-emitter");
|
|
|
|
const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
|
2016-05-31 19:29:50 +00:00
|
|
|
|
2016-06-06 00:25:39 +00:00
|
|
|
// Some margin may be required for visible element detection.
|
|
|
|
const SCROLL_MARGIN = 1;
|
|
|
|
|
2016-05-31 19:29:50 +00:00
|
|
|
/**
|
|
|
|
* Component to replicate functionality of XUL arrowscrollbox
|
|
|
|
* for breadcrumbs
|
|
|
|
*
|
|
|
|
* @param {Window} win The window containing the breadcrumbs
|
|
|
|
* @parem {DOMNode} container The element in which to put the scroll box
|
|
|
|
*/
|
|
|
|
function ArrowScrollBox(win, container) {
|
|
|
|
this.win = win;
|
|
|
|
this.doc = win.document;
|
|
|
|
this.container = container;
|
|
|
|
EventEmitter.decorate(this);
|
|
|
|
this.init();
|
|
|
|
}
|
|
|
|
|
|
|
|
ArrowScrollBox.prototype = {
|
|
|
|
|
2016-06-06 00:25:39 +00:00
|
|
|
// Scroll behavior, exposed for testing
|
|
|
|
scrollBehavior: "smooth",
|
|
|
|
|
2016-05-31 19:29:50 +00:00
|
|
|
/**
|
|
|
|
* Build the HTML, add to the DOM and start listening to
|
|
|
|
* events
|
|
|
|
*/
|
|
|
|
init: function () {
|
|
|
|
this.constructHtml();
|
|
|
|
|
|
|
|
this.onUnderflow();
|
|
|
|
|
|
|
|
this.onScroll = this.onScroll.bind(this);
|
|
|
|
this.onStartBtnClick = this.onStartBtnClick.bind(this);
|
|
|
|
this.onEndBtnClick = this.onEndBtnClick.bind(this);
|
|
|
|
this.onStartBtnDblClick = this.onStartBtnDblClick.bind(this);
|
|
|
|
this.onEndBtnDblClick = this.onEndBtnDblClick.bind(this);
|
|
|
|
this.onUnderflow = this.onUnderflow.bind(this);
|
|
|
|
this.onOverflow = this.onOverflow.bind(this);
|
|
|
|
|
|
|
|
this.inner.addEventListener("scroll", this.onScroll, false);
|
|
|
|
this.startBtn.addEventListener("mousedown", this.onStartBtnClick, false);
|
|
|
|
this.endBtn.addEventListener("mousedown", this.onEndBtnClick, false);
|
|
|
|
this.startBtn.addEventListener("dblclick", this.onStartBtnDblClick, false);
|
|
|
|
this.endBtn.addEventListener("dblclick", this.onEndBtnDblClick, false);
|
|
|
|
|
|
|
|
// Overflow and underflow are moz specific events
|
|
|
|
this.inner.addEventListener("underflow", this.onUnderflow, false);
|
|
|
|
this.inner.addEventListener("overflow", this.onOverflow, false);
|
|
|
|
},
|
|
|
|
|
2016-06-06 00:25:39 +00:00
|
|
|
/**
|
|
|
|
* Determine whether the current text directionality is RTL
|
|
|
|
*/
|
|
|
|
isRtl: function () {
|
|
|
|
return this.win.getComputedStyle(this.container).direction === "rtl";
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Scroll to the specified element using the current scroll behavior
|
|
|
|
* @param {Element} element element to scroll
|
|
|
|
* @param {String} block desired alignment of element after scrolling
|
|
|
|
*/
|
|
|
|
scrollToElement: function (element, block) {
|
|
|
|
element.scrollIntoView({ block: block, behavior: this.scrollBehavior });
|
|
|
|
},
|
|
|
|
|
2016-05-31 19:29:50 +00:00
|
|
|
/**
|
|
|
|
* Call the given function once; then continuously
|
|
|
|
* while the mouse button is held
|
2016-06-06 00:25:39 +00:00
|
|
|
* @param {Function} repeatFn the function to repeat while the button is held
|
2016-05-31 19:29:50 +00:00
|
|
|
*/
|
|
|
|
clickOrHold: function (repeatFn) {
|
|
|
|
let timer;
|
|
|
|
let container = this.container;
|
|
|
|
|
|
|
|
function handleClick() {
|
|
|
|
cancelHold();
|
|
|
|
repeatFn();
|
|
|
|
}
|
|
|
|
|
|
|
|
let window = this.win;
|
|
|
|
function cancelHold() {
|
|
|
|
window.clearTimeout(timer);
|
|
|
|
container.removeEventListener("mouseout", cancelHold, false);
|
|
|
|
container.removeEventListener("mouseup", handleClick, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
function repeated() {
|
|
|
|
repeatFn();
|
|
|
|
timer = window.setTimeout(repeated, SCROLL_REPEAT_MS);
|
|
|
|
}
|
|
|
|
|
|
|
|
container.addEventListener("mouseout", cancelHold, false);
|
|
|
|
container.addEventListener("mouseup", handleClick, false);
|
|
|
|
timer = window.setTimeout(repeated, SCROLL_REPEAT_MS);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* When start button is dbl clicked scroll to first element
|
|
|
|
*/
|
|
|
|
onStartBtnDblClick: function () {
|
|
|
|
let children = this.inner.childNodes;
|
|
|
|
if (children.length < 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let element = this.inner.childNodes[0];
|
2016-06-06 00:25:39 +00:00
|
|
|
this.scrollToElement(element, "start");
|
2016-05-31 19:29:50 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* When end button is dbl clicked scroll to last element
|
|
|
|
*/
|
|
|
|
onEndBtnDblClick: function () {
|
|
|
|
let children = this.inner.childNodes;
|
|
|
|
if (children.length < 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let element = children[children.length - 1];
|
2016-06-06 00:25:39 +00:00
|
|
|
this.scrollToElement(element, "start");
|
2016-05-31 19:29:50 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* When start arrow button is clicked scroll towards first element
|
|
|
|
*/
|
|
|
|
onStartBtnClick: function () {
|
|
|
|
let scrollToStart = () => {
|
|
|
|
let element = this.getFirstInvisibleElement();
|
|
|
|
if (!element) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-06-06 00:25:39 +00:00
|
|
|
let block = this.isRtl() ? "end" : "start";
|
|
|
|
this.scrollToElement(element, block);
|
2016-05-31 19:29:50 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
this.clickOrHold(scrollToStart);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* When end arrow button is clicked scroll towards last element
|
|
|
|
*/
|
|
|
|
onEndBtnClick: function () {
|
|
|
|
let scrollToEnd = () => {
|
|
|
|
let element = this.getLastInvisibleElement();
|
|
|
|
if (!element) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-06-06 00:25:39 +00:00
|
|
|
let block = this.isRtl() ? "start" : "end";
|
|
|
|
this.scrollToElement(element, block);
|
2016-05-31 19:29:50 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
this.clickOrHold(scrollToEnd);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Event handler for scrolling, update the
|
|
|
|
* enabled/disabled status of the arrow buttons
|
|
|
|
*/
|
|
|
|
onScroll: function () {
|
|
|
|
let first = this.getFirstInvisibleElement();
|
|
|
|
if (!first) {
|
|
|
|
this.startBtn.setAttribute("disabled", "true");
|
|
|
|
} else {
|
|
|
|
this.startBtn.removeAttribute("disabled");
|
|
|
|
}
|
|
|
|
|
|
|
|
let last = this.getLastInvisibleElement();
|
|
|
|
if (!last) {
|
|
|
|
this.endBtn.setAttribute("disabled", "true");
|
|
|
|
} else {
|
|
|
|
this.endBtn.removeAttribute("disabled");
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* On underflow, make the arrow buttons invisible
|
|
|
|
*/
|
|
|
|
onUnderflow: function () {
|
|
|
|
this.startBtn.style.visibility = "collapse";
|
|
|
|
this.endBtn.style.visibility = "collapse";
|
|
|
|
this.emit("underflow");
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* On overflow, show the arrow buttons
|
|
|
|
*/
|
|
|
|
onOverflow: function () {
|
|
|
|
this.startBtn.style.visibility = "visible";
|
|
|
|
this.endBtn.style.visibility = "visible";
|
|
|
|
this.emit("overflow");
|
|
|
|
},
|
|
|
|
|
2016-06-06 00:25:39 +00:00
|
|
|
/**
|
|
|
|
* Check whether the element is to the left of its container but does
|
|
|
|
* not also span the entire container.
|
|
|
|
* @param {Number} left the left scroll point of the container
|
|
|
|
* @param {Number} right the right edge of the container
|
|
|
|
* @param {Number} elementLeft the left edge of the element
|
|
|
|
* @param {Number} elementRight the right edge of the element
|
|
|
|
*/
|
|
|
|
elementLeftOfContainer: function (left, right, elementLeft, elementRight) {
|
|
|
|
return elementLeft < (left - SCROLL_MARGIN)
|
|
|
|
&& elementRight < (right - SCROLL_MARGIN);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check whether the element is to the right of its container but does
|
|
|
|
* not also span the entire container.
|
|
|
|
* @param {Number} left the left scroll point of the container
|
|
|
|
* @param {Number} right the right edge of the container
|
|
|
|
* @param {Number} elementLeft the left edge of the element
|
|
|
|
* @param {Number} elementRight the right edge of the element
|
|
|
|
*/
|
|
|
|
elementRightOfContainer: function (left, right, elementLeft, elementRight) {
|
|
|
|
return elementLeft > (left + SCROLL_MARGIN)
|
|
|
|
&& elementRight > (right + SCROLL_MARGIN);
|
|
|
|
},
|
|
|
|
|
2016-05-31 19:29:50 +00:00
|
|
|
/**
|
|
|
|
* Get the first (i.e. furthest left for LTR)
|
2016-06-06 00:25:39 +00:00
|
|
|
* non or partly visible element in the scroll box
|
2016-05-31 19:29:50 +00:00
|
|
|
*/
|
|
|
|
getFirstInvisibleElement: function () {
|
2016-06-06 00:25:39 +00:00
|
|
|
let elementsList = Array.from(this.inner.childNodes).reverse();
|
2016-05-31 19:29:50 +00:00
|
|
|
|
2016-06-06 00:25:39 +00:00
|
|
|
let predicate = this.isRtl() ?
|
|
|
|
this.elementRightOfContainer : this.elementLeftOfContainer;
|
|
|
|
return this.findFirstWithBounds(elementsList, predicate);
|
2016-05-31 19:29:50 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the last (i.e. furthest right for LTR)
|
2016-06-06 00:25:39 +00:00
|
|
|
* non or partly visible element in the scroll box
|
2016-05-31 19:29:50 +00:00
|
|
|
*/
|
|
|
|
getLastInvisibleElement: function () {
|
2016-06-06 00:25:39 +00:00
|
|
|
let predicate = this.isRtl() ?
|
|
|
|
this.elementLeftOfContainer : this.elementRightOfContainer;
|
|
|
|
return this.findFirstWithBounds(this.inner.childNodes, predicate);
|
|
|
|
},
|
2016-05-31 19:29:50 +00:00
|
|
|
|
2016-06-06 00:25:39 +00:00
|
|
|
/**
|
|
|
|
* Find the first element that matches the given predicate, called with bounds
|
|
|
|
* information
|
|
|
|
* @param {Array} elements an ordered list of elements
|
|
|
|
* @param {Function} predicate a function to be called with bounds
|
|
|
|
* information
|
|
|
|
*/
|
|
|
|
findFirstWithBounds: function (elements, predicate) {
|
|
|
|
let left = this.inner.scrollLeft;
|
|
|
|
let right = left + this.inner.clientWidth;
|
|
|
|
for (let element of elements) {
|
|
|
|
let elementLeft = element.offsetLeft - element.parentElement.offsetLeft;
|
|
|
|
let elementRight = elementLeft + element.offsetWidth;
|
|
|
|
|
|
|
|
// Check that the starting edge of the element is out of the visible area
|
|
|
|
// and that the ending edge does not span the whole container
|
|
|
|
if (predicate(left, right, elementLeft, elementRight)) {
|
|
|
|
return element;
|
|
|
|
}
|
2016-05-31 19:29:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Build the HTML for the scroll box and insert it into the DOM
|
|
|
|
*/
|
|
|
|
constructHtml: function () {
|
|
|
|
this.startBtn = this.createElement("div", "scrollbutton-up",
|
|
|
|
this.container);
|
|
|
|
this.createElement("div", "toolbarbutton-icon", this.startBtn);
|
|
|
|
|
|
|
|
this.createElement("div", "arrowscrollbox-overflow-start-indicator",
|
|
|
|
this.container);
|
|
|
|
this.inner = this.createElement("div", "html-arrowscrollbox-inner",
|
|
|
|
this.container);
|
|
|
|
this.createElement("div", "arrowscrollbox-overflow-end-indicator",
|
|
|
|
this.container);
|
|
|
|
|
|
|
|
this.endBtn = this.createElement("div", "scrollbutton-down",
|
|
|
|
this.container);
|
|
|
|
this.createElement("div", "toolbarbutton-icon", this.endBtn);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create an XHTML element with the given class name, and append it to the
|
|
|
|
* parent.
|
|
|
|
* @param {String} tagName name of the tag to create
|
|
|
|
* @param {String} className class of the element
|
|
|
|
* @param {DOMNode} parent the parent node to which it should be appended
|
|
|
|
* @return {DOMNode} The new element
|
|
|
|
*/
|
|
|
|
createElement: function (tagName, className, parent) {
|
|
|
|
let el = this.doc.createElementNS(NS_XHTML, tagName);
|
|
|
|
el.className = className;
|
|
|
|
if (parent) {
|
|
|
|
parent.appendChild(el);
|
|
|
|
}
|
|
|
|
|
|
|
|
return el;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove event handlers and clean up
|
|
|
|
*/
|
|
|
|
destroy: function () {
|
|
|
|
this.inner.removeEventListener("scroll", this.onScroll, false);
|
|
|
|
this.startBtn.removeEventListener("mousedown",
|
|
|
|
this.onStartBtnClick, false);
|
|
|
|
this.endBtn.removeEventListener("mousedown", this.onEndBtnClick, false);
|
|
|
|
this.startBtn.removeEventListener("dblclick",
|
|
|
|
this.onStartBtnDblClick, false);
|
|
|
|
this.endBtn.removeEventListener("dblclick",
|
|
|
|
this.onRightBtnDblClick, false);
|
|
|
|
|
|
|
|
// Overflow and underflow are moz specific events
|
|
|
|
this.inner.removeEventListener("underflow", this.onUnderflow, false);
|
|
|
|
this.inner.removeEventListener("overflow", this.onOverflow, false);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2012-11-30 08:07:59 +00:00
|
|
|
/**
|
|
|
|
* Display the ancestors of the current node and its children.
|
|
|
|
* Only one "branch" of children are displayed (only one line).
|
|
|
|
*
|
|
|
|
* Mechanism:
|
2015-04-08 10:16:25 +00:00
|
|
|
* - If no nodes displayed yet:
|
|
|
|
* then display the ancestor of the selected node and the selected node;
|
2012-11-30 08:07:59 +00:00
|
|
|
* else select the node;
|
2015-04-08 10:16:25 +00:00
|
|
|
* - If the selected node is the last node displayed, append its first (if any).
|
|
|
|
*
|
|
|
|
* @param {InspectorPanel} inspector The inspector hosting this widget.
|
2012-11-30 08:07:59 +00:00
|
|
|
*/
|
2015-04-08 10:16:25 +00:00
|
|
|
function HTMLBreadcrumbs(inspector) {
|
|
|
|
this.inspector = inspector;
|
2012-11-30 08:07:59 +00:00
|
|
|
this.selection = this.inspector.selection;
|
|
|
|
this.chromeWin = this.inspector.panelWin;
|
|
|
|
this.chromeDoc = this.inspector.panelDoc;
|
|
|
|
this._init();
|
|
|
|
}
|
|
|
|
|
2013-04-11 20:59:08 +00:00
|
|
|
exports.HTMLBreadcrumbs = HTMLBreadcrumbs;
|
|
|
|
|
2012-11-30 08:07:59 +00:00
|
|
|
HTMLBreadcrumbs.prototype = {
|
2015-05-22 18:50:01 +00:00
|
|
|
get walker() {
|
|
|
|
return this.inspector.walker;
|
|
|
|
},
|
2013-06-07 21:33:39 +00:00
|
|
|
|
2016-05-04 21:48:15 +00:00
|
|
|
_init: function () {
|
2016-05-31 19:29:50 +00:00
|
|
|
this.outer = this.chromeDoc.getElementById("inspector-breadcrumbs");
|
|
|
|
this.arrowScrollBox = new ArrowScrollBox(
|
|
|
|
this.chromeWin,
|
|
|
|
this.outer);
|
|
|
|
|
|
|
|
this.container = this.arrowScrollBox.inner;
|
2016-06-02 14:32:46 +00:00
|
|
|
this.scroll = this.scroll.bind(this);
|
2016-05-31 19:29:50 +00:00
|
|
|
this.arrowScrollBox.on("overflow", this.scroll);
|
2014-01-30 20:24:30 +00:00
|
|
|
|
|
|
|
// These separators are used for CSS purposes only, and are positioned
|
|
|
|
// off screen, but displayed with -moz-element.
|
2016-05-31 19:29:50 +00:00
|
|
|
this.separators = this.chromeDoc.createElementNS(NS_XHTML, "div");
|
2014-01-30 20:24:30 +00:00
|
|
|
this.separators.className = "breadcrumb-separator-container";
|
|
|
|
this.separators.innerHTML =
|
2016-05-31 19:29:50 +00:00
|
|
|
"<div id='breadcrumb-separator-before'></div>" +
|
|
|
|
"<div id='breadcrumb-separator-after'></div>" +
|
|
|
|
"<div id='breadcrumb-separator-normal'></div>";
|
2014-01-30 20:24:30 +00:00
|
|
|
this.container.parentNode.appendChild(this.separators);
|
|
|
|
|
2016-05-31 19:29:50 +00:00
|
|
|
this.outer.addEventListener("click", this, true);
|
|
|
|
this.outer.addEventListener("mouseover", this, true);
|
2015-07-20 19:13:00 +00:00
|
|
|
this.outer.addEventListener("mouseout", this, true);
|
2016-05-31 19:29:50 +00:00
|
|
|
this.outer.addEventListener("focus", this, true);
|
2012-11-30 08:07:59 +00:00
|
|
|
|
2016-06-02 10:04:14 +00:00
|
|
|
this.shortcuts = new KeyShortcuts({ window: this.chromeWin, target: this.outer });
|
|
|
|
this.handleShortcut = this.handleShortcut.bind(this);
|
|
|
|
|
|
|
|
this.shortcuts.on("Right", this.handleShortcut);
|
|
|
|
this.shortcuts.on("Left", this.handleShortcut);
|
|
|
|
|
2012-11-30 08:07:59 +00:00
|
|
|
// We will save a list of already displayed nodes in this array.
|
|
|
|
this.nodeHierarchy = [];
|
|
|
|
|
|
|
|
// Last selected node in nodeHierarchy.
|
|
|
|
this.currentIndex = -1;
|
|
|
|
|
2016-08-03 20:52:59 +00:00
|
|
|
// Used to build a unique breadcrumb button Id.
|
|
|
|
this.breadcrumbsWidgetItemId = 0;
|
|
|
|
|
2012-11-30 08:07:59 +00:00
|
|
|
this.update = this.update.bind(this);
|
|
|
|
this.updateSelectors = this.updateSelectors.bind(this);
|
2013-06-07 21:33:39 +00:00
|
|
|
this.selection.on("new-node-front", this.update);
|
2012-11-30 08:07:59 +00:00
|
|
|
this.selection.on("pseudoclass", this.updateSelectors);
|
|
|
|
this.selection.on("attribute-changed", this.updateSelectors);
|
2013-10-02 00:14:00 +00:00
|
|
|
this.inspector.on("markupmutation", this.update);
|
2012-11-30 08:07:59 +00:00
|
|
|
this.update();
|
|
|
|
},
|
|
|
|
|
2013-06-07 21:33:39 +00:00
|
|
|
/**
|
|
|
|
|
2012-11-30 08:07:59 +00:00
|
|
|
* Build a string that represents the node: tagName#id.class1.class2.
|
2015-04-08 10:16:25 +00:00
|
|
|
* @param {NodeFront} node The node to pretty-print
|
|
|
|
* @return {String}
|
2012-11-30 08:07:59 +00:00
|
|
|
*/
|
2016-05-04 21:48:15 +00:00
|
|
|
prettyPrintNodeAsText: function (node) {
|
2016-05-12 05:18:58 +00:00
|
|
|
let text = node.displayName;
|
2015-04-08 10:16:25 +00:00
|
|
|
if (node.isPseudoElement) {
|
|
|
|
text = node.isBeforePseudoElement ? "::before" : "::after";
|
2014-09-29 07:29:00 +00:00
|
|
|
}
|
|
|
|
|
2015-04-08 10:16:25 +00:00
|
|
|
if (node.id) {
|
|
|
|
text += "#" + node.id;
|
2012-11-30 08:07:59 +00:00
|
|
|
}
|
2013-06-07 21:33:39 +00:00
|
|
|
|
2015-04-08 10:16:25 +00:00
|
|
|
if (node.className) {
|
|
|
|
let classList = node.className.split(/\s+/);
|
2013-06-07 21:33:39 +00:00
|
|
|
for (let i = 0; i < classList.length; i++) {
|
|
|
|
text += "." + classList[i];
|
|
|
|
}
|
2012-11-30 08:07:59 +00:00
|
|
|
}
|
2013-06-07 21:33:39 +00:00
|
|
|
|
2015-04-08 10:16:25 +00:00
|
|
|
for (let pseudo of node.pseudoClassLocks) {
|
2013-06-11 04:18:46 +00:00
|
|
|
text += pseudo;
|
2012-11-30 08:07:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return text;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2016-05-31 19:29:50 +00:00
|
|
|
* Build <span>s that represent the node:
|
|
|
|
* <span class="breadcrumbs-widget-item-tag">tagName</span>
|
|
|
|
* <span class="breadcrumbs-widget-item-id">#id</span>
|
|
|
|
* <span class="breadcrumbs-widget-item-classes">.class1.class2</span>
|
2015-04-08 10:16:25 +00:00
|
|
|
* @param {NodeFront} node The node to pretty-print
|
|
|
|
* @returns {DocumentFragment}
|
2012-11-30 08:07:59 +00:00
|
|
|
*/
|
2016-05-31 19:29:50 +00:00
|
|
|
prettyPrintNodeAsXHTML: function (node) {
|
|
|
|
let tagLabel = this.chromeDoc.createElementNS(NS_XHTML, "span");
|
2013-02-20 23:33:36 +00:00
|
|
|
tagLabel.className = "breadcrumbs-widget-item-tag plain";
|
2012-11-30 08:07:59 +00:00
|
|
|
|
2016-05-31 19:29:50 +00:00
|
|
|
let idLabel = this.chromeDoc.createElementNS(NS_XHTML, "span");
|
2013-02-20 23:33:36 +00:00
|
|
|
idLabel.className = "breadcrumbs-widget-item-id plain";
|
2012-11-30 08:07:59 +00:00
|
|
|
|
2016-05-31 19:29:50 +00:00
|
|
|
let classesLabel = this.chromeDoc.createElementNS(NS_XHTML, "span");
|
2013-02-20 23:33:36 +00:00
|
|
|
classesLabel.className = "breadcrumbs-widget-item-classes plain";
|
2012-11-30 08:07:59 +00:00
|
|
|
|
2016-05-31 19:29:50 +00:00
|
|
|
let pseudosLabel = this.chromeDoc.createElementNS(NS_XHTML, "span");
|
2013-02-20 23:33:36 +00:00
|
|
|
pseudosLabel.className = "breadcrumbs-widget-item-pseudo-classes plain";
|
2012-11-30 08:07:59 +00:00
|
|
|
|
2016-05-12 05:18:58 +00:00
|
|
|
let tagText = node.displayName;
|
2015-04-08 10:16:25 +00:00
|
|
|
if (node.isPseudoElement) {
|
|
|
|
tagText = node.isBeforePseudoElement ? "::before" : "::after";
|
2014-09-29 07:29:00 +00:00
|
|
|
}
|
2015-04-08 10:16:25 +00:00
|
|
|
let idText = node.id ? ("#" + node.id) : "";
|
2014-01-30 20:24:30 +00:00
|
|
|
let classesText = "";
|
2012-11-30 08:07:59 +00:00
|
|
|
|
2015-04-08 10:16:25 +00:00
|
|
|
if (node.className) {
|
|
|
|
let classList = node.className.split(/\s+/);
|
2013-06-07 21:33:39 +00:00
|
|
|
for (let i = 0; i < classList.length; i++) {
|
|
|
|
classesText += "." + classList[i];
|
|
|
|
}
|
2012-11-30 08:07:59 +00:00
|
|
|
}
|
2013-06-07 21:33:39 +00:00
|
|
|
|
2014-01-30 20:24:30 +00:00
|
|
|
// Figure out which element (if any) needs ellipsing.
|
|
|
|
// Substring for that element, then clear out any extras
|
|
|
|
// (except for pseudo elements).
|
|
|
|
let maxTagLength = MAX_LABEL_LENGTH;
|
|
|
|
let maxIdLength = MAX_LABEL_LENGTH - tagText.length;
|
|
|
|
let maxClassLength = MAX_LABEL_LENGTH - tagText.length - idText.length;
|
|
|
|
|
|
|
|
if (tagText.length > maxTagLength) {
|
2015-06-02 09:26:24 +00:00
|
|
|
tagText = tagText.substr(0, maxTagLength) + ELLIPSIS;
|
|
|
|
idText = classesText = "";
|
2014-01-30 20:24:30 +00:00
|
|
|
} else if (idText.length > maxIdLength) {
|
2015-06-02 09:26:24 +00:00
|
|
|
idText = idText.substr(0, maxIdLength) + ELLIPSIS;
|
|
|
|
classesText = "";
|
2014-01-30 20:24:30 +00:00
|
|
|
} else if (classesText.length > maxClassLength) {
|
|
|
|
classesText = classesText.substr(0, maxClassLength) + ELLIPSIS;
|
|
|
|
}
|
|
|
|
|
|
|
|
tagLabel.textContent = tagText;
|
|
|
|
idLabel.textContent = idText;
|
|
|
|
classesLabel.textContent = classesText;
|
2015-04-08 10:16:25 +00:00
|
|
|
pseudosLabel.textContent = node.pseudoClassLocks.join("");
|
2012-11-30 08:07:59 +00:00
|
|
|
|
2016-05-31 19:29:50 +00:00
|
|
|
let fragment = this.chromeDoc.createDocumentFragment();
|
2012-11-30 08:07:59 +00:00
|
|
|
fragment.appendChild(tagLabel);
|
|
|
|
fragment.appendChild(idLabel);
|
|
|
|
fragment.appendChild(classesLabel);
|
|
|
|
fragment.appendChild(pseudosLabel);
|
|
|
|
|
|
|
|
return fragment;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generic event handler.
|
2015-04-08 10:16:25 +00:00
|
|
|
* @param {DOMEvent} event.
|
2012-11-30 08:07:59 +00:00
|
|
|
*/
|
2016-05-04 21:48:15 +00:00
|
|
|
handleEvent: function (event) {
|
|
|
|
if (event.type == "click" && event.button == 0) {
|
|
|
|
this.handleClick(event);
|
2015-04-08 10:16:25 +00:00
|
|
|
} else if (event.type == "mouseover") {
|
|
|
|
this.handleMouseOver(event);
|
2015-07-20 19:13:00 +00:00
|
|
|
} else if (event.type == "mouseout") {
|
2016-08-01 20:53:22 +00:00
|
|
|
this.handleMouseOut(event);
|
2016-04-12 15:53:54 +00:00
|
|
|
} else if (event.type == "focus") {
|
|
|
|
this.handleFocus(event);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2016-08-03 20:52:59 +00:00
|
|
|
* Focus event handler. When breadcrumbs container gets focus,
|
|
|
|
* aria-activedescendant needs to be updated to currently selected
|
|
|
|
* breadcrumb. Ensures that the focus stays on the container at all times.
|
2016-04-12 15:53:54 +00:00
|
|
|
* @param {DOMEvent} event.
|
|
|
|
*/
|
2016-05-04 21:48:15 +00:00
|
|
|
handleFocus: function (event) {
|
2016-08-03 20:52:59 +00:00
|
|
|
event.stopPropagation();
|
|
|
|
|
|
|
|
this.outer.setAttribute("aria-activedescendant",
|
|
|
|
this.nodeHierarchy[this.currentIndex].button.id);
|
|
|
|
|
|
|
|
this.outer.focus();
|
2015-04-08 10:16:25 +00:00
|
|
|
},
|
2012-11-30 08:07:59 +00:00
|
|
|
|
2015-04-08 10:16:25 +00:00
|
|
|
/**
|
2016-05-04 21:48:15 +00:00
|
|
|
* On click navigate to the correct node.
|
2015-04-08 10:16:25 +00:00
|
|
|
* @param {DOMEvent} event.
|
|
|
|
*/
|
2016-05-04 21:48:15 +00:00
|
|
|
handleClick: function (event) {
|
|
|
|
let target = event.originalTarget;
|
|
|
|
if (target.tagName == "button") {
|
|
|
|
target.onBreadcrumbsClick();
|
2015-04-08 10:16:25 +00:00
|
|
|
}
|
|
|
|
},
|
2013-06-07 21:33:39 +00:00
|
|
|
|
2015-04-08 10:16:25 +00:00
|
|
|
/**
|
|
|
|
* On mouse over, highlight the corresponding content DOM Node.
|
|
|
|
* @param {DOMEvent} event.
|
|
|
|
*/
|
2016-05-04 21:48:15 +00:00
|
|
|
handleMouseOver: function (event) {
|
2015-04-08 10:16:25 +00:00
|
|
|
let target = event.originalTarget;
|
|
|
|
if (target.tagName == "button") {
|
|
|
|
target.onBreadcrumbsHover();
|
2012-11-30 08:07:59 +00:00
|
|
|
}
|
2015-04-08 10:16:25 +00:00
|
|
|
},
|
2014-07-30 19:46:00 +00:00
|
|
|
|
2015-04-08 10:16:25 +00:00
|
|
|
/**
|
2015-07-20 19:13:00 +00:00
|
|
|
* On mouse out, make sure to unhighlight.
|
2015-04-08 10:16:25 +00:00
|
|
|
* @param {DOMEvent} event.
|
|
|
|
*/
|
2015-07-20 19:13:00 +00:00
|
|
|
handleMouseOut: function (event) {
|
2015-04-08 10:16:25 +00:00
|
|
|
this.inspector.toolbox.highlighterUtils.unhighlight();
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2016-06-02 10:04:14 +00:00
|
|
|
* Handle a keyboard shortcut supported by the breadcrumbs widget.
|
|
|
|
*
|
|
|
|
* @param {String} name
|
|
|
|
* Name of the keyboard shortcut received.
|
|
|
|
* @param {DOMEvent} event
|
|
|
|
* Original event that triggered the shortcut.
|
2015-04-08 10:16:25 +00:00
|
|
|
*/
|
2016-06-02 10:04:14 +00:00
|
|
|
handleShortcut: function (name, event) {
|
|
|
|
if (!this.selection.isElementNode()) {
|
2016-04-15 10:03:33 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
|
|
this.keyPromise = (this.keyPromise || promise.resolve(null)).then(() => {
|
2016-08-03 20:52:59 +00:00
|
|
|
let currentnode;
|
2016-06-02 10:04:14 +00:00
|
|
|
if (name === "Left" && this.currentIndex != 0) {
|
2016-08-03 20:52:59 +00:00
|
|
|
currentnode = this.nodeHierarchy[this.currentIndex - 1];
|
2016-06-02 10:04:14 +00:00
|
|
|
} else if (name === "Right" && this.currentIndex < this.nodeHierarchy.length - 1) {
|
2016-08-03 20:52:59 +00:00
|
|
|
currentnode = this.nodeHierarchy[this.currentIndex + 1];
|
|
|
|
} else {
|
|
|
|
return null;
|
2014-07-30 19:46:00 +00:00
|
|
|
}
|
|
|
|
|
2016-08-03 20:52:59 +00:00
|
|
|
this.outer.setAttribute("aria-activedescendant", currentnode.button.id);
|
|
|
|
return this.selection.setNodeFront(currentnode.node, "breadcrumbs");
|
2015-04-08 10:16:25 +00:00
|
|
|
});
|
2012-11-30 08:07:59 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2015-04-08 10:16:25 +00:00
|
|
|
* Remove nodes and clean up.
|
2012-11-30 08:07:59 +00:00
|
|
|
*/
|
2016-05-04 21:48:15 +00:00
|
|
|
destroy: function () {
|
2013-06-07 21:33:39 +00:00
|
|
|
this.selection.off("new-node-front", this.update);
|
2012-11-30 08:07:59 +00:00
|
|
|
this.selection.off("pseudoclass", this.updateSelectors);
|
|
|
|
this.selection.off("attribute-changed", this.updateSelectors);
|
2013-10-02 00:14:00 +00:00
|
|
|
this.inspector.off("markupmutation", this.update);
|
2012-11-30 08:07:59 +00:00
|
|
|
|
2016-05-04 21:48:15 +00:00
|
|
|
this.container.removeEventListener("click", this, true);
|
2014-07-30 19:46:00 +00:00
|
|
|
this.container.removeEventListener("mouseover", this, true);
|
2015-07-20 19:13:00 +00:00
|
|
|
this.container.removeEventListener("mouseout", this, true);
|
2016-04-12 15:53:54 +00:00
|
|
|
this.container.removeEventListener("focus", this, true);
|
2016-06-02 10:04:14 +00:00
|
|
|
this.shortcuts.destroy();
|
2014-01-30 20:24:30 +00:00
|
|
|
|
2015-04-08 10:16:25 +00:00
|
|
|
this.empty();
|
2014-01-30 20:24:30 +00:00
|
|
|
this.separators.remove();
|
|
|
|
|
2016-05-31 19:29:50 +00:00
|
|
|
this.arrowScrollBox.off("overflow", this.scroll);
|
|
|
|
this.arrowScrollBox.destroy();
|
|
|
|
this.arrowScrollBox = null;
|
|
|
|
this.outer = null;
|
2015-04-08 10:16:25 +00:00
|
|
|
this.container = null;
|
|
|
|
this.separators = null;
|
2012-11-30 08:07:59 +00:00
|
|
|
this.nodeHierarchy = null;
|
2014-07-01 04:32:00 +00:00
|
|
|
|
|
|
|
this.isDestroyed = true;
|
2012-11-30 08:07:59 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Empty the breadcrumbs container.
|
|
|
|
*/
|
2016-05-04 21:48:15 +00:00
|
|
|
empty: function () {
|
2012-11-30 08:07:59 +00:00
|
|
|
while (this.container.hasChildNodes()) {
|
2015-04-08 10:16:25 +00:00
|
|
|
this.container.firstChild.remove();
|
2012-11-30 08:07:59 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set which button represent the selected node.
|
2015-04-08 10:16:25 +00:00
|
|
|
* @param {Number} index Index of the displayed-button to select.
|
2012-11-30 08:07:59 +00:00
|
|
|
*/
|
2016-05-04 21:48:15 +00:00
|
|
|
setCursor: function (index) {
|
2012-11-30 08:07:59 +00:00
|
|
|
// Unselect the previously selected button
|
2016-05-04 21:48:15 +00:00
|
|
|
if (this.currentIndex > -1
|
|
|
|
&& this.currentIndex < this.nodeHierarchy.length) {
|
2012-11-30 08:07:59 +00:00
|
|
|
this.nodeHierarchy[this.currentIndex].button.removeAttribute("checked");
|
|
|
|
}
|
2015-04-08 10:16:25 +00:00
|
|
|
if (index > -1) {
|
|
|
|
this.nodeHierarchy[index].button.setAttribute("checked", "true");
|
2015-06-02 09:26:24 +00:00
|
|
|
if (this.hadFocus) {
|
2015-04-08 10:16:25 +00:00
|
|
|
this.nodeHierarchy[index].button.focus();
|
2015-06-02 09:26:24 +00:00
|
|
|
}
|
2012-11-30 08:07:59 +00:00
|
|
|
}
|
2015-04-08 10:16:25 +00:00
|
|
|
this.currentIndex = index;
|
2012-11-30 08:07:59 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the index of the node in the cache.
|
2015-04-08 10:16:25 +00:00
|
|
|
* @param {NodeFront} node.
|
|
|
|
* @returns {Number} The index for this node or -1 if not found.
|
2012-11-30 08:07:59 +00:00
|
|
|
*/
|
2016-05-04 21:48:15 +00:00
|
|
|
indexOf: function (node) {
|
2012-11-30 08:07:59 +00:00
|
|
|
for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) {
|
2015-04-08 10:16:25 +00:00
|
|
|
if (this.nodeHierarchy[i].node === node) {
|
2012-11-30 08:07:59 +00:00
|
|
|
return i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
2015-04-08 10:16:25 +00:00
|
|
|
* Remove all the buttons and their references in the cache after a given
|
|
|
|
* index.
|
|
|
|
* @param {Number} index.
|
2012-11-30 08:07:59 +00:00
|
|
|
*/
|
2016-05-04 21:48:15 +00:00
|
|
|
cutAfter: function (index) {
|
2015-04-08 10:16:25 +00:00
|
|
|
while (this.nodeHierarchy.length > (index + 1)) {
|
2012-11-30 08:07:59 +00:00
|
|
|
let toRemove = this.nodeHierarchy.pop();
|
|
|
|
this.container.removeChild(toRemove.button);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Build a button representing the node.
|
2015-04-08 10:16:25 +00:00
|
|
|
* @param {NodeFront} node The node from the page.
|
|
|
|
* @return {DOMNode} The <button> for this node.
|
2012-11-30 08:07:59 +00:00
|
|
|
*/
|
2016-05-04 21:48:15 +00:00
|
|
|
buildButton: function (node) {
|
2016-05-31 19:29:50 +00:00
|
|
|
let button = this.chromeDoc.createElementNS(NS_XHTML, "button");
|
|
|
|
button.appendChild(this.prettyPrintNodeAsXHTML(node));
|
2013-02-20 23:33:36 +00:00
|
|
|
button.className = "breadcrumbs-widget-item";
|
2016-08-03 20:52:59 +00:00
|
|
|
button.id = "breadcrumbs-widget-item-" + this.breadcrumbsWidgetItemId++;
|
2012-11-30 08:07:59 +00:00
|
|
|
|
2016-08-03 20:52:59 +00:00
|
|
|
button.setAttribute("tabindex", "-1");
|
2016-05-31 19:29:50 +00:00
|
|
|
button.setAttribute("title", this.prettyPrintNodeAsText(node));
|
2012-11-30 08:07:59 +00:00
|
|
|
|
2016-05-04 21:48:15 +00:00
|
|
|
button.onclick = () => {
|
|
|
|
button.focus();
|
|
|
|
};
|
|
|
|
|
2014-06-14 10:49:00 +00:00
|
|
|
button.onBreadcrumbsClick = () => {
|
2016-04-15 10:03:33 +00:00
|
|
|
this.selection.setNodeFront(node, "breadcrumbs");
|
2014-06-14 10:49:00 +00:00
|
|
|
};
|
2012-11-30 08:07:59 +00:00
|
|
|
|
2014-07-30 19:46:00 +00:00
|
|
|
button.onBreadcrumbsHover = () => {
|
2015-04-08 10:16:25 +00:00
|
|
|
this.inspector.toolbox.highlighterUtils.highlightNodeFront(node);
|
2014-07-30 19:46:00 +00:00
|
|
|
};
|
|
|
|
|
2012-11-30 08:07:59 +00:00
|
|
|
return button;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Connecting the end of the breadcrumbs to a node.
|
2015-04-08 10:16:25 +00:00
|
|
|
* @param {NodeFront} node The node to reach.
|
2012-11-30 08:07:59 +00:00
|
|
|
*/
|
2016-05-04 21:48:15 +00:00
|
|
|
expand: function (node) {
|
2015-04-08 10:16:25 +00:00
|
|
|
let fragment = this.chromeDoc.createDocumentFragment();
|
|
|
|
let lastButtonInserted = null;
|
|
|
|
let originalLength = this.nodeHierarchy.length;
|
|
|
|
let stopNode = null;
|
|
|
|
if (originalLength > 0) {
|
|
|
|
stopNode = this.nodeHierarchy[originalLength - 1].node;
|
|
|
|
}
|
|
|
|
while (node && node != stopNode) {
|
|
|
|
if (node.tagName) {
|
|
|
|
let button = this.buildButton(node);
|
|
|
|
fragment.insertBefore(button, lastButtonInserted);
|
|
|
|
lastButtonInserted = button;
|
2015-04-13 08:22:05 +00:00
|
|
|
this.nodeHierarchy.splice(originalLength, 0, {
|
|
|
|
node,
|
|
|
|
button,
|
|
|
|
currentPrettyPrintText: this.prettyPrintNodeAsText(node)
|
|
|
|
});
|
2012-11-30 08:07:59 +00:00
|
|
|
}
|
2015-04-08 10:16:25 +00:00
|
|
|
node = node.parentNode();
|
|
|
|
}
|
|
|
|
this.container.appendChild(fragment, this.container.firstChild);
|
2012-11-30 08:07:59 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find the "youngest" ancestor of a node which is already in the breadcrumbs.
|
2015-04-08 10:16:25 +00:00
|
|
|
* @param {NodeFront} node.
|
|
|
|
* @return {Number} Index of the ancestor in the cache, or -1 if not found.
|
2012-11-30 08:07:59 +00:00
|
|
|
*/
|
2016-05-04 21:48:15 +00:00
|
|
|
getCommonAncestor: function (node) {
|
2012-11-30 08:07:59 +00:00
|
|
|
while (node) {
|
|
|
|
let idx = this.indexOf(node);
|
|
|
|
if (idx > -1) {
|
|
|
|
return idx;
|
|
|
|
}
|
2015-06-02 09:26:24 +00:00
|
|
|
node = node.parentNode();
|
2012-11-30 08:07:59 +00:00
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Ensure the selected node is visible.
|
|
|
|
*/
|
2016-05-04 21:48:15 +00:00
|
|
|
scroll: function () {
|
2012-11-30 08:07:59 +00:00
|
|
|
// FIXME bug 684352: make sure its immediate neighbors are visible too.
|
2016-05-29 21:58:32 +00:00
|
|
|
if (!this.isDestroyed) {
|
|
|
|
let element = this.nodeHierarchy[this.currentIndex].button;
|
2016-06-06 00:25:39 +00:00
|
|
|
this.arrowScrollBox.scrollToElement(element, "end");
|
2016-05-29 21:58:32 +00:00
|
|
|
}
|
2012-11-30 08:07:59 +00:00
|
|
|
},
|
|
|
|
|
2015-04-08 10:16:25 +00:00
|
|
|
/**
|
|
|
|
* Update all button outputs.
|
|
|
|
*/
|
2016-05-04 21:48:15 +00:00
|
|
|
updateSelectors: function () {
|
2014-07-01 04:32:00 +00:00
|
|
|
if (this.isDestroyed) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2012-11-30 08:07:59 +00:00
|
|
|
for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) {
|
2015-04-13 08:22:05 +00:00
|
|
|
let {node, button, currentPrettyPrintText} = this.nodeHierarchy[i];
|
2012-11-30 08:07:59 +00:00
|
|
|
|
2015-04-13 08:22:05 +00:00
|
|
|
// If the output of the node doesn't change, skip the update.
|
|
|
|
let textOutput = this.prettyPrintNodeAsText(node);
|
|
|
|
if (currentPrettyPrintText === textOutput) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, update the whole markup for the button.
|
2015-04-08 10:16:25 +00:00
|
|
|
while (button.hasChildNodes()) {
|
2015-04-13 08:22:05 +00:00
|
|
|
button.firstChild.remove();
|
2012-11-30 08:07:59 +00:00
|
|
|
}
|
2016-05-31 19:29:50 +00:00
|
|
|
button.appendChild(this.prettyPrintNodeAsXHTML(node));
|
|
|
|
button.setAttribute("title", textOutput);
|
2015-04-13 08:22:05 +00:00
|
|
|
|
|
|
|
this.nodeHierarchy[i].currentPrettyPrintText = textOutput;
|
2012-11-30 08:07:59 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2015-04-13 08:22:05 +00:00
|
|
|
/**
|
|
|
|
* Given a list of mutation changes (passed by the markupmutation event),
|
|
|
|
* decide whether or not they are "interesting" to the current state of the
|
|
|
|
* breadcrumbs widget, i.e. at least one of them should cause part of the
|
|
|
|
* widget to be updated.
|
|
|
|
* @param {Array} mutations The mutations array.
|
|
|
|
* @return {Boolean}
|
|
|
|
*/
|
2016-05-04 21:48:15 +00:00
|
|
|
_hasInterestingMutations: function (mutations) {
|
2015-04-13 08:22:05 +00:00
|
|
|
if (!mutations || !mutations.length) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let {type, added, removed, target, attributeName} of mutations) {
|
|
|
|
if (type === "childList") {
|
|
|
|
// Only interested in childList mutations if the added or removed
|
2016-04-15 10:03:33 +00:00
|
|
|
// nodes are currently displayed.
|
2015-04-13 08:22:05 +00:00
|
|
|
return added.some(node => this.indexOf(node) > -1) ||
|
2016-04-15 10:03:33 +00:00
|
|
|
removed.some(node => this.indexOf(node) > -1);
|
2015-04-13 08:22:05 +00:00
|
|
|
} else if (type === "attributes" && this.indexOf(target) > -1) {
|
|
|
|
// Only interested in attributes mutations if the target is
|
|
|
|
// currently displayed, and the attribute is either id or class.
|
|
|
|
return attributeName === "class" || attributeName === "id";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Catch all return in case the mutations array was empty, or in case none
|
|
|
|
// of the changes iterated above were interesting.
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
2012-11-30 08:07:59 +00:00
|
|
|
/**
|
|
|
|
* Update the breadcrumbs display when a new node is selected.
|
2015-04-08 10:16:25 +00:00
|
|
|
* @param {String} reason The reason for the update, if any.
|
2015-04-13 08:22:05 +00:00
|
|
|
* @param {Array} mutations An array of mutations in case this was called as
|
|
|
|
* the "markupmutation" event listener.
|
2012-11-30 08:07:59 +00:00
|
|
|
*/
|
2016-05-04 21:48:15 +00:00
|
|
|
update: function (reason, mutations) {
|
2014-07-01 04:32:00 +00:00
|
|
|
if (this.isDestroyed) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-04-13 08:22:05 +00:00
|
|
|
let hasInterestingMutations = this._hasInterestingMutations(mutations);
|
|
|
|
if (reason === "markupmutation" && !hasInterestingMutations) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2012-11-30 08:07:59 +00:00
|
|
|
let cmdDispatcher = this.chromeDoc.commandDispatcher;
|
|
|
|
this.hadFocus = (cmdDispatcher.focusedElement &&
|
|
|
|
cmdDispatcher.focusedElement.parentNode == this.container);
|
|
|
|
|
|
|
|
if (!this.selection.isConnected()) {
|
2015-06-02 09:26:24 +00:00
|
|
|
// remove all the crumbs
|
|
|
|
this.cutAfter(-1);
|
2012-11-30 08:07:59 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-05-24 23:06:28 +00:00
|
|
|
// If this was an interesting deletion; then trim the breadcrumb trail
|
2016-08-13 07:14:01 +00:00
|
|
|
let trimmed = false;
|
2016-05-24 23:06:28 +00:00
|
|
|
if (reason === "markupmutation") {
|
|
|
|
for (let {type, removed} of mutations) {
|
|
|
|
if (type !== "childList") {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let node of removed) {
|
|
|
|
let removedIndex = this.indexOf(node);
|
|
|
|
if (removedIndex > -1) {
|
|
|
|
this.cutAfter(removedIndex - 1);
|
2016-08-13 07:14:01 +00:00
|
|
|
trimmed = true;
|
2016-05-24 23:06:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-11-30 08:07:59 +00:00
|
|
|
if (!this.selection.isElementNode()) {
|
2015-06-02 09:26:24 +00:00
|
|
|
// no selection
|
|
|
|
this.setCursor(-1);
|
2016-08-13 07:14:01 +00:00
|
|
|
if (trimmed) {
|
|
|
|
// Since something changed, notify the interested parties.
|
|
|
|
this.inspector.emit("breadcrumbs-updated", this.selection.nodeFront);
|
|
|
|
}
|
2012-11-30 08:07:59 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2013-06-07 21:33:39 +00:00
|
|
|
let idx = this.indexOf(this.selection.nodeFront);
|
2012-11-30 08:07:59 +00:00
|
|
|
|
|
|
|
// Is the node already displayed in the breadcrumbs?
|
2013-10-02 00:14:00 +00:00
|
|
|
// (and there are no mutations that need re-display of the crumbs)
|
2015-04-13 08:22:05 +00:00
|
|
|
if (idx > -1 && !hasInterestingMutations) {
|
2012-11-30 08:07:59 +00:00
|
|
|
// Yes. We select it.
|
|
|
|
this.setCursor(idx);
|
|
|
|
} else {
|
|
|
|
// No. Is the breadcrumbs display empty?
|
|
|
|
if (this.nodeHierarchy.length > 0) {
|
|
|
|
// No. We drop all the element that are not direct ancestors
|
|
|
|
// of the selection
|
2013-06-07 21:33:39 +00:00
|
|
|
let parent = this.selection.nodeFront.parentNode();
|
2016-05-04 21:48:15 +00:00
|
|
|
let ancestorIdx = this.getCommonAncestor(parent);
|
|
|
|
this.cutAfter(ancestorIdx);
|
2012-11-30 08:07:59 +00:00
|
|
|
}
|
|
|
|
// we append the missing button between the end of the breadcrumbs display
|
|
|
|
// and the current node.
|
2013-06-07 21:33:39 +00:00
|
|
|
this.expand(this.selection.nodeFront);
|
2012-11-30 08:07:59 +00:00
|
|
|
|
|
|
|
// we select the current node button
|
2013-06-07 21:33:39 +00:00
|
|
|
idx = this.indexOf(this.selection.nodeFront);
|
2012-11-30 08:07:59 +00:00
|
|
|
this.setCursor(idx);
|
|
|
|
}
|
|
|
|
|
2013-06-07 21:33:39 +00:00
|
|
|
let doneUpdating = this.inspector.updating("breadcrumbs");
|
2014-07-01 04:32:00 +00:00
|
|
|
|
2016-04-15 10:03:33 +00:00
|
|
|
this.updateSelectors();
|
|
|
|
|
|
|
|
// Make sure the selected node and its neighbours are visible.
|
2016-08-05 21:41:01 +00:00
|
|
|
setTimeout(() => {
|
|
|
|
try {
|
|
|
|
this.scroll();
|
|
|
|
this.inspector.emit("breadcrumbs-updated", this.selection.nodeFront);
|
|
|
|
doneUpdating();
|
|
|
|
} catch (e) {
|
|
|
|
// Only log this as an error if we haven't been destroyed in the meantime.
|
|
|
|
if (!this.isDestroyed) {
|
|
|
|
console.error(e);
|
|
|
|
}
|
2016-07-20 08:30:43 +00:00
|
|
|
}
|
2016-08-05 21:41:01 +00:00
|
|
|
}, 0);
|
2013-10-02 00:14:00 +00:00
|
|
|
}
|
2014-03-09 17:03:00 +00:00
|
|
|
};
|