@atlaskit/editor-plugin-media-editing
Version:
MediaEditing plugin for @atlaskit/editor-core
328 lines (307 loc) • 14.4 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.useImageEditor = void 0;
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
var _react = require("react");
var _bindEventListener = require("bind-event-listener");
var _reactIntl = require("react-intl");
var _imageEditActions = require("./imageEditActions");
var useImageEditor = exports.useImageEditor = function useImageEditor() {
var cropperRef = (0, _react.useRef)(null);
var doneButtonRef = (0, _react.useRef)(null);
var aspectRatioResolverRef = (0, _react.useRef)(null);
var isInitialSetupRef = (0, _react.useRef)(true);
var _useState = (0, _react.useState)(false),
_useState2 = (0, _slicedToArray2.default)(_useState, 2),
isImageReady = _useState2[0],
setIsImageReady = _useState2[1];
var _useState3 = (0, _react.useState)(undefined),
_useState4 = (0, _slicedToArray2.default)(_useState3, 2),
currentAspectRatio = _useState4[0],
setCurrentAspectRatio = _useState4[1];
var _useState5 = (0, _react.useState)('custom'),
_useState6 = (0, _slicedToArray2.default)(_useState5, 2),
aspectRatioSelection = _useState6[0],
setAspectRatioSelection = _useState6[1];
var _useImageFlip = (0, _imageEditActions.useImageFlip)(cropperRef),
flipHorizontal = _useImageFlip.flipHorizontal,
flipVertical = _useImageFlip.flipVertical;
var _useImageRotate = (0, _imageEditActions.useImageRotate)(cropperRef),
rotateRight = _useImageRotate.rotateRight;
var _useImageAspectRatio = (0, _imageEditActions.useImageAspectRatio)(),
getAspectRatioValue = _useImageAspectRatio.getAspectRatioValue;
var intl = (0, _reactIntl.useIntl)();
// Initialize editor state and track canvas size changes
(0, _react.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
(0, _react.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 (0, _bindEventListener.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
(0, _react.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 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(selectedRatio) {
var _cropperRef$current4, _cropperRef$current5, _cropperRef$current6;
var selection, canvas, shade, ratioVal;
return _regenerator.default.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 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.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 _regenerator.default.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
};
};