diff --git a/packages/keystatic/src/component-blocks/cloud-image-preview.tsx b/packages/keystatic/src/component-blocks/cloud-image-preview.tsx index 3318b589b..12e1d2567 100644 --- a/packages/keystatic/src/component-blocks/cloud-image-preview.tsx +++ b/packages/keystatic/src/component-blocks/cloud-image-preview.tsx @@ -2,159 +2,404 @@ import { useEffect, useState } from 'react'; import { NotEditable, ObjectField, PreviewProps } from '@keystatic/core'; -import { TextField } from '@keystar/ui/text-field'; -import { ActionButton } from '@keystar/ui/button'; -import { VStack, Flex } from '@keystar/ui/layout'; -import { Text } from '@keystar/ui/typography'; +import { TextArea, TextField } from '@keystar/ui/text-field'; +import { ActionButton, Button, ButtonGroup } from '@keystar/ui/button'; +import { VStack, Flex, Box } from '@keystar/ui/layout'; +import { Heading, Text } from '@keystar/ui/typography'; import { Icon } from '@keystar/ui/icon'; import { externalLinkIcon } from '@keystar/ui/icon/icons/externalLinkIcon'; +import { imageIcon } from '@keystar/ui/icon/icons/imageIcon'; +import { pencilIcon } from '@keystar/ui/icon/icons/pencilIcon'; +import { trash2Icon } from '@keystar/ui/icon/icons/trash2Icon'; +import { xIcon } from '@keystar/ui/icon/icons/xIcon'; import { useConfig } from '../app/shell/context'; -export function CloudImagePreview( - props: PreviewProps< - ObjectField - > & { - onRemove(): void; - } -) { - const src = props.fields.src.value; - const alt = props.fields.alt.value; +import { css, tokenSchema } from '@keystar/ui/style'; +import { Dialog, DialogTrigger } from '@keystar/ui/dialog'; +import { Content, Header } from '@keystar/ui/slots'; +import { ProgressCircle } from '@keystar/ui/progress'; +import { Tooltip, TooltipTrigger } from '@keystar/ui/tooltip'; - const dimensions = useImageDimensions(src); +type ImageData = { + src: string; + width: string; + height: string; + alt: string; +}; - useEffect(() => { - if ( - dimensions && - props.fields.width.value === '' && - props.fields.height.value === '' - ) { - props.onChange({ - height: dimensions.height.toString(), - width: dimensions.width.toString(), - }); - } - }); +type ImageDimensions = Pick; - const config = useConfig(); +function cleanImageData( + data: { + src: string; + width?: string | number; + height?: string | number; + alt?: string | number; + } = { src: '' } +): ImageData { + return { + src: data.src, + alt: 'alt' in data && typeof data.alt === 'string' ? data.alt : '', + height: 'height' in data ? getDimension(data.height) : '', + width: 'width' in data ? getDimension(data.width) : '', + }; +} + +type ImageStatus = '' | 'loading' | 'good' | 'error'; + +function ImageDialog({ + image, + onChange, + onClose, +}: { + image?: ImageData; + onChange: (data: ImageData) => void; + onClose: () => void; +}) { + const [state, setState] = useState(cleanImageData(image)); + const [status, setStatus] = useState(''); + const [dimensions, setDimensions] = useState( + cleanImageData() + ); + const imageLibraryURL = useImageLibraryURL(); const onPaste = (event: React.ClipboardEvent) => { + event.preventDefault(); const text = event.clipboardData.getData('text/plain'); try { - const data: unknown = JSON.parse(text); + const data: any = JSON.parse(text); if ( typeof data === 'object' && data !== null && 'src' in data && typeof data.src === 'string' ) { - event.preventDefault(); - props.onChange({ - src: data.src, - alt: 'alt' in data && typeof data.alt === 'string' ? data.alt : '', - height: 'height' in data ? getDimension(data.height) : '', - width: 'width' in data ? getDimension(data.width) : '', - }); + setState(cleanImageData(data)); return; } } catch (err) {} const pattern = /^\s*!\[(.*)\]\(([a-z0-9_\-/:.]+)\)\s*$/; const match = text.match(pattern); if (match) { - event.preventDefault(); - props.onChange({ - src: match[2], - alt: match[1], - height: '', - width: '', - }); + setState( + cleanImageData({ + src: match[2], + alt: match[1], + }) + ); return; } + setState(cleanImageData({ src: text })); }; + const src = state.src; + + useEffect(() => { + if (!src) { + setStatus(''); + return; + } + if (!isValidURL(src)) { + return; + } + setStatus('loading'); + const img = new Image(); + img.onload = () => { + const dimensions = { + width: img.width.toString(), + height: img.height.toString(), + }; + setState(state => ({ + ...state, + ...dimensions, + })); + setDimensions(dimensions); + setStatus('good'); + }; + img.onerror = () => { + setStatus('error'); + }; + img.src = src; + return () => { + img.onload = null; + }; + }, [src]); + + return ( + + {image ? 'Edit' : 'Insert'} Cloud Image +
+ +
+ + { + e.preventDefault(); + if (status !== 'good') return; + onChange(state); + onClose(); + }} + direction="column" + gap="large" + > + { + if (e.code === 'Backspace' || e.code === 'Delete') { + setState(cleanImageData()); + } + }} + value={state.src} + description={ + image + ? undefined + : 'Copy an Image URL from the Image Library and paste into this field to insert it.' + } + endElement={ + status === 'loading' ? ( + + + + ) : state.src ? ( + setState(cleanImageData())} + > + + + ) : null + } + /> + {status === 'good' ? ( + <> +