UNPKG

rich-text-editor

Version:
307 lines (306 loc) 15.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = useEditorState; exports.EditorStateProvider = EditorStateProvider; const jsx_runtime_1 = require("react/jsx-runtime"); const react_1 = require("react"); const history_1 = __importDefault(require("./history")); const math_editor_1 = __importDefault(require("../components/math-editor")); const create_math_stub_1 = require("../utils/create-math-stub"); const FI_1 = __importDefault(require("../../FI")); const SV_1 = __importDefault(require("../../SV")); const react_dom_1 = require("react-dom"); const utility_1 = require("../utility"); const editorCtx = (0, react_1.createContext)(null); function useEditorState() { const ctx = (0, react_1.useContext)(editorCtx); if (!ctx) throw Error('Tried to use Editor State Context outside a Provider'); return ctx; } let nextKey = 0; const getNextKey = () => { const next = nextKey; nextKey = next + 1; return next; }; const defaultPasteSource = (file) => new Promise((resolve) => { const reader = new FileReader(); reader.onload = (evt) => resolve(reader.result); reader.readAsDataURL(file); }); const defaultOnLatexUpdate = (baseUrl) => (img, latex) => { img.setAttribute('src', `${baseUrl}/math.svg?latex=${encodeURIComponent(latex)}`); img.setAttribute('alt', latex); }; const setCursorAroundElement = (element, position = 'after') => { const selection = window.getSelection(); const range = document.createRange(); if (position === 'before') { range.setStartBefore(element); } else { range.setStartAfter(element); } range.collapse(true); selection === null || selection === void 0 ? void 0 : selection.removeAllRanges(); selection === null || selection === void 0 ? void 0 : selection.addRange(range); }; function EditorStateProvider({ children, language = 'FI', getPasteSource = defaultPasteSource, allowedFileTypes = ['image/png', 'image/jpeg'], invalidImageSelector = 'img:not(img[src^="data"], img[src^="/math.svg?latex="], img[src^="/screenshot/"])', onValueChange = () => { }, initialValue = '', baseUrl = '', onLatexUpdate: _onLatexUpdate = defaultOnLatexUpdate(baseUrl), }) { var _a; const [isToolbarOpen, setIsToolbarOpen] = (0, react_1.useState)(false); const [isMathToolbarOpen, setIsMathToolbarOpen] = (0, react_1.useState)(false); const [isToolbarExpanded, setIsToolbarExpanded] = (0, react_1.useState)(false); const [isHelpDialogOpen, setIsHelpDialogOpen] = (0, react_1.useState)(false); const [activeMathEditor, setActiveMathEditor] = (0, react_1.useState)(null); const [hasBeenInitialized, setHasBeenInitialized] = (0, react_1.useState)(false); /** url search parameter for dev use * `?forceToolbars=1` to force basic toolbar to stay open * `?forceToolbars=2` to force basic and math toolbars to stay open * NOTE: Using this will likely cause things to break, as this is for debug/dev reasons * */ const forceToolbarsOpen = (_a = new URL(window.location.href).searchParams.get('forceToolbars')) !== null && _a !== void 0 ? _a : '0'; const [mathEditorPortal, setMathEditorPortal] = (0, react_1.useState)(null); const equationEditorHistory = (0, history_1.default)(); const mainTextAreaHistory = (0, history_1.default)(); const mainTextAreaRef = (0, react_1.useRef)(null); const t = { FI: FI_1.default, SV: SV_1.default }[language]; /* Sometimes blur events cause multiple onBlur calls (at least in Chrome). Removing elements can then * cause uncaught errors, which require no actions to be taken.*/ function safeRemove(e) { try { e === null || e === void 0 ? void 0 : e.remove(); } catch (_) { /* No action needed.*/ } } function spawnMathEditor(stub, image, props) { // This is called both on the creation of the component and each time the equation is opened after that function onOpen(handle) { equationEditorHistory.clear(); setActiveMathEditor(handle); setIsToolbarOpen(true); setIsMathToolbarOpen(true); image.classList.add('active'); } function onBlur(latex, forceCursorPosition) { equationEditorHistory.clear(); setActiveMathEditor(null); setIsMathToolbarOpen(false); setIsToolbarOpen(false); onAnswerChange((0, utility_1.getCaretPositionAfterElement)(mainTextAreaRef.current, image), true, true); if (forceCursorPosition) { setCursorAroundElement(image, forceCursorPosition); } safeRemove(stub); if (!latex) { safeRemove(image); } else image.classList.remove('active'); } function onChange(latex) { equationEditorHistory.write(latex); onAnswerChange((0, utility_1.getCaretPositionAfterElement)(mainTextAreaRef.current, image), false, false); } function onEnter(latex) { if (image) { spawnMathEditorInNewLine(image); } onBlur(latex); } const portal = (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)(math_editor_1.default, Object.assign({ onOpen: onOpen, onBlur: onBlur, onChange: onChange, onEnter: onEnter, errorText: t.editor.render_error }, props)), stub); setMathEditorPortal([stub, portal]); } function onLatexUpdate(img, latex) { img.setAttribute('data-latex', latex); _onLatexUpdate(img, latex); } function onMathImageClick(img, e) { var _a; const parent = img.parentElement; e.stopPropagation(); e.preventDefault(); const stub = (0, create_math_stub_1.createMathStub)(getNextKey()); if (parent) { parent.insertBefore(stub, img.nextSibling); } else { (_a = mainTextAreaRef.current) === null || _a === void 0 ? void 0 : _a.appendChild(stub); } spawnMathEditor(stub, img, { initialLatex: img.getAttribute('data-latex') || img.getAttribute('alt'), onLatexUpdate: (latex) => onLatexUpdate(img, latex), }); } function createMathImage() { const mathImage = document.createElement('img'); mathImage.addEventListener('click', (e) => onMathImageClick(mathImage, e)); mathImage.setAttribute('data-math-image', ''); mathImage.setAttribute('initialized', ''); mathImage.setAttribute('src', ''); // Browsers add a border to images without a source attribute mathImage.classList.add('equation'); return mathImage; } function spawnMathEditorAtCursor() { const mathImage = createMathImage(); spawnMathEditor((0, create_math_stub_1.createMathStub)(getNextKey(), true, mathImage), mathImage, { onLatexUpdate: (latex) => onLatexUpdate(mathImage, latex), }); } function spawnMathEditorInNewLine(afterElement) { // Find the closest div. This is necessary because the browser sometimes wraps lines in <div>s automatically, // so we don't know whether the parent is the main text area or a random div inserted by the browser. const parent = afterElement.closest('div'); const mathImage = createMathImage(); const newStub = (0, create_math_stub_1.createMathStub)(getNextKey(), false, mathImage); const nextSibling = afterElement.nextSibling; // Ensure mainTextAreaRef exists before inserting if (parent) { parent.insertBefore(document.createElement('br'), nextSibling); parent.insertBefore(mathImage, nextSibling); parent.insertBefore(newStub, nextSibling); spawnMathEditor(newStub, mathImage, { onLatexUpdate: (latex) => onLatexUpdate(mathImage, latex) }); } else { console.error('parent element not found for math editor'); } } function initMathImages() { if (mainTextAreaRef.current) { const selector = ['src*="/math.svg?"', 'src^="data:image/svg+xml"', 'data-math-image'] .map((attr) => `img[${attr}][alt]:not([initialized])`) .join(', '); Array.from(mainTextAreaRef.current.querySelectorAll(selector)).forEach((oldImage) => { var _a, _b; const mathImage = createMathImage(); const src = oldImage.getAttribute('src'); if (src) { const { origin, pathname, search, protocol } = new URL(src, baseUrl || document.location.toString()); if (protocol !== 'data:' && origin !== baseUrl) { mathImage.setAttribute('src', `${baseUrl}${pathname}${search}`); } else { mathImage.setAttribute('src', src); } } mathImage.setAttribute('alt', (_a = oldImage.getAttribute('alt')) !== null && _a !== void 0 ? _a : ''); mathImage.setAttribute('data-latex', (_b = oldImage.getAttribute('data-latex')) !== null && _b !== void 0 ? _b : ''); oldImage.replaceWith(mathImage); }); } } async function persistValidImages() { var _a, _b, _c; (_a = mainTextAreaRef.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll(invalidImageSelector).forEach((e) => e.remove()); const images = Array.from((_c = (_b = mainTextAreaRef.current) === null || _b === void 0 ? void 0 : _b.querySelectorAll('img[src^="data:image/')) !== null && _c !== void 0 ? _c : []); const imagesWithFile = images.flatMap((e) => { const src = e.getAttribute('src'); if (src) { const file = (0, utility_1.decodeBase64Image)(src); if (file) { return [{ file, element: e }]; } } return []; }); await Promise.all(imagesWithFile.map(async (img) => { if (img.element instanceof HTMLImageElement && (0, utility_1.isForbiddenInlineImage)(img.file.type, img.element, allowedFileTypes)) { img.element.remove(); } img.element.setAttribute('src', utility_1.loadingImage); const url = await getPasteSource(new File([img.file.data], 'image', { type: img.file.type })); img.element.setAttribute('src', url); })); } const updateAnswerHistory = (content, caretPositionBefore) => { if (mainTextAreaRef.current) { mainTextAreaHistory.write(content, caretPositionBefore, (0, utility_1.getCaretPosition)(mainTextAreaRef.current)); } }; const updateAnswerHistoryDebounced = (0, utility_1.debounceAnswerSave)((content, caretPositionBefore) => updateAnswerHistory(content, caretPositionBefore), 500); /** * @param shouldUpdateHistory - Whether to update the answer history (defaults to true) * @param shouldUpdateHistoryImmediately - Whether to update the answer history immediately, without debouncing (defaults to false) */ function onAnswerChange(caretPosition = 0, shouldUpdateHistory = true, shouldUpdateHistoryImmediately = false) { const fn = () => { var _a; const content = (_a = mainTextAreaRef.current) === null || _a === void 0 ? void 0 : _a.innerHTML; if (content !== undefined) { const answer = (0, utility_1.getAnswer)(content); onValueChange(answer); if (shouldUpdateHistory) { if (shouldUpdateHistoryImmediately) { updateAnswerHistory(answer.answerHtml, caretPosition); } else { updateAnswerHistoryDebounced(answer.answerHtml, caretPosition); } } } }; /** This 0ms timeout is crucial - it essentially moves the callback into the next event loop, * after pending DOM changes have been made in the current loop (in practice, * this is needed because closing a math editor changes the HTML of the answer, * but doesn't trigger the text area's onInput event. With this hack we can get the answer HTML with * the changes already included.) */ setTimeout(fn, 0); } /** Initialization */ (0, react_1.useEffect)(() => { if (!hasBeenInitialized && mainTextAreaRef.current !== null) { if (initialValue) { mainTextAreaRef.current.innerHTML = initialValue; } setTimeout(() => { initMathImages(); setHasBeenInitialized(true); if (mainTextAreaRef.current) { mainTextAreaHistory.write(mainTextAreaRef.current.innerHTML); } }, 0); } }, [hasBeenInitialized, initialValue, mainTextAreaRef.current, mainTextAreaHistory]); return ((0, jsx_runtime_1.jsx)(editorCtx.Provider, { value: { isToolbarOpen: forceToolbarsOpen !== '0' || isToolbarOpen, isMathToolbarOpen: forceToolbarsOpen === '2' || isMathToolbarOpen, showToolbar: () => setIsToolbarOpen(true), hideToolbar: () => setIsToolbarOpen(false), isToolbarExpanded, expandToolbar: () => setIsToolbarExpanded(true), collapseToolbar: () => setIsToolbarExpanded(false), isHelpDialogOpen, showHelpDialog: () => setIsHelpDialogOpen(true), hideHelpDialog: () => setIsHelpDialogOpen(false), activeMathEditor: activeMathEditor, setActiveMathEditor: setActiveMathEditor, ref: mainTextAreaRef, mathEditorPortal: mathEditorPortal, spawnMathEditorAtCursor: spawnMathEditorAtCursor, spawnMathEditorInNewLine: spawnMathEditorInNewLine, initMathImages, persistValidImages, canUndoEquation: equationEditorHistory.canUndo, canRedoEquation: equationEditorHistory.canRedo, undoEquation: () => { var _a; return (_a = equationEditorHistory.undo()) === null || _a === void 0 ? void 0 : _a.content; }, redoEquation: () => { var _a; return (_a = equationEditorHistory.redo()) === null || _a === void 0 ? void 0 : _a.content; }, canUndoEditor: mainTextAreaHistory.canUndo, canRedoEditor: mainTextAreaHistory.canRedo, undoEditor: mainTextAreaHistory.undo, redoEditor: mainTextAreaHistory.redo, t, handlePastedImage: getPasteSource, allowedFileTypes, invalidImageSelector, onAnswerChange, initialValue, baseUrl, }, children: children })); }