UNPKG

rich-text-editor

Version:
224 lines (216 loc) 9.63 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const jsx_runtime_1 = require("react/jsx-runtime"); const react_1 = require("react"); const react_dom_1 = require("react-dom"); const styled_components_1 = __importDefault(require("styled-components")); const dedupe_1 = __importDefault(require("classnames/dedupe")); // Removes duplicates in class list const state_1 = __importDefault(require("../../state")); const toolbar_1 = __importDefault(require("../toolbar")); const help_dialog_1 = require("../help-dialog"); const sanitization_1 = require("../../utils/sanitization"); const utility_1 = require("../../utility"); const use_keyboard_events_1 = require("../../hooks/use-keyboard-events"); const MainTextArea = (0, react_1.forwardRef)((props, ref) => { const { toolbarRoot, ariaInvalid, ariaLabelledBy, questionId, editorStyle, className, id, lang } = props; const editor = (0, state_1.default)(); const storedCaretPosition = (0, react_1.useRef)(0); (0, react_1.useImperativeHandle)(ref, () => ({ setValue: (value) => { if (editor.ref.current) { editor.ref.current.innerHTML = value; } setTimeout(() => { editor.initMathImages(); setTimeout(() => { editor.onAnswerChange(storedCaretPosition.current); }, 0); }, 0); }, })); const historyHandler = (fn) => () => { var _a; if (editor.ref.current !== document.activeElement) { return; } const oldValue = (_a = editor.ref.current) === null || _a === void 0 ? void 0 : _a.innerHTML; const fromHistory = fn(); if (fromHistory === undefined) { return; } const { content: newValue, newCaretPosition } = fromHistory; if (editor.ref.current && newValue !== oldValue) { editor.ref.current.innerHTML = newValue !== null && newValue !== void 0 ? newValue : ''; // TODO: Extract this into a function instead of pasting it all over the place setTimeout(() => { editor.initMathImages(); setTimeout(() => { if (editor.ref.current && newCaretPosition) { (0, utility_1.setCaretPosition)(editor.ref.current, newCaretPosition); } editor.onAnswerChange(newCaretPosition, false); }, 0); }, 0); } }; // Prevent browser's native undo/redo history use on MacOS, // as it would cause strange behaviour especially when mixed with our own implementation (0, use_keyboard_events_1.useKeyboardEventListener)('z', false, (e) => { if (e === null || e === void 0 ? void 0 : e.metaKey) { e.preventDefault(); e.stopPropagation(); } }, false); (0, use_keyboard_events_1.useKeyboardEventListener)('z', true, historyHandler(editor.undoEditor)); (0, use_keyboard_events_1.useKeyboardEventListener)('y', true, historyHandler(editor.redoEditor)); (0, use_keyboard_events_1.useKeyboardEventListener)('e', true, (e) => { if (editor.ref.current === document.activeElement) { e === null || e === void 0 ? void 0 : e.preventDefault(); editor.spawnMathEditorAtCursor(); } }); async function onPaste(e) { var _a; e.preventDefault(); e.stopPropagation(); const content = e.nativeEvent.clipboardData; if (!content) return; // We only allow pasting one image at a time, so we pick the last one const file = (_a = Array.from(content.items).at(-1)) === null || _a === void 0 ? void 0 : _a.getAsFile(); const html = content.getData('text/html'); const text = content.getData('text/plain'); const pasteType = file ? 'file' : html ? 'html' : 'text'; if (file && editor.allowedFileTypes.includes(file.type)) { try { const src = await editor.handlePastedImage(file); const img = document.createElement('img'); img.src = src; document.execCommand('insertHTML', false, (0, sanitization_1.sanitize)(img.outerHTML)); } catch (error) { console.error('Error while pasting a file', error); return; } } else if (html) { document.execCommand('insertHTML', false, (0, sanitization_1.sanitize)(html)); } else if (text) { document.execCommand('insertHTML', false, text); } /** setTimeout makes the callback run in the next (or later) loop of * the event loop. We use this to make sure that the we run these operations after * the innerHtml of the text field has already been updated */ setTimeout(() => { editor.initMathImages(); if (pasteType === 'html') { editor.persistValidImages(); } setTimeout(() => { editor.onAnswerChange(storedCaretPosition.current); }, 0); }, 0); } function onBlur(e) { // We don't want to hide the toolbar when it's the toolbar itself // that steals focus from the editor if (!(toolbarRoot === null || toolbarRoot === void 0 ? void 0 : toolbarRoot.contains(e.relatedTarget))) { editor.hideToolbar(); } } /* Prevent dragging content into and out of the editor. As content dragged into the editor would need its own sanitization logic etc., we just block drag and drop. The text field itself has the proper `onDragOver` & `onDrop` handlers, but for some reason it is still possible to drag content into the surrounding element and bypass those handlers entirely. */ (0, react_1.useEffect)(() => { const el = editor.ref.current; if (!el) return; const handleDrop = (e) => { if (e.inputType === 'insertFromDrop' || e.inputType === 'deleteByDrag') { e.preventDefault(); e.stopPropagation(); } }; el.addEventListener('beforeinput', handleDrop); return () => { el.removeEventListener('beforeinput', handleDrop); }; }, []); return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [toolbarRoot && editor.isToolbarOpen && (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)(toolbar_1.default, {}), toolbarRoot), editor.isHelpDialogOpen && (0, jsx_runtime_1.jsx)(help_dialog_1.HelpDialog, {}), (0, jsx_runtime_1.jsx)(Box, { ref: editor.ref, id: id, "aria-invalid": ariaInvalid, "aria-labelledby": ariaLabelledBy, "aria-multiline": true, className: (0, dedupe_1.default)('rich-text-editor answer', className), contentEditable: true, "data-question-id": questionId, "data-testid": "rich-text-editor", lang: lang, onBlur: onBlur, onFocus: editor.showToolbar, onInput: (e) => { const inputType = e.nativeEvent.inputType; if (inputType === 'historyUndo') { historyHandler(editor.undoEditor)(); e.preventDefault(); e.stopPropagation(); } else if (inputType === 'historyRedo') { historyHandler(editor.redoEditor)(); e.preventDefault(); e.stopPropagation(); } editor.onAnswerChange(storedCaretPosition.current); }, onKeyDown: (e) => { storedCaretPosition.current = (0, utility_1.getCaretPosition)(editor.ref.current); if (e.key.toLowerCase() === 'e' && e.ctrlKey) { e.preventDefault(); e.stopPropagation(); editor.spawnMathEditorAtCursor(); } }, onMouseDown: (_e) => { storedCaretPosition.current = (0, utility_1.getCaretPosition)(editor.ref.current); }, onPaste: onPaste, onDragOver: (e) => { e.preventDefault(); e.stopPropagation(); }, onDrop: (e) => { e.preventDefault(); e.stopPropagation(); }, spellCheck: false, style: editorStyle, role: "textbox" }), editor.mathEditorPortal !== null ? (0, jsx_runtime_1.jsx)(react_1.Fragment, { children: editor.mathEditorPortal[1] }) : null] })); }); const Box = styled_components_1.default.div ` box-sizing: border-box; border: 1px solid #aaa; min-height: 100px; padding: 5px; font: 17px Times New Roman; & img { padding: 5px; max-width: 100%; max-height: 1000px; vertical-align: middle; } &:focus > img { box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.2); } & .mq-math-mode .mq-root-block { white-space: nowrap; } &:focus, & .mq-editable-field.mq-focused, & textarea:focus { box-shadow: none; outline: 1px solid #359bb7; z-index: 2; } & img.equation { min-height: 20px; min-width: 20px; box-shadow: none; } &:focus img.equation { background-color: #edf9ff; } & img.equation.active { border: 2px solid #caedff; border-radius: 3px; } `; exports.default = MainTextArea;