From f53ed8c8e7ea120e25cc22971923bc3de402fb5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 23 Aug 2026 19:52:11 +0200 Subject: [PATCH] this is mostly working --- client/homebrew/editor/editor.jsx | 635 +++++++++++++++++------------- 1 file changed, 354 insertions(+), 281 deletions(-) diff --git a/client/homebrew/editor/editor.jsx b/client/homebrew/editor/editor.jsx index f500bebeb..66e6aaadf 100644 --- a/client/homebrew/editor/editor.jsx +++ b/client/homebrew/editor/editor.jsx @@ -1,29 +1,22 @@ -/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/ -import './editor.less'; -import React from 'react'; -import createReactClass from 'create-react-class'; -import _ from 'lodash'; -import dedent from 'dedent'; +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'; +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'; +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'; +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); - + .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\ .*$/; @@ -45,306 +38,386 @@ const DEFAULT_SNIPPET_TEXT = dedent` let isJumping = false; let jumpSource = null; -const Editor = createReactClass({ - displayName : 'Editor', - getDefaultProps : function() { - return { - brew : { - text : '', - style : '' - }, +const Editor = forwardRef( + ( + { + brew = {}, - onBrewChange : ()=>{}, - reportError : ()=>{}, + onBrewChange = () => {}, + reportError = () => {}, - onCursorPageChange : ()=>{}, - onViewPageChange : ()=>{}, + onCursorPageChange = () => {}, + onViewPageChange = () => {}, - editorTheme : 'default', - renderer : 'legacy', + editorTheme = "default", + renderer = "legacy", - currentEditorCursorPageNum : 1, - currentEditorViewPageNum : 1, - currentBrewRendererPageNum : 1, + 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"; }; - }, - getInitialState : function() { - return { - editorTheme : this.props.editorTheme, - view : 'text', //'text', 'style', 'meta', 'snippet' - snippetBarHeight : 26, + const isStyle = () => { + return view === "style"; + }; + const isMeta = () => { + return view === "meta"; + }; + const isSnip = () => { + return view === "snippet"; }; - }, - editor : React.createRef(null), - codeEditor : React.createRef(null), + //componentDidMount equivalent + useEffect(() => { + const brewRenderer = document.getElementById("BrewRenderer"); + brewRenderer.onload = () => brewRenderer.contentDocument?.addEventListener("keydown", handleControlKeys); + document.addEventListener("keydown", handleControlKeys); - isText : function() {return this.state.view == 'text';}, - isStyle : function() {return this.state.view == 'style';}, - isMeta : function() {return this.state.view == 'meta';}, - isSnip : function() {return this.state.view == 'snippet';}, - - componentDidMount : function() { - - const brewRenderer = document.getElementById('BrewRenderer'); - brewRenderer.onload = ()=>brewRenderer.contentDocument?.addEventListener('keydown', this.handleControlKeys); - document.addEventListener('keydown', this.handleControlKeys); - - const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY); - if(editorTheme && EditorThemes.includes(editorTheme)) { - this.setState({ editorTheme }); - } else { - this.setState({ editorTheme: 'default' }); - } - const snippetBar = document.querySelector('.editor > .snippetBar'); - if(!snippetBar) return; - - this.resizeObserver = new ResizeObserver((entries)=>{ - const height = document.querySelector('.editor > .snippetBar').offsetHeight; - this.setState({ snippetBarHeight: height }); - }); - - this.resizeObserver.observe(snippetBar); - }, - - componentDidUpdate : function(prevProps, prevState, snapshot) { - - if(prevProps.moveBrew !== this.props.moveBrew) - this.brewJump(); - - if(prevProps.moveSource !== this.props.moveSource) - this.sourceJump(); - - if(this.props.liveScroll) { - if(prevProps.currentBrewRendererPageNum !== this.props.currentBrewRendererPageNum) { - this.sourceJump(this.props.currentBrewRendererPageNum, false); - } else if(prevProps.currentEditorViewPageNum !== this.props.currentEditorViewPageNum) { - this.brewJump(this.props.currentEditorViewPageNum, false); - } else if(prevProps.currentEditorCursorPageNum !== this.props.currentEditorCursorPageNum) { - this.brewJump(this.props.currentEditorCursorPageNum, false); + 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; - componentWillUnmount() { - if(this.resizeObserver) this.resizeObserver.disconnect(); - }, + const resizeObserver = new ResizeObserver((entries) => { + const height = document.querySelector(".editor > .snippetBar").offsetHeight; + setSnippetBarHeight(height); + }); - handleControlKeys : function(e){ - if(!(e.ctrlKey && e.metaKey && e.shiftKey)) return; - const LEFTARROW_KEY = 37; - const RIGHTARROW_KEY = 39; - if(e.keyCode == RIGHTARROW_KEY) this.brewJump(); - if(e.keyCode == LEFTARROW_KEY) this.sourceJump(); - if(e.keyCode == LEFTARROW_KEY || e.keyCode == RIGHTARROW_KEY) { - e.stopPropagation(); - e.preventDefault(); - } - }, + resizeObserver.observe(snippetBar); - updateCurrentCursorPage : function(pageNumber) { - this.props.onCursorPageChange(pageNumber); - }, + //ComponentWillUnmount equivalent + return () => { + if (resizeObserver) resizeObserver.disconnect(); + }; + }, []); - updateCurrentViewPage : function(pageNumber) { - this.props.onViewPageChange(pageNumber); - }, - - handleInject : function(injectText){ - this.codeEditor.current?.injectText(injectText); - }, - - handleViewChange : function(newView){ - this.props.setMoveArrows(newView === 'text'); - - this.setState({ - view : newView - }, ()=>{ - this.codeEditor.current?.focus(); + const previousProps = useRef({ + moveBrew, + moveSource, + currentBrewRendererPageNum, + currentEditorViewPageNum, + currentEditorCursorPageNum, }); - }, - brewJump : function(targetPage=this.props.currentEditorCursorPageNum, smooth=true){ - if(!window || !this.isText() || isJumping || jumpSource === 'source') - return; + //componentDidUpdate Equivalent + useEffect(() => { + const prev = previousProps.current; - // Get current brewRenderer scroll position and calculate target position - const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0]; - const currentPos = brewRenderer.scrollTop; - const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top; + if (prev.moveBrew !== moveBrew) { + brewJump(); + } - let scrollingTimeout; - const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times - clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs - scrollingTimeout = setTimeout(()=>{ - isJumping = false; - jumpSource = null; - brewRenderer.removeEventListener('scroll', checkIfScrollComplete); - }, 150); // If 150 ms pass without a brewRenderer scroll event, assume scrolling is done + 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); }; - isJumping = true; - jumpSource = 'brew'; - checkIfScrollComplete(); - brewRenderer.addEventListener('scroll', checkIfScrollComplete); + const updateCurrentViewPage = (pageNumber) => { + onViewPageChange(pageNumber); + }; - if(smooth) { - const bouncePos = targetPos >= 0 ? -30 : 30; //Do a little bounce before scrolling - const bounceDelay = 100; - const scrollDelay = 500; + const handleInject = (injectText) => { + codeEditor.current?.injectText(injectText); + }; - if(!this.throttleBrewMove) { - this.throttleBrewMove = _.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 }); + 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); }; - this.throttleBrewMove(currentPos, bouncePos, targetPos); - } else { - brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'instant', block: 'start' }); - } - }, - sourceJump : function(targetPage=this.props.currentBrewRendererPageNum, smooth=true){ - if(!this.isText() || isJumping || jumpSource === 'brew') - return; + isJumping = true; + jumpSource = "brew"; - const editor = this.codeEditor.current; - if(!editor) return; - jumpSource = 'source'; + checkIfScrollComplete(); - editor.scrollToPage(targetPage); - setTimeout(()=>{ - jumpSource = null; - }, 200); - }, + brewRenderer.addEventListener("scroll", checkIfScrollComplete); - //Called when there are changes to the editor's dimensions - update : function(){}, + if (smooth) { + const bouncePos = targetPos >= 0 ? -30 : 30; + const bounceDelay = 100; + const scrollDelay = 500; - updateEditorTheme : function(newTheme){ - window.localStorage.setItem(EDITOR_THEME_KEY, newTheme); - this.setState({ - editorTheme : newTheme - }); - }, + if (!throttleBrewMove.current) { + throttleBrewMove.current = _.throttle( + (currentPos, bouncePos, targetPos) => { + brewRenderer.scrollTo({ + top: currentPos + bouncePos, + behavior: "smooth", + }); - //Called by CodeEditor after document switch, so Snippetbar can refresh UndoHistory - rerenderParent : function (){ - this.forceUpdate(); - }, + setTimeout(() => { + brewRenderer.scrollTo({ + top: currentPos + targetPos, + behavior: "smooth", + block: "start", + }); + }, bounceDelay); + }, + scrollDelay, + { + leading: true, + trailing: false, + }, + ); + } - renderEditor : function(){ - if(this.isText()){ - return <> - this.updateCurrentCursorPage(page)} - onViewChange={(page)=>this.updateCurrentViewPage(page)} - editorTheme={this.state.editorTheme} - renderer={this.props.brew.renderer} - style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }}/> - ; - } - if(this.isStyle()){ - return <> - - ; - } - if(this.isMeta()){ - return <> - - - ; - } - if(this.isSnip()){ - if(!this.props.brew.snippets) { this.props.brew.snippets = DEFAULT_SNIPPET_TEXT; } - return <> - - ; - } - }, + throttleBrewMove.current(currentPos, bouncePos, targetPos); + } else { + brewRenderer.scrollTo({ + top: currentPos + targetPos, + behavior: "instant", + block: "start", + }); + } + }; - redo : function(){ - return this.codeEditor.current?.redo(); - }, + const sourceJump = (targetPage = currentBrewRendererPageNum, smooth = true) => { + if (!isText() || isJumping || jumpSource === "brew") return; - historySize : function(){ - return this.codeEditor.current?.historySize(); - }, + const editor = codeEditor.current; + if (!editor) return; + jumpSource = "source"; - undo : function(){ - return this.codeEditor.current?.undo(); - }, + editor.scrollToPage(targetPage); + setTimeout(() => { + jumpSource = null; + }, 200); + }; - foldCode : function() { - return this.codeEditor.current?.foldAll(); - }, + 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, + })); - unfoldCode : function() { - return this.codeEditor.current?.unfoldAll(); - }, - render : function(){ return ( -
+
- {this.renderEditor()} + {renderEditor()}
); - } -}); + }, +); -export default Editor; \ No newline at end of file +export default Editor;