suneditor
Version:
Vanilla JavaScript WYSIWYG web editor (2.x legacy version, actively maintained)
301 lines (257 loc) • 12.7 kB
JavaScript
'use strict';
import dialog from '../modules/dialog';
const KATEX_WEBSITE = "https://katex.org/docs/supported.html";
export default {
name: 'math',
display: 'dialog',
add: function (core) {
core.addModule([dialog]);
const context = core.context;
context.math = {
focusElement: null, // @Override dialog // This element has focus when the dialog is opened.
previewElement: null,
fontSizeElement: null,
defaultFontSize: '',
_mathExp: null
};
/** math dialog */
let math_dialog = this.setDialog(core);
context.math.modal = math_dialog;
context.math.focusElement = math_dialog.querySelector('.se-math-exp');
context.math.previewElement = math_dialog.querySelector('.se-math-preview');
context.math.fontSizeElement = math_dialog.querySelector('.se-math-size');
context.math.focusElement.addEventListener('paste', function (e) {
if (typeof core.functions.onPasteMath === 'function') {
core.functions.onPasteMath(e, core);
}
}, false);
context.math.focusElement.addEventListener(core.util.isIE ? 'textinput' : 'input', this._renderMathExp.bind(core, context.math), false);
context.math.fontSizeElement.addEventListener('change', function (e) { this.fontSize = e.target.value; }.bind(context.math.previewElement.style), false);
/** math controller */
let math_controller = this.setController_MathButton(core);
context.math.mathController = math_controller;
context.math._mathExp = null;
/** add event listeners */
math_dialog.querySelector('form').addEventListener('submit', this.submit.bind(core), false);
math_controller.addEventListener('click', this.onClick_mathController.bind(core));
context.math.previewElement.style.fontSize = context.math.defaultFontSize;
/** append html */
context.dialog.modal.appendChild(math_dialog);
context.element.relative.appendChild(math_controller);
/** empty memory */
math_dialog = null, math_controller = null;
},
/** dialog */
setDialog: function (core) {
const lang = core.lang;
const dialog = core.util.createElement('DIV');
const fontSize = core.options.mathFontSize;
let defaultFontSize = fontSize[0].value;
dialog.className = 'se-dialog-content';
dialog.style.display = 'none';
let html = '' +
'<form>' +
'<div class="se-dialog-header">' +
'<button type="button" data-command="close" class="se-btn se-dialog-close" title="' + lang.dialogBox.close + '" aria-label="' + lang.dialogBox.close + '">' +
core.icons.cancel +
'</button>' +
'<span class="se-modal-title">' + lang.dialogBox.mathBox.title + '</span>' +
'</div>' +
'<div class="se-dialog-body">' +
'<div class="se-dialog-form">' +
'<label>' + lang.dialogBox.mathBox.inputLabel + ' (<a href="' + KATEX_WEBSITE + '" target="_blank">KaTeX</a>)</label>' +
'<textarea class="se-input-form se-math-exp" type="text"></textarea>' +
'</div>' +
'<div class="se-dialog-form">' +
'<label>' + lang.dialogBox.mathBox.fontSizeLabel + '</label>' +
'<select class="se-input-select se-math-size">';
for (let i = 0, len = fontSize.length, f; i < len; i++) {
f = fontSize[i];
if (f.default) defaultFontSize = f.value;
html += '<option value="' + f.value + '"' + (f.default ? ' selected' : '') + '>' + f.text + '</option>';
}
html += '</select>' +
'</div>' +
'<div class="se-dialog-form">' +
'<label>' + lang.dialogBox.mathBox.previewLabel + '</label>' +
'<p class="se-math-preview"></p>' +
'</div>' +
'</div>' +
'<div class="se-dialog-footer">' +
'<button type="submit" class="se-btn-primary" title="' + lang.dialogBox.submitButton + '" aria-label="' + lang.dialogBox.submitButton + '"><span>' + lang.dialogBox.submitButton + '</span></button>' +
'</div>' +
'</form>';
core.context.math.defaultFontSize = defaultFontSize;
dialog.innerHTML = html;
return dialog;
},
/** modify controller button */
setController_MathButton: function (core) {
const lang = core.lang;
const math_btn = core.util.createElement('DIV');
math_btn.className = 'se-controller se-controller-link';
math_btn.innerHTML = '' +
'<div class="se-arrow se-arrow-up"></div>' +
'<div class="link-content">' +
'<div class="se-btn-group">' +
'<button type="button" data-command="update" tabindex="-1" class="se-btn se-tooltip">' +
core.icons.edit +
'<span class="se-tooltip-inner"><span class="se-tooltip-text">' + lang.controller.edit + '</span></span>' +
'</button>' +
'<button type="button" data-command="delete" tabindex="-1" class="se-btn se-tooltip">' +
core.icons.delete +
'<span class="se-tooltip-inner"><span class="se-tooltip-text">' + lang.controller.remove + '</span></span>' +
'</button>' +
'</div>' +
'</div>' +
'';
return math_btn;
},
/**
* @Required @Override dialog
*/
open: function () {
this.plugins.dialog.open.call(this, 'math', 'math' === this.currentControllerName);
},
/**
* @Override core - managedTagsInfo
*/
managedTags: function () {
return {
className: 'katex',
method: function (element) {
if (!element.getAttribute('data-exp') || !this.options.katex) return;
const dom = this._d.createRange().createContextualFragment(this.plugins.math._renderer.call(this, this.util.HTMLDecoder(element.getAttribute('data-exp'))));
element.innerHTML = dom.querySelector('.katex').innerHTML;
element.setAttribute('contenteditable', false);
}
};
},
_renderer: function (exp) {
let result = '';
try {
this.util.removeClass(this.context.math.focusElement, 'se-error');
result = this.options.katex.src.renderToString(exp, {throwOnError: true, displayMode: true});
} catch(error) {
this.util.addClass(this.context.math.focusElement, 'se-error');
result = '<span class="se-math-katex-error">Katex syntax error. (Refer <a href="' + KATEX_WEBSITE + '" target="_blank">KaTeX</a>)</span>';
console.warn('[SUNEDITOR.math.Katex.error] ', error);
}
return result;
},
_renderMathExp: function (contextMath, e) {
contextMath.previewElement.innerHTML = this.plugins.math._renderer.call(this, e.target.value);
},
submit: function (e) {
this.showLoading();
e.preventDefault();
e.stopPropagation();
const submitAction = function () {
if (this.context.math.focusElement.value.trim().length === 0) return false;
const contextMath = this.context.math;
const mathExp = contextMath.focusElement.value;
const katexEl = contextMath.previewElement.querySelector('.katex');
if (!katexEl) return false;
katexEl.className = '__se__katex ' + katexEl.className;
katexEl.setAttribute('contenteditable', false);
katexEl.setAttribute('data-exp', this.util.HTMLEncoder(mathExp));
katexEl.setAttribute('data-font-size', contextMath.fontSizeElement.value);
katexEl.style.fontSize = contextMath.fontSizeElement.value;
if (!this.context.dialog.updateModal) {
const selectedFormats = this.getSelectedElements();
if (selectedFormats.length > 1) {
const oFormat = this.util.createElement(selectedFormats[0].nodeName);
oFormat.appendChild(katexEl);
if (!this.insertNode(oFormat, null, true)) return false;
} else {
if (!this.insertNode(katexEl, null, true)) return false;
}
const empty = this.util.createTextNode(this.util.zeroWidthSpace);
katexEl.parentNode.insertBefore(empty, katexEl.nextSibling);
this.setRange(katexEl, 0, katexEl, 1);
} else {
const containerEl = this.util.getParentElement(contextMath._mathExp, '.katex');
containerEl.parentNode.replaceChild(katexEl, containerEl);
this.setRange(katexEl, 0, katexEl, 1);
}
contextMath.focusElement.value = '';
contextMath.fontSizeElement.value = '1em';
contextMath.previewElement.style.fontSize = '1em';
contextMath.previewElement.innerHTML = '';
return true;
}.bind(this);
try {
if (submitAction()) {
this.plugins.dialog.close.call(this);
// history stack
this.history.push(false);
}
} catch (e) {
this.plugins.dialog.close.call(this);
} finally {
this.closeLoading();
}
return false;
},
active: function (element) {
if (!element) {
if (this.controllerArray.indexOf(this.context.math.mathController) > -1) {
this.controllersOff();
}
} else if (element.getAttribute('data-exp')) {
if (this.controllerArray.indexOf(this.context.math.mathController) < 0) {
this.setRange(element, 0, element, 1);
this.plugins.math.call_controller.call(this, element);
}
return true;
}
return false;
},
on: function (update) {
if (!update) {
this.plugins.math.init.call(this);
} else {
const contextMath = this.context.math;
if (contextMath._mathExp) {
const exp = this.util.HTMLDecoder(contextMath._mathExp.getAttribute('data-exp'));
const fontSize = contextMath._mathExp.getAttribute('data-font-size') || '1em';
this.context.dialog.updateModal = true;
contextMath.focusElement.value = exp;
contextMath.fontSizeElement.value = fontSize;
contextMath.previewElement.innerHTML = this.plugins.math._renderer.call(this, exp);
contextMath.previewElement.style.fontSize = fontSize;
}
}
},
call_controller: function (mathTag) {
this.context.math._mathExp = mathTag;
const mathBtn = this.context.math.mathController;
this.setControllerPosition(mathBtn, mathTag, 'bottom', {left: 0, top: 0});
this.controllersOn(mathBtn, mathTag, 'math');
},
onClick_mathController: function (e) {
e.stopPropagation();
const command = e.target.getAttribute('data-command') || e.target.parentNode.getAttribute('data-command');
if (!command) return;
e.preventDefault();
if (/update/.test(command)) {
this.context.math.focusElement.value = this.util.HTMLDecoder(this.context.math._mathExp.getAttribute('data-exp'));
this.plugins.dialog.open.call(this, 'math', true);
} else {
/** delete */
this.util.removeItem(this.context.math._mathExp);
this.context.math._mathExp = null;
this.focus();
// history stack
this.history.push(false);
}
this.controllersOff();
},
init: function () {
const contextMath = this.context.math;
contextMath.mathController.style.display = 'none';
contextMath._mathExp = null;
contextMath.focusElement.value = '';
contextMath.previewElement.innerHTML = '';
}
};