author | Steve Melia <steve.j.melia@gmail.com> |
Tue, 31 May 2016 20:29:50 +0100 | |
changeset 338889 | 7cd31f8bca620bd6942349de365937a8115ae46b |
parent 338888 | faa1661edc002da6a574b08a4b24c9a55f29e6d3 |
child 338890 | 5d2cb7fff024a1d3ed59ad2432882d158ce5ee91 |
push id | 6249 |
push user | jlund@mozilla.com |
push date | Mon, 01 Aug 2016 13:59:36 +0000 |
treeherder | mozilla-beta@bad9d4f5bf7e [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | pbro |
bugs | 1259812 |
milestone | 49.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/devtools/client/inspector/breadcrumbs.js +++ b/devtools/client/inspector/breadcrumbs.js @@ -7,28 +7,306 @@ "use strict"; const {Ci} = require("chrome"); const Services = require("Services"); const promise = require("promise"); const FocusManager = Services.focus; const {waitForTick} = require("devtools/shared/DevToolsUtils"); -const ENSURE_SELECTION_VISIBLE_DELAY_MS = 50; const ELLIPSIS = Services.prefs.getComplexValue( "intl.ellipsis", Ci.nsIPrefLocalizedString).data; const MAX_LABEL_LENGTH = 40; +const NS_XHTML = "http://www.w3.org/1999/xhtml"; +const SCROLL_REPEAT_MS = 100; + +loader.lazyRequireGetter(this, "EventEmitter", + "devtools/shared/event-emitter"); + +/** + * 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 = { + + /** + * 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); + }, + + /** + * Call the given function once; then continuously + * while the mouse button is held + * @param {repeatFn} the function to repeat while the button is held + */ + 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]; + element.scrollIntoView({ block: "start", behavior: "smooth" }); + }, + + /** + * 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]; + element.scrollIntoView({ block: "start", behavior: "smooth" }); + }, + + /** + * When start arrow button is clicked scroll towards first element + */ + onStartBtnClick: function () { + let scrollToStart = () => { + let element = this.getFirstInvisibleElement(); + if (!element) { + return; + } + + element.scrollIntoView({ block: "start", behavior: "smooth" }); + }; + + this.clickOrHold(scrollToStart); + }, + + /** + * When end arrow button is clicked scroll towards last element + */ + onEndBtnClick: function () { + let scrollToEnd = () => { + let element = this.getLastInvisibleElement(); + if (!element) { + return; + } + + element.scrollIntoView({ block: "end", behavior: "smooth" }); + }; + + 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"); + }, + + /** + * Get the first (i.e. furthest left for LTR) + * non visible element in the scroll box + */ + getFirstInvisibleElement: function () { + let start = this.inner.scrollLeft; + let end = this.inner.scrollLeft + this.inner.clientWidth; + let crumbs = this.inner.childNodes; + for (let i = crumbs.length - 1; i > -1; i--) { + let element = crumbs[i]; + let elementRight = element.offsetLeft + element.offsetWidth; + if (element.offsetLeft < start) { + // edge case, check the element isn't already visible + if (elementRight >= end) { + continue; + } + return element; + } + } + + return null; + }, + + /** + * Get the last (i.e. furthest right for LTR) + * non-visible element in the scroll box + */ + getLastInvisibleElement: function () { + let end = this.inner.scrollLeft + this.inner.clientWidth; + let elementStart = 0; + for (let element of this.inner.childNodes) { + let elementEnd = elementStart + element.offsetWidth; + if (elementEnd > end) { + // Edge case: check the element isn't bigger than the + // container and thus already in view + if (elementStart > this.inner.scrollLeft) { + return element; + } + } + + elementStart = elementEnd; + } + + 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); + }, +}; + /** * 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). * * @param {InspectorPanel} inspector The inspector hosting this widget. */ @@ -43,57 +321,46 @@ function HTMLBreadcrumbs(inspector) { exports.HTMLBreadcrumbs = HTMLBreadcrumbs; HTMLBreadcrumbs.prototype = { get walker() { return this.inspector.walker; }, _init: function () { - this.container = this.chromeDoc.getElementById("inspector-breadcrumbs"); + this.outer = this.chromeDoc.getElementById("inspector-breadcrumbs"); + this.arrowScrollBox = new ArrowScrollBox( + this.chromeWin, + this.outer); + + this.container = this.arrowScrollBox.inner; + this.arrowScrollBox.on("overflow", this.scroll); // 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 = this.chromeDoc.createElementNS(NS_XHTML, "div"); this.separators.className = "breadcrumb-separator-container"; this.separators.innerHTML = - "<box id='breadcrumb-separator-before'></box>" + - "<box id='breadcrumb-separator-after'></box>" + - "<box id='breadcrumb-separator-normal'></box>"; + "<div id='breadcrumb-separator-before'></div>" + + "<div id='breadcrumb-separator-after'></div>" + + "<div id='breadcrumb-separator-normal'></div>"; this.container.parentNode.appendChild(this.separators); - this.container.addEventListener("click", this, true); - this.container.addEventListener("keypress", this, true); - this.container.addEventListener("mouseover", this, true); - this.container.addEventListener("mouseleave", this, true); - this.container.addEventListener("focus", this, true); + this.outer.addEventListener("click", this, true); + this.outer.addEventListener("keypress", this, true); + this.outer.addEventListener("mouseover", this, true); + this.outer.addEventListener("mouseleave", this, true); + this.outer.addEventListener("focus", 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 <scrollbox> 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(); }, @@ -124,36 +391,34 @@ HTMLBreadcrumbs.prototype = { for (let pseudo of node.pseudoClassLocks) { text += pseudo; } return text; }, /** - * Build <label>s that represent the node: - * <label class="breadcrumbs-widget-item-tag">tagName</label> - * <label class="breadcrumbs-widget-item-id">#id</label> - * <label class="breadcrumbs-widget-item-classes">.class1.class2</label> + * 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> * @param {NodeFront} node The node to pretty-print * @returns {DocumentFragment} */ - prettyPrintNodeAsXUL: function (node) { - let fragment = this.chromeDoc.createDocumentFragment(); - - let tagLabel = this.chromeDoc.createElement("label"); + prettyPrintNodeAsXHTML: function (node) { + let tagLabel = this.chromeDoc.createElementNS(NS_XHTML, "span"); tagLabel.className = "breadcrumbs-widget-item-tag plain"; - let idLabel = this.chromeDoc.createElement("label"); + let idLabel = this.chromeDoc.createElementNS(NS_XHTML, "span"); idLabel.className = "breadcrumbs-widget-item-id plain"; - let classesLabel = this.chromeDoc.createElement("label"); + let classesLabel = this.chromeDoc.createElementNS(NS_XHTML, "span"); classesLabel.className = "breadcrumbs-widget-item-classes plain"; - let pseudosLabel = this.chromeDoc.createElement("label"); + let pseudosLabel = this.chromeDoc.createElementNS(NS_XHTML, "span"); pseudosLabel.className = "breadcrumbs-widget-item-pseudo-classes plain"; let tagText = node.displayName; if (node.isPseudoElement) { tagText = node.isBeforePseudoElement ? "::before" : "::after"; } let idText = node.id ? ("#" + node.id) : ""; let classesText = ""; @@ -182,16 +447,17 @@ HTMLBreadcrumbs.prototype = { classesText = classesText.substr(0, maxClassLength) + ELLIPSIS; } tagLabel.textContent = tagText; idLabel.textContent = idText; classesLabel.textContent = classesText; pseudosLabel.textContent = node.pseudoClassLocks.join(""); + let fragment = this.chromeDoc.createDocumentFragment(); fragment.appendChild(tagLabel); fragment.appendChild(idLabel); fragment.appendChild(classesLabel); fragment.appendChild(pseudosLabel); return fragment; }, @@ -216,29 +482,34 @@ HTMLBreadcrumbs.prototype = { /** * Focus event handler. When breadcrumbs container gets focus, if there is an * already selected breadcrumb, move focus to it. * @param {DOMEvent} event. */ handleFocus: function (event) { let control = this.container.querySelector( ".breadcrumbs-widget-item[checked]"); - if (control && control !== event.target) { + if (!this.suspendFocus && control && control !== event.target) { // If we already have a selected breadcrumb and focus target is not it, - // move focus to selected breadcrumb. + // move focus to selected breadcrumb event.preventDefault(); control.focus(); } + this.suspendFocus = false; }, /** * On click navigate to the correct node. * @param {DOMEvent} event. */ handleClick: function (event) { + // When clicking a button temporarily suspend the behaviour that refocuses + // the currently selected button, to prevent flicking back to that button + // See Bug 1272011 + this.suspendFocus = true; let target = event.originalTarget; if (target.tagName == "button") { target.onBreadcrumbsClick(); } }, /** * On mouse over, highlight the corresponding content DOM Node. @@ -320,30 +591,29 @@ HTMLBreadcrumbs.prototype = { * Remove nodes and clean up. */ destroy: function () { this.selection.off("new-node-front", this.update); this.selection.off("pseudoclass", this.updateSelectors); this.selection.off("attribute-changed", this.updateSelectors); this.inspector.off("markupmutation", this.update); - this.container.removeEventListener("underflow", - this.onscrollboxreflow, false); - this.container.removeEventListener("overflow", - this.onscrollboxreflow, false); this.container.removeEventListener("click", this, true); this.container.removeEventListener("keypress", this, true); this.container.removeEventListener("mouseover", this, true); this.container.removeEventListener("mouseleave", this, true); this.container.removeEventListener("focus", this, true); this.empty(); this.separators.remove(); - this.onscrollboxreflow = null; + this.arrowScrollBox.off("overflow", this.scroll); + this.arrowScrollBox.destroy(); + this.arrowScrollBox = null; + this.outer = null; this.container = null; this.separators = null; this.nodeHierarchy = null; this.isDestroyed = true; }, /** @@ -401,21 +671,21 @@ HTMLBreadcrumbs.prototype = { }, /** * Build a button representing the node. * @param {NodeFront} node The node from the page. * @return {DOMNode} The <button> for this node. */ buildButton: function (node) { - let button = this.chromeDoc.createElement("button"); - button.appendChild(this.prettyPrintNodeAsXUL(node)); + let button = this.chromeDoc.createElementNS(NS_XHTML, "button"); + button.appendChild(this.prettyPrintNodeAsXHTML(node)); button.className = "breadcrumbs-widget-item"; - button.setAttribute("tooltiptext", this.prettyPrintNodeAsText(node)); + button.setAttribute("title", this.prettyPrintNodeAsText(node)); button.onkeypress = function onBreadcrumbsKeypress(e) { if (e.charCode == Ci.nsIDOMKeyEvent.DOM_VK_SPACE || e.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN) { button.click(); } }; @@ -478,26 +748,18 @@ HTMLBreadcrumbs.prototype = { return -1; }, /** * Ensure the selected node is visible. */ scroll: function () { // FIXME bug 684352: make sure its immediate neighbors are visible too. - - let scrollbox = this.container; let element = this.nodeHierarchy[this.currentIndex].button; - - // Repeated calls to ensureElementIsVisible would interfere with each other - // and may sometimes result in incorrect scroll positions. - this.chromeWin.clearTimeout(this._ensureVisibleTimeout); - this._ensureVisibleTimeout = this.chromeWin.setTimeout(function () { - scrollbox.ensureElementIsVisible(element); - }, ENSURE_SELECTION_VISIBLE_DELAY_MS); + element.scrollIntoView({ block: "end", behavior: "smooth" }); }, /** * Update all button outputs. */ updateSelectors: function () { if (this.isDestroyed) { return; @@ -511,18 +773,18 @@ HTMLBreadcrumbs.prototype = { if (currentPrettyPrintText === textOutput) { continue; } // Otherwise, update the whole markup for the button. while (button.hasChildNodes()) { button.firstChild.remove(); } - button.appendChild(this.prettyPrintNodeAsXUL(node)); - button.setAttribute("tooltiptext", textOutput); + button.appendChild(this.prettyPrintNodeAsXHTML(node)); + button.setAttribute("title", textOutput); this.nodeHierarchy[i].currentPrettyPrintText = textOutput; } }, /** * Given a list of mutation changes (passed by the markupmutation event), * decide whether or not they are "interesting" to the current state of the @@ -615,15 +877,15 @@ HTMLBreadcrumbs.prototype = { this.setCursor(idx); } let doneUpdating = this.inspector.updating("breadcrumbs"); this.updateSelectors(); // Make sure the selected node and its neighbours are visible. - this.scroll(); waitForTick().then(() => { + this.scroll(); this.inspector.emit("breadcrumbs-updated", this.selection.nodeFront); doneUpdating(); }); } };
--- a/devtools/client/inspector/inspector.xul +++ b/devtools/client/inspector/inspector.xul @@ -163,24 +163,19 @@ class="devtools-searchinput" placeholder="&inspectorSearchHTML.label3;"/> <html:button id="inspector-pane-toggle" class="devtools-button" tabindex="0" /> </html:div> <vbox flex="1" id="markup-box"> </vbox> - <toolbar id="inspector-breadcrumbs-toolbar" - class="devtools-toolbar" - nowindowdrag="true"> - <arrowscrollbox id="inspector-breadcrumbs" - class="breadcrumbs-widget-container" - flex="1" orient="horizontal" - clicktoscroll="true"/> - </toolbar> + <html:div id="inspector-breadcrumbs-toolbar" class="devtools-toolbar"> + <html:div id="inspector-breadcrumbs" class="breadcrumbs-widget-container"/> + </html:div> </vbox> <splitter class="devtools-side-splitter"/> <tabbox id="inspector-sidebar" handleCtrlTab="false" class="devtools-sidebar-tabs" hidden="true"> <tabs> <tab id="sidebar-tab-ruleview" label="&ruleViewTitle;" crop="end"/> <tab id="sidebar-tab-computedview"
--- a/devtools/client/inspector/test/browser_inspector_breadcrumbs.js +++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs.js @@ -22,17 +22,18 @@ const NODES = [ {selector: "#i3", ids: "i3", nodeName: "article", title: "article#i3"}, {selector: "clipPath", ids: "vector clip", nodeName: "clipPath", title: "clipPath#clip"}, ]; add_task(function* () { let { inspector } = yield openInspectorForURL(TEST_URI); - let container = inspector.panelDoc.getElementById("inspector-breadcrumbs"); + let breadcrumbs = inspector.panelDoc.getElementById("inspector-breadcrumbs"); + let container = breadcrumbs.querySelector(".html-arrowscrollbox-inner"); for (let node of NODES) { info("Testing node " + node.selector); info("Selecting node and waiting for breadcrumbs to update"); let breadcrumbsUpdated = inspector.once("breadcrumbs-updated"); yield selectNode(node.selector, inspector); yield breadcrumbsUpdated; @@ -57,17 +58,17 @@ add_task(function* () { let id = inspector.selection.nodeFront.id; is(labelId.textContent, "#" + id, "Node " + node.selector + ": selection matches"); let labelTag = checkedButton.querySelector(".breadcrumbs-widget-item-tag"); is(labelTag.textContent, node.nodeName, "Node " + node.selector + " has the expected tag name"); - is(checkedButton.getAttribute("tooltiptext"), node.title, + is(checkedButton.getAttribute("title"), node.title, "Node " + node.selector + " has the expected tooltip"); } yield testPseudoElements(inspector, container); }); function* testPseudoElements(inspector, container) { info("Checking for pseudo elements");
--- a/devtools/client/inspector/test/browser_inspector_breadcrumbs_mutations.js +++ b/devtools/client/inspector/test/browser_inspector_breadcrumbs_mutations.js @@ -144,17 +144,18 @@ const TEST_DATA = [{ }]); }, shouldRefresh: true, output: ["html", "body#new-id.test-class-changed"] }]; add_task(function* () { let {inspector} = yield openInspectorForURL(TEST_URI); - let container = inspector.panelDoc.getElementById("inspector-breadcrumbs"); + let breadcrumbs = inspector.panelDoc.getElementById("inspector-breadcrumbs"); + let container = breadcrumbs.querySelector(".html-arrowscrollbox-inner"); let win = container.ownerDocument.defaultView; for (let {desc, setup, run, shouldRefresh, output} of TEST_DATA) { info("Running test case: " + desc); info("Listen to markupmutation events from the inspector to know when a " + "test case has completed"); let onContentMutation = inspector.once("markupmutation");
--- a/devtools/client/themes/inspector.css +++ b/devtools/client/themes/inspector.css @@ -32,16 +32,55 @@ .theme-firebug #inspector-searchbox { line-height: 17px; } #inspector-breadcrumbs-toolbar { padding: 0px; border-bottom-width: 0px; border-top-width: 1px; + display: block; + position: relative; +} + +#inspector-breadcrumbs-toolbar, +#inspector-breadcrumbs-toolbar * { + box-sizing: border-box; +} + +#inspector-breadcrumbs { + display: flex; + + /* Break out of the XUL flexbox, so the splitter can still shrink the + markup view even if the contents of the breadcrumbs are wider than + the new width. */ + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; +} + +#inspector-breadcrumbs .scrollbutton-up, +#inspector-breadcrumbs .scrollbutton-down { + flex: 0; + display: flex; + align-items: center; +} + +#inspector-breadcrumbs .html-arrowscrollbox-inner { + flex: 1; + display: flex; + overflow: hidden; +} + +#inspector-breadcrumbs .breadcrumbs-widget-item { + white-space: nowrap; + flex-shrink: 0; + font: message-box; } /* Expand/collapse panel toolbar button */ #inspector-pane-toggle::before { background-image: var(--theme-pane-collapse-image); }