diff --git a/remix/app/components/FocalPointPicker/focalPointPicker.css b/remix/app/components/FocalPointPicker/focalPointPicker.css new file mode 100644 index 0000000..9fd18f5 --- /dev/null +++ b/remix/app/components/FocalPointPicker/focalPointPicker.css @@ -0,0 +1,47 @@ +.container { + display: grid; + gap: 1rem; + border-right: 1px solid #eee; + padding-right: 1rem; +} + +.focalPointWrapper { + position: relative; + user-select: none; + justify-self: center; +} + +.focalPointButton { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: rgba(255, 0, 0, 0.4); + border: 3px solid red; + display: block; + width: 25px; + height: 25px; + border-radius: 50%; +} + +.focalPointImage { + height: 100%; + max-height: 350px; +} + +.examples { + width: 100%; + max-width: 600px; + max-height: 400px; + display: grid; + grid-template-columns: 3fr 1fr; + grid-template-rows: 2fr 1fr; + gap: 0.25rem; +} + +.examples img { + width: 100%; + height: 100%; + object-fit: cover; + overflow: hidden; +} diff --git a/remix/app/components/FocalPointPicker/index.tsx b/remix/app/components/FocalPointPicker/index.tsx new file mode 100644 index 0000000..c2d5466 --- /dev/null +++ b/remix/app/components/FocalPointPicker/index.tsx @@ -0,0 +1,50 @@ +import { FocalPoint } from "~/types/imagevault" +import "./focalPointPicker.css" +import useFocalPoint from "~/hooks/useFocalPoint" + +export interface FocalPointPickerProps { + focalPoint?: FocalPoint + imageSrc: string + onChange: (focalPoint: FocalPoint) => void +} + +export default function FocalPointPicker({ + focalPoint, + imageSrc, + onChange, +}: FocalPointPickerProps) { + const { ref, x, y, onMove, canMove, setCanMove } = useFocalPoint({ + focalPoint, + onChange, + }) + + const imagesArray = Array.from({ length: 4 }) + + return ( +
+
+ + +
+
+ {imagesArray.map((_, idx) => ( + + ))} +
+
+ ) +} diff --git a/remix/app/components/ImageEditModal.tsx b/remix/app/components/ImageEditModal.tsx index bc89912..9ee53ab 100644 --- a/remix/app/components/ImageEditModal.tsx +++ b/remix/app/components/ImageEditModal.tsx @@ -10,7 +10,8 @@ import { TextInput, } from "@contentstack/venus-components" -import type { InsertResponse } from "~/types/imagevault" +import type { FocalPoint, InsertResponse } from "~/types/imagevault" +import FocalPointPicker from "./FocalPointPicker" type ImageEditModalProps = { fieldData: InsertResponse @@ -25,6 +26,7 @@ export default function ImageEditModal({ }: ImageEditModalProps) { const [altText, setAltText] = useState("") const [caption, setCaption] = useState("") + const [focalPoint, setFocalPoint] = useState({ x: 50, y: 50 }) const assetUrl = fieldData.MediaConversions[0].Url @@ -43,6 +45,12 @@ export default function ImageEditModal({ } }, [fieldData.Metadata]) + useEffect(() => { + if (fieldData.FocalPoint) { + setFocalPoint(fieldData.FocalPoint) + } + }, [fieldData.FocalPoint]) + function handleSave() { const metaData = fieldData.Metadata ?? [] @@ -59,31 +67,34 @@ export default function ImageEditModal({ setData({ ...fieldData, Metadata: newMetadata, + FocalPoint: focalPoint, }) closeModal() } + function changeFocalPoint(focalPoint: FocalPoint) { + setFocalPoint(focalPoint) + } + return ( <> -
- {altText} -
-
+ +
Alt text + + + Focal Point + +
diff --git a/remix/app/components/ImageVaultDAM.tsx b/remix/app/components/ImageVaultDAM.tsx index 6c1d748..f2cefc4 100644 --- a/remix/app/components/ImageVaultDAM.tsx +++ b/remix/app/components/ImageVaultDAM.tsx @@ -138,9 +138,15 @@ export default function ImageVaultDAM({ (result?: InsertResponse) => { if (field) { flushSync(() => { - setMedia(result || null) + const data = result + ? { + ...result, + FocalPoint: result.FocalPoint || { x: 50, y: 50 }, + } + : null + setMedia(data) // Data inside the field is supposed to be an empty object if nothing is selected - field.setData(result || {}) + field.setData(data || {}) document.body.style.overflow = "hidden" }) } @@ -168,6 +174,13 @@ export default function ImageVaultDAM({ ), modalProps: { size: "max", + style: { + content: { + maxHeight: "90dvh", + width: "auto", + }, + overlay: {}, + }, }, }) } diff --git a/remix/app/hooks/useFocalPoint.ts b/remix/app/hooks/useFocalPoint.ts new file mode 100644 index 0000000..16d3c8f --- /dev/null +++ b/remix/app/hooks/useFocalPoint.ts @@ -0,0 +1,59 @@ +import { useCallback, useRef, useState, MouseEvent, useEffect } from "react" +import { FocalPoint } from "~/types/imagevault" + +interface UseFocalPointProps { + focalPoint?: FocalPoint + onChange: (focalPoint: FocalPoint) => void +} + +const DEFAULT_PERCENTAGE = 50 + +export default function useFocalPoint({ + focalPoint, + onChange, +}: UseFocalPointProps) { + const ref = useRef(null) + const [x, setX] = useState(DEFAULT_PERCENTAGE) + const [y, setY] = useState(DEFAULT_PERCENTAGE) + const [canMove, setCanMove] = useState(false) + + useEffect(() => { + if (focalPoint) { + setX(focalPoint.x) + setY(focalPoint.y) + } + }, [focalPoint]) + + const onMove = useCallback( + (e: MouseEvent) => { + if (canMove) { + const containerBoundingRectangle = ref.current!.getBoundingClientRect() + const xPixels = e.clientX - containerBoundingRectangle.left + const yPixels = e.clientY - containerBoundingRectangle.top + let x = Math.min( + Math.max((xPixels * 100) / ref.current!.clientWidth, 0), + 100 + ) + let y = Math.min( + Math.max((yPixels * 100) / ref.current!.clientHeight, 0), + 100 + ) + x = parseFloat(x.toFixed(2)) + y = parseFloat(y.toFixed(2)) + setX(x) + setY(y) + onChange({ x, y }) + } + }, + [canMove, onChange] + ) + + return { + ref, + x, + y, + onMove, + canMove, + setCanMove, + } +} diff --git a/remix/app/types/FocalPoint.ts b/remix/app/types/FocalPoint.ts new file mode 100644 index 0000000..e69de29 diff --git a/types/imagevault.ts b/types/imagevault.ts index fd43e94..d895741 100644 --- a/types/imagevault.ts +++ b/types/imagevault.ts @@ -16,6 +16,11 @@ declare global { } } +export interface FocalPoint { + x: number + y: number +} + export declare class InsertMediaWindow { constructor(config: Config, windowOptions: string) openImageVault: () => void @@ -58,6 +63,7 @@ export type ImageVaultAsset = { height: number aspectRatio: number } + focalPoint: FocalPoint meta: { alt: string | undefined; caption: string | undefined } } @@ -91,6 +97,8 @@ export declare class InsertResponse { AddedBy: string Metadata?: MetaData[] | undefined + + FocalPoint: FocalPoint } /** diff --git a/utils/imagevault.ts b/utils/imagevault.ts index caae2d0..ff787b3 100644 --- a/utils/imagevault.ts +++ b/utils/imagevault.ts @@ -4,6 +4,7 @@ import type { GenericObjectType } from "@contentstack/app-sdk/dist/src/types/com import type { Lang } from "../types/lang" import type { Config, + FocalPoint, ImageVaultAsset, InsertResponse, PublishDetails, @@ -70,7 +71,8 @@ export function isImageVaultDAMConfig( // For RTE this function is not enough since rte:s also need attrs, like position and the size thats // chosen in the editor export function insertResponseToImageVaultAsset( - response: InsertResponse + response: InsertResponse, + focalPoint: FocalPoint ): ImageVaultAsset { const alt = response.Metadata?.find((meta) => meta.Name.includes("AltText_") @@ -93,6 +95,7 @@ export function insertResponseToImageVaultAsset( height: response.MediaConversions[0].Height, aspectRatio: response.MediaConversions[0].FormatAspectRatio, }, + focalPoint, } }