From a871c7ef957d3c7a9fdf4e80d625c8d0a98d4d81 Mon Sep 17 00:00:00 2001 From: sam-m-m Date: Thu, 15 Apr 2021 16:38:09 -0700 Subject: [PATCH] feat #299 - Add code copy feat --- src/components/Code/CodeControls.tsx | 72 ++++++++++++++++++++++++++++ src/components/Code/index.tsx | 34 +++++++++++-- src/components/Code/utils.ts | 32 ++++++++++++- 3 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 src/components/Code/CodeControls.tsx diff --git a/src/components/Code/CodeControls.tsx b/src/components/Code/CodeControls.tsx new file mode 100644 index 00000000..ed850adf --- /dev/null +++ b/src/components/Code/CodeControls.tsx @@ -0,0 +1,72 @@ +import cn from 'classnames' +import { createUseStyles } from 'react-jss' +import { faCopy } from '@fortawesome/pro-regular-svg-icons' +import { generateThemedControlsStyles } from './utils' +import { IconButton } from 'components/IconButton' +import { Tooltip } from 'components/Tooltip' +import React, { FC } from 'react' +import { styleguide, ThemeType } from 'components/assets/styles' + +const { borderRadius, spacing } = styleguide + +const { light, dark } = ThemeType + +const useStyles = createUseStyles({ + codeControls: { + ...generateThemedControlsStyles(light), + borderRadius, + padding: `${spacing.xs}px ${spacing.s}px`, + position: 'absolute', + right: spacing['s+'] + }, + iconButton: { + padding: { + left: spacing.s, + right: spacing.s + } + }, + // eslint-disable-next-line sort-keys + '@global': { + [`.${dark}`]: { + '& $codeControls': generateThemedControlsStyles(dark) + } + } +}) + +// This is left as an object so more controls (e.g. download) can be added later +export interface DisplayCodeControls { + copy?: boolean +} + +interface Props { + classes?: string[] + displayControls?: DisplayCodeControls + isCopied?: boolean + onClickCopyCode: () => void +} + +export const CodeControls: FC = ({ + classes = [], + displayControls = {}, + isCopied = false, + onClickCopyCode +}: Props) => { + const compClasses = useStyles() + + const { copy = true } = displayControls + + return ( +
+ {copy && ( + + + + )} +
+ ) +} diff --git a/src/components/Code/index.tsx b/src/components/Code/index.tsx index b08fbac5..e1e98534 100644 --- a/src/components/Code/index.tsx +++ b/src/components/Code/index.tsx @@ -1,16 +1,20 @@ import './prism.css' import cn from 'classnames' +import { isObject } from 'lodash' import Mark from 'mark.js' import Prism from 'prismjs' -import { useStyles } from './utils' +import { CodeControls, DisplayCodeControls } from './CodeControls' +import { copyToClipboard, useStyles } from './utils' import { Input, InputProps } from 'components/Input' -import React, { ChangeEvent, FC, useEffect, useRef } from 'react' +import React, { ChangeEvent, FC, useEffect, useRef, useState } from 'react' + require('prismjs/plugins/line-numbers/prism-line-numbers') require('prismjs/components/prism-json') export interface CodeProps { classes?: string[] code: string | Record + displayControls?: DisplayCodeControls | false language?: 'css' | 'html' | 'javascript' | 'json' lineNumbers?: boolean readOnly?: boolean @@ -22,6 +26,7 @@ export interface CodeProps { export const Code: FC = ({ classes = [], code, + displayControls = {}, language = 'json', lineNumbers = true, readOnly = true, @@ -30,6 +35,20 @@ export const Code: FC = ({ const compClasses = useStyles() const codeRef = useRef(null) + const [isCopied, setIsCopied] = useState(false) + + const copyCode = () => { + const stringifiedCode = isObject(code) + ? JSON.stringify(code, null, '\t') + : code + + copyToClipboard(stringifiedCode, () => setIsCopied(true)) + } + + useEffect(() => { + if (isCopied) setTimeout(() => setIsCopied(false), 1250) + }, [isCopied]) + useEffect(() => { /** * We want to highlight the code after it is rendered onto the page, @@ -72,9 +91,18 @@ export const Code: FC = ({
{search && }
+				{displayControls && (
+					
+				)}
 				
 					{typeof code === 'string'
 						? code
diff --git a/src/components/Code/utils.ts b/src/components/Code/utils.ts
index 4aeab3a8..47b436f8 100644
--- a/src/components/Code/utils.ts
+++ b/src/components/Code/utils.ts
@@ -1,5 +1,10 @@
 import { createUseStyles } from 'react-jss'
-import { styleguide, themedStyles, ThemeType } from 'components/assets/styles'
+import {
+	styleguide,
+	themedStyles,
+	themes,
+	ThemeType
+} from 'components/assets/styles'
 
 const { dark, light } = ThemeType
 
@@ -10,6 +15,12 @@ const {
 	spacing
 } = styleguide
 
+interface CopyToClipboard {
+	(str: string, callback?: () => void): void
+}
+export const copyToClipboard: CopyToClipboard = (str, callback) =>
+	navigator.clipboard.writeText(str).then(callback)
+
 const codePalette = {
 	[dark]: {
 		background: blacks['darken-20']
@@ -32,6 +43,17 @@ const generateThemedCodeStyles = (themeType: ThemeType) => {
 	}
 }
 
+export const generateThemedControlsStyles = (themeType: ThemeType) => {
+	const {
+		base: { borderColor }
+	} = themedStyles[themeType]
+
+	return {
+		background: themes[themeType].background.secondary,
+		border: `1px solid ${borderColor}`
+	}
+}
+
 const generateThemedPreCodeStyles = (themeType: ThemeType) => {
 	const { background } = codePalette[themeType]
 
@@ -96,6 +118,14 @@ const preCodeSelector = "pre[class*='language-']"
 /* eslint-enable quotes */
 
 export const useStyles = createUseStyles({
+	controls: { opacity: 0 },
+	preCode: {
+		'&:hover': {
+			'& $controls': { opacity: 1 }
+		},
+		position: 'relative'
+	},
+	// eslint-disable-next-line sort-keys
 	'@global': {
 		[preCodeSelector]: {
 			'& .line-numbers-rows': {