-
-
Save gerryfletch/09d9995725027fcbf060d0149441c3a8 to your computer and use it in GitHub Desktop.
import * as React from "react"; | |
import { CSSProperties, useEffect, useMemo } from "react"; | |
import { MathfieldElement, MathfieldOptions } from "mathlive"; | |
import { create } from "jss"; | |
import camelCase from "jss-plugin-camel-case"; | |
export type MathEditorProps = { | |
options?: Partial<MathfieldOptions>; | |
value: string; | |
onChange: (latex: string) => void; | |
onPlaceholderChange?: (placeholderId: string, latex: string) => void; | |
className?: string; | |
containerStyle?: CSSProperties; | |
placeholderStyle?: CSSProperties; | |
}; | |
/** | |
* @returns a styled math-editor as a non-controlled React component with placeholder support. | |
*/ | |
export const MathEditor = (props: MathEditorProps) => { | |
const containerRef = React.useRef<HTMLDivElement>(null); | |
const mfe = useMemo(() => new MathfieldElement(props.options), []); | |
useEffect(() => { | |
const container = containerRef.current!!; | |
container.innerHTML = ""; | |
container.appendChild(mfe); | |
mfe.className = props.className || ""; | |
// Listen to changes to main mathfield | |
mfe.addEventListener("input", ({ target }) => | |
props.onChange((target as HTMLInputElement).value || "") | |
); | |
// Listen to placeholders, firing if onPlaceholderChange present | |
mfe.addEventListener("placeholder-change", ({ detail }) => { | |
const { placeholderId } = detail; | |
const value = mfe.getPlaceholderField(placeholderId)?.getValue() || ""; | |
if (props.onPlaceholderChange) { | |
props.onPlaceholderChange(placeholderId, value); | |
} | |
}); | |
// Add custom styles to placeholder mathfield elements | |
Object.values(mfe.placeholders).forEach((placeholder) => | |
addStyleEl(placeholder, props.placeholderStyle ?? {}) | |
); | |
}, []); | |
useEffect(() => { | |
mfe.value = props.value; | |
}, [props.value]); | |
return <div ref={containerRef} style={props.containerStyle} />; | |
}; | |
/** | |
* Mathlive uses shadow DOMs which don't inherit global styles. | |
* We patch this by creating <style> tags for nested mathlive | |
* elements. `jss` is used to translate React CSSProperties into | |
* a stylesheet string which is inserted in a new style node. | |
* @param el Mathfield element to create styles for | |
* @param st CSS Properties | |
*/ | |
const addStyleEl = (el: MathfieldElement, css: CSSProperties) => { | |
const node = document.createElement("style"); | |
node.innerHTML = stylesheet("placeholder-mathfield", css); | |
el.appendChild(node); | |
el.classList.add("placeholder-mathfield"); | |
}; | |
const jss = create({ plugins: [camelCase()] }); | |
const stylesheet = (className: string, styles?: CSSProperties): string => | |
jss | |
.createStyleSheet({ [className]: styles }, { generateId: ({ key }) => key }) | |
.toString(); |
Thank you very much for your help!! I had made it work with the official mathlive documentation, creating the customElement but the documentation for react is very poor and I had some problems.
Just one question, why do you return a 'div' instead of 'math-field'?
Thank you very much, I will be attentive to the changes you make when you return
@enzomarin At the time of creating this gist, the simplest way to create the MathLive element and make it a controlled component (value, onChange), was to write the instantiated class as a child node. There may be a nicer way to do things now but I haven’t paid much attention in the last few months.
@gerryfletch Sorry for the inconvenience, but I have a problem and I wanted to see if you could help me.
I want to use the component you gave me in another "MathComponent" component. In this I want to get the reference to the mfe element to get its functions like for example "getPrompts" and "setPromptState", but no matter what I do I get "undefined" in "mfeInstance" and "promts" (line 27 and 28 of MathComponent) .
Basically I want to do what I do in the button of the "Mathfield" component (line 83) but from the "MathComponent" component.
MathComponent :
import { Box, Button } from "@chakra-ui/react"
import type { MathComponentMeta } from "../types"
import dynamic from "next/dynamic"
import { useRef, useState } from "react"
import type { MathEditorRef } from "./tools/mathLive"
const MathField = dynamic(() => import("./tools/mathLive"),{
ssr:false
})
interface Props {
meta: MathComponentMeta
}
const MathComponent = ({meta}: Props) => {
const mfeRef = useRef<MathEditorRef>(null)
const {expression, readonly, answers, correctAnswer} = meta
const [answer,setAnswer] = useState([])
const checkAnswer = () => {
// Acceder a la instancia completa de mfe utilizando la referencia
const mfeInstance = mfeRef.current.mfe;
const prompts = mfeRef.current.getPrompts;
console.log("answer ---->", answer);
console.log("mfeInstance--->", mfeInstance);
console.log("prompts--->", prompts);
// Utilizar mfeInstance para realizar acciones en la instancia de MathfieldElement
/*
if (mfeInstance && prompts) {
mfeInstance.setPromptState(prompts[0], "correct", true);
}
*/
};
const handleMathEditorChange = (latex, promptsValues) => {
// ... hacer algo con mfeInstance
const entries = Object.entries(promptsValues)
console.log("entries ------>",entries)
setAnswer(entries)
};
return(
<Box display='flex' flexDirection='row' justifyContent='' >
<MathField
ref={mfeRef}
readOnly={readonly}
value={expression}
onChange={handleMathEditorChange}>
</MathField>
<Box display='flex' flexDirection='column'>
<Button onClick={checkAnswer}>Aceptar</Button>
<Button>Hint</Button>
</Box>
</Box>
)
}
export default MathComponent
Mathfield component:
import * as React from 'react';
import { useEffect, useMemo, useImperativeHandle, forwardRef, useRef} from 'react';
import { MathfieldElement} from 'mathlive';
import { Button } from '@chakra-ui/react';
export type MathEditorProps = {
readOnly?: boolean;
value: string;
onChange: (latex: string, prompts: Record<string, string>) => void;
className?: string;
};
export interface MathEditorRef {
getPrompts: () => string[];
mfe: MathfieldElement | null;
}
/**
* @returns a styled math-editor as a non-controlled React component with placeholder support.
*/
const Mathfield = forwardRef<MathEditorRef,MathEditorProps> ((props, ref ) => {
const containerRef = React.useRef<HTMLDivElement>(null);
const mfe = useMemo(() => new MathfieldElement(), []);
mfe.readOnly = props.readOnly ?? false;
mfe.disabled = false
const currentValue = React.useRef<string>(''); // Esta variable se utilizará para realizar un seguimiento del valor actual del editor de matemáticas.
/*
mfe.applyStyle(
{backgroundColor: 'yellow' },
{range: [0, -1]}
)
*/
useEffect(() => { // ejecuta un efecto secundario cuando el componente se monta por primera vez
const container = containerRef.current!!;
container.innerHTML = '';
container.appendChild(mfe);
mfe.className = props.className || '';
mfe.addEventListener('input', ({ target }) => {
const value = (target as HTMLInputElement).value || '';
const promptValues: Record<string, string> = mfe
.getPrompts()
.reduce((acc, id) => ({ ...acc, [id]: mfe.getPromptValue(id) }), {});
if (currentValue.current !== value) {
currentValue.current = value;
props.onChange(value, promptValues);
}
});
}, []);
useEffect(() => { // Este efecto se encarga de actualizar el valor del editor de matemáticas cuando props.value cambia.
if (currentValue.current !== props.value) {
const position = mfe.position;
mfe.setValue(props.value, { focus: true, feedback: false });
mfe.position = position;
currentValue.current = props.value;
}
}, [props.value]);//se ejecutará cada vez que el valor de props.value
const getPrompts = () => {
return mfe.getPrompts();
};
useImperativeHandle(ref, () => ({
getPrompts,
mfe,
}));
return (
<>
<div onFocus={()=>{console.log("FOCUS!!!!!")}} ref={containerRef} style={{ maxWidth: '100%'}} />
<Button onClick={()=>{
console.log("mfe from mathlive-->",mfe)
console.log("prompts from mathlive-->",mfe.getPrompts())
mfe.setPromptState('a',"correct",true)
}}>Aceptar</Button>
</>
)
})
export default Mathfield
image of the outputs:
https://i.postimg.cc/KzyVgXKh/Captura-de-pantalla-2023-06-26-a-la-s-02-31-43.png
Hey @enzomarin I'm not 100% sure what you're trying to accomplish, but calling getPrompts
seems unnecessary when the button is clicked, can't you just rely on the output of prompts
in the onChange
function?
I would avoid editing the MathEditor
component I pasted to you earlier. It should be generic enough to cover whatever your use case is.
If you could clarify what the goal of checkAnswer
should be, and what data it is using, I may be able to help more. Sorry I couldn't be of more assistance.
@gerryfletch thank you very much for replying, indeed getPrompts
is unnecessary, I was just testing since I always got undefined
for the mfe
instance.
The purpose of checkAnswer
is to compare the answer entered by a student (which is stored in the answer
state) with an answer object which is stored in answers
and obtained from meta
. If the answer is correct what I want is to do mfe.setPrompts(placeholderId, state, locked)
so that the 'placeholder' (textbox) is locked and colored green (this is done with mfe.setPrompts, this is what I do on the button from the Mathfield component).
The onChange function gives me value
and promptValues
which are the complete latex and a key object with its value (placeholderId: value) respectively of each 'placeholder'. But from onChange
I can't access the setPromts
function of mfe
, is there any way to access or pass the properties and functions of mfe
from onChange
?
(sorry for my english)
I see what you're saying. Yeah, getting ahold of the ref makes sense then. Forwarding the ref might be a little tricky because the mathlive element isn't actually a react component, so it could be worth you adding a new optional prop, mfe
, that lets you pass the instance into the component? That way the parent can instantiate it.
const mfe = useMemo(() => new MathfieldElement(), []);
...
<MathEditor readOnly={...} mfe={mfe} ... />
...
export const MathEditor = (props: MathEditorProps) => {
const containerRef = React.useRef<HTMLDivElement>(null);
const mfe = useMemo(() => props.mfe ?? new MathfieldElement(), []);
Not ideal, I know, but until I get some time to sit down and figure that out, passing the mfe is probably the easiest way to unblock you.
great, it's a solution that had not occurred to me, I'll try it.
Thank you very much for your time!
Hello again, sorry to bother you but I need your help again. It turns out that when I use the virtual keyboard, that floating element appears on the screen that, in addition to being annoying, allows you to modify the mathematical expression (it shouldn't since it is defined as static). I've tried looking in the documentation but I can't find what it is or how to disable it.
Hey @enzomarin I haven't used or updated mathlive in months, but I don't recall seeing that floating window before so it may be something new. I'd advise raising a discussion on the main repository or on Gitter if the community is still active on there.
@enzomarin Hey. Mathlive completely revamped placeholders to be “prompts”. I’m away on holiday for the next two weeks, but this is a version that may work for you (it’s likely also slightly outdated):
I will update the gist when I’m back. Thank you for the reminder!
also note that the placeholder rework removed the shadow-Dom’s, so the CSS style sheet injection is no longer necessary