From 10b1e89bf01e63f4f63845aaefd1d7878a6bc19d Mon Sep 17 00:00:00 2001 From: swve Date: Wed, 26 Feb 2025 10:38:49 +0100 Subject: [PATCH] feat: Enhance MathEquationBlockComponent with advanced LaTeX editing features --- .../MathEquationBlockComponent.tsx | 499 +++++++++++++++--- 1 file changed, 423 insertions(+), 76 deletions(-) diff --git a/apps/web/components/Objects/Editor/Extensions/MathEquation/MathEquationBlockComponent.tsx b/apps/web/components/Objects/Editor/Extensions/MathEquation/MathEquationBlockComponent.tsx index df903a40..b1ef87bb 100644 --- a/apps/web/components/Objects/Editor/Extensions/MathEquation/MathEquationBlockComponent.tsx +++ b/apps/web/components/Objects/Editor/Extensions/MathEquation/MathEquationBlockComponent.tsx @@ -3,20 +3,241 @@ import React from 'react' import styled from 'styled-components' import 'katex/dist/katex.min.css' import { BlockMath } from 'react-katex' -import { Save } from 'lucide-react' +import { Save, Sigma, ExternalLink, ChevronDown, BookOpen, Lightbulb } from 'lucide-react' import Link from 'next/link' import { useEditorProvider } from '@components/Contexts/Editor/EditorContext' +import { motion } from 'framer-motion' + +// Predefined LaTeX templates +const mathTemplates = [ + { + name: 'Fraction', + latex: '\\frac{a}{b}', + description: 'Simple fraction' + }, + { + name: 'Square Root', + latex: '\\sqrt{x}', + description: 'Square root' + }, + { + name: 'Summation', + latex: '\\sum_{i=1}^{n} x_i', + description: 'Sum with limits' + }, + { + name: 'Integral', + latex: '\\int_{a}^{b} f(x) \\, dx', + description: 'Definite integral' + }, + { + name: 'Limit', + latex: '\\lim_{x \\to \\infty} f(x)', + description: 'Limit expression' + }, + { + name: 'Matrix 2×2', + latex: '\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}', + description: '2×2 matrix with parentheses' + }, + { + name: 'Binomial', + latex: '\\binom{n}{k}', + description: 'Binomial coefficient' + }, + { + name: 'Quadratic Formula', + latex: 'x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}', + description: 'Solution to quadratic equation' + }, + { + name: 'Vector', + latex: '\\vec{v} = \\begin{pmatrix} x \\\\ y \\\\ z \\end{pmatrix}', + description: '3D vector' + }, + { + name: 'System of Equations', + latex: '\\begin{cases} a_1x + b_1y = c_1 \\\\ a_2x + b_2y = c_2 \\end{cases}', + description: 'System of linear equations' + } +]; + +// Common LaTeX symbols +const mathSymbols = [ + { symbol: '\\alpha', display: 'α' }, + { symbol: '\\beta', display: 'β' }, + { symbol: '\\gamma', display: 'γ' }, + { symbol: '\\delta', display: 'δ' }, + { symbol: '\\theta', display: 'θ' }, + { symbol: '\\pi', display: 'π' }, + { symbol: '\\sigma', display: 'σ' }, + { symbol: '\\infty', display: '∞' }, + { symbol: '\\pm', display: '±' }, + { symbol: '\\div', display: '÷' }, + { symbol: '\\cdot', display: '·' }, + { symbol: '\\leq', display: '≤' }, + { symbol: '\\geq', display: '≥' }, + { symbol: '\\neq', display: '≠' }, + { symbol: '\\approx', display: '≈' }, +]; + +// Styled components +const MathEqWrapper = styled.div` + transition: all 0.2s ease; + background-color: #f9f9f9; + border: 1px solid #eaeaea; +` + +const EditBar = styled.div` + display: flex; + justify-content: space-between; + border-radius: 8px; + padding: 0 5px 0 12px; + background-color: white; + color: #5252528d; + align-items: center; + height: 45px; + border: solid 1px #e2e2e2; + transition: all 0.2s ease; + + &:focus-within { + border-color: #d1d1d1; + box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.03); + } + + input { + border: none; + background: none; + font-size: 14px; + color: #494949; + width: 100%; + font-family: 'DM Sans', sans-serif; + + &:focus { + outline: none; + } + + &::placeholder { + color: #49494980; + } + } +` + +const SaveButton = styled(motion.button)` + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + border-radius: 6px; + border: none; + background: rgba(217, 217, 217, 0.5); + color: #494949; + cursor: pointer; +` + +const InfoLink = styled.div` + padding-left: 2px; +` + +const TemplateButton = styled.button` + display: flex; + align-items: center; + padding: 6px 10px; + background: rgba(217, 217, 217, 0.4); + border-radius: 6px; + border: none; + font-size: 13px; + color: #494949; + cursor: pointer; +` + +const TemplateDropdown = styled.div` + background: white; + border-radius: 8px; + border: 1px solid #e2e2e2; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + overflow: hidden; +` + +const TemplateItem = styled.div` + padding: 8px 12px; + cursor: pointer; + transition: background 0.15s; + + &:hover { + background: rgba(217, 217, 217, 0.24); + } +` + +const SymbolsDropdown = styled.div` + background: white; + border-radius: 8px; + border: 1px solid #e2e2e2; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + overflow: hidden; +` + +const SymbolButton = styled.button` + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + margin: 2px; + background: rgba(217, 217, 217, 0.3); + border-radius: 4px; + border: none; + font-size: 16px; + color: #494949; + cursor: pointer; +` + +const HelpDropdown = styled.div` + background: white; + border-radius: 8px; + border: 1px solid #e2e2e2; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + overflow: hidden; +` function MathEquationBlockComponent(props: any) { const [equation, setEquation] = React.useState(props.node.attrs.math_equation) const [isEditing, setIsEditing] = React.useState(true) + const [showTemplates, setShowTemplates] = React.useState(false) + const [showSymbols, setShowSymbols] = React.useState(false) + const [showHelp, setShowHelp] = React.useState(false) const editorState = useEditorProvider() as any const isEditable = editorState.isEditable + const inputRef = React.useRef(null) + const templatesRef = React.useRef(null) + const symbolsRef = React.useRef(null) + const helpRef = React.useRef(null) - const handleEquationChange = (event: React.ChangeEvent) => { + // Close dropdowns when clicking outside + React.useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (templatesRef.current && !templatesRef.current.contains(event.target as Node)) { + setShowTemplates(false) + } + if (symbolsRef.current && !symbolsRef.current.contains(event.target as Node)) { + setShowSymbols(false) + } + if (helpRef.current && !helpRef.current.contains(event.target as Node)) { + setShowHelp(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, []) + + const handleEquationChange = (event: React.ChangeEvent) => { setEquation(event.target.value) props.updateAttributes({ - math_equation: equation, + math_equation: event.target.value, }) } @@ -27,83 +248,209 @@ function MathEquationBlockComponent(props: any) { //setIsEditing(false); } + const insertTemplate = (template: string) => { + setEquation(template) + props.updateAttributes({ + math_equation: template, + }) + setShowTemplates(false) + + // Focus the input and place cursor at the end + if (inputRef.current) { + inputRef.current.focus() + inputRef.current.setSelectionRange(template.length, template.length) + } + } + + const insertSymbol = (symbol: string) => { + const cursorPosition = inputRef.current?.selectionStart || equation.length + const newEquation = equation.substring(0, cursorPosition) + symbol + equation.substring(cursorPosition) + + setEquation(newEquation) + props.updateAttributes({ + math_equation: newEquation, + }) + + // Focus the input and place cursor after the inserted symbol + setTimeout(() => { + if (inputRef.current) { + inputRef.current.focus() + inputRef.current.setSelectionRange(cursorPosition + symbol.length, cursorPosition + symbol.length) + } + }, 0) + } + return ( - - {equation} - {isEditing && isEditable && ( - <> - - - - - - Please refer to this{' '} - - {' '} - guide - {' '} - for supported TeX functions{' '} - - - )} - + + +
+ + Math Equation +
+ +
+ {equation} +
+ + {isEditing && isEditable && ( + +
+
+ setShowTemplates(!showTemplates)} + className="flex items-center space-x-1" + > + + Templates + + + + {showTemplates && ( + +
+ Select a template to insert +
+ {mathTemplates.map((template, index) => ( + insertTemplate(template.latex)} + > +
+ {template.name} + {template.description} +
+
+ ))} +
+ )} +
+ +
+ setShowSymbols(!showSymbols)} + className="flex items-center space-x-1" + > + + Symbols + + + + {showSymbols && ( + +
+ Click a symbol to insert +
+
+ {mathSymbols.map((symbol, index) => ( + insertSymbol(symbol.symbol)} + title={symbol.symbol} + > + {symbol.display} + + ))} +
+
+ )} +
+ +
+ setShowHelp(!showHelp)} + className="flex items-center space-x-1" + > + + Help + + + + {showHelp && ( + +
+ LaTeX Math Quick Reference +
+
+
+ Fractions: \frac{'{'}'numerator'{'}'}{'{'}denominator{'}'} +
+
+ Exponents: x^{'{'}'power'{'}'} +
+
+ Subscripts: x_{'{'}'subscript'{'}'} +
+
+ Square root: \sqrt{'{'}'x'{'}'} +
+
+ Summation: \sum_{'{'}'lower'{'}'}^{'{'}'upper'{'}'} +
+
+ Integral: \int_{'{'}'lower'{'}'}^{'{'}'upper'{'}'} +
+
+ + View complete reference + + +
+
+
+ )} +
+
+ + + + saveEquation()} + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > + + + + + + Please refer to this + + guide + + + for supported TeX functions + +
+ )} +
+
) } export default MathEquationBlockComponent - -const MathEqWrapper = styled.div`` - -const EditBar = styled.div` - display: flex; - justify-content: flex-end; - margin-top: 10px; - background-color: white; - border-radius: 10px; - padding: 5px; - color: #5252528d; - align-items: center; - justify-content: space-between; - height: 50px; - border: solid 1px #52525224; - - button { - margin-left: 10px; - margin-right: 7px; - cursor: pointer; - border: none; - background: none; - font-size: 14px; - color: #494949; - } - - input { - border: none; - background: none; - font-size: 14px; - color: #494949; - width: 100%; - font-family: 'DM Sans', sans-serif; - padding-left: 10px; - &:focus { - outline: none; - } - - &::placeholder { - color: #49494936; - } - } -`