From b9b940fc842d13715cf085e4a78a495e6bac01cb Mon Sep 17 00:00:00 2001 From: Jarrod Flesch <30633324+JarrodMFlesch@users.noreply.github.com> Date: Wed, 8 May 2024 08:27:30 -0400 Subject: [PATCH] feat: improves crop rendering in thumbnail (#6260) --- packages/ui/src/elements/EditUpload/index.tsx | 70 +++++++++++-------- packages/ui/src/elements/Upload/index.scss | 13 +++- packages/ui/src/elements/Upload/index.tsx | 46 +++++++++++- 3 files changed, 98 insertions(+), 31 deletions(-) diff --git a/packages/ui/src/elements/EditUpload/index.tsx b/packages/ui/src/elements/EditUpload/index.tsx index d1aa7d4e19b..fcef403a3c9 100644 --- a/packages/ui/src/elements/EditUpload/index.tsx +++ b/packages/ui/src/elements/EditUpload/index.tsx @@ -9,7 +9,6 @@ import 'react-image-crop/dist/ReactCrop.css' import { editDrawerSlug } from '../../elements/Upload/index.js' import { Plus } from '../../icons/Plus/index.js' -import { useFormQueryParams } from '../../providers/FormQueryParams/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { Button } from '../Button/index.js' import './index.scss' @@ -27,40 +26,60 @@ const Input: React.FC<{ name: string; onChange: (value: string) => void; value: ) +type FocalPosition = { + x: number + y: number +} + export type EditUploadProps = { doc?: Data fileName: string fileSrc: string imageCacheTag?: string + initialCrop?: CropType + initialFocalPoint?: FocalPosition + onSave?: ({ crop, pointPosition }: { crop: CropType; pointPosition: FocalPosition }) => void showCrop?: boolean showFocalPoint?: boolean } +const defaultCrop: CropType = { + height: 100, + unit: '%', + width: 100, + x: 0, + y: 0, +} + +const defaultPointPosition: FocalPosition = { + x: 50, + y: 50, +} + export const EditUpload: React.FC = ({ fileName, fileSrc, imageCacheTag, + initialCrop, + initialFocalPoint, + onSave, showCrop, showFocalPoint, }) => { - const { Modal, useModal } = facelessUIImport + const { useModal } = facelessUIImport const { closeModal } = useModal() const { t } = useTranslation() - const { dispatchFormQueryParams, formQueryParams } = useFormQueryParams() - const { uploadEdits } = formQueryParams || {} - const [crop, setCrop] = useState({ - height: uploadEdits?.crop?.height || 100, - unit: '%', - width: uploadEdits?.crop?.width || 100, - x: uploadEdits?.crop?.x || 0, - y: uploadEdits?.crop?.y || 0, - }) - - const [pointPosition, setPointPosition] = useState<{ x: number; y: number }>({ - x: uploadEdits?.focalPoint?.x || 50, - y: uploadEdits?.focalPoint?.y || 50, - }) + + const [crop, setCrop] = useState(() => ({ + ...defaultCrop, + ...initialCrop, + })) + + const [pointPosition, setPointPosition] = useState(() => ({ + ...defaultPointPosition, + ...initialFocalPoint, + })) const [checkBounds, setCheckBounds] = useState(false) const [originalHeight, setOriginalHeight] = useState(0) const [originalWidth, setOriginalWidth] = useState(0) @@ -92,18 +111,11 @@ export const EditUpload: React.FC = ({ } const saveEdits = () => { - dispatchFormQueryParams({ - type: 'SET', - params: { - uploadEdits: - crop || pointPosition - ? { - crop: crop || null, - focalPoint: pointPosition ? pointPosition : null, - } - : null, - }, - }) + if (typeof onSave === 'function') + onSave({ + crop, + pointPosition, + }) closeModal(editDrawerSlug) } @@ -145,7 +157,7 @@ export const EditUpload: React.FC = ({ aria-label={t('general:applyChanges')} buttonStyle="primary" className={`${baseClass}__save`} - onClick={() => saveEdits()} + onClick={saveEdits} > {t('general:applyChanges')} diff --git a/packages/ui/src/elements/Upload/index.scss b/packages/ui/src/elements/Upload/index.scss index 44342c72e62..772a812f5a7 100644 --- a/packages/ui/src/elements/Upload/index.scss +++ b/packages/ui/src/elements/Upload/index.scss @@ -23,13 +23,24 @@ width: 150px; .thumbnail { - position: absolute; + position: relative; width: 100%; height: 100%; object-fit: contain; } } + .file-details { + img { + position: relative; + min-width: 100%; + height: 100%; + transform: scale(var(--file-details-thumbnail--zoom)); + top: var(--file-details-thumbnail--top-offset); + left: var(--file-details-thumbnail--left-offset); + } + } + &__remove { margin: calc($baseline * 1.5) $baseline $baseline 0; place-self: flex-start; diff --git a/packages/ui/src/elements/Upload/index.tsx b/packages/ui/src/elements/Upload/index.tsx index da459b30a10..53569910dbc 100644 --- a/packages/ui/src/elements/Upload/index.tsx +++ b/packages/ui/src/elements/Upload/index.tsx @@ -2,11 +2,12 @@ import type { FormState, SanitizedCollectionConfig } from 'payload/types' import { FieldError } from '@payloadcms/ui/forms/FieldError' +import { useFormQueryParams } from '@payloadcms/ui/providers/FormQueryParams' import { isImage } from 'payload/utilities' import React, { useCallback, useEffect, useState } from 'react' import { fieldBaseClass } from '../../fields/shared/index.js' -import { useFormSubmitted } from '../../forms/Form/context.js' +import { useForm, useFormSubmitted } from '../../forms/Form/context.js' import { useField } from '../../forms/useField/index.js' import { useDocumentInfo } from '../../providers/DocumentInfo/index.js' import { useTranslation } from '../../providers/Translation/index.js' @@ -65,12 +66,15 @@ export const Upload: React.FC = (props) => { const [replacingFile, setReplacingFile] = useState(false) const [fileSrc, setFileSrc] = useState(null) const { t } = useTranslation() + const { setModified } = useForm() + const { dispatchFormQueryParams, formQueryParams } = useFormQueryParams() const [doc, setDoc] = useState(reduceFieldsToValues(initialState || {}, true)) const { docPermissions } = useDocumentInfo() const { errorMessage, setValue, showError, value } = useField({ path: 'file', validate, }) + const [crop, setCrop] = useState({ x: 0, y: 0 }) const handleFileNameChange = (e: React.ChangeEvent) => { const updatedFileName = e.target.value @@ -96,6 +100,40 @@ export const Upload: React.FC = (props) => { setFileSrc('') }, [setValue]) + const onEditsSave = React.useCallback( + ({ crop, pointPosition }) => { + setCrop({ + x: crop.x || 0, + y: crop.y || 0, + }) + const zoomScale = 100 / Math.min(crop.width, crop.height) + + document.documentElement.style.setProperty('--file-details-thumbnail--zoom', `${zoomScale}`) + document.documentElement.style.setProperty( + '--file-details-thumbnail--top-offset', + `${zoomScale * (50 - crop.height / 2 - crop.y)}%`, + ) + document.documentElement.style.setProperty( + '--file-details-thumbnail--left-offset', + `${zoomScale * (50 - crop.width / 2 - crop.x)}%`, + ) + setModified(true) + dispatchFormQueryParams({ + type: 'SET', + params: { + uploadEdits: + crop || pointPosition + ? { + crop: crop || null, + focalPoint: pointPosition ? pointPosition : null, + } + : null, + }, + }) + }, + [dispatchFormQueryParams, setModified], + ) + useEffect(() => { setDoc(reduceFieldsToValues(initialState || {}, true)) setReplacingFile(false) @@ -200,6 +238,12 @@ export const Upload: React.FC = (props) => { fileName={value?.name || doc?.filename} fileSrc={fileSrc || doc?.url} imageCacheTag={lastSubmittedTime} + initialCrop={formQueryParams?.uploadEdits?.crop ?? {}} + initialFocalPoint={{ + x: formQueryParams?.uploadEdits?.focalPoint.x || 0, + y: formQueryParams?.uploadEdits?.focalPoint.y || 0, + }} + onSave={onEditsSave} showCrop={showCrop} showFocalPoint={showFocalPoint} />