@atlaskit/editor-plugin-media-editing
Version:
MediaEditing plugin for @atlaskit/editor-core
272 lines (244 loc) • 10.6 kB
JavaScript
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
};
};