import "./editor.less"; import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle } from "react"; import dedent from "dedent"; import CodeEditor from "@components/codeEditor/codeEditor.jsx"; import SnippetBar from "./snippetbar/snippetbar.jsx"; import MetadataEditor from "./metadataEditor/metadataEditor.jsx"; const EDITOR_THEME_KEY = "HB_editor_theme"; import defaultCM5Theme from "@themes/codeMirror/default.js"; import darkbrewery from "@themes/codeMirror/darkbrewery.js"; import cm5Themes from "codemirror-5-themes"; const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery }; const EditorThemes = Object.entries(themes) .filter(([name, value]) => Array.isArray(value) && !name.endsWith("Init") && !name.endsWith("Style")) .map(([name]) => name); //const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; //const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/; const DEFAULT_STYLE_TEXT = dedent` /*=======--- Example CSS styling ---=======*/ /* Any CSS here will apply to your document! */ .myExampleClass { color: black; }`; const DEFAULT_SNIPPET_TEXT = dedent` \snippet example snippet The text between \`\snippet title\` lines will become a snippet of name \`title\` as this example provides. This snippet is accessible in the brew tab, and will be inherited if the brew is used as a theme. `; let isJumping = false; let jumpSource = null; const Editor = forwardRef( ( { brew = {}, onBrewChange = () => {}, reportError = () => {}, onCursorPageChange = () => {}, onViewPageChange = () => {}, editorTheme = "default", renderer = "legacy", moveBrew, moveSource, liveScroll, setMoveArrows, updateBrew, showEditButtons, themeBundle, userThemes, currentEditorCursorPageNum = 1, currentEditorViewPageNum = 1, currentBrewRendererPageNum = 1, }, ref, ) => { const [currentEditorTheme, setEditorTheme] = useState(editorTheme); const [view, setView] = useState("text"); // 'text', 'style', 'meta', 'snippet' const [snippetBarHeight, setSnippetBarHeight] = useState(26); const editor = useRef(null); const codeEditor = useRef(null); const throttleBrewMove = useRef(null); const isText = () => { return view === "text"; }; const isStyle = () => { return view === "style"; }; const isMeta = () => { return view === "meta"; }; const isSnip = () => { return view === "snippet"; }; //componentDidMount equivalent useEffect(() => { const brewRenderer = document.getElementById("BrewRenderer"); brewRenderer.onload = () => brewRenderer.contentDocument?.addEventListener("keydown", handleControlKeys); document.addEventListener("keydown", handleControlKeys); const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY); if (editorTheme && EditorThemes.includes(editorTheme)) { setEditorTheme(editorTheme); } else { setEditorTheme("default"); } const snippetBar = document.querySelector(".editor > .snippetBar"); if (!snippetBar) return; const resizeObserver = new ResizeObserver((entries) => { const height = document.querySelector(".editor > .snippetBar").offsetHeight; setSnippetBarHeight(height); }); resizeObserver.observe(snippetBar); //ComponentWillUnmount equivalent return () => { if (resizeObserver) resizeObserver.disconnect(); }; }, []); const previousProps = useRef({ moveBrew, moveSource, currentBrewRendererPageNum, currentEditorViewPageNum, currentEditorCursorPageNum, }); //componentDidUpdate Equivalent useEffect(() => { const prev = previousProps.current; if (prev.moveBrew !== moveBrew) { brewJump(); } if (prev.moveSource !== moveSource) { sourceJump(); } if (liveScroll) { if (prev.currentBrewRendererPageNum !== currentBrewRendererPageNum) { sourceJump(currentBrewRendererPageNum, false); } else if (prev.currentEditorViewPageNum !== currentEditorViewPageNum) { brewJump(currentEditorViewPageNum, false); } else if (prev.currentEditorCursorPageNum !== currentEditorCursorPageNum) { brewJump(currentEditorCursorPageNum, false); } } previousProps.current = { moveBrew, moveSource, currentBrewRendererPageNum, currentEditorViewPageNum, currentEditorCursorPageNum, }; }, [ moveBrew, moveSource, liveScroll, currentBrewRendererPageNum, currentEditorViewPageNum, currentEditorCursorPageNum, ]); const handleControlKeys = (e) => { if (!(e.ctrlKey && e.metaKey && e.shiftKey)) return; const LEFTARROW_KEY = 37; const RIGHTARROW_KEY = 39; if (e.keyCode == RIGHTARROW_KEY) brewJump(); if (e.keyCode == LEFTARROW_KEY) sourceJump(); if (e.keyCode == LEFTARROW_KEY || e.keyCode == RIGHTARROW_KEY) { e.stopPropagation(); e.preventDefault(); } }; const updateCurrentCursorPage = (pageNumber) => { onCursorPageChange(pageNumber); }; const updateCurrentViewPage = (pageNumber) => { onViewPageChange(pageNumber); }; const handleInject = (injectText) => { codeEditor.current?.injectText(injectText); }; const handleViewChange = (newView) => { setMoveArrows(newView === "text"); setView(newView); }; useEffect(() => { codeEditor.current?.focus(); }, [view]); const brewJump = (targetPage = currentEditorCursorPageNum, smooth = true) => { if (!window || !isText() || isJumping || jumpSource === "source") return; const brewRenderer = window.frames["BrewRenderer"].contentDocument.getElementsByClassName("brewRenderer")[0]; const currentPos = brewRenderer.scrollTop; const targetPos = window.frames["BrewRenderer"].contentDocument .getElementById(`p${targetPage}`) .getBoundingClientRect().top; let scrollingTimeout; const checkIfScrollComplete = () => { clearTimeout(scrollingTimeout); scrollingTimeout = setTimeout(() => { isJumping = false; jumpSource = null; brewRenderer.removeEventListener("scroll", checkIfScrollComplete); }, 150); }; isJumping = true; jumpSource = "brew"; checkIfScrollComplete(); brewRenderer.addEventListener("scroll", checkIfScrollComplete); if (smooth) { const bouncePos = targetPos >= 0 ? -30 : 30; const bounceDelay = 100; const scrollDelay = 500; if (!throttleBrewMove.current) { throttleBrewMove.current = _.throttle( (currentPos, bouncePos, targetPos) => { brewRenderer.scrollTo({ top: currentPos + bouncePos, behavior: "smooth", }); setTimeout(() => { brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: "smooth", block: "start", }); }, bounceDelay); }, scrollDelay, { leading: true, trailing: false, }, ); } throttleBrewMove.current(currentPos, bouncePos, targetPos); } else { brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: "instant", block: "start", }); } }; const sourceJump = (targetPage = currentBrewRendererPageNum, smooth = true) => { if (!isText() || isJumping || jumpSource === "brew") return; const editor = codeEditor.current; if (!editor) return; jumpSource = "source"; editor.scrollToPage(targetPage); setTimeout(() => { jumpSource = null; }, 200); }; const updateEditorTheme = (newTheme) => { window.localStorage.setItem(EDITOR_THEME_KEY, newTheme); setEditorTheme(newTheme); }; const renderEditor = () => { if (isText()) { return ( <> updateCurrentCursorPage(page)} onViewChange={(page) => updateCurrentViewPage(page)} editorTheme={currentEditorTheme} renderer={brew.renderer} style={{ height: `calc(100% - ${snippetBarHeight}px)` }} /> ); } if (isStyle()) { return ( <> ); } if (isMeta()) { return ( <> ); } if (isSnip()) { if (!brew.snippets) { brew.snippets = DEFAULT_SNIPPET_TEXT; } return ( <> ); } }; const redo = () => { return codeEditor.current?.redo(); }; const historySize = () => { return codeEditor.current?.historySize(); }; const undo = () => { return codeEditor.current?.undo(); }; const foldCode = () => { return codeEditor.current?.foldAll(); }; const unfoldCode = () => { return codeEditor.current?.unfoldAll(); }; const update = () => {}; useImperativeHandle(ref, () => ({ update, undo, redo, foldCode, unfoldCode, historySize, })); return (
{renderEditor()}
); }, ); export default Editor;