nocobase/.dumi/theme/builtins/Previewer.tsx

254 lines
7.6 KiB
TypeScript
Raw Normal View History

import React, { useState, useContext, useRef, useEffect } from 'react';
import Tabs, { TabPane } from 'rc-tabs';
// @ts-ignore
import { history } from 'dumi';
import type { IPreviewerComponentProps} from 'dumi/theme';
import {
context,
useCodeSandbox,
useRiddle,
useMotions,
useCopy,
useLocaleProps,
useDemoUrl,
useTSPlaygroundUrl,
Link,
AnchorLink,
usePrefersColor
} from 'dumi/theme';
import type { ICodeBlockProps } from './SourceCode';
import SourceCode from './SourceCode';
import './Previewer.less';
export interface IPreviewerProps extends IPreviewerComponentProps {
/**
* enable transform to change CSS containing block for demo
*/
transform?: boolean;
/**
* modify background for demo area
*/
background?: string;
/**
* collapse padding of demo area
*/
compact?: boolean;
/**
* configurations for action button
*/
hideActions?: ('CSB' | 'EXTERNAL' | 'RIDDLE')[];
/**
* show source code by default
*/
defaultShowCode?: boolean;
/**
* use iframe mode for this demo
*/
iframe?: true | number;
/**
* replace builtin demo url
*/
demoUrl?: string;
}
/**
* get source code type for file
* @param file file path
* @param source file source object
*/
function getSourceType(file: string, source: IPreviewerComponentProps['sources']['_']) {
// use file extension as source type first
let type = file.match(/\.(\w+)$/)?.[1];
if (!type) {
type = source.tsx ? 'tsx' : 'jsx';
}
return type as ICodeBlockProps['lang'];
}
const Previewer: React.FC<IPreviewerProps> = oProps => {
const demoRef = useRef();
const { locale } = useContext(context);
const props = useLocaleProps<IPreviewerProps>(locale, oProps);
const builtinDemoUrl = useDemoUrl(props.identifier);
const demoUrl = props.demoUrl || builtinDemoUrl;
const isActive = history?.location.hash === `#${props.identifier}`;
const isSingleFile = Object.keys(props.sources).length === 1;
const openCSB = useCodeSandbox(props.hideActions?.includes('CSB') ? null : props);
const openRiddle = useRiddle(props.hideActions?.includes('RIDDLE') ? null : props);
const [execMotions, isMotionRunning] = useMotions(props.motions || [], demoRef.current);
const [copyCode, copyStatus] = useCopy();
const [currentFile, setCurrentFile] = useState('_');
const [sourceType, setSourceType] = useState(
getSourceType(currentFile, props.sources[currentFile]),
);
const [showSource, setShowSource] = useState(Boolean(props.defaultShowCode));
const [iframeKey, setIframeKey] = useState(Math.random());
const currentFileCode =
props.sources[currentFile][sourceType] || props.sources[currentFile].content;
const playgroundUrl = useTSPlaygroundUrl(locale, currentFileCode);
const iframeRef = useRef<HTMLIFrameElement>();
const [color] = usePrefersColor();
// re-render iframe if prefers color changed
useEffect(() => {
setIframeKey(Math.random());
}, [color]);
function handleFileChange(filename: string) {
setCurrentFile(filename);
setSourceType(getSourceType(filename, props.sources[filename]));
}
return (
<div
style={props.style}
className={[
props.className,
'__dumi-default-previewer',
isActive ? '__dumi-default-previewer-target' : '',
]
.filter(Boolean)
.join(' ')}
id={props.identifier}
data-debug={props.debug || undefined}
data-iframe={props.iframe || undefined}
>
{props.iframe && <div className="__dumi-default-previewer-browser-nav" />}
<div
ref={demoRef}
className="__dumi-default-previewer-demo"
style={{
transform: props.transform ? 'translate(0, 0)' : undefined,
padding: props.compact || (props.iframe && props.compact !== false) ? '0' : undefined,
background: props.background,
}}
>
{props.iframe ? (
<iframe
title="dumi-previewer"
style={{
// both compatible with unit or non-unit, such as 100, 100px, 100vh
height: String(props.iframe).replace(/(\d)$/, '$1px'),
}}
key={iframeKey}
src={demoUrl}
ref={iframeRef}
/>
) : (
props.children
)}
</div>
<div className="__dumi-default-previewer-desc" data-title={props.title}>
{props.title && <AnchorLink to={`#${props.identifier}`}>{props.title}</AnchorLink>}
{props.description && (
<div
// eslint-disable-next-line
dangerouslySetInnerHTML={{ __html: props.description }}
/>
)}
</div>
<div className="__dumi-default-previewer-actions">
{openCSB && (
<button
title="Open demo on CodeSandbox.io"
className="__dumi-default-icon"
role="codesandbox"
onClick={openCSB}
/>
)}
{openRiddle && (
<button
title="Open demo on Riddle"
className="__dumi-default-icon"
role="riddle"
onClick={openRiddle}
/>
)}
{props.motions && (
<button
title="Execute motions"
className="__dumi-default-icon"
role="motions"
disabled={isMotionRunning}
onClick={() => execMotions()}
/>
)}
{props.iframe && (
<button
title="Reload demo iframe page"
className="__dumi-default-icon"
role="refresh"
onClick={() => setIframeKey(Math.random())}
/>
)}
{!props.hideActions?.includes('EXTERNAL') && (
<Link target="_blank" to={demoUrl}>
<button
title="Open demo in new tab"
className="__dumi-default-icon"
role="open-demo"
type="button"
/>
</Link>
)}
<span />
<button
title="Copy source code"
className="__dumi-default-icon"
role="copy"
data-status={copyStatus}
onClick={() => copyCode(currentFileCode)}
/>
{sourceType === 'tsx' && showSource && (
<Link target="_blank" to={playgroundUrl}>
<button
title="Get JSX via TypeScript Playground"
className="__dumi-default-icon"
role="change-tsx"
type="button"
/>
</Link>
)}
<button
title="Toggle source code panel"
className={`__dumi-default-icon${showSource ? ' __dumi-default-btn-expand' : ''}`}
role="source"
type="button"
onClick={() => setShowSource(!showSource)}
/>
</div>
{showSource && (
<div className="__dumi-default-previewer-source-wrapper">
{!isSingleFile && (
<Tabs
className="__dumi-default-previewer-source-tab"
prefixCls="__dumi-default-tabs"
moreIcon="···"
defaultActiveKey={currentFile}
onChange={handleFileChange}
>
{Object.keys(props.sources).map(filename => (
<TabPane
tab={
filename === '_'
? `index.${getSourceType(filename, props.sources[filename])}`
: filename
}
key={filename}
/>
))}
</Tabs>
)}
<div className="__dumi-default-previewer-source">
<SourceCode code={currentFileCode} lang={sourceType} showCopy={false} />
</div>
</div>
)}
</div>
);
};
export default Previewer;