diff --git a/.gitignore b/.gitignore index 8af2535..1e2c69d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ lib/* /.idea/ +node_modules diff --git a/.npmignore b/.npmignore index 7bc73bc..4e0df9a 100644 --- a/.npmignore +++ b/.npmignore @@ -4,7 +4,7 @@ node_modules # sources -/src/ +/draft-js-katex-plugin/src.bak/ webpack.config.js .babelrc .gitignore diff --git a/.storybook/config.js b/.storybook/config.js new file mode 100644 index 0000000..9154670 --- /dev/null +++ b/.storybook/config.js @@ -0,0 +1,7 @@ +import { configure } from '@storybook/react'; + +function loadStories() { + require('../stories'); +} + +configure(loadStories, module); diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html new file mode 100644 index 0000000..0bc1f2a --- /dev/null +++ b/.storybook/preview-head.html @@ -0,0 +1,2 @@ + + diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js new file mode 100644 index 0000000..3e11872 --- /dev/null +++ b/.storybook/webpack.config.js @@ -0,0 +1,51 @@ +// you can use this file to add your custom webpack plugins, loaders and anything you like. +// This is just the basic way to add additional webpack configurations. +// For more information refer the docs: https://storybook.js.org/docs/react-storybook/configurations/custom-webpack-config + +// IMPORTANT +// When you add this file, we won't add the default configurations which is similar +// to "React Create App". This only has babel loader to load JavaScript. +const path = require('path'); + +module.exports = { + plugins: [ + // your custom plugins + ], + module: { + rules: [ + /* + { + test: /\.scss$/, + loaders: ["style-loader", "css-loader", "sass-loader", "postcss-loader"], + include: path.resolve(__dirname, '../') + }, + */ + { + test: /\.css$/, + loaders: ['style-loader', 'css-loader', 'postcss-loader'], + include: path.resolve(__dirname, '../'), + }, + ], + /* + loaders: [ + + { + test: /\.css$/, + use: [ + { + loader: "style-loader" + }, + { + loader: "css-loader", + options: { + modules: true, + importLoaders: 1, + localIdentName: 'draftJsKatexPlugin__[local]__[hash:base64:5]', + }, + }, + 'postcss-loader' + ] + }] + */ + }, +}; diff --git a/README.md b/README.md index e0d26e5..e245d62 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,118 @@ 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). +- Integrated with [Khan/math-input](https://github.com/Khan/math-input). + ## Usage -Add KaTeX style +Add MathQuill libs if you want to use MathInput: ```html - + + ``` Add plugin ```js import createKaTeXPlugin from 'draft-js-katex-plugin'; +import katex from 'katex' + +const kaTeXPlugin = createKaTeXPlugin({katex}); + +const { InsertButton } = kaTeXPlugin; +``` + +With MathInput: + +```js +import createKaTeXPlugin from 'draft-js-katex-plugin'; +import katex from 'katex' +import MathInput from '../src/components/math-input/components/app'; + -const kaTeXPlugin = createKaTeXPlugin(); +const kaTeXPlugin = createKaTeXPlugin({katex, MathInput}); const { InsertButton } = kaTeXPlugin; ``` -## TODO +There are more examples in the `stories/index.js` file. + +## Configuration options: + +Here shown with defaults: +```js +{ + katex, // the katex object or a wrapper defining render() and __parse(). + + doneContent: { + valid: 'Done', + invalid: 'Invalid TeX', + }, + + MathInput: null, // Sett to the MathInput element to use MathInput + removeContent: 'Remove', + theme: { + // CSS classes, either you define them or they come from the plugin.css import + saveButton: '', + removeButton: '', + invalidButton: '', + buttons: '', + } + // function (string) -> string. + translator: null, +} +``` + -- [ ] Integrate with [Khan/math-input](https://github.com/Khan/math-input). +## Loading katex async +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: + +```js +//file: asyncKatex.js +let katex = null +const renderQueue = [] + +System.import(/* webpackChunkName: 'katex' */ 'katex') + .then(function methodName(module) { + katex = module.default + }) + .then(() => { + console.log('Katex loaded, ', renderQueue) + if (renderQueue.length) { + const now = Date.now() + renderQueue.map(([d, expression, baseNode, options]) => { + if (now - d < 4000) { + katex.render(expression, baseNode, options) + } + }) + } + }) + +export default { + render: (expression, baseNode, options) => { + if (katex) { + return katex.render(expression, baseNode, options) + } + + renderQueue.push([Date.now(), expression, baseNode, options]) + }, + // parse is only used by this plugin to check syntax validity. + __parse: (expression, options) => { + if (katex) { + return katex.parse(expression, options) + } + return null + } +} + + +``` + +Store this in a separate file and and pass it to the plugin config: + +```js +import createKaTeXPlugin from 'draft-js-katex-plugin'; +import katex from './asyncKatex' + +const kaTeXPlugin = createKaTeXPlugin({katex}); + +``` diff --git a/package.json b/package.json index 5173cdf..7c83b73 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,21 @@ "url": "https://github.com/letranloc/draft-js-katex-plugin/issues" }, "dependencies": { + "aphrodite": "^1.2.3", "decorate-component-with-props": "^1.0.2", "draft-js": ">=0.9.1", - "katex": "^0.7.1", + "jquery": "^3.2.1", + "katex": "^0.8.3", "react": "^15.4.2", + "react-addons-css-transition-group": "^15.6.0", + "react-addons-pure-render-mixin": "^15.6.0", + "react-redux": "^5.0.6", + "redux": "^3.7.2", "union-class-names": "^1.0.0" }, "devDependencies": { + "@storybook/react": "^3.2.8", + "asciimath-to-latex": "^0.3.2", "autoprefixer": "^6.7.7", "babel-cli": "^6.14.0", "babel-core": "^6.14.0", @@ -29,6 +37,7 @@ "babel-preset-react": "^6.11.1", "babel-preset-stage-0": "^6.5.0", "css-loader": "^0.27.2", + "draft-js-plugins-editor": "2.0.0-rc7", "eslint": "^3.5.0", "eslint-config-airbnb": "^14.1.0", "eslint-plugin-import": "^2.2.0", @@ -79,6 +88,8 @@ "lint:eslint": "eslint --rule 'mocha/no-exclusive-tests:2' ./", "lint:eslint:fix": "eslint --fix --rule 'mocha/no-exclusive-tests:2' ./", "lint:staged": "lint-staged", - "prepublish": "npm run build" + "prepublish": "npm run build", + "storybook": "start-storybook -p 6006", + "build-storybook": "build-storybook" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..b58cda9 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,7 @@ +/* eslint-disable global-require */ + +module.exports = { + plugins: [ + require('autoprefixer')({ browsers: ['> 1%'] }) + ] +}; diff --git a/src/components/InsertKatexButton.js b/src/components/InsertKatexButton.js index 1fddb0b..083b72f 100644 --- a/src/components/InsertKatexButton.js +++ b/src/components/InsertKatexButton.js @@ -1,33 +1,34 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Children, Component } from 'react'; +import PropTypes from 'prop-types'; import unionClassNames from 'union-class-names'; import insertTeXBlock from '../modifiers/insertTeXBlock'; export default class InsertKatexButton extends Component { static propTypes = { - children: PropTypes.node.isRequired, + children: PropTypes.node, + initialValue: PropTypes.string, + translator: PropTypes.func.isRequired, theme: PropTypes.any, }; - static defaultProps = { - theme: {} + initialValue: null, + tex: null, }; onClick = () => { - const { store } = this.props; + const { store, translator, initialValue } = this.props; const editorState = store.getEditorState(); - store.setEditorState(insertTeXBlock(editorState)); + store.setEditorState(insertTeXBlock(editorState, translator, initialValue)); }; render() { - const { theme, className, children } = this.props; + const { theme = {}, className, children, defaultContent } = this.props; const combinedClassName = unionClassNames(theme.insertButton, className); + const content = Children.count(children) ? children : defaultContent; return ( - ); } diff --git a/src/components/KatexOutput.js b/src/components/KatexOutput.js index ff503ae..bb1de71 100644 --- a/src/components/KatexOutput.js +++ b/src/components/KatexOutput.js @@ -1,7 +1,16 @@ -import katex from 'katex'; import React from 'react'; +import PropTypes from 'prop-types'; export default class KatexOutput extends React.Component { + static propTypes = { + value: PropTypes.string.isRequired, + katex: PropTypes.object.isRequired, + displayMode: PropTypes.bool.isRequired, + onClick: PropTypes.func, + }; + static defaultProps = { + onClick: () => {}, + }; constructor(props) { super(props); this.timer = null; @@ -11,8 +20,8 @@ export default class KatexOutput extends React.Component { this.update(); } - componentWillReceiveProps({ content }) { - if (content !== this.props.content) { + componentWillReceiveProps({ value }) { + if (value !== this.props.value) { this.update(); } } @@ -22,24 +31,25 @@ export default class KatexOutput extends React.Component { this.timer = null; } - update() { + update = () => { + const { katex } = this.props; if (this.timer) { clearTimeout(this.timer); } this.timer = setTimeout(() => { - katex.render( - this.props.content, - this.container, - { displayMode: true } - ); + katex.render(this.props.value, this.container, { + displayMode: this.props.displayMode, + }); }, 0); - } + }; render() { return (
{ this.container = container; }} + ref={container => { + this.container = container; + }} onClick={this.props.onClick} /> ); diff --git a/src/components/TeXBlock.js b/src/components/TeXBlock.js index 2bd38c6..e26cb49 100644 --- a/src/components/TeXBlock.js +++ b/src/components/TeXBlock.js @@ -1,8 +1,7 @@ import React, { Component } from 'react'; -import katex from 'katex'; -import { Entity } from 'draft-js'; import unionClassNames from 'union-class-names'; import KatexOutput from './KatexOutput'; +//import MathInput from './math-input/components/app'; export default class TeXBlock extends Component { constructor(props) { @@ -10,38 +9,54 @@ export default class TeXBlock extends Component { this.state = { editMode: false }; } + callbacks = {}; + onClick = () => { - if (this.state.editMode) { + if (this.state.editMode || this.props.store.getReadOnly()) { return; } - - this.setState({ - editMode: true, - texValue: this.getValue(), - }, () => { - this.startEdit(); - }); + this.setState( + { + editMode: true, + ...this.getValue(), + }, + () => { + this.startEdit(); + } + ); }; - onValueChange = (evt) => { + onValueChange = evt => { const value = evt.target.value; + this.onMathInputChange(value); + }; + + onFocus = () => { + if (this.callbacks.blur) { + this.callbacks.blur(); + } + }; + + onMathInputChange = inputValue => { let invalid = false; + const value = this.props.translator(inputValue); try { - katex.__parse(value); // eslint-disable-line no-underscore-dangle + this.props.katex.__parse(value); // eslint-disable-line no-underscore-dangle } catch (e) { invalid = true; } finally { this.setState({ invalidTeX: invalid, - texValue: value, + value, + inputValue, }); } }; getValue = () => { - const entityKey = this.props.block.getEntityAt(0); - const entityData = Entity.get(entityKey).getData(); - return entityData.content; + const contentState = this.props.store.getEditorState().getCurrentContent(); + const entityData = contentState.getEntity(this.props.block.getEntityAt(0)).getData(); + return entityData; }; startEdit = () => { @@ -49,7 +64,7 @@ export default class TeXBlock extends Component { blockProps.onStartEdit(block.getKey()); }; - finishEdit = (newContentState) => { + finishEdit = newContentState => { const { block, blockProps } = this.props; blockProps.onFinishEdit(block.getKey(), newContentState); }; @@ -65,28 +80,37 @@ export default class TeXBlock extends Component { const entityKey = block.getEntityAt(0); const editorState = store.getEditorState(); - Entity.mergeData(entityKey, { content: this.state.texValue }); + const contentState = editorState.getCurrentContent(); + + const newContentState = contentState.mergeEntityData(entityKey, { + value: this.state.value, + inputValue: this.state.inputValue, + }); - this.setState({ - invalidTeX: false, - editMode: false, - texValue: null, - }, this.finishEdit.bind(this, editorState)); + this.setState( + { + invalidTeX: false, + editMode: false, + value: null, + }, + this.finishEdit.bind(this, editorState) + ); }; render() { - const { theme, doneContent, removeContent } = this.props; + const { theme, doneContent, removeContent, katex } = this.props; let texContent = null; if (this.state.editMode) { if (this.state.invalidTeX) { texContent = ''; } else { - texContent = this.state.texValue; + texContent = this.state.value; } } else { - texContent = this.getValue(); + texContent = this.getValue().value; } + const displayMode = this.getValue().displayMode; let className = theme.tex; if (this.state.editMode) { @@ -101,29 +125,21 @@ export default class TeXBlock extends Component { } editPanel = ( -
+