fix: 🐛 update focus on @tiptap editor
This commit is contained in:
parent
a146824ff3
commit
3f06aa64c6
3 changed files with 123 additions and 50 deletions
|
|
@ -9,6 +9,7 @@
|
|||
.tiptap {
|
||||
@apply px-3 py-2;
|
||||
|
||||
display: flow-root;
|
||||
min-height: 180px;
|
||||
|
||||
.link {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ $tertiary-color: #222222;
|
|||
font-size: 14px;
|
||||
color: $secondary-color;
|
||||
line-height: 1.75;
|
||||
display: flow-root;
|
||||
}
|
||||
|
||||
.description.small {
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ const PLACEMENT_STYLES: Record<PlacementValue, string> = {
|
|||
};
|
||||
|
||||
export default function ImageNodeView(props: NodeViewProps) {
|
||||
const { node, updateAttributes, selected } = props;
|
||||
const { node, updateAttributes, editor, selected } = props;
|
||||
const attrs = node.attrs as ImageAttrs;
|
||||
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
|
|
@ -96,7 +96,10 @@ export default function ImageNodeView(props: NodeViewProps) {
|
|||
startWidth: number;
|
||||
ratio: number;
|
||||
} | null>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const isInteractingWithChildren = useRef<boolean>(false);
|
||||
|
||||
const [editorFocused, setEditorFocused] = useState<boolean>(false);
|
||||
const [isResizing, setIsResizing] = useState<boolean>(false);
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
// Value of errors.
|
||||
|
|
@ -131,9 +134,21 @@ export default function ImageNodeView(props: NodeViewProps) {
|
|||
const transValidationAlt = transValidation(transAlt);
|
||||
const transValidationCaption = transValidation(transCaption);
|
||||
|
||||
/**
|
||||
* Handle click on specific children.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
const handleClickOnChildren = (): void => {
|
||||
isInteractingWithChildren.current = true;
|
||||
requestAnimationFrame(() => {
|
||||
isInteractingWithChildren.current = false;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current placement of the image (align or wrap or null).
|
||||
*
|
||||
*
|
||||
* @return PlacementValue | null
|
||||
*/
|
||||
const getCurrentPlacement = (): PlacementValue | null => {
|
||||
|
|
@ -148,8 +163,8 @@ export default function ImageNodeView(props: NodeViewProps) {
|
|||
|
||||
/**
|
||||
* Update attribute when changing placement.
|
||||
*
|
||||
* @param PlacementValue value
|
||||
*
|
||||
* @param PlacementValue value
|
||||
*/
|
||||
const handlePlacementChange = (value: PlacementValue) => {
|
||||
updateAttributes({
|
||||
|
|
@ -158,6 +173,7 @@ export default function ImageNodeView(props: NodeViewProps) {
|
|||
? (value.split("-")[1] as FloatOption)
|
||||
: null,
|
||||
});
|
||||
setEditorFocused(false);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -241,11 +257,10 @@ export default function ImageNodeView(props: NodeViewProps) {
|
|||
|
||||
try {
|
||||
// Validate data.
|
||||
const parsedZod = inputTextSchema
|
||||
.parse({
|
||||
alt: inputText.alt,
|
||||
caption: inputText.caption,
|
||||
});
|
||||
const parsedZod = inputTextSchema.parse({
|
||||
alt: inputText.alt,
|
||||
caption: inputText.caption,
|
||||
});
|
||||
|
||||
// Update data of the image in @tiptap editor.
|
||||
updateAttributes({
|
||||
|
|
@ -257,31 +272,31 @@ export default function ImageNodeView(props: NodeViewProps) {
|
|||
setNewErrors(error);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Set new errors from object errors
|
||||
*
|
||||
* @param err unknown
|
||||
* @return void
|
||||
*/
|
||||
const setNewErrors = (err: unknown): void => {
|
||||
const fieldErrors: typeof errors = {};
|
||||
const fieldMapping: Record<string, string> = {
|
||||
alt: "alt",
|
||||
caption: "caption",
|
||||
};
|
||||
if (err instanceof z.ZodError) {
|
||||
err.issues.forEach((error) => {
|
||||
const key = error.path[0] as string | undefined;
|
||||
const mappedKey =
|
||||
(key && fieldMapping[key]) ??
|
||||
(error.path.length === 0 ? "form" : undefined);
|
||||
if (mappedKey) fieldErrors[mappedKey] = error.message;
|
||||
});
|
||||
|
||||
setErrors(fieldErrors);
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Set new errors from object errors
|
||||
*
|
||||
* @param err unknown
|
||||
* @return void
|
||||
*/
|
||||
const setNewErrors = (err: unknown): void => {
|
||||
const fieldErrors: typeof errors = {};
|
||||
const fieldMapping: Record<string, string> = {
|
||||
alt: "alt",
|
||||
caption: "caption",
|
||||
};
|
||||
if (err instanceof z.ZodError) {
|
||||
err.issues.forEach((error) => {
|
||||
const key = error.path[0] as string | undefined;
|
||||
const mappedKey =
|
||||
(key && fieldMapping[key]) ??
|
||||
(error.path.length === 0 ? "form" : undefined);
|
||||
if (mappedKey) fieldErrors[mappedKey] = error.message;
|
||||
});
|
||||
|
||||
setErrors(fieldErrors);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Close modal and reset component.
|
||||
|
|
@ -291,8 +306,49 @@ export default function ImageNodeView(props: NodeViewProps) {
|
|||
*/
|
||||
const closeModal = (status: boolean = false): void => {
|
||||
setOpen(status);
|
||||
setEditorFocused(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle focus on editor.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
const handleFocus = (): void => {
|
||||
setEditorFocused(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle blur on editor.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
const handleBlur = ({ event }: { event?: FocusEvent }): void => {
|
||||
if (isInteractingWithChildren.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relatedTarget = event?.relatedTarget as HTMLElement | null;
|
||||
if (relatedTarget && wrapperRef.current?.contains(relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEditorFocused(false);
|
||||
};
|
||||
|
||||
// Update the focus on @tiptap editor.
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
|
||||
editor.on("focus", handleFocus);
|
||||
editor.on("blur", handleBlur);
|
||||
|
||||
return () => {
|
||||
editor.off("focus", handleFocus);
|
||||
editor.off("blur", handleBlur);
|
||||
};
|
||||
}, [editor]);
|
||||
|
||||
// Reset events when resizing.
|
||||
useEffect(() => {
|
||||
if (!isResizing) return;
|
||||
|
|
@ -318,31 +374,38 @@ export default function ImageNodeView(props: NodeViewProps) {
|
|||
// Set height of header + @tiptap toolbar editor with resize event.
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
const currentHeaderHeight = document.querySelector("header")?.clientHeight ?? 0;
|
||||
const currentToolbarHeight = document.querySelectorAll(".tiptap-toolbar")[0].clientHeight ?? 0;
|
||||
const currentHeaderHeight =
|
||||
document.querySelector("header")?.clientHeight ?? 0;
|
||||
const currentToolbarHeight =
|
||||
document.querySelectorAll(".tiptap-toolbar")[0].clientHeight ??
|
||||
0;
|
||||
setTopSticky(currentHeaderHeight + currentToolbarHeight);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
handleResize();
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper
|
||||
ref={wrapperRef}
|
||||
as="figure"
|
||||
data-type="image"
|
||||
style={parseStyleString(attrs.style)}
|
||||
className={cn("relative w-fit", {
|
||||
"outline outline-2 outline-dashed": selected,
|
||||
"outline outline-2 outline-dashed": editorFocused && selected,
|
||||
})}
|
||||
>
|
||||
{selected && (
|
||||
<div className="sticky" style={{
|
||||
top: topSticky,
|
||||
}}>
|
||||
<span className="absolute top-2 left-2 bg-background/90 backdrop-blur-sm rounded-md border shadow-md m-0 p-1">
|
||||
{editorFocused && selected && (
|
||||
<div
|
||||
className="sticky"
|
||||
style={{
|
||||
top: topSticky,
|
||||
}}
|
||||
>
|
||||
<span className="absolute top-2 left-2 bg-background backdrop-blur-sm rounded-md border shadow-md m-0 p-1">
|
||||
{`${attrs.width || imageDetails.width}x${attrs.height || imageDetails.height}`}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -355,18 +418,23 @@ export default function ImageNodeView(props: NodeViewProps) {
|
|||
height={attrs.height}
|
||||
draggable={false}
|
||||
/>
|
||||
{selected && (
|
||||
<div className="absolute bottom-2 right-2 flex flex-col gap-1 bg-background/90 backdrop-blur-sm rounded-md border shadow-md p-1">
|
||||
{editorFocused && selected && (
|
||||
<div className="absolute bottom-2 right-2 flex flex-col gap-1 bg-background backdrop-blur-sm rounded-md border shadow-md p-1">
|
||||
{/* Placement dropdown */}
|
||||
<Select
|
||||
value={getCurrentPlacement() || "default"}
|
||||
onValueChange={handlePlacementChange}
|
||||
>
|
||||
<SelectTrigger className="w-10 h-10 hover:bg-accent hover:text-accent-foreground p-0">
|
||||
<SelectTrigger
|
||||
className="w-10 h-10 hover:bg-accent hover:text-accent-foreground p-0"
|
||||
onPointerDown={handleClickOnChildren}
|
||||
>
|
||||
<LayoutIcon className="w-4 h-4 mx-auto" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="default" disabled>{useTrans("back.wysiwyg.image.position")}</SelectItem>
|
||||
<SelectItem value="default" disabled>
|
||||
{useTrans("back.wysiwyg.image.position")}
|
||||
</SelectItem>
|
||||
{PLACEMENT_OPTIONS.map((value) => {
|
||||
const [type, dir] = value.split("-") as [
|
||||
"align" | "float",
|
||||
|
|
@ -394,7 +462,9 @@ export default function ImageNodeView(props: NodeViewProps) {
|
|||
{value === "float-right" && (
|
||||
<ArrowRightLeftIcon className="w-4 h-4" />
|
||||
)}
|
||||
{useTrans(`back.wysiwyg.image.${str(label).slug().value()}`)}
|
||||
{useTrans(
|
||||
`back.wysiwyg.image.${str(label).slug().value()}`,
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
);
|
||||
|
|
@ -411,6 +481,7 @@ export default function ImageNodeView(props: NodeViewProps) {
|
|||
e.stopPropagation();
|
||||
setOpen(true);
|
||||
}}
|
||||
onPointerDown={handleClickOnChildren}
|
||||
>
|
||||
<ChevronsLeftRightEllipsisIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue