feat: implemented focal point picker inside ImageVault
This commit is contained in:
47
remix/app/components/FocalPointPicker/focalPointPicker.css
Normal file
47
remix/app/components/FocalPointPicker/focalPointPicker.css
Normal 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;
|
||||||
|
}
|
||||||
50
remix/app/components/FocalPointPicker/index.tsx
Normal file
50
remix/app/components/FocalPointPicker/index.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
59
remix/app/hooks/useFocalPoint.ts
Normal file
59
remix/app/hooks/useFocalPoint.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
0
remix/app/types/FocalPoint.ts
Normal file
0
remix/app/types/FocalPoint.ts
Normal 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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user