element-container.js (8001B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const MarkupContainer = require("resource://devtools/client/inspector/markup/views/markup-container.js"); 8 const ElementEditor = require("resource://devtools/client/inspector/markup/views/element-editor.js"); 9 const { 10 ELEMENT_NODE, 11 } = require("resource://devtools/shared/dom-node-constants.js"); 12 13 loader.lazyRequireGetter( 14 this, 15 "EventTooltip", 16 "resource://devtools/client/shared/widgets/tooltip/EventTooltipHelper.js", 17 true 18 ); 19 loader.lazyRequireGetter( 20 this, 21 ["setImageTooltip", "setBrokenImageTooltip"], 22 "resource://devtools/client/shared/widgets/tooltip/ImageTooltipHelper.js", 23 true 24 ); 25 loader.lazyRequireGetter( 26 this, 27 "clipboardHelper", 28 "resource://devtools/shared/platform/clipboard.js" 29 ); 30 31 const PREVIEW_MAX_DIM_PREF = "devtools.inspector.imagePreviewTooltipSize"; 32 33 /** 34 * An implementation of MarkupContainer for Elements that can contain 35 * child nodes. 36 * Allows editing of tag name, attributes, expanding / collapsing. 37 */ 38 class MarkupElementContainer extends MarkupContainer { 39 /** 40 * @param {MarkupView} markupView 41 * The markup view that owns this container. 42 * @param {NodeFront} node 43 * The node to display. 44 */ 45 constructor(markupView, node) { 46 super(); 47 this.initialize(markupView, node, "elementcontainer"); 48 49 if (node.nodeType === ELEMENT_NODE) { 50 this.editor = new ElementEditor(this, node); 51 } else { 52 throw new Error("Invalid node for MarkupElementContainer"); 53 } 54 55 this.tagLine.appendChild(this.editor.elt); 56 } 57 onContainerClick(event) { 58 if (!event.target.hasAttribute("data-event")) { 59 return; 60 } 61 62 event.target.setAttribute("aria-pressed", "true"); 63 this._buildEventTooltipContent(event.target); 64 } 65 66 async _buildEventTooltipContent(target) { 67 const tooltip = this.markup.eventDetailsTooltip; 68 await tooltip.hide(); 69 70 const listenerInfo = await this.node.getEventListenerInfo(); 71 72 const toolbox = this.markup.toolbox; 73 74 // Create the EventTooltip which will populate the tooltip content. 75 const eventTooltip = new EventTooltip( 76 tooltip, 77 listenerInfo, 78 toolbox, 79 this.node 80 ); 81 82 // Add specific styling to the "event" badge when at least one event is disabled. 83 // The eventTooltip will take care of clearing the event listener when it's destroyed. 84 eventTooltip.on( 85 "event-tooltip-listener-toggled", 86 ({ hasDisabledEventListeners }) => { 87 const className = "has-disabled-events"; 88 if (hasDisabledEventListeners) { 89 this.editor._eventBadge.classList.add(className); 90 } else { 91 this.editor._eventBadge.classList.remove(className); 92 } 93 } 94 ); 95 96 // Disable the image preview tooltip while we display the event details 97 this.markup._disableImagePreviewTooltip(); 98 tooltip.once("hidden", () => { 99 eventTooltip.destroy(); 100 101 // Enable the image preview tooltip after closing the event details 102 this.markup._enableImagePreviewTooltip(); 103 104 // Allow clicks on the event badge to display the event popup again 105 // (but allow the currently queued click event to run first). 106 this.markup.win.setTimeout(() => { 107 if (this.editor._eventBadge) { 108 this.editor._eventBadge.style.pointerEvents = "auto"; 109 this.editor._eventBadge.setAttribute("aria-pressed", "false"); 110 } 111 }, 0); 112 }); 113 114 // Prevent clicks on the event badge to display the event popup again. 115 if (this.editor._eventBadge) { 116 this.editor._eventBadge.style.pointerEvents = "none"; 117 } 118 tooltip.show(target); 119 tooltip.focus(); 120 } 121 122 /** 123 * Generates the an image preview for this Element. The element must be an 124 * image or canvas (@see isPreviewable). 125 * 126 * @return {Promise} that is resolved with an object of form 127 * { data, size: { naturalWidth, naturalHeight, resizeRatio } } where 128 * - data is the data-uri for the image preview. 129 * - size contains information about the original image size and if 130 * the preview has been resized. 131 * 132 * If this element is not previewable or the preview cannot be generated for 133 * some reason, the Promise is rejected. 134 */ 135 _getPreview() { 136 if (!this.isPreviewable()) { 137 return Promise.reject("_getPreview called on a non-previewable element."); 138 } 139 140 if (this.tooltipDataPromise) { 141 // A preview request is already pending. Re-use that request. 142 return this.tooltipDataPromise; 143 } 144 145 // Fetch the preview from the server. 146 this.tooltipDataPromise = async function () { 147 const maxDim = Services.prefs.getIntPref(PREVIEW_MAX_DIM_PREF); 148 const preview = await this.node.getImageData(maxDim); 149 const data = await preview.data.string(); 150 151 // Clear the pending preview request. We can't reuse the results later as 152 // the preview contents might have changed. 153 this.tooltipDataPromise = null; 154 return { data, size: preview.size }; 155 }.bind(this)(); 156 157 return this.tooltipDataPromise; 158 } 159 160 /** 161 * Executed by MarkupView._isImagePreviewTarget which is itself called when 162 * the mouse hovers over a target in the markup-view. 163 * Checks if the target is indeed something we want to have an image tooltip 164 * preview over and, if so, inserts content into the tooltip. 165 * 166 * @return {Promise} that resolves when the tooltip content is ready. Resolves 167 * true if the tooltip should be displayed, false otherwise. 168 */ 169 async isImagePreviewTarget(target, tooltip) { 170 // Is this Element previewable. 171 if (!this.isPreviewable()) { 172 return false; 173 } 174 175 // If the Element has an src attribute, the tooltip is shown when hovering 176 // over the src url. If not, the tooltip is shown when hovering over the tag 177 // name. 178 const src = this.editor.getAttributeElement("src"); 179 const expectedTarget = src ? src.querySelector(".link") : this.editor.tag; 180 if (target !== expectedTarget) { 181 return false; 182 } 183 184 try { 185 const { data, size } = await this._getPreview(); 186 // The preview is ready. 187 const options = { 188 naturalWidth: size.naturalWidth, 189 naturalHeight: size.naturalHeight, 190 maxDim: Services.prefs.getIntPref(PREVIEW_MAX_DIM_PREF), 191 }; 192 193 setImageTooltip(tooltip, this.markup.doc, data, options); 194 } catch (e) { 195 // Indicate the failure but show the tooltip anyway. 196 setBrokenImageTooltip(tooltip, this.markup.doc); 197 } 198 return true; 199 } 200 201 copyImageDataUri() { 202 // We need to send again a request to gettooltipData even if one was sent 203 // for the tooltip, because we want the full-size image 204 this.node.getImageData().then(data => { 205 data.data.string().then(str => { 206 clipboardHelper.copyString(str); 207 }); 208 }); 209 } 210 211 setInlineTextChild(inlineTextChild) { 212 this.inlineTextChild = inlineTextChild; 213 this.editor.updateTextEditor(); 214 } 215 216 clearInlineTextChild() { 217 this.inlineTextChild = undefined; 218 this.editor.updateTextEditor(); 219 } 220 221 /** 222 * Trigger new attribute field for input. 223 */ 224 addAttribute() { 225 this.editor.newAttr.editMode(); 226 } 227 228 /** 229 * Trigger attribute field for editing. 230 */ 231 editAttribute(attrName) { 232 this.editor.attrElements.get(attrName).editMode(); 233 } 234 235 /** 236 * Remove attribute from container. 237 * This is an undoable action. 238 */ 239 removeAttribute(attrName) { 240 const doMods = this.editor._startModifyingAttributes(); 241 const undoMods = this.editor._startModifyingAttributes(); 242 this.editor._saveAttribute(attrName, undoMods); 243 doMods.removeAttribute(attrName); 244 this.undo.do( 245 () => { 246 doMods.apply(); 247 }, 248 () => { 249 undoMods.apply(); 250 } 251 ); 252 } 253 } 254 255 module.exports = MarkupElementContainer;