rich-text-editor
Version:
Rich text editor
215 lines (207 loc) • 9.22 kB
JavaScript
;
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 shortcuts_1 = require("../../utils/shortcuts");
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);
}
};
(0, use_keyboard_events_1.useKeyboardEventListener)([
{ keyMatch: (event) => (0, shortcuts_1.isMatch)(event, shortcuts_1.undoShortcut), fn: historyHandler(editor.undoEditor) },
{ keyMatch: (event) => (0, shortcuts_1.isMatch)(event, shortcuts_1.redoShortcut), fn: historyHandler(editor.redoEditor) },
]);
async function onPaste(e) {
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 = Array.from(content.items)
.map((item) => item.getAsFile())
.filter((file) => file !== null)
.at(-1);
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, (0, sanitization_1.sanitizeText)(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(async () => {
editor.initMathImages();
if (pasteType === 'html') {
await 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;