UNPKG

@atlaskit/editor-plugin-media-editing

Version:

MediaEditing plugin for @atlaskit/editor-core

328 lines (307 loc) 14.4 kB
"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 }; };