Source: xxe/view/MathJaxPane.js

class EditMathSourceDialog extends XUI.Dialog {
    static showDialog(mathNotation, mathSource, readOnly, reference=null) {
        return new Promise((resolve, reject) => { 
            let dialog = new EditMathSourceDialog(mathNotation, mathSource,
                                                  readOnly, resolve);
            dialog.open("center", reference);
        });
    }

    constructor(mathNotation, mathSource, readOnly, resolve) {
        super({ title: `Edit ${mathNotation}`,
                movable: true, resizable: true, closeable: true,
                template: EditMathSourceDialog.TEMPLATE,
                buttons: [ { label: "Cancel", action: "cancelAction" },
                           { label: "OK", action: "okAction" } ]});

        this._mathSource = mathSource.trim();
        this._resolve = resolve;
        
        this._mathArea = this.contentPane.firstElementChild.firstElementChild;
        this._mathArea.value = mathSource;
        this._mathArea.setSelectionRange(0, 0);

        if (readOnly) {
            this._mathArea.readOnly = readOnly;
            
            const okButton = this.buttons[1];
            okButton.disabled = true;
            okButton.classList.add("xui-control-disabled");
        }
    }
    
    dialogClosed(mathSource) {
        this._resolve(mathSource);
    }
    
    cancelAction() {
        this.close(null);
    }
    
    okAction() {
        let editedSource = this._mathArea.value.trim();
        if (editedSource.length === 0) {
            this._mathArea.focus();
            return;
        }
        
        if (editedSource === this._mathSource) {
            // Not changed by user.
            editedSource = null;
        }
        this.close(editedSource);
    }
}

EditMathSourceDialog.TEMPLATE = document.createElement("template");
EditMathSourceDialog.TEMPLATE.innerHTML = `<div class="xxe-edmath-pane">
  <textarea class="xui-control xxe-edmath-source" 
    rows="20" cols="70" wrap="off" 
    autocomplete="off" spellcheck="false"></textarea>
</div>`;

// ===========================================================================

/**
 * A custom control containing MathML or TeX/LaTeX math rendered by MathJax.
 * <p>This custom control automatically loads 
 * <a href="https://www.mathjax.org/">MathJax</a> code when needed to.
 */
class MathJaxPane extends HTMLElement {
    constructor() {
        super();
        
        this._xmlEditor = null;
        this._mathNotation = null;
        this._mathStartTag = null;
        this._mathEndTag = null;
        this._mathSource = null;

        this._editMathSource = this.editMathSource.bind(this);
        this.addEventListener("dblclick", this._editMathSource);
    }

    get contextualMenuItems() {
        return [ { label: `Edit ${this._mathNotation}...`,
                   cmdName: "_editMathSource()",
                   enabled: true } ];
    }
    
    // -----------------------------------
    // editMathSource
    // -----------------------------------
    
    editMathSource(event) {
        event.preventDefault();
        event.stopPropagation();

        let view = NodeView.lookupView(this);
        if (view === null) {
            // Should not happen.
            return;
        }
        
        const xmlEditor = this._xmlEditor;
        const docView = xmlEditor.documentView;
        
        docView.selectNode(view)
            .then((selected) => {
                if (!selected) {
                    // Not expected to happen.
                    return null;
                } else {
                    return EditMathSourceDialog.showDialog(
                        this._mathNotation, this._mathSource,
                        !docView.canEdit(), xmlEditor);
                }
            })
            .then((mathSource) => {
                if (mathSource === null) {
                    return CommandResult.CANCELED;
                } else {
                    let pasteParam = "to <?xml version='1.0'?>";
                    if (this._mathNotation === "MathML") {
                        // Remove whitespace between XML tags.
                        pasteParam += mathSource.replaceAll(/>\s+</g, "><");
                    } else {
                        pasteParam += this._mathStartTag;
                        pasteParam += DOMUtil.escapeXML(mathSource);
                        pasteParam += this._mathEndTag;
                    }
                    
                    return docView.executeCommand(EXECUTE_HELPER,
                                                  "paste", pasteParam);
                }
            })
            .then((result) => {
                if (result === null ||
                    result.status === COMMAND_RESULT_FAILED ||
                    result.status === COMMAND_RESULT_STOPPED) {
                    let msg;
                    if (result === null) {
                        msg = `Could not change ${this._mathNotation}.`;
                    } else {
                        msg = `Failed to change ${this._mathNotation}:
${result}`;
                    }
                    XUI.Alert.showError(msg, xmlEditor);
                }
            });
            // Catch not useful. DocumentView.executeCommand handles errors
            // by returning a null Promise.
    }
    
    // -----------------------------------------------------------------------
    // Custom element
    // -----------------------------------------------------------------------

    connectedCallback() {
        this._xmlEditor = DOMUtil.lookupAncestorByTag(this, "xxe-client");
        if (this._xmlEditor === null) {
            // Should not happen.
            return;
        }
        
        this._mathNotation = null;
        this._mathStartTag = null;
        this._mathEndTag = null;
        this._mathSource = null;
        
        const replacedSrc = this.getAttribute("replace");
        if (replacedSrc) {
            this.removeAttribute("replace"); // No longer useful.

            if (replacedSrc.match(/^<([\w]+:)?math\s/)) {
                this._mathNotation = "MathML";
                
                this._mathSource = replacedSrc;
            } else if (replacedSrc.startsWith("<")) {
                this._mathNotation = "TeX/LaTeX math";

                if (replacedSrc.endsWith("/>")) {
                    this._mathStartTag =
                        replacedSrc.substring(0, replacedSrc.length-2).trim() +
                        ">";
                    
                    let end = replacedSrc.indexOf(' ', 1);
                    if (end < 0) {
                        end = replacedSrc.indexOf('/', 1);
                    }
                    if (end > 1) {
                        this._mathEndTag =
                            "</" + replacedSrc.substring(1, end) + ">";
                        this._mathSource = "";
                    }
                } else {
                    const start = replacedSrc.indexOf('>');
                    if (start > 0) {
                        const end = replacedSrc.lastIndexOf("</");
                        if (end >= start+1) {
                            this._mathSource = DOMUtil.unescapeXML(
                                replacedSrc.substring(start+1, end).trim());
                            this._mathStartTag =
                                replacedSrc.substring(0, start+1);
                            this._mathEndTag = replacedSrc.substring(end);
                        }
                    }
                }
            }
        }
        if (this._mathSource === null) {
            // Should not happen.
            return;
        }
        
        this.setAttribute("title",
                          `Double-click to edit ${this._mathNotation}.`);
        
        // ---
        
        let addMathJax = true;
        if (document.getElementById("xxe-mathjax-script") !== null) {
            addMathJax = false;
        } else {
            const head = document.head;
            let child = head.firstElementChild;
            while (child !== null) {
                if (child.localName === "script") {
                    const src = child.getAttribute("src");
                    if (src && src.indexOf("mathjax") > 0) {
                        addMathJax = true;
                        break;
                    }
                }

                child = child.nextElementSibling;
            }
            
            if (addMathJax) {
                let script = document.createElement("script");
                script.setAttribute("type", "text/javascript");
                script.textContent = `MathJax = {
    options: { enableMenu: false },
    loader: { load: ['[tex]/color'] },
    tex: { packages: { '[+]': ['color'] } }
};`;
                head.appendChild(script);
                
                script = document.createElement("script");
                script.setAttribute("id", "xxe-mathjax-script");
                script.setAttribute("type", "text/javascript");
                script.setAttribute("src",
                "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js");
                head.appendChild(script);
            }
        }

        if (!addMathJax &&
            // Test needed because the document may initially contain
            // several MathJaxPanes.
            window.MathJax.typesetPromise) {
            window.MathJax.typesetPromise();
        }
    }
}

window.customElements.define("xxe-mathjax-pane", MathJaxPane);