├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── examples └── example1 │ ├── .gitignore │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ └── index.html │ ├── src │ ├── App.tsx │ ├── index.tsx │ └── react-app-env.d.ts │ └── tsconfig.json ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── MathfieldComponent.test.tsx ├── MathfieldComponent.tsx └── index.ts └── tsconfig.json /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 12 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: 12 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # testing 5 | /coverage 6 | 7 | # production 8 | /build 9 | /dist 10 | 11 | # misc 12 | .DS_Store 13 | .env 14 | npm-debug.log -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Before contributing 4 | 5 | This library is a thin wrapper (the "react wrapper") around Mathlive (the "base library"). It's likely that errors encountered when using the wrapper originate in the base library. Likewise, feature requests and feature PRs should probably go directly into the base library as well. 6 | 7 | ## What to contribute 8 | 9 | Contributions to the react wrapper can be issues, feature requests or pull requests (code), that should 10 | 11 | * improve the interaction of react and mathlive, 12 | * make sure best practices in react are used 13 | * simplify using the wrapper 14 | * improve the general code style 15 | * add test cases 16 | * adjust for new react / mathlive versions or 17 | * fix existing bugs in the implementation. 18 | 19 | This wrapper does not aim to add too much functionality or additional wrapping around the base library. This minimizes the necessary maintenance to keep the wrapper compatible with future versions. 20 | 21 | ## How to contribute 22 | 23 | Feel free to create an issue if you have any problem that you think originates in this wrapper. If you want to contribute code, fork the repository on github and create a pull request for master. 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Fabian Grewing 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # react-mathlive 3 | [![Build Status](https://semaphoreci.com/api/v1/concludio/react-mathlive/branches/master/shields_badge.svg)](https://semaphoreci.com/concludio/react-mathlive) 4 | [![Coverage Status](https://coveralls.io/repos/github/concludio/react-mathlive/badge.svg?branch=master)](https://coveralls.io/github/concludio/react-mathlive?branch=master) 5 | 6 | A react component using [mathlive.js](https://mathlive.io)'s mathfield (interactive math editor). 7 | 8 | ## How to install 9 | 10 | You can install *react-mathlive* like any other Javascript or Typescript library via npm: 11 | 12 | ``` 13 | npm i react-mathlive 14 | ``` 15 | 16 | For Typescript users: As *react-mathlive* is written in Typescript, it comes with its own typings. 17 | 18 | ## How to use 19 | 20 | This text assumes you know [how to build simple react components](https://reactjs.org/tutorial/tutorial.html). 21 | 22 | You can use the `MathfieldComponent` in your web application as follows: 23 | 24 | ```JS 25 | import { MathfieldComponent } from "react-mathlive"; 26 | ``` 27 | 28 | You can then either use it as a controlled or uncontrolled component. 29 | 30 | ### Uncontrolled component 31 | 32 | In this mode the mathfield is initialized with the `initialLatex`. Whenever the user performs changes the `onChange`-handler is called. 33 | 34 | This example is part of an assumed react component class. 35 | 36 | ```JSX 37 | render() { 38 | return ; 42 | } 43 | 44 | onMathChange(mathText) { 45 | console.log(mathText); 46 | } 47 | ``` 48 | 49 | In an uncontrolled `MathfieldComponent` the only way to programmatically change the mathfields contents are by accessing the `mathfield` directly and calling the modifying methods in there (see [Interacting with the native library](#Interacting-with-the-native-library)). 50 | 51 | ### Controlled component 52 | 53 | In this mode the mathfield gets its contents updated whenever the `latex`-property changes. 54 | 55 | This example makes use of reacts functional components. 56 | 57 | ```JSX 58 | export function MyComponent() { 59 | const [latex, setLatex] = React.useState("f(x)=\\log _10 x"); 60 | return ; 64 | } 65 | ``` 66 | 67 | There is also an [example Typescript react application](/examples/example1/) using this library. 68 | 69 | ### Interacting with the native library 70 | 71 | The `MathfieldComponent` also allows retrieving the native [`Mathfield` object](https://cortexjs.io/docs/mathlive/#(%22mathfield%22%3Amodule).(Mathfield%3Ainterface)) from the Mathlive library via the `mathfieldRef` parameter: 72 | 73 | ```JavaScript 74 | render() { 75 | return (this.internalMathfield = mf)} 77 | />; 78 | } 79 | ``` 80 | 81 | The object stored in `internalMathfield` can be used to read and modify the underlying mathfield directly. 82 | 83 | Via the optional `mathfieldConfig` parameter it is possible to provide the native `Mathfield` with a [`MathfieldConfig`](https://cortexjs.io/docs/mathlive/#(%22config%22%3Amodule).(MathfieldConfig%3Atype)) on its creation: 84 | 85 | 86 | ```JSX 87 | render() { 88 | return ; 94 | } 95 | ``` 96 | 97 | ## Contribute 98 | 99 | This is an open source library and issues and pull requests are very welcome. 100 | 101 | See [Contributing](CONTRIBUTING.md). 102 | 103 | -------------------------------------------------------------------------------- /examples/example1/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/example1/README.md: -------------------------------------------------------------------------------- 1 | # Example 1 2 | 3 | This simple react app uses both a `MathFieldComponent` and a standard html input control to display the same content. Changes to either of them should propagate to the other one immediately. It demonstrates how to provide and retrieve the value of the math input in a react environment. 4 | 5 | Install all dependencies by calling `npm install` in this folder. 6 | 7 | Run via `npm run start`. 8 | 9 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 10 | -------------------------------------------------------------------------------- /examples/example1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "mathlive": "0.55.0", 7 | "react": "^16.8.6", 8 | "react-dom": "^16.8.6", 9 | "react-mathlive": "file:../..", 10 | "react-scripts": "^3.4.3" 11 | }, 12 | "devDependencies": { 13 | "@types/jest": "24.0.15", 14 | "@types/node": "12.6.8", 15 | "@types/react": "16.8.23", 16 | "@types/react-dom": "16.8.4", 17 | "typescript": "3.9.5" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build" 22 | }, 23 | "eslintConfig": { 24 | "extends": "react-app" 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/example1/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/concludio/react-mathlive/0c746ce6b2174fcb52bd5c17ce1570317d8c75b6/examples/example1/public/favicon.ico -------------------------------------------------------------------------------- /examples/example1/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React-Mathlive-Example1 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/example1/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MathfieldComponent } from 'react-mathlive'; 3 | 4 | interface State { 5 | latex: string; 6 | } 7 | 8 | class App extends React.Component<{}, State> { 9 | public state: State = { latex: "\\frac{1}{x}" }; 10 | 11 | public render() { 12 | return ( 13 |
14 | MathField: 15 | 19 |
20 | TextField: 21 | this.onLatexChange(ev.target.value)} 25 | /> 26 | 29 |
30 | ); 31 | } 32 | 33 | private readonly onLatexChange = (latex: string) => this.setState({ latex }); 34 | } 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /examples/example1/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /examples/example1/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/example1/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": false, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "preserve" 21 | }, 22 | "include": [ 23 | "src" 24 | ], 25 | "exclude": [ 26 | "node_modules" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jest-environment-jsdom', 4 | roots: [ 'src/' ], 5 | moduleNameMapper: { 6 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", 7 | "\\.(css|less|scss|sass)$": "identity-obj-proxy" 8 | }, 9 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-mathlive", 3 | "version": "3.0.5-preview.1", 4 | "description": "An interactive math input for react.", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "/dist" 8 | ], 9 | "types": "dist/index.d.ts", 10 | "repository": { 11 | "url": "https://github.com/concludio/react-mathlive" 12 | }, 13 | "scripts": { 14 | "build": "tsc", 15 | "prepare": "npm run build", 16 | "test": "jest --coverage", 17 | "coverage": "jest --coverage --coverageReporters=text-lcov | coveralls" 18 | }, 19 | "author": "Fabian Grewing", 20 | "license": "MIT", 21 | "keywords": [ 22 | "react", 23 | "math", 24 | "editor", 25 | "input", 26 | "interactive", 27 | "wysiwyg", 28 | "latex" 29 | ], 30 | "devDependencies": { 31 | "@testing-library/react": "^8.0.7", 32 | "@types/jest": "^24.0.16", 33 | "@types/react": "^16.7.22", 34 | "@types/react-dom": "^16.0.11", 35 | "coveralls": "^3.0.5", 36 | "identity-obj-proxy": "^3.0.0", 37 | "jest": "^25.2.0", 38 | "ts-jest": "^25.3.0", 39 | "typescript": "^3.9.5" 40 | }, 41 | "dependencies": { 42 | "mathlive": ">= 0.55.0 < 1", 43 | "react": "^16.7.0", 44 | "react-dom": "^16.7.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/MathfieldComponent.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | render, 4 | fireEvent, 5 | cleanup, 6 | waitForElement, 7 | } from '@testing-library/react'; 8 | import { MathfieldComponent, combineConfig } from './MathfieldComponent'; 9 | // @ts-ignore 10 | import Mathlive from 'mathlive/dist/mathlive'; 11 | 12 | afterEach(cleanup); 13 | 14 | describe("combineConfig", () => { 15 | it(" combines onChange and native onContentDidChange handlers", () => { 16 | let value1: string | undefined, 17 | value2: string | undefined; 18 | 19 | const combinedConfig = combineConfig({ 20 | latex: "fubar", 21 | onChange: v => value1 = v, 22 | mathfieldConfig: { 23 | onContentDidChange: (mf: Mathlive.Mathfield) => value2 = mf.$latex(), 24 | }, 25 | }); 26 | 27 | const mockMathField = { 28 | $latex: () => "bar", 29 | } as Mathlive.Mathfield; 30 | 31 | combinedConfig.onContentDidChange!(mockMathField); 32 | 33 | expect(value1).toBe("bar"); 34 | expect(value2).toBe("bar"); 35 | }); 36 | }); 37 | 38 | describe("MathFieldComponent", () => { 39 | it(" mounts mathfield", () => { 40 | const mountingResult = render( 41 | 44 | ); 45 | const mlTextAreas = mountingResult.baseElement.getElementsByClassName("ML__textarea"); 46 | expect(mlTextAreas).toHaveLength(1); 47 | }); 48 | 49 | it(" internal mathfield yields correct latex", () => { 50 | let mathField: any; 51 | render( 52 | mathField = mf} 54 | latex="fubar" 55 | /> 56 | ); 57 | expect(mathField.$latex()).toBe("fubar"); 58 | }); 59 | 60 | it(" reacts to direct mathfield interaction", () => { 61 | let value = "foo"; 62 | let mathField: any; 63 | render( 64 | mathField = mf} 66 | onChange={v => value = v} 67 | latex={value} 68 | /> 69 | ); 70 | expect(value).toBe("foo"); 71 | expect(mathField.$latex()).toBe("foo"); 72 | 73 | mathField.$latex("bar"); 74 | expect(value).toBe("bar"); 75 | expect(mathField.$latex()).toBe("bar"); 76 | }); 77 | 78 | it(" accepts changed props", () => { 79 | class Container extends React.Component<{}, { value: string }> { 80 | public state = { value: "foo" }; 81 | public mathField: any; 82 | 83 | public render() { 84 | return this.mathField = mf} 86 | latex={this.state.value} 87 | 88 | />; 89 | } 90 | } 91 | 92 | let container!: Container; 93 | render( container = c!} />); 94 | let mathField = container.mathField; 95 | 96 | expect(mathField.$latex()).toBe("foo"); 97 | 98 | container.setState({ value: "bar" }); 99 | expect(mathField.$latex()).toBe("bar"); 100 | 101 | container.setState({ value: "" }); 102 | expect(mathField.$latex()).toBe(""); 103 | }); 104 | 105 | test("invalidly created instances throw correct errors", () => { 106 | const mathFieldComponent = new MathfieldComponent({ latex: "fubar" }); 107 | 108 | try { 109 | mathFieldComponent.componentDidMount(); 110 | fail("The previous line should have thrown."); 111 | } catch (e) { 112 | expect(e.message).toBe("React did apparently not mount the insert point correctly."); 113 | } 114 | }); 115 | }); -------------------------------------------------------------------------------- /src/MathfieldComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | // @ts-ignore 3 | import Mathlive from "mathlive"; 4 | import "mathlive/dist/mathlive-fonts.css"; 5 | 6 | interface BaseProps { 7 | onChange?: (latex: string) => void; 8 | 9 | /** 10 | * The raw options of mathlive's makeMathField. 11 | * */ 12 | mathfieldConfig?: Mathlive.MathfieldConfig; 13 | 14 | /** 15 | * The mathfield object returned by makeMathField. 16 | */ 17 | mathfieldRef?: (mathfield: Mathlive.Mathfield) => void; 18 | } 19 | 20 | interface ControlledProps extends BaseProps { 21 | latex: string; 22 | initialLatex?: undefined; 23 | } 24 | 25 | interface UncontrolledProps extends BaseProps { 26 | latex?: undefined; 27 | initialLatex: string; 28 | } 29 | 30 | export type Props = ControlledProps | UncontrolledProps; 31 | 32 | export function combineConfig(props: Props): Mathlive.MathfieldConfig { 33 | const combinedConfiguration: Mathlive.MathfieldConfig = { 34 | ...props.mathfieldConfig, 35 | }; 36 | 37 | const { onChange } = props; 38 | 39 | if (onChange) { 40 | if (props.mathfieldConfig && props.mathfieldConfig.onContentDidChange) { 41 | const fromConfig = props.mathfieldConfig.onContentDidChange; 42 | combinedConfiguration.onContentDidChange = (mf: Mathlive.Mathfield) => { 43 | onChange(mf.$latex()); 44 | fromConfig(mf); 45 | }; 46 | } else { 47 | combinedConfiguration.onContentDidChange = (mf: Mathlive.Mathfield) => 48 | onChange(mf.$latex()); 49 | } 50 | } 51 | 52 | return combinedConfiguration; 53 | } 54 | 55 | /** A react-control that hosts a mathlive-mathfield in it. */ 56 | export class MathfieldComponent extends React.Component { 57 | private insertElement: HTMLElement | null = null; 58 | private readonly combinedConfiguration = combineConfig(this.props); 59 | private mathfield: Mathlive.Mathfield | undefined; 60 | 61 | componentDidUpdate(prevProps: Props) { 62 | if (!this.mathfield) { 63 | throw new Error("Component was not correctly initialized."); 64 | } 65 | if (prevProps.latex !== undefined) { 66 | if (this.props.latex === undefined) { 67 | throw new Error( 68 | "Cannot change from controlled to uncontrolled state!" 69 | ); 70 | } 71 | if (this.props.latex !== prevProps.latex) { 72 | if (this.props.latex === "") { 73 | this.mathfield.$perform("deleteAll"); 74 | } else { 75 | this.mathfield.$latex(this.props.latex, { 76 | suppressChangeNotifications: true, 77 | }); 78 | } 79 | } 80 | } 81 | } 82 | 83 | render() { 84 | return
(this.insertElement = instance)} />; 85 | } 86 | 87 | componentDidMount() { 88 | if (!this.insertElement) { 89 | throw new Error( 90 | "React did apparently not mount the insert point correctly." 91 | ); 92 | } 93 | 94 | const initialValue = this.props.initialLatex ?? this.props.latex; 95 | 96 | this.mathfield = Mathlive.makeMathField( 97 | this.insertElement, 98 | this.combinedConfiguration 99 | ); 100 | this.mathfield.$latex(initialValue, { 101 | suppressChangeNotifications: true, 102 | }); 103 | 104 | if (this.props.mathfieldRef) { 105 | this.props.mathfieldRef(this.mathfield); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { MathfieldComponent, Props as MathfieldComponentProps } from './MathfieldComponent'; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "target": "es5", 5 | "lib": [ "dom", "dom.iterable", "esnext" ], 6 | "module": "commonjs", 7 | "allowJs": false, 8 | "noImplicitAny": true, 9 | "strict": true, 10 | "declaration": true, 11 | "sourceMap": true, 12 | "esModuleInterop": true, 13 | "jsx": "react", 14 | "typeRoots": [ 15 | "index.d.ts", 16 | "node_modules/@types" 17 | ] 18 | }, 19 | "include": [ 20 | "src/**/*.tsx", 21 | "src/**/*.ts" 22 | ], 23 | "exclude": [ 24 | "node_modules", 25 | "dist", 26 | "examples", 27 | "**/*.test.ts", 28 | "**/*.test.tsx" 29 | ] 30 | } --------------------------------------------------------------------------------