119 lines
4.3 KiB
TypeScript
119 lines
4.3 KiB
TypeScript
import { useId, type ChangeEvent, type ReactNode } from "react";
|
||
import { Icon } from "./Icon.js";
|
||
|
||
export type MediaSource = "file" | "url";
|
||
export type MediaPreviewKind = "image" | "gif" | "video";
|
||
|
||
export interface MediaSourceFieldProps {
|
||
label: string;
|
||
kindLabel?: string;
|
||
source: MediaSource;
|
||
url: string;
|
||
fileName?: string | null;
|
||
uploading?: boolean;
|
||
previewSrc?: string | null;
|
||
previewKind?: MediaPreviewKind | null;
|
||
accept?: string;
|
||
fileButtonLabel?: string;
|
||
emptyFileLabel?: string;
|
||
path?: ReactNode;
|
||
hint?: ReactNode;
|
||
error?: ReactNode;
|
||
onSourceChange: (source: MediaSource) => void;
|
||
onUrlChange: (url: string) => void;
|
||
onFileChange: (file?: File) => void | Promise<void>;
|
||
}
|
||
|
||
function inferredPreviewKind(src: string): MediaPreviewKind {
|
||
if (/\.(mp4|webm|mov|m4v|avi|mkv)(\?.*)?$/i.test(src)) return "video";
|
||
if (/\.gif(\?.*)?$/i.test(src)) return "gif";
|
||
return "image";
|
||
}
|
||
|
||
function MediaPreview({ src, kind }: { src: string; kind?: MediaPreviewKind | null }) {
|
||
const resolvedKind = kind ?? inferredPreviewKind(src);
|
||
if (resolvedKind === "video") return <video src={src} autoPlay loop muted playsInline />;
|
||
return <img src={src} alt="" />;
|
||
}
|
||
|
||
export function MediaSourceField({
|
||
label,
|
||
kindLabel = "media",
|
||
source,
|
||
url,
|
||
fileName,
|
||
uploading = false,
|
||
previewSrc,
|
||
previewKind,
|
||
accept = "image/*,video/*",
|
||
fileButtonLabel = "Выберите файл",
|
||
emptyFileLabel = "Файл не выбран",
|
||
path,
|
||
hint,
|
||
error,
|
||
onSourceChange,
|
||
onUrlChange,
|
||
onFileChange,
|
||
}: MediaSourceFieldProps) {
|
||
const inputId = useId();
|
||
const displayFileName = uploading ? "Сохраняем в storage..." : (fileName || emptyFileLabel);
|
||
|
||
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||
void onFileChange(event.currentTarget.files?.[0]);
|
||
event.currentTarget.value = "";
|
||
};
|
||
|
||
return (
|
||
<div className="nodedc-media-field">
|
||
<div className="nodedc-media-field__label-row">
|
||
<span className="nodedc-media-field__label">{label}</span>
|
||
<span className="nodedc-media-field__kind">{kindLabel}</span>
|
||
</div>
|
||
<div className="nodedc-media-control" data-source={source}>
|
||
<div className="nodedc-media-file" hidden={source !== "file"} data-nodedc-media-source-panel="file">
|
||
<label className="nodedc-media-file__button" htmlFor={inputId}>{fileButtonLabel}</label>
|
||
<span className="nodedc-media-file__name" title={displayFileName}>{displayFileName}</span>
|
||
<input id={inputId} type="file" accept={accept} disabled={uploading} onChange={handleFileChange} />
|
||
</div>
|
||
<input
|
||
className="nodedc-media-url"
|
||
type="url"
|
||
value={url}
|
||
hidden={source !== "url"}
|
||
data-nodedc-media-source-panel="url"
|
||
placeholder="https://..."
|
||
autoComplete="off"
|
||
aria-label={`${label}: внешняя ссылка`}
|
||
onChange={(event) => onUrlChange(event.target.value)}
|
||
/>
|
||
<div className="nodedc-media-source-switch" aria-label={`${label}: источник`}>
|
||
<button
|
||
type="button"
|
||
className="nodedc-media-source-button"
|
||
data-active={source === "file" ? "true" : undefined}
|
||
data-nodedc-media-source-option="file"
|
||
aria-label="Файл с диска"
|
||
aria-pressed={source === "file"}
|
||
onClick={() => onSourceChange("file")}
|
||
>HD</button>
|
||
<button
|
||
type="button"
|
||
className="nodedc-media-source-button"
|
||
data-active={source === "url" ? "true" : undefined}
|
||
data-nodedc-media-source-option="url"
|
||
aria-label="Внешняя ссылка"
|
||
aria-pressed={source === "url"}
|
||
onClick={() => onSourceChange("url")}
|
||
>URL</button>
|
||
</div>
|
||
<div className="nodedc-media-preview" aria-hidden="true">
|
||
{previewSrc ? <MediaPreview src={previewSrc} kind={previewKind} /> : <Icon name="image" size={16} />}
|
||
</div>
|
||
</div>
|
||
{path ? <span className="nodedc-media-field__path">{path}</span> : null}
|
||
{hint ? <span className="nodedc-media-field__hint">{hint}</span> : null}
|
||
{error ? <span className="nodedc-media-field__error" role="alert">{error}</span> : null}
|
||
</div>
|
||
);
|
||
}
|