/* 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 Editor = require(
"resource://devtools/client/shared/sourceeditor/editor.js");
const EventEmitter = require(
"resource://devtools/shared/event-emitter.js");
/**
* A wrapper around the Editor component, that allows editing of HTML.
*
* The main functionality this provides around the Editor is the ability
* to show/hide/position an editor inplace. It only appends once to the
* body, and uses CSS to position the editor. The reason it is done this
* way is that the editor is loaded in an iframe, and calling appendChild
* causes it to reload.
*
* Meant to be embedded inside of an HTML page, as in markup.html.
*/
class HTMLEditor
extends EventEmitter {
/**
* @param {HTMLDocument} htmlDocument
* The document to attach the editor to. Will also use this
* document as a basis for listening resize events.
*/
constructor(htmlDocument) {
super();
this.doc = htmlDocument;
this.#container =
this.doc.createElement(
"div");
this.#container.className =
"html-editor theme-body";
this.#container.style.display =
"none";
this.#editorInner =
this.doc.createElement(
"div");
this.#editorInner.className =
"html-editor-inner devtools-input";
this.#container.appendChild(
this.#editorInner);
this.doc.body.appendChild(
this.#container);
this.doc.defaultView.addEventListener(
"resize",
this.refresh,
true);
const config = {
mode: Editor.modes.html,
lineWrapping:
true,
styleActiveLine:
false,
keyMap: [
{ key: HTMLEditor.#ctrl(
"Enter"), run:
this.hide },
{ key:
"F2", run:
this.hide },
{
key:
"Escape",
run:
this.hide.bind(
this,
false),
preventDefault:
true,
},
],
theme:
"mozilla markup-view",
cm6:
true,
};
this.#container.addEventListener(
"click",
this.hide);
this.#editorInner.addEventListener(
"click", HTMLEditor.#stopPropagation);
// Avoid the hijack of the backspace key by the markup when the
// html editor is open.
this.#editorInner.addEventListener(
"keydown", HTMLEditor.#stopPropagation);
this.editor =
new Editor(config);
this.editor.appendToLocalElement(
this.#editorInner);
this.hide(
false);
}
editor =
null;
doc =
null;
#container =
null;
#editorInner =
null;
#attachedElement =
null;
#originalValue;
static #ctrl(k) {
return (Services.appinfo.OS ==
"Darwin" ?
"Cmd-" :
"Ctrl-") + k;
}
static #stopPropagation(e) {
e.stopPropagation();
}
/**
* Need to refresh position by manually setting CSS values, so this will
* need to be called on resizes and other sizing changes.
*/
refresh = () => {
const element =
this.#attachedElement;
if (element) {
this.#container.style.top = element.offsetTop +
"px";
this.#container.style.left = element.offsetLeft +
"px";
this.#container.style.width = element.offsetWidth +
"px";
this.#container.style.height = element.parentNode.offsetHeight +
"px";
}
};
/**
* Anchor the editor to a particular element.
*
* @param {DOMNode} element
* The element that the editor will be anchored to.
* Should belong to the HTMLDocument passed into the constructor.
*/
#attach(element) {
this.#detach();
this.#attachedElement = element;
element.classList.add(
"html-editor-container");
this.refresh();
}
/**
* Unanchor the editor from an element.
*/
#detach() {
if (
this.#attachedElement) {
this.#attachedElement.classList.remove(
"html-editor-container");
this.#attachedElement = undefined;
}
}
/**
* Anchor the editor to a particular element, and show the editor.
*
* @param {DOMNode} element
* The element that the editor will be anchored to.
* Should belong to the HTMLDocument passed into the constructor.
* @param {string} text
* Value to set the contents of the editor to
* @param {Function} cb
* The function to call when hiding
*/
show = (element, text) => {
if (
this.isVisible) {
return;
}
this.#originalValue = text;
this.editor.setText(text, { saveTransactionToHistory:
false });
this.#attach(element);
this.#container.style.display =
"flex";
this.isVisible =
true;
this.editor.focus();
this.emit(
"popupshown");
};
/**
* Hide the editor, optionally committing the changes
*
* @param {boolean} shouldCommit
* A change will be committed by default. If this param
* strictly equals false, no change will occur.
*/
hide = shouldCommit => {
if (!
this.isVisible) {
return;
}
this.#container.style.display =
"none";
this.#detach();
const newValue =
this.editor.getText();
const valueHasChanged =
this.#originalValue !== newValue;
const preventCommit = shouldCommit ===
false || !valueHasChanged;
this.#originalValue = undefined;
this.isVisible = undefined;
this.emit(
"popuphidden", !preventCommit, newValue);
};
/**
* Destroy this object and unbind all event handlers
*/
destroy() {
this.doc.defaultView.removeEventListener(
"resize",
this.refresh,
true);
this.#container.removeEventListener(
"click",
this.hide);
this.#editorInner.removeEventListener(
"click", HTMLEditor.#stopPropagation);
this.#editorInner.removeEventListener(
"keydown",
HTMLEditor.#stopPropagation
);
this.hide(
false);
this.#container.remove();
this.editor.destroy();
}
}
module.exports = HTMLEditor;