Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@ const AnchorField: React.FC<AnchorFieldProps> = ({
minWidth="0"
p="0"
isDisabled={!positions.includes(position.value)}
onClick={() => onChange(position.value)}
onClick={() => {
if (value === position.value) {
return onChange("")
}
onChange(position.value)
}}
borderRadius="md"
border={
value === position.value
Expand Down
37 changes: 36 additions & 1 deletion packages/imagekit-editor-dev/src/components/common/Hover.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Box, type BoxProps, Flex, type FlexProps } from "@chakra-ui/react"
import { useState } from "react"
import { useState, useEffect, useRef, useCallback } from "react"

interface FlexHoverProps extends FlexProps {
children(isHover: boolean): JSX.Element
Expand All @@ -15,6 +15,40 @@ const Hover = ({
}: BoxHoverProps | FlexHoverProps): JSX.Element => {
const [isHover, setIsHover] = useState<boolean>(false)

const hoverAreaRef = useRef<HTMLDivElement>(null)
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null)

const handleClickOutside = useCallback((event: MouseEvent): void => {
const hoverArea = hoverAreaRef.current
if (
hoverArea &&
!hoverArea.contains(event.target as Node)
) {
setIsHover(false)
}
}, [])

const debouncedHandleClickOutside = useCallback((event: MouseEvent): void => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current)
}
debounceTimerRef.current = setTimeout(() => {
handleClickOutside(event)
}, 100)
}, [handleClickOutside])

useEffect(() => {
document.addEventListener('mousedown', handleClickOutside)
document.addEventListener('mouseover', debouncedHandleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
document.removeEventListener('mouseover', debouncedHandleClickOutside)
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current)
}
}
}, [handleClickOutside, debouncedHandleClickOutside])

if (props.display === "flex") {
return (
<Flex
Expand All @@ -25,6 +59,7 @@ const Hover = ({
onMouseLeave={() => {
setIsHover(false)
}}
ref={hoverAreaRef}
>
{children(isHover)}
</Flex>
Expand Down
285 changes: 285 additions & 0 deletions packages/imagekit-editor-dev/src/components/common/PaddingInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
import {
Box,
Flex,
HStack,
Icon,
Text,
Input,
InputGroup,
InputLeftElement,
IconButton,
IconButtonProps,
useColorModeValue,
Tooltip,
} from "@chakra-ui/react"
import { set } from "lodash"
import type * as React from "react"
import { useState, useEffect, forwardRef } from "react"
import { LuArrowLeftToLine } from "@react-icons/all-files/lu/LuArrowLeftToLine"
import { LuArrowRightToLine } from "@react-icons/all-files/lu/LuArrowRightToLine"
import { LuArrowUpToLine } from "@react-icons/all-files/lu/LuArrowUpToLine"
import { LuArrowDownToLine } from "@react-icons/all-files/lu/LuArrowDownToLine"
import { TbBoxPadding } from "@react-icons/all-files/tb/TbBoxPadding"
import { MdOutlinePadding } from "@react-icons/all-files/md/MdOutlinePadding"
import { FieldErrors } from "react-hook-form"

type PaddingInputFieldProps = {
id?: string
onChange: (value: number | PaddingObject | string) => void
errors?: FieldErrors<Record<string, unknown>>
name: string
}

type PaddingObject = {
top: number | null
right: number | null
bottom: number | null
left: number | null
}

function getUpdatedPaddingValue(
current: number | PaddingObject | null | string,
side: "top" | "right" | "bottom" | "left" | "all",
value: string,
mode: "uniform" | "individual"
): number | PaddingObject | null | string {
let inputValue: number | PaddingObject | null | string
try {
inputValue = JSON.parse(value)
} catch {
inputValue = value
}
if (mode === "uniform") {
if (typeof inputValue === "number") {
return inputValue
} else if (inputValue === null) {
return null
} else if (typeof inputValue === "string") {
return inputValue
} else {
const { top, right, bottom, left } = inputValue
if (top === right && top === bottom && top === left) {
return top
} else {
return null
}
}
} else {
let commonValue: number | null = null
if (typeof inputValue === "number") {
commonValue = inputValue
}
const updatedPadding = current && typeof current === "object"
? { ...current }
: { top: commonValue, right: commonValue, bottom: commonValue, left: commonValue }
if (side !== "all") {
set(updatedPadding, side, inputValue)
}
return updatedPadding
}
}

export const PaddingInputField: React.FC<PaddingInputFieldProps> = ({
id,
onChange,
errors,
name: propertyName,
}) => {
const [paddingMode, setPaddingMode] = useState<"uniform" | "individual">("uniform")
const [paddingValue, setPaddingValue] = useState<number | PaddingObject | null | string>("")
const errorRed = useColorModeValue("red.500", "red.300")
const activeColor = useColorModeValue("blue.500", "blue.600")
const inactiveColor = useColorModeValue("gray.600", "gray.400")

useEffect(() => {
const formatPaddingValue = (value: number | PaddingObject | null | string): string | PaddingObject => {
if (value === null) return ""
if (typeof value === "number") {
return value.toString()
} else if (typeof value === "string") {
return value
} else {
return value;
}
}
const formattedValue = formatPaddingValue(paddingValue)
onChange(formattedValue)
}, [paddingValue])


return (
<HStack
as="fieldset"
id={id}
role="group"
spacing={2}
alignItems="stretch"
justifyContent="space-between"
>
<Flex direction="row" flex="1" flexWrap="wrap" gap={2}>
{ paddingMode === "uniform" ? (
<Box flex="1">
<Input
type="number"
min={0}
onChange={(e) => {
const val = e.target.value
setPaddingValue(getUpdatedPaddingValue(
paddingValue,
"all",
val,
paddingMode
))
}}
value={["number", "string"].includes(typeof paddingValue) ? paddingValue : ""}
placeholder="Uniform Padding"
isInvalid={!!errors?.[propertyName]}
/>
<Text fontSize='xs' color={errorRed}>{errors?.[propertyName]?.message}</Text>
</Box>
) : (
<>
<Box flex="1 1 calc(50% - 4px)">
<InputGroup>
<InputLeftElement pointerEvents="none" fontSize="0.7em">
<Icon as={LuArrowUpToLine} color="gray.500" />
</InputLeftElement>
<Input
type="number"
min={0}
onChange={(e) => {
const val = e.target.value
setPaddingValue(getUpdatedPaddingValue(
paddingValue,
"top",
val,
paddingMode
))
}}
value={typeof paddingValue === "object" ? paddingValue?.top ?? "" : ""}
placeholder="Top"
isInvalid={!!errors?.[propertyName]?.top}
/>
</InputGroup>
<Text fontSize='xs' color={errorRed}>{errors?.[propertyName]?.top?.message}</Text>
</Box>

<Box flex="1 1 calc(50% - 4px)">
<InputGroup>
<InputLeftElement pointerEvents="none" fontSize="0.7em">
<Icon as={LuArrowRightToLine} color="gray.500" />
</InputLeftElement>
<Input
type="number"
min={0}
onChange={(e) => {
const val = e.target.value
setPaddingValue(getUpdatedPaddingValue(
paddingValue,
"right",
val,
paddingMode
))
}}
value={typeof paddingValue === "object" ? paddingValue?.right ?? "" : ""}
placeholder="Right"
isInvalid={!!errors?.[propertyName]?.right}
/>
</InputGroup>
<Text fontSize='xs' color={errorRed}>{errors?.[propertyName]?.right?.message}</Text>
</Box>

<Box flex="1 1 calc(50% - 4px)">
<InputGroup>
<InputLeftElement pointerEvents="none" fontSize="0.7em">
<Icon as={LuArrowDownToLine} color="gray.500" />
</InputLeftElement>
<Input
type="number"
min={0}
flex="1 1 calc(50% - 4px)"
onChange={(e) => {
const val = e.target.value
setPaddingValue(getUpdatedPaddingValue(
paddingValue,
"bottom",
val,
paddingMode
))
}}
value={typeof paddingValue === "object" ? paddingValue?.bottom ?? "" : ""}
placeholder="Bottom"
isInvalid={!!errors?.[propertyName]?.bottom}
/>
</InputGroup>
<Text fontSize='xs' color={errorRed}>{errors?.[propertyName]?.bottom?.message}</Text>
</Box>

<Box flex="1 1 calc(50% - 4px)">
<InputGroup>
<InputLeftElement pointerEvents="none" fontSize="0.7em">
<Icon as={LuArrowLeftToLine} color="gray.500" />
</InputLeftElement>
<Input
type="number"
min={0}
flex="1 1 calc(50% - 4px)"
onChange={(e) => {
const val = e.target.value
setPaddingValue(getUpdatedPaddingValue(
paddingValue,
"left",
val,
paddingMode
))
}}
value={typeof paddingValue === "object" ? paddingValue?.left ?? "" : ""}
placeholder="Left"
isInvalid={!!errors?.[propertyName]?.left}
/>
</InputGroup>
<Text fontSize='xs' color={errorRed}>{errors?.[propertyName]?.left?.message}</Text>
</Box>

</>
) }
</Flex>
<Tooltip
hasArrow
label={paddingMode === "uniform" ? "Enable individual padding" : "Disable individual padding"}
openDelay={200}
modifiers={[
{
name: 'zIndex',
enabled: true,
phase: 'write',
fn({ state }) {
state.elements.popper.style.zIndex = '2100';
},
},
]}
>
<IconButton
aria-label={paddingMode === "uniform" ? "Switch to individual padding" : "Switch to uniform padding"}
aria-pressed={paddingMode === "individual"}
icon={<TbBoxPadding size={20} />}
padding="0.05em"
onClick={() => {
const newPaddingMode = paddingMode === "uniform" ? "individual" : "uniform"
setPaddingValue(getUpdatedPaddingValue(
paddingValue,
"all",
JSON.stringify(paddingValue),
newPaddingMode
))
setPaddingMode(newPaddingMode)
}}
variant="outline"
color={paddingMode === "individual" ? activeColor : inactiveColor}
/>
</Tooltip>
</HStack>
)
}

export default PaddingInputField
Loading