/** * Copyright (c) 2013-present, Facebook, Inc. * * @emails react-core */ import React, {Component} from 'react'; import ReactDOM from 'react-dom'; import Remarkable from 'remarkable'; import {LiveEditor, LiveProvider} from 'react-live'; import {colors, media} from 'theme'; import MetaTitle from 'templates/components/MetaTitle'; const compileES5 = ( code, // eslint-disable-next-line no-undef ) => Babel.transform(code, {presets: ['es2015', 'react']}).code; // eslint-disable-next-line no-undef const compileES6 = code => Babel.transform(code, {presets: ['react']}).code; class CodeEditor extends Component { constructor(props, context) { super(props, context); this.state = this._updateState(props.code); this.state.showJSX = true; } componentDidMount() { // Initial render() will always be a no-op, // Because the mountNode ref won't exist yet. this._render(); } componentDidUpdate(prevProps, prevState) { if (prevState.compiled !== this.state.compiled) { this._render(); } } render() { const {children} = this.props; const { compiledES6, code, error, showBabelErrorMessage, showJSX, } = this.state; let errorMessage; if (showBabelErrorMessage) { errorMessage = ( <span> Babel could not be loaded. <br /> <br /> This can be caused by an ad blocker. If you're using one, consider adding reactjs.org to the whitelist so the live code examples will work. </span> ); } else if (error != null) { errorMessage = error.message; } return ( <LiveProvider code={showJSX ? code : compiledES6} mountStylesheet={false}> <div css={{ [media.greaterThan('xlarge')]: { display: 'flex', flexDirection: 'row', }, [media.lessThan('large')]: { display: 'block', }, }}> {children && ( <div css={{ flex: '0 0 33%', [media.lessThan('xlarge')]: { marginBottom: 20, }, '& h3': { color: colors.dark, maxWidth: '11em', paddingTop: 0, }, '& p': { marginTop: 15, marginRight: 40, lineHeight: 1.7, [media.greaterThan('xlarge')]: { marginTop: 25, }, }, }}> {children} </div> )} <div css={{ [media.greaterThan('medium')]: { flex: '0 0 67%', display: 'flex', alignItems: 'stretch', flexDirection: 'row', }, [media.lessThan('small')]: { display: 'block', }, }}> <div css={{ flex: '0 0 70%', overflow: 'hidden', borderRadius: '10px 0 0 10px', [media.lessThan('medium')]: { borderRadius: '10px 10px 0 0', }, }}> <div css={{ padding: '0px 10px', background: colors.darker, color: colors.white, }}> <MetaTitle onDark={true}> Live JSX Editor <label css={{ fontSize: 14, float: 'right', cursor: 'pointer', }}> <input checked={this.state.showJSX} onChange={event => this.setState({showJSX: event.target.checked}) } type="checkbox" />{' '} JSX? </label> </MetaTitle> </div> <div css={{ height: '100%', width: '100%', borderRadius: '0', maxHeight: '340px !important', marginTop: '0 !important', marginLeft: '0 !important', paddingLeft: '0 !important', marginRight: '0 !important', paddingRight: '0 !important', marginBottom: '0 !important', paddingBottom: '20px !important', [media.lessThan('medium')]: { marginBottom: '0 !important', }, '& pre.prism-code[contenteditable]': { outline: 0, overflow: 'auto', marginRight: '0 !important', marginBottom: '0 !important', }, }} className="gatsby-highlight"> <LiveEditor ignoreTabKey={true} onChange={this._onChange} /> </div> </div> {error && ( <div css={{ flex: '0 0 30%', overflow: 'hidden', border: `1px solid ${colors.error}`, borderRadius: '0 10px 10px 0', fontSize: 12, lineHeight: 1.5, [media.lessThan('medium')]: { borderRadius: '0 0 10px 10px', }, }}> <div css={{ padding: '0px 10px', background: colors.error, color: colors.white, }}> <MetaTitle cssProps={{ color: colors.white, }}> Error </MetaTitle> </div> <pre css={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', color: colors.error, padding: 10, }}> {errorMessage} </pre> </div> )} {!error && ( <div css={{ flex: '0 0 30%', overflow: 'hidden', border: `1px solid ${colors.divider}`, borderRadius: '0 10px 10px 0', [media.lessThan('medium')]: { borderRadius: '0 0 10px 10px', }, }}> <div css={{ padding: '0 10px', backgroundColor: colors.divider, }}> <MetaTitle>Result</MetaTitle> </div> <div css={{ padding: 10, maxHeight: '340px !important', overflow: 'auto', '& input': { width: '100%', display: 'block', border: '1px solid #ccc', // TODO padding: 5, }, '& button': { marginTop: 10, padding: '5px 10px', }, '& textarea': { width: '100%', marginTop: 10, height: 60, padding: 5, }, }} ref={this._setMountRef} /> </div> )} </div> </div> </LiveProvider> ); } _render() { if (!this._mountNode) { return; } const {compiled} = this.state; try { // Example code requires React, ReactDOM, and Remarkable to be within scope. // It also requires a "mountNode" variable for ReactDOM.render() // eslint-disable-next-line no-new-func new Function('React', 'ReactDOM', 'Remarkable', 'mountNode', compiled)( React, ReactDOM, Remarkable, this._mountNode, ); } catch (error) { console.error(error); this.setState({ compiled: null, error, }); } } _setMountRef = ref => { this._mountNode = ref; }; _updateState(code, showJSX = true) { try { let newState = { compiled: compileES5(code), error: null, }; if (showJSX) { newState.code = code; newState.compiledES6 = compileES6(code); } else { newState.compiledES6 = code; } return newState; } catch (error) { console.error(error); // Certain ad blockers (eg Fair AdBlocker) prevent Babel from loading. // If we suspect this is the case, we can show a more helpful error. const showBabelErrorMessage = !window.Babel; return { compiled: null, error, showBabelErrorMessage, }; } } _onChange = code => { this.setState(state => this._updateState(code, state.showJSX)); }; } export default CodeEditor;