tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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;