react-components
Version:
React components used by Khan Academy
221 lines (194 loc) • 6.63 kB
JavaScript
"use strict";
/**
* For math rendered using KaTex and/or MathJax. Use me like <TeX>2x + 3</TeX>.
*/
/* global katex, MathJax */
// TODO(joel) - require MathJax / katex so they don't have to be global
const PureRenderMixin = require('react-addons-pure-render-mixin');
const React = require('react');
const ReactDOM = require('react-dom');
const katexA11y = require('./katex-a11y.js');
let pendingScripts = [];
let pendingCallbacks = [];
let needsProcess = false;
const process = function(script, callback) {
pendingScripts.push(script);
pendingCallbacks.push(callback);
if (!needsProcess) {
needsProcess = true;
setTimeout(doProcess, 0);
}
};
const loadMathJax = function(callback) {
if (typeof MathJax !== "undefined") {
callback();
} else if (typeof Khan !== "undefined" && Khan.mathJaxLoaded) {
Khan.mathJaxLoaded.then(callback);
} else {
throw new Error(
"MathJax wasn't loaded before it was needed by <TeX/>");
}
};
const doProcess = function() {
loadMathJax(function() {
MathJax.Hub.Queue(function() {
const oldElementScripts = MathJax.Hub.elementScripts;
MathJax.Hub.elementScripts = function(element) {return pendingScripts;};
try {
return MathJax.Hub.Process(null, function() {
// Trigger all of the pending callbacks before clearing them
// out.
for (const callback of pendingCallbacks) {
callback();
}
pendingScripts = [];
pendingCallbacks = [];
needsProcess = false;
});
} catch (e) {
// IE8 requires `catch` in order to use `finally`
throw e;
} finally {
MathJax.Hub.elementScripts = oldElementScripts;
}
});
});
};
// Make content only visible to screen readers.
// Both collegeboard.org and Bootstrap 3 use this exact implementation.
const srOnly = {
border: 0,
clip: "rect(0,0,0,0)",
height: "1px",
margin: "-1px",
overflow: "hidden",
padding: 0,
position: "absolute",
width: "1px",
};
const TeX = React.createClass({displayName: "TeX",
propTypes: {
children: React.PropTypes.node,
onClick: React.PropTypes.func,
onRender: React.PropTypes.func,
style: React.PropTypes.any,
},
mixins: [PureRenderMixin],
getDefaultProps: function() {
return {
// Called after math is rendered or re-rendered
onRender: function() {},
onClick: null,
};
},
componentDidMount: function() {
this._root = ReactDOM.findDOMNode(this);
if (this.refs.katex.childElementCount > 0) {
// If we already rendered katex in the render function, we don't
// need to render anything here.
this.props.onRender(this._root);
return;
}
const text = this.props.children;
this.setScriptText(text);
process(this.script, function() {return this.props.onRender(this._root);}.bind(this));
},
componentDidUpdate: function(prevProps, prevState) {
// If we already rendered katex in the render function, we don't
// need to render anything here.
if (this.refs.katex.childElementCount > 0) {
if (this.script) {
// If we successfully rendered KaTeX, check if there's
// lingering MathJax from the last render, and if so remove it.
loadMathJax(function() {
const jax = MathJax.Hub.getJaxFor(this.script);
if (jax) {
jax.Remove();
}
}.bind(this));
}
this.props.onRender();
return;
}
const newText = this.props.children;
if (this.script) {
loadMathJax(function() {
MathJax.Hub.Queue(function() {
const jax = MathJax.Hub.getJaxFor(this.script);
if (jax) {
return jax.Text(newText, this.props.onRender);
} else {
this.setScriptText(newText);
process(this.script, this.props.onRender);
}
}.bind(this));
}.bind(this));
} else {
this.setScriptText(newText);
process(this.script, this.props.onRender);
}
},
componentWillUnmount: function() {
if (this.script) {
loadMathJax(function() {
const jax = MathJax.Hub.getJaxFor(this.script);
if (jax) {
jax.Remove();
}
}.bind(this));
}
},
setScriptText: function(text) {
if (!this.script) {
this.script = document.createElement("script");
this.script.type = "math/tex";
ReactDOM.findDOMNode(this.refs.mathjax).appendChild(this.script);
}
if ("text" in this.script) {
// IE8, etc
this.script.text = text;
} else {
this.script.textContent = text;
}
},
render: function() {
let katexHtml = null;
try {
katexHtml = {
__html: katex.renderToString(this.props.children),
};
} catch (e) {
/* jshint -W103 */
if (e.__proto__ !== katex.ParseError.prototype) {
/* jshint +W103 */
throw e;
}
}
let katexA11yHtml = null;
if (katexHtml) {
try {
katexA11yHtml = {
__html: katexA11y.renderString(this.props.children),
};
} catch (e) {
// Nothing
}
}
return React.createElement("span", {
style: this.props.style,
onClick: this.props.onClick
},
React.createElement("span", {ref: "mathjax"}),
React.createElement("span", {
ref: "katex",
dangerouslySetInnerHTML: katexHtml,
"aria-hidden": !!katexHtml && !!katexA11yHtml}
),
React.createElement("span", {
dangerouslySetInnerHTML: katexA11yHtml,
style: srOnly}
)
);
},
});
module.exports = TeX;