diff options
Diffstat (limited to 'devtools/client/inspector/markup/views/element-container.js')
-rw-r--r-- | devtools/client/inspector/markup/views/element-container.js | 260 |
1 files changed, 260 insertions, 0 deletions
diff --git a/devtools/client/inspector/markup/views/element-container.js b/devtools/client/inspector/markup/views/element-container.js new file mode 100644 index 0000000000..c47edccef1 --- /dev/null +++ b/devtools/client/inspector/markup/views/element-container.js @@ -0,0 +1,260 @@ +/* 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; |