Source code
Revision control
Copy as Markdown
Other Tools
/* 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
"use strict";
const MarkupContainer = require("resource://devtools/client/inspector/markup/views/markup-container.js");
const ElementEditor = require("resource://devtools/client/inspector/markup/views/element-editor.js");
const {
ELEMENT_NODE,
} = require("resource://devtools/shared/dom-node-constants.js");
const { extend } = require("resource://devtools/shared/extend.js");
loader.lazyRequireGetter(
this,
"EventTooltip",
"resource://devtools/client/shared/widgets/tooltip/EventTooltipHelper.js",
true
);
loader.lazyRequireGetter(
this,
["setImageTooltip", "setBrokenImageTooltip"],
"resource://devtools/client/shared/widgets/tooltip/ImageTooltipHelper.js",
true
);
loader.lazyRequireGetter(
this,
"clipboardHelper",
"resource://devtools/shared/platform/clipboard.js"
);
const PREVIEW_MAX_DIM_PREF = "devtools.inspector.imagePreviewTooltipSize";
/**
* An implementation of MarkupContainer for Elements that can contain
* child nodes.
* Allows editing of tag name, attributes, expanding / collapsing.
*
* @param {MarkupView} markupView
* The markup view that owns this container.
* @param {NodeFront} node
* The node to display.
*/
function MarkupElementContainer(markupView, node) {
MarkupContainer.prototype.initialize.call(
this,
markupView,
node,
"elementcontainer"
);
if (node.nodeType === ELEMENT_NODE) {
this.editor = new ElementEditor(this, node);
} else {
throw new Error("Invalid node for MarkupElementContainer");
}
this.tagLine.appendChild(this.editor.elt);
}
MarkupElementContainer.prototype = extend(MarkupContainer.prototype, {
onContainerClick(event) {
if (!event.target.hasAttribute("data-event")) {
return;
}
event.target.setAttribute("aria-pressed", "true");
this._buildEventTooltipContent(event.target);
},
async _buildEventTooltipContent(target) {
const tooltip = this.markup.eventDetailsTooltip;
await tooltip.hide();
const listenerInfo = await this.node.getEventListenerInfo();
const toolbox = this.markup.toolbox;
// Create the EventTooltip which will populate the tooltip content.
const eventTooltip = new EventTooltip(
tooltip,
listenerInfo,
toolbox,
this.node
);
// Add specific styling to the "event" badge when at least one event is disabled.
// The eventTooltip will take care of clearing the event listener when it's destroyed.
eventTooltip.on(
"event-tooltip-listener-toggled",
({ hasDisabledEventListeners }) => {
const className = "has-disabled-events";
if (hasDisabledEventListeners) {
this.editor._eventBadge.classList.add(className);
} else {
this.editor._eventBadge.classList.remove(className);
}
}
);
// Disable the image preview tooltip while we display the event details
this.markup._disableImagePreviewTooltip();
tooltip.once("hidden", () => {
eventTooltip.destroy();
// Enable the image preview tooltip after closing the event details
this.markup._enableImagePreviewTooltip();
// Allow clicks on the event badge to display the event popup again
// (but allow the currently queued click event to run first).
this.markup.win.setTimeout(() => {
if (this.editor._eventBadge) {
this.editor._eventBadge.style.pointerEvents = "auto";
this.editor._eventBadge.setAttribute("aria-pressed", "false");
}
}, 0);
});
// Prevent clicks on the event badge to display the event popup again.
if (this.editor._eventBadge) {
this.editor._eventBadge.style.pointerEvents = "none";
}
tooltip.show(target);
tooltip.focus();
},
/**
* Generates the an image preview for this Element. The element must be an
* image or canvas (@see isPreviewable).
*
* @return {Promise} that is resolved with an object of form
* { data, size: { naturalWidth, naturalHeight, resizeRatio } } where
* - data is the data-uri for the image preview.
* - size contains information about the original image size and if
* the preview has been resized.
*
* If this element is not previewable or the preview cannot be generated for
* some reason, the Promise is rejected.
*/
_getPreview() {
if (!this.isPreviewable()) {
return Promise.reject("_getPreview called on a non-previewable element.");
}
if (this.tooltipDataPromise) {
// A preview request is already pending. Re-use that request.
return this.tooltipDataPromise;
}
// Fetch the preview from the server.
this.tooltipDataPromise = async function () {
const maxDim = Services.prefs.getIntPref(PREVIEW_MAX_DIM_PREF);
const preview = await this.node.getImageData(maxDim);
const data = await preview.data.string();
// Clear the pending preview request. We can't reuse the results later as
// the preview contents might have changed.
this.tooltipDataPromise = null;
return { data, size: preview.size };
}.bind(this)();
return this.tooltipDataPromise;
},
/**
* Executed by MarkupView._isImagePreviewTarget which is itself called when
* the mouse hovers over a target in the markup-view.
* Checks if the target is indeed something we want to have an image tooltip
* preview over and, if so, inserts content into the tooltip.
*
* @return {Promise} that resolves when the tooltip content is ready. Resolves
* true if the tooltip should be displayed, false otherwise.
*/
async isImagePreviewTarget(target, tooltip) {
// Is this Element previewable.
if (!this.isPreviewable()) {
return false;
}
// If the Element has an src attribute, the tooltip is shown when hovering
// over the src url. If not, the tooltip is shown when hovering over the tag
// name.
const src = this.editor.getAttributeElement("src");
const expectedTarget = src ? src.querySelector(".link") : this.editor.tag;
if (target !== expectedTarget) {
return false;
}
try {
const { data, size } = await this._getPreview();
// The preview is ready.
const options = {
naturalWidth: size.naturalWidth,
naturalHeight: size.naturalHeight,
maxDim: Services.prefs.getIntPref(PREVIEW_MAX_DIM_PREF),
};
setImageTooltip(tooltip, this.markup.doc, data, options);
} catch (e) {
// Indicate the failure but show the tooltip anyway.
setBrokenImageTooltip(tooltip, this.markup.doc);
}
return true;
},
copyImageDataUri() {
// We need to send again a request to gettooltipData even if one was sent
// for the tooltip, because we want the full-size image
this.node.getImageData().then(data => {
data.data.string().then(str => {
clipboardHelper.copyString(str);
});
});
},
setInlineTextChild(inlineTextChild) {
this.inlineTextChild = inlineTextChild;
this.editor.updateTextEditor();
},
clearInlineTextChild() {
this.inlineTextChild = undefined;
this.editor.updateTextEditor();
},
/**
* Trigger new attribute field for input.
*/
addAttribute() {
this.editor.newAttr.editMode();
},
/**
* Trigger attribute field for editing.
*/
editAttribute(attrName) {
this.editor.attrElements.get(attrName).editMode();
},
/**
* Remove attribute from container.
* This is an undoable action.
*/
removeAttribute(attrName) {
const doMods = this.editor._startModifyingAttributes();
const undoMods = this.editor._startModifyingAttributes();
this.editor._saveAttribute(attrName, undoMods);
doMods.removeAttribute(attrName);
this.undo.do(
() => {
doMods.apply();
},
() => {
undoMods.apply();
}
);
},
});
module.exports = MarkupElementContainer;