mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
chore: add measured container
This commit is contained in:
39
web/shared/editor/components/measured-container.tsx
Normal file
39
web/shared/editor/components/measured-container.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { useContainerSize } from "../hooks/use-container-size"
|
||||||
|
|
||||||
|
interface MeasuredContainerProps<T extends React.ElementType> {
|
||||||
|
as: T
|
||||||
|
name: string
|
||||||
|
children?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MeasuredContainer = React.forwardRef(
|
||||||
|
<T extends React.ElementType>(
|
||||||
|
{
|
||||||
|
as: Component,
|
||||||
|
name,
|
||||||
|
children,
|
||||||
|
style = {},
|
||||||
|
...props
|
||||||
|
}: MeasuredContainerProps<T> & React.ComponentProps<T>,
|
||||||
|
ref: React.Ref<HTMLElement>,
|
||||||
|
) => {
|
||||||
|
const innerRef = React.useRef<HTMLElement>(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 (
|
||||||
|
<Component {...props} ref={innerRef} style={{ ...customStyle, ...style }}>
|
||||||
|
{children}
|
||||||
|
</Component>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
MeasuredContainer.displayName = "MeasuredContainer"
|
||||||
@@ -4,6 +4,7 @@ import { Content } from "@tiptap/core"
|
|||||||
import { BubbleMenu } from "./components/bubble-menu"
|
import { BubbleMenu } from "./components/bubble-menu"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useLaEditor, UseLaEditorProps } from "./hooks/use-la-editor"
|
import { useLaEditor, UseLaEditorProps } from "./hooks/use-la-editor"
|
||||||
|
import { MeasuredContainer } from "./components/measured-container"
|
||||||
|
|
||||||
export interface LaEditorProps extends UseLaEditorProps {
|
export interface LaEditorProps extends UseLaEditorProps {
|
||||||
value?: Content
|
value?: Content
|
||||||
@@ -21,7 +22,9 @@ export const LaEditor = React.memo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<MeasuredContainer
|
||||||
|
as="div"
|
||||||
|
name="editor"
|
||||||
className={cn("relative flex h-full w-full grow flex-col", className)}
|
className={cn("relative flex h-full w-full grow flex-col", className)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
@@ -30,7 +33,7 @@ export const LaEditor = React.memo(
|
|||||||
className={cn("la-editor", editorContentClassName)}
|
className={cn("la-editor", editorContentClassName)}
|
||||||
/>
|
/>
|
||||||
<BubbleMenu editor={editor} />
|
<BubbleMenu editor={editor} />
|
||||||
</div>
|
</MeasuredContainer>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ import type { ElementDimensions } from "../hooks/use-drag-resize"
|
|||||||
import { useDragResize } from "../hooks/use-drag-resize"
|
import { useDragResize } from "../hooks/use-drag-resize"
|
||||||
import { ResizeHandle } from "./resize-handle"
|
import { ResizeHandle } from "./resize-handle"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { NodeSelection } from "@tiptap/pm/state"
|
|
||||||
import { Controlled as ControlledZoom } from "react-medium-image-zoom"
|
import { Controlled as ControlledZoom } from "react-medium-image-zoom"
|
||||||
import { ActionButton, ActionWrapper, ImageActions } from "./image-actions"
|
import { ActionButton, ActionWrapper, ImageActions } from "./image-actions"
|
||||||
import { useImageActions } from "../hooks/use-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 { InfoCircledIcon, TrashIcon } from "@radix-ui/react-icons"
|
||||||
import { ImageOverlay } from "./image-overlay"
|
import { ImageOverlay } from "./image-overlay"
|
||||||
|
import { blobUrlToBase64 } from "@shared/editor/lib/utils"
|
||||||
import { Spinner } from "@shared/components/spinner"
|
import { Spinner } from "@shared/components/spinner"
|
||||||
|
|
||||||
const MAX_HEIGHT = 600
|
const MAX_HEIGHT = 600
|
||||||
@@ -29,7 +28,6 @@ interface ImageState {
|
|||||||
export const ImageViewBlock: React.FC<NodeViewProps> = ({
|
export const ImageViewBlock: React.FC<NodeViewProps> = ({
|
||||||
editor,
|
editor,
|
||||||
node,
|
node,
|
||||||
getPos,
|
|
||||||
selected,
|
selected,
|
||||||
updateAttributes,
|
updateAttributes,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -52,23 +50,23 @@ export const ImageViewBlock: React.FC<NodeViewProps> = ({
|
|||||||
"left" | "right" | null
|
"left" | "right" | null
|
||||||
>(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(
|
const onDimensionsChange = React.useCallback(
|
||||||
({ width, height }: ElementDimensions) => {
|
({ width, height }: ElementDimensions) => {
|
||||||
focus()
|
|
||||||
updateAttributes({ width, height })
|
updateAttributes({ width, height })
|
||||||
},
|
},
|
||||||
[focus, updateAttributes],
|
[updateAttributes],
|
||||||
)
|
)
|
||||||
|
|
||||||
const aspectRatio =
|
const aspectRatio =
|
||||||
imageState.naturalSize.width / imageState.naturalSize.height
|
imageState.naturalSize.width / imageState.naturalSize.height
|
||||||
const maxWidth = MAX_HEIGHT * aspectRatio
|
const maxWidth = MAX_HEIGHT * aspectRatio
|
||||||
|
const containerMaxWidth = containerRef.current
|
||||||
|
? parseFloat(
|
||||||
|
getComputedStyle(containerRef.current).getPropertyValue(
|
||||||
|
"--editor-width",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Infinity
|
||||||
|
|
||||||
const { isLink, onView, onDownload, onCopy, onCopyLink, onRemoveImg } =
|
const { isLink, onView, onDownload, onCopy, onCopyLink, onRemoveImg } =
|
||||||
useImageActions({
|
useImageActions({
|
||||||
@@ -94,7 +92,7 @@ export const ImageViewBlock: React.FC<NodeViewProps> = ({
|
|||||||
onDimensionsChange,
|
onDimensionsChange,
|
||||||
minWidth: MIN_WIDTH,
|
minWidth: MIN_WIDTH,
|
||||||
minHeight: MIN_HEIGHT,
|
minHeight: MIN_HEIGHT,
|
||||||
maxWidth,
|
maxWidth: containerMaxWidth > 0 ? containerMaxWidth : maxWidth,
|
||||||
})
|
})
|
||||||
|
|
||||||
const shouldMerge = React.useMemo(() => currentWidth <= 180, [currentWidth])
|
const shouldMerge = React.useMemo(() => currentWidth <= 180, [currentWidth])
|
||||||
@@ -182,6 +180,8 @@ export const ImageViewBlock: React.FC<NodeViewProps> = ({
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
URL.revokeObjectURL(initialSrc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
55
web/shared/editor/hooks/use-container-size.ts
Normal file
55
web/shared/editor/hooks/use-container-size.ts
Normal file
@@ -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<DOMRect>(
|
||||||
|
() => 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user