feat: implemented focal point picker inside ImageVault

This commit is contained in:
Erik Tiekstra
2024-10-14 16:07:32 +02:00
parent 5f9bd57a7c
commit b1493bcd3d
8 changed files with 217 additions and 16 deletions

View File

@@ -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;
}

View File

@@ -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 (
<div className="container">
<div className="focalPointWrapper" ref={ref} onMouseMove={onMove}>
<button
className="focalPointButton"
style={{
left: `${x}%`,
top: `${y}%`,
cursor: canMove ? "grabbing" : "grab",
}}
onMouseDown={() => setCanMove(true)}
onMouseUp={() => setCanMove(false)}
></button>
<img className="focalPointImage" src={imageSrc} alt="" />
</div>
<div className="examples">
{imagesArray.map((_, idx) => (
<img
key={idx}
src={imageSrc}
alt=""
style={{ objectPosition: `${x}% ${y}%` }}
/>
))}
</div>
</div>
)
}

View File

@@ -10,7 +10,8 @@ import {
TextInput, TextInput,
} from "@contentstack/venus-components" } from "@contentstack/venus-components"
import type { InsertResponse } from "~/types/imagevault" import type { FocalPoint, InsertResponse } from "~/types/imagevault"
import FocalPointPicker from "./FocalPointPicker"
type ImageEditModalProps = { type ImageEditModalProps = {
fieldData: InsertResponse fieldData: InsertResponse
@@ -25,6 +26,7 @@ export default function ImageEditModal({
}: ImageEditModalProps) { }: ImageEditModalProps) {
const [altText, setAltText] = useState("") const [altText, setAltText] = useState("")
const [caption, setCaption] = useState("") const [caption, setCaption] = useState("")
const [focalPoint, setFocalPoint] = useState<FocalPoint>({ x: 50, y: 50 })
const assetUrl = fieldData.MediaConversions[0].Url const assetUrl = fieldData.MediaConversions[0].Url
@@ -43,6 +45,12 @@ export default function ImageEditModal({
} }
}, [fieldData.Metadata]) }, [fieldData.Metadata])
useEffect(() => {
if (fieldData.FocalPoint) {
setFocalPoint(fieldData.FocalPoint)
}
}, [fieldData.FocalPoint])
function handleSave() { function handleSave() {
const metaData = fieldData.Metadata ?? [] const metaData = fieldData.Metadata ?? []
@@ -59,31 +67,34 @@ export default function ImageEditModal({
setData({ setData({
...fieldData, ...fieldData,
Metadata: newMetadata, Metadata: newMetadata,
FocalPoint: focalPoint,
}) })
closeModal() closeModal()
} }
function changeFocalPoint(focalPoint: FocalPoint) {
setFocalPoint(focalPoint)
}
return ( return (
<> <>
<ModalHeader title="Update image" closeModal={closeModal} /> <ModalHeader title="Update image" closeModal={closeModal} />
<ModalBody <ModalBody
style={{ style={{
display: "flex", display: "grid",
gridTemplateColumns: "1fr minmax(max-content, 250px)",
gap: "1rem", gap: "1rem",
justifyContent: "space-between",
alignItems: "center", alignItems: "center",
width: "100%", width: "auto",
maxHeight: "none",
}} }}
> >
<div style={{ flex: 1, overflowY: "auto" }}> <FocalPointPicker
<img imageSrc={assetUrl}
src={assetUrl} focalPoint={focalPoint}
alt={altText} onChange={changeFocalPoint}
height="100%" />
style={{ maxHeight: "345px" }} <div>
/>
</div>
<div style={{ flex: 1 }}>
<FieldComponent> <FieldComponent>
<FieldLabel htmlFor="alt">Alt text</FieldLabel> <FieldLabel htmlFor="alt">Alt text</FieldLabel>
<TextInput <TextInput
@@ -107,6 +118,16 @@ export default function ImageEditModal({
} }
/> />
</FieldComponent> </FieldComponent>
<FieldComponent>
<FieldLabel htmlFor="focalPoint">Focal Point</FieldLabel>
<TextInput
value={`X: ${focalPoint.x}, Y: ${focalPoint.y}`}
placeholder="Caption for image..."
name="caption"
disabled
/>
</FieldComponent>
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>

View File

@@ -138,9 +138,15 @@ export default function ImageVaultDAM({
(result?: InsertResponse) => { (result?: InsertResponse) => {
if (field) { if (field) {
flushSync(() => { 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 // 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" document.body.style.overflow = "hidden"
}) })
} }
@@ -168,6 +174,13 @@ export default function ImageVaultDAM({
), ),
modalProps: { modalProps: {
size: "max", size: "max",
style: {
content: {
maxHeight: "90dvh",
width: "auto",
},
overlay: {},
},
}, },
}) })
} }

View File

@@ -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<HTMLDivElement>(null)
const [x, setX] = useState<number>(DEFAULT_PERCENTAGE)
const [y, setY] = useState<number>(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,
}
}

View File

View File

@@ -16,6 +16,11 @@ declare global {
} }
} }
export interface FocalPoint {
x: number
y: number
}
export declare class InsertMediaWindow { export declare class InsertMediaWindow {
constructor(config: Config, windowOptions: string) constructor(config: Config, windowOptions: string)
openImageVault: () => void openImageVault: () => void
@@ -58,6 +63,7 @@ export type ImageVaultAsset = {
height: number height: number
aspectRatio: number aspectRatio: number
} }
focalPoint: FocalPoint
meta: { alt: string | undefined; caption: string | undefined } meta: { alt: string | undefined; caption: string | undefined }
} }
@@ -91,6 +97,8 @@ export declare class InsertResponse {
AddedBy: string AddedBy: string
Metadata?: MetaData[] | undefined Metadata?: MetaData[] | undefined
FocalPoint: FocalPoint
} }
/** /**

View File

@@ -4,6 +4,7 @@ import type { GenericObjectType } from "@contentstack/app-sdk/dist/src/types/com
import type { Lang } from "../types/lang" import type { Lang } from "../types/lang"
import type { import type {
Config, Config,
FocalPoint,
ImageVaultAsset, ImageVaultAsset,
InsertResponse, InsertResponse,
PublishDetails, 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 // For RTE this function is not enough since rte:s also need attrs, like position and the size thats
// chosen in the editor // chosen in the editor
export function insertResponseToImageVaultAsset( export function insertResponseToImageVaultAsset(
response: InsertResponse response: InsertResponse,
focalPoint: FocalPoint
): ImageVaultAsset { ): ImageVaultAsset {
const alt = response.Metadata?.find((meta) => const alt = response.Metadata?.find((meta) =>
meta.Name.includes("AltText_") meta.Name.includes("AltText_")
@@ -93,6 +95,7 @@ export function insertResponseToImageVaultAsset(
height: response.MediaConversions[0].Height, height: response.MediaConversions[0].Height,
aspectRatio: response.MediaConversions[0].FormatAspectRatio, aspectRatio: response.MediaConversions[0].FormatAspectRatio,
}, },
focalPoint,
} }
} }