mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
chore: Enhancement + New Feature (#185)
* wip * wip page * chore: style * wip pages * wip pages * chore: toggle * chore: link * feat: topic search * chore: page section * refactor: apply tailwind class ordering * fix: handle loggedIn user for guest route * feat: folder & image schema * chore: move utils to shared * refactor: tailwind class ordering * feat: img ext for editor * refactor: remove qa * fix: tanstack start * fix: wrong import * chore: use toast * chore: schema
This commit is contained in:
171
web/shared/editor/extensions/image/hooks/use-drag-resize.ts
Normal file
171
web/shared/editor/extensions/image/hooks/use-drag-resize.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { useState, useCallback, useEffect } from "react"
|
||||
|
||||
type ResizeDirection = "left" | "right"
|
||||
export type ElementDimensions = { width: number; height: number }
|
||||
|
||||
type HookParams = {
|
||||
initialWidth?: number
|
||||
initialHeight?: number
|
||||
contentWidth?: number
|
||||
contentHeight?: number
|
||||
gridInterval: number
|
||||
minWidth: number
|
||||
minHeight: number
|
||||
maxWidth: number
|
||||
onDimensionsChange?: (dimensions: ElementDimensions) => void
|
||||
}
|
||||
|
||||
export function useDragResize({
|
||||
initialWidth,
|
||||
initialHeight,
|
||||
contentWidth,
|
||||
contentHeight,
|
||||
gridInterval,
|
||||
minWidth,
|
||||
minHeight,
|
||||
maxWidth,
|
||||
onDimensionsChange,
|
||||
}: HookParams) {
|
||||
const [dimensions, updateDimensions] = useState<ElementDimensions>({
|
||||
width: Math.max(initialWidth ?? minWidth, minWidth),
|
||||
height: Math.max(initialHeight ?? minHeight, minHeight),
|
||||
})
|
||||
const [boundaryWidth, setBoundaryWidth] = useState(Infinity)
|
||||
const [resizeOrigin, setResizeOrigin] = useState(0)
|
||||
const [initialDimensions, setInitialDimensions] = useState(dimensions)
|
||||
const [resizeDirection, setResizeDirection] = useState<
|
||||
ResizeDirection | undefined
|
||||
>()
|
||||
|
||||
const widthConstraint = useCallback(
|
||||
(proposedWidth: number, maxAllowedWidth: number) => {
|
||||
const effectiveMinWidth = Math.max(
|
||||
minWidth,
|
||||
Math.min(
|
||||
contentWidth ?? minWidth,
|
||||
(gridInterval / 100) * maxAllowedWidth,
|
||||
),
|
||||
)
|
||||
return Math.min(
|
||||
maxAllowedWidth,
|
||||
Math.max(proposedWidth, effectiveMinWidth),
|
||||
)
|
||||
},
|
||||
[gridInterval, contentWidth, minWidth],
|
||||
)
|
||||
|
||||
const handlePointerMove = useCallback(
|
||||
(event: PointerEvent) => {
|
||||
event.preventDefault()
|
||||
const movementDelta =
|
||||
(resizeDirection === "left"
|
||||
? resizeOrigin - event.pageX
|
||||
: event.pageX - resizeOrigin) * 2
|
||||
const gridUnitWidth = (gridInterval / 100) * boundaryWidth
|
||||
const proposedWidth = initialDimensions.width + movementDelta
|
||||
const alignedWidth =
|
||||
Math.round(proposedWidth / gridUnitWidth) * gridUnitWidth
|
||||
const finalWidth = widthConstraint(alignedWidth, boundaryWidth)
|
||||
const aspectRatio =
|
||||
contentHeight && contentWidth ? contentHeight / contentWidth : 1
|
||||
|
||||
updateDimensions({
|
||||
width: Math.max(finalWidth, minWidth),
|
||||
height: Math.max(
|
||||
contentWidth
|
||||
? finalWidth * aspectRatio
|
||||
: (contentHeight ?? minHeight),
|
||||
minHeight,
|
||||
),
|
||||
})
|
||||
},
|
||||
[
|
||||
widthConstraint,
|
||||
resizeDirection,
|
||||
boundaryWidth,
|
||||
resizeOrigin,
|
||||
gridInterval,
|
||||
contentHeight,
|
||||
contentWidth,
|
||||
initialDimensions.width,
|
||||
minWidth,
|
||||
minHeight,
|
||||
],
|
||||
)
|
||||
|
||||
const handlePointerUp = useCallback(
|
||||
(event: PointerEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
setResizeOrigin(0)
|
||||
setResizeDirection(undefined)
|
||||
onDimensionsChange?.(dimensions)
|
||||
},
|
||||
[onDimensionsChange, dimensions],
|
||||
)
|
||||
|
||||
const handleKeydown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
updateDimensions({
|
||||
width: Math.max(initialDimensions.width, minWidth),
|
||||
height: Math.max(initialDimensions.height, minHeight),
|
||||
})
|
||||
setResizeDirection(undefined)
|
||||
}
|
||||
},
|
||||
[initialDimensions, minWidth, minHeight],
|
||||
)
|
||||
|
||||
const initiateResize = useCallback(
|
||||
(direction: ResizeDirection) =>
|
||||
(event: React.PointerEvent<HTMLDivElement>) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
setBoundaryWidth(maxWidth)
|
||||
setInitialDimensions({
|
||||
width: Math.max(
|
||||
widthConstraint(dimensions.width, maxWidth),
|
||||
minWidth,
|
||||
),
|
||||
height: Math.max(dimensions.height, minHeight),
|
||||
})
|
||||
setResizeOrigin(event.pageX)
|
||||
setResizeDirection(direction)
|
||||
},
|
||||
[
|
||||
maxWidth,
|
||||
widthConstraint,
|
||||
dimensions.width,
|
||||
dimensions.height,
|
||||
minWidth,
|
||||
minHeight,
|
||||
],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (resizeDirection) {
|
||||
document.addEventListener("keydown", handleKeydown)
|
||||
document.addEventListener("pointermove", handlePointerMove)
|
||||
document.addEventListener("pointerup", handlePointerUp)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeydown)
|
||||
document.removeEventListener("pointermove", handlePointerMove)
|
||||
document.removeEventListener("pointerup", handlePointerUp)
|
||||
}
|
||||
}
|
||||
}, [resizeDirection, handleKeydown, handlePointerMove, handlePointerUp])
|
||||
|
||||
return {
|
||||
initiateResize,
|
||||
isResizing: !!resizeDirection,
|
||||
updateDimensions,
|
||||
currentWidth: Math.max(dimensions.width, minWidth),
|
||||
currentHeight: Math.max(dimensions.height, minHeight),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import * as React from "react"
|
||||
import type { Editor } from "@tiptap/core"
|
||||
import type { Node } from "@tiptap/pm/model"
|
||||
import { isUrl } from "@shared/editor/lib/utils"
|
||||
|
||||
interface UseImageActionsProps {
|
||||
editor: Editor
|
||||
node: Node
|
||||
src: string
|
||||
onViewClick: (value: boolean) => void
|
||||
}
|
||||
|
||||
export type ImageActionHandlers = {
|
||||
onView?: () => void
|
||||
onDownload?: () => void
|
||||
onCopy?: () => void
|
||||
onCopyLink?: () => void
|
||||
onRemoveImg?: () => void
|
||||
}
|
||||
|
||||
export const useImageActions = ({
|
||||
editor,
|
||||
node,
|
||||
src,
|
||||
onViewClick,
|
||||
}: UseImageActionsProps) => {
|
||||
const isLink = React.useMemo(() => isUrl(src), [src])
|
||||
|
||||
const onView = React.useCallback(() => {
|
||||
onViewClick(true)
|
||||
}, [onViewClick])
|
||||
|
||||
const onDownload = React.useCallback(() => {
|
||||
editor.commands.downloadImage({ src: node.attrs.src, alt: node.attrs.alt })
|
||||
}, [editor.commands, node.attrs.alt, node.attrs.src])
|
||||
|
||||
const onCopy = React.useCallback(() => {
|
||||
editor.commands.copyImage({ src: node.attrs.src })
|
||||
}, [editor.commands, node.attrs.src])
|
||||
|
||||
const onCopyLink = React.useCallback(() => {
|
||||
editor.commands.copyLink({ src: node.attrs.src })
|
||||
}, [editor.commands, node.attrs.src])
|
||||
|
||||
const onRemoveImg = React.useCallback(() => {
|
||||
editor.commands.command(({ tr, dispatch }) => {
|
||||
const { selection } = tr
|
||||
const nodeAtSelection = tr.doc.nodeAt(selection.from)
|
||||
|
||||
if (nodeAtSelection && nodeAtSelection.type.name === "image") {
|
||||
if (dispatch) {
|
||||
tr.deleteSelection()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}, [editor.commands])
|
||||
|
||||
return { isLink, onView, onDownload, onCopy, onCopyLink, onRemoveImg }
|
||||
}
|
||||
Reference in New Issue
Block a user