text-editor-studio-ts
Version:
A powerful mobile-responsive rich text editor built with Lexical and React
1 lines • 15.5 kB
Source Map (JSON)
{"version":3,"file":"image-resizer-B_I9lFMZ.cjs","sources":["../src/components/editor/editor-ui/image-resizer.tsx"],"sourcesContent":["import { useRef, JSX } from \"react\";\n\nimport { calculateZoomLevel } from \"@lexical/utils\";\nimport type { LexicalEditor } from \"lexical\";\n\nimport { Button } from \"../../../../components/Button\";\n\nfunction clamp(value: number, min: number, max: number) {\n return Math.min(Math.max(value, min), max);\n}\n\nconst Direction = {\n east: 1 << 0,\n north: 1 << 3,\n south: 1 << 1,\n west: 1 << 2,\n};\n\nexport function ImageResizer({\n onResizeStart,\n onResizeEnd,\n buttonRef,\n imageRef,\n maxWidth,\n editor,\n showCaption,\n setShowCaption,\n captionsEnabled,\n}: {\n editor: LexicalEditor;\n buttonRef: { current: null | HTMLButtonElement };\n imageRef: { current: null | HTMLElement };\n maxWidth?: number;\n onResizeEnd: (width: \"inherit\" | number, height: \"inherit\" | number) => void;\n onResizeStart: () => void;\n setShowCaption: (show: boolean) => void;\n showCaption: boolean;\n captionsEnabled: boolean;\n}): JSX.Element {\n const controlWrapperRef = useRef<HTMLDivElement>(null);\n const userSelect = useRef({\n priority: \"\",\n value: \"default\",\n });\n const positioningRef = useRef<{\n currentHeight: \"inherit\" | number;\n currentWidth: \"inherit\" | number;\n direction: number;\n isResizing: boolean;\n ratio: number;\n startHeight: number;\n startWidth: number;\n startX: number;\n startY: number;\n }>({\n currentHeight: 0,\n currentWidth: 0,\n direction: 0,\n isResizing: false,\n ratio: 0,\n startHeight: 0,\n startWidth: 0,\n startX: 0,\n startY: 0,\n });\n const editorRootElement = editor.getRootElement();\n // Find max width, accounting for editor padding.\n const maxWidthContainer = maxWidth\n ? maxWidth\n : editorRootElement !== null\n ? editorRootElement.getBoundingClientRect().width - 20\n : 100;\n const maxHeightContainer =\n editorRootElement !== null\n ? editorRootElement.getBoundingClientRect().height - 20\n : 100;\n\n const minWidth = 100;\n const minHeight = 100;\n\n const setStartCursor = (direction: number) => {\n const ew = direction === Direction.east || direction === Direction.west;\n const ns = direction === Direction.north || direction === Direction.south;\n const nwse =\n (direction & Direction.north && direction & Direction.west) ||\n (direction & Direction.south && direction & Direction.east);\n\n const cursorDir = ew ? \"ew\" : ns ? \"ns\" : nwse ? \"nwse\" : \"nesw\";\n\n if (editorRootElement !== null) {\n editorRootElement.style.setProperty(\n \"cursor\",\n `${cursorDir}-resize`,\n \"important\"\n );\n }\n if (document.body !== null) {\n document.body.style.setProperty(\n \"cursor\",\n `${cursorDir}-resize`,\n \"important\"\n );\n userSelect.current.value = document.body.style.getPropertyValue(\n \"-webkit-user-select\"\n );\n userSelect.current.priority = document.body.style.getPropertyPriority(\n \"-webkit-user-select\"\n );\n document.body.style.setProperty(\n \"-webkit-user-select\",\n `none`,\n \"important\"\n );\n }\n };\n\n const setEndCursor = () => {\n if (editorRootElement !== null) {\n editorRootElement.style.setProperty(\"cursor\", \"text\");\n }\n if (document.body !== null) {\n document.body.style.setProperty(\"cursor\", \"default\");\n document.body.style.setProperty(\n \"-webkit-user-select\",\n userSelect.current.value,\n userSelect.current.priority\n );\n }\n };\n\n const handlePointerDown = (\n event: React.PointerEvent<HTMLDivElement>,\n direction: number\n ) => {\n if (!editor.isEditable()) {\n return;\n }\n\n const image = imageRef.current;\n const controlWrapper = controlWrapperRef.current;\n\n if (image !== null && controlWrapper !== null) {\n event.preventDefault();\n const { width, height } = image.getBoundingClientRect();\n const zoom = calculateZoomLevel(image);\n const positioning = positioningRef.current;\n positioning.startWidth = width;\n positioning.startHeight = height;\n positioning.ratio = width / height;\n positioning.currentWidth = width;\n positioning.currentHeight = height;\n positioning.startX = event.clientX / zoom;\n positioning.startY = event.clientY / zoom;\n positioning.isResizing = true;\n positioning.direction = direction;\n\n setStartCursor(direction);\n onResizeStart();\n\n controlWrapper.classList.add(\"touch-action-none\");\n image.style.height = `${height}px`;\n image.style.width = `${width}px`;\n\n document.addEventListener(\"pointermove\", handlePointerMove);\n document.addEventListener(\"pointerup\", handlePointerUp);\n }\n };\n const handlePointerMove = (event: PointerEvent) => {\n const image = imageRef.current;\n const positioning = positioningRef.current;\n\n const isHorizontal =\n positioning.direction & (Direction.east | Direction.west);\n const isVertical =\n positioning.direction & (Direction.south | Direction.north);\n\n if (image !== null && positioning.isResizing) {\n const zoom = calculateZoomLevel(image);\n // Corner cursor\n if (isHorizontal && isVertical) {\n let diff = Math.floor(positioning.startX - event.clientX / zoom);\n diff = positioning.direction & Direction.east ? -diff : diff;\n\n const width = clamp(\n positioning.startWidth + diff,\n minWidth,\n maxWidthContainer\n );\n\n const height = width / positioning.ratio;\n image.style.width = `${width}px`;\n image.style.height = `${height}px`;\n positioning.currentHeight = height;\n positioning.currentWidth = width;\n } else if (isVertical) {\n let diff = Math.floor(positioning.startY - event.clientY / zoom);\n diff = positioning.direction & Direction.south ? -diff : diff;\n\n const height = clamp(\n positioning.startHeight + diff,\n minHeight,\n maxHeightContainer\n );\n\n image.style.height = `${height}px`;\n positioning.currentHeight = height;\n } else {\n let diff = Math.floor(positioning.startX - event.clientX / zoom);\n diff = positioning.direction & Direction.east ? -diff : diff;\n\n const width = clamp(\n positioning.startWidth + diff,\n minWidth,\n maxWidthContainer\n );\n\n image.style.width = `${width}px`;\n positioning.currentWidth = width;\n }\n }\n };\n const handlePointerUp = () => {\n const image = imageRef.current;\n const positioning = positioningRef.current;\n const controlWrapper = controlWrapperRef.current;\n if (image !== null && controlWrapper !== null && positioning.isResizing) {\n const width = positioning.currentWidth;\n const height = positioning.currentHeight;\n positioning.startWidth = 0;\n positioning.startHeight = 0;\n positioning.ratio = 0;\n positioning.startX = 0;\n positioning.startY = 0;\n positioning.currentWidth = 0;\n positioning.currentHeight = 0;\n positioning.isResizing = false;\n\n controlWrapper.classList.remove(\"touch-action-none\");\n\n setEndCursor();\n onResizeEnd(width, height);\n\n document.removeEventListener(\"pointermove\", handlePointerMove);\n document.removeEventListener(\"pointerup\", handlePointerUp);\n }\n };\n return (\n <div ref={controlWrapperRef}>\n {!showCaption && captionsEnabled && (\n <Button\n className=\"image-caption-button absolute bottom-1 left-1/2 -translate-x-1/2\"\n ref={buttonRef}\n variant={\"BorderStyle\"}\n onClick={() => {\n setShowCaption(!showCaption);\n }}\n >\n Add Caption\n </Button>\n )}\n <div\n className=\"image-resizer image-resizer-n absolute -top-2.5 left-1/2 h-3 w-3 -translate-x-1/2 cursor-ns-resize bg-background-presentation-action-primary\"\n onPointerDown={(event) => {\n handlePointerDown(event, Direction.north);\n }}\n />\n <div\n className=\"image-resizer image-resizer-ne absolute -right-2.5 -top-2.5 h-3 w-3 cursor-nesw-resize bg-background-presentation-action-primary\"\n onPointerDown={(event) => {\n handlePointerDown(event, Direction.north | Direction.east);\n }}\n />\n <div\n className=\"image-resizer image-resizer-e absolute -right-2.5 top-1/2 h-3 w-3 -translate-y-1/2 cursor-ew-resize bg-background-presentation-action-primary\"\n onPointerDown={(event) => {\n handlePointerDown(event, Direction.east);\n }}\n />\n <div\n className=\"image-resizer image-resizer-se absolute -bottom-2.5 -right-2.5 h-3 w-3 cursor-nwse-resize bg-background-presentation-action-primary\"\n onPointerDown={(event) => {\n handlePointerDown(event, Direction.south | Direction.east);\n }}\n />\n <div\n className=\"image-resizer image-resizer-s absolute -bottom-2.5 left-1/2 h-3 w-3 -translate-x-1/2 cursor-ns-resize bg-background-presentation-action-primary\"\n onPointerDown={(event) => {\n handlePointerDown(event, Direction.south);\n }}\n />\n <div\n className=\"image-resizer image-resizer-sw absolute -bottom-2.5 -left-2.5 h-3 w-3 cursor-nesw-resize bg-background-presentation-action-primary\"\n onPointerDown={(event) => {\n handlePointerDown(event, Direction.south | Direction.west);\n }}\n />\n <div\n className=\"image-resizer image-resizer-w absolute -left-2.5 top-1/2 h-3 w-3 -translate-y-1/2 cursor-ew-resize bg-background-presentation-action-primary\"\n onPointerDown={(event) => {\n handlePointerDown(event, Direction.west);\n }}\n />\n <div\n className=\"image-resizer image-resizer-nw absolute -left-2.5 -top-2.5 h-3 w-3 cursor-nwse-resize bg-background-presentation-action-primary\"\n onPointerDown={(event) => {\n handlePointerDown(event, Direction.north | Direction.west);\n }}\n />\n </div>\n );\n}\n"],"names":["clamp","value","min","max","Math","Direction","onResizeStart","onResizeEnd","buttonRef","imageRef","maxWidth","editor","showCaption","setShowCaption","captionsEnabled","controlWrapperRef","useRef","userSelect","priority","positioningRef","currentHeight","currentWidth","direction","isResizing","ratio","startHeight","startWidth","startX","startY","editorRootElement","getRootElement","maxWidthContainer","getBoundingClientRect","width","maxHeightContainer","height","handlePointerDown","event","isEditable","image","current","controlWrapper","preventDefault","zoom","calculateZoomLevel","positioning","clientX","clientY","cursorDir","style","setProperty","document","body","getPropertyValue","getPropertyPriority","setStartCursor","classList","add","addEventListener","handlePointerMove","handlePointerUp","isHorizontal","isVertical","diff","floor","remove","removeEventListener","jsxs","ref","children","jsxRuntime","jsx","Button","className","variant","onClick","onPointerDown"],"mappings":"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"}