gecko/browser/devtools/markupview/html-editor.js

188 lines
5.4 KiB
JavaScript

/* vim:set ts=2 sw=2 sts=2 et tw=80:
* 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 {Cu} = require("chrome");
const Editor = require("devtools/sourceeditor/editor");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/devtools/event-emitter.js");
exports.HTMLEditor = HTMLEditor;
function ctrl(k) {
return (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") + k;
}
function stopPropagation(e) {
e.stopPropagation();
}
/**
* 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-view.xhtml.
*
* @param HTMLDocument htmlDocument
* The document to attach the editor to. Will also use this
* document as a basis for listening resize events.
*/
function HTMLEditor(htmlDocument)
{
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";
this.container.appendChild(this.editorInner);
this.doc.body.appendChild(this.container);
this.hide = this.hide.bind(this);
this.refresh = this.refresh.bind(this);
EventEmitter.decorate(this);
this.doc.defaultView.addEventListener("resize",
this.refresh, true);
let config = {
mode: Editor.modes.html,
lineWrapping: true,
styleActiveLine: false,
extraKeys: {},
theme: "mozilla markup-view"
};
config.extraKeys[ctrl("Enter")] = this.hide;
config.extraKeys["F2"] = this.hide;
config.extraKeys["Esc"] = this.hide.bind(this, false);
this.container.addEventListener("click", this.hide, false);
this.editorInner.addEventListener("click", stopPropagation, false);
this.editor = new Editor(config);
let iframe = this.editorInner.ownerDocument.createElement("iframe");
this.editor.appendTo(this.editorInner, iframe).then(() => {
this.hide(false);
}).then(null, (err) => console.log(err.message));
}
HTMLEditor.prototype = {
/**
* Need to refresh position by manually setting CSS values, so this will
* need to be called on resizes and other sizing changes.
*/
refresh: function() {
let 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";
this.editor.refresh();
}
},
/**
* 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: function(element)
{
this._detach();
this._attachedElement = element;
element.classList.add("html-editor-container");
this.refresh();
},
/**
* Unanchor the editor from an element.
*/
_detach: function()
{
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: function(element, text)
{
if (this._visible) {
return;
}
this._originalValue = text;
this.editor.setText(text);
this._attach(element);
this.container.style.display = "flex";
this._visible = true;
this.editor.refresh();
this.editor.focus();
this.emit("popupshown");
},
/**
* Hide the editor, optionally committing the changes
*
* @param bool shouldCommit
* A change will be committed by default. If this param
* strictly equals false, no change will occur.
*/
hide: function(shouldCommit)
{
if (!this._visible) {
return;
}
this.container.style.display = "none";
this._detach();
let newValue = this.editor.getText();
let valueHasChanged = this._originalValue !== newValue;
let preventCommit = shouldCommit === false || !valueHasChanged;
this._originalValue = undefined;
this._visible = undefined;
this.emit("popuphidden", !preventCommit, newValue);
},
/**
* Destroy this object and unbind all event handlers
*/
destroy: function()
{
this.doc.defaultView.removeEventListener("resize",
this.refresh, true);
this.container.removeEventListener("click", this.hide, false);
this.editorInner.removeEventListener("click", stopPropagation, false);
this.hide(false);
this.container.remove();
this.editor.destroy();
}
};