michael@0: /* vim:set ts=2 sw=2 sts=2 et tw=80: michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: const {Cu} = require("chrome"); michael@0: const Editor = require("devtools/sourceeditor/editor"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/devtools/event-emitter.js"); michael@0: michael@0: exports.HTMLEditor = HTMLEditor; michael@0: michael@0: function ctrl(k) { michael@0: return (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") + k; michael@0: } michael@0: function stopPropagation(e) { michael@0: e.stopPropagation(); michael@0: } michael@0: /** michael@0: * A wrapper around the Editor component, that allows editing of HTML. michael@0: * michael@0: * The main functionality this provides around the Editor is the ability michael@0: * to show/hide/position an editor inplace. It only appends once to the michael@0: * body, and uses CSS to position the editor. The reason it is done this michael@0: * way is that the editor is loaded in an iframe, and calling appendChild michael@0: * causes it to reload. michael@0: * michael@0: * Meant to be embedded inside of an HTML page, as in markup-view.xhtml. michael@0: * michael@0: * @param HTMLDocument htmlDocument michael@0: * The document to attach the editor to. Will also use this michael@0: * document as a basis for listening resize events. michael@0: */ michael@0: function HTMLEditor(htmlDocument) michael@0: { michael@0: this.doc = htmlDocument; michael@0: this.container = this.doc.createElement("div"); michael@0: this.container.className = "html-editor theme-body"; michael@0: this.container.style.display = "none"; michael@0: this.editorInner = this.doc.createElement("div"); michael@0: this.editorInner.className = "html-editor-inner"; michael@0: this.container.appendChild(this.editorInner); michael@0: michael@0: this.doc.body.appendChild(this.container); michael@0: this.hide = this.hide.bind(this); michael@0: this.refresh = this.refresh.bind(this); michael@0: michael@0: EventEmitter.decorate(this); michael@0: michael@0: this.doc.defaultView.addEventListener("resize", michael@0: this.refresh, true); michael@0: michael@0: let config = { michael@0: mode: Editor.modes.html, michael@0: lineWrapping: true, michael@0: styleActiveLine: false, michael@0: extraKeys: {}, michael@0: theme: "mozilla markup-view" michael@0: }; michael@0: michael@0: config.extraKeys[ctrl("Enter")] = this.hide; michael@0: config.extraKeys["F2"] = this.hide; michael@0: config.extraKeys["Esc"] = this.hide.bind(this, false); michael@0: michael@0: this.container.addEventListener("click", this.hide, false); michael@0: this.editorInner.addEventListener("click", stopPropagation, false); michael@0: this.editor = new Editor(config); michael@0: michael@0: let iframe = this.editorInner.ownerDocument.createElement("iframe"); michael@0: this.editor.appendTo(this.editorInner, iframe).then(() => { michael@0: this.hide(false); michael@0: }).then(null, (err) => console.log(err.message)); michael@0: } michael@0: michael@0: HTMLEditor.prototype = { michael@0: michael@0: /** michael@0: * Need to refresh position by manually setting CSS values, so this will michael@0: * need to be called on resizes and other sizing changes. michael@0: */ michael@0: refresh: function() { michael@0: let element = this._attachedElement; michael@0: michael@0: if (element) { michael@0: this.container.style.top = element.offsetTop + "px"; michael@0: this.container.style.left = element.offsetLeft + "px"; michael@0: this.container.style.width = element.offsetWidth + "px"; michael@0: this.container.style.height = element.parentNode.offsetHeight + "px"; michael@0: this.editor.refresh(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Anchor the editor to a particular element. michael@0: * michael@0: * @param DOMNode element michael@0: * The element that the editor will be anchored to. michael@0: * Should belong to the HTMLDocument passed into the constructor. michael@0: */ michael@0: _attach: function(element) michael@0: { michael@0: this._detach(); michael@0: this._attachedElement = element; michael@0: element.classList.add("html-editor-container"); michael@0: this.refresh(); michael@0: }, michael@0: michael@0: /** michael@0: * Unanchor the editor from an element. michael@0: */ michael@0: _detach: function() michael@0: { michael@0: if (this._attachedElement) { michael@0: this._attachedElement.classList.remove("html-editor-container"); michael@0: this._attachedElement = undefined; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Anchor the editor to a particular element, and show the editor. michael@0: * michael@0: * @param DOMNode element michael@0: * The element that the editor will be anchored to. michael@0: * Should belong to the HTMLDocument passed into the constructor. michael@0: * @param string text michael@0: * Value to set the contents of the editor to michael@0: * @param function cb michael@0: * The function to call when hiding michael@0: */ michael@0: show: function(element, text) michael@0: { michael@0: if (this._visible) { michael@0: return; michael@0: } michael@0: michael@0: this._originalValue = text; michael@0: this.editor.setText(text); michael@0: this._attach(element); michael@0: this.container.style.display = "flex"; michael@0: this._visible = true; michael@0: michael@0: this.editor.refresh(); michael@0: this.editor.focus(); michael@0: michael@0: this.emit("popupshown"); michael@0: }, michael@0: michael@0: /** michael@0: * Hide the editor, optionally committing the changes michael@0: * michael@0: * @param bool shouldCommit michael@0: * A change will be committed by default. If this param michael@0: * strictly equals false, no change will occur. michael@0: */ michael@0: hide: function(shouldCommit) michael@0: { michael@0: if (!this._visible) { michael@0: return; michael@0: } michael@0: michael@0: this.container.style.display = "none"; michael@0: this._detach(); michael@0: michael@0: let newValue = this.editor.getText(); michael@0: let valueHasChanged = this._originalValue !== newValue; michael@0: let preventCommit = shouldCommit === false || !valueHasChanged; michael@0: this._originalValue = undefined; michael@0: this._visible = undefined; michael@0: this.emit("popuphidden", !preventCommit, newValue); michael@0: }, michael@0: michael@0: /** michael@0: * Destroy this object and unbind all event handlers michael@0: */ michael@0: destroy: function() michael@0: { michael@0: this.doc.defaultView.removeEventListener("resize", michael@0: this.refresh, true); michael@0: this.container.removeEventListener("click", this.hide, false); michael@0: this.editorInner.removeEventListener("click", stopPropagation, false); michael@0: michael@0: this.hide(false); michael@0: this.container.parentNode.removeChild(this.container); michael@0: } michael@0: };