@atlaskit/editor-plugin-media-editing
Version:
MediaEditing plugin for @atlaskit/editor-core
321 lines (301 loc) • 13.9 kB
JavaScript
import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
import _regeneratorRuntime from "@babel/runtime/regenerator";
import { useEffect, useRef, useState } from 'react';
import { bind } from 'bind-event-listener';
import { useIntl } from 'react-intl';
import { useImageFlip, useImageRotate, useImageAspectRatio } from './imageEditActions';
export var useImageEditor = function useImageEditor() {
var cropperRef = useRef(null);
var doneButtonRef = useRef(null);
var aspectRatioResolverRef = useRef(null);
var isInitialSetupRef = useRef(true);
var _useState = useState(false),
_useState2 = _slicedToArray(_useState, 2),
isImageReady = _useState2[0],
setIsImageReady = _useState2[1];
var _useState3 = useState(undefined),
_useState4 = _slicedToArray(_useState3, 2),
currentAspectRatio = _useState4[0],
setCurrentAspectRatio = _useState4[1];
var _useState5 = useState('custom'),
_useState6 = _slicedToArray(_useState5, 2),
aspectRatioSelection = _useState6[0],
setAspectRatioSelection = _useState6[1];
var _useImageFlip = useImageFlip(cropperRef),
flipHorizontal = _useImageFlip.flipHorizontal,
flipVertical = _useImageFlip.flipVertical;
var _useImageRotate = useImageRotate(cropperRef),
rotateRight = _useImageRotate.rotateRight;
var _useImageAspectRatio = useImageAspectRatio(),
getAspectRatioValue = _useImageAspectRatio.getAspectRatioValue;
var intl = useIntl();
// Initialize editor state and track canvas size changes
useEffect(function () {
var _cropperRef$current;
if (!isImageReady) {
return;
}
// Get the canvas element to observe for size changes
var 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
var lastWidth = canvas.clientWidth;
var lastHeight = canvas.clientHeight;
// Canvas size will change when the viewport size changes
// Monitor canvas resizing to detect when user manually adjusts the crop area
var observer = new ResizeObserver(function (entries) {
// Skip the first observation during setup
if (isInitialSetupRef.current) {
isInitialSetupRef.current = false;
return;
}
var entry = entries[0];
var _entry$contentRect = entry.contentRect,
width = _entry$contentRect.width,
height = _entry$contentRect.height;
// 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 function () {
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(function () {
if (isImageReady && aspectRatioSelection !== '') {
var _cropperRef$current2;
var 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
var handleSelectionChange = function handleSelectionChange() {
var 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(function () {
var 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
var calculateOriginalRatio = function calculateOriginalRatio() {
var _cropperRef$current3;
var image = (_cropperRef$current3 = cropperRef.current) === null || _cropperRef$current3 === void 0 ? void 0 : _cropperRef$current3.getImage();
if (image) {
var img = image.getBoundingClientRect();
var 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
var waitForCurrentAspectRatio = function waitForCurrentAspectRatio(target) {
return new Promise(function (resolve) {
// If already at target, resolve on next animation frame
if (currentAspectRatio === target) {
requestAnimationFrame(function () {
return resolve();
});
return;
}
// Otherwise, store resolve callback to be called when target is reached
aspectRatioResolverRef.current = {
resolve: resolve,
target: target
};
});
};
// Change the aspect ratio selection and animate the transition
// This function handles smooth transitions between different aspect ratios
var setSelectionArea = /*#__PURE__*/function () {
var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(selectedRatio) {
var _cropperRef$current4, _cropperRef$current5, _cropperRef$current6;
var selection, canvas, shade, ratioVal;
return _regeneratorRuntime.wrap(function _callee$(_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
if (!(selectedRatio === 'custom')) {
_context.next = 6;
break;
}
setAspectRatioSelection(selectedRatio);
setCurrentAspectRatio(undefined);
_context.next = 5;
return waitForCurrentAspectRatio(undefined);
case 5:
return _context.abrupt("return");
case 6:
selection = (_cropperRef$current4 = cropperRef.current) === null || _cropperRef$current4 === void 0 ? void 0 : _cropperRef$current4.getSelection();
canvas = (_cropperRef$current5 = cropperRef.current) === null || _cropperRef$current5 === void 0 ? void 0 : _cropperRef$current5.getCanvas();
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);
_context.next = 14;
return waitForCurrentAspectRatio(undefined);
case 14:
(_cropperRef$current6 = cropperRef.current) === null || _cropperRef$current6 === void 0 || _cropperRef$current6.fitStencilToImage();
// Wait for DOM to update
_context.next = 17;
return new Promise(function (resolve) {
return requestAnimationFrame(resolve);
});
case 17:
if (!(selectedRatio === 'original')) {
_context.next = 21;
break;
}
calculateOriginalRatio();
_context.next = 25;
break;
case 21:
ratioVal = getAspectRatioValue(selectedRatio);
setCurrentAspectRatio(ratioVal);
_context.next = 25;
return waitForCurrentAspectRatio(ratioVal);
case 25:
setAspectRatioSelection(selectedRatio);
// Allow time for the new crop area to be positioned and rendered
_context.next = 28;
return new Promise(function (resolve) {
return setTimeout(resolve, 300);
});
case 28:
// Center the selection and fade back in
if (selection) {
selection.$center();
selection.style.opacity = '1';
}
if (shade) {
shade.style.opacity = '1';
}
case 30:
case "end":
return _context.stop();
}
}, _callee);
}));
return function setSelectionArea(_x) {
return _ref.apply(this, arguments);
};
}();
// Extract the cropped image and save it
// Converts the cropped canvas to a blob and calls the onSave callback
var handleSave = /*#__PURE__*/function () {
var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2(onSave, onClose, errorReporter) {
var _cropperRef$current7, _cropperRef$current8, _cropperRef$current9, selection, image, canvasWidth, _image$shadowRoot, actualImg, naturalWidth, displayedRect, displayedWidth, scaleX, selectionWidth, cropWidthInOriginal, canvas, outWidth, outHeight;
return _regeneratorRuntime.wrap(function _callee2$(_context2) {
while (1) switch (_context2.prev = _context2.next) {
case 0:
_context2.prev = 0;
// Get the selection to determine the crop size relative to original image
selection = (_cropperRef$current7 = cropperRef.current) === null || _cropperRef$current7 === void 0 ? void 0 : _cropperRef$current7.getSelection();
image = (_cropperRef$current8 = cropperRef.current) === null || _cropperRef$current8 === void 0 ? void 0 : _cropperRef$current8.getImage();
if (selection && image) {
// Try to get the actual <img> from shadow DOM
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
naturalWidth = actualImg.naturalWidth; // Get the displayed image dimensions
displayedRect = image.getBoundingClientRect();
displayedWidth = displayedRect.width; // Calculate the scale factor between displayed and original image
scaleX = naturalWidth / displayedWidth; // Get selection dimensions in displayed coordinates
selectionWidth = selection.width || 0; // Calculate the crop width in original image coordinates
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)
_context2.next = 6;
return (_cropperRef$current9 = cropperRef.current) === null || _cropperRef$current9 === void 0 ? void 0 : _cropperRef$current9.getCroppedCanvas(canvasWidth ? {
width: canvasWidth
} : {
width: 1500
});
case 6:
canvas = _context2.sent;
if (canvas) {
outWidth = canvas.width;
outHeight = canvas.height; // Convert canvas to blob (defaults to png)
canvas.toBlob(function (blob) {
if (blob) {
onSave === null || onSave === void 0 || onSave(blob, outWidth, outHeight);
// Don't close here - let the upload completion handle closing
}
});
}
_context2.next = 13;
break;
case 10:
_context2.prev = 10;
_context2.t0 = _context2["catch"](0);
// Report any errors to the error reporter
if (errorReporter) {
errorReporter.captureException(_context2.t0 instanceof Error ? _context2.t0 : new Error(String(_context2.t0)));
}
case 13:
case "end":
return _context2.stop();
}
}, _callee2, null, [[0, 10]]);
}));
return function handleSave(_x2, _x3, _x4) {
return _ref2.apply(this, arguments);
};
}();
return {
cropperRef: cropperRef,
doneButtonRef: doneButtonRef,
isImageReady: isImageReady,
setIsImageReady: setIsImageReady,
currentAspectRatio: currentAspectRatio,
aspectRatioSelection: aspectRatioSelection,
flipHorizontal: flipHorizontal,
flipVertical: flipVertical,
rotateRight: rotateRight,
setSelectionArea: setSelectionArea,
handleSave: handleSave,
formatMessage: intl.formatMessage
};
};