html-editor.js (5782B)
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 Editor = require("resource://devtools/client/shared/sourceeditor/editor.js"); 8 const EventEmitter = require("resource://devtools/shared/event-emitter.js"); 9 10 /** 11 * A wrapper around the Editor component, that allows editing of HTML. 12 * 13 * The main functionality this provides around the Editor is the ability 14 * to show/hide/position an editor inplace. It only appends once to the 15 * body, and uses CSS to position the editor. The reason it is done this 16 * way is that the editor is loaded in an iframe, and calling appendChild 17 * causes it to reload. 18 * 19 * Meant to be embedded inside of an HTML page, as in markup.xhtml. 20 */ 21 class HTMLEditor extends EventEmitter { 22 /** 23 * @param {HTMLDocument} htmlDocument 24 * The document to attach the editor to. Will also use this 25 * document as a basis for listening resize events. 26 */ 27 constructor(htmlDocument) { 28 super(); 29 30 this.doc = htmlDocument; 31 this.#container = this.doc.createElement("div"); 32 this.#container.className = "html-editor theme-body"; 33 this.#container.style.display = "none"; 34 this.#editorInner = this.doc.createElement("div"); 35 this.#editorInner.className = "html-editor-inner"; 36 this.#container.appendChild(this.#editorInner); 37 this.doc.body.appendChild(this.#container); 38 this.doc.defaultView.addEventListener("resize", this.refresh, true); 39 40 const config = { 41 mode: Editor.modes.html, 42 lineWrapping: true, 43 styleActiveLine: false, 44 keyMap: [ 45 { key: HTMLEditor.#ctrl("Enter"), run: this.hide }, 46 { key: "F2", run: this.hide }, 47 { 48 key: "Escape", 49 run: this.hide.bind(this, false), 50 preventDefault: true, 51 }, 52 ], 53 theme: "mozilla markup-view", 54 cm6: true, 55 }; 56 57 this.#container.addEventListener("click", this.hide); 58 this.#editorInner.addEventListener("click", HTMLEditor.#stopPropagation); 59 // Avoid the hijack of the backspace key by the markup when the 60 // html editor is open. 61 this.#editorInner.addEventListener("keydown", HTMLEditor.#stopPropagation); 62 63 this.editor = new Editor(config); 64 this.editor.appendToLocalElement(this.#editorInner); 65 66 this.hide(false); 67 } 68 69 editor = null; 70 doc = null; 71 72 #container = null; 73 #editorInner = null; 74 #attachedElement = null; 75 #originalValue; 76 77 static #ctrl(k) { 78 return (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") + k; 79 } 80 81 static #stopPropagation(e) { 82 e.stopPropagation(); 83 } 84 85 /** 86 * Need to refresh position by manually setting CSS values, so this will 87 * need to be called on resizes and other sizing changes. 88 */ 89 refresh = () => { 90 const element = this.#attachedElement; 91 92 if (element) { 93 this.#container.style.top = element.offsetTop + "px"; 94 this.#container.style.left = element.offsetLeft + "px"; 95 this.#container.style.width = element.offsetWidth + "px"; 96 this.#container.style.height = element.parentNode.offsetHeight + "px"; 97 } 98 }; 99 100 /** 101 * Anchor the editor to a particular element. 102 * 103 * @param {DOMNode} element 104 * The element that the editor will be anchored to. 105 * Should belong to the HTMLDocument passed into the constructor. 106 */ 107 #attach(element) { 108 this.#detach(); 109 this.#attachedElement = element; 110 element.classList.add("html-editor-container"); 111 this.refresh(); 112 } 113 114 /** 115 * Unanchor the editor from an element. 116 */ 117 #detach() { 118 if (this.#attachedElement) { 119 this.#attachedElement.classList.remove("html-editor-container"); 120 this.#attachedElement = undefined; 121 } 122 } 123 124 /** 125 * Anchor the editor to a particular element, and show the editor. 126 * 127 * @param {DOMNode} element 128 * The element that the editor will be anchored to. 129 * Should belong to the HTMLDocument passed into the constructor. 130 * @param {string} text 131 * Value to set the contents of the editor to 132 * @param {Function} cb 133 * The function to call when hiding 134 */ 135 show = (element, text) => { 136 if (this.isVisible) { 137 return; 138 } 139 140 this.#originalValue = text; 141 this.editor.setText(text, { saveTransactionToHistory: false }); 142 this.#attach(element); 143 this.#container.style.display = "flex"; 144 this.isVisible = true; 145 146 this.editor.focus(); 147 148 this.emit("popupshown"); 149 }; 150 151 /** 152 * Hide the editor, optionally committing the changes 153 * 154 * @param {boolean} shouldCommit 155 * A change will be committed by default. If this param 156 * strictly equals false, no change will occur. 157 */ 158 hide = shouldCommit => { 159 if (!this.isVisible) { 160 return; 161 } 162 163 this.#container.style.display = "none"; 164 this.#detach(); 165 166 const newValue = this.editor.getText(); 167 const valueHasChanged = this.#originalValue !== newValue; 168 const preventCommit = shouldCommit === false || !valueHasChanged; 169 this.#originalValue = undefined; 170 this.isVisible = undefined; 171 this.emit("popuphidden", !preventCommit, newValue); 172 }; 173 174 /** 175 * Destroy this object and unbind all event handlers 176 */ 177 destroy() { 178 this.doc.defaultView.removeEventListener("resize", this.refresh, true); 179 this.#container.removeEventListener("click", this.hide); 180 this.#editorInner.removeEventListener("click", HTMLEditor.#stopPropagation); 181 this.#editorInner.removeEventListener( 182 "keydown", 183 HTMLEditor.#stopPropagation 184 ); 185 186 this.hide(false); 187 this.#container.remove(); 188 this.editor.destroy(); 189 } 190 } 191 192 module.exports = HTMLEditor;