// eslint-disable-next-line no-unused-vars, import/named
import Core, { ReturnObject } from './core.src';
import Image from './image';
import Listeners from './listeners';
import Util from './util';
import Configuration from './configuration';
import ServiceProvider from './serviceprovider';
/**
* @typedef {Object} IntegrationModelProperties
* @property {string} configurationService - Configuration service path.
* This parameter is needed to determine all services paths.
* @property {HTMLElement} integrationModelProperties.target - HTML target.
* @property {string} integrationModelProperties.scriptName - Integration script name.
* Usually the name of the integration script.
* @property {Object} integrationModelProperties.environment - integration environment properties.
* @property {Object} [integrationModelProperties.callbackMethodArguments] - object containing
* callback method arguments.
* @property {string} [integrationModelProperties.version] - integration version number.
* @property {Object} [integrationModelProperties.editorObject] - object containing
* the integration editor instance.
* @property {boolean} [integrationModelProperties.rtl] - true if the editor is in RTL mode.
* false otherwise.
* @property {ServiceProviderProperties} [integrationModelProperties.serviceProviderProperties]
* - The service parameters.
* @property {Object} [integrationModelProperties.integrationParameters]
* - Overwritten integration parameters.
*/
export default class IntegrationModel {
/**
* @classdesc
* This class represents an integration model, allowing the integration script to
* communicate with Core class. Each integration must extend this class.
* @constructs
* @param {IntegrationModelProperties} integrationModelProperties
*/
constructor(integrationModelProperties) {
/**
* Language. Needed for accessibility and locales. English by default.
*/
this.language = 'en';
/**
* Service parameters
* @type {ServiceProviderProperties}
*/
this.serviceProviderProperties = {};
if ('serviceProviderProperties' in integrationModelProperties) {
this.serviceProviderProperties = integrationModelProperties.serviceProviderProperties;
}
/**
* Configuration service path. The integration service is needed by Core class to
* load all the backend configuration into the frontend and also to create the paths
* of all services (all services lives in the same route). Mandatory property.
*/
this.configurationService = '';
if ('configurationService' in integrationModelProperties) {
this.serviceProviderProperties.URI = integrationModelProperties.configurationService;
console.warn('Deprecated property configurationService. Use serviceParameters on instead.',
[integrationModelProperties.configurationService]);
}
/**
* Plugin version. Needed to stats and caching.
* @type {string}
*/
this.version = ('version' in integrationModelProperties ? integrationModelProperties.version : '');
/**
* DOM target in which the plugin works. Needed to associate events, insert formulas, etc.
* Mandatory property.
*/
this.target = null;
if ('target' in integrationModelProperties) {
this.target = integrationModelProperties.target;
} else {
throw new Error('IntegrationModel constructor error: target property missed.');
}
/**
* Integration script name. Needed to know the plugin path.
*/
if ('scriptName' in integrationModelProperties) {
this.scriptName = integrationModelProperties.scriptName;
}
/**
* Object containing the arguments needed by the callback function.
*/
this.callbackMethodArguments = {};
if ('callbackMethodArguments' in integrationModelProperties) {
this.callbackMethodArguments = integrationModelProperties.callbackMethodArguments;
}
/**
* Contains information about the integration environment:
* like the name of the editor, the version, etc.
*/
this.environment = {};
if ('environment' in integrationModelProperties) {
this.environment = integrationModelProperties.environment;
}
/**
* Indicates if the DOM target is - or not - and iframe.
*/
this.isIframe = false;
if (this.target != null) {
this.isIframe = (this.target.tagName.toUpperCase() === 'IFRAME');
}
/**
* Instance of the integration editor object. Usually the entry point to access the API
* of a HTML editor.
*/
this.editorObject = null;
if ('editorObject' in integrationModelProperties) {
this.editorObject = integrationModelProperties.editorObject;
}
/**
* Specifies if the direction of the text is RTL. false by default.
*/
this.rtl = false;
if ('rtl' in integrationModelProperties) {
this.rtl = integrationModelProperties.rtl;
}
/**
* Specifies if the integration model exposes the locale strings. false by default.
*/
this.managesLanguage = false;
if ('managesLanguage' in integrationModelProperties) {
this.managesLanguage = integrationModelProperties.managesLanguage;
}
/**
* Indicates if an image is selected. Needed to resize the image to the original size in case
* the image is resized.
* @type {boolean}
*/
this.temporalImageResizing = false;
/**
* The Core class instance associated to the integration model.
* @type {Core}
*/
this.core = null;
/**
* Integration model listeners.
* @type {Listeners}
*/
this.listeners = new Listeners();
// Parameters overwrite.
if ('integrationParameters' in integrationModelProperties) {
IntegrationModel.integrationParameters.forEach((parameter) => {
if (parameter in integrationModelProperties.integrationParameters) {
// Don't add empty parameters.
const value = integrationModelProperties.integrationParameters[parameter];
if (Object.keys(value).length !== 0) {
this[parameter] = value;
}
}
});
}
}
/**
* Init function. Usually called from the integration side once the core.js file is loaded.
*/
init() {
// Check if language is an object and select the property necessary
this.language = this.getLanguage();
// We need to wait until Core class is loaded ('onLoad' event) before
// call the callback method.
const listener = Listeners.newListener('onLoad', () => {
this.callbackFunction(this.callbackMethodArguments);
});
// Backwards compatibility.
if (this.serviceProviderProperties.URI.indexOf('configuration') !== -1) {
const uri = this.serviceProviderProperties.URI;
const server = ServiceProvider.getServerLanguageFromService(uri);
this.serviceProviderProperties.server = server;
const configurationIndex = this.serviceProviderProperties.URI.indexOf('configuration');
const subsTring = this.serviceProviderProperties.URI.substring(0, configurationIndex);
this.serviceProviderProperties.URI = subsTring;
}
let serviceParametersURI = this.serviceProviderProperties.URI;
serviceParametersURI = serviceParametersURI.indexOf('/') === 0 || serviceParametersURI.indexOf('http') === 0
? serviceParametersURI
: Util.concatenateUrl(this.getPath(), serviceParametersURI);
this.serviceProviderProperties.URI = serviceParametersURI;
const coreProperties = {};
coreProperties.serviceProviderProperties = this.serviceProviderProperties;
this.setCore(new Core(coreProperties));
this.core.addListener(listener);
this.core.language = this.language;
// Initializing Core class.
this.core.init();
// TODO: Move to Core constructor.
this.core.setEnvironment(this.environment);
}
/**
* Returns the absolute path of the integration script.
* @return {string} - Absolute path for the integration script.
*/
getPath() {
if (typeof this.scriptName === 'undefined') {
throw new Error('scriptName property needed for getPath.');
}
const col = document.getElementsByTagName('script');
let path = '';
for (let i = 0; i < col.length; i += 1) {
const j = col[i].src.lastIndexOf(this.scriptName);
if (j >= 0) {
path = col[i].src.substr(0, j - 1);
}
}
return path;
}
/**
* Returns integration model plugin version
* @param {string} - Plugin version
*/
getVersion() {
return this.version;
}
/**
* Sets the language property.
* @param {string} language - language code.
*/
setLanguage(language) {
this.language = language;
}
/**
* Sets a Core instance.
* @param {Core} core - instance of Core class.
*/
setCore(core) {
this.core = core;
core.setIntegrationModel(this);
}
/**
* Returns the Core instance.
* @returns {Core} instance of Core class.
*/
getCore() {
return this.core;
}
/**
* Sets the object target and updates the iframe property.
* @param {HTMLElement} target - target object.
*/
setTarget(target) {
this.target = target;
this.isIframe = (this.target.tagName.toUpperCase() === 'IFRAME');
}
/**
* Sets the editor object.
* @param {Object} editorObject - The editor object.
*/
setEditorObject(editorObject) {
this.editorObject = editorObject;
}
/**
* Opens formula editor to editing a new formula. Can be overwritten in order to make some
* actions from integration part before the formula is edited.
*/
openNewFormulaEditor() {
this.core.editionProperties.isNewElement = true;
this.core.openModalDialog(this.target, this.isIframe);
}
/**
* Opens formula editor to editing an existing formula. Can be overwritten in order to make some
* actions from integration part before the formula is edited.
*/
openExistingFormulaEditor() {
this.core.editionProperties.isNewElement = false;
this.core.openModalDialog(this.target, this.isIframe);
}
/**
* Wrapper to Core.updateFormula method.
* Transform a MathML into a image formula.
* Then the image formula is inserted in the specified target, creating a new image (new formula)
* or updating an existing one.
* @param {string} mathml - MathML to generate the formula.
* @param {string} editMode - Edit Mode (LaTeX or images).
*/
updateFormula(mathml) {
if (this.editorParameters) {
mathml = com.wiris.editor.util.EditorUtils.addAnnotation(mathml, 'application/vnd.wiris.mtweb-params+json', JSON.stringify(this.editorParameters));
}
let focusElement;
let windowTarget;
const wirisProperties = null;
if (this.isIframe) {
focusElement = this.target.contentWindow;
windowTarget = this.target.contentWindow;
} else {
focusElement = this.target;
windowTarget = window;
}
let obj = this.core.beforeUpdateFormula(mathml, wirisProperties);
if (!obj) {
return '';
}
obj = this.insertFormula(focusElement, windowTarget, obj.mathml, obj.wirisProperties);
if (!obj) {
return '';
}
return this.core.afterUpdateFormula(obj.focusElement, obj.windowTarget, obj.node, obj.latex);
}
/**
* Wrapper to Core.insertFormula method.
* Inserts the formula in the specified target, creating
* a new image (new formula) or updating an existing one.
* @param {string} mathml - MathML to generate the formula.
* @param {string} editMode - Edit Mode (LaTeX or images).
* @returns {ReturnObject} - Object with the information of the node or latex to insert.
*/
insertFormula(focusElement, windowTarget, mathml, wirisProperties) {
return this.core.insertFormula(focusElement, windowTarget, mathml, wirisProperties);
}
/**
* Returns the target selection.
* @returns {Selection} target selection.
*/
getSelection() {
if (this.isIframe) {
this.target.contentWindow.focus();
return this.target.contentWindow.getSelection();
}
this.target.focus();
return window.getSelection();
}
/**
* Add events to formulas in the DOM target. The events added are the following:
* - doubleClickHandler: handles double click event on formulas by opening an editor
* to edit them.
* - mouseDownHandler: handles mouse down event on formulas by saving the size of the formula
* in case the the formula is resized.
* - mouseUpHandler: handles mouse up event on formulas by restoring the saved formula size
* in case the formula is resized.
*/
addEvents() {
const eventTarget = this.isIframe ? this.target.contentWindow.document : this.target;
Util.addElementEvents(
eventTarget,
(element, event) => {
this.doubleClickHandler(element, event);
},
(element, event) => {
this.mousedownHandler(element, event);
},
(element, event) => {
this.mouseupHandler(element, event);
},
);
}
/**
* Handles a double click on the target element. Opens an editor
* to re-edit the double-clicked formula.
* @param {HTMLElement} element - DOM object target.
*/
doubleClickHandler(element) {
if (element.nodeName.toLowerCase() === 'img') {
this.core.getCustomEditors().disable();
const customEditorAttributeName = Configuration.get('imageCustomEditorName');
if (element.hasAttribute(customEditorAttributeName)) {
const customEditor = element.getAttribute(customEditorAttributeName);
this.core.getCustomEditors().enable(customEditor);
}
if (Util.containsClass(element, Configuration.get('imageClassName'))) {
this.core.editionProperties.temporalImage = element;
this.core.editionProperties.isNewElement = true;
this.openExistingFormulaEditor();
}
}
}
/**
* Handles a mouse up event on the target element. Restores the image size to avoid
* resizing formulas.
*/
mouseupHandler() {
if (this.temporalImageResizing) {
setTimeout(() => {
Image.fixAfterResize(this.temporalImageResizing);
}, 10);
}
}
/**
* Handles a mouse down event on the target element. Saves the formula size to avoid
* resizing formulas.
* @param {HTMLElement} element - target element.
*/
mousedownHandler(element) {
if (element.nodeName.toLowerCase() === 'img') {
if (Util.containsClass(element, Configuration.get('imageClassName'))) {
this.temporalImageResizing = element;
}
}
}
/**
* Returns the integration language. By default the browser agent. This method
* should be overwritten to obtain the integration language, for example using the
* plugin API of an HTML editor.
* @returns {string} integration language.
*/
getLanguage() {
return this.getBrowserLanguage();
}
/**
* Returns the browser language.
* @returns {string} the browser language.
*/
// eslint-disable-next-line class-methods-use-this
getBrowserLanguage() {
let language = 'en';
if (navigator.userLanguage) {
language = navigator.userLanguage.substring(0, 2);
} else if (navigator.language) {
language = navigator.language.substring(0, 2);
} else {
language = 'en';
}
return language;
}
/**
* This function is called once the {@link Core} is loaded. IntegrationModel class
* will fire this method when {@link Core} 'onLoad' event is fired.
* This method should content all the logic to init
* the integration.
*/
callbackFunction() {
// It's needed to wait until the integration target is ready. The event is fired
// from the integration side.
const listener = Listeners.newListener('onTargetReady', () => {
this.addEvents(this.target);
});
this.listeners.add(listener);
}
/**
* Function called when the content submits an action.
*/
// eslint-disable-next-line class-methods-use-this
notifyWindowClosed() {
// Nothing.
}
/**
* Wrapper.
* Extracts mathml of a determined text node. This function is used as a wrapper inside core.js
* in order to get mathml from a text node that can contain normal LaTeX or other chosen text.
* @param {string} textNode - text node to extract the MathML.
* @param {int} caretPosition - caret position inside the text node.
* @returns {string} MathML inside the text node.
*/
// eslint-disable-next-line class-methods-use-this, no-unused-vars
getMathmlFromTextNode(textNode, caretPosition) {}
/**
* Wrapper
* It fills wrs event object of nonLatex with the desired data.
* @param {Object} event - event object.
* @param {Object} window dom window object.
* @param {string} mathml valid mathml.
*/
// eslint-disable-next-line class-methods-use-this, no-unused-vars
fillNonLatexNode(event, window, mathml) {}
/**
Wrapper.
* Returns selected item from the target.
* @param {HTMLElement} target - target element
* @param {boolean} iframe
*/
// eslint-disable-next-line class-methods-use-this, no-unused-vars
getSelectedItem(target, isIframe) {}
static setTemporalImageToNull() {
// eslint-disable-next-line no-undef
if (WirisPlugin.currentInstance) {
WirisPlugin.currentInstance.core.editionProperties.temporalImage = null; // eslint-disable-line
}
}
}
// To know if the integration that extends this class implements
// wrapper methods, they are set as undefined.
IntegrationModel.prototype.getMathmlFromTextNode = undefined;
IntegrationModel.prototype.fillNonLatexNode = undefined;
IntegrationModel.prototype.getSelectedItem = undefined;
/**
* An object containing a list with the overwritable class constructor properties.
* @type {Object}
*/
IntegrationModel.integrationParameters = ['serviceProviderProperties', 'editorParameters'];