# components/Logbook/HTMLEditor.js

## Maintainability

65.87

## Lines of code

211

## Difficulty

32.29

## Estimated Errors

1.99

## Function weight

### By Complexity

### By SLOC

import React, { Component } from 'react'; import _ from 'lodash' import axios from 'axios'; import PropTypes from 'prop-types' import { uploadFile } from '../../api/icat/icatPlus.js' // tinymce editor import { Editor } from '@tinymce/tinymce-react' import tinymce from 'tinymce/tinymce'; import { EditionModeConfig, ViewModeConfig } from '../../config/tinymce/tinymce.js' import UI from '../../config/ui/config'; import { getFileByEventId } from "../../api/icat/icatPlus" import { NEW_EVENT_CONTEXT, PLAINTEXT_CONTENT_FORMAT, EDIT_EVENT_CONTEXT, HTML_CONTENT_FORMAT } from '../../constants/EventTypes.js'; /** * The HTML editor used to read and write the logbook's annotations. */ const EDITOR_ID_FOR_CREATION = 'myEditorForCreation'; const EDITOR_ID_FOR_EDITION = 'myEditorForEdition'; const EVENT_FOOTER_HEIGHT = 37; class HTMLEditor extends Component { constructor(props) { super(props); this.originalText = props.text; // Stores original text provided in props, could be undefined this.state = { editorContent: this.originalText } this.imagesUploadHandler = this.imagesUploadHandler.bind(this); this.onEditorChange = this.onEditorChange.bind(this); this.storeToLocalStorage = this.storeToLocalStorage.bind(this); this.setImageHeightByCSSRule = this.setImageHeightByCSSRule.bind(this); this.onImageLoaded = this.onImageLoaded.bind(this); } render() { let isEditionMode = this.props.isEditionMode; const { editorContent } = this.state; const config = (isEditionMode === true) ? new EditionModeConfig() : new ViewModeConfig(); return ( <Editor id={this.props.text ? EDITOR_ID_FOR_EDITION : EDITOR_ID_FOR_CREATION} init={{ plugins: config.plugins, skin_url: config.skin_url, branding: config.branding, readonly: config.readonly, toolbar: config.toolbar, menubar: config.menubar, statusbar: config.statusbar, images_upload_handler: this.imagesUploadHandler, paste_data_images: config.paste_data_images, formats: config.formats, content_css: config.content_css, }} value={editorContent} onEditorChange={this.onEditorChange} /> ); } componentDidMount() { this.storeToLocalStorage() this.setEditingAreaHeight(); } componentDidUpdate() { this.setEditingAreaHeight(); } setEditingAreaHeight() { let panelPart = document.getElementById('refForEditorHeightCalculation'); let editorHeader = document.getElementsByClassName("mce-top-part mce-container mce-stack-layout-item mce-first")[0]; let iframe = this.props.text ? document.getElementById(EDITOR_ID_FOR_EDITION + '_ifr') : document.getElementById(EDITOR_ID_FOR_CREATION + '_ifr'); iframe.style.height = (panelPart.clientHeight - EVENT_FOOTER_HEIGHT - editorHeader.clientHeight).toString() + 'px'; // this.props.maxEditorHeight; let iframeBody = iframe.contentWindow.document.getElementById('tinymce'); iframeBody.style.height = (panelPart.clientHeight - 8 - 8 - EVENT_FOOTER_HEIGHT - editorHeader.clientHeight - 1).toString() + 'px'; //8 = marginTop and marginBottom of the iframe's body } /** * Callback function triggered when the image has been downloaded * @param {*} event the event */ onImageLoaded(event) { let element = tinymce.activeEditor.selection.select(event.target); tinymce.activeEditor.dom.setStyles(element, { 'max-width': '100%', 'height': 'auto' }); } /** * Sets a default image height using a CSS rule. This is a trick to make sure the image height * is applied in the editor (for scrollabar propose) especially because the image is not necessary * yet downloaded at this step. The height is changed to auto after the image is fully downloaded * such that image ratio is kept. */ setImageHeightByCSSRule() { if (tinymce && tinymce.activeEditor) { let selectedNode = tinymce.activeEditor.selection.getNode(); if (selectedNode.nodeName === 'IMG' && selectedNode.style.height !== 'auto') { let nxElement = selectedNode; nxElement.style.height = UI.logbook.NEW_EVENT_MAX_HEIGHT; nxElement.style.width = 'auto'; // a css trick for some browsers IE8 and old iceweasel nxElement.onload = this.onImageLoaded; tinymce.activeEditor.dom.replace(nxElement, selectedNode); } } } /** * The function executed when the editor state changes (mouse click, key press for example ) * @param {string} editorContent the editor content in html format */ onEditorChange(editorContent) { this.setImageHeightByCSSRule(); // Inform parent component that the current text equals the original text as provided in the props if (this.props.onEventModified) { let hasText = editorContent.length !== 0 ? true : false; let isCurrentTextEqualsOriginal; if (this.originalText) { isCurrentTextEqualsOriginal = (_.isEqual(editorContent, this.originalText)) ? true : false; } else { isCurrentTextEqualsOriginal = false; } this.props.onEventModified({ hasText: hasText, currentTextEqualsOriginal: isCurrentTextEqualsOriginal }); } this.setState({ editorContent: editorContent }); this.storeToLocalStorage(editorContent); } /** * Defines what to do when the user drag an image onto the dropzone of the editor image plugin. This function must return a promise. * The value of a fullfilled promise must be an array of the form { data: { link: url } } where url value is the link to the image which * has just been uoloaded to the ICAT+ server. * @param {*} file : the image which has just been dropped on the drop zone. */ imagesUploadHandler(blobInfo, success, failure) { let { investigationId } = this.props; var sessionId = this.props.user.sessionId; let data = new FormData(); data.append('file', blobInfo.blob(), blobInfo.filename()); data.append('investigationId', investigationId); data.append('creationDate', Date()); data.append('type', 'attachment'); data.append('category', 'file'); data.append('username', this.props.user.username); axios({ method: "post", url: uploadFile(sessionId, investigationId), data: data, }) .then(function (value) { let eventId = value.data._id; success(getFileByEventId(sessionId, investigationId, eventId)); }, function (error) { console.log("[ERROR] Retrieval of the image you have just upladed into the editor failed ! "); failure(error); }); } /** * Store the editor content to localStorage. * @param {*} editorContent the editor content in HTML format */ storeToLocalStorage(editorContent) { if (editorContent) { // Editor content has been modified by the user. Save the update to localStorage if (tinymce && tinymce.activeEditor) { // Save the plain text format to localstorage this.props.storeToLocalStorage(this.props.text ? EDIT_EVENT_CONTEXT : NEW_EVENT_CONTEXT, tinymce.activeEditor.getContent({ format: 'text' }), PLAINTEXT_CONTENT_FORMAT) } // Save the HTML format to localstorage this.props.storeToLocalStorage(this.props.text ? EDIT_EVENT_CONTEXT : NEW_EVENT_CONTEXT, editorContent, HTML_CONTENT_FORMAT); } else { // The following is executed on componentDidMount. Usefull when the event title is changed only while the editor content was not modified this.props.storeToLocalStorage(this.props.text ? EDIT_EVENT_CONTEXT : NEW_EVENT_CONTEXT, this.originalText, HTML_CONTENT_FORMAT); } } } HTMLEditor.defaultProps = { /** by default, the editor is not in editing mode */ isEditionMode: false }; HTMLEditor.propTypes = { /** Determines whether the editor is in editing mode or not. */ isEditionMode: PropTypes.bool, /** The text provided to the editor. No text indicates that HTMLEditor is begin used for the creation of a new event. */ text: PropTypes.string, /** the investigationId of the event being edited. */ investigationId: PropTypes.string, /** the user who is currently logged in */ user: PropTypes.object.isRequired, /** callback function called when editor content changed : from no text to text or vice versa, or when the current text is identical to the original text provided to the editor*/ onEventModified: PropTypes.func, } export default HTMLEditor;