2023-12-12 09:06:58 -08:00
|
|
|
/**
|
|
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
|
|
*
|
|
|
|
* This source code is licensed under the MIT license found in the
|
|
|
|
* LICENSE file in the root directory of this source tree.
|
|
|
|
*
|
|
|
|
* @format
|
|
|
|
*/
|
|
|
|
|
|
|
|
import React, {
|
|
|
|
CSSProperties,
|
|
|
|
Suspense,
|
|
|
|
lazy,
|
|
|
|
useCallback,
|
|
|
|
useEffect,
|
2023-12-13 20:05:29 -08:00
|
|
|
useLayoutEffect,
|
2023-12-12 09:06:58 -08:00
|
|
|
useRef,
|
|
|
|
useState,
|
|
|
|
} from 'react';
|
|
|
|
|
|
|
|
import {usePrismTheme} from '@docusaurus/theme-common';
|
|
|
|
import clsx from 'clsx';
|
|
|
|
import {LiveProvider, LiveEditor, LivePreview, LiveError} from 'react-live';
|
|
|
|
import EditorToolbar from './EditorToolbar';
|
|
|
|
|
|
|
|
import type {FlexStyle} from './FlexStyle';
|
|
|
|
import type {StyleNode} from './YogaViewer';
|
|
|
|
|
|
|
|
import styles from './Playground.module.css';
|
2023-12-19 12:46:26 -08:00
|
|
|
import useIsBrowser from '@docusaurus/useIsBrowser';
|
2023-12-12 09:06:58 -08:00
|
|
|
|
|
|
|
export type Props = Readonly<{
|
2023-12-19 12:46:26 -08:00
|
|
|
code: string;
|
2023-12-12 09:06:58 -08:00
|
|
|
height?: CSSProperties['height'];
|
|
|
|
autoFocus?: boolean;
|
|
|
|
}>;
|
|
|
|
|
|
|
|
export default function Playground({code, height, autoFocus}: Props) {
|
|
|
|
const prismTheme = usePrismTheme();
|
2023-12-13 20:05:29 -08:00
|
|
|
const editorScrollRef = useRef<HTMLDivElement>(null);
|
2023-12-19 12:46:26 -08:00
|
|
|
const isBrowser = useIsBrowser();
|
2023-12-13 20:05:29 -08:00
|
|
|
|
2023-12-19 12:46:26 -08:00
|
|
|
const [liveCode, setLiveCode] = useState(code);
|
2023-12-15 22:46:58 -08:00
|
|
|
const [hasCodeChanged, setHasCodeChanged] = useState(false);
|
2023-12-13 20:05:29 -08:00
|
|
|
const [scrollbarWidth, setScrollbarWidth] = useState(0);
|
2023-12-12 09:06:58 -08:00
|
|
|
|
2023-12-15 22:46:58 -08:00
|
|
|
// Once react-live has hydrated the content-editable area, set focus to it
|
|
|
|
// if requested
|
2023-12-12 09:06:58 -08:00
|
|
|
useEffect(() => {
|
2023-12-15 22:46:58 -08:00
|
|
|
if (autoFocus && hasCodeChanged) {
|
|
|
|
const codeElem = editorScrollRef?.current?.querySelector('.prism-code');
|
2023-12-12 09:06:58 -08:00
|
|
|
const sel = window.getSelection();
|
2023-12-13 20:05:29 -08:00
|
|
|
if (codeElem?.clientHeight && sel != null) {
|
2023-12-12 09:06:58 -08:00
|
|
|
sel.selectAllChildren(codeElem);
|
|
|
|
sel.collapseToStart();
|
|
|
|
}
|
|
|
|
}
|
2023-12-15 22:46:58 -08:00
|
|
|
}, [autoFocus, hasCodeChanged]);
|
2023-12-12 09:06:58 -08:00
|
|
|
|
2023-12-13 20:05:29 -08:00
|
|
|
useLayoutEffect(() => {
|
|
|
|
// The toolbar is positioned relative to the outside of the scrolling
|
|
|
|
// container so it stays in the same place when scrolling, but this means
|
2023-12-15 22:46:58 -08:00
|
|
|
// it isn't automatically adjusted for scrollbar width. If code change
|
|
|
|
// causes overflow/scrollbar, adjust its position based on its width progrmatically.
|
2023-12-13 20:05:29 -08:00
|
|
|
if (editorScrollRef.current) {
|
|
|
|
setScrollbarWidth(
|
|
|
|
editorScrollRef.current.offsetWidth -
|
|
|
|
editorScrollRef.current.clientWidth,
|
|
|
|
);
|
|
|
|
}
|
2023-12-15 22:46:58 -08:00
|
|
|
}, [editorScrollRef, code]);
|
2023-12-13 20:05:29 -08:00
|
|
|
|
2023-12-12 09:06:58 -08:00
|
|
|
const heightStyle = height
|
|
|
|
? ({'--yg-playground-height': height} as React.CSSProperties)
|
|
|
|
: undefined;
|
|
|
|
|
2023-12-19 12:46:26 -08:00
|
|
|
const handleCodeChange = useCallback((code: string) => {
|
|
|
|
setHasCodeChanged(true);
|
|
|
|
setLiveCode(code);
|
|
|
|
}, []);
|
|
|
|
|
2023-12-12 09:06:58 -08:00
|
|
|
return (
|
2023-12-15 22:46:58 -08:00
|
|
|
<LiveProvider
|
|
|
|
code={liveCode}
|
|
|
|
theme={prismTheme}
|
|
|
|
scope={{Node: LiveNode, Layout: RootLiveNode}}>
|
|
|
|
<div className={styles.wrapper} style={heightStyle}>
|
2024-03-12 15:17:57 -07:00
|
|
|
<div className={clsx(styles.playgroundRow)}>
|
2023-12-13 20:05:29 -08:00
|
|
|
<div className={clsx(styles.editorColumn, 'playground-editor')}>
|
|
|
|
<div className={styles.editorScroll} ref={editorScrollRef}>
|
|
|
|
<EditorToolbar
|
2023-12-15 22:46:58 -08:00
|
|
|
code={liveCode}
|
2023-12-13 20:05:29 -08:00
|
|
|
className={styles.editorToolbar}
|
|
|
|
style={{paddingRight: scrollbarWidth + 'px'}}
|
|
|
|
/>
|
2023-12-19 12:46:26 -08:00
|
|
|
|
|
|
|
{isBrowser ? (
|
|
|
|
<LiveEditor
|
|
|
|
className={clsx(styles.playgroundEditor)}
|
|
|
|
onChange={handleCodeChange}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<LiveEditorFallback code={liveCode} />
|
|
|
|
)}
|
2023-12-13 20:05:29 -08:00
|
|
|
</div>
|
2023-12-12 09:06:58 -08:00
|
|
|
</div>
|
|
|
|
<div className={clsx(styles.previewColumn)}>
|
2023-12-15 22:46:58 -08:00
|
|
|
<LivePreview className={clsx(styles.livePreview)} />
|
2023-12-12 09:06:58 -08:00
|
|
|
<LiveError className={clsx(styles.liveError)} />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</LiveProvider>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-12-19 12:46:26 -08:00
|
|
|
/**
|
|
|
|
* Provides a non-editable approximation of the LiveEditor result, without
|
|
|
|
* relying on prism rendering, for use during SSR.
|
|
|
|
* See https://github.com/facebook/docusaurus/issues/9629
|
|
|
|
*/
|
|
|
|
function LiveEditorFallback({code}: Readonly<{code: string}>) {
|
|
|
|
return (
|
|
|
|
<div className={clsx(styles.playgroundEditor)}>
|
|
|
|
<pre className={clsx('prism-code', styles.liveEditorFallback)}>
|
|
|
|
{code}
|
|
|
|
</pre>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-12-15 22:46:58 -08:00
|
|
|
type RootLiveNodeProps = Readonly<{
|
2023-12-12 09:06:58 -08:00
|
|
|
children: React.ReactNode;
|
|
|
|
config?: {useWebDefaults?: boolean};
|
|
|
|
}>;
|
|
|
|
|
2023-12-15 22:46:58 -08:00
|
|
|
function RootLiveNode({children, config}: RootLiveNodeProps) {
|
2023-12-12 09:06:58 -08:00
|
|
|
if (React.Children.count(children) !== 1) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const child = React.Children.only(children);
|
2023-12-15 22:46:58 -08:00
|
|
|
if (!React.isValidElement(child) || child.type !== LiveNode) {
|
2023-12-12 09:06:58 -08:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2023-12-15 22:46:58 -08:00
|
|
|
const styleNode = styleNodeFromLiveNode(child as unknown as LiveNode);
|
2023-12-12 09:06:58 -08:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Suspense fallback={null}>
|
|
|
|
<LazyYogaViewer
|
|
|
|
rootNode={styleNode}
|
|
|
|
useWebDefaults={config?.useWebDefaults}
|
|
|
|
/>
|
|
|
|
</Suspense>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-12-15 22:46:58 -08:00
|
|
|
type LiveNodeProps = Readonly<{
|
2023-12-12 09:06:58 -08:00
|
|
|
children: React.ReactNode;
|
|
|
|
style: FlexStyle;
|
|
|
|
}>;
|
|
|
|
|
2023-12-15 22:46:58 -08:00
|
|
|
class LiveNode extends React.PureComponent<LiveNodeProps> {}
|
2023-12-12 09:06:58 -08:00
|
|
|
|
2023-12-15 22:46:58 -08:00
|
|
|
function styleNodeFromLiveNode(
|
|
|
|
liveNode: React.ElementRef<typeof LiveNode>,
|
2023-12-12 09:06:58 -08:00
|
|
|
): StyleNode {
|
|
|
|
const children: StyleNode[] = [];
|
|
|
|
|
2023-12-15 22:46:58 -08:00
|
|
|
React.Children.forEach(liveNode.props.children, child => {
|
|
|
|
if (React.isValidElement(child) && child.type === LiveNode) {
|
|
|
|
children.push(styleNodeFromLiveNode(child as unknown as LiveNode));
|
2023-12-12 09:06:58 -08:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
2023-12-15 22:46:58 -08:00
|
|
|
style: liveNode.props.style,
|
2023-12-12 09:06:58 -08:00
|
|
|
children,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Docusaurus SSR does not correctly support top-level await in the import
|
|
|
|
// chain
|
|
|
|
// 1. https://github.com/facebook/docusaurus/issues/7238
|
|
|
|
// 2. https://github.com/facebook/docusaurus/issues/9468
|
|
|
|
const LazyYogaViewer = lazy(() => import('./YogaViewer'));
|