import CloseIcon from '@mui/icons-material/Close'; import PhotoIcon from '@mui/icons-material/Photo'; import Button from '@mui/material/Button'; import IconButton from '@mui/material/IconButton'; import { styled } from '@mui/material/styles'; import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import ObjectWidgetTopBar from '@staticcms/core/components/UI/ObjectWidgetTopBar'; import Outline from '@staticcms/core/components/UI/Outline'; import { borders, effects, lengths, shadows } from '@staticcms/core/components/UI/styles'; import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert'; import useUUID from '@staticcms/core/lib/hooks/useUUID'; import { basename, transientOptions } from '@staticcms/core/lib/util'; import { isEmpty } from '@staticcms/core/lib/util/string.util'; import type { FileOrImageField, GetAssetFunction, WidgetControlProps, } from '@staticcms/core/interface'; import type { FC, MouseEvent, MouseEventHandler } from 'react'; const MAX_DISPLAY_LENGTH = 50; const StyledFileControlWrapper = styled('div')` display: flex; flex-direction: column; position: relative; width: 100%; `; interface StyledFileControlContentProps { $collapsed: boolean; } const StyledFileControlContent = styled( 'div', transientOptions, )( ({ $collapsed }) => ` display: flex; flex-direction: column; gap: 16px; ${ $collapsed ? ` display: none; ` : ` padding: 16px; ` } `, ); const StyledSelection = styled('div')` display: flex; flex-direction: column; `; const StyledButtonWrapper = styled('div')` display: flex; gap: 16px; `; interface ImageWrapperProps { $sortable?: boolean; } const ImageWrapper = styled( 'div', transientOptions, )( ({ $sortable }) => ` flex-basis: 155px; width: 155px; height: 100px; margin-right: 20px; margin-bottom: 20px; border: ${borders.textField}; border-radius: ${lengths.borderRadius}; overflow: hidden; ${effects.checkerboard}; ${shadows.inset}; cursor: ${$sortable ? 'pointer' : 'auto'}; `, ); const SortableImageButtonsWrapper = styled('div')` display: flex; justify-content: center; column-gap: 10px; margin-right: 20px; margin-top: -10px; margin-bottom: 10px; `; const StyledImage = styled('img')` width: 100%; height: 100%; object-fit: contain; `; interface ImageProps { src: string; } const Image: FC = ({ src }) => { return ; }; interface SortableImageButtonsProps { onRemove: MouseEventHandler; onReplace: MouseEventHandler; } const SortableImageButtons: FC = ({ onRemove, onReplace }) => { return ( ); }; interface SortableImageProps { itemValue: string; getAsset: GetAssetFunction; field: FileOrImageField; onRemove: MouseEventHandler; onReplace: MouseEventHandler; } const SortableImage: FC = ({ itemValue, getAsset, field, onRemove, onReplace, }: SortableImageProps) => { const [assetSource, setAssetSource] = useState(''); useEffect(() => { const getImage = async () => { const asset = (await getAsset(itemValue, field))?.toString() ?? ''; setAssetSource(asset); }; getImage(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [itemValue]); return (
); }; const StyledMultiImageWrapper = styled('div')` display: flex; flex-wrap: wrap; `; const FileLink = styled('a')` margin-bottom: 20px; font-weight: normal; color: inherit; &:hover, &:active, &:focus { text-decoration: underline; } `; const FileLinks = styled('div')` margin-bottom: 12px; `; const FileLinkList = styled('ul')` list-style-type: none; `; function isMultiple(value: string | string[] | null | undefined): value is string[] { return Array.isArray(value); } export function getValidFileValue(value: string | string[] | null | undefined) { if (value) { return isMultiple(value) ? value.map(v => basename(v)) : basename(value); } return value; } export interface WithFileControlProps { forImage?: boolean; } const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => { const FileControl: FC> = memo( ({ value, field, onChange, openMediaLibrary, clearMediaControl, removeMediaControl, getAsset, hasErrors, t, }) => { const controlID = useUUID(); const [collapsed, setCollapsed] = useState(false); const [internalValue, setInternalValue] = useState(value ?? ''); const handleOnChange = useCallback( (newValue: string | string[]) => { if (newValue !== internalValue) { setInternalValue(newValue); setTimeout(() => { onChange(newValue); }); } }, [internalValue, onChange], ); const handleOpenMediaLibrary = useMediaInsert( internalValue, { field, controlID }, handleOnChange, ); const handleCollapseToggle = useCallback(() => { setCollapsed(!collapsed); }, [collapsed]); useEffect(() => { return () => { removeMediaControl(controlID); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const mediaLibraryFieldOptions = useMemo(() => { return field.media_library ?? {}; }, [field.media_library]); const config = useMemo( () => ('config' in mediaLibraryFieldOptions ? mediaLibraryFieldOptions.config : undefined), [mediaLibraryFieldOptions], ); const allowsMultiple = useMemo(() => { return config?.multiple ?? false; }, [config?.multiple]); const chooseUrl = useMemo( () => 'choose_url' in mediaLibraryFieldOptions && (mediaLibraryFieldOptions.choose_url ?? true), [mediaLibraryFieldOptions], ); const handleUrl = useCallback( (subject: 'image' | 'file') => (e: MouseEvent) => { e.preventDefault(); const url = window.prompt(t(`editor.editorWidgets.${subject}.promptUrl`)); handleOnChange(url ?? ''); }, [handleOnChange, t], ); const handleRemove = useCallback( (e: MouseEvent) => { e.preventDefault(); clearMediaControl(controlID); handleOnChange(''); }, [clearMediaControl, controlID, handleOnChange], ); const onRemoveOne = useCallback( (index: number) => () => { if (Array.isArray(internalValue)) { const newValue = [...internalValue]; newValue.splice(index, 1); handleOnChange(newValue); } }, [handleOnChange, internalValue], ); const onReplaceOne = useCallback( (index: number) => () => { return openMediaLibrary({ controlID, forImage, value: internalValue, replaceIndex: index, allowMultiple: false, config, field, }); }, [config, controlID, field, openMediaLibrary, internalValue], ); // TODO Readd when multiple uploads is supported // const onSortEnd = useCallback( // ({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) => { // if (Array.isArray(internalValue)) { // const newValue = arrayMoveImmutable(internalValue, oldIndex, newIndex); // handleOnChange(newValue); // } // }, // [handleOnChange, internalValue], // ); const renderFileLink = useCallback((link: string | undefined | null) => { const size = MAX_DISPLAY_LENGTH; if (!link || link.length <= size) { return link; } const text = `${link.slice(0, size / 2)}\u2026${link.slice(-(size / 2) + 1)}`; return ( {text} ); }, []); const [assetSource, setAssetSource] = useState(''); useEffect(() => { if (Array.isArray(internalValue)) { return; } const getImage = async () => { const newValue = (await getAsset(internalValue, field))?.toString() ?? ''; if (newValue !== internalValue) { setAssetSource(newValue); } }; getImage(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [internalValue]); const renderedImagesLinks = useMemo(() => { if (forImage) { if (!internalValue) { return null; } if (isMultiple(internalValue)) { return ( {internalValue.map((itemValue, index) => ( ))} ); } return ( ); } if (isMultiple(internalValue)) { return ( {internalValue.map(val => (
  • {renderFileLink(val)}
  • ))}
    ); } return {renderFileLink(internalValue)}; }, [assetSource, field, getAsset, internalValue, onRemoveOne, onReplaceOne, renderFileLink]); const content = useMemo(() => { const subject = forImage ? 'image' : 'file'; if (Array.isArray(internalValue) ? internalValue.length === 0 : isEmpty(internalValue)) { return ( {chooseUrl ? ( ) : null} ); } return ( {renderedImagesLinks} {chooseUrl && !allowsMultiple ? ( ) : null} ); }, [ internalValue, renderedImagesLinks, handleOpenMediaLibrary, t, allowsMultiple, chooseUrl, handleUrl, handleRemove, ]); return useMemo( () => ( {content} ), [collapsed, content, field.label, field.name, handleCollapseToggle, hasErrors, t], ); }, ); FileControl.displayName = 'FileControl'; return FileControl; }; export default withFileControl;