diff options
Diffstat (limited to 'packages/dev-tools-pages/ts/components/Code.tsx')
-rw-r--r-- | packages/dev-tools-pages/ts/components/Code.tsx | 218 |
1 files changed, 218 insertions, 0 deletions
diff --git a/packages/dev-tools-pages/ts/components/Code.tsx b/packages/dev-tools-pages/ts/components/Code.tsx new file mode 100644 index 000000000..da2bb83e6 --- /dev/null +++ b/packages/dev-tools-pages/ts/components/Code.tsx @@ -0,0 +1,218 @@ +import * as React from 'react'; +import styled from 'styled-components'; + +import { colors } from 'ts/variables'; + +import { Button as BaseButton } from './Button'; + +const isTouch = Boolean( + 'ontouchstart' in window || + (window as any).navigator.maxTouchPoints > 0 || + (window as any).navigator.msMaxTouchPoints > 0, +); + +interface CodeProps { + children: React.ReactNode; + language?: string; + isLight?: boolean; + isDiff?: boolean; + gutter?: Array<number | undefined>; + gutterLength?: number; + canCopy?: boolean; + isEtc?: boolean; +} + +interface CodeState { + hlCode?: string; + copied?: boolean; +} + +const Button = styled(BaseButton)` + opacity: ${isTouch ? '1' : '0'}; + position: absolute; + top: 1rem; + right: 1rem; + transition: opacity 0.2s; + :focus { + opacity: 1; + } +`; + +const Container = styled.div` + position: relative; + &:hover ${Button} { + opacity: 1; + } +`; + +const Base = + styled.div < + CodeProps > + ` + font-size: .875rem; + color: ${props => (props.language === undefined ? colors.white : 'inherit')}; + background-color: ${props => + props.isLight ? 'rgba(255,255,255,.15)' : props.language === undefined ? colors.black : '#F1F4F5'}; + white-space: ${props => (props.language === undefined ? 'nowrap' : '')}; + position: relative; + + ${props => + props.isDiff + ? ` + background-color: #E9ECED; + display: flex; + padding-top: 1.5rem; + padding-bottom: 1.5rem; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + ` + : ``} +`; + +const StyledCodeDiff = styled(({ gutterLength, children, ...props }: any) => <code {...props}>{children}</code>)` + ::before { + content: ''; + width: calc(0.75rem + ${props => props.gutterLength}ch); + background-color: #e2e5e6; + position: absolute; + top: 0; + left: 0; + bottom: 0; + } + + [class^='line-'] { + display: inline-block; + width: 100%; + position: relative; + padding-right: 1.5rem; + padding-left: calc(2.25rem + ${props => props.gutterLength}ch); + + ::before { + content: attr(data-gutter); + + width: ${props => props.gutterLength}; + padding-left: 0.375rem; + padding-right: 0.375rem; + position: absolute; + top: 50%; + left: 0; + transform: translateY(-50%); + z-index: 1; + } + } + + .line-addition { + background-color: rgba(0, 202, 105, 0.1); + } + .line-deletion { + background-color: rgba(255, 0, 0, 0.07); + } +`; + +const StyledPre = styled.pre` + margin: 0; + ${(props: { isDiff: boolean }) => + !props.isDiff + ? ` + padding: 1.5rem; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + ` + : ``}; +`; + +const StyledCopyInput = styled.textarea` + opacity: 0; + height: 0; + position: absolute; + top: 0; + right: 0; + z-index: -1; +`; + +const CopyInput = StyledCopyInput as any; + +class Code extends React.Component<CodeProps, CodeState> { + public state: CodeState = {}; + private readonly _code = React.createRef<HTMLTextAreaElement>(); + + constructor(props: CodeProps) { + super(props); + } + + public componentDidMount(): void { + /* + * _onMountAsync is only setting state, so no point in handling the promise + */ + /* tslint:disable:no-floating-promises */ + this._onMountAsync(); + /* tslint:enable:no-floating-promises */ + } + + public render(): React.ReactNode { + const { language, isLight, isDiff, children, gutterLength, canCopy } = this.props; + const { hlCode } = this.state; + + let CodeComponent = 'code'; + let codeProps = {}; + if (isDiff) { + codeProps = { gutterLength }; + CodeComponent = StyledCodeDiff as any; + } + + return ( + <Container> + <Base language={language} isDiff={isDiff} isLight={isLight}> + <StyledPre isDiff={isDiff}> + <CodeComponent + {...codeProps} + dangerouslySetInnerHTML={hlCode ? { __html: this.state.hlCode } : null} + > + {hlCode === undefined ? children : null} + </CodeComponent> + </StyledPre> + {!('clipboard' in navigator) ? ( + <CopyInput readOnly={true} aria-hidden="true" ref={this._code} value={children} /> + ) : null} + </Base> + {navigator.userAgent !== 'ReactSnap' && canCopy ? ( + <Button onClick={this._handleCopyAsync}>{this.state.copied ? 'Copied' : 'Copy'}</Button> + ) : null} + </Container> + ); + } + + private async _onMountAsync(): Promise<void> { + const { language, children, isDiff, gutter, isEtc } = this.props; + + const code = children as string; + + if (language !== undefined) { + const { highlight } = await System.import(/* webpackChunkName: 'highlightjs' */ 'ts/highlight'); + + this.setState({ + hlCode: highlight({ language, code, isDiff, gutter, isEtc }), + }); + } + } + + private readonly _handleCopyAsync = async () => { + try { + if ('clipboard' in navigator) { + await (navigator as any).clipboard.writeText(this.props.children); + this.setState({ copied: true }); + } else { + const lastActive = document.activeElement as HTMLElement; + this._code.current.focus(); + this._code.current.select(); + document.execCommand('copy'); + lastActive.focus(); + this.setState({ copied: true }); + } + } catch (error) { + this.setState({ copied: false }); + } + }; +} + +export { Code }; |