UNPKG

@instructure/canvas-rce

Version:

A component wrapping Canvas's usage of Tinymce

309 lines (302 loc) 11 kB
/* * Copyright (C) 2021 - present Instructure, Inc. * * This file is part of Canvas. * * Canvas is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the Free * Software Foundation, version 3 of the License. * * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along * with this program. If not, see <http://www.gnu.org/licenses/>. */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { TextArea } from '@instructure/ui-text-area'; import { Button, CloseButton } from '@instructure/ui-buttons'; import { Checkbox } from '@instructure/ui-checkbox'; import { Heading } from '@instructure/ui-heading'; import { Modal } from '@instructure/ui-modal'; import { Flex } from '@instructure/ui-flex'; import { debounce } from '@instructure/debounce'; import { ConditionalTooltip } from '../../shared/ConditionalTooltip'; import formatMessage from '../../../../format-message'; import MemoizedEquationEditorToolbar from '../EquationEditorToolbar'; import { containsAdvancedSyntax } from './advancedOnlySyntax'; import * as advancedPreference from './advancedPreference'; import { instuiPopupMountNodeFn } from '../../../../util/fullscreenHelpers'; import { css } from 'aphrodite'; import { MathJaxDirective, Mathml } from '../../../../enhance-user-content/mathml'; import styles from './styles'; import RCEGlobals from '../../../RCEGlobals'; // Import the <math-field> container and all // the relevant math fonts from mathlive import '../mathlive'; import { insertTextIntoLatexTextarea } from './latexTextareaUtil'; export default class EquationEditorModal extends Component { constructor(props) { super(props); this.state = { advanced: this.props.openAdvanced || !!this.props.originalLatex.advancedOnly, workingFormula: this.props.originalLatex.latex || '' }; this.previewElement = /*#__PURE__*/React.createRef(); this.advancedEditor = /*#__PURE__*/React.createRef(); this.executeCommand = (cmd, advancedCmd) => { if (this.state.advanced) { const insertionText = advancedCmd || cmd; const textarea = this.advancedEditor.current?._textarea; if (textarea) { insertTextIntoLatexTextarea(textarea, insertionText); this.setState({ workingFormula: textarea.value }); } } else { this.mathField.insert(cmd, { focus: 'true' }); } }; this.handleModalCancel = () => { this.props.onModalDismiss(); }; this.handleModalDone = () => { const { onModalDismiss, onEquationSubmit } = this.props; const output = this.state.advanced ? this.state.workingFormula : this.getMathFiled(); if (output) { onEquationSubmit(output); } onModalDismiss(); }; this.renderMathInAdvancedPreview = debounce(() => { if (this.previewElement.current) { this.previewElement.current.innerHTML = String.raw`\(${this.state.workingFormula}\)`; this.mathml.processNewMathInElem(this.previewElement.current); } }, EquationEditorModal.debounceRate, { leading: false, trailing: true }); this.toggleAdvanced = () => { this.setState(state => { if (state.advanced) { this.setMathField(state.workingFormula || ''); return { advanced: false, workingFormula: '' }; } else { return { advanced: true, workingFormula: this.getMathFiled() }; } }); this.setPreviewElementContent(); }; this.toggleAndUpdatePreference = () => { this.toggleAdvanced(); advancedPreference.isSet() ? advancedPreference.remove() : advancedPreference.set(); }; this.registerBasicEditorListener = () => { const basicEditor = document.querySelector('math-field'); basicEditor.addEventListener('input', e => { if (this.advancedModeOnly(e.target.value)) { this.toggleAdvanced(); this.setState({ workingFormula: e.target.value }); } }); }; this.handleFieldRef = node => { this.mathField = node; }; this.renderFooter = () => { return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Button, { "data-testid": "equation-editor-modal-cancel", onClick: this.handleModalCancel }, formatMessage('Cancel')), /*#__PURE__*/React.createElement(Button, { "data-testid": "equation-editor-modal-done", margin: "none none none xx-small", onClick: this.handleModalDone, color: "primary" }, formatMessage('Done'))); }; this.renderToggle = () => { const lockToggle = this.state.advanced && this.advancedModeOnly(this.state.workingFormula); return /*#__PURE__*/React.createElement(ConditionalTooltip, { condition: lockToggle, renderTip: formatMessage('This equation cannot be rendered in Basic View.'), on: ['hover', 'focus'] }, /*#__PURE__*/React.createElement(Checkbox, { onChange: this.toggleAndUpdatePreference, checked: this.state.advanced, label: formatMessage('Directly Edit LaTeX'), variant: "toggle", disabled: lockToggle, "data-testid": "advanced-toggle" })); }; this.render = () => { const { onModalClose } = this.props; return /*#__PURE__*/React.createElement(Modal, { "data-mce-component": true, label: formatMessage('Equation Editor'), mountNode: instuiPopupMountNodeFn, onClose: onModalClose, onDismiss: this.handleModalCancel, open: true, transition: "fade", shouldCloseOnDocumentClick: false }, /*#__PURE__*/React.createElement(Modal.Header, null, /*#__PURE__*/React.createElement(CloseButton, { "data-testid": "equation-editor-modal-close", placement: "end", offset: "medium", color: "primary", onClick: this.handleModalCancel, screenReaderLabel: formatMessage('Close') }), /*#__PURE__*/React.createElement(Heading, null, formatMessage('Equation Editor'))), /*#__PURE__*/React.createElement(Modal.Body, null, /*#__PURE__*/React.createElement("div", { className: css(styles.mathfieldContainer) }, /*#__PURE__*/React.createElement(MemoizedEquationEditorToolbar, { executeCommand: this.executeCommand }), /*#__PURE__*/React.createElement("div", { style: { display: this.state.advanced ? 'none' : null } }, /*#__PURE__*/React.createElement("math-field", { style: { padding: '0.5em', overflow: 'auto', border: 'solid 1px', borderRadius: '4px' }, ref: this.handleFieldRef, "default-mode": "inline-math", "virtual-keyboard-mode": "off", "keypress-sound": "none", "plonk-sound": "none", "math-mode-space": " ", "data-testid": "math-field" })), /*#__PURE__*/React.createElement("div", { style: { display: this.state.advanced ? null : 'none' } }, /*#__PURE__*/React.createElement(TextArea, { style: { height: '5.1rem', overflowY: 'auto', lineHeight: '1.7rem' }, label: "", value: this.state.workingFormula, onChange: e => this.setState({ workingFormula: e.target.value }), ref: this.advancedEditor, "data-testid": "advanced-editor" })), /*#__PURE__*/React.createElement("div", { className: css(styles.latexToggle) }, /*#__PURE__*/React.createElement(Flex, null, /*#__PURE__*/React.createElement(Flex.Item, null, this.renderToggle()))), /*#__PURE__*/React.createElement("div", { style: { display: this.state.advanced ? null : 'none', marginTop: '1em' } }, /*#__PURE__*/React.createElement("span", { "data-testid": "mathml-preview-element", ref: this.previewElement, className: RCEGlobals.getFeatures()?.explicit_latex_typesetting ? MathJaxDirective.Process : null })))), /*#__PURE__*/React.createElement(Modal.Footer, null, this.renderFooter())); }; this.mathml = new Mathml(RCEGlobals.getFeatures(), RCEGlobals.getConfig()); } // **************** // // Helper functions // // **************** // insertNewRange() { const { editor, originalLatex } = this.props; const { startContainer, leftIndex, rightIndex } = originalLatex; if (!startContainer) return; const range = document.createRange(); range.setStart(startContainer, leftIndex); range.setEnd(startContainer, rightIndex); editor.selection.setRng(range); } advancedModeOnly(latex) { const normalizedLatex = latex.replace(/\s+/, ''); return containsAdvancedSyntax(normalizedLatex); } // ********* // // Callbacks // // ********* // setPreviewElementContent() { if (this.state.workingFormula) { this.renderMathInAdvancedPreview(); } else { this.previewElement.current.innerHTML = ''; } } // ******************* // // Rendering functions // // ******************* // componentDidMount() { this.registerBasicEditorListener(); this.setPreviewElementContent(); this.stubMacros(); if (!this.state.advanced) this.setMathField(this.state.workingFormula); this.insertNewRange(); } componentDidUpdate(prevProps, prevState) { if (this.state.workingFormula !== prevState.workingFormula) { this.setPreviewElementContent(); } } stubMacros() { // Mathlive's macros exist for a different use case; // we don't intend for our users to utilize them. // This effectively disables all of them to prevent // weird behaviors that users don't expect. this.mathField?.setOptions({ macros: {} }); } setMathField(formula) { this.mathField.setValue(formula); } getMathFiled() { return this.mathField.getValue(); } } EquationEditorModal.debounceRate = 1000; EquationEditorModal.propTypes = { editor: PropTypes.object.isRequired, onModalDismiss: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired, onEquationSubmit: PropTypes.func.isRequired, originalLatex: PropTypes.shape({ latex: PropTypes.string, advancedOnly: PropTypes.bool, startContainer: PropTypes.element, leftIndex: PropTypes.number, rightIndex: PropTypes.number }).isRequired, openAdvanced: PropTypes.bool.isRequired };