/* 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 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;