UNPKG

@atlaskit/editor-plugin-media-editing

Version:

MediaEditing plugin for @atlaskit/editor-core

272 lines (244 loc) 10.6 kB
import { useEffect, useRef, useState } from 'react'; import { bind } from 'bind-event-listener'; import { useIntl } from 'react-intl'; import { useImageFlip, useImageRotate, useImageAspectRatio } from './imageEditActions'; export const useImageEditor = () => { const cropperRef = useRef(null); const doneButtonRef = useRef(null); const aspectRatioResolverRef = useRef(null); const isInitialSetupRef = useRef(true); const [isImageReady, setIsImageReady] = useState(false); const [currentAspectRatio, setCurrentAspectRatio] = useState(undefined); const [aspectRatioSelection, setAspectRatioSelection] = useState('custom'); const { flipHorizontal, flipVertical } = useImageFlip(cropperRef); const { rotateRight } = useImageRotate(cropperRef); const { getAspectRatioValue } = useImageAspectRatio(); const intl = useIntl(); // Initialize editor state and track canvas size changes useEffect(() => { var _cropperRef$current; if (!isImageReady) { return; } // Get the canvas element to observe for size changes const canvas = (_cropperRef$current = cropperRef.current) === null || _cropperRef$current === void 0 ? void 0 : _cropperRef$current.getCanvas(); if (!canvas) { return; } // Track initial canvas dimensions before any state updates let lastWidth = canvas.clientWidth; let lastHeight = canvas.clientHeight; // Canvas size will change when the viewport size changes // Monitor canvas resizing to detect when user manually adjusts the crop area const observer = new ResizeObserver(entries => { // Skip the first observation during setup if (isInitialSetupRef.current) { isInitialSetupRef.current = false; return; } const entry = entries[0]; const { width, height } = entry.contentRect; // If canvas size changes, switch to custom aspect ratio mode if (width !== lastWidth || height !== lastHeight) { lastWidth = width; lastHeight = height; setAspectRatioSelection('custom'); } }); observer.observe(canvas); // Calculate and set the original aspect ratio based on the image dimensions setAspectRatioSelection('custom'); // Focus on the done button as soon as image loads if (doneButtonRef.current) { doneButtonRef.current.focus(); } return () => observer.disconnect(); }, [isImageReady]); // Listen for aspect ratio selection changes and update current aspect ratio // This effect monitors the cropper's selection element for any changes // and updates the currentAspectRatio state accordingly useEffect(() => { if (isImageReady && aspectRatioSelection !== '') { var _cropperRef$current2; const selection = (_cropperRef$current2 = cropperRef.current) === null || _cropperRef$current2 === void 0 ? void 0 : _cropperRef$current2.getSelection(); if (!selection) { return; } // Update aspect ratio whenever aspect ratio selection changes const handleSelectionChange = () => { const selectionAspectRatio = selection.aspectRatio ? selection.aspectRatio : undefined; setCurrentAspectRatio(selectionAspectRatio); }; // Call once to set initial value // eslint-disable-next-line @atlassian/perf-linting/no-chain-state-updates -- Ignored via go/ees017 (to be fixed) handleSelectionChange(); // Attach change listener to selection element return bind(selection, { type: 'change', listener: handleSelectionChange }); } }, [isImageReady, aspectRatioSelection]); // Resolve pending aspect ratio changes // This effect watches for when the aspect ratio reaches its target value // and resolves the corresponding promise to allow animations to proceed useEffect(() => { const pending = aspectRatioResolverRef.current; if (pending && currentAspectRatio === pending.target) { // Resolve the promise stored in the ref when target is reached pending.resolve(); aspectRatioResolverRef.current = null; } }, [currentAspectRatio]); // Calculate the original aspect ratio of the image // Gets the image element's dimensions and computes width/height ratio const calculateOriginalRatio = () => { var _cropperRef$current3; const image = (_cropperRef$current3 = cropperRef.current) === null || _cropperRef$current3 === void 0 ? void 0 : _cropperRef$current3.getImage(); if (image) { const img = image.getBoundingClientRect(); const ratio = img.width / img.height; setCurrentAspectRatio(ratio); } }; // Wait for the aspect ratio to reach a target value // Used to coordinate aspect ratio changes with animation frames // Returns a promise that resolves when the target ratio is reached const waitForCurrentAspectRatio = target => new Promise(resolve => { // If already at target, resolve on next animation frame if (currentAspectRatio === target) { requestAnimationFrame(() => resolve()); return; } // Otherwise, store resolve callback to be called when target is reached aspectRatioResolverRef.current = { resolve, target }; }); // Change the aspect ratio selection and animate the transition // This function handles smooth transitions between different aspect ratios const setSelectionArea = async selectedRatio => { var _cropperRef$current4, _cropperRef$current5, _cropperRef$current6; // Custom ratio mode: free-form selection without constraints if (selectedRatio === 'custom') { setAspectRatioSelection(selectedRatio); setCurrentAspectRatio(undefined); await waitForCurrentAspectRatio(undefined); return; } const selection = (_cropperRef$current4 = cropperRef.current) === null || _cropperRef$current4 === void 0 ? void 0 : _cropperRef$current4.getSelection(); const canvas = (_cropperRef$current5 = cropperRef.current) === null || _cropperRef$current5 === void 0 ? void 0 : _cropperRef$current5.getCanvas(); const shade = canvas === null || canvas === void 0 ? void 0 : canvas.querySelector('cropper-shade'); // Fade out selection and shade during transition for smooth animation if (selection) { selection.style.opacity = '0'; } if (shade) { shade.style.opacity = '0'; } // Reset aspect ratio and wait for the crop area to clear setCurrentAspectRatio(undefined); await waitForCurrentAspectRatio(undefined); (_cropperRef$current6 = cropperRef.current) === null || _cropperRef$current6 === void 0 ? void 0 : _cropperRef$current6.fitStencilToImage(); // Wait for DOM to update await new Promise(resolve => requestAnimationFrame(resolve)); // Set the new aspect ratio if (selectedRatio === 'original') { calculateOriginalRatio(); } else { const ratioVal = getAspectRatioValue(selectedRatio); setCurrentAspectRatio(ratioVal); await waitForCurrentAspectRatio(ratioVal); } setAspectRatioSelection(selectedRatio); // Allow time for the new crop area to be positioned and rendered await new Promise(resolve => setTimeout(resolve, 300)); // Center the selection and fade back in if (selection) { selection.$center(); selection.style.opacity = '1'; } if (shade) { shade.style.opacity = '1'; } }; // Extract the cropped image and save it // Converts the cropped canvas to a blob and calls the onSave callback const handleSave = async (onSave, onClose, errorReporter) => { try { var _cropperRef$current7, _cropperRef$current8, _cropperRef$current9; // Get the selection to determine the crop size relative to original image const selection = (_cropperRef$current7 = cropperRef.current) === null || _cropperRef$current7 === void 0 ? void 0 : _cropperRef$current7.getSelection(); const image = (_cropperRef$current8 = cropperRef.current) === null || _cropperRef$current8 === void 0 ? void 0 : _cropperRef$current8.getImage(); let canvasWidth; if (selection && image) { var _image$shadowRoot; // Try to get the actual <img> from shadow DOM const actualImg = ((_image$shadowRoot = image.shadowRoot) === null || _image$shadowRoot === void 0 ? void 0 : _image$shadowRoot.querySelector('img')) || null; if (actualImg) { // Get the natural (original) image dimensions const naturalWidth = actualImg.naturalWidth; // Get the displayed image dimensions const displayedRect = image.getBoundingClientRect(); const displayedWidth = displayedRect.width; // Calculate the scale factor between displayed and original image const scaleX = naturalWidth / displayedWidth; // Get selection dimensions in displayed coordinates const selectionWidth = selection.width || 0; // Calculate the crop width in original image coordinates const cropWidthInOriginal = selectionWidth * scaleX; // Use the crop width from original image, capped at a reasonable maximum canvasWidth = Math.min(cropWidthInOriginal, 1500); } } // Get the cropped canvas with calculated width // Fallback to width = 1500 (a reasonable size for keeping high quality images relatively high quality) const canvas = await ((_cropperRef$current9 = cropperRef.current) === null || _cropperRef$current9 === void 0 ? void 0 : _cropperRef$current9.getCroppedCanvas(canvasWidth ? { width: canvasWidth } : { width: 1500 })); if (canvas) { const outWidth = canvas.width; const outHeight = canvas.height; // Convert canvas to blob (defaults to png) canvas.toBlob(blob => { if (blob) { onSave === null || onSave === void 0 ? void 0 : onSave(blob, outWidth, outHeight); // Don't close here - let the upload completion handle closing } }); } } catch (error) { // Report any errors to the error reporter if (errorReporter) { errorReporter.captureException(error instanceof Error ? error : new Error(String(error))); } } }; return { cropperRef, doneButtonRef, isImageReady, setIsImageReady, currentAspectRatio, aspectRatioSelection, flipHorizontal, flipVertical, rotateRight, setSelectionArea, handleSave, formatMessage: intl.formatMessage }; };