import { css } from '@emotion/react'; import styled from '@emotion/styled'; import { message } from '@os-design/core'; import { Loading, Picture } from '@os-design/icons'; import { omitEmotionProps, useEvent } from '@os-design/utils'; import { EditorBlock } from 'draft-js'; import React, { useCallback, useRef, useState } from 'react'; import changeBlock from '../utils/changeBlock.js'; import getCurrentBlock from '../utils/getCurrentBlock.js'; import Figure from './Figure.js'; import FigureCaption from './FigureCaption.js'; import type { BlockProps, BlockToolbarItem } from './types.js'; const widthStyles = (p) => p.width && css` width: ${p.width}px; `; interface ImageFigureProps { width: number; } const ImageFigure = styled(Figure, omitEmotionProps('width'))` position: relative; display: inline-block; max-width: 100%; ${widthStyles}; `; const Mask = styled.div` position: absolute; top: 0; right: 0; bottom: 0; left: 0; display: flex; justify-content: center; align-items: center; background-color: hsla( 0, 0%, 0%, ${(p) => p.theme.editorBlockImageMaskOpacity} ); color: hsl(0, 0%, 100%); `; const LoadingIcon = styled(Loading)` font-size: ${(p) => p.theme.editorBlockImageLoadingFontSize}em; `; const Image = styled.img` max-width: 100%; max-height: ${(p) => p.theme.editorBlockImageMaxHeight}em; vertical-align: bottom; `; const ImageBlock: React.FC = (props) => { const { block } = props; const data = block.getData(); const src = data.get('src') as string; const loading = data.get('loading') as boolean; // Update the width of the image const imageRef = useRef(null); const [width, setWidth] = useState(0); const updateWidth = useCallback(() => { if (!imageRef.current) return; setWidth(imageRef.current.width); }, []); useEvent( (typeof window !== 'undefined' ? window : undefined) as EventTarget, 'resize', updateWidth ); if (!src) return null; return ( {loading && ( )} {block.getText()} {!loading && ( )} ); }; export const IMAGE_BLOCK = 'atomic:image'; const imageBlock = ( onImageUpload: (file: File) => Promise ): BlockToolbarItem => ({ type: IMAGE_BLOCK, component: ImageBlock, icon: , onClick: ({ value, onChange, setReadOnly }) => { if (!onImageUpload) throw new Error('Specify the onImageUpload method'); // Not working in mobile Safari. // The input must be actually appended to the DOM. const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/jpeg,image/png,image/webp'; input.onchange = async (e) => { const target = e.target as HTMLInputElement | null; if (!target) return; const { files } = target; if (!files) return; setReadOnly(true); const file = files[0]; let src = URL.createObjectURL(file); // Add the local image const currentBlock = getCurrentBlock(value); let nextEditorState = changeBlock(value, currentBlock, IMAGE_BLOCK, { src, loading: true, }); onChange(nextEditorState); // Replace the local image with the remote one try { src = await onImageUpload(file); nextEditorState = changeBlock(value, currentBlock, IMAGE_BLOCK, { src, }); } catch (err) { if (err instanceof Error) { message.error(err.message); } nextEditorState = changeBlock(value, currentBlock, 'unstyled'); } setReadOnly(false); onChange(nextEditorState); }; input.click(); }, }); export default imageBlock;