├── .eslintignore
├── .gitignore
├── postcss.config.js
├── .storybook
├── config.js
├── preview-head.html
└── webpack.config.js
├── src
├── katex.js
├── components
│ ├── InsertKatexButton.js
│ ├── KatexOutput.js
│ └── TeXBlock.js
├── modifiers
│ ├── insertTeXBlock.js
│ └── removeTeXBlock.js
├── styles.css
└── index.js
├── .babelrc
├── .npmignore
├── .editorconfig
├── scripts
└── concatCssFiles.js
├── .eslintrc
├── stories
├── index.js
└── ConfiguredEditor.js
├── CHANGELOG.md
├── webpack.config.js
├── package.json
└── README.md
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/node_modules/**
2 | lib/**
3 | scripts/**
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | lib/*
2 | /.idea/
3 | node_modules
4 | /storybook-static/
5 | /lib-css/
6 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 |
3 | module.exports = {
4 | plugins: [
5 | require('autoprefixer')({ browsers: ['> 1%'] })
6 | ]
7 | };
8 |
--------------------------------------------------------------------------------
/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import { configure } from '@storybook/react';
2 |
3 | function loadStories() {
4 | require('../stories');
5 | }
6 |
7 | configure(loadStories, module);
8 |
--------------------------------------------------------------------------------
/src/katex.js:
--------------------------------------------------------------------------------
1 | import katex from 'katex';
2 |
3 | /**
4 | * A simple katex wrapper to enable injecting katex from outside this plugin
5 | */
6 |
7 | export default {
8 | render: katex.render,
9 | __parse: katex.__parse,
10 | };
11 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react", "es2015", "stage-0"],
3 | "env": {
4 | "production": {
5 | "plugins": [
6 | [
7 | "babel-plugin-webpack-loaders",
8 | {
9 | "config": "${WEBPACK_CONFIG}",
10 | "verbose": true
11 | }
12 | ]
13 | ]
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Dependency directory
2 | # Commenting this out is preferred by some people, see
3 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
4 | node_modules
5 |
6 | # sources
7 | webpack.config.js
8 | .babelrc
9 | .gitignore
10 | README.md
11 | CHANGELOG.md
12 |
13 | # NPM debug
14 | npm-debug.log
15 | /.idea/
16 |
--------------------------------------------------------------------------------
/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 |
10 | # Change these settings to your own preference
11 | indent_style = space
12 | indent_size = 2
13 |
14 | # We recommend you to keep these unchanged
15 | end_of_line = lf
16 | charset = utf-8
17 | trim_trailing_whitespace = true
18 | insert_final_newline = true
19 |
20 | [*.md]
21 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/scripts/concatCssFiles.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const dirname = path.join(process.argv[2], 'lib-css'); // where to place webpack files
4 | var content = ''; // eslint-disable-line no-var
5 |
6 | const filenames = fs.readdirSync(dirname);
7 | filenames.forEach((filename) => {
8 | const filePath = path.join(dirname, filename);
9 | content += fs.readFileSync(filePath, 'utf-8');
10 | });
11 |
12 | const outputFilePath = path.join(process.argv[2], 'lib', 'plugin.css');
13 | fs.writeFileSync(outputFilePath, content);
14 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "plugins": [ "mocha" ],
4 | "env": {
5 | "browser": true,
6 | "mocha": true,
7 | "node": true
8 | },
9 | "extends": "airbnb",
10 | "rules": {
11 | "arrow-parens": 0,
12 | "max-len": 0,
13 | "comma-dangle": 0,
14 | "new-cap": 0,
15 | "react/prop-types": 0,
16 | "react/forbid-prop-types": 0,
17 | "react/prefer-stateless-function": 0,
18 | "react/jsx-filename-extension": 0,
19 | "import/no-extraneous-dependencies": 0,
20 | "import/prefer-default-export": 0,
21 | "jsx-a11y/no-static-element-interactions": 0,
22 | "class-methods-use-this": 0
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/stories/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { storiesOf } from '@storybook/react';
4 |
5 | import ConfiguredEditor from './ConfiguredEditor';
6 |
7 | storiesOf('Katex editor', module)
8 | .add('Without mathinput', () => (
9 |
{
53 | this.container = container;
54 | }}
55 | onClick={this.props.onClick}
56 | />
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/modifiers/removeTeXBlock.js:
--------------------------------------------------------------------------------
1 | import {
2 | Modifier,
3 | EditorState,
4 | SelectionState
5 | } from 'draft-js';
6 |
7 | export default (editorState: Object, blockKey: String) => {
8 | let content = editorState.getCurrentContent();
9 | const newSelection = new SelectionState({
10 | anchorKey: blockKey,
11 | anchorOffset: 0,
12 | focusKey: blockKey,
13 | focusOffset: 0,
14 | });
15 |
16 | const afterKey = content.getKeyAfter(blockKey);
17 | const afterBlock = content.getBlockForKey(afterKey);
18 | let targetRange;
19 |
20 | // Only if the following block the last with no text then the whole block
21 | // should be removed. Otherwise the block should be reduced to an unstyled block
22 | // without any characters.
23 | if (afterBlock &&
24 | afterBlock.getType() === 'unstyled' &&
25 | afterBlock.getLength() === 0 &&
26 | afterBlock === content.getBlockMap().last()) {
27 | targetRange = new SelectionState({
28 | anchorKey: blockKey,
29 | anchorOffset: 0,
30 | focusKey: afterKey,
31 | focusOffset: 0,
32 | });
33 | } else {
34 | targetRange = new SelectionState({
35 | anchorKey: blockKey,
36 | anchorOffset: 0,
37 | focusKey: blockKey,
38 | focusOffset: 1,
39 | });
40 | }
41 |
42 | // change the blocktype and remove the characterList entry with the sticker
43 | content = Modifier.setBlockType(
44 | content,
45 | targetRange,
46 | 'unstyled'
47 | );
48 | content = Modifier.removeRange(content, targetRange, 'backward');
49 |
50 | // force to new selection
51 | const newState = EditorState.push(editorState, content, 'remove-range');
52 | return EditorState.forceSelection(newState, newSelection);
53 | };
54 |
--------------------------------------------------------------------------------
/.storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | // you can use this file to add your custom webpack plugins, loaders and anything you like.
2 | // This is just the basic way to add additional webpack configurations.
3 | // For more information refer the docs: https://storybook.js.org/docs/react-storybook/configurations/custom-webpack-config
4 |
5 | // IMPORTANT
6 | // When you add this file, we won't add the default configurations which is similar
7 | // to "React Create App". This only has babel loader to load JavaScript.
8 | const path = require('path');
9 |
10 | module.exports = {
11 | plugins: [
12 | // your custom plugins
13 | ],
14 | module: {
15 | rules: [
16 | /*
17 | {
18 | test: /\.scss$/,
19 | loaders: ["style-loader", "css-loader", "sass-loader", "postcss-loader"],
20 | include: path.resolve(__dirname, '../')
21 | },
22 | */
23 | {
24 | test: /\.css$/,
25 | use: [
26 | {
27 | loader: 'style-loader',
28 | },
29 | {
30 | loader: 'css-loader',
31 | options: {
32 | modules: true,
33 | importLoaders: 1,
34 | localIdentName: 'draftJsKatexPlugin__[local]__[hash:base64:5]',
35 | },
36 | },
37 | ],
38 | },
39 | ],
40 | /*
41 | loaders: [
42 |
43 | {
44 | test: /\.css$/,
45 | use: [
46 | {
47 | loader: "style-loader"
48 | },
49 | {
50 | loader: "css-loader",
51 | options: {
52 | modules: true,
53 | importLoaders: 1,
54 | localIdentName: 'draftJsKatexPlugin__[local]__[hash:base64:5]',
55 | },
56 | },
57 | 'postcss-loader'
58 | ]
59 | }]
60 | */
61 | },
62 | };
63 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | .tex {
2 | text-align: center;
3 | background-color: #fff;
4 | cursor: pointer;
5 | margin: 20px auto;
6 | /*padding: 20px;*/
7 | -webkit-transition: background-color 0.2s fade-in-out;
8 | user-select: none;
9 | -webkit-user-select: none;
10 | }
11 |
12 | .activeTeX {
13 | color: #888;
14 | }
15 |
16 | .panel {
17 | font-family: 'Helvetica', sans-serif;
18 | font-weight: 200;
19 | }
20 |
21 | .panel .texValue {
22 | border: 1px solid #e1e1e1;
23 | display: block;
24 | font-family: 'Inconsolata', 'Menlo', monospace;
25 | font-size: 14px;
26 | height: 110px;
27 | margin: 20px auto 10px;
28 | outline: none;
29 | padding: 14px;
30 | resize: none;
31 | -webkit-box-sizing: border-box;
32 | width: 500px;
33 | }
34 |
35 | .buttons {
36 | text-align: center;
37 | }
38 |
39 | .saveButton,
40 | .removeButton {
41 | background-color: #fff;
42 | border: 1px solid #0a0;
43 | cursor: pointer;
44 | font-family: 'Helvetica', 'Arial', sans-serif;
45 | font-size: 16px;
46 | font-weight: 200;
47 | margin: 10px auto;
48 | padding: 6px;
49 | -webkit-border-radius: 3px;
50 | width: 100px;
51 | }
52 |
53 | .removeButton {
54 | border-color: #aaa;
55 | color: #999;
56 | margin-left: 8px;
57 | }
58 |
59 | .invalidButton {
60 | background-color: #eee;
61 | border-color: #a00;
62 | color: #666;
63 | }
64 |
65 | .insertButton {
66 | box-sizing: border-box;
67 | border: 1px solid #ddd;
68 | height: 1.5em;
69 | color: #888;
70 | border-radius: 1.5em;
71 | line-height: 1.2em;
72 | cursor: pointer;
73 | background-color: #fff;
74 | width: 2.5em;
75 | font-weight: bold;
76 | font-size: 1.5em;
77 | padding: 0;
78 | margin: 0;
79 | }
80 |
81 | .insertButton:focus {
82 | background-color: #eee;
83 | color: #999;
84 | outline: 0; /* reset for :focus */
85 | }
86 |
87 | .insertButton:hover {
88 | background-color: #eee;
89 | color: #999;
90 | }
91 |
92 | .insertButton:active {
93 | background-color: #ddd;
94 | color: #777;
95 | }
96 |
97 | .insertButton:disabled {
98 | background-color: #F5F5F5;
99 | color: #ccc;
100 | }
101 |
--------------------------------------------------------------------------------
/stories/ConfiguredEditor.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import asciimath2latex from 'asciimath-to-latex';
3 | import { EditorState } from 'draft-js';
4 |
5 | import Editor from 'draft-js-plugins-editor';
6 |
7 | import MathInput from 'math-input/dist/components/app';
8 | import createKaTeXPlugin from '../src/index';
9 | import '../src/styles.css';
10 |
11 | import katex from '../src/katex';
12 |
13 | const katexTheme = {
14 | insertButton: 'Button Button-small Button-insert',
15 | };
16 |
17 | function configuredEditor(props) {
18 | const kaTeXPlugin = createKaTeXPlugin({
19 | // the configs here are mainly to show you that it is possible. Feel free to use w/o config
20 | doneContent: { valid: 'Close', invalid: 'Invalid syntax' },
21 | katex, // <-- required
22 | MathInput: props.withMathInput ? MathInput : null,
23 | removeContent: 'Remove',
24 | theme: katexTheme,
25 | translator: props.withAsciimath ? asciimath2latex : null,
26 | });
27 |
28 | const plugins = [kaTeXPlugin];
29 |
30 | const baseEditorProps = Object.assign({
31 | plugins,
32 | });
33 |
34 | return { baseEditorProps, InsertButton: kaTeXPlugin.InsertButton };
35 | }
36 |
37 | export default class ConfiguredEditor extends Component {
38 | static propTypes = {};
39 |
40 | constructor(props) {
41 | super(props);
42 | const { baseEditorProps, InsertButton } = configuredEditor(props);
43 | this.baseEditorProps = baseEditorProps;
44 | this.InsertButton = InsertButton;
45 | this.state = { editorState: EditorState.createEmpty() };
46 | }
47 |
48 | componentDidMount() {
49 | this.focus();
50 | }
51 |
52 | // use this when triggering a button that only changes editorstate
53 | onEditorStateChange = editorState => {
54 | this.setState(() => ({ editorState }));
55 | };
56 |
57 | focus = () => {
58 | this.editor.focus();
59 | };
60 |
61 | render() {
62 | const { InsertButton } = this;
63 |
64 | return (
65 |
66 |
DraftJS KaTeX Plugin
67 |
68 |
69 | Insert ascii math
70 |
71 |
this.editor = element}
74 | editorState={this.state.editorState}
75 | onChange={this.onEditorStateChange}
76 | />
77 |
78 | );
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "draft-js-katex-plugin",
3 | "description": "Katex Plugin for DraftJS",
4 | "version": "1.3.1",
5 | "author": {
6 | "name": "letranloc",
7 | "email": "letranloc1994@gmail.com",
8 | "url": "https://letranloc.com"
9 | },
10 | "bugs": {
11 | "url": "https://github.com/letranloc/draft-js-katex-plugin/issues"
12 | },
13 | "dependencies": {
14 | "aphrodite": "^1.2.3",
15 | "decorate-component-with-props": "^1.0.2",
16 | "draft-js": ">=0.9.1",
17 | "jquery": "^3.2.1",
18 | "katex": "^0.8.3",
19 | "math-input": "github:letranloc/math-input#web-support",
20 | "mathquill": "^0.10.1-a",
21 | "react-addons-css-transition-group": "^15.6.0",
22 | "react-addons-pure-render-mixin": "^15.6.0",
23 | "react-redux": "^5.0.6",
24 | "redux": "^3.7.2",
25 | "union-class-names": "^1.0.0"
26 | },
27 | "devDependencies": {
28 | "@storybook/react": "^3.2.8",
29 | "asciimath-to-latex": "^0.3.2",
30 | "autoprefixer": "^6.7.7",
31 | "babel-cli": "^6.14.0",
32 | "babel-core": "^6.14.0",
33 | "babel-eslint": "^7.1.1",
34 | "babel-loader": "^6.2.5",
35 | "babel-plugin-webpack-loaders": "^0.9.0",
36 | "babel-polyfill": "^6.13.0",
37 | "babel-preset-es2015": "^6.14.0",
38 | "babel-preset-react": "^6.11.1",
39 | "babel-preset-stage-0": "^6.5.0",
40 | "css-loader": "^0.27.2",
41 | "draft-js-plugins-editor": "2.0.0-rc7",
42 | "eslint": "^3.5.0",
43 | "eslint-config-airbnb": "^14.1.0",
44 | "eslint-plugin-import": "^2.2.0",
45 | "eslint-plugin-jsx-a11y": "^4.0.0",
46 | "eslint-plugin-mocha": "^4.5.1",
47 | "eslint-plugin-react": "^6.2.0",
48 | "extract-text-webpack-plugin": "^2.1.0",
49 | "flow-bin": "^0.41.0",
50 | "lint-staged": "^3.0.1",
51 | "postcss-loader": "^1.3.3",
52 | "prop-types": "^15.6.0",
53 | "react": "^15.4.2",
54 | "react-dom": "^15.4.2",
55 | "rimraf": "^2.6.1",
56 | "style-loader": "^0.13.2",
57 | "webpack": "^2.2.1"
58 | },
59 | "homepage": "https://github.com/letranloc/draft-js-katex-plugin",
60 | "keywords": [
61 | "components",
62 | "draft",
63 | "editor",
64 | "react",
65 | "react-component",
66 | "ux",
67 | "widget",
68 | "wysiwyg"
69 | ],
70 | "license": "MIT",
71 | "lint-staged": {
72 | "lint:eslint": "*.js"
73 | },
74 | "main": "lib/index.js",
75 | "peerDependencies": {
76 | "react": "^15.0.0",
77 | "react-dom": "^15.0.0"
78 | },
79 | "pre-commit": "lint:staged",
80 | "repository": {
81 | "type": "git",
82 | "url": "https://github.com/letranloc/draft-js-katex-plugin.git"
83 | },
84 | "scripts": {
85 | "build": "npm run clean && npm run build:js && npm run build:css",
86 | "build:css": "node ./scripts/concatCssFiles $(pwd) && rimraf lib-css",
87 | "build:js": "WEBPACK_CONFIG=$(pwd)/webpack.config.js BABEL_DISABLE_CACHE=1 BABEL_ENV=production NODE_ENV=production babel --out-dir='lib' --ignore='__test__/*' src",
88 | "clean": "rimraf lib storybook-static",
89 | "lint": "npm run lint:eslint",
90 | "lint:eslint": "eslint --rule 'mocha/no-exclusive-tests:2' ./",
91 | "lint:eslint:fix": "eslint --fix --rule 'mocha/no-exclusive-tests:2' ./",
92 | "lint:staged": "lint-staged",
93 | "prepublish": "npm run build",
94 | "storybook": "start-storybook -p 6006",
95 | "build-storybook": "WEBPACK_CONFIG=$(pwd)/webpack.config.js BABEL_DISABLE_CACHE=1 build-storybook -c .storybook -o storybook-static"
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { EditorState } from 'draft-js';
2 | import decorateComponentWithProps from 'decorate-component-with-props';
3 | import TeXBlock from './components/TeXBlock';
4 | import removeTeXBlock from './modifiers/removeTeXBlock';
5 | import InsertButton from './components/InsertKatexButton';
6 |
7 | import styles from './styles.css';
8 |
9 | function noopTranslator(tex) {
10 | return tex;
11 | }
12 |
13 | const defaultTheme = {
14 | tex: styles.tex,
15 | activeTex: styles.activeTeX,
16 | panel: styles.panel,
17 | texValue: styles.texValue,
18 | buttons: styles.buttons,
19 | saveButton: styles.saveButton,
20 | removeButton: styles.removeButton,
21 | invalidButton: styles.invalidButton,
22 | insertButton: styles.insertButton
23 | };
24 |
25 | export default (config = {}) => {
26 | const theme = Object.assign(defaultTheme, config.theme || {});
27 | const insertContent = config.insertContent || 'Ω';
28 | const doneContent = config.doneContent || {
29 | valid: 'Done',
30 | invalid: 'Invalid TeX',
31 | };
32 | const removeContent = config.removeContent || 'Remove';
33 | const translator = config.translator || noopTranslator;
34 | const katex = config.katex;
35 |
36 | if (!katex || !katex.render) {
37 | throw new Error('Invalid katex plugin provided!');
38 | }
39 |
40 | const store = {
41 | getEditorState: undefined,
42 | setEditorState: undefined,
43 | getReadOnly: undefined,
44 | setReadOnly: undefined,
45 | onChange: undefined,
46 | };
47 |
48 | const liveTeXEdits = new Map();
49 |
50 | const component = decorateComponentWithProps(TeXBlock, {
51 | theme,
52 | store,
53 | doneContent,
54 | removeContent,
55 | translator,
56 | katex,
57 | MathInput: config.MathInput,
58 | });
59 |
60 | return {
61 | initialize: ({ getEditorState, setEditorState, getReadOnly, setReadOnly }) => {
62 | store.getEditorState = getEditorState;
63 | store.setEditorState = setEditorState;
64 | store.getReadOnly = getReadOnly;
65 | store.setReadOnly = setReadOnly;
66 | },
67 |
68 | blockRendererFn: block => {
69 | if (block.getType() === 'atomic') {
70 | const entity = store
71 | .getEditorState()
72 | .getCurrentContent()
73 | .getEntity(block.getEntityAt(0));
74 | const type = entity.getType();
75 |
76 | if (type === 'KateX') {
77 | return {
78 | component,
79 | editable: false,
80 | props: {
81 | onStartEdit: blockKey => {
82 | liveTeXEdits.set(blockKey, true);
83 | store.setReadOnly(liveTeXEdits.size);
84 | },
85 |
86 | onFinishEdit: (blockKey, newEditorState) => {
87 | liveTeXEdits.delete(blockKey);
88 | store.setReadOnly(liveTeXEdits.size);
89 | store.setEditorState(EditorState.forceSelection(newEditorState, newEditorState.getSelection()));
90 | },
91 |
92 | onRemove: blockKey => {
93 | liveTeXEdits.delete(blockKey);
94 | store.setReadOnly(liveTeXEdits.size);
95 |
96 | const editorState = store.getEditorState();
97 | const newEditorState = removeTeXBlock(editorState, blockKey);
98 | store.setEditorState(newEditorState);
99 | },
100 | },
101 | };
102 | }
103 | }
104 | return null;
105 | },
106 | InsertButton: decorateComponentWithProps(InsertButton, {
107 | theme,
108 | store,
109 | translator,
110 | defaultContent: insertContent,
111 | }),
112 | };
113 | };
114 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DraftJS KaTeX Plugin
2 |
3 | *This is a plugin for the `draft-js-plugins-editor`.*
4 |
5 | This plugin insert and render LaTeX using [KaTeX](https://github.com/Khan/KaTeX), modified from [TeX example](https://github.com/facebook/draft-js/tree/master/examples/draft-0-10-0/tex).
6 |
7 | - Integrated with [Khan/math-input](https://github.com/Khan/math-input).
8 |
9 | [](https://www.npmjs.com/package/draft-js-katex-plugin)
10 |
11 | ## Usage
12 |
13 | Add MathQuill libs if you want to use MathInput:
14 | ```html
15 |
16 |
17 | ```
18 |
19 | Add plugin
20 | ```js
21 | import createKaTeXPlugin from 'draft-js-katex-plugin';
22 | import katex from 'katex'
23 |
24 | const kaTeXPlugin = createKaTeXPlugin({katex});
25 |
26 | const { InsertButton } = kaTeXPlugin;
27 | ```
28 |
29 | With MathInput:
30 |
31 | ```js
32 | import createKaTeXPlugin from 'draft-js-katex-plugin';
33 | import katex from 'katex'
34 | import MathInput from '../src/components/math-input/components/app';
35 |
36 |
37 | const kaTeXPlugin = createKaTeXPlugin({katex, MathInput});
38 |
39 | const { InsertButton } = kaTeXPlugin;
40 | ```
41 |
42 | There are more examples in the `stories/index.js` file.
43 |
44 | ## Configuration options:
45 |
46 | Here shown with defaults:
47 | ```js
48 | {
49 | katex, // the katex object or a wrapper defining render() and __parse().
50 |
51 | doneContent: {
52 | valid: 'Done',
53 | invalid: 'Invalid TeX',
54 | },
55 |
56 | MathInput: null, // Sett to the MathInput element to use MathInput
57 | removeContent: 'Remove',
58 | theme: {
59 | // CSS classes, either you define them or they come from the plugin.css import
60 | saveButton: '',
61 | removeButton: '',
62 | invalidButton: '',
63 | buttons: '',
64 | }
65 | // function (string) -> string.
66 | translator: null,
67 | }
68 | ```
69 |
70 |
71 | ## Loading katex async
72 | If you want to load katex in the background instead of right away, then you can do this by wrapping the katex object that you pass into the plugin:
73 |
74 | ```js
75 | //file: asyncKatex.js
76 | let katex = null
77 | const renderQueue = []
78 |
79 | System.import(/* webpackChunkName: 'katex' */ 'katex')
80 | .then(function methodName(module) {
81 | katex = module.default
82 | })
83 | .then(() => {
84 | console.log('Katex loaded, ', renderQueue)
85 | if (renderQueue.length) {
86 | const now = Date.now()
87 | renderQueue.map(([d, expression, baseNode, options]) => {
88 | if (now - d < 4000) {
89 | katex.render(expression, baseNode, options)
90 | }
91 | })
92 | }
93 | })
94 |
95 | export default {
96 | render: (expression, baseNode, options) => {
97 | if (katex) {
98 | return katex.render(expression, baseNode, options)
99 | }
100 |
101 | renderQueue.push([Date.now(), expression, baseNode, options])
102 | },
103 | // parse is only used by this plugin to check syntax validity.
104 | __parse: (expression, options) => {
105 | if (katex) {
106 | return katex.parse(expression, options)
107 | }
108 | return null
109 | }
110 | }
111 |
112 |
113 | ```
114 |
115 | Store this in a separate file and and pass it to the plugin config:
116 |
117 | ```js
118 | import createKaTeXPlugin from 'draft-js-katex-plugin';
119 | import katex from './asyncKatex'
120 |
121 | const kaTeXPlugin = createKaTeXPlugin({katex});
122 |
123 | ```
124 |
--------------------------------------------------------------------------------
/src/components/TeXBlock.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import unionClassNames from 'union-class-names';
3 | import KatexOutput from './KatexOutput';
4 |
5 | export default class TeXBlock extends Component {
6 | constructor(props) {
7 | super(props);
8 | this.state = { editMode: false };
9 | }
10 |
11 | onClick = () => {
12 | if (this.state.editMode || this.props.store.getReadOnly()) {
13 | return;
14 | }
15 | this.setState(
16 | {
17 | editMode: true,
18 | ...this.getValue(),
19 | },
20 | () => {
21 | this.startEdit();
22 | }
23 | );
24 | };
25 |
26 | onValueChange = evt => {
27 | const value = evt.target.value;
28 | this.onMathInputChange(value);
29 | };
30 |
31 | onFocus = () => {
32 | if (this.callbacks.blur) {
33 | this.callbacks.blur();
34 | }
35 | };
36 |
37 | onMathInputChange = inputValue => {
38 | let invalid = false;
39 | const value = this.props.translator(inputValue);
40 | try {
41 | this.props.katex.__parse(value); // eslint-disable-line no-underscore-dangle
42 | } catch (e) {
43 | invalid = true;
44 | } finally {
45 | this.setState({
46 | invalidTeX: invalid,
47 | value,
48 | inputValue,
49 | });
50 | }
51 | };
52 |
53 | getValue = () => {
54 | const contentState = this.props.store.getEditorState().getCurrentContent();
55 | return contentState.getEntity(this.props.block.getEntityAt(0)).getData();
56 | };
57 |
58 | callbacks = {};
59 |
60 | startEdit = () => {
61 | const { block, blockProps } = this.props;
62 | blockProps.onStartEdit(block.getKey());
63 | };
64 |
65 | finishEdit = newContentState => {
66 | const { block, blockProps } = this.props;
67 | blockProps.onFinishEdit(block.getKey(), newContentState);
68 | };
69 |
70 | remove = () => {
71 | const { block, blockProps } = this.props;
72 | blockProps.onRemove(block.getKey());
73 | };
74 |
75 | save = () => {
76 | const { block, store } = this.props;
77 |
78 | const entityKey = block.getEntityAt(0);
79 | const editorState = store.getEditorState();
80 |
81 | const contentState = editorState.getCurrentContent();
82 |
83 | contentState.mergeEntityData(entityKey, {
84 | value: this.state.value,
85 | inputValue: this.state.inputValue,
86 | });
87 |
88 | this.setState(
89 | {
90 | invalidTeX: false,
91 | editMode: false,
92 | value: null,
93 | },
94 | this.finishEdit.bind(this, editorState)
95 | );
96 | };
97 |
98 | render() {
99 | const { theme, doneContent, removeContent, katex } = this.props;
100 |
101 | let texContent = null;
102 | if (this.state.editMode) {
103 | if (this.state.invalidTeX) {
104 | texContent = '';
105 | } else {
106 | texContent = this.state.value;
107 | }
108 | } else {
109 | texContent = this.getValue().value;
110 | }
111 | const displayMode = this.getValue().displayMode;
112 |
113 | let className = theme.tex;
114 | if (this.state.editMode) {
115 | className = unionClassNames(className, theme.activeTeX);
116 | }
117 |
118 | let editPanel = null;
119 | if (this.state.editMode) {
120 | let buttonClass = theme.saveButton;
121 | if (this.state.invalidTeX) {
122 | buttonClass = unionClassNames(buttonClass, theme.invalidButton);
123 | }
124 |
125 | editPanel = (
126 |
127 |
133 |
134 |
137 |
140 |
141 |
142 | );
143 | }
144 |
145 | const MathInput = this.props.MathInput || KatexOutput;
146 | return (
147 |
148 | {this.state.editMode ? (
149 |
156 | ) : (
157 |
158 | )}
159 |
160 | {editPanel}
161 |
162 | );
163 | }
164 | }
165 |
--------------------------------------------------------------------------------