Bug 1471460 - Adjust the styles of the active markup display badges. r=miker
[gecko.git] / devtools / client / inspector / markup / views / element-editor.js
blob264ed3ffe1928350d1fd4cd715d01eed29e2d768
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/. */
5 "use strict";
7 const Services = require("Services");
8 const TextEditor = require("devtools/client/inspector/markup/views/text-editor");
9 const {
10   getAutocompleteMaxWidth,
11   flashElementOn,
12   flashElementOff,
13   parseAttributeValues,
14 } = require("devtools/client/inspector/markup/utils");
15 const { truncateString } = require("devtools/shared/inspector/utils");
16 const {editableField, InplaceEditor} =
17       require("devtools/client/shared/inplace-editor");
18 const {parseAttribute} =
19       require("devtools/client/shared/node-attribute-parser");
20 const {getCssProperties} = require("devtools/shared/fronts/css-properties");
22 // Global tooltip inspector
23 const {LocalizationHelper} = require("devtools/shared/l10n");
24 const INSPECTOR_L10N =
25   new LocalizationHelper("devtools/client/locales/inspector.properties");
27 // Page size for pageup/pagedown
28 const COLLAPSE_DATA_URL_REGEX = /^data.+base64/;
29 const COLLAPSE_DATA_URL_LENGTH = 60;
31 // Contains only void (without end tag) HTML elements
32 const HTML_VOID_ELEMENTS = [
33   "area", "base", "br", "col", "command", "embed",
34   "hr", "img", "input", "keygen", "link", "meta", "param", "source",
35   "track", "wbr"
38 // Contains only valid computed display property types of the node to display in the
39 // element markup and their respective title tooltip text.
40 const DISPLAY_TYPES = {
41   "flex": INSPECTOR_L10N.getStr("markupView.display.flex.tooltiptext"),
42   "inline-flex": INSPECTOR_L10N.getStr("markupView.display.flex.tooltiptext"),
43   "grid": INSPECTOR_L10N.getStr("markupView.display.grid.tooltiptext"),
44   "inline-grid": INSPECTOR_L10N.getStr("markupView.display.inlineGrid.tooltiptext"),
45   "subgrid": INSPECTOR_L10N.getStr("markupView.display.subgrid.tooltiptiptext"),
46   "flow-root": INSPECTOR_L10N.getStr("markupView.display.flowRoot.tooltiptext"),
47   "contents": INSPECTOR_L10N.getStr("markupView.display.contents.tooltiptext2"),
50 /**
51  * Creates an editor for an Element node.
52  *
53  * @param  {MarkupContainer} container
54  *         The container owning this editor.
55  * @param  {Element} node
56  *         The node being edited.
57  */
58 function ElementEditor(container, node) {
59   this.container = container;
60   this.node = node;
61   this.markup = this.container.markup;
62   this.doc = this.markup.doc;
63   this.inspector = this.markup.inspector;
64   this.highlighters = this.markup.highlighters;
65   this._cssProperties = getCssProperties(this.markup.toolbox);
67   this.attrElements = new Map();
68   this.animationTimers = {};
70   this.elt = null;
71   this.tag = null;
72   this.closeTag = null;
73   this.attrList = null;
74   this.newAttr = null;
75   this.closeElt = null;
77   this.onDisplayBadgeClick = this.onDisplayBadgeClick.bind(this);
78   this.onCustomBadgeClick = this.onCustomBadgeClick.bind(this);
79   this.onTagEdit = this.onTagEdit.bind(this);
81   // Create the main editor
82   this.buildMarkup();
84   // Make the tag name editable (unless this is a remote node or
85   // a document element)
86   if (!node.isDocumentElement) {
87     // Make the tag optionally tabbable but not by default.
88     this.tag.setAttribute("tabindex", "-1");
89     editableField({
90       element: this.tag,
91       multiline: true,
92       maxWidth: () => getAutocompleteMaxWidth(this.tag, this.container.elt),
93       trigger: "dblclick",
94       stopOnReturn: true,
95       done: this.onTagEdit,
96       cssProperties: this._cssProperties
97     });
98   }
100   // Make the new attribute space editable.
101   this.newAttr.editMode = editableField({
102     element: this.newAttr,
103     multiline: true,
104     maxWidth: () => getAutocompleteMaxWidth(this.newAttr, this.container.elt),
105     trigger: "dblclick",
106     stopOnReturn: true,
107     contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
108     popup: this.markup.popup,
109     done: (val, commit) => {
110       if (!commit) {
111         return;
112       }
114       const doMods = this._startModifyingAttributes();
115       const undoMods = this._startModifyingAttributes();
116       this._applyAttributes(val, null, doMods, undoMods);
117       this.container.undo.do(() => {
118         doMods.apply();
119       }, function() {
120         undoMods.apply();
121       });
122     },
123     cssProperties: this._cssProperties
124   });
126   const displayName = this.node.displayName;
127   this.tag.textContent = displayName;
128   this.closeTag.textContent = displayName;
130   const isVoidElement = HTML_VOID_ELEMENTS.includes(displayName);
131   if (node.isInHTMLDocument && isVoidElement) {
132     this.elt.classList.add("void-element");
133   }
135   this.update();
136   this.initialized = true;
139 ElementEditor.prototype = {
140   buildMarkup: function() {
141     this.elt = this.doc.createElement("span");
142     this.elt.classList.add("editor");
144     const open = this.doc.createElement("span");
145     open.classList.add("open");
146     open.appendChild(this.doc.createTextNode("<"));
147     this.elt.appendChild(open);
149     this.tag = this.doc.createElement("span");
150     this.tag.classList.add("tag", "theme-fg-color3");
151     this.tag.setAttribute("tabindex", "-1");
152     open.appendChild(this.tag);
154     this.attrList = this.doc.createElement("span");
155     open.appendChild(this.attrList);
157     this.newAttr = this.doc.createElement("span");
158     this.newAttr.classList.add("newattr");
159     this.newAttr.setAttribute("tabindex", "-1");
160     this.newAttr.setAttribute("aria-label",
161       INSPECTOR_L10N.getStr("markupView.newAttribute.label"));
162     open.appendChild(this.newAttr);
164     const closingBracket = this.doc.createElement("span");
165     closingBracket.classList.add("closing-bracket");
166     closingBracket.textContent = ">";
167     open.appendChild(closingBracket);
169     const close = this.doc.createElement("span");
170     close.classList.add("close");
171     close.appendChild(this.doc.createTextNode("</"));
172     this.elt.appendChild(close);
174     this.closeTag = this.doc.createElement("span");
175     this.closeTag.classList.add("tag", "theme-fg-color3");
176     close.appendChild(this.closeTag);
178     close.appendChild(this.doc.createTextNode(">"));
179   },
181   set selected(value) {
182     if (this.textEditor) {
183       this.textEditor.selected = value;
184     }
185   },
187   flashAttribute: function(attrName) {
188     if (this.animationTimers[attrName]) {
189       clearTimeout(this.animationTimers[attrName]);
190     }
192     flashElementOn(this.getAttributeElement(attrName));
194     this.animationTimers[attrName] = setTimeout(() => {
195       flashElementOff(this.getAttributeElement(attrName));
196     }, this.markup.CONTAINER_FLASHING_DURATION);
197   },
199   /**
200    * Returns information about node in the editor.
201    *
202    * @param  {DOMNode} node
203    *         The node to get information from.
204    * @return {Object} An object literal with the following information:
205    *         {type: "attribute", name: "rel", value: "index", el: node}
206    */
207   getInfoAtNode: function(node) {
208     if (!node) {
209       return null;
210     }
212     let type = null;
213     let name = null;
214     let value = null;
216     // Attribute
217     const attribute = node.closest(".attreditor");
218     if (attribute) {
219       type = "attribute";
220       name = attribute.dataset.attr;
221       value = attribute.dataset.value;
222     }
224     return {type, name, value, el: node};
225   },
227   /**
228    * Update the state of the editor from the node.
229    */
230   update: function() {
231     const nodeAttributes = this.node.attributes || [];
233     // Keep the data model in sync with attributes on the node.
234     const currentAttributes = new Set(nodeAttributes.map(a => a.name));
235     for (const name of this.attrElements.keys()) {
236       if (!currentAttributes.has(name)) {
237         this.removeAttribute(name);
238       }
239     }
241     // Only loop through the current attributes on the node.  Missing
242     // attributes have already been removed at this point.
243     for (const attr of nodeAttributes) {
244       const el = this.attrElements.get(attr.name);
245       const valueChanged = el &&
246         el.dataset.value !== attr.value;
247       const isEditing = el && el.querySelector(".editable").inplaceEditor;
248       const canSimplyShowEditor = el && (!valueChanged || isEditing);
250       if (canSimplyShowEditor) {
251         // Element already exists and doesn't need to be recreated.
252         // Just show it (it's hidden by default).
253         el.style.removeProperty("display");
254       } else {
255         // Create a new editor, because the value of an existing attribute
256         // has changed.
257         const attribute = this._createAttribute(attr, el);
258         attribute.style.removeProperty("display");
260         // Temporarily flash the attribute to highlight the change.
261         // But not if this is the first time the editor instance has
262         // been created.
263         if (this.initialized) {
264           this.flashAttribute(attr.name);
265         }
266       }
267     }
269     this.updateEventBadge();
270     this.updateDisplayBadge();
271     this.updateCustomBadge();
272     this.updateTextEditor();
273   },
275   updateEventBadge: function() {
276     const showEventBadge = this.node.hasEventListeners;
277     if (this._eventBadge && !showEventBadge) {
278       this._eventBadge.remove();
279     } else if (showEventBadge && !this._eventBadge) {
280       this._createEventBadge();
281     }
282   },
284   _createEventBadge: function() {
285     this._eventBadge = this.doc.createElement("div");
286     this._eventBadge.classList.add("markup-badge");
287     this._eventBadge.dataset.event = "true";
288     this._eventBadge.textContent = "event";
289     this._eventBadge.title = INSPECTOR_L10N.getStr("markupView.event.tooltiptext");
290     // Badges order is [event][display][custom], insert event badge before others.
291     this.elt.insertBefore(this._eventBadge, this._displayBadge || this._customBadge);
292   },
294   /**
295    * Update the markup display badge.
296    */
297   updateDisplayBadge: function() {
298     const showDisplayBadge = this.node.displayType in DISPLAY_TYPES;
299     if (this._displayBadge && !showDisplayBadge) {
300       this._displayBadge.remove();
301     } else if (showDisplayBadge) {
302       if (!this._displayBadge) {
303         this._createDisplayBadge();
304       }
305       this._updateDisplayBadgeContent();
306     }
307   },
309   _createDisplayBadge: function() {
310     this._displayBadge = this.doc.createElement("div");
311     this._displayBadge.classList.add("markup-badge");
312     this._displayBadge.addEventListener("click", this.onDisplayBadgeClick);
313     // Badges order is [event][display][custom], insert display badge before custom.
314     this.elt.insertBefore(this._displayBadge, this._customBadge);
315   },
317   _updateDisplayBadgeContent: function() {
318     this._displayBadge.textContent = this.node.displayType;
319     this._displayBadge.dataset.display = this.node.displayType;
320     this._displayBadge.title = DISPLAY_TYPES[this.node.displayType];
321     this._displayBadge.classList.toggle("active",
322       this.highlighters.flexboxHighlighterShown === this.node ||
323       this.highlighters.gridHighlighterShown === this.node);
324     this._displayBadge.classList.toggle("interactive",
325       Services.prefs.getBoolPref("devtools.inspector.flexboxHighlighter.enabled") &&
326       (this.node.displayType === "flex" || this.node.displayType === "inline-flex"));
327   },
329   /**
330    * Update the markup custom element badge.
331    */
332   updateCustomBadge: function() {
333     const showCustomBadge = !!this.node.customElementLocation;
334     if (this._customBadge && !showCustomBadge) {
335       this._customBadge.remove();
336     } else if (!this._customBadge && showCustomBadge) {
337       this._createCustomBadge();
338     }
339   },
341   _createCustomBadge: function() {
342     this._customBadge = this.doc.createElement("div");
343     this._customBadge.classList.add("markup-badge");
344     this._customBadge.dataset.custom = "true";
345     this._customBadge.textContent = "custom…";
346     this._customBadge.title = INSPECTOR_L10N.getStr("markupView.custom.tooltiptext");
347     this._customBadge.addEventListener("click", this.onCustomBadgeClick);
348     // Badges order is [event][display][custom], insert custom badge at the end.
349     this.elt.appendChild(this._customBadge);
350   },
352   /**
353    * Update the inline text editor in case of a single text child node.
354    */
355   updateTextEditor: function() {
356     const node = this.node.inlineTextChild;
358     if (this.textEditor && this.textEditor.node != node) {
359       this.elt.removeChild(this.textEditor.elt);
360       this.textEditor = null;
361     }
363     if (node && !this.textEditor) {
364       // Create a text editor added to this editor.
365       // This editor won't receive an update automatically, so we rely on
366       // child text editors to let us know that we need updating.
367       this.textEditor = new TextEditor(this.container, node, "text");
368       this.elt.insertBefore(this.textEditor.elt, this.elt.querySelector(".close"));
369     }
371     if (this.textEditor) {
372       this.textEditor.update();
373     }
374   },
376   _startModifyingAttributes: function() {
377     return this.node.startModifyingAttributes();
378   },
380   /**
381    * Get the element used for one of the attributes of this element.
382    *
383    * @param  {String} attrName
384    *         The name of the attribute to get the element for
385    * @return {DOMNode}
386    */
387   getAttributeElement: function(attrName) {
388     return this.attrList.querySelector(
389       ".attreditor[data-attr=" + CSS.escape(attrName) + "] .attr-value");
390   },
392   /**
393    * Remove an attribute from the attrElements object and the DOM.
394    *
395    * @param  {String} attrName
396    *         The name of the attribute to remove
397    */
398   removeAttribute: function(attrName) {
399     const attr = this.attrElements.get(attrName);
400     if (attr) {
401       this.attrElements.delete(attrName);
402       attr.remove();
403     }
404   },
406   _createAttribute: function(attribute, before = null) {
407     const attr = this.doc.createElement("span");
408     attr.dataset.attr = attribute.name;
409     attr.dataset.value = attribute.value;
410     attr.classList.add("attreditor");
411     attr.style.display = "none";
413     attr.appendChild(this.doc.createTextNode(" "));
415     const inner = this.doc.createElement("span");
416     inner.classList.add("editable");
417     inner.setAttribute("tabindex", this.container.canFocus ? "0" : "-1");
418     attr.appendChild(inner);
420     const name = this.doc.createElement("span");
421     name.classList.add("attr-name");
422     name.classList.add("theme-fg-color2");
423     inner.appendChild(name);
425     inner.appendChild(this.doc.createTextNode('="'));
427     const val = this.doc.createElement("span");
428     val.classList.add("attr-value");
429     val.classList.add("theme-fg-color4");
430     inner.appendChild(val);
432     inner.appendChild(this.doc.createTextNode('"'));
434     // Double quotes need to be handled specially to prevent DOMParser failing.
435     // name="v"a"l"u"e" when editing -> name='v"a"l"u"e"'
436     // name="v'a"l'u"e" when editing -> name="v'a&quot;l'u&quot;e"
437     let editValueDisplayed = attribute.value || "";
438     const hasDoubleQuote = editValueDisplayed.includes('"');
439     const hasSingleQuote = editValueDisplayed.includes("'");
440     let initial = attribute.name + '="' + editValueDisplayed + '"';
442     // Can't just wrap value with ' since the value contains both " and '.
443     if (hasDoubleQuote && hasSingleQuote) {
444       editValueDisplayed = editValueDisplayed.replace(/\"/g, "&quot;");
445       initial = attribute.name + '="' + editValueDisplayed + '"';
446     }
448     // Wrap with ' since there are no single quotes in the attribute value.
449     if (hasDoubleQuote && !hasSingleQuote) {
450       initial = attribute.name + "='" + editValueDisplayed + "'";
451     }
453     // Make the attribute editable.
454     attr.editMode = editableField({
455       element: inner,
456       trigger: "dblclick",
457       stopOnReturn: true,
458       selectAll: false,
459       initial: initial,
460       multiline: true,
461       maxWidth: () => getAutocompleteMaxWidth(inner, this.container.elt),
462       contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
463       popup: this.markup.popup,
464       start: (editor, event) => {
465         // If the editing was started inside the name or value areas,
466         // select accordingly.
467         if (event && event.target === name) {
468           editor.input.setSelectionRange(0, name.textContent.length);
469         } else if (event && event.target.closest(".attr-value") === val) {
470           const length = editValueDisplayed.length;
471           const editorLength = editor.input.value.length;
472           const start = editorLength - (length + 1);
473           editor.input.setSelectionRange(start, start + length);
474         } else {
475           editor.input.select();
476         }
477       },
478       done: (newValue, commit, direction) => {
479         if (!commit || newValue === initial) {
480           return;
481         }
483         const doMods = this._startModifyingAttributes();
484         const undoMods = this._startModifyingAttributes();
486         // Remove the attribute stored in this editor and re-add any attributes
487         // parsed out of the input element. Restore original attribute if
488         // parsing fails.
489         this.refocusOnEdit(attribute.name, attr, direction);
490         this._saveAttribute(attribute.name, undoMods);
491         doMods.removeAttribute(attribute.name);
492         this._applyAttributes(newValue, attr, doMods, undoMods);
493         this.container.undo.do(() => {
494           doMods.apply();
495         }, () => {
496           undoMods.apply();
497         });
498       },
499       cssProperties: this._cssProperties
500     });
502     // Figure out where we should place the attribute.
503     if (attribute.name == "id") {
504       before = this.attrList.firstChild;
505     } else if (attribute.name == "class") {
506       const idNode = this.attrElements.get("id");
507       before = idNode ? idNode.nextSibling : this.attrList.firstChild;
508     }
509     this.attrList.insertBefore(attr, before);
511     this.removeAttribute(attribute.name);
512     this.attrElements.set(attribute.name, attr);
514     // Parse the attribute value to detect whether there are linkable parts in
515     // it (make sure to pass a complete list of existing attributes to the
516     // parseAttribute function, by concatenating attribute, because this could
517     // be a newly added attribute not yet on this.node).
518     const attributes = this.node.attributes.filter(existingAttribute => {
519       return existingAttribute.name !== attribute.name;
520     });
521     attributes.push(attribute);
522     const parsedLinksData = parseAttribute(this.node.namespaceURI,
523       this.node.tagName, attributes, attribute.name);
525     // Create links in the attribute value, and collapse long attributes if
526     // needed.
527     const collapse = value => {
528       if (value && value.match(COLLAPSE_DATA_URL_REGEX)) {
529         return truncateString(value, COLLAPSE_DATA_URL_LENGTH);
530       }
531       return this.markup.collapseAttributes
532         ? truncateString(value, this.markup.collapseAttributeLength)
533         : value;
534     };
536     val.innerHTML = "";
537     for (const token of parsedLinksData) {
538       if (token.type === "string") {
539         val.appendChild(this.doc.createTextNode(collapse(token.value)));
540       } else {
541         const link = this.doc.createElement("span");
542         link.classList.add("link");
543         link.setAttribute("data-type", token.type);
544         link.setAttribute("data-link", token.value);
545         link.textContent = collapse(token.value);
546         val.appendChild(link);
547       }
548     }
550     name.textContent = attribute.name;
552     return attr;
553   },
555   /**
556    * Parse a user-entered attribute string and apply the resulting
557    * attributes to the node. This operation is undoable.
558    *
559    * @param  {String} value
560    *         The user-entered value.
561    * @param  {DOMNode} attrNode
562    *         The attribute editor that created this
563    *         set of attributes, used to place new attributes where the
564    *         user put them.
565    */
566   _applyAttributes: function(value, attrNode, doMods, undoMods) {
567     const attrs = parseAttributeValues(value, this.doc);
568     for (const attr of attrs) {
569       // Create an attribute editor next to the current attribute if needed.
570       this._createAttribute(attr, attrNode ? attrNode.nextSibling : null);
571       this._saveAttribute(attr.name, undoMods);
572       doMods.setAttribute(attr.name, attr.value);
573     }
574   },
576   /**
577    * Saves the current state of the given attribute into an attribute
578    * modification list.
579    */
580   _saveAttribute: function(name, undoMods) {
581     const node = this.node;
582     if (node.hasAttribute(name)) {
583       const oldValue = node.getAttribute(name);
584       undoMods.setAttribute(name, oldValue);
585     } else {
586       undoMods.removeAttribute(name);
587     }
588   },
590   /**
591    * Listen to mutations, and when the attribute list is regenerated
592    * try to focus on the attribute after the one that's being edited now.
593    * If the attribute order changes, go to the beginning of the attribute list.
594    */
595   refocusOnEdit: function(attrName, attrNode, direction) {
596     // Only allow one refocus on attribute change at a time, so when there's
597     // more than 1 request in parallel, the last one wins.
598     if (this._editedAttributeObserver) {
599       this.markup.inspector.off("markupmutation", this._editedAttributeObserver);
600       this._editedAttributeObserver = null;
601     }
603     const activeElement = this.markup.doc.activeElement;
604     if (!activeElement || !activeElement.inplaceEditor) {
605       // The focus was already removed from the current inplace editor, we should not
606       // refocus the editable attribute.
607       return;
608     }
610     const container = this.markup.getContainer(this.node);
612     const activeAttrs = [...this.attrList.childNodes]
613       .filter(el => el.style.display != "none");
614     const attributeIndex = activeAttrs.indexOf(attrNode);
616     const onMutations = this._editedAttributeObserver = mutations => {
617       let isDeletedAttribute = false;
618       let isNewAttribute = false;
620       for (const mutation of mutations) {
621         const inContainer =
622           this.markup.getContainer(mutation.target) === container;
623         if (!inContainer) {
624           continue;
625         }
627         const isOriginalAttribute = mutation.attributeName === attrName;
629         isDeletedAttribute = isDeletedAttribute || isOriginalAttribute &&
630                              mutation.newValue === null;
631         isNewAttribute = isNewAttribute || mutation.attributeName !== attrName;
632       }
634       const isModifiedOrder = isDeletedAttribute && isNewAttribute;
635       this._editedAttributeObserver = null;
637       // "Deleted" attributes are merely hidden, so filter them out.
638       const visibleAttrs = [...this.attrList.childNodes]
639         .filter(el => el.style.display != "none");
640       let activeEditor;
641       if (visibleAttrs.length > 0) {
642         if (!direction) {
643           // No direction was given; stay on current attribute.
644           activeEditor = visibleAttrs[attributeIndex];
645         } else if (isModifiedOrder) {
646           // The attribute was renamed, reordering the existing attributes.
647           // So let's go to the beginning of the attribute list for consistency.
648           activeEditor = visibleAttrs[0];
649         } else {
650           let newAttributeIndex;
651           if (isDeletedAttribute) {
652             newAttributeIndex = attributeIndex;
653           } else if (direction == Services.focus.MOVEFOCUS_FORWARD) {
654             newAttributeIndex = attributeIndex + 1;
655           } else if (direction == Services.focus.MOVEFOCUS_BACKWARD) {
656             newAttributeIndex = attributeIndex - 1;
657           }
659           // The number of attributes changed (deleted), or we moved through
660           // the array so check we're still within bounds.
661           if (newAttributeIndex >= 0 &&
662               newAttributeIndex <= visibleAttrs.length - 1) {
663             activeEditor = visibleAttrs[newAttributeIndex];
664           }
665         }
666       }
668       // Either we have no attributes left,
669       // or we just edited the last attribute and want to move on.
670       if (!activeEditor) {
671         activeEditor = this.newAttr;
672       }
674       // Refocus was triggered by tab or shift-tab.
675       // Continue in edit mode.
676       if (direction) {
677         activeEditor.editMode();
678       } else {
679         // Refocus was triggered by enter.
680         // Exit edit mode (but restore focus).
681         const editable = activeEditor === this.newAttr ?
682           activeEditor : activeEditor.querySelector(".editable");
683         editable.focus();
684       }
686       this.markup.emit("refocusedonedit");
687     };
689     // Start listening for mutations until we find an attributes change
690     // that modifies this attribute.
691     this.markup.inspector.once("markupmutation", onMutations);
692   },
694   /**
695    * Called when the display badge is clicked. Toggles on the grid highlighter for the
696    * selected node if it is a grid container.
697    */
698   onDisplayBadgeClick: function(event) {
699     event.stopPropagation();
701     const target = event.target;
703     if (Services.prefs.getBoolPref("devtools.inspector.flexboxHighlighter.enabled") &&
704         (target.dataset.display === "flex" || target.dataset.display === "inline-flex")) {
705       this._displayBadge.classList.add("active");
706       this.highlighters.toggleFlexboxHighlighter(this.inspector.selection.nodeFront,
707         "markup");
708     }
710     if (target.dataset.display === "grid" || target.dataset.display === "inline-grid") {
711       this._displayBadge.classList.add("active");
712       this.highlighters.toggleGridHighlighter(this.inspector.selection.nodeFront,
713         "markup");
714     }
715   },
717   onCustomBadgeClick: function() {
718     const { url, line } = this.node.customElementLocation;
719     this.markup.toolbox.viewSourceInDebugger(url, line, "show_custom_element");
720   },
722   /**
723    * Called when the tag name editor has is done editing.
724    */
725   onTagEdit: function(newTagName, isCommit) {
726     if (!isCommit ||
727         newTagName.toLowerCase() === this.node.tagName.toLowerCase() ||
728         !("editTagName" in this.markup.walker)) {
729       return;
730     }
732     // Changing the tagName removes the node. Make sure the replacing node gets
733     // selected afterwards.
734     this.markup.reselectOnRemoved(this.node, "edittagname");
735     this.markup.walker.editTagName(this.node, newTagName).catch(() => {
736       // Failed to edit the tag name, cancel the reselection.
737       this.markup.cancelReselectOnRemoved();
738     });
739   },
741   destroy: function() {
742     if (this._displayBadge) {
743       this._displayBadge.removeEventListener("click", this.onDisplayBadgeClick);
744     }
745     if (this._customBadge) {
746       this._customBadge.removeEventListener("click", this.onCustomBadgeClick);
747     }
749     for (const key in this.animationTimers) {
750       clearTimeout(this.animationTimers[key]);
751     }
752     this.animationTimers = null;
753   }
756 module.exports = ElementEditor;