diff --git a/resources/sass/back/inputs/_wysiwyg.scss b/resources/sass/back/inputs/_wysiwyg.scss index 9777f93d..ed85066a 100644 --- a/resources/sass/back/inputs/_wysiwyg.scss +++ b/resources/sass/back/inputs/_wysiwyg.scss @@ -9,6 +9,7 @@ .tiptap { @apply px-3 py-2; + display: flow-root; min-height: 180px; .link { diff --git a/resources/sass/common/_wysiwyg.scss b/resources/sass/common/_wysiwyg.scss index 3cc9fd1f..b6b82174 100644 --- a/resources/sass/common/_wysiwyg.scss +++ b/resources/sass/common/_wysiwyg.scss @@ -7,6 +7,7 @@ $tertiary-color: #222222; font-size: 14px; color: $secondary-color; line-height: 1.75; + display: flow-root; } .description.small { diff --git a/resources/ts/back/Components/Inputs/Wysiwyg/Extensions/Image/ImageNodeView.tsx b/resources/ts/back/Components/Inputs/Wysiwyg/Extensions/Image/ImageNodeView.tsx index 19efe267..ecdb27ad 100644 --- a/resources/ts/back/Components/Inputs/Wysiwyg/Extensions/Image/ImageNodeView.tsx +++ b/resources/ts/back/Components/Inputs/Wysiwyg/Extensions/Image/ImageNodeView.tsx @@ -87,7 +87,7 @@ const PLACEMENT_STYLES: Record = { }; 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(null); @@ -96,7 +96,10 @@ export default function ImageNodeView(props: NodeViewProps) { startWidth: number; ratio: number; } | null>(null); + const wrapperRef = useRef(null); + const isInteractingWithChildren = useRef(false); + const [editorFocused, setEditorFocused] = useState(false); const [isResizing, setIsResizing] = useState(false); const [open, setOpen] = useState(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 = { - 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 = { + 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 ( - {selected && ( -
- + {editorFocused && selected && ( +
+ {`${attrs.width || imageDetails.width}x${attrs.height || imageDetails.height}`}
@@ -355,18 +418,23 @@ export default function ImageNodeView(props: NodeViewProps) { height={attrs.height} draggable={false} /> - {selected && ( -
+ {editorFocused && selected && ( +
{/* Placement dropdown */}