import Configuration from './configuration';
import EditorListener from './editorlistener';
import Listeners from './listeners';
import MathML from './mathml';
import Util from './util';
export default class ContentManager {
/**
* @classdesc
* This class represents a modal dialog, managing the following:
* - The insertion of content into the current instance of the {@link ModalDialog} class.
* - The actions to be done once the modal object has been submitted
* (submitAction} method).
* - The update of the content when the {@link ModalDialog} class is also updated,
* for example when ModalDialog is re-opened.
* - The communication between the {@link ModalDialog} class and itself, if the content
* has been changed (hasChanges} method).
* @constructs
* @param {Object} contentManagerAttributes - Object containing all attributes needed to
* create a new instance.
*/
constructor(contentManagerAttributes) {
/**
* An object containing MathType editor parameters. See
* http://docs.wiris.com/en/mathtype/mathtype_web/sdk-api/parameters for further information.
* @type {Object}
*/
this.editorAttributes = {};
if ('editorAttributes' in contentManagerAttributes) {
this.editorAttributes = contentManagerAttributes.editorAttributes;
} else {
throw new Error('ContentManager constructor error: editorAttributes property missed.');
}
/**
* CustomEditors instance. Contains the custom editors.
* @type {CustomEditors}
*/
this.customEditors = null;
if ('customEditors' in contentManagerAttributes) {
this.customEditors = contentManagerAttributes.customEditors;
}
/**
* Environment properties. This object contains data about the integration platform.
* @type {Object}
* @property {String} editor - Editor name. Usually the HTML editor.
* @property {String} mode - Save mode. Xml by default.
* @property {String} version - Plugin version.
*/
this.environment = {};
if ('environment' in contentManagerAttributes) {
this.environment = contentManagerAttributes.environment;
} else {
throw new Error('ContentManager constructor error: environment property missed');
}
/**
* ContentManager language.
* @type {String}
*/
this.language = '';
if ('language' in contentManagerAttributes) {
this.language = contentManagerAttributes.language;
} else {
throw new Error('ContentManager constructor error: language property missed');
}
/**
* {@link EditorListener} instance. Manages the changes inside the editor.
* @type {EditorListener}
*/
this.editorListener = new EditorListener();
/**
* MathType editor instance.
* @type {JsEditor}
*/
this.editor = null;
/**
* Navigator user agent.
* @type {String}
*/
this.ua = navigator.userAgent.toLowerCase();
/**
* Mobile device properties object
* @type {DeviceProperties}
*/
this.deviceProperties = {};
this.deviceProperties.isAndroid = this.ua.indexOf('android') > -1;
this.deviceProperties.isIOS = ContentManager.isIOS();
/**
* Custom editor toolbar.
* @type {String}
*/
this.toolbar = null;
/**
* Instance of the {@link ModalDialog} class associated with the current
* {@link ContentManager} instance.
* @type {ModalDialog}
*/
this.modalDialogInstance = null;
/**
* ContentManager listeners.
* @type {Listeners}
*/
this.listeners = new Listeners();
/**
* MathML associated to the ContentManager instance.
* @type {String}
*/
this.mathML = null;
/**
* Indicates if the edited element is a new one or not.
* @type {Boolean}
*/
this.isNewElement = true;
/**
* {@link IntegrationModel} instance. Needed to call wrapper methods.
* @type {IntegrationModel}
*/
this.integrationModel = null;
}
/**
* Adds a new listener to the current {@link ContentManager} instance.
* @param {Object} listener - The listener to be added.
*/
addListener(listener) {
this.listeners.add(listener);
}
/**
* Sets an instance of {@link IntegrationModel} class to the current {@link ContentManager}
* instance.
* @param {IntegrationModel} integrationModel - The {@link IntegrationModel} instance.
*/
setIntegrationModel(integrationModel) {
this.integrationModel = integrationModel;
}
/**
* Sets the {@link ModalDialog} instance into the current {@link ContentManager} instance.
* @param {ModalDialog} modalDialogInstance - The {@link ModalDialog} instance
*/
setModalDialogInstance(modalDialogInstance) {
this.modalDialogInstance = modalDialogInstance;
}
/**
* Inserts the content into the current {@link ModalDialog} instance updating
* the title and inserting the JavaScript editor.
*/
insert() {
// Before insert the editor we update the modal object title to avoid weird render display.
this.updateTitle(this.modalDialogInstance);
this.insertEditor(this.modalDialogInstance);
}
/**
* Inserts MathType editor into the {@link ModalDialog.contentContainer}. It waits until
* editor's JavaScript is loaded.
*/
insertEditor() {
if (ContentManager.isEditorLoaded()) {
this.editor = window.com.wiris.jsEditor.JsEditor.newInstance(this.editorAttributes);
this.editor.insertInto(this.modalDialogInstance.contentContainer);
this.editor.focus();
if (this.modalDialogInstance.rtl) {
this.editor.action('rtl');
}
// Setting div in rtl in case of it's activated.
if (this.editor.getEditorModel().isRTL()) {
this.editor.element.style.direction = 'rtl';
}
// Editor listener: this object manages the changes logic of editor.
this.editor.getEditorModel().addEditorListener(this.editorListener);
// iOS events.
if (this.modalDialogInstance.deviceProperties.isIOS) {
setTimeout(function hide() {
// Make sure the modalDialogInstance is available when the timeout is over
// to avoid throw errors and stop execution.
if(this.hasOwnProperty('modalDialogInstance')) this.modalDialogInstance.hideKeyboard();
}, 400);
const formulaDisplayDiv = document.getElementsByClassName('wrs_formulaDisplay')[0];
Util.addEvent(formulaDisplayDiv, 'focus', this.modalDialogInstance.handleOpenedIosSoftkeyboard);
Util.addEvent(formulaDisplayDiv, 'blur', this.modalDialogInstance.handleClosedIosSoftkeyboard);
}
// Fire onLoad event. Necessary to set the MathML into the editor
// after is loaded.
this.listeners.fire('onLoad', {});
} else {
setTimeout(ContentManager.prototype.insertEditor.bind(this), 100);
}
}
/**
* Initializes the current class by loading MathType script.
*/
init() {
if (!ContentManager.isEditorLoaded()) {
this.addEditorAsExternalDependency();
}
}
/**
* Adds script element to the DOM to include editor externally.
*/
addEditorAsExternalDependency() {
const script = document.createElement('script');
script.type = 'text/javascript';
let editorUrl = Configuration.get('editorUrl');
// We create an object url for parse url string and work more efficiently.
const anchorElement = document.createElement('a');
ContentManager.setHrefToAnchorElement(anchorElement, editorUrl);
ContentManager.setProtocolToAnchorElement(anchorElement);
editorUrl = ContentManager.getURLFromAnchorElement(anchorElement);
// Load editor URL. We add stats as GET params.
const stats = this.getEditorStats();
script.src = `${editorUrl}?lang=${this.language}&stats-editor=${stats.editor}&stats-mode=${stats.mode}&stats-version=${stats.version}`;
document.getElementsByTagName('head')[0].appendChild(script);
}
/**
* Sets the specified url to the anchor element.
* @param {HTMLAnchorElement} anchorElement - Element where set 'url'.
* @param {String} url - URL to set.
*/
static setHrefToAnchorElement(anchorElement, url) {
anchorElement.href = url;
}
/**
* Sets the current protocol to the anchor element.
* @param {HTMLAnchorElement} anchorElement - Element where set its protocol.
*/
static setProtocolToAnchorElement(anchorElement) {
// Change to https if necessary.
if (window.location.href.indexOf('https://') === 0) {
// It check if browser is https and configuration is http.
// If this is so, we will replace protocol.
if (anchorElement.protocol === 'http:') {
anchorElement.protocol = 'https:';
}
}
}
/**
* Returns the url of the anchor element adding the current port
* if it is needed.
* @param {HTMLAnchorElement} anchorElement - Element where extract the url.
* @returns {String}
*/
static getURLFromAnchorElement(anchorElement) {
// Check protocol and remove port if it's standard.
const removePort = anchorElement.port === '80' || anchorElement.port === '443' || anchorElement.port === '';
return `${anchorElement.protocol}//${anchorElement.hostname}${ removePort ? '' : (':' + anchorElement.port) }${anchorElement.pathname.startsWith('/') ? anchorElement.pathname : ('/' + anchorElement.pathname)}`;
}
/**
* Returns object with editor stats.
*
* @typedef {Object} EditorStatsObject
* @property {string} editor - Editor name.
* @property {string} mode - Current configuration for formula save mode.
* @property {string} version - Current plugins version.
* @returns {EditorStatsObject}
*/
getEditorStats() {
// Editor stats. Use environment property to set it.
const stats = {};
if ('editor' in this.environment) {
stats.editor = this.environment.editor;
} else {
stats.editor = 'unknown';
}
if ('mode' in this.environment) {
stats.mode = this.environment.mode;
} else {
stats.mode = Configuration.get('saveMode');
}
if ('version' in this.environment) {
stats.version = this.environment.version;
} else {
stats.version = Configuration.get('version');
}
return stats;
}
/**
* Returns true if device is iOS. Otherwise, false.
* @returns {Boolean}
*/
static isIOS() {
return [
'iPad Simulator',
'iPhone Simulator',
'iPod Simulator',
'iPad',
'iPhone',
'iPod'
].includes(navigator.platform)
// iPad on iOS 13 detection
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document);
}
/**
* Returns true if editor is loaded. Otherwise, false.
* @returns {Boolean}
*/
static isEditorLoaded() {
// To know if editor JavaScript is loaded we need to wait until
// window.com.wiris.jsEditor.JsEditor.newInstance is ready.
return (window.com && window.com.wiris && window.com.wiris.jsEditor
&& window.com.wiris.jsEditor.JsEditor && window.com.wiris.jsEditor.JsEditor.newInstance);
}
/**
* Sets the {@link ContentManager.editor} initial content.
*/
setInitialContent() {
if (!this.isNewElement) {
this.setMathML(this.mathML);
}
}
/**
* Sets a MathML into {@link ContentManager.editor} instance.
* @param {String} mathml - MathML string.
* @param {Boolean} focusDisabled - If true editor don't get focus after the MathML is set.
* False by default.
*/
setMathML(mathml, focusDisabled) {
// By default focus is enabled.
if (typeof focusDisabled === 'undefined') {
focusDisabled = false;
}
// Using setMathML method is not a change produced by the user but for the API
// so we set to false the contentChange property of editorListener.
this.editor.setMathMLWithCallback(mathml, () => {
this.editorListener.setWaitingForChanges(true);
});
// We need to wait a little until the callback finish.
setTimeout(() => {
this.editorListener.setIsContentChanged(false);
}, 500);
// In some scenarios - like closing modal object - editor mustn't be focused.
if (!focusDisabled) {
this.onFocus();
}
}
/**
* Sets the focus to the current instance of {@link ContentManager.editor}. Triggered by
* {@link ModalDialog.focus}.
*/
onFocus() {
if (typeof this.editor !== 'undefined' && this.editor != null) {
this.editor.focus();
}
}
/**
* Updates the edition area by calling {@link IntegrationModel.updateFormula}.
* Triggered by {@link ModalDialog.submitAction}.
*/
submitAction() {
if (!this.editor.isFormulaEmpty()) {
let mathML = this.editor.getMathMLWithSemantics();
// Add class for custom editors.
if (this.customEditors.getActiveEditor() !== null) {
const { toolbar } = this.customEditors.getActiveEditor();
mathML = MathML.addCustomEditorClassAttribute(mathML, toolbar);
} else {
// We need - if exists - the editor name from MathML
// class attribute.
Object.keys(this.customEditors.editors).forEach((key) => {
mathML = MathML.removeCustomEditorClassAttribute(mathML, key);
});
}
const mathmlEntitiesEncoded = MathML.mathMLEntities(mathML);
this.integrationModel.updateFormula(mathmlEntitiesEncoded);
} else {
this.integrationModel.updateFormula(null);
}
this.customEditors.disable();
this.integrationModel.notifyWindowClosed();
// Set disabled focus to prevent lost focus.
this.setEmptyMathML();
this.customEditors.disable();
}
/**
* Sets an empty MathML as {@link ContentManager.editor} content.
*/
setEmptyMathML() {
// As second argument we pass.
if (this.deviceProperties.isAndroid || this.deviceProperties.isIOS) {
// We need to set a empty annotation in order to maintain editor in Hand mode.
// Adding dir rtl in case of it's activated.
if (this.editor.getEditorModel().isRTL()) {
this.setMathML('<math dir="rtl"><semantics><annotation encoding="application/json">[]</annotation></semantics></math>', true);
} else {
this.setMathML('<math><semantics><annotation encoding="application/json">[]</annotation></semantics></math>', true);
}
} else if (this.editor.getEditorModel().isRTL()) {
this.setMathML('<math dir="rtl"/>', true);
} else {
this.setMathML('<math/>', true);
}
}
/**
* Open event. Triggered by {@link ModalDialog.open}. Does the following:
* - Updates the {@link ContentManager.editor} content
* (with an empty MathML or an existing formula),
* - Updates the {@link ContentManager.editor} toolbar.
* - Recovers the the focus.
*/
onOpen() {
if (this.isNewElement) {
this.setEmptyMathML();
} else {
this.setMathML(this.mathML);
}
this.updateToolbar();
this.onFocus();
}
/**
* Sets the correct toolbar depending if exist other custom toolbars
* at the same time (e.g: Chemistry).
*/
updateToolbar() {
this.updateTitle(this.modalDialogInstance);
const customEditor = this.customEditors.getActiveEditor();
if (customEditor) {
const toolbar = customEditor.toolbar
? customEditor.toolbar
: _wrs_int_wirisProperties.toolbar;
if (this.toolbar == null || this.toolbar !== toolbar) {
this.setToolbar(toolbar);
}
} else {
const toolbar = this.getToolbar();
if (this.toolbar == null || this.toolbar !== toolbar) {
this.setToolbar(toolbar);
this.customEditors.disable();
}
}
}
/**
* Updates the current {@link ModalDialog.title}. If a {@link CustomEditors} is enabled
* sets the custom editor title. Otherwise sets the default title.
*/
updateTitle() {
const customEditor = this.customEditors.getActiveEditor();
if (customEditor) {
this.modalDialogInstance.setTitle(customEditor.title);
} else {
this.modalDialogInstance.setTitle('MathType');
}
}
/**
* Returns the editor toolbar, depending on the configuration local or server side.
* @returns {String} - Toolbar identifier.
*/
getToolbar() {
let toolbar = 'general';
if ('toolbar' in this.editorAttributes) {
({ toolbar } = this.editorAttributes);
}
// TODO: Change global integration variable for integration custom toolbar.
if (toolbar === 'general') {
// eslint-disable-next-line camelcase
toolbar = (typeof _wrs_int_wirisProperties === 'undefined' || typeof _wrs_int_wirisProperties.toolbar === 'undefined') ? 'general' : _wrs_int_wirisProperties.toolbar;
}
return toolbar;
}
/**
* Sets the current {@link ContentManager.editor} instance toolbar.
* @param {String} toolbar - The toolbar name.
*/
setToolbar(toolbar) {
this.toolbar = toolbar;
this.editor.setParams({ toolbar: this.toolbar });
}
/**
* Returns true if the content of the editor has been changed. The logic of the changes
* is delegated to {@link EditorListener} class.
* @returns {Boolean} True if the editor content has been changed. False otherwise.
*/
hasChanges() {
return (!this.editor.isFormulaEmpty() && this.editorListener.getIsContentChanged());
}
/**
* Handle keyboard events detected in modal when elements of this class intervene.
* @param {KeyboardEvent} keyboardEvent - The keyboard event.
*/
onKeyDown(keyboardEvent) {
if (keyboardEvent.key !== undefined && keyboardEvent.repeat === false) {
if (keyboardEvent.key === 'Escape' || keyboardEvent.key === 'Esc') { // Code to detect Esc event.
// There should be only one element with class name 'wrs_pressed' at the same time.
let list = document.getElementsByClassName('wrs_expandButton wrs_expandButtonFor3RowsLayout wrs_pressed');
if (list.length === 0) {
list = document.getElementsByClassName('wrs_expandButton wrs_expandButtonFor2RowsLayout wrs_pressed');
if (list.length === 0) {
list = document.getElementsByClassName('wrs_select wrs_pressed');
if (list.length === 0) {
this.modalDialogInstance.cancelAction();
keyboardEvent.stopPropagation();
keyboardEvent.preventDefault();
}
}
}
} else if (keyboardEvent.shiftKey && keyboardEvent.key === 'Tab') { // Code to detect shift Tab event.
if (document.activeElement === this.modalDialogInstance.submitButton) {
// Focus is on OK button.
this.editor.focus();
keyboardEvent.stopPropagation();
keyboardEvent.preventDefault();
} else {
const element = document.querySelector('[title="Manual"]');
if (document.activeElement === element) {
// Focus is on editor help.
this.modalDialogInstance.cancelButton.focus();
keyboardEvent.stopPropagation();
keyboardEvent.preventDefault();
}
}
} else if (keyboardEvent.key === 'Tab') { // Code to detect Tab event.
if (document.activeElement === this.modalDialogInstance.cancelButton) {
// Focus is on cancel button.
const element = document.querySelector('[title="Manual"]');
element.focus();
keyboardEvent.stopPropagation();
keyboardEvent.preventDefault();
} else {
// There should be only one element with class name 'wrs_formulaDisplay'.
const element = document.getElementsByClassName('wrs_formulaDisplay')[0];
if (element.getAttribute('class') === 'wrs_formulaDisplay wrs_focused') {
// Focus is on formuladisplay.
this.modalDialogInstance.submitButton.focus();
keyboardEvent.stopPropagation();
keyboardEvent.preventDefault();
}
}
}
}
}
}