diff --git a/web/shared/editor/components/measured-container.tsx b/web/shared/editor/components/measured-container.tsx new file mode 100644 index 00000000..074b7c74 --- /dev/null +++ b/web/shared/editor/components/measured-container.tsx @@ -0,0 +1,39 @@ +import * as React from "react" +import { useContainerSize } from "../hooks/use-container-size" + +interface MeasuredContainerProps { + as: T + name: string + children?: React.ReactNode +} + +export const MeasuredContainer = React.forwardRef( + ( + { + as: Component, + name, + children, + style = {}, + ...props + }: MeasuredContainerProps & React.ComponentProps, + ref: React.Ref, + ) => { + const innerRef = React.useRef(null) + const rect = useContainerSize(innerRef.current) + + React.useImperativeHandle(ref, () => innerRef.current as HTMLElement) + + const customStyle = { + [`--${name}-width`]: `${rect.width}px`, + [`--${name}-height`]: `${rect.height}px`, + } + + return ( + + {children} + + ) + }, +) + +MeasuredContainer.displayName = "MeasuredContainer" diff --git a/web/shared/editor/editor.tsx b/web/shared/editor/editor.tsx index f5ba1123..ff775eb9 100644 --- a/web/shared/editor/editor.tsx +++ b/web/shared/editor/editor.tsx @@ -4,6 +4,7 @@ import { Content } from "@tiptap/core" import { BubbleMenu } from "./components/bubble-menu" import { cn } from "@/lib/utils" import { useLaEditor, UseLaEditorProps } from "./hooks/use-la-editor" +import { MeasuredContainer } from "./components/measured-container" export interface LaEditorProps extends UseLaEditorProps { value?: Content @@ -21,7 +22,9 @@ export const LaEditor = React.memo( } return ( -
@@ -30,7 +33,7 @@ export const LaEditor = React.memo( className={cn("la-editor", editorContentClassName)} /> -
+ ) }, ), diff --git a/web/shared/editor/extensions/image/components/image-view-block.tsx b/web/shared/editor/extensions/image/components/image-view-block.tsx index a66acee7..0242235f 100644 --- a/web/shared/editor/extensions/image/components/image-view-block.tsx +++ b/web/shared/editor/extensions/image/components/image-view-block.tsx @@ -4,13 +4,12 @@ import type { ElementDimensions } from "../hooks/use-drag-resize" import { useDragResize } from "../hooks/use-drag-resize" import { ResizeHandle } from "./resize-handle" import { cn } from "@/lib/utils" -import { NodeSelection } from "@tiptap/pm/state" import { Controlled as ControlledZoom } from "react-medium-image-zoom" import { ActionButton, ActionWrapper, ImageActions } from "./image-actions" import { useImageActions } from "../hooks/use-image-actions" -import { blobUrlToBase64 } from "@shared/editor/lib/utils" import { InfoCircledIcon, TrashIcon } from "@radix-ui/react-icons" import { ImageOverlay } from "./image-overlay" +import { blobUrlToBase64 } from "@shared/editor/lib/utils" import { Spinner } from "@shared/components/spinner" const MAX_HEIGHT = 600 @@ -29,7 +28,6 @@ interface ImageState { export const ImageViewBlock: React.FC = ({ editor, node, - getPos, selected, updateAttributes, }) => { @@ -52,23 +50,23 @@ export const ImageViewBlock: React.FC = ({ "left" | "right" | null >(null) - const focus = React.useCallback(() => { - const { view } = editor - const $pos = view.state.doc.resolve(getPos()) - view.dispatch(view.state.tr.setSelection(new NodeSelection($pos))) - }, [editor, getPos]) - const onDimensionsChange = React.useCallback( ({ width, height }: ElementDimensions) => { - focus() updateAttributes({ width, height }) }, - [focus, updateAttributes], + [updateAttributes], ) const aspectRatio = imageState.naturalSize.width / imageState.naturalSize.height const maxWidth = MAX_HEIGHT * aspectRatio + const containerMaxWidth = containerRef.current + ? parseFloat( + getComputedStyle(containerRef.current).getPropertyValue( + "--editor-width", + ), + ) + : Infinity const { isLink, onView, onDownload, onCopy, onCopyLink, onRemoveImg } = useImageActions({ @@ -94,7 +92,7 @@ export const ImageViewBlock: React.FC = ({ onDimensionsChange, minWidth: MIN_WIDTH, minHeight: MIN_HEIGHT, - maxWidth, + maxWidth: containerMaxWidth > 0 ? containerMaxWidth : maxWidth, }) const shouldMerge = React.useMemo(() => currentWidth <= 180, [currentWidth]) @@ -182,6 +180,8 @@ export const ImageViewBlock: React.FC = ({ })) } } + + URL.revokeObjectURL(initialSrc) } } diff --git a/web/shared/editor/hooks/use-container-size.ts b/web/shared/editor/hooks/use-container-size.ts new file mode 100644 index 00000000..574a84b8 --- /dev/null +++ b/web/shared/editor/hooks/use-container-size.ts @@ -0,0 +1,55 @@ +import { useState, useEffect, useCallback } from "react" + +const DEFAULT_RECT: DOMRect = { + top: 0, + left: 0, + bottom: 0, + right: 0, + x: 0, + y: 0, + width: 0, + height: 0, + toJSON: () => "{}", +} + +export function useContainerSize(element: HTMLElement | null): DOMRect { + const [size, setSize] = useState( + () => element?.getBoundingClientRect() ?? DEFAULT_RECT, + ) + + const handleResize = useCallback(() => { + if (!element) return + + const newRect = element.getBoundingClientRect() + + setSize((prevRect) => { + if ( + Math.round(prevRect.width) === Math.round(newRect.width) && + Math.round(prevRect.height) === Math.round(newRect.height) && + Math.round(prevRect.x) === Math.round(newRect.x) && + Math.round(prevRect.y) === Math.round(newRect.y) + ) { + return prevRect + } + return newRect + }) + }, [element]) + + useEffect(() => { + if (!element) return + + const resizeObserver = new ResizeObserver(handleResize) + resizeObserver.observe(element) + + window.addEventListener("click", handleResize) + window.addEventListener("resize", handleResize) + + return () => { + resizeObserver.disconnect() + window.removeEventListener("click", handleResize) + window.removeEventListener("resize", handleResize) + } + }, [element, handleResize]) + + return size +}