This commit is contained in:
Víctor Losada Hernández
2026-08-23 20:16:10 +02:00
parent b655454348
commit ac30edffd6
+115 -121
View File
@@ -1,22 +1,22 @@
import "./editor.less"; import './editor.less';
import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle } from "react"; import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
import dedent from "dedent"; import dedent from 'dedent';
import CodeEditor from "@components/codeEditor/codeEditor.jsx"; import CodeEditor from '@components/codeEditor/codeEditor.jsx';
import SnippetBar from "./snippetbar/snippetbar.jsx"; import SnippetBar from './snippetbar/snippetbar.jsx';
import MetadataEditor from "./metadataEditor/metadataEditor.jsx"; import MetadataEditor from './metadataEditor/metadataEditor.jsx';
const EDITOR_THEME_KEY = "HB_editor_theme"; const EDITOR_THEME_KEY = 'HB_editor_theme';
import defaultCM5Theme from "@themes/codeMirror/default.js"; import defaultCM5Theme from '@themes/codeMirror/default.js';
import darkbrewery from "@themes/codeMirror/darkbrewery.js"; import darkbrewery from '@themes/codeMirror/darkbrewery.js';
import cm5Themes from "codemirror-5-themes"; import cm5Themes from 'codemirror-5-themes';
const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery }; const themes = { default: defaultCM5Theme, ...cm5Themes, darkbrewery };
const EditorThemes = Object.entries(themes) const EditorThemes = Object.entries(themes)
.filter(([name, value]) => Array.isArray(value) && !name.endsWith("Init") && !name.endsWith("Style")) .filter(([name, value])=>Array.isArray(value) && !name.endsWith('Init') && !name.endsWith('Style'))
.map(([name]) => name); .map(([name])=>name);
//const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; //const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
//const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/; //const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
@@ -43,14 +43,14 @@ const Editor = forwardRef(
{ {
brew = {}, brew = {},
onBrewChange = () => {}, onBrewChange = ()=>{},
reportError = () => {}, reportError = ()=>{},
onCursorPageChange = () => {}, onCursorPageChange = ()=>{},
onViewPageChange = () => {}, onViewPageChange = ()=>{},
editorTheme = "default", editorTheme = 'default',
renderer = "legacy", renderer = 'legacy',
moveBrew, moveBrew,
moveSource, moveSource,
@@ -67,53 +67,47 @@ const Editor = forwardRef(
currentBrewRendererPageNum = 1, currentBrewRendererPageNum = 1,
}, },
ref, ref,
) => { )=>{
const [currentEditorTheme, setEditorTheme] = useState(editorTheme); const [currentEditorTheme, setEditorTheme] = useState(editorTheme);
const [view, setView] = useState("text"); // 'text', 'style', 'meta', 'snippet' const [view, setView] = useState('text'); // 'text', 'style', 'meta', 'snippet'
const [snippetBarHeight, setSnippetBarHeight] = useState(26); const [snippetBarHeight, setSnippetBarHeight] = useState(26);
const editor = useRef(null); const editor = useRef(null);
const codeEditor = useRef(null); const codeEditor = useRef(null);
const throttleBrewMove = useRef(null); const throttleBrewMove = useRef(null);
const isText = () => { const isText = ()=>isView('text');
return view === "text"; const isStyle = ()=>isView('style');
}; const isMeta = ()=>isView('meta');
const isStyle = () => { const isSnip = ()=>isView('snippet');
return view === "style";
}; const isView = (name)=>view === name;
const isMeta = () => {
return view === "meta";
};
const isSnip = () => {
return view === "snippet";
};
//componentDidMount equivalent //componentDidMount equivalent
useEffect(() => { useEffect(()=>{
const brewRenderer = document.getElementById("BrewRenderer"); const brewRenderer = document.getElementById('BrewRenderer');
brewRenderer.onload = () => brewRenderer.contentDocument?.addEventListener("keydown", handleControlKeys); brewRenderer.onload = ()=>brewRenderer.contentDocument?.addEventListener('keydown', handleControlKeys);
document.addEventListener("keydown", handleControlKeys); document.addEventListener('keydown', handleControlKeys);
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY); const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
if (editorTheme && EditorThemes.includes(editorTheme)) { if(editorTheme && EditorThemes.includes(editorTheme)) {
setEditorTheme(editorTheme); setEditorTheme(editorTheme);
} else { } else {
setEditorTheme("default"); setEditorTheme('default');
} }
const snippetBar = document.querySelector(".editor > .snippetBar"); const snippetBar = document.querySelector('.editor > .snippetBar');
if (!snippetBar) return; if(!snippetBar) return;
const resizeObserver = new ResizeObserver((entries) => { const resizeObserver = new ResizeObserver((entries)=>{
const height = document.querySelector(".editor > .snippetBar").offsetHeight; const height = document.querySelector('.editor > .snippetBar').offsetHeight;
setSnippetBarHeight(height); setSnippetBarHeight(height);
}); });
resizeObserver.observe(snippetBar); resizeObserver.observe(snippetBar);
//ComponentWillUnmount equivalent //ComponentWillUnmount equivalent
return () => { return ()=>{
if (resizeObserver) resizeObserver.disconnect(); if(resizeObserver) resizeObserver.disconnect();
}; };
}, []); }, []);
@@ -126,23 +120,23 @@ const Editor = forwardRef(
}); });
//componentDidUpdate Equivalent //componentDidUpdate Equivalent
useEffect(() => { useEffect(()=>{
const prev = previousProps.current; const prev = previousProps.current;
if (prev.moveBrew !== moveBrew) { if(prev.moveBrew !== moveBrew) {
brewJump(); brewJump();
} }
if (prev.moveSource !== moveSource) { if(prev.moveSource !== moveSource) {
sourceJump(); sourceJump();
} }
if (liveScroll) { if(liveScroll) {
if (prev.currentBrewRendererPageNum !== currentBrewRendererPageNum) { if(prev.currentBrewRendererPageNum !== currentBrewRendererPageNum) {
sourceJump(currentBrewRendererPageNum, false); sourceJump(currentBrewRendererPageNum, false);
} else if (prev.currentEditorViewPageNum !== currentEditorViewPageNum) { } else if(prev.currentEditorViewPageNum !== currentEditorViewPageNum) {
brewJump(currentEditorViewPageNum, false); brewJump(currentEditorViewPageNum, false);
} else if (prev.currentEditorCursorPageNum !== currentEditorCursorPageNum) { } else if(prev.currentEditorCursorPageNum !== currentEditorCursorPageNum) {
brewJump(currentEditorCursorPageNum, false); brewJump(currentEditorCursorPageNum, false);
} }
} }
@@ -163,95 +157,95 @@ const Editor = forwardRef(
currentEditorCursorPageNum, currentEditorCursorPageNum,
]); ]);
const handleControlKeys = (e) => { const handleControlKeys = (e)=>{
if (!(e.ctrlKey && e.metaKey && e.shiftKey)) return; if(!(e.ctrlKey && e.metaKey && e.shiftKey)) return;
const LEFTARROW_KEY = 37; const LEFTARROW_KEY = 37;
const RIGHTARROW_KEY = 39; const RIGHTARROW_KEY = 39;
if (e.keyCode == RIGHTARROW_KEY) brewJump(); if(e.keyCode == RIGHTARROW_KEY) brewJump();
if (e.keyCode == LEFTARROW_KEY) sourceJump(); if(e.keyCode == LEFTARROW_KEY) sourceJump();
if (e.keyCode == LEFTARROW_KEY || e.keyCode == RIGHTARROW_KEY) { if(e.keyCode == LEFTARROW_KEY || e.keyCode == RIGHTARROW_KEY) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
} }
}; };
const updateCurrentCursorPage = (pageNumber) => { const updateCurrentCursorPage = (pageNumber)=>{
onCursorPageChange(pageNumber); onCursorPageChange(pageNumber);
}; };
const updateCurrentViewPage = (pageNumber) => { const updateCurrentViewPage = (pageNumber)=>{
onViewPageChange(pageNumber); onViewPageChange(pageNumber);
}; };
const handleInject = (injectText) => { const handleInject = (injectText)=>{
codeEditor.current?.injectText(injectText); codeEditor.current?.injectText(injectText);
}; };
const handleViewChange = (newView) => { const handleViewChange = (newView)=>{
setMoveArrows(newView === "text"); setMoveArrows(newView === 'text');
setView(newView); setView(newView);
}; };
useEffect(() => { useEffect(()=>{
codeEditor.current?.focus(); codeEditor.current?.focus();
}, [view]); }, [view]);
const brewJump = (targetPage = currentEditorCursorPageNum, smooth = true) => { const brewJump = (targetPage = currentEditorCursorPageNum, smooth = true)=>{
if (!window || !isText() || isJumping || jumpSource === "source") return; if(!window || !isText() || isJumping || jumpSource === 'source') return;
const brewRenderer = const brewRenderer =
window.frames["BrewRenderer"].contentDocument.getElementsByClassName("brewRenderer")[0]; window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0];
const currentPos = brewRenderer.scrollTop; const currentPos = brewRenderer.scrollTop;
const targetPos = window.frames["BrewRenderer"].contentDocument const targetPos = window.frames['BrewRenderer'].contentDocument
.getElementById(`p${targetPage}`) .getElementById(`p${targetPage}`)
.getBoundingClientRect().top; .getBoundingClientRect().top;
let scrollingTimeout; let scrollingTimeout;
const checkIfScrollComplete = () => { const checkIfScrollComplete = ()=>{
clearTimeout(scrollingTimeout); clearTimeout(scrollingTimeout);
scrollingTimeout = setTimeout(() => { scrollingTimeout = setTimeout(()=>{
isJumping = false; isJumping = false;
jumpSource = null; jumpSource = null;
brewRenderer.removeEventListener("scroll", checkIfScrollComplete); brewRenderer.removeEventListener('scroll', checkIfScrollComplete);
}, 150); }, 150);
}; };
isJumping = true; isJumping = true;
jumpSource = "brew"; jumpSource = 'brew';
checkIfScrollComplete(); checkIfScrollComplete();
brewRenderer.addEventListener("scroll", checkIfScrollComplete); brewRenderer.addEventListener('scroll', checkIfScrollComplete);
if (smooth) { if(smooth) {
const bouncePos = targetPos >= 0 ? -30 : 30; const bouncePos = targetPos >= 0 ? -30 : 30;
const bounceDelay = 100; const bounceDelay = 100;
const scrollDelay = 500; const scrollDelay = 500;
if (!throttleBrewMove.current) { if(!throttleBrewMove.current) {
throttleBrewMove.current = _.throttle( throttleBrewMove.current = _.throttle(
(currentPos, bouncePos, targetPos) => { (currentPos, bouncePos, targetPos)=>{
brewRenderer.scrollTo({ brewRenderer.scrollTo({
top: currentPos + bouncePos, top : currentPos + bouncePos,
behavior: "smooth", behavior : 'smooth',
}); });
setTimeout(() => { setTimeout(()=>{
brewRenderer.scrollTo({ brewRenderer.scrollTo({
top: currentPos + targetPos, top : currentPos + targetPos,
behavior: "smooth", behavior : 'smooth',
block: "start", block : 'start',
}); });
}, bounceDelay); }, bounceDelay);
}, },
scrollDelay, scrollDelay,
{ {
leading: true, leading : true,
trailing: false, trailing : false,
}, },
); );
} }
@@ -259,45 +253,45 @@ const Editor = forwardRef(
throttleBrewMove.current(currentPos, bouncePos, targetPos); throttleBrewMove.current(currentPos, bouncePos, targetPos);
} else { } else {
brewRenderer.scrollTo({ brewRenderer.scrollTo({
top: currentPos + targetPos, top : currentPos + targetPos,
behavior: "instant", behavior : 'instant',
block: "start", block : 'start',
}); });
} }
}; };
const sourceJump = (targetPage = currentBrewRendererPageNum, smooth = true) => { const sourceJump = (targetPage = currentBrewRendererPageNum, smooth = true)=>{
if (!isText() || isJumping || jumpSource === "brew") return; if(!isText() || isJumping || jumpSource === 'brew') return;
const editor = codeEditor.current; const editor = codeEditor.current;
if (!editor) return; if(!editor) return;
jumpSource = "source"; jumpSource = 'source';
editor.scrollToPage(targetPage); editor.scrollToPage(targetPage);
setTimeout(() => { setTimeout(()=>{
jumpSource = null; jumpSource = null;
}, 200); }, 200);
}; };
const updateEditorTheme = (newTheme) => { const updateEditorTheme = (newTheme)=>{
window.localStorage.setItem(EDITOR_THEME_KEY, newTheme); window.localStorage.setItem(EDITOR_THEME_KEY, newTheme);
setEditorTheme(newTheme); setEditorTheme(newTheme);
}; };
const renderEditor = () => { const renderEditor = ()=>{
if (isText()) { if(isText()) {
return ( return (
<> <>
<CodeEditor <CodeEditor
key="codeEditor" key='codeEditor'
ref={codeEditor} ref={codeEditor}
language="gfm" language='gfm'
tab="brewText" tab='brewText'
view={view} view={view}
value={brew.text} value={brew.text}
onChange={onBrewChange("text")} onChange={onBrewChange('text')}
onCursorChange={(page) => updateCurrentCursorPage(page)} onCursorChange={(page)=>updateCurrentCursorPage(page)}
onViewChange={(page) => updateCurrentViewPage(page)} onViewChange={(page)=>updateCurrentViewPage(page)}
editorTheme={currentEditorTheme} editorTheme={currentEditorTheme}
renderer={brew.renderer} renderer={brew.renderer}
style={{ height: `calc(100% - ${snippetBarHeight}px)` }} style={{ height: `calc(100% - ${snippetBarHeight}px)` }}
@@ -305,17 +299,17 @@ const Editor = forwardRef(
</> </>
); );
} }
if (isStyle()) { if(isStyle()) {
return ( return (
<> <>
<CodeEditor <CodeEditor
key="codeEditor" key='codeEditor'
ref={codeEditor} ref={codeEditor}
language="css" language='css'
tab="brewStyles" tab='brewStyles'
view={view} view={view}
value={brew.style ?? DEFAULT_STYLE_TEXT} value={brew.style ?? DEFAULT_STYLE_TEXT}
onChange={onBrewChange("style")} onChange={onBrewChange('style')}
editorTheme={currentEditorTheme} editorTheme={currentEditorTheme}
renderer={brew.renderer} renderer={brew.renderer}
style={{ height: `calc(100% - ${snippetBarHeight}px)` }} style={{ height: `calc(100% - ${snippetBarHeight}px)` }}
@@ -323,34 +317,34 @@ const Editor = forwardRef(
</> </>
); );
} }
if (isMeta()) { if(isMeta()) {
return ( return (
<> <>
<CodeEditor key="codeEditor" view={view} style={{ display: "none" }} /> <CodeEditor key='codeEditor' view={view} style={{ display: 'none' }} />
<MetadataEditor <MetadataEditor
metadata={brew} metadata={brew}
themeBundle={themeBundle} themeBundle={themeBundle}
onChange={onBrewChange("metadata")} onChange={onBrewChange('metadata')}
reportError={reportError} reportError={reportError}
userThemes={userThemes} userThemes={userThemes}
/> />
</> </>
); );
} }
if (isSnip()) { if(isSnip()) {
if (!brew.snippets) { if(!brew.snippets) {
brew.snippets = DEFAULT_SNIPPET_TEXT; brew.snippets = DEFAULT_SNIPPET_TEXT;
} }
return ( return (
<> <>
<CodeEditor <CodeEditor
key="codeEditor" key='codeEditor'
ref={codeEditor} ref={codeEditor}
language="gfm" language='gfm'
tab="brewSnippets" tab='brewSnippets'
view={view} view={view}
value={brew.snippets} value={brew.snippets}
onChange={onBrewChange("snippets")} onChange={onBrewChange('snippets')}
enableFolding={true} enableFolding={true}
editorTheme={currentEditorTheme} editorTheme={currentEditorTheme}
renderer={brew.renderer} renderer={brew.renderer}
@@ -360,29 +354,29 @@ const Editor = forwardRef(
); );
} }
}; };
const redo = () => { const redo = ()=>{
return codeEditor.current?.redo(); return codeEditor.current?.redo();
}; };
const historySize = () => { const historySize = ()=>{
return codeEditor.current?.historySize(); return codeEditor.current?.historySize();
}; };
const undo = () => { const undo = ()=>{
return codeEditor.current?.undo(); return codeEditor.current?.undo();
}; };
const foldCode = () => { const foldCode = ()=>{
return codeEditor.current?.foldAll(); return codeEditor.current?.foldAll();
}; };
const unfoldCode = () => { const unfoldCode = ()=>{
return codeEditor.current?.unfoldAll(); return codeEditor.current?.unfoldAll();
}; };
const update = () => {}; const update = ()=>{};
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, ()=>({
update, update,
undo, undo,
redo, redo,
@@ -392,7 +386,7 @@ const Editor = forwardRef(
})); }));
return ( return (
<div className="editor" ref={editor}> <div className='editor' ref={editor}>
<SnippetBar <SnippetBar
brew={brew} brew={brew}
view={view} view={view}