UNPKG

@wiris/mathtype-ckeditor5

Version:

MathType Web for CKEditor5 editor

343 lines (298 loc) 13.4 kB
import IntegrationModel from "@wiris/mathtype-html-integration-devkit/src/integrationmodel.js"; import Util from "@wiris/mathtype-html-integration-devkit/src/util.js"; import Configuration from "@wiris/mathtype-html-integration-devkit/src/configuration.js"; import Latex from "@wiris/mathtype-html-integration-devkit/src/latex.js"; import MathML from "@wiris/mathtype-html-integration-devkit/src/mathml.js"; import Telemeter from "@wiris/mathtype-html-integration-devkit/src/telemeter.js"; /** * This class represents the MathType integration for CKEditor5. * @extends {IntegrationModel} */ export default class CKEditor5Integration extends IntegrationModel { constructor(ckeditorIntegrationModelProperties) { const editor = ckeditorIntegrationModelProperties.editorObject; if (typeof editor.config !== "undefined" && typeof editor.config.get("mathTypeParameters") !== "undefined") { ckeditorIntegrationModelProperties.integrationParameters = editor.config.get("mathTypeParameters"); } /** * CKEditor5 Integration. * * @param {integrationModelProperties} integrationModelAttributes */ super(ckeditorIntegrationModelProperties); /** * Folder name used for the integration inside CKEditor plugins folder. */ this.integrationFolderName = "ckeditor_wiris"; } /** * @inheritdoc * @returns {string} - The CKEditor instance language. * @override */ getLanguage() { // Returns the CKEDitor instance language taking into account that the language can be an object. // Try to get editorParameters.language, fail silently otherwise try { return this.editorParameters.language; } catch (e) {} const languageObject = this.editorObject.config.get("language"); if (languageObject != null) { if (typeof languageObject === "object") { // eslint-disable-next-line no-prototype-builtins if (languageObject.hasOwnProperty("ui")) { return languageObject.ui; } return languageObject; } return languageObject; } return super.getLanguage(); } /** * Adds callbacks to the following CKEditor listeners: * - 'focus' - updates the current instance. * - 'contentDom' - adds 'doubleclick' callback. * - 'doubleclick' - sets to null data.dialog property to avoid modifications for MathType formulas. * - 'setData' - parses the data converting MathML into images. * - 'afterSetData' - adds an observer to MathType formulas to avoid modifications. * - 'getData' - parses the data converting images into selected save mode (MathML by default). * - 'mode' - recalculates the active element. */ addEditorListeners() { const editor = this.editorObject; if (typeof editor.config.wirislistenersdisabled === "undefined" || !editor.config.wirislistenersdisabled) { this.checkElement(); } } /** * Checks the current container and assign events in case that it doesn't have them. * CKEditor replaces several times the element element during its execution, * so we must assign the events again to editor element. */ checkElement() { const editor = this.editorObject; const newElement = editor.sourceElement; // If the element wasn't treated, add the events. if (!newElement.wirisActive) { this.setTarget(newElement); this.addEvents(); // Set the element as treated newElement.wirisActive = true; } } /** * @inheritdoc * @param {HTMLElement} element - HTMLElement target. * @param {MouseEvent} event - event which trigger the handler. */ doubleClickHandler(element, event) { this.core.editionProperties.dbclick = true; if (this.editorObject.isReadOnly === false) { if (element.nodeName.toLowerCase() === "img") { if (Util.containsClass(element, Configuration.get("imageClassName"))) { // Some plugins (image2, image) open a dialog on Double-click. On formulas // doubleclick event ends here. if (typeof event.stopPropagation !== "undefined") { // old I.E compatibility. event.stopPropagation(); } else { event.returnValue = false; } this.core.getCustomEditors().disable(); const customEditorAttr = element.getAttribute(Configuration.get("imageCustomEditorName")); if (customEditorAttr) { this.core.getCustomEditors().enable(customEditorAttr); } this.core.editionProperties.temporalImage = element; this.openExistingFormulaEditor(); } } } } /** @inheritdoc */ static getCorePath() { return null; // TODO } /** @inheritdoc */ callbackFunction() { super.callbackFunction(); this.addEditorListeners(); } openNewFormulaEditor() { // Store the editor selection as it will be lost upon opening the modal this.core.editionProperties.selection = this.editorObject.editing.view.document.selection; // Focus on the selected editor when multiple editor instances are present WirisPlugin.currentInstance = this; return super.openNewFormulaEditor(); } /** * Replaces old formula with new MathML or inserts it in caret position if new * @param {String} mathml MathML to update old one or insert * @returns {module:engine/model/element~Element} The model element corresponding to the inserted image */ insertMathml(mathml) { // This returns the value returned by the callback function (writer => {...}) return this.editorObject.model.change((writer) => { const core = this.getCore(); const selection = this.editorObject.model.document.selection; const modelElementNew = writer.createElement("mathml", { formula: mathml, ...Object.fromEntries(selection.getAttributes()), // To keep the format, such as style and font }); // Obtain the DOM <span><img ... /></span> object corresponding to the formula if (core.editionProperties.isNewElement) { // Don't bother inserting anything at all if the MathML is empty. if (!mathml) return; const viewSelection = this.core.editionProperties.selection || this.editorObject.editing.view.document.selection; const modelPosition = this.editorObject.editing.mapper.toModelPosition(viewSelection.getLastPosition()); this.editorObject.model.insertObject(modelElementNew, modelPosition); // Remove selection if (!viewSelection.isCollapsed) { for (const range of viewSelection.getRanges()) { writer.remove(this.editorObject.editing.mapper.toModelRange(range)); } } // Set carret after the formula const position = this.editorObject.model.createPositionAfter(modelElementNew); writer.setSelection(position); } else { const img = core.editionProperties.temporalImage; const viewElement = this.editorObject.editing.view.domConverter.domToView(img).parent; const modelElementOld = this.editorObject.editing.mapper.toModelElement(viewElement); // Insert the new <mathml> and remove the old one const position = this.editorObject.model.createPositionBefore(modelElementOld); // If the given MathML is empty, don't insert a new formula. if (mathml) { this.editorObject.model.insertObject(modelElementNew, position); } writer.remove(modelElementOld); } // eslint-disable-next-line consistent-return return modelElementNew; }); } /** * Finds the text node corresponding to given DOM text element. * @param {element} viewElement Element to find corresponding text node of. * @returns {module:engine/model/text~Text|undefined} Text node corresponding to the given element or undefined if it doesn't exist. */ findText(viewElement) { // eslint-disable-line consistent-return // mapper always converts text nodes to *new* model elements so we need to convert the text's parents and then come back down let pivot = viewElement; let element; while (!element) { element = this.editorObject.editing.mapper.toModelElement( this.editorObject.editing.view.domConverter.domToView(pivot), ); pivot = pivot.parentElement; } // Navigate through all the subtree under `pivot` in order to find the correct text node const range = this.editorObject.model.createRangeIn(element); const descendants = Array.from(range.getItems()); for (const node of descendants) { let viewElementData = viewElement.data; if (viewElement.nodeType === 3) { // Remove invisible white spaces viewElementData = viewElementData.replaceAll(String.fromCharCode(8288), ""); } if (node.is("textProxy") && node.data === viewElementData.replace(String.fromCharCode(160), " ")) { return node.textNode; } } } /** @inheritdoc */ insertFormula(focusElement, windowTarget, mathml, wirisProperties) { // eslint-disable-line no-unused-vars const returnObject = {}; let mathmlOrigin; if (!mathml) { this.insertMathml(""); } else if (this.core.editMode === "latex") { returnObject.latex = Latex.getLatexFromMathML(mathml); returnObject.node = windowTarget.document.createTextNode(`$$${returnObject.latex}$$`); this.editorObject.model.change((writer) => { const { latexRange } = this.core.editionProperties; const startNode = this.findText(latexRange.startContainer); const endNode = this.findText(latexRange.endContainer); let startPosition = writer.createPositionAt(startNode.parent, startNode.startOffset + latexRange.startOffset); let endPosition = writer.createPositionAt(endNode.parent, endNode.startOffset + latexRange.endOffset); let range = writer.createRange(startPosition, endPosition); // When Latex is next to image/formula. if (latexRange.startContainer.nodeType === 3 && latexRange.startContainer.previousSibling?.nodeType === 1) { // Get the position of the latex to be replaced. const latexEdited = `$$${Latex.getLatexFromMathML( MathML.safeXmlDecode(this.core.editionProperties.temporalImage.dataset.mathml), )}$$`; let data = latexRange.startContainer.data; // Remove invisible characters. data = data.replaceAll(String.fromCharCode(8288), ""); // Get to the start of the latex we are editing. const offset = data.indexOf(latexEdited); const dataOffset = data.substring(offset); const second$ = dataOffset.substring(2).indexOf("$$") + 4; const substring = dataOffset.substr(0, second$); data = data.replace(substring, ""); if (!data) { startPosition = writer.createPositionBefore(startNode); range = startNode; } else { startPosition = startPosition = writer.createPositionAt(startNode.parent, startNode.startOffset + offset); endPosition = writer.createPositionAt(endNode.parent, endNode.startOffset + second$ + offset); range = writer.createRange(startPosition, endPosition); } } writer.remove(range); writer.insertText(`$$${returnObject.latex}$$`, startNode.getAttributes(), startPosition); }); } else { mathmlOrigin = this.core.editionProperties.temporalImage?.dataset.mathml; try { returnObject.node = this.editorObject.editing.view.domConverter.viewToDom( this.editorObject.editing.mapper.toViewElement(this.insertMathml(mathml)), windowTarget.document, ); } catch (e) { const x = e.toString(); if (x.includes("CKEditorError: Cannot read property 'parent' of undefined")) { this.core.modalDialog.cancelAction(); } } } // Build the telemeter payload separated to delete null/undefined entries. const payload = { mathml_origin: mathmlOrigin ? MathML.safeXmlDecode(mathmlOrigin) : mathmlOrigin, mathml: mathml ? MathML.safeXmlDecode(mathml) : mathml, elapsed_time: Date.now() - this.core.editionProperties.editionStartTime, editor_origin: null, // TODO read formula to find out whether it comes from Oxygen Desktop toolbar: this.core.modalDialog.contentManager.toolbar, size: mathml?.length, }; // Remove desired null keys. Object.keys(payload).forEach((key) => { if (key === "mathml_origin" || key === "editor_origin") !payload[key] ? delete payload[key] : {}; }); // Call Telemetry service to track the event. try { Telemeter.telemeter.track("INSERTED_FORMULA", { ...payload, }); } catch (error) { console.error("Error tracking INSERTED_FORMULA", error); } /* Due to PLUGINS-1329, we add the onChange event to the CK4 insertFormula. We probably should add it here as well, but we should look further into how */ // this.editorObject.fire('change'); // Remove temporal image of inserted formula this.core.editionProperties.temporalImage = null; return returnObject; } /** * Function called when the content submits an action. */ notifyWindowClosed() { this.editorObject.editing.view.focus(); } }