import React from 'react'; import PropTypes from 'prop-types'; import { LiveProvider, LiveError, LivePreview } from 'react-live'; import classnames from 'classnames'; import { Collapse } from 'react-collapse'; import DuplicateSmall from '../icons/DuplicateSmall'; import RevertSmall from '../icons/RevertSmall'; import CodeSmall from '../icons/CodeSmall'; import MagicWandSmall from '../icons/MagicWandSmall'; import LinkSmall from '../icons/LinkSmall'; import { transformCode, formatCode } from './doctor-code'; import { CopyButton } from '../CopyButton'; import ToggleSwitch from '../ui/toggle-switch'; import TextButton from '../TextButton'; import Editor from './Editor'; import styles from './index.scss'; import { getComponentsHints } from '../utils'; import { ComponentsHints } from '../typings/components-hints'; import { StoryConfig } from '../typings/story-config'; export interface Props { initialCode?: string; storage?: Storage; title?: string; figmaLink?: string; storyName?: string; scope?: object; compact?: boolean; initiallyOpen?: boolean; previewRow?: boolean; previewProps?: object & { className?: string }; autoRender?: boolean; darkBackground?: boolean; noBackground?: boolean; onChange?: Function; previewWarning?({ onConfirm: Function }): React.ReactNode | null; hints?: ComponentsHints; forceInitialCode?: boolean; examplesPreviewWrapper?: StoryConfig['story']['examplesPreviewWrapper']; hideCodeEditor?: boolean; } interface State { initialOriginalCode: string; initialFormattedCode: string; code: string; isRtl: boolean; isDarkBackground: boolean; isEditorOpened: boolean; parseError: object | null; renderPreview: boolean; setEditorValue: Function; hints?: ComponentsHints; } export default class LiveCodeExample extends React.PureComponent { static propTypes = { initialCode: PropTypes.string, title: PropTypes.string, figmaLink: PropTypes.string, storyName: PropTypes.string, storage: PropTypes.object, scope: PropTypes.object, compact: PropTypes.bool, initiallyOpen: PropTypes.bool, previewRow: PropTypes.bool, previewProps: PropTypes.object, autoRender: PropTypes.bool, darkBackground: PropTypes.bool, noBackground: PropTypes.bool, forceInitialCode: PropTypes.bool, }; static defaultProps = { compact: false, initiallyOpen: false, previewRow: false, previewProps: {}, autoRender: true, darkBackground: false, noBackground: false, forceInitialCode: false, onChange: () => {}, scope: {}, }; constructor(props: Props) { super(props); const { storyName, title, initialCode, darkBackground, compact, initiallyOpen, previewWarning, storage, forceInitialCode, } = props; const codeFromStorage = storage && storage.getItem(`${storyName}-${title}`); const initialOriginalCode = formatCode(initialCode).trim(); const formattedCode = codeFromStorage && !forceInitialCode ? codeFromStorage : initialOriginalCode; this.state = { initialOriginalCode, initialFormattedCode: formattedCode, code: formattedCode, isRtl: false, isDarkBackground: darkBackground, isEditorOpened: !compact || initiallyOpen, parseError: null, renderPreview: !Boolean(previewWarning), setEditorValue: null, }; } componentDidUpdate(prevProps: Props) { const { renderPreview } = this.state; if ( this.props.previewWarning !== prevProps.previewWarning && typeof prevProps.previewWarning === 'function' && renderPreview ) { this.setState({ renderPreview: false }); } } componentDidMount() { const { hints, scope } = this.props; this.setState({ hints: hints || getComponentsHints(scope) }); } prettifyCode = () => { try { this.setState( ({ code }) => ({ code: formatCode(code) }), () => this.state.setEditorValue(this.state.code), ); } catch (e) { console.error('Unable to prettify code', e); } }; onResetCode = () => { const code = formatCode(this.props.initialCode); this.state.setEditorValue(code); this.setState({ code }); }; onEditorChange = code => { const { title, onChange, storyName, storage } = this.props; this.setState({ code }, () => onChange(this.state.code)); storage && storage.setItem(`${storyName}-${title}`, code); }; onToggleRtl = (isRtl: boolean) => this.setState({ isRtl }); onToggleBackground = (isDarkBackground: boolean) => this.setState({ isDarkBackground }); onToggleCode = () => this.setState(state => ({ isEditorOpened: !state.isEditorOpened, })); transformCode = (code = '') => { const { parseError } = this.state; let newParseError; let returnValue = code; try { returnValue = transformCode(code); newParseError = null; } catch (error) { // @ts-expect-error newParseError = error.message; } if (newParseError !== parseError) { this.setState({ parseError: newParseError }); } return returnValue; }; previewWarning = () => this.props.previewWarning({ onConfirm: () => this.setState({ renderPreview: true }), }); renderError = () => { if (this.state.renderPreview) { return this.state.parseError ? (
{this.state.parseError}
) : ( ); } return null; }; renderLivePreview = () => { const { examplesPreviewWrapper, previewProps, previewRow } = this.props; const livePreviewComp = ( ); return examplesPreviewWrapper ? examplesPreviewWrapper({ component: livePreviewComp }) : livePreviewComp; }; render() { const { scope, compact, previewProps, autoRender, noBackground, hideCodeEditor, figmaLink, } = this.props; const { code, isRtl, isDarkBackground, isEditorOpened, renderPreview, hints, } = this.state; const dirty = this.state.initialOriginalCode !== this.state.code; return (
{!hideCodeEditor && !compact && (
} />
{dirty && (
this.prettifyCode()} prefixIcon={} > Prettify
)}
Imitate RTL: 
Dark Background: 
{dirty && ( } > Reset )}
)}
{renderPreview ? this.renderLivePreview() : this.previewWarning()} {this.renderError()}
{!hideCodeEditor && ( this.setState({ setEditorValue: updater }) } /> )}
{!hideCodeEditor && compact && (
} /> {figmaLink && } onClick={() => window.open(figmaLink, '_blank')}>Figma} {dirty && ( this.prettifyCode()} prefixIcon={} > Prettify )}
{dirty && ( } > Reset )} }> {this.state.isEditorOpened ? 'Hide' : 'Show'} code
)}
); } }