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 (
<>
-
-

-
-
+
+
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,
}
}