Files
static-cms/packages/core/src/widgets/file/withFileControl.tsx
2023-04-12 22:27:15 -04:00

335 lines
10 KiB
TypeScript

import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
import Button from '@staticcms/core/components/common/button/Button';
import Field from '@staticcms/core/components/common/field/Field';
import Image from '@staticcms/core/components/common/image/Image';
import Link from '@staticcms/core/components/common/link/Link';
import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
import useUUID from '@staticcms/core/lib/hooks/useUUID';
import { basename } from '@staticcms/core/lib/util';
import { isEmpty } from '@staticcms/core/lib/util/string.util';
import { selectConfig } from '@staticcms/core/reducers/selectors/config';
import { useAppSelector } from '@staticcms/core/store/hooks';
import SortableImage from './components/SortableImage';
import type {
BaseField,
Collection,
FileOrImageField,
MediaPath,
WidgetControlProps,
} from '@staticcms/core/interface';
import type { FC, MouseEvent } from 'react';
const MAX_DISPLAY_LENGTH = 50;
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<WidgetControlProps<string | string[], FileOrImageField>> = memo(
({
value,
label,
collection,
field,
errors,
forSingleList,
duplicate,
onChange,
openMediaLibrary,
hasErrors,
disabled,
t,
}) => {
const controlID = useUUID();
const [internalRawValue, setInternalValue] = useState(value ?? '');
const internalValue = useMemo(
() => (duplicate ? value ?? '' : internalRawValue),
[internalRawValue, duplicate, value],
);
const uploadButtonRef = useRef<HTMLButtonElement | null>(null);
const handleOnChange = useCallback(
({ path: newValue }: MediaPath) => {
if (newValue !== internalValue) {
setInternalValue(newValue);
setTimeout(() => {
onChange(newValue);
});
}
},
[internalValue, onChange],
);
const handleOpenMediaLibrary = useMediaInsert(
{ path: internalValue },
{ collection, field, controlID, forImage },
handleOnChange,
);
const config = useAppSelector(selectConfig);
const allowsMultiple = useMemo(() => {
return field.multiple ?? false;
}, [field.multiple]);
const chooseUrl = useMemo(() => field.choose_url ?? false, [field.choose_url]);
const handleUrl = useCallback(
(subject: 'image' | 'file') => (e: MouseEvent) => {
e.preventDefault();
const url = window.prompt(t(`editor.editorWidgets.${subject}.promptUrl`));
handleOnChange({ path: url ?? '' });
},
[handleOnChange, t],
);
const handleRemove = useCallback(
(e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
handleOnChange({ path: '' });
},
[handleOnChange],
);
const onRemoveOne = useCallback(
(index: number) => () => {
if (Array.isArray(internalValue)) {
const newValue = [...internalValue];
newValue.splice(index, 1);
handleOnChange({ path: newValue });
}
},
[handleOnChange, internalValue],
);
const onReplaceOne = useCallback(
(index: number) => () => {
return openMediaLibrary({
controlID,
forImage,
value: internalValue,
replaceIndex: index,
allowMultiple: false,
config: config?.media_library,
collection: collection as Collection<BaseField>,
field,
});
},
[openMediaLibrary, controlID, internalValue, config, collection, field],
);
// 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) => {
if (!link) {
return null;
}
const text =
link.length <= MAX_DISPLAY_LENGTH
? link
: `${link.slice(0, MAX_DISPLAY_LENGTH / 2)}\u2026${link.slice(
-(MAX_DISPLAY_LENGTH / 2) + 1,
)}`;
return (
<Link href={link} collection={collection} field={field}>
{text}
</Link>
);
},
[collection, field],
);
const renderedImagesLinks = useMemo(() => {
if (forImage) {
if (!internalValue) {
return null;
}
if (isMultiple(internalValue)) {
return (
<div key="multi-image-wrapper">
{internalValue.map((itemValue, index) => (
<SortableImage
key={`item-${itemValue}`}
itemValue={itemValue}
collection={collection}
field={field}
onRemove={onRemoveOne(index)}
onReplace={onReplaceOne(index)}
/>
))}
</div>
);
}
return (
<div key="single-image-wrapper">
<Image key="single-image" src={internalValue} collection={collection} field={field} />
</div>
);
}
if (isMultiple(internalValue)) {
return (
<div key="mulitple-file-links">
<ul key="file-links-list">
{internalValue.map(val => (
<li key={val}>{renderFileLink(val)}</li>
))}
</ul>
</div>
);
}
return <div key="single-file-links">{renderFileLink(internalValue)}</div>;
}, [collection, field, internalValue, onRemoveOne, onReplaceOne, renderFileLink]);
const content: JSX.Element = useMemo(() => {
const subject = forImage ? 'image' : 'file';
if (Array.isArray(internalValue) ? internalValue.length === 0 : isEmpty(internalValue)) {
return (
<div key="selection" className="flex flex-col gap-2 px-3 pt-2 pb-4">
<div key="controls" className="flex gap-2">
<Button
buttonRef={uploadButtonRef}
color="primary"
variant="outlined"
key="upload"
onClick={handleOpenMediaLibrary}
data-testid="choose-upload"
disabled={disabled}
>
{t(`editor.editorWidgets.${subject}.choose${allowsMultiple ? 'Multiple' : ''}`)}
</Button>
{chooseUrl ? (
<Button
color="primary"
variant="outlined"
key="choose-url"
onClick={handleUrl(subject)}
data-testid="choose-url"
disabled={disabled}
>
{t(`editor.editorWidgets.${subject}.chooseUrl`)}
</Button>
) : null}
</div>
</div>
);
}
return (
<div key="selection" className="flex flex-col gap-2 px-3 pt-2 pb-4">
{renderedImagesLinks}
<div key="controls" className="flex gap-2">
<Button
buttonRef={uploadButtonRef}
color="primary"
variant="outlined"
key="add-replace"
onClick={handleOpenMediaLibrary}
data-testid="add-replace-upload"
disabled={disabled}
>
{t(
`editor.editorWidgets.${subject}.${
allowsMultiple ? 'addMore' : 'chooseDifferent'
}`,
)}
</Button>
{chooseUrl && !allowsMultiple ? (
<Button
color="primary"
variant="outlined"
key="replace-url"
onClick={handleUrl(subject)}
data-testid="replace-url"
disabled={disabled}
>
{t(`editor.editorWidgets.${subject}.replaceUrl`)}
</Button>
) : null}
<Button
color="error"
variant="outlined"
key="remove"
onClick={handleRemove}
data-testid="remove-upload"
disabled={disabled}
>
{t(`editor.editorWidgets.${subject}.remove${allowsMultiple ? 'All' : ''}`)}
</Button>
</div>
</div>
);
}, [
internalValue,
renderedImagesLinks,
handleOpenMediaLibrary,
disabled,
t,
allowsMultiple,
chooseUrl,
handleUrl,
handleRemove,
]);
return useMemo(
() => (
<Field
inputRef={uploadButtonRef}
label={label}
errors={errors}
noPadding={!hasErrors}
hint={field.hint}
forSingleList={forSingleList}
cursor="pointer"
disabled={disabled}
>
{content}
</Field>
),
[content, disabled, errors, field.hint, forSingleList, hasErrors, label],
);
},
);
FileControl.displayName = 'FileControl';
return FileControl;
};
export default withFileControl;