diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b36a62054..5e6d0462a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Fix positioning of `Popup` with changable content @layershifter ([#678](https://github.com/stardust-ui/react/pull/678)) - Fix default props in `Accordion` and `Dropdown` components @layershifter ([#675](https://github.com/stardust-ui/react/pull/675)) +### Documentation +- Add ability to edit examples' code in JavaScript and TypeScript @layershifter ([#650](https://github.com/stardust-ui/react/pull/650)) +- Fix broken switch to Children API when an example is not present @layershifter ([#650](https://github.com/stardust-ui/react/pull/650)) + ## [v0.16.0](https://github.com/stardust-ui/react/tree/v0.16.0) (2019-01-07) [Compare changes](https://github.com/stardust-ui/react/compare/v0.15.0...v0.16.0) diff --git a/docs/src/components/ComponentDoc/ComponentExample/ComponentExample.tsx b/docs/src/components/ComponentDoc/ComponentExample/ComponentExample.tsx index cc9fc5724c..0429ee7fd8 100644 --- a/docs/src/components/ComponentDoc/ComponentExample/ComponentExample.tsx +++ b/docs/src/components/ComponentDoc/ComponentExample/ComponentExample.tsx @@ -13,15 +13,17 @@ import Editor, { EDITOR_BACKGROUND_COLOR, EDITOR_GUTTER_COLOR } from 'docs/src/c import { babelConfig, importResolver } from 'docs/src/components/Playground/renderConfig' import ComponentControls from '../ComponentControls' import ComponentExampleTitle from './ComponentExampleTitle' -import ContributionPrompt from '../ContributionPrompt' -import SourceCodeManager, { SourceCodeType } from './SourceCodeManager' +import ComponentSourceManager, { + ComponentSourceManagerRenderProps, +} from '../ComponentSourceManager' import { ThemeInput, ThemePrepared } from 'src/themes/types' import { mergeThemeVariables } from '../../../../../src/lib/mergeThemes' import { ThemeContext } from '../../../context/theme-context' import CodeSnippet from '../../CodeSnippet' -import formatCode from '../../../utils/formatCode' -export interface ComponentExampleProps extends RouteComponentProps { +export interface ComponentExampleProps + extends RouteComponentProps, + ComponentSourceManagerRenderProps { title: React.ReactNode description?: React.ReactNode examplePath: string @@ -34,7 +36,6 @@ interface ComponentExampleState { componentVariables: Object handleMouseLeave: () => void handleMouseMove: () => void - sourceCode: string showCode: boolean showRtl: boolean showTransparent: boolean @@ -48,11 +49,6 @@ const childrenStyle: React.CSSProperties = { maxWidth: pxToRem(500), } -const codeTypeApiButtonLabels: { [key in SourceCodeType]: string } = { - normal: 'Children API', - shorthand: 'Shorthand API', -} - const disabledStyle = { opacity: 0.5, pointerEvents: 'none' } /** @@ -60,7 +56,6 @@ const disabledStyle = { opacity: 0.5, pointerEvents: 'none' } * Allows toggling the the raw `code` code block. */ class ComponentExample extends React.Component { - sourceCodeMgr: SourceCodeManager anchorName: string kebabExamplePath: string KnobsComponent: any @@ -71,7 +66,6 @@ class ComponentExample extends React.Component { - copyToClipboard(this.state.sourceCode) + copySourceCode = () => { + copyToClipboard(this.props.currentCode) + this.setState({ copiedCode: true }) setTimeout(() => this.setState({ copiedCode: false }), 1000) } - resetJSX = () => { - if (this.sourceCodeMgr.originalCodeHasChanged && confirm('Lose your changes?')) { - this.sourceCodeMgr.resetToOriginalCode() - this.updateAndRenderSourceCode() + resetSourceCode = () => { + if (confirm('Lose your changes?')) { + this.props.handleCodeReset() } } @@ -239,16 +231,6 @@ class ComponentExample extends React.Component _.includes(knobsContext.keys(), this.getKnobsFilename()) - renderExampleFromCode = (): JSX.Element => { - const { sourceCode } = this.state - - if (sourceCode == null) { - return this.renderMissingExample() - } - - return {({ element }) => element} - } - renderElement = (element: React.ReactElement) => { const { examplePath } = this.props const { showRtl, componentVariables, themeName } = this.state @@ -270,16 +252,6 @@ class ComponentExample extends React.Component { - const missingExamplePath = `./docs/src/examples/${this.sourceCodeMgr.currentPath}.tsx` - return ( - - Looks like we're need an example file at: -

{missingExamplePath}

-
- ) - } - handleKnobChange = knobs => { this.setState(prevState => ({ knobs: { @@ -313,148 +285,148 @@ class ComponentExample extends React.Component this.props.examplePath.split('/')[1] - handleChangeCode = (sourceCode: string) => { - this.sourceCodeMgr.currentCode = sourceCode - this.updateAndRenderSourceCode() + handleCodeApiChange = apiType => () => { + this.props.handleCodeAPIChange(apiType) } - updateAndRenderSourceCode = () => { - this.setState({ sourceCode: this.sourceCodeMgr.currentCode }) - } + handleCodeLanguageChange = language => () => { + const { handleCodeLanguageChange, wasCodeChanged } = this.props - setApiCodeType = (codeType: SourceCodeType) => { - this.sourceCodeMgr.codeType = codeType - this.updateAndRenderSourceCode() + if (wasCodeChanged) { + if (confirm('Lose your changes?')) { + handleCodeLanguageChange(language) + } + } else { + handleCodeLanguageChange(language) + } } - renderApiCodeMenu = (): JSX.Element => { - const { sourceCode } = this.state - const lineCount = sourceCode && sourceCode.match(/^/gm)!.length - - const menuItems = [SourceCodeType.shorthand, SourceCodeType.normal].map(codeType => { - // we disable the menu button for Children API in case we don't have the example for it - const disabled = - codeType === SourceCodeType.normal && !this.sourceCodeMgr.isCodeValidForType(codeType) - - return { - active: this.sourceCodeMgr.codeType === codeType, - disabled, - key: codeType, - onClick: this.setApiCodeType.bind(this, codeType), - content: ( + renderAPIsMenu = (): JSX.Element => { + const { componentAPIs, currentCodeAPI } = this.props + const menuItems = _.map(componentAPIs, ({ name, supported }, type) => ( + - {codeTypeApiButtonLabels[codeType]} - {disabled && (not supported)} + {name} + {!supported && (not supported)} - ), - } - }) - - return ( - // match code editor background and gutter size and colors -
-
9 ? 41 : 34}px solid ${EDITOR_GUTTER_COLOR}`, - paddingBottom: '1rem', - } as React.CSSProperties - } - > - -
-
- ) - } - - canBePrettified = () => { - const { sourceCode } = this.state + } + disabled={!supported} + key={type} + onClick={this.handleCodeApiChange(type)} + /> + )) - try { - return sourceCode !== formatCode(sourceCode) - } catch (err) { - return false - } + return {menuItems} } - handleFormat = () => { - const { sourceCode } = this.state + renderLanguagesMenu = (): JSX.Element => { + const { currentCodeLanguage } = this.props - this.handleChangeCode(formatCode(sourceCode)) + return ( + + + + + ) } renderCodeEditorMenu = (): JSX.Element => { + const { + currentCodeLanguage, + currentCodePath, + canCodeBeFormatted, + handleCodeFormat, + wasCodeChanged, + } = this.props const { copiedCode } = this.state - const { originalCodeHasChanged, currentPath } = this.sourceCodeMgr - const codeEditorStyle: React.CSSProperties = { - position: 'absolute', - margin: 0, - top: '2px', - right: '0.5rem', - } // get component name from file path: // elements/Button/Types/ButtonButtonExample - const pathParts = currentPath.split(__PATH_SEP__) + const pathParts = currentCodePath.split(__PATH_SEP__) const filename = pathParts[pathParts.length - 1] const ghEditHref = [ - `${constants.repoURL}/edit/master/docs/src/examples/${currentPath}.tsx`, + `${constants.repoURL}/edit/master/docs/src/examples/${currentCodePath}.tsx`, `?message=docs(${filename}): your description`, ].join('') return ( - + {({ error }) => ( )} - + {currentCodeLanguage === 'ts' && ( + + )} ) } - renderJSX = () => { - const { showCode, sourceCode } = this.state + renderSourceCode = () => { + const { currentCode = '', handleCodeChange } = this.props + const { showCode } = this.state - if (!showCode) return null + const lineCount = currentCode.match(/^/gm)!.length - return ( -
- {this.renderApiCodeMenu()} + return showCode ? ( + // match code editor background and gutter size and colors +
+
9 ? 41 : 34}px solid ${EDITOR_GUTTER_COLOR}`, + paddingBottom: '2.6rem', + } as React.CSSProperties + } + > + + {this.renderAPIsMenu()} + {this.renderLanguagesMenu()} + -
{this.renderCodeEditorMenu()} -
+ +
- ) + ) : null } renderError = () => { @@ -550,7 +522,7 @@ class ComponentExample extends React.Component - {this.renderExampleFromCode()} + {({ element }) => element} ) }} /> - {this.renderJSX()} + {this.renderSourceCode()} {this.renderError()} {this.renderHTML()} {this.renderVariables()} @@ -669,7 +638,11 @@ class ComponentExample extends React.Component ( - {({ themeName }) => } + {({ themeName }) => ( + + {codeProps => } + + )} ) diff --git a/docs/src/components/ComponentDoc/ComponentExample/SourceCodeManager.ts b/docs/src/components/ComponentDoc/ComponentExample/SourceCodeManager.ts deleted file mode 100644 index e0ff8f8c31..0000000000 --- a/docs/src/components/ComponentDoc/ComponentExample/SourceCodeManager.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { exampleSourcesContext } from '../../../utils' - -interface SourceCodeData { - path: string - code: string - originalCode: string -} - -export enum SourceCodeType { - normal = 'normal', - shorthand = 'shorthand', -} - -export const examplePathPatterns: { [key in SourceCodeType]: string } = { - normal: '', - shorthand: '.shorthand', -} - -class SourceCodeManager { - private readonly data: { [key in SourceCodeType]: SourceCodeData } = { - normal: {} as SourceCodeData, - shorthand: {} as SourceCodeData, - } - - public codeType: SourceCodeType - - constructor(private sourceCodePath: string) { - const prioritizedCodeTypes = [SourceCodeType.shorthand, SourceCodeType.normal] // order is relevant - prioritizedCodeTypes.forEach(sourceCodeType => { - this.setDataForCodeType(sourceCodeType) - }) - - this.codeType = - prioritizedCodeTypes.find(codeType => this.isCodeValidForType(codeType)) || - SourceCodeType.shorthand - } - - public set currentCode(currentCode: string) { - this.currentCodeData.code = currentCode - } - - public get currentCode(): string { - return this.currentCodeData.code - } - - public get currentPath(): string { - return this.currentCodeData.path - } - - public get originalCodeHasChanged(): boolean { - return this.currentCodeData.code !== this.currentCodeData.originalCode - } - - public isCodeValidForType(codeType: SourceCodeType): boolean { - return this.data[codeType].code != null - } - - public resetToOriginalCode(): void { - this.currentCodeData.code = this.currentCodeData.originalCode - } - - private get currentCodeData(): SourceCodeData { - return this.data[this.codeType] - } - - private set currentCodeData(codeData: SourceCodeData) { - this.data[this.codeType] = codeData - } - - private setDataForCodeType(sourceCodeType: SourceCodeType): void { - const path = this.sourceCodePath + examplePathPatterns[sourceCodeType] - const code = this.safeRequire(path) - - if (!code) { - // Returning as there are no examples provided for this type - // - e.g. there is no children API example for component - return - } - - this.data[sourceCodeType] = { - path, - code, - originalCode: code, - } - } - - private safeRequire = (path: string): string | undefined => { - try { - const filename = `${path.replace(/^components\//, './')}.source.json` - - return exampleSourcesContext(filename).js - } catch (e) { - return undefined - } - } -} - -export default SourceCodeManager diff --git a/docs/src/components/ComponentDoc/ComponentExample/index.tsx b/docs/src/components/ComponentDoc/ComponentExample/index.tsx index 188402ef1f..21cefe42f8 100644 --- a/docs/src/components/ComponentDoc/ComponentExample/index.tsx +++ b/docs/src/components/ComponentDoc/ComponentExample/index.tsx @@ -1,2 +1 @@ -export { examplePathPatterns } from './SourceCodeManager' export { default, ComponentExampleProps } from './ComponentExample' diff --git a/docs/src/components/ComponentDoc/ComponentExamples.tsx b/docs/src/components/ComponentDoc/ComponentExamples.tsx index 524a355b30..b2ca6b019d 100644 --- a/docs/src/components/ComponentDoc/ComponentExamples.tsx +++ b/docs/src/components/ComponentDoc/ComponentExamples.tsx @@ -4,7 +4,7 @@ import * as React from 'react' import { exampleIndexContext, exampleSourcesContext } from 'docs/src/utils' import { Grid, List } from 'semantic-ui-react' -import { examplePathPatterns } from './ComponentExample' +import { componentAPIs } from './ComponentSourceManager' import ContributionPrompt from './ContributionPrompt' interface ComponentExamplesProps { @@ -83,8 +83,8 @@ export default class ComponentExamples extends React.Component `${pattern}.source.json`) const [normalRegExp, shorthandRegExp] = [normalExtension, shorthandExtension].map( diff --git a/docs/src/components/ComponentDoc/ComponentSourceManager/ComponentSourceManager.ts b/docs/src/components/ComponentDoc/ComponentSourceManager/ComponentSourceManager.ts new file mode 100644 index 0000000000..c418070072 --- /dev/null +++ b/docs/src/components/ComponentDoc/ComponentSourceManager/ComponentSourceManager.ts @@ -0,0 +1,138 @@ +import * as _ from 'lodash' +import * as React from 'react' + +import { ExampleSource } from 'docs/src/types' +import formatCode from 'docs/src/utils/formatCode' +import { componentAPIs as APIdefinitions, ComponentAPIs } from './componentAPIs' +import getExampleSource from './getExampeSource' + +export type ComponentSourceManagerRenderProps = ComponentSourceManagerState & { + handleCodeAPIChange: (newApi: keyof ComponentAPIs) => void + handleCodeChange: (newCode: string) => void + handleCodeFormat: () => void + handleCodeLanguageChange: (newLanguage: ComponentSourceManagerLanguage) => void + handleCodeReset: () => void +} + +export type ComponentSourceManagerLanguage = 'js' | 'ts' + +export type ComponentSourceManagerProps = { + examplePath: string + children: (renderProps: ComponentSourceManagerRenderProps) => React.ReactNode +} + +type ComponentSourceManagerAPIs = ComponentAPIs<{ + sourceCode: ExampleSource | undefined + supported: boolean +}> + +export type ComponentSourceManagerState = { + currentCodeLanguage: ComponentSourceManagerLanguage + currentCodeAPI: keyof ComponentAPIs + currentCodePath: string + + componentAPIs: ComponentSourceManagerAPIs + currentCode?: string + formattedCode?: string + originalCode?: string + + canCodeBeFormatted: boolean + wasCodeChanged: boolean +} + +export default class ComponentSourceManager extends React.Component< + ComponentSourceManagerProps, + ComponentSourceManagerState +> { + constructor(props: ComponentSourceManagerProps) { + super(props) + + const componentAPIs = _.mapValues(APIdefinitions, (definition, name: keyof ComponentAPIs) => { + const sourceCode = getExampleSource(props.examplePath, name) + + return { + ...definition, + sourceCode, + supported: !!sourceCode, + } + }) as ComponentSourceManagerAPIs + + this.state = { + currentCodeLanguage: 'js' as ComponentSourceManagerLanguage, + currentCodeAPI: _.findLastKey(componentAPIs, { supported: true }) as keyof ComponentAPIs, + currentCodePath: '', + + componentAPIs, + canCodeBeFormatted: false, + wasCodeChanged: false, + } + } + + static getDerivedStateFromProps( + props: ComponentSourceManagerProps, + state: ComponentSourceManagerState, + ): Partial { + const { examplePath } = props + const { componentAPIs, currentCodeAPI, currentCodeLanguage, currentCode: storedCode } = state + + const sourceCodes = componentAPIs[currentCodeAPI].sourceCode + const originalCode = sourceCodes[currentCodeLanguage] + + const currentCode = storedCode || originalCode + const currentCodePath = examplePath + componentAPIs[currentCodeAPI].fileSuffix + + const prettierParser = currentCodeLanguage === 'ts' ? 'typescript' : 'babylon' + let formattedCode + + try { + formattedCode = formatCode(currentCode, prettierParser) + } catch (e) {} + + return { + currentCode, + currentCodePath, + formattedCode, + originalCode, + + canCodeBeFormatted: !!formattedCode ? currentCode !== formattedCode : false, + wasCodeChanged: originalCode !== currentCode, + } + } + + handleCodeAPIChange = (newAPI: keyof ComponentAPIs): void => { + this.setState({ + currentCodeAPI: newAPI, + currentCode: undefined, + }) + } + + handleCodeChange = (newCode: string): void => { + this.setState({ currentCode: newCode }) + } + + handleCodeFormat = (): void => { + this.setState(prevState => ({ currentCode: prevState.formattedCode })) + } + + handleCodeReset = (): void => { + this.setState({ currentCode: undefined }) + } + + handleLanguageChange = (newLanguage: ComponentSourceManagerLanguage): void => { + this.setState({ + currentCodeLanguage: newLanguage, + currentCode: undefined, + }) + } + + render() { + return this.props.children({ + ...this.state, + handleCodeAPIChange: this.handleCodeAPIChange, + handleCodeChange: this.handleCodeChange, + handleCodeFormat: this.handleCodeFormat, + handleCodeReset: this.handleCodeReset, + handleCodeLanguageChange: this.handleLanguageChange, + }) + } +} diff --git a/docs/src/components/ComponentDoc/ComponentSourceManager/componentAPIs.ts b/docs/src/components/ComponentDoc/ComponentSourceManager/componentAPIs.ts new file mode 100644 index 0000000000..2d8a8a01a3 --- /dev/null +++ b/docs/src/components/ComponentDoc/ComponentSourceManager/componentAPIs.ts @@ -0,0 +1,14 @@ +export type ComponentAPI = { + name: string + fileSuffix: string +} & T + +export type ComponentAPIs = { + children: ComponentAPI + shorthand: ComponentAPI +} + +export const componentAPIs: ComponentAPIs = { + children: { name: 'Children API', fileSuffix: '' }, + shorthand: { name: 'Shorthand API', fileSuffix: '.shorthand' }, +} diff --git a/docs/src/components/ComponentDoc/ComponentSourceManager/getExampeSource.ts b/docs/src/components/ComponentDoc/ComponentSourceManager/getExampeSource.ts new file mode 100644 index 0000000000..c8a2fb00fa --- /dev/null +++ b/docs/src/components/ComponentDoc/ComponentSourceManager/getExampeSource.ts @@ -0,0 +1,20 @@ +import { ExampleSource } from 'docs/src/types' +import { exampleSourcesContext } from 'docs/src/utils' +import { componentAPIs, ComponentAPIs } from './componentAPIs' + +const getExampleSource = ( + examplePath: string, + componentAPI: keyof ComponentAPIs, +): ExampleSource | undefined => { + const sourcePath = `${examplePath.replace(/^components/, '.')}${ + componentAPIs[componentAPI].fileSuffix + }.source.json` + + try { + return exampleSourcesContext(sourcePath) + } catch (e) { + return undefined + } +} + +export default getExampleSource diff --git a/docs/src/components/ComponentDoc/ComponentSourceManager/index.ts b/docs/src/components/ComponentDoc/ComponentSourceManager/index.ts new file mode 100644 index 0000000000..bfe6fe6171 --- /dev/null +++ b/docs/src/components/ComponentDoc/ComponentSourceManager/index.ts @@ -0,0 +1,2 @@ +export * from './componentAPIs' +export { default, ComponentSourceManagerRenderProps } from './ComponentSourceManager' diff --git a/docs/src/index.ejs b/docs/src/index.ejs index 5b1776b6da..508a790d46 100644 --- a/docs/src/index.ejs +++ b/docs/src/index.ejs @@ -39,6 +39,10 @@ crossOrigin="anonymous" src="https://unpkg.com/prettier@<%= htmlWebpackPlugin.options.versions.prettier %>/parser-html.js" > + diff --git a/docs/src/utils/formatCode.ts b/docs/src/utils/formatCode.ts index 0c87458b26..6b5bd0cef7 100644 --- a/docs/src/utils/formatCode.ts +++ b/docs/src/utils/formatCode.ts @@ -8,7 +8,7 @@ delete prettierConfig.overrides // Please use this function directly and don't reexport it in utils. // https://github.com/prettier/prettier/issues/4959 -const formatCode = (code, parser = 'babylon') => { +const formatCode = (code: string, parser = 'babylon') => { if (!code) return '' const formatted = prettier.format(code, {