mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-04 23:08:39 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
93
devtools/client/shared/widgets/tooltip/CssDocsTooltip.js
Normal file
93
devtools/client/shared/widgets/tooltip/CssDocsTooltip.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
|
||||
const {MdnDocsWidget} = require("devtools/client/shared/widgets/MdnDocsWidget");
|
||||
const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
|
||||
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
const TOOLTIP_WIDTH = 418;
|
||||
const TOOLTIP_HEIGHT = 308;
|
||||
|
||||
/**
|
||||
* Tooltip for displaying docs for CSS properties from MDN.
|
||||
*
|
||||
* @param {Document} toolboxDoc
|
||||
* The toolbox document to attach the CSS docs tooltip.
|
||||
*/
|
||||
function CssDocsTooltip(toolboxDoc) {
|
||||
this.tooltip = new HTMLTooltip(toolboxDoc, {
|
||||
type: "arrow",
|
||||
consumeOutsideClicks: true,
|
||||
autofocus: true,
|
||||
useXulWrapper: true,
|
||||
stylesheet: "chrome://devtools/content/shared/widgets/mdn-docs.css",
|
||||
});
|
||||
this.widget = this.setMdnDocsContent();
|
||||
this._onVisitLink = this._onVisitLink.bind(this);
|
||||
this.widget.on("visitlink", this._onVisitLink);
|
||||
|
||||
// Initialize keyboard shortcuts
|
||||
this.shortcuts = new KeyShortcuts({ window: this.tooltip.topWindow });
|
||||
this._onShortcut = this._onShortcut.bind(this);
|
||||
|
||||
this.shortcuts.on("Escape", this._onShortcut);
|
||||
}
|
||||
|
||||
CssDocsTooltip.prototype = {
|
||||
/**
|
||||
* Load CSS docs for the given property,
|
||||
* then display the tooltip.
|
||||
*/
|
||||
show: function (anchor, propertyName) {
|
||||
this.tooltip.once("shown", () => {
|
||||
this.widget.loadCssDocs(propertyName);
|
||||
});
|
||||
this.tooltip.show(anchor);
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
this.tooltip.hide();
|
||||
},
|
||||
|
||||
_onShortcut: function (shortcut, event) {
|
||||
if (!this.tooltip.isVisible()) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.hide();
|
||||
},
|
||||
|
||||
_onVisitLink: function () {
|
||||
this.hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the content of this tooltip to the MDN docs widget. This is called when the
|
||||
* tooltip is first constructed.
|
||||
* The caller can use the MdnDocsWidget to update the tooltip's UI with new content
|
||||
* each time the tooltip is shown.
|
||||
*
|
||||
* @return {MdnDocsWidget} the created MdnDocsWidget instance.
|
||||
*/
|
||||
setMdnDocsContent: function () {
|
||||
let container = this.tooltip.doc.createElementNS(XHTML_NS, "div");
|
||||
container.setAttribute("class", "mdn-container theme-body");
|
||||
this.tooltip.setContent(container, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT});
|
||||
return new MdnDocsWidget(container);
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
this.widget.off("visitlink", this._onVisitLink);
|
||||
this.widget.destroy();
|
||||
|
||||
this.shortcuts.destroy();
|
||||
this.tooltip.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = CssDocsTooltip;
|
||||
313
devtools/client/shared/widgets/tooltip/EventTooltipHelper.js
Normal file
313
devtools/client/shared/widgets/tooltip/EventTooltipHelper.js
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
/* -*- 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {LocalizationHelper} = require("devtools/shared/l10n");
|
||||
const L10N = new LocalizationHelper("devtools/client/locales/inspector.properties");
|
||||
|
||||
const Editor = require("devtools/client/sourceeditor/editor");
|
||||
const beautify = require("devtools/shared/jsbeautify/beautify");
|
||||
|
||||
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
const CONTAINER_WIDTH = 500;
|
||||
|
||||
/**
|
||||
* Set the content of a provided HTMLTooltip instance to display a list of event
|
||||
* listeners, with their event type, capturing argument and a link to the code
|
||||
* of the event handler.
|
||||
*
|
||||
* @param {HTMLTooltip} tooltip
|
||||
* The tooltip instance on which the event details content should be set
|
||||
* @param {Array} eventListenerInfos
|
||||
* A list of event listeners
|
||||
* @param {Toolbox} toolbox
|
||||
* Toolbox used to select debugger panel
|
||||
*/
|
||||
function setEventTooltip(tooltip, eventListenerInfos, toolbox) {
|
||||
let eventTooltip = new EventTooltip(tooltip, eventListenerInfos, toolbox);
|
||||
eventTooltip.init();
|
||||
}
|
||||
|
||||
function EventTooltip(tooltip, eventListenerInfos, toolbox) {
|
||||
this._tooltip = tooltip;
|
||||
this._eventListenerInfos = eventListenerInfos;
|
||||
this._toolbox = toolbox;
|
||||
this._eventEditors = new WeakMap();
|
||||
|
||||
// Used in tests: add a reference to the EventTooltip instance on the HTMLTooltip.
|
||||
this._tooltip.eventTooltip = this;
|
||||
|
||||
this._headerClicked = this._headerClicked.bind(this);
|
||||
this._debugClicked = this._debugClicked.bind(this);
|
||||
this.destroy = this.destroy.bind(this);
|
||||
}
|
||||
|
||||
EventTooltip.prototype = {
|
||||
init: function () {
|
||||
let config = {
|
||||
mode: Editor.modes.js,
|
||||
lineNumbers: false,
|
||||
lineWrapping: true,
|
||||
readOnly: true,
|
||||
styleActiveLine: true,
|
||||
extraKeys: {},
|
||||
theme: "mozilla markup-view"
|
||||
};
|
||||
|
||||
let doc = this._tooltip.doc;
|
||||
this.container = doc.createElementNS(XHTML_NS, "div");
|
||||
this.container.className = "devtools-tooltip-events-container";
|
||||
|
||||
for (let listener of this._eventListenerInfos) {
|
||||
let phase = listener.capturing ? "Capturing" : "Bubbling";
|
||||
let level = listener.DOM0 ? "DOM0" : "DOM2";
|
||||
|
||||
// Header
|
||||
let header = doc.createElementNS(XHTML_NS, "div");
|
||||
header.className = "event-header devtools-toolbar";
|
||||
this.container.appendChild(header);
|
||||
|
||||
if (!listener.hide.debugger) {
|
||||
let debuggerIcon = doc.createElementNS(XHTML_NS, "img");
|
||||
debuggerIcon.className = "event-tooltip-debugger-icon";
|
||||
debuggerIcon.setAttribute("src",
|
||||
"chrome://devtools/skin/images/tool-debugger.svg");
|
||||
let openInDebugger = L10N.getStr("eventsTooltip.openInDebugger");
|
||||
debuggerIcon.setAttribute("title", openInDebugger);
|
||||
header.appendChild(debuggerIcon);
|
||||
}
|
||||
|
||||
if (!listener.hide.type) {
|
||||
let eventTypeLabel = doc.createElementNS(XHTML_NS, "span");
|
||||
eventTypeLabel.className = "event-tooltip-event-type";
|
||||
eventTypeLabel.textContent = listener.type;
|
||||
eventTypeLabel.setAttribute("title", listener.type);
|
||||
header.appendChild(eventTypeLabel);
|
||||
}
|
||||
|
||||
if (!listener.hide.filename) {
|
||||
let filename = doc.createElementNS(XHTML_NS, "span");
|
||||
filename.className = "event-tooltip-filename devtools-monospace";
|
||||
filename.textContent = listener.origin;
|
||||
filename.setAttribute("title", listener.origin);
|
||||
header.appendChild(filename);
|
||||
}
|
||||
|
||||
let attributesContainer = doc.createElementNS(XHTML_NS, "div");
|
||||
attributesContainer.className = "event-tooltip-attributes-container";
|
||||
header.appendChild(attributesContainer);
|
||||
|
||||
if (!listener.hide.capturing) {
|
||||
let attributesBox = doc.createElementNS(XHTML_NS, "div");
|
||||
attributesBox.className = "event-tooltip-attributes-box";
|
||||
attributesContainer.appendChild(attributesBox);
|
||||
|
||||
let capturing = doc.createElementNS(XHTML_NS, "span");
|
||||
capturing.className = "event-tooltip-attributes";
|
||||
capturing.textContent = phase;
|
||||
capturing.setAttribute("title", phase);
|
||||
attributesBox.appendChild(capturing);
|
||||
}
|
||||
|
||||
if (listener.tags) {
|
||||
for (let tag of listener.tags.split(",")) {
|
||||
let attributesBox = doc.createElementNS(XHTML_NS, "div");
|
||||
attributesBox.className = "event-tooltip-attributes-box";
|
||||
attributesContainer.appendChild(attributesBox);
|
||||
|
||||
let tagBox = doc.createElementNS(XHTML_NS, "span");
|
||||
tagBox.className = "event-tooltip-attributes";
|
||||
tagBox.textContent = tag;
|
||||
tagBox.setAttribute("title", tag);
|
||||
attributesBox.appendChild(tagBox);
|
||||
}
|
||||
}
|
||||
|
||||
if (!listener.hide.dom0) {
|
||||
let attributesBox = doc.createElementNS(XHTML_NS, "div");
|
||||
attributesBox.className = "event-tooltip-attributes-box";
|
||||
attributesContainer.appendChild(attributesBox);
|
||||
|
||||
let dom0 = doc.createElementNS(XHTML_NS, "span");
|
||||
dom0.className = "event-tooltip-attributes";
|
||||
dom0.textContent = level;
|
||||
dom0.setAttribute("title", level);
|
||||
attributesBox.appendChild(dom0);
|
||||
}
|
||||
|
||||
// Content
|
||||
let content = doc.createElementNS(XHTML_NS, "div");
|
||||
let editor = new Editor(config);
|
||||
this._eventEditors.set(content, {
|
||||
editor: editor,
|
||||
handler: listener.handler,
|
||||
searchString: listener.searchString,
|
||||
uri: listener.origin,
|
||||
dom0: listener.DOM0,
|
||||
appended: false
|
||||
});
|
||||
|
||||
content.className = "event-tooltip-content-box";
|
||||
this.container.appendChild(content);
|
||||
|
||||
this._addContentListeners(header);
|
||||
}
|
||||
|
||||
this._tooltip.setContent(this.container, {width: CONTAINER_WIDTH});
|
||||
this._tooltip.on("hidden", this.destroy);
|
||||
},
|
||||
|
||||
_addContentListeners: function (header) {
|
||||
header.addEventListener("click", this._headerClicked);
|
||||
},
|
||||
|
||||
_headerClicked: function (event) {
|
||||
if (event.target.classList.contains("event-tooltip-debugger-icon")) {
|
||||
this._debugClicked(event);
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
let doc = this._tooltip.doc;
|
||||
let header = event.currentTarget;
|
||||
let content = header.nextElementSibling;
|
||||
|
||||
if (content.hasAttribute("open")) {
|
||||
content.removeAttribute("open");
|
||||
} else {
|
||||
let contentNodes = doc.querySelectorAll(".event-tooltip-content-box");
|
||||
|
||||
for (let node of contentNodes) {
|
||||
if (node !== content) {
|
||||
node.removeAttribute("open");
|
||||
}
|
||||
}
|
||||
|
||||
content.setAttribute("open", "");
|
||||
|
||||
let eventEditor = this._eventEditors.get(content);
|
||||
|
||||
if (eventEditor.appended) {
|
||||
return;
|
||||
}
|
||||
|
||||
let {editor, handler} = eventEditor;
|
||||
|
||||
let iframe = doc.createElementNS(XHTML_NS, "iframe");
|
||||
iframe.setAttribute("style", "width: 100%; height: 100%; border-style: none;");
|
||||
|
||||
editor.appendTo(content, iframe).then(() => {
|
||||
let tidied = beautify.js(handler, { "indent_size": 2 });
|
||||
editor.setText(tidied);
|
||||
|
||||
eventEditor.appended = true;
|
||||
|
||||
let container = header.parentElement.getBoundingClientRect();
|
||||
if (header.getBoundingClientRect().top < container.top) {
|
||||
header.scrollIntoView(true);
|
||||
} else if (content.getBoundingClientRect().bottom > container.bottom) {
|
||||
content.scrollIntoView(false);
|
||||
}
|
||||
|
||||
this._tooltip.emit("event-tooltip-ready");
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_debugClicked: function (event) {
|
||||
let header = event.currentTarget;
|
||||
let content = header.nextElementSibling;
|
||||
|
||||
let {uri, searchString, dom0} = this._eventEditors.get(content);
|
||||
|
||||
if (uri && uri !== "?") {
|
||||
// Save a copy of toolbox as it will be set to null when we hide the tooltip.
|
||||
let toolbox = this._toolbox;
|
||||
|
||||
this._tooltip.hide();
|
||||
|
||||
uri = uri.replace(/"/g, "");
|
||||
|
||||
let showSource = ({ DebuggerView }) => {
|
||||
let matches = uri.match(/(.*):(\d+$)/);
|
||||
let line = 1;
|
||||
|
||||
if (matches) {
|
||||
uri = matches[1];
|
||||
line = matches[2];
|
||||
}
|
||||
|
||||
let item = DebuggerView.Sources.getItemForAttachment(a => a.source.url === uri);
|
||||
if (item) {
|
||||
let actor = item.attachment.source.actor;
|
||||
DebuggerView.setEditorLocation(
|
||||
actor, line, {noDebug: true}
|
||||
).then(() => {
|
||||
if (dom0) {
|
||||
let text = DebuggerView.editor.getText();
|
||||
let index = text.indexOf(searchString);
|
||||
let lastIndex = text.lastIndexOf(searchString);
|
||||
|
||||
// To avoid confusion we only search for DOM0 event handlers when
|
||||
// there is only one possible match in the file.
|
||||
if (index !== -1 && index === lastIndex) {
|
||||
text = text.substr(0, index);
|
||||
let newlineMatches = text.match(/\n/g);
|
||||
|
||||
if (newlineMatches) {
|
||||
DebuggerView.editor.setCursor({
|
||||
line: newlineMatches.length
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
|
||||
toolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => {
|
||||
if (debuggerAlreadyOpen) {
|
||||
showSource(dbg);
|
||||
} else {
|
||||
dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg));
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
if (this._tooltip) {
|
||||
this._tooltip.off("hidden", this.destroy);
|
||||
|
||||
let boxes = this.container.querySelectorAll(".event-tooltip-content-box");
|
||||
|
||||
for (let box of boxes) {
|
||||
let {editor} = this._eventEditors.get(box);
|
||||
editor.destroy();
|
||||
}
|
||||
|
||||
this._eventEditors = null;
|
||||
this._tooltip.eventTooltip = null;
|
||||
}
|
||||
|
||||
let headerNodes = this.container.querySelectorAll(".event-header");
|
||||
|
||||
for (let node of headerNodes) {
|
||||
node.removeEventListener("click", this._headerClicked);
|
||||
}
|
||||
|
||||
let sourceNodes = this.container.querySelectorAll(".event-tooltip-debugger-icon");
|
||||
for (let node of sourceNodes) {
|
||||
node.removeEventListener("click", this._debugClicked);
|
||||
}
|
||||
|
||||
this._eventListenerInfos = this._toolbox = this._tooltip = null;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.setEventTooltip = setEventTooltip;
|
||||
638
devtools/client/shared/widgets/tooltip/HTMLTooltip.js
Normal file
638
devtools/client/shared/widgets/tooltip/HTMLTooltip.js
Normal file
|
|
@ -0,0 +1,638 @@
|
|||
/* -*- 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const {TooltipToggle} = require("devtools/client/shared/widgets/tooltip/TooltipToggle");
|
||||
const {listenOnce} = require("devtools/shared/async-utils");
|
||||
const {Task} = require("devtools/shared/task");
|
||||
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
const POSITION = {
|
||||
TOP: "top",
|
||||
BOTTOM: "bottom",
|
||||
};
|
||||
|
||||
module.exports.POSITION = POSITION;
|
||||
|
||||
const TYPE = {
|
||||
NORMAL: "normal",
|
||||
ARROW: "arrow",
|
||||
};
|
||||
|
||||
module.exports.TYPE = TYPE;
|
||||
|
||||
const ARROW_WIDTH = 32;
|
||||
|
||||
// Default offset between the tooltip's left edge and the tooltip arrow.
|
||||
const ARROW_OFFSET = 20;
|
||||
|
||||
const EXTRA_HEIGHT = {
|
||||
"normal": 0,
|
||||
// The arrow is 16px tall, but merges on 3px with the panel border
|
||||
"arrow": 13,
|
||||
};
|
||||
|
||||
const EXTRA_BORDER = {
|
||||
"normal": 0,
|
||||
"arrow": 3,
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate the vertical position & offsets to use for the tooltip. Will attempt to
|
||||
* respect the provided height and position preferences, unless the available height
|
||||
* prevents this.
|
||||
*
|
||||
* @param {DOMRect} anchorRect
|
||||
* Bounding rectangle for the anchor, relative to the tooltip document.
|
||||
* @param {DOMRect} viewportRect
|
||||
* Bounding rectangle for the viewport. top/left can be different from 0 if some
|
||||
* space should not be used by tooltips (for instance OS toolbars, taskbars etc.).
|
||||
* @param {Number} height
|
||||
* Preferred height for the tooltip.
|
||||
* @param {String} pos
|
||||
* Preferred position for the tooltip. Possible values: "top" or "bottom".
|
||||
* @return {Object}
|
||||
* - {Number} top: the top offset for the tooltip.
|
||||
* - {Number} height: the height to use for the tooltip container.
|
||||
* - {String} computedPosition: Can differ from the preferred position depending
|
||||
* on the available height). "top" or "bottom"
|
||||
*/
|
||||
const calculateVerticalPosition =
|
||||
function (anchorRect, viewportRect, height, pos, offset) {
|
||||
let {TOP, BOTTOM} = POSITION;
|
||||
|
||||
let {top: anchorTop, height: anchorHeight} = anchorRect;
|
||||
|
||||
// Translate to the available viewport space before calculating dimensions and position.
|
||||
anchorTop -= viewportRect.top;
|
||||
|
||||
// Calculate available space for the tooltip.
|
||||
let availableTop = anchorTop;
|
||||
let availableBottom = viewportRect.height - (anchorTop + anchorHeight);
|
||||
|
||||
// Find POSITION
|
||||
let keepPosition = false;
|
||||
if (pos === TOP) {
|
||||
keepPosition = availableTop >= height + offset;
|
||||
} else if (pos === BOTTOM) {
|
||||
keepPosition = availableBottom >= height + offset;
|
||||
}
|
||||
if (!keepPosition) {
|
||||
pos = availableTop > availableBottom ? TOP : BOTTOM;
|
||||
}
|
||||
|
||||
// Calculate HEIGHT.
|
||||
let availableHeight = pos === TOP ? availableTop : availableBottom;
|
||||
height = Math.min(height, availableHeight - offset);
|
||||
height = Math.floor(height);
|
||||
|
||||
// Calculate TOP.
|
||||
let top = pos === TOP ? anchorTop - height - offset : anchorTop + anchorHeight + offset;
|
||||
|
||||
// Translate back to absolute coordinates by re-including viewport top margin.
|
||||
top += viewportRect.top;
|
||||
|
||||
return {top, height, computedPosition: pos};
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate the vertical position & offsets to use for the tooltip. Will attempt to
|
||||
* respect the provided height and position preferences, unless the available height
|
||||
* prevents this.
|
||||
*
|
||||
* @param {DOMRect} anchorRect
|
||||
* Bounding rectangle for the anchor, relative to the tooltip document.
|
||||
* @param {DOMRect} viewportRect
|
||||
* Bounding rectangle for the viewport. top/left can be different from 0 if some
|
||||
* space should not be used by tooltips (for instance OS toolbars, taskbars etc.).
|
||||
* @param {Number} width
|
||||
* Preferred width for the tooltip.
|
||||
* @param {String} type
|
||||
* The tooltip type (e.g. "arrow").
|
||||
* @param {Number} offset
|
||||
* Horizontal offset in pixels.
|
||||
* @param {Boolean} isRtl
|
||||
* If the anchor is in RTL, the tooltip should be aligned to the right.
|
||||
* @return {Object}
|
||||
* - {Number} left: the left offset for the tooltip.
|
||||
* - {Number} width: the width to use for the tooltip container.
|
||||
* - {Number} arrowLeft: the left offset to use for the arrow element.
|
||||
*/
|
||||
const calculateHorizontalPosition =
|
||||
function (anchorRect, viewportRect, width, type, offset, isRtl) {
|
||||
let anchorWidth = anchorRect.width;
|
||||
let anchorStart = isRtl ? anchorRect.right : anchorRect.left;
|
||||
|
||||
// Translate to the available viewport space before calculating dimensions and position.
|
||||
anchorStart -= viewportRect.left;
|
||||
|
||||
// Calculate WIDTH.
|
||||
width = Math.min(width, viewportRect.width);
|
||||
|
||||
// Calculate LEFT.
|
||||
// By default the tooltip is aligned with the anchor left edge. Unless this
|
||||
// makes it overflow the viewport, in which case is shifts to the left.
|
||||
let left = anchorStart + offset - (isRtl ? width : 0);
|
||||
left = Math.min(left, viewportRect.width - width);
|
||||
left = Math.max(0, left);
|
||||
|
||||
// Calculate ARROW LEFT (tooltip's LEFT might be updated)
|
||||
let arrowLeft;
|
||||
// Arrow style tooltips may need to be shifted to the left
|
||||
if (type === TYPE.ARROW) {
|
||||
let arrowCenter = left + ARROW_OFFSET + ARROW_WIDTH / 2;
|
||||
let anchorCenter = anchorStart + anchorWidth / 2;
|
||||
// If the anchor is too narrow, align the arrow and the anchor center.
|
||||
if (arrowCenter > anchorCenter) {
|
||||
left = Math.max(0, left - (arrowCenter - anchorCenter));
|
||||
}
|
||||
// Arrow's left offset relative to the anchor.
|
||||
arrowLeft = Math.min(ARROW_OFFSET, (anchorWidth - ARROW_WIDTH) / 2) | 0;
|
||||
// Translate the coordinate to tooltip container
|
||||
arrowLeft += anchorStart - left;
|
||||
// Make sure the arrow remains in the tooltip container.
|
||||
arrowLeft = Math.min(arrowLeft, width - ARROW_WIDTH);
|
||||
arrowLeft = Math.max(arrowLeft, 0);
|
||||
}
|
||||
|
||||
// Translate back to absolute coordinates by re-including viewport left margin.
|
||||
left += viewportRect.left;
|
||||
|
||||
return {left, width, arrowLeft};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the bounding client rectangle for a given node, relative to a custom
|
||||
* reference element (instead of the default for getBoundingClientRect which
|
||||
* is always the element's ownerDocument).
|
||||
*/
|
||||
const getRelativeRect = function (node, relativeTo) {
|
||||
// Width and Height can be taken from the rect.
|
||||
let {width, height} = node.getBoundingClientRect();
|
||||
|
||||
let quads = node.getBoxQuads({relativeTo});
|
||||
let top = quads[0].bounds.top;
|
||||
let left = quads[0].bounds.left;
|
||||
|
||||
// Compute right and bottom coordinates using the rest of the data.
|
||||
let right = left + width;
|
||||
let bottom = top + height;
|
||||
|
||||
return {top, right, bottom, left, width, height};
|
||||
};
|
||||
|
||||
/**
|
||||
* The HTMLTooltip can display HTML content in a tooltip popup.
|
||||
*
|
||||
* @param {Document} toolboxDoc
|
||||
* The toolbox document to attach the HTMLTooltip popup.
|
||||
* @param {Object}
|
||||
* - {String} type
|
||||
* Display type of the tooltip. Possible values: "normal", "arrow"
|
||||
* - {Boolean} autofocus
|
||||
* Defaults to false. Should the tooltip be focused when opening it.
|
||||
* - {Boolean} consumeOutsideClicks
|
||||
* Defaults to true. The tooltip is closed when clicking outside.
|
||||
* Should this event be stopped and consumed or not.
|
||||
* - {Boolean} useXulWrapper
|
||||
* Defaults to false. If the tooltip is hosted in a XUL document, use a XUL panel
|
||||
* in order to use all the screen viewport available.
|
||||
* - {String} stylesheet
|
||||
* Style sheet URL to apply to the tooltip content.
|
||||
*/
|
||||
function HTMLTooltip(toolboxDoc, {
|
||||
type = "normal",
|
||||
autofocus = false,
|
||||
consumeOutsideClicks = true,
|
||||
useXulWrapper = false,
|
||||
stylesheet = "",
|
||||
} = {}) {
|
||||
EventEmitter.decorate(this);
|
||||
|
||||
this.doc = toolboxDoc;
|
||||
this.type = type;
|
||||
this.autofocus = autofocus;
|
||||
this.consumeOutsideClicks = consumeOutsideClicks;
|
||||
this.useXulWrapper = this._isXUL() && useXulWrapper;
|
||||
|
||||
// The top window is used to attach click event listeners to close the tooltip if the
|
||||
// user clicks on the content page.
|
||||
this.topWindow = this._getTopWindow();
|
||||
|
||||
this._position = null;
|
||||
|
||||
this._onClick = this._onClick.bind(this);
|
||||
this._onXulPanelHidden = this._onXulPanelHidden.bind(this);
|
||||
|
||||
this._toggle = new TooltipToggle(this);
|
||||
this.startTogglingOnHover = this._toggle.start.bind(this._toggle);
|
||||
this.stopTogglingOnHover = this._toggle.stop.bind(this._toggle);
|
||||
|
||||
this.container = this._createContainer();
|
||||
|
||||
if (stylesheet) {
|
||||
this._applyStylesheet(stylesheet);
|
||||
}
|
||||
if (this.useXulWrapper) {
|
||||
// When using a XUL panel as the wrapper, the actual markup for the tooltip is as
|
||||
// follows :
|
||||
// <panel> <!-- XUL panel used to position the tooltip anywhere on screen -->
|
||||
// <div> <!-- div wrapper used to isolate the tooltip container -->
|
||||
// <div> <! the actual tooltip.container element -->
|
||||
this.xulPanelWrapper = this._createXulPanelWrapper();
|
||||
let inner = this.doc.createElementNS(XHTML_NS, "div");
|
||||
inner.classList.add("tooltip-xul-wrapper-inner");
|
||||
|
||||
this.doc.documentElement.appendChild(this.xulPanelWrapper);
|
||||
this.xulPanelWrapper.appendChild(inner);
|
||||
inner.appendChild(this.container);
|
||||
} else if (this._isXUL()) {
|
||||
this.doc.documentElement.appendChild(this.container);
|
||||
} else {
|
||||
// In non-XUL context the container is ready to use as is.
|
||||
this.doc.body.appendChild(this.container);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.HTMLTooltip = HTMLTooltip;
|
||||
|
||||
HTMLTooltip.prototype = {
|
||||
/**
|
||||
* The tooltip panel is the parentNode of the tooltip content provided in
|
||||
* setContent().
|
||||
*/
|
||||
get panel() {
|
||||
return this.container.querySelector(".tooltip-panel");
|
||||
},
|
||||
|
||||
/**
|
||||
* The arrow element. Might be null depending on the tooltip type.
|
||||
*/
|
||||
get arrow() {
|
||||
return this.container.querySelector(".tooltip-arrow");
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the displayed position used for the tooltip. Null if the tooltip is hidden.
|
||||
*/
|
||||
get position() {
|
||||
return this.isVisible() ? this._position : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the tooltip content element. The preferred width/height should also be
|
||||
* specified here.
|
||||
*
|
||||
* @param {Element} content
|
||||
* The tooltip content, should be a HTML element.
|
||||
* @param {Object}
|
||||
* - {Number} width: preferred width for the tooltip container. If not specified
|
||||
* the tooltip container will be measured before being displayed, and the
|
||||
* measured width will be used as preferred width.
|
||||
* - {Number} height: optional, preferred height for the tooltip container. If
|
||||
* not specified, the tooltip will be able to use all the height available.
|
||||
*/
|
||||
setContent: function (content, {width = "auto", height = Infinity} = {}) {
|
||||
this.preferredWidth = width;
|
||||
this.preferredHeight = height;
|
||||
|
||||
this.panel.innerHTML = "";
|
||||
this.panel.appendChild(content);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the tooltip next to the provided anchor element. A preferred position
|
||||
* can be set. The event "shown" will be fired after the tooltip is displayed.
|
||||
*
|
||||
* @param {Element} anchor
|
||||
* The reference element with which the tooltip should be aligned
|
||||
* @param {Object}
|
||||
* - {String} position: optional, possible values: top|bottom
|
||||
* If layout permits, the tooltip will be displayed on top/bottom
|
||||
* of the anchor. If ommitted, the tooltip will be displayed where
|
||||
* more space is available.
|
||||
* - {Number} x: optional, horizontal offset between the anchor and the tooltip
|
||||
* - {Number} y: optional, vertical offset between the anchor and the tooltip
|
||||
*/
|
||||
show: Task.async(function* (anchor, {position, x = 0, y = 0} = {}) {
|
||||
// Get anchor geometry
|
||||
let anchorRect = getRelativeRect(anchor, this.doc);
|
||||
if (this.useXulWrapper) {
|
||||
anchorRect = this._convertToScreenRect(anchorRect);
|
||||
}
|
||||
|
||||
// Get viewport size
|
||||
let viewportRect = this._getViewportRect();
|
||||
|
||||
let themeHeight = EXTRA_HEIGHT[this.type] + 2 * EXTRA_BORDER[this.type];
|
||||
let preferredHeight = this.preferredHeight + themeHeight;
|
||||
|
||||
let {top, height, computedPosition} =
|
||||
calculateVerticalPosition(anchorRect, viewportRect, preferredHeight, position, y);
|
||||
|
||||
this._position = computedPosition;
|
||||
// Apply height before measuring the content width (if width="auto").
|
||||
let isTop = computedPosition === POSITION.TOP;
|
||||
this.container.classList.toggle("tooltip-top", isTop);
|
||||
this.container.classList.toggle("tooltip-bottom", !isTop);
|
||||
|
||||
// If the preferred height is set to Infinity, the tooltip container should grow based
|
||||
// on its content's height and use as much height as possible.
|
||||
this.container.classList.toggle("tooltip-flexible-height",
|
||||
this.preferredHeight === Infinity);
|
||||
|
||||
this.container.style.height = height + "px";
|
||||
|
||||
let preferredWidth;
|
||||
if (this.preferredWidth === "auto") {
|
||||
preferredWidth = this._measureContainerWidth();
|
||||
} else {
|
||||
let themeWidth = 2 * EXTRA_BORDER[this.type];
|
||||
preferredWidth = this.preferredWidth + themeWidth;
|
||||
}
|
||||
|
||||
let anchorWin = anchor.ownerDocument.defaultView;
|
||||
let isRtl = anchorWin.getComputedStyle(anchor).direction === "rtl";
|
||||
let {left, width, arrowLeft} = calculateHorizontalPosition(
|
||||
anchorRect, viewportRect, preferredWidth, this.type, x, isRtl);
|
||||
|
||||
this.container.style.width = width + "px";
|
||||
|
||||
if (this.type === TYPE.ARROW) {
|
||||
this.arrow.style.left = arrowLeft + "px";
|
||||
}
|
||||
|
||||
if (this.useXulWrapper) {
|
||||
yield this._showXulWrapperAt(left, top);
|
||||
} else {
|
||||
this.container.style.left = left + "px";
|
||||
this.container.style.top = top + "px";
|
||||
}
|
||||
|
||||
this.container.classList.add("tooltip-visible");
|
||||
|
||||
// Keep a pointer on the focused element to refocus it when hiding the tooltip.
|
||||
this._focusedElement = this.doc.activeElement;
|
||||
|
||||
this.doc.defaultView.clearTimeout(this.attachEventsTimer);
|
||||
this.attachEventsTimer = this.doc.defaultView.setTimeout(() => {
|
||||
this._maybeFocusTooltip();
|
||||
// Updated the top window reference each time in case the host changes.
|
||||
this.topWindow = this._getTopWindow();
|
||||
this.topWindow.addEventListener("click", this._onClick, true);
|
||||
this.emit("shown");
|
||||
}, 0);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Calculate the rect of the viewport that limits the tooltip dimensions. When using a
|
||||
* XUL panel wrapper, the viewport will be able to use the whole screen (excluding space
|
||||
* reserved by the OS for toolbars etc.). Otherwise, the viewport is limited to the
|
||||
* tooltip's document.
|
||||
*
|
||||
* @return {Object} DOMRect-like object with the Number properties: top, right, bottom,
|
||||
* left, width, height
|
||||
*/
|
||||
_getViewportRect: function () {
|
||||
if (this.useXulWrapper) {
|
||||
// availLeft/Top are the coordinates first pixel available on the screen for
|
||||
// applications (excluding space dedicated for OS toolbars, menus etc...)
|
||||
// availWidth/Height are the dimensions available to applications excluding all
|
||||
// the OS reserved space
|
||||
let {availLeft, availTop, availHeight, availWidth} = this.doc.defaultView.screen;
|
||||
return {
|
||||
top: availTop,
|
||||
right: availLeft + availWidth,
|
||||
bottom: availTop + availHeight,
|
||||
left: availLeft,
|
||||
width: availWidth,
|
||||
height: availHeight,
|
||||
};
|
||||
}
|
||||
|
||||
return this.doc.documentElement.getBoundingClientRect();
|
||||
},
|
||||
|
||||
_measureContainerWidth: function () {
|
||||
let xulParent = this.container.parentNode;
|
||||
if (this.useXulWrapper && !this.isVisible()) {
|
||||
// Move the container out of the XUL Panel to measure it.
|
||||
this.doc.documentElement.appendChild(this.container);
|
||||
}
|
||||
|
||||
this.container.classList.add("tooltip-hidden");
|
||||
this.container.style.width = "auto";
|
||||
let width = this.container.getBoundingClientRect().width;
|
||||
this.container.classList.remove("tooltip-hidden");
|
||||
|
||||
if (this.useXulWrapper && !this.isVisible()) {
|
||||
xulParent.appendChild(this.container);
|
||||
}
|
||||
|
||||
return width;
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide the current tooltip. The event "hidden" will be fired when the tooltip
|
||||
* is hidden.
|
||||
*/
|
||||
hide: Task.async(function* () {
|
||||
this.doc.defaultView.clearTimeout(this.attachEventsTimer);
|
||||
if (!this.isVisible()) {
|
||||
this.emit("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
this.topWindow.removeEventListener("click", this._onClick, true);
|
||||
this.container.classList.remove("tooltip-visible");
|
||||
if (this.useXulWrapper) {
|
||||
yield this._hideXulWrapper();
|
||||
}
|
||||
|
||||
this.emit("hidden");
|
||||
|
||||
let tooltipHasFocus = this.container.contains(this.doc.activeElement);
|
||||
if (tooltipHasFocus && this._focusedElement) {
|
||||
this._focusedElement.focus();
|
||||
this._focusedElement = null;
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Check if the tooltip is currently displayed.
|
||||
* @return {Boolean} true if the tooltip is visible
|
||||
*/
|
||||
isVisible: function () {
|
||||
return this.container.classList.contains("tooltip-visible");
|
||||
},
|
||||
|
||||
/**
|
||||
* Destroy the tooltip instance. Hide the tooltip if displayed, remove the
|
||||
* tooltip container from the document.
|
||||
*/
|
||||
destroy: function () {
|
||||
this.hide();
|
||||
this.container.remove();
|
||||
if (this.xulPanelWrapper) {
|
||||
this.xulPanelWrapper.remove();
|
||||
}
|
||||
},
|
||||
|
||||
_createContainer: function () {
|
||||
let container = this.doc.createElementNS(XHTML_NS, "div");
|
||||
container.setAttribute("type", this.type);
|
||||
container.classList.add("tooltip-container");
|
||||
|
||||
let html = '<div class="tooltip-filler"></div>';
|
||||
html += '<div class="tooltip-panel"></div>';
|
||||
|
||||
if (this.type === TYPE.ARROW) {
|
||||
html += '<div class="tooltip-arrow"></div>';
|
||||
}
|
||||
container.innerHTML = html;
|
||||
return container;
|
||||
},
|
||||
|
||||
_onClick: function (e) {
|
||||
if (this._isInTooltipContainer(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hide();
|
||||
if (this.consumeOutsideClicks && e.button === 0) {
|
||||
// Consume only left click events (button === 0).
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
_isInTooltipContainer: function (node) {
|
||||
// Check if the target is the tooltip arrow.
|
||||
if (this.arrow && this.arrow === node) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let tooltipWindow = this.panel.ownerDocument.defaultView;
|
||||
let win = node.ownerDocument.defaultView;
|
||||
|
||||
// Check if the tooltip panel contains the node if they live in the same document.
|
||||
if (win === tooltipWindow) {
|
||||
return this.panel.contains(node);
|
||||
}
|
||||
|
||||
// Check if the node window is in the tooltip container.
|
||||
while (win.parent && win.parent !== win) {
|
||||
if (win.parent === tooltipWindow) {
|
||||
// If the parent window is the tooltip window, check if the tooltip contains
|
||||
// the current frame element.
|
||||
return this.panel.contains(win.frameElement);
|
||||
}
|
||||
win = win.parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
_onXulPanelHidden: function () {
|
||||
if (this.isVisible()) {
|
||||
this.hide();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* If the tootlip is configured to autofocus and a focusable element can be found,
|
||||
* focus it.
|
||||
*/
|
||||
_maybeFocusTooltip: function () {
|
||||
// Simplied selector targetting elements that can receive the focus, full version at
|
||||
// http://stackoverflow.com/questions/1599660/which-html-elements-can-receive-focus .
|
||||
let focusableSelector = "a, button, iframe, input, select, textarea";
|
||||
let focusableElement = this.panel.querySelector(focusableSelector);
|
||||
if (this.autofocus && focusableElement) {
|
||||
focusableElement.focus();
|
||||
}
|
||||
},
|
||||
|
||||
_getTopWindow: function () {
|
||||
return this.doc.defaultView.top;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the tooltip's owner document is a XUL document.
|
||||
*/
|
||||
_isXUL: function () {
|
||||
return this.doc.documentElement.namespaceURI === XUL_NS;
|
||||
},
|
||||
|
||||
_createXulPanelWrapper: function () {
|
||||
let panel = this.doc.createElementNS(XUL_NS, "panel");
|
||||
|
||||
// XUL panel is only a way to display DOM elements outside of the document viewport,
|
||||
// so disable all features that impact the behavior.
|
||||
panel.setAttribute("animate", false);
|
||||
panel.setAttribute("consumeoutsideclicks", false);
|
||||
panel.setAttribute("noautofocus", true);
|
||||
panel.setAttribute("ignorekeys", true);
|
||||
panel.setAttribute("tooltip", "aHTMLTooltip");
|
||||
|
||||
// Use type="arrow" to prevent side effects (see Bug 1285206)
|
||||
panel.setAttribute("type", "arrow");
|
||||
|
||||
panel.setAttribute("level", "top");
|
||||
panel.setAttribute("class", "tooltip-xul-wrapper");
|
||||
|
||||
return panel;
|
||||
},
|
||||
|
||||
_showXulWrapperAt: function (left, top) {
|
||||
this.xulPanelWrapper.addEventListener("popuphidden", this._onXulPanelHidden);
|
||||
let onPanelShown = listenOnce(this.xulPanelWrapper, "popupshown");
|
||||
this.xulPanelWrapper.openPopupAtScreen(left, top, false);
|
||||
return onPanelShown;
|
||||
},
|
||||
|
||||
_hideXulWrapper: function () {
|
||||
this.xulPanelWrapper.removeEventListener("popuphidden", this._onXulPanelHidden);
|
||||
|
||||
if (this.xulPanelWrapper.state === "closed") {
|
||||
// XUL panel is already closed, resolve immediately.
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let onPanelHidden = listenOnce(this.xulPanelWrapper, "popuphidden");
|
||||
this.xulPanelWrapper.hidePopup();
|
||||
return onPanelHidden;
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert from coordinates relative to the tooltip's document, to coordinates relative
|
||||
* to the "available" screen. By "available" we mean the screen, excluding the OS bars
|
||||
* display on screen edges.
|
||||
*/
|
||||
_convertToScreenRect: function ({left, top, width, height}) {
|
||||
// mozInnerScreenX/Y are the coordinates of the top left corner of the window's
|
||||
// viewport, excluding chrome UI.
|
||||
left += this.doc.defaultView.mozInnerScreenX;
|
||||
top += this.doc.defaultView.mozInnerScreenY;
|
||||
return {top, right: left + width, bottom: top + height, left, width, height};
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply a scoped stylesheet to the container so that this css file only
|
||||
* applies to it.
|
||||
*/
|
||||
_applyStylesheet: function (url) {
|
||||
let style = this.doc.createElementNS(XHTML_NS, "style");
|
||||
style.setAttribute("scoped", "true");
|
||||
url = url.replace(/"/g, "\\\"");
|
||||
style.textContent = `@import url("${url}");`;
|
||||
this.container.appendChild(style);
|
||||
}
|
||||
};
|
||||
131
devtools/client/shared/widgets/tooltip/ImageTooltipHelper.js
Normal file
131
devtools/client/shared/widgets/tooltip/ImageTooltipHelper.js
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
/* -*- 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {LocalizationHelper} = require("devtools/shared/l10n");
|
||||
const L10N = new LocalizationHelper("devtools/client/locales/inspector.properties");
|
||||
|
||||
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
// Default image tooltip max dimension
|
||||
const MAX_DIMENSION = 200;
|
||||
const CONTAINER_MIN_WIDTH = 100;
|
||||
const LABEL_HEIGHT = 20;
|
||||
const IMAGE_PADDING = 4;
|
||||
|
||||
/**
|
||||
* Image preview tooltips should be provided with the naturalHeight and
|
||||
* naturalWidth value for the image to display. This helper loads the provided
|
||||
* image URL in an image object in order to retrieve the image dimensions after
|
||||
* the load.
|
||||
*
|
||||
* @param {Document} doc the document element to use to create the image object
|
||||
* @param {String} imageUrl the url of the image to measure
|
||||
* @return {Promise} returns a promise that will resolve after the iamge load:
|
||||
* - {Number} naturalWidth natural width of the loaded image
|
||||
* - {Number} naturalHeight natural height of the loaded image
|
||||
*/
|
||||
function getImageDimensions(doc, imageUrl) {
|
||||
return new Promise(resolve => {
|
||||
let imgObj = new doc.defaultView.Image();
|
||||
imgObj.onload = () => {
|
||||
imgObj.onload = null;
|
||||
let { naturalWidth, naturalHeight } = imgObj;
|
||||
resolve({ naturalWidth, naturalHeight });
|
||||
};
|
||||
imgObj.src = imageUrl;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tooltip content of a provided HTMLTooltip instance to display an
|
||||
* image preview matching the provided imageUrl.
|
||||
*
|
||||
* @param {HTMLTooltip} tooltip
|
||||
* The tooltip instance on which the image preview content should be set
|
||||
* @param {Document} doc
|
||||
* A document element to create the HTML elements needed for the tooltip
|
||||
* @param {String} imageUrl
|
||||
* Absolute URL of the image to display in the tooltip
|
||||
* @param {Object} options
|
||||
* - {Number} naturalWidth mandatory, width of the image to display
|
||||
* - {Number} naturalHeight mandatory, height of the image to display
|
||||
* - {Number} maxDim optional, max width/height of the preview
|
||||
* - {Boolean} hideDimensionLabel optional, pass true to hide the label
|
||||
*/
|
||||
function setImageTooltip(tooltip, doc, imageUrl, options) {
|
||||
let {naturalWidth, naturalHeight, hideDimensionLabel, maxDim} = options;
|
||||
maxDim = maxDim || MAX_DIMENSION;
|
||||
|
||||
let imgHeight = naturalHeight;
|
||||
let imgWidth = naturalWidth;
|
||||
if (imgHeight > maxDim || imgWidth > maxDim) {
|
||||
let scale = maxDim / Math.max(imgHeight, imgWidth);
|
||||
// Only allow integer values to avoid rounding errors.
|
||||
imgHeight = Math.floor(scale * naturalHeight);
|
||||
imgWidth = Math.ceil(scale * naturalWidth);
|
||||
}
|
||||
|
||||
// Create tooltip content
|
||||
let div = doc.createElementNS(XHTML_NS, "div");
|
||||
div.style.cssText = `
|
||||
height: 100%;
|
||||
min-width: 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;`;
|
||||
let html = `
|
||||
<div style="flex: 1;
|
||||
display: flex;
|
||||
padding: ${IMAGE_PADDING}px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 1px;">
|
||||
<img style="height: ${imgHeight}px; max-height: 100%;"
|
||||
src="${encodeURI(imageUrl)}"/>
|
||||
</div>`;
|
||||
|
||||
if (!hideDimensionLabel) {
|
||||
let label = naturalWidth + " \u00D7 " + naturalHeight;
|
||||
html += `
|
||||
<div style="height: ${LABEL_HEIGHT}px;
|
||||
text-align: center;">
|
||||
<span class="theme-comment devtools-tooltip-caption">${label}</span>
|
||||
</div>`;
|
||||
}
|
||||
div.innerHTML = html;
|
||||
|
||||
// Calculate tooltip dimensions
|
||||
let height = imgHeight + 2 * IMAGE_PADDING;
|
||||
if (!hideDimensionLabel) {
|
||||
height += LABEL_HEIGHT;
|
||||
}
|
||||
let width = Math.max(CONTAINER_MIN_WIDTH, imgWidth + 2 * IMAGE_PADDING);
|
||||
|
||||
tooltip.setContent(div, {width, height});
|
||||
}
|
||||
|
||||
/*
|
||||
* Set the tooltip content of a provided HTMLTooltip instance to display a
|
||||
* fallback error message when an image preview tooltip can not be displayed.
|
||||
*
|
||||
* @param {HTMLTooltip} tooltip
|
||||
* The tooltip instance on which the image preview content should be set
|
||||
* @param {Document} doc
|
||||
* A document element to create the HTML elements needed for the tooltip
|
||||
*/
|
||||
function setBrokenImageTooltip(tooltip, doc) {
|
||||
let div = doc.createElementNS(XHTML_NS, "div");
|
||||
div.className = "theme-comment devtools-tooltip-image-broken";
|
||||
let message = L10N.getStr("previewTooltip.image.brokenImage");
|
||||
div.textContent = message;
|
||||
tooltip.setContent(div, {width: 150, height: 30});
|
||||
}
|
||||
|
||||
module.exports.getImageDimensions = getImageDimensions;
|
||||
module.exports.setImageTooltip = setImageTooltip;
|
||||
module.exports.setBrokenImageTooltip = setBrokenImageTooltip;
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
|
||||
const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
|
||||
|
||||
/**
|
||||
* Base class for all (color, gradient, ...)-swatch based value editors inside
|
||||
* tooltips
|
||||
*
|
||||
* @param {Document} document
|
||||
* The document to attach the SwatchBasedEditorTooltip. This is either the toolbox
|
||||
* document if the tooltip is a popup tooltip or the panel's document if it is an
|
||||
* inline editor.
|
||||
*/
|
||||
function SwatchBasedEditorTooltip(document, stylesheet) {
|
||||
EventEmitter.decorate(this);
|
||||
// Creating a tooltip instance
|
||||
// This one will consume outside clicks as it makes more sense to let the user
|
||||
// close the tooltip by clicking out
|
||||
// It will also close on <escape> and <enter>
|
||||
this.tooltip = new HTMLTooltip(document, {
|
||||
type: "arrow",
|
||||
consumeOutsideClicks: true,
|
||||
useXulWrapper: true,
|
||||
stylesheet
|
||||
});
|
||||
|
||||
// By default, swatch-based editor tooltips revert value change on <esc> and
|
||||
// commit value change on <enter>
|
||||
this.shortcuts = new KeyShortcuts({
|
||||
window: this.tooltip.topWindow
|
||||
});
|
||||
this.shortcuts.on("Escape", (name, event) => {
|
||||
if (!this.tooltip.isVisible()) {
|
||||
return;
|
||||
}
|
||||
this.revert();
|
||||
this.hide();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
});
|
||||
this.shortcuts.on("Return", (name, event) => {
|
||||
if (!this.tooltip.isVisible()) {
|
||||
return;
|
||||
}
|
||||
this.commit();
|
||||
this.hide();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
// All target swatches are kept in a map, indexed by swatch DOM elements
|
||||
this.swatches = new Map();
|
||||
|
||||
// When a swatch is clicked, and for as long as the tooltip is shown, the
|
||||
// activeSwatch property will hold the reference to the swatch DOM element
|
||||
// that was clicked
|
||||
this.activeSwatch = null;
|
||||
|
||||
this._onSwatchClick = this._onSwatchClick.bind(this);
|
||||
}
|
||||
|
||||
SwatchBasedEditorTooltip.prototype = {
|
||||
/**
|
||||
* Show the editor tooltip for the currently active swatch.
|
||||
*
|
||||
* @return {Promise} a promise that resolves once the editor tooltip is displayed, or
|
||||
* immediately if there is no currently active swatch.
|
||||
*/
|
||||
show: function () {
|
||||
if (this.activeSwatch) {
|
||||
let onShown = this.tooltip.once("shown");
|
||||
this.tooltip.show(this.activeSwatch, "topcenter bottomleft");
|
||||
|
||||
// When the tooltip is closed by clicking outside the panel we want to
|
||||
// commit any changes.
|
||||
this.tooltip.once("hidden", () => {
|
||||
if (!this._reverted && !this.eyedropperOpen) {
|
||||
this.commit();
|
||||
}
|
||||
this._reverted = false;
|
||||
|
||||
// Once the tooltip is hidden we need to clean up any remaining objects.
|
||||
if (!this.eyedropperOpen) {
|
||||
this.activeSwatch = null;
|
||||
}
|
||||
});
|
||||
|
||||
return onShown;
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
hide: function () {
|
||||
this.tooltip.hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new swatch DOM element to the list of swatch elements this editor
|
||||
* tooltip knows about. That means from now on, clicking on that swatch will
|
||||
* toggle the editor.
|
||||
*
|
||||
* @param {node} swatchEl
|
||||
* The element to add
|
||||
* @param {object} callbacks
|
||||
* Callbacks that will be executed when the editor wants to preview a
|
||||
* value change, or revert a change, or commit a change.
|
||||
* - onShow: will be called when one of the swatch tooltip is shown
|
||||
* - onPreview: will be called when one of the sub-classes calls
|
||||
* preview
|
||||
* - onRevert: will be called when the user ESCapes out of the tooltip
|
||||
* - onCommit: will be called when the user presses ENTER or clicks
|
||||
* outside the tooltip.
|
||||
*/
|
||||
addSwatch: function (swatchEl, callbacks = {}) {
|
||||
if (!callbacks.onShow) {
|
||||
callbacks.onShow = function () {};
|
||||
}
|
||||
if (!callbacks.onPreview) {
|
||||
callbacks.onPreview = function () {};
|
||||
}
|
||||
if (!callbacks.onRevert) {
|
||||
callbacks.onRevert = function () {};
|
||||
}
|
||||
if (!callbacks.onCommit) {
|
||||
callbacks.onCommit = function () {};
|
||||
}
|
||||
|
||||
this.swatches.set(swatchEl, {
|
||||
callbacks: callbacks
|
||||
});
|
||||
swatchEl.addEventListener("click", this._onSwatchClick, false);
|
||||
},
|
||||
|
||||
removeSwatch: function (swatchEl) {
|
||||
if (this.swatches.has(swatchEl)) {
|
||||
if (this.activeSwatch === swatchEl) {
|
||||
this.hide();
|
||||
this.activeSwatch = null;
|
||||
}
|
||||
swatchEl.removeEventListener("click", this._onSwatchClick, false);
|
||||
this.swatches.delete(swatchEl);
|
||||
}
|
||||
},
|
||||
|
||||
_onSwatchClick: function (event) {
|
||||
let swatch = this.swatches.get(event.target);
|
||||
|
||||
if (event.shiftKey) {
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
if (swatch) {
|
||||
this.activeSwatch = event.target;
|
||||
this.show();
|
||||
swatch.callbacks.onShow();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Not called by this parent class, needs to be taken care of by sub-classes
|
||||
*/
|
||||
preview: function (value) {
|
||||
if (this.activeSwatch) {
|
||||
let swatch = this.swatches.get(this.activeSwatch);
|
||||
swatch.callbacks.onPreview(value);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This parent class only calls this on <esc> keypress
|
||||
*/
|
||||
revert: function () {
|
||||
if (this.activeSwatch) {
|
||||
this._reverted = true;
|
||||
let swatch = this.swatches.get(this.activeSwatch);
|
||||
this.tooltip.once("hidden", () => {
|
||||
swatch.callbacks.onRevert();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This parent class only calls this on <enter> keypress
|
||||
*/
|
||||
commit: function () {
|
||||
if (this.activeSwatch) {
|
||||
let swatch = this.swatches.get(this.activeSwatch);
|
||||
swatch.callbacks.onCommit();
|
||||
}
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
this.swatches.clear();
|
||||
this.activeSwatch = null;
|
||||
this.tooltip.off("keypress", this._onTooltipKeypress);
|
||||
this.tooltip.destroy();
|
||||
this.shortcuts.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = SwatchBasedEditorTooltip;
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {Task} = require("devtools/shared/task");
|
||||
const {colorUtils} = require("devtools/shared/css/color");
|
||||
const {Spectrum} = require("devtools/client/shared/widgets/Spectrum");
|
||||
const SwatchBasedEditorTooltip = require("devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip");
|
||||
const {LocalizationHelper} = require("devtools/shared/l10n");
|
||||
const L10N = new LocalizationHelper("devtools/client/locales/inspector.properties");
|
||||
|
||||
const Heritage = require("sdk/core/heritage");
|
||||
|
||||
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
/**
|
||||
* The swatch color picker tooltip class is a specific class meant to be used
|
||||
* along with output-parser's generated color swatches.
|
||||
* It extends the parent SwatchBasedEditorTooltip class.
|
||||
* It just wraps a standard Tooltip and sets its content with an instance of a
|
||||
* color picker.
|
||||
*
|
||||
* @param {Document} document
|
||||
* The document to attach the SwatchColorPickerTooltip. This is either the toolbox
|
||||
* document if the tooltip is a popup tooltip or the panel's document if it is an
|
||||
* inline editor.
|
||||
* @param {InspectorPanel} inspector
|
||||
* The inspector panel, needed for the eyedropper.
|
||||
*/
|
||||
function SwatchColorPickerTooltip(document, inspector) {
|
||||
let stylesheet = "chrome://devtools/content/shared/widgets/spectrum.css";
|
||||
SwatchBasedEditorTooltip.call(this, document, stylesheet);
|
||||
|
||||
this.inspector = inspector;
|
||||
|
||||
// Creating a spectrum instance. this.spectrum will always be a promise that
|
||||
// resolves to the spectrum instance
|
||||
this.spectrum = this.setColorPickerContent([0, 0, 0, 1]);
|
||||
this._onSpectrumColorChange = this._onSpectrumColorChange.bind(this);
|
||||
this._openEyeDropper = this._openEyeDropper.bind(this);
|
||||
}
|
||||
|
||||
SwatchColorPickerTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.prototype, {
|
||||
/**
|
||||
* Fill the tooltip with a new instance of the spectrum color picker widget
|
||||
* initialized with the given color, and return the instance of spectrum
|
||||
*/
|
||||
setColorPickerContent: function (color) {
|
||||
let { doc } = this.tooltip;
|
||||
|
||||
let container = doc.createElementNS(XHTML_NS, "div");
|
||||
container.id = "spectrum-tooltip";
|
||||
let spectrumNode = doc.createElementNS(XHTML_NS, "div");
|
||||
spectrumNode.id = "spectrum";
|
||||
container.appendChild(spectrumNode);
|
||||
let eyedropper = doc.createElementNS(XHTML_NS, "button");
|
||||
eyedropper.id = "eyedropper-button";
|
||||
eyedropper.className = "devtools-button";
|
||||
/* pointerEvents for eyedropper has to be set auto to display tooltip when
|
||||
* eyedropper is disabled in non-HTML documents.
|
||||
*/
|
||||
eyedropper.style.pointerEvents = "auto";
|
||||
container.appendChild(eyedropper);
|
||||
|
||||
this.tooltip.setContent(container, { width: 218, height: 224 });
|
||||
|
||||
let spectrum = new Spectrum(spectrumNode, color);
|
||||
|
||||
// Wait for the tooltip to be shown before calling spectrum.show
|
||||
// as it expect to be visible in order to compute DOM element sizes.
|
||||
this.tooltip.once("shown", () => {
|
||||
spectrum.show();
|
||||
});
|
||||
|
||||
return spectrum;
|
||||
},
|
||||
|
||||
/**
|
||||
* Overriding the SwatchBasedEditorTooltip.show function to set spectrum's
|
||||
* color.
|
||||
*/
|
||||
show: Task.async(function* () {
|
||||
// Call then parent class' show function
|
||||
yield SwatchBasedEditorTooltip.prototype.show.call(this);
|
||||
// Then set spectrum's color and listen to color changes to preview them
|
||||
if (this.activeSwatch) {
|
||||
this.currentSwatchColor = this.activeSwatch.nextSibling;
|
||||
this._originalColor = this.currentSwatchColor.textContent;
|
||||
let color = this.activeSwatch.style.backgroundColor;
|
||||
this.spectrum.off("changed", this._onSpectrumColorChange);
|
||||
this.spectrum.rgb = this._colorToRgba(color);
|
||||
this.spectrum.on("changed", this._onSpectrumColorChange);
|
||||
this.spectrum.updateUI();
|
||||
}
|
||||
|
||||
let {target} = this.inspector;
|
||||
target.actorHasMethod("inspector", "pickColorFromPage").then(value => {
|
||||
let tooltipDoc = this.tooltip.doc;
|
||||
let eyeButton = tooltipDoc.querySelector("#eyedropper-button");
|
||||
if (value && this.inspector.selection.nodeFront.isInHTMLDocument) {
|
||||
eyeButton.disabled = false;
|
||||
eyeButton.removeAttribute("title");
|
||||
eyeButton.addEventListener("click", this._openEyeDropper);
|
||||
} else {
|
||||
eyeButton.disabled = true;
|
||||
eyeButton.title = L10N.getStr("eyedropper.disabled.title");
|
||||
}
|
||||
this.emit("ready");
|
||||
}, e => console.error(e));
|
||||
}),
|
||||
|
||||
_onSpectrumColorChange: function (event, rgba, cssColor) {
|
||||
this._selectColor(cssColor);
|
||||
},
|
||||
|
||||
_selectColor: function (color) {
|
||||
if (this.activeSwatch) {
|
||||
this.activeSwatch.style.backgroundColor = color;
|
||||
this.activeSwatch.parentNode.dataset.color = color;
|
||||
|
||||
color = this._toDefaultType(color);
|
||||
this.currentSwatchColor.textContent = color;
|
||||
this.preview(color);
|
||||
|
||||
if (this.eyedropperOpen) {
|
||||
this.commit();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_openEyeDropper: function () {
|
||||
let {inspector, toolbox, telemetry} = this.inspector;
|
||||
telemetry.toolOpened("pickereyedropper");
|
||||
inspector.pickColorFromPage(toolbox, {copyOnSelect: false}).then(() => {
|
||||
this.eyedropperOpen = true;
|
||||
|
||||
// close the colorpicker tooltip so that only the eyedropper is open.
|
||||
this.hide();
|
||||
|
||||
this.tooltip.emit("eyedropper-opened");
|
||||
}, e => console.error(e));
|
||||
|
||||
inspector.once("color-picked", color => {
|
||||
toolbox.win.focus();
|
||||
this._selectColor(color);
|
||||
this._onEyeDropperDone();
|
||||
});
|
||||
|
||||
inspector.once("color-pick-canceled", () => {
|
||||
this._onEyeDropperDone();
|
||||
});
|
||||
},
|
||||
|
||||
_onEyeDropperDone: function () {
|
||||
this.eyedropperOpen = false;
|
||||
this.activeSwatch = null;
|
||||
},
|
||||
|
||||
_colorToRgba: function (color) {
|
||||
color = new colorUtils.CssColor(color);
|
||||
let rgba = color._getRGBATuple();
|
||||
return [rgba.r, rgba.g, rgba.b, rgba.a];
|
||||
},
|
||||
|
||||
_toDefaultType: function (color) {
|
||||
let colorObj = new colorUtils.CssColor(color);
|
||||
colorObj.setAuthoredUnitFromColor(this._originalColor);
|
||||
return colorObj.toString();
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
SwatchBasedEditorTooltip.prototype.destroy.call(this);
|
||||
this.inspector = null;
|
||||
this.currentSwatchColor = null;
|
||||
this.spectrum.off("changed", this._onSpectrumColorChange);
|
||||
this.spectrum.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = SwatchColorPickerTooltip;
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const defer = require("devtools/shared/defer");
|
||||
const {Task} = require("devtools/shared/task");
|
||||
const {CubicBezierWidget} = require("devtools/client/shared/widgets/CubicBezierWidget");
|
||||
const SwatchBasedEditorTooltip = require("devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip");
|
||||
|
||||
const Heritage = require("sdk/core/heritage");
|
||||
|
||||
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
/**
|
||||
* The swatch cubic-bezier tooltip class is a specific class meant to be used
|
||||
* along with rule-view's generated cubic-bezier swatches.
|
||||
* It extends the parent SwatchBasedEditorTooltip class.
|
||||
* It just wraps a standard Tooltip and sets its content with an instance of a
|
||||
* CubicBezierWidget.
|
||||
*
|
||||
* @param {Document} document
|
||||
* The document to attach the SwatchCubicBezierTooltip. This is either the toolbox
|
||||
* document if the tooltip is a popup tooltip or the panel's document if it is an
|
||||
* inline editor.
|
||||
*/
|
||||
function SwatchCubicBezierTooltip(document) {
|
||||
let stylesheet = "chrome://devtools/content/shared/widgets/cubic-bezier.css";
|
||||
SwatchBasedEditorTooltip.call(this, document, stylesheet);
|
||||
|
||||
// Creating a cubic-bezier instance.
|
||||
// this.widget will always be a promise that resolves to the widget instance
|
||||
this.widget = this.setCubicBezierContent([0, 0, 1, 1]);
|
||||
this._onUpdate = this._onUpdate.bind(this);
|
||||
}
|
||||
|
||||
SwatchCubicBezierTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.prototype, {
|
||||
/**
|
||||
* Fill the tooltip with a new instance of the cubic-bezier widget
|
||||
* initialized with the given value, and return a promise that resolves to
|
||||
* the instance of the widget
|
||||
*/
|
||||
setCubicBezierContent: function (bezier) {
|
||||
let { doc } = this.tooltip;
|
||||
|
||||
let container = doc.createElementNS(XHTML_NS, "div");
|
||||
container.className = "cubic-bezier-container";
|
||||
|
||||
this.tooltip.setContent(container, { width: 510, height: 370 });
|
||||
|
||||
let def = defer();
|
||||
|
||||
// Wait for the tooltip to be shown before calling instanciating the widget
|
||||
// as it expect its DOM elements to be visible.
|
||||
this.tooltip.once("shown", () => {
|
||||
let widget = new CubicBezierWidget(container, bezier);
|
||||
def.resolve(widget);
|
||||
});
|
||||
|
||||
return def.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Overriding the SwatchBasedEditorTooltip.show function to set the cubic
|
||||
* bezier curve in the widget
|
||||
*/
|
||||
show: Task.async(function* () {
|
||||
// Call the parent class' show function
|
||||
yield SwatchBasedEditorTooltip.prototype.show.call(this);
|
||||
// Then set the curve and listen to changes to preview them
|
||||
if (this.activeSwatch) {
|
||||
this.currentBezierValue = this.activeSwatch.nextSibling;
|
||||
this.widget.then(widget => {
|
||||
widget.off("updated", this._onUpdate);
|
||||
widget.cssCubicBezierValue = this.currentBezierValue.textContent;
|
||||
widget.on("updated", this._onUpdate);
|
||||
this.emit("ready");
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
_onUpdate: function (event, bezier) {
|
||||
if (!this.activeSwatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentBezierValue.textContent = bezier + "";
|
||||
this.preview(bezier + "");
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
SwatchBasedEditorTooltip.prototype.destroy.call(this);
|
||||
this.currentBezierValue = null;
|
||||
this.widget.then(widget => {
|
||||
widget.off("updated", this._onUpdate);
|
||||
widget.destroy();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = SwatchCubicBezierTooltip;
|
||||
116
devtools/client/shared/widgets/tooltip/SwatchFilterTooltip.js
Normal file
116
devtools/client/shared/widgets/tooltip/SwatchFilterTooltip.js
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {Task} = require("devtools/shared/task");
|
||||
const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
|
||||
const SwatchBasedEditorTooltip = require("devtools/client/shared/widgets/tooltip/SwatchBasedEditorTooltip");
|
||||
|
||||
const Heritage = require("sdk/core/heritage");
|
||||
|
||||
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
/**
|
||||
* The swatch-based css filter tooltip class is a specific class meant to be
|
||||
* used along with rule-view's generated css filter swatches.
|
||||
* It extends the parent SwatchBasedEditorTooltip class.
|
||||
* It just wraps a standard Tooltip and sets its content with an instance of a
|
||||
* CSSFilterEditorWidget.
|
||||
*
|
||||
* @param {Document} document
|
||||
* The document to attach the SwatchFilterTooltip. This is either the toolbox
|
||||
* document if the tooltip is a popup tooltip or the panel's document if it is an
|
||||
* inline editor.
|
||||
* @param {function} cssIsValid
|
||||
* A function to check that css declaration's name and values are valid together.
|
||||
* This can be obtained from "shared/fronts/css-properties.js".
|
||||
*/
|
||||
function SwatchFilterTooltip(document, cssIsValid) {
|
||||
let stylesheet = "chrome://devtools/content/shared/widgets/filter-widget.css";
|
||||
SwatchBasedEditorTooltip.call(this, document, stylesheet);
|
||||
this._cssIsValid = cssIsValid;
|
||||
|
||||
// Creating a filter editor instance.
|
||||
this.widget = this.setFilterContent("none");
|
||||
this._onUpdate = this._onUpdate.bind(this);
|
||||
}
|
||||
|
||||
SwatchFilterTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.prototype, {
|
||||
/**
|
||||
* Fill the tooltip with a new instance of the CSSFilterEditorWidget
|
||||
* widget initialized with the given filter value, and return a promise
|
||||
* that resolves to the instance of the widget when ready.
|
||||
*/
|
||||
setFilterContent: function (filter) {
|
||||
let { doc } = this.tooltip;
|
||||
|
||||
let container = doc.createElementNS(XHTML_NS, "div");
|
||||
container.id = "filter-container";
|
||||
|
||||
this.tooltip.setContent(container, { width: 510, height: 200 });
|
||||
|
||||
return new CSSFilterEditorWidget(container, filter, this._cssIsValid);
|
||||
},
|
||||
|
||||
show: Task.async(function* () {
|
||||
// Call the parent class' show function
|
||||
yield SwatchBasedEditorTooltip.prototype.show.call(this);
|
||||
// Then set the filter value and listen to changes to preview them
|
||||
if (this.activeSwatch) {
|
||||
this.currentFilterValue = this.activeSwatch.nextSibling;
|
||||
this.widget.off("updated", this._onUpdate);
|
||||
this.widget.on("updated", this._onUpdate);
|
||||
this.widget.setCssValue(this.currentFilterValue.textContent);
|
||||
this.widget.render();
|
||||
this.emit("ready");
|
||||
}
|
||||
}),
|
||||
|
||||
_onUpdate: function (event, filters) {
|
||||
if (!this.activeSwatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the old children and reparse the property value to
|
||||
// recompute them.
|
||||
while (this.currentFilterValue.firstChild) {
|
||||
this.currentFilterValue.firstChild.remove();
|
||||
}
|
||||
let node = this._parser.parseCssProperty("filter", filters, this._options);
|
||||
this.currentFilterValue.appendChild(node);
|
||||
|
||||
this.preview();
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
SwatchBasedEditorTooltip.prototype.destroy.call(this);
|
||||
this.currentFilterValue = null;
|
||||
this.widget.off("updated", this._onUpdate);
|
||||
this.widget.destroy();
|
||||
},
|
||||
|
||||
/**
|
||||
* Like SwatchBasedEditorTooltip.addSwatch, but accepts a parser object
|
||||
* to use when previewing the updated property value.
|
||||
*
|
||||
* @param {node} swatchEl
|
||||
* @see SwatchBasedEditorTooltip.addSwatch
|
||||
* @param {object} callbacks
|
||||
* @see SwatchBasedEditorTooltip.addSwatch
|
||||
* @param {object} parser
|
||||
* A parser object; @see OutputParser object
|
||||
* @param {object} options
|
||||
* options to pass to the output parser, with
|
||||
* the option |filterSwatch| set.
|
||||
*/
|
||||
addSwatch: function (swatchEl, callbacks, parser, options) {
|
||||
SwatchBasedEditorTooltip.prototype.addSwatch.call(this, swatchEl,
|
||||
callbacks);
|
||||
this._parser = parser;
|
||||
this._options = options;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = SwatchFilterTooltip;
|
||||
410
devtools/client/shared/widgets/tooltip/Tooltip.js
Normal file
410
devtools/client/shared/widgets/tooltip/Tooltip.js
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const defer = require("devtools/shared/defer");
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const {KeyCodes} = require("devtools/client/shared/keycodes");
|
||||
const {TooltipToggle} = require("devtools/client/shared/widgets/tooltip/TooltipToggle");
|
||||
|
||||
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
const ESCAPE_KEYCODE = KeyCodes.DOM_VK_ESCAPE;
|
||||
const POPUP_EVENTS = ["shown", "hidden", "showing", "hiding"];
|
||||
|
||||
/**
|
||||
* Tooltip widget.
|
||||
*
|
||||
* This widget is intended at any tool that may need to show rich content in the
|
||||
* form of floating panels.
|
||||
* A common use case is image previewing in the CSS rule view, but more complex
|
||||
* use cases may include color pickers, object inspection, etc...
|
||||
*
|
||||
* Tooltips are based on XUL (namely XUL arrow-type <panel>s), and therefore
|
||||
* need a XUL Document to live in.
|
||||
* This is pretty much the only requirement they have on their environment.
|
||||
*
|
||||
* The way to use a tooltip is simply by instantiating a tooltip yourself and
|
||||
* attaching some content in it, or using one of the ready-made content types.
|
||||
*
|
||||
* A convenient `startTogglingOnHover` method may avoid having to register event
|
||||
* handlers yourself if the tooltip has to be shown when hovering over a
|
||||
* specific element or group of elements (which is usually the most common case)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tooltip class.
|
||||
*
|
||||
* Basic usage:
|
||||
* let t = new Tooltip(xulDoc);
|
||||
* t.content = someXulContent;
|
||||
* t.show();
|
||||
* t.hide();
|
||||
* t.destroy();
|
||||
*
|
||||
* Better usage:
|
||||
* let t = new Tooltip(xulDoc);
|
||||
* t.startTogglingOnHover(container, target => {
|
||||
* if (<condition based on target>) {
|
||||
* t.content = el;
|
||||
* return true;
|
||||
* }
|
||||
* });
|
||||
* t.destroy();
|
||||
*
|
||||
* @param {XULDocument} doc
|
||||
* The XUL document hosting this tooltip
|
||||
* @param {Object} options
|
||||
* Optional options that give options to consumers:
|
||||
* - consumeOutsideClick {Boolean} Wether the first click outside of the
|
||||
* tooltip should close the tooltip and be consumed or not.
|
||||
* Defaults to false.
|
||||
* - closeOnKeys {Array} An array of key codes that should close the
|
||||
* tooltip. Defaults to [27] (escape key).
|
||||
* - closeOnEvents [{emitter: {Object}, event: {String},
|
||||
* useCapture: {Boolean}}]
|
||||
* Provide an optional list of emitter objects and event names here to
|
||||
* trigger the closing of the tooltip when these events are fired by the
|
||||
* emitters. The emitter objects should either implement
|
||||
* on/off(event, cb) or addEventListener/removeEventListener(event, cb).
|
||||
* Defaults to [].
|
||||
* For instance, the following would close the tooltip whenever the
|
||||
* toolbox selects a new tool and when a DOM node gets scrolled:
|
||||
* new Tooltip(doc, {
|
||||
* closeOnEvents: [
|
||||
* {emitter: toolbox, event: "select"},
|
||||
* {emitter: myContainer, event: "scroll", useCapture: true}
|
||||
* ]
|
||||
* });
|
||||
* - noAutoFocus {Boolean} Should the focus automatically go to the panel
|
||||
* when it opens. Defaults to true.
|
||||
*
|
||||
* Fires these events:
|
||||
* - showing : just before the tooltip shows
|
||||
* - shown : when the tooltip is shown
|
||||
* - hiding : just before the tooltip closes
|
||||
* - hidden : when the tooltip gets hidden
|
||||
* - keypress : when any key gets pressed, with keyCode
|
||||
*/
|
||||
function Tooltip(doc, {
|
||||
consumeOutsideClick = false,
|
||||
closeOnKeys = [ESCAPE_KEYCODE],
|
||||
noAutoFocus = true,
|
||||
closeOnEvents = [],
|
||||
} = {}) {
|
||||
EventEmitter.decorate(this);
|
||||
|
||||
this.doc = doc;
|
||||
this.consumeOutsideClick = consumeOutsideClick;
|
||||
this.closeOnKeys = closeOnKeys;
|
||||
this.noAutoFocus = noAutoFocus;
|
||||
this.closeOnEvents = closeOnEvents;
|
||||
|
||||
this.panel = this._createPanel();
|
||||
|
||||
// Create tooltip toggle helper and decorate the Tooltip instance with
|
||||
// shortcut methods.
|
||||
this._toggle = new TooltipToggle(this);
|
||||
this.startTogglingOnHover = this._toggle.start.bind(this._toggle);
|
||||
this.stopTogglingOnHover = this._toggle.stop.bind(this._toggle);
|
||||
|
||||
// Emit show/hide events when the panel does.
|
||||
for (let eventName of POPUP_EVENTS) {
|
||||
this["_onPopup" + eventName] = (name => {
|
||||
return e => {
|
||||
if (e.target === this.panel) {
|
||||
this.emit(name);
|
||||
}
|
||||
};
|
||||
})(eventName);
|
||||
this.panel.addEventListener("popup" + eventName,
|
||||
this["_onPopup" + eventName], false);
|
||||
}
|
||||
|
||||
// Listen to keypress events to close the tooltip if configured to do so
|
||||
let win = this.doc.querySelector("window");
|
||||
this._onKeyPress = event => {
|
||||
if (this.panel.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit("keypress", event.keyCode);
|
||||
if (this.closeOnKeys.indexOf(event.keyCode) !== -1 &&
|
||||
this.isShown()) {
|
||||
event.stopPropagation();
|
||||
this.hide();
|
||||
}
|
||||
};
|
||||
win.addEventListener("keypress", this._onKeyPress, false);
|
||||
|
||||
// Listen to custom emitters' events to close the tooltip
|
||||
this.hide = this.hide.bind(this);
|
||||
for (let {emitter, event, useCapture} of this.closeOnEvents) {
|
||||
for (let add of ["addEventListener", "on"]) {
|
||||
if (add in emitter) {
|
||||
emitter[add](event, this.hide, useCapture);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Tooltip.prototype = {
|
||||
defaultPosition: "before_start",
|
||||
// px
|
||||
defaultOffsetX: 0,
|
||||
// px
|
||||
defaultOffsetY: 0,
|
||||
// px
|
||||
|
||||
/**
|
||||
* Show the tooltip. It might be wise to append some content first if you
|
||||
* don't want the tooltip to be empty. You may access the content of the
|
||||
* tooltip by setting a XUL node to t.content.
|
||||
* @param {node} anchor
|
||||
* Which node should the tooltip be shown on
|
||||
* @param {string} position [optional]
|
||||
* Optional tooltip position. Defaults to before_start
|
||||
* https://developer.mozilla.org/en-US/docs/XUL/PopupGuide/Positioning
|
||||
* @param {number} x, y [optional]
|
||||
* The left and top offset coordinates, in pixels.
|
||||
*/
|
||||
show: function (anchor,
|
||||
position = this.defaultPosition,
|
||||
x = this.defaultOffsetX,
|
||||
y = this.defaultOffsetY) {
|
||||
this.panel.hidden = false;
|
||||
this.panel.openPopup(anchor, position, x, y);
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide the tooltip
|
||||
*/
|
||||
hide: function () {
|
||||
this.panel.hidden = true;
|
||||
this.panel.hidePopup();
|
||||
},
|
||||
|
||||
isShown: function () {
|
||||
return this.panel &&
|
||||
this.panel.state !== "closed" &&
|
||||
this.panel.state !== "hiding";
|
||||
},
|
||||
|
||||
setSize: function (width, height) {
|
||||
this.panel.sizeTo(width, height);
|
||||
},
|
||||
|
||||
/**
|
||||
* Empty the tooltip's content
|
||||
*/
|
||||
empty: function () {
|
||||
while (this.panel.hasChildNodes()) {
|
||||
this.panel.removeChild(this.panel.firstChild);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets this panel's visibility state.
|
||||
* @return boolean
|
||||
*/
|
||||
isHidden: function () {
|
||||
return this.panel.state == "closed" || this.panel.state == "hiding";
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets if this panel has any child nodes.
|
||||
* @return boolean
|
||||
*/
|
||||
isEmpty: function () {
|
||||
return !this.panel.hasChildNodes();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get rid of references and event listeners
|
||||
*/
|
||||
destroy: function () {
|
||||
this.hide();
|
||||
|
||||
for (let eventName of POPUP_EVENTS) {
|
||||
this.panel.removeEventListener("popup" + eventName,
|
||||
this["_onPopup" + eventName], false);
|
||||
}
|
||||
|
||||
let win = this.doc.querySelector("window");
|
||||
win.removeEventListener("keypress", this._onKeyPress, false);
|
||||
|
||||
for (let {emitter, event, useCapture} of this.closeOnEvents) {
|
||||
for (let remove of ["removeEventListener", "off"]) {
|
||||
if (remove in emitter) {
|
||||
emitter[remove](event, this.hide, useCapture);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.content = null;
|
||||
|
||||
this._toggle.destroy();
|
||||
|
||||
this.doc = null;
|
||||
|
||||
this.panel.remove();
|
||||
this.panel = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the outer container node (that includes the arrow etc.). Happens
|
||||
* to be identical to this.panel here, can be different element in other
|
||||
* Tooltip implementations.
|
||||
*/
|
||||
get container() {
|
||||
return this.panel;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the content of this tooltip. Will first empty the tooltip and then
|
||||
* append the new content element.
|
||||
* Consider using one of the set<type>Content() functions instead.
|
||||
* @param {node} content
|
||||
* A node that can be appended in the tooltip XUL element
|
||||
*/
|
||||
set content(content) {
|
||||
if (this.content == content) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.empty();
|
||||
this.panel.removeAttribute("clamped-dimensions");
|
||||
this.panel.removeAttribute("clamped-dimensions-no-min-height");
|
||||
this.panel.removeAttribute("clamped-dimensions-no-max-or-min-height");
|
||||
this.panel.removeAttribute("wide");
|
||||
|
||||
if (content) {
|
||||
this.panel.appendChild(content);
|
||||
}
|
||||
},
|
||||
|
||||
get content() {
|
||||
return this.panel.firstChild;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets some text as the content of this tooltip.
|
||||
*
|
||||
* @param {array} messages
|
||||
* A list of text messages.
|
||||
* @param {string} messagesClass [optional]
|
||||
* A style class for the text messages.
|
||||
* @param {string} containerClass [optional]
|
||||
* A style class for the text messages container.
|
||||
*/
|
||||
setTextContent: function (
|
||||
{
|
||||
messages,
|
||||
messagesClass,
|
||||
containerClass
|
||||
},
|
||||
extraButtons = []) {
|
||||
messagesClass = messagesClass || "default-tooltip-simple-text-colors";
|
||||
containerClass = containerClass || "default-tooltip-simple-text-colors";
|
||||
|
||||
let vbox = this.doc.createElement("vbox");
|
||||
vbox.className = "devtools-tooltip-simple-text-container " + containerClass;
|
||||
vbox.setAttribute("flex", "1");
|
||||
|
||||
for (let text of messages) {
|
||||
let description = this.doc.createElement("description");
|
||||
description.setAttribute("flex", "1");
|
||||
description.className = "devtools-tooltip-simple-text " + messagesClass;
|
||||
description.textContent = text;
|
||||
vbox.appendChild(description);
|
||||
}
|
||||
|
||||
for (let { label, className, command } of extraButtons) {
|
||||
let button = this.doc.createElement("button");
|
||||
button.className = className;
|
||||
button.setAttribute("label", label);
|
||||
button.addEventListener("command", command);
|
||||
vbox.appendChild(button);
|
||||
}
|
||||
|
||||
this.content = vbox;
|
||||
},
|
||||
|
||||
/**
|
||||
* Load a document into an iframe, and set the iframe
|
||||
* to be the tooltip's content.
|
||||
*
|
||||
* Used by tooltips that want to load their interface
|
||||
* into an iframe from a URL.
|
||||
*
|
||||
* @param {string} width
|
||||
* Width of the iframe.
|
||||
* @param {string} height
|
||||
* Height of the iframe.
|
||||
* @param {string} url
|
||||
* URL of the document to load into the iframe.
|
||||
*
|
||||
* @return {promise} A promise which is resolved with
|
||||
* the iframe.
|
||||
*
|
||||
* This function creates an iframe, loads the specified document
|
||||
* into it, sets the tooltip's content to the iframe, and returns
|
||||
* a promise.
|
||||
*
|
||||
* When the document is loaded, the function gets the content window
|
||||
* and resolves the promise with the content window.
|
||||
*/
|
||||
setIFrameContent: function ({width, height}, url) {
|
||||
let def = defer();
|
||||
|
||||
// Create an iframe
|
||||
let iframe = this.doc.createElementNS(XHTML_NS, "iframe");
|
||||
iframe.setAttribute("transparent", true);
|
||||
iframe.setAttribute("width", width);
|
||||
iframe.setAttribute("height", height);
|
||||
iframe.setAttribute("flex", "1");
|
||||
iframe.setAttribute("tooltip", "aHTMLTooltip");
|
||||
iframe.setAttribute("class", "devtools-tooltip-iframe");
|
||||
|
||||
// Wait for the load to initialize the widget
|
||||
function onLoad() {
|
||||
iframe.removeEventListener("load", onLoad, true);
|
||||
def.resolve(iframe);
|
||||
}
|
||||
iframe.addEventListener("load", onLoad, true);
|
||||
|
||||
// load the document from url into the iframe
|
||||
iframe.setAttribute("src", url);
|
||||
|
||||
// Put the iframe in the tooltip
|
||||
this.content = iframe;
|
||||
|
||||
return def.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create the tooltip panel
|
||||
*/
|
||||
_createPanel() {
|
||||
let panel = this.doc.createElement("panel");
|
||||
panel.setAttribute("hidden", true);
|
||||
panel.setAttribute("ignorekeys", true);
|
||||
panel.setAttribute("animate", false);
|
||||
|
||||
panel.setAttribute("consumeoutsideclicks",
|
||||
this.consumeOutsideClick);
|
||||
panel.setAttribute("noautofocus", this.noAutoFocus);
|
||||
panel.setAttribute("type", "arrow");
|
||||
panel.setAttribute("level", "top");
|
||||
|
||||
panel.setAttribute("class", "devtools-tooltip theme-tooltip-panel");
|
||||
this.doc.querySelector("window").appendChild(panel);
|
||||
|
||||
return panel;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Tooltip;
|
||||
182
devtools/client/shared/widgets/tooltip/TooltipToggle.js
Normal file
182
devtools/client/shared/widgets/tooltip/TooltipToggle.js
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
/* -*- 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {Task} = require("devtools/shared/task");
|
||||
|
||||
const DEFAULT_TOGGLE_DELAY = 50;
|
||||
|
||||
/**
|
||||
* Tooltip helper designed to show/hide the tooltip when the mouse hovers over
|
||||
* particular nodes.
|
||||
*
|
||||
* This works by tracking mouse movements on a base container node (baseNode)
|
||||
* and showing the tooltip when the mouse stops moving. A callback can be
|
||||
* provided to the start() method to know whether or not the node being
|
||||
* hovered over should indeed receive the tooltip.
|
||||
*/
|
||||
function TooltipToggle(tooltip) {
|
||||
this.tooltip = tooltip;
|
||||
this.win = tooltip.doc.defaultView;
|
||||
|
||||
this._onMouseMove = this._onMouseMove.bind(this);
|
||||
this._onMouseOut = this._onMouseOut.bind(this);
|
||||
|
||||
this._onTooltipMouseOver = this._onTooltipMouseOver.bind(this);
|
||||
this._onTooltipMouseOut = this._onTooltipMouseOut.bind(this);
|
||||
}
|
||||
|
||||
module.exports.TooltipToggle = TooltipToggle;
|
||||
|
||||
TooltipToggle.prototype = {
|
||||
/**
|
||||
* Start tracking mouse movements on the provided baseNode to show the
|
||||
* tooltip.
|
||||
*
|
||||
* 2 Ways to make this work:
|
||||
* - Provide a single node to attach the tooltip to, as the baseNode, and
|
||||
* omit the second targetNodeCb argument
|
||||
* - Provide a baseNode that is the container of possibly numerous children
|
||||
* elements that may receive a tooltip. In this case, provide the second
|
||||
* targetNodeCb argument to decide wether or not a child should receive
|
||||
* a tooltip.
|
||||
*
|
||||
* Note that if you call this function a second time, it will itself call
|
||||
* stop() before adding mouse tracking listeners again.
|
||||
*
|
||||
* @param {node} baseNode
|
||||
* The container for all target nodes
|
||||
* @param {Function} targetNodeCb
|
||||
* A function that accepts a node argument and that checks if a tooltip
|
||||
* should be displayed. Possible return values are:
|
||||
* - false (or a falsy value) if the tooltip should not be displayed
|
||||
* - true if the tooltip should be displayed
|
||||
* - a DOM node to display the tooltip on the returned anchor
|
||||
* The function can also return a promise that will resolve to one of
|
||||
* the values listed above.
|
||||
* If omitted, the tooltip will be shown everytime.
|
||||
* @param {Object} options
|
||||
Set of optional arguments:
|
||||
* - {Number} toggleDelay
|
||||
* An optional delay (in ms) that will be observed before showing
|
||||
* and before hiding the tooltip. Defaults to DEFAULT_TOGGLE_DELAY.
|
||||
* - {Boolean} interactive
|
||||
* If enabled, the tooltip is not hidden when mouse leaves the
|
||||
* target element and enters the tooltip. Allows the tooltip
|
||||
* content to be interactive.
|
||||
*/
|
||||
start: function (baseNode, targetNodeCb,
|
||||
{toggleDelay = DEFAULT_TOGGLE_DELAY, interactive = false} = {}) {
|
||||
this.stop();
|
||||
|
||||
if (!baseNode) {
|
||||
// Calling tool is in the process of being destroyed.
|
||||
return;
|
||||
}
|
||||
|
||||
this._baseNode = baseNode;
|
||||
this._targetNodeCb = targetNodeCb || (() => true);
|
||||
this._toggleDelay = toggleDelay;
|
||||
this._interactive = interactive;
|
||||
|
||||
baseNode.addEventListener("mousemove", this._onMouseMove);
|
||||
baseNode.addEventListener("mouseout", this._onMouseOut);
|
||||
|
||||
if (this._interactive) {
|
||||
this.tooltip.container.addEventListener("mouseover", this._onTooltipMouseOver);
|
||||
this.tooltip.container.addEventListener("mouseout", this._onTooltipMouseOut);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* If the start() function has been used previously, and you want to get rid
|
||||
* of this behavior, then call this function to remove the mouse movement
|
||||
* tracking
|
||||
*/
|
||||
stop: function () {
|
||||
this.win.clearTimeout(this.toggleTimer);
|
||||
|
||||
if (!this._baseNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._baseNode.removeEventListener("mousemove", this._onMouseMove);
|
||||
this._baseNode.removeEventListener("mouseout", this._onMouseOut);
|
||||
|
||||
if (this._interactive) {
|
||||
this.tooltip.container.removeEventListener("mouseover", this._onTooltipMouseOver);
|
||||
this.tooltip.container.removeEventListener("mouseout", this._onTooltipMouseOut);
|
||||
}
|
||||
|
||||
this._baseNode = null;
|
||||
this._targetNodeCb = null;
|
||||
this._lastHovered = null;
|
||||
},
|
||||
|
||||
_onMouseMove: function (event) {
|
||||
if (event.target !== this._lastHovered) {
|
||||
this._lastHovered = event.target;
|
||||
|
||||
this.win.clearTimeout(this.toggleTimer);
|
||||
this.toggleTimer = this.win.setTimeout(() => {
|
||||
this.tooltip.hide();
|
||||
this.isValidHoverTarget(event.target).then(target => {
|
||||
if (target === null) {
|
||||
return;
|
||||
}
|
||||
this.tooltip.show(target);
|
||||
}, reason => {
|
||||
console.error("isValidHoverTarget rejected with unexpected reason:");
|
||||
console.error(reason);
|
||||
});
|
||||
}, this._toggleDelay);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Is the given target DOMNode a valid node for toggling the tooltip on hover.
|
||||
* This delegates to the user-defined _targetNodeCb callback.
|
||||
* @return {Promise} a promise that will resolve the anchor to use for the
|
||||
* tooltip or null if no valid target was found.
|
||||
*/
|
||||
isValidHoverTarget: Task.async(function* (target) {
|
||||
let res = yield this._targetNodeCb(target, this.tooltip);
|
||||
if (res) {
|
||||
return res.nodeName ? res : target;
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
|
||||
_onMouseOut: function (event) {
|
||||
// Only hide the tooltip if the mouse leaves baseNode.
|
||||
if (event && this._baseNode && !this._baseNode.contains(event.relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._lastHovered = null;
|
||||
this.win.clearTimeout(this.toggleTimer);
|
||||
this.toggleTimer = this.win.setTimeout(() => {
|
||||
this.tooltip.hide();
|
||||
}, this._toggleDelay);
|
||||
},
|
||||
|
||||
_onTooltipMouseOver() {
|
||||
this.win.clearTimeout(this.toggleTimer);
|
||||
},
|
||||
|
||||
_onTooltipMouseOut() {
|
||||
this.win.clearTimeout(this.toggleTimer);
|
||||
this.toggleTimer = this.win.setTimeout(() => {
|
||||
this.tooltip.hide();
|
||||
}, this._toggleDelay);
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
this.stop();
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
/* -*- 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "VariablesView",
|
||||
"resource://devtools/client/shared/widgets/VariablesView.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "VariablesViewController",
|
||||
"resource://devtools/client/shared/widgets/VariablesViewController.jsm");
|
||||
|
||||
/**
|
||||
* Fill the tooltip with a variables view, inspecting an object via its
|
||||
* corresponding object actor, as specified in the remote debugging protocol.
|
||||
*
|
||||
* @param {Tooltip} tooltip
|
||||
* The tooltip to use
|
||||
* @param {object} objectActor
|
||||
* The value grip for the object actor.
|
||||
* @param {object} viewOptions [optional]
|
||||
* Options for the variables view visualization.
|
||||
* @param {object} controllerOptions [optional]
|
||||
* Options for the variables view controller.
|
||||
* @param {object} relayEvents [optional]
|
||||
* A collection of events to listen on the variables view widget.
|
||||
* For example, { fetched: () => ... }
|
||||
* @param {array} extraButtons [optional]
|
||||
* An array of extra buttons to add. Each element of the array
|
||||
* should be of the form {label, className, command}.
|
||||
* @param {Toolbox} toolbox [optional]
|
||||
* Pass the instance of the current toolbox if you want the variables
|
||||
* view widget to allow highlighting and selection of DOM nodes
|
||||
*/
|
||||
|
||||
function setTooltipVariableContent(tooltip, objectActor,
|
||||
viewOptions = {}, controllerOptions = {},
|
||||
relayEvents = {}, extraButtons = [],
|
||||
toolbox = null) {
|
||||
let doc = tooltip.doc;
|
||||
let vbox = doc.createElement("vbox");
|
||||
vbox.className = "devtools-tooltip-variables-view-box";
|
||||
vbox.setAttribute("flex", "1");
|
||||
|
||||
let innerbox = doc.createElement("vbox");
|
||||
innerbox.className = "devtools-tooltip-variables-view-innerbox";
|
||||
innerbox.setAttribute("flex", "1");
|
||||
vbox.appendChild(innerbox);
|
||||
|
||||
for (let { label, className, command } of extraButtons) {
|
||||
let button = doc.createElement("button");
|
||||
button.className = className;
|
||||
button.setAttribute("label", label);
|
||||
button.addEventListener("command", command);
|
||||
vbox.appendChild(button);
|
||||
}
|
||||
|
||||
let widget = new VariablesView(innerbox, viewOptions);
|
||||
|
||||
// If a toolbox was provided, link it to the vview
|
||||
if (toolbox) {
|
||||
widget.toolbox = toolbox;
|
||||
}
|
||||
|
||||
// Analyzing state history isn't useful with transient object inspectors.
|
||||
widget.commitHierarchy = () => {};
|
||||
|
||||
for (let e in relayEvents) {
|
||||
widget.on(e, relayEvents[e]);
|
||||
}
|
||||
VariablesViewController.attach(widget, controllerOptions);
|
||||
|
||||
// Some of the view options are allowed to change between uses.
|
||||
widget.searchPlaceholder = viewOptions.searchPlaceholder;
|
||||
widget.searchEnabled = viewOptions.searchEnabled;
|
||||
|
||||
// Use the object actor's grip to display it as a variable in the widget.
|
||||
// The controller options are allowed to change between uses.
|
||||
widget.controller.setSingleVariable(
|
||||
{ objectActor: objectActor }, controllerOptions);
|
||||
|
||||
tooltip.content = vbox;
|
||||
tooltip.panel.setAttribute("clamped-dimensions", "");
|
||||
}
|
||||
|
||||
exports.setTooltipVariableContent = setTooltipVariableContent;
|
||||
19
devtools/client/shared/widgets/tooltip/moz.build
Normal file
19
devtools/client/shared/widgets/tooltip/moz.build
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
DevToolsModules(
|
||||
'CssDocsTooltip.js',
|
||||
'EventTooltipHelper.js',
|
||||
'HTMLTooltip.js',
|
||||
'ImageTooltipHelper.js',
|
||||
'SwatchBasedEditorTooltip.js',
|
||||
'SwatchColorPickerTooltip.js',
|
||||
'SwatchCubicBezierTooltip.js',
|
||||
'SwatchFilterTooltip.js',
|
||||
'Tooltip.js',
|
||||
'TooltipToggle.js',
|
||||
'VariableContentHelper.js',
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue