UNPKG

@atlaskit/editor-plugin-media-editing

Version:

MediaEditing plugin for @atlaskit/editor-core

530 lines (503 loc) 24.1 kB
/* Cropper.tsx generated by @compiled/babel-plugin v0.39.1 */ "use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _typeof = require("@babel/runtime/helpers/typeof"); Object.defineProperty(exports, "__esModule", { value: true }); exports.Cropper = void 0; var _runtime = require("@compiled/react/runtime"); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); var _react = _interopRequireWildcard(require("react")); var _bindEventListener = require("bind-event-listener"); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } var isSSRRender = function isSSRRender() { return typeof document === 'undefined' || process.env.REACT_SSR === 'true'; }; /** * Props for the Cropper component */ // Type-safe intrinsic element types for CropperJS web components (local to this file) // Helper to create web component elements with proper typing var CropperCanvas = 'cropper-canvas'; var CropperImage = 'cropper-image'; var CropperSelection = 'cropper-selection'; var CropperShade = 'cropper-shade'; var CropperGrid = 'cropper-grid'; var CropperCrosshair = 'cropper-crosshair'; var CropperHandle = 'cropper-handle'; /** * Options for getting the cropped canvas */ /** * Methods exposed via ref */ /** * Cropper component - A React wrapper for CropperJS 2.x web components * * @example * ```tsx * const cropperRef = useRef<CropperRef>(null); * * <Cropper * ref={cropperRef} * src="/image.jpg" * aspectRatio={16/9} * onReady={(canvas) => console.log('Ready!')} * onChange={(e) => console.log(e.detail.bounds)} * /> * ``` */ var Cropper = exports.Cropper = /*#__PURE__*/(0, _react.forwardRef)(function (_ref, ref) { var src = _ref.src, _ref$alt = _ref.alt, alt = _ref$alt === void 0 ? '' : _ref$alt, crossOrigin = _ref.crossOrigin, aspectRatio = _ref.aspectRatio, initialAspectRatio = _ref.initialAspectRatio, _ref$initialCoverage = _ref.initialCoverage, initialCoverage = _ref$initialCoverage === void 0 ? 1 : _ref$initialCoverage, _ref$background = _ref.background, background = _ref$background === void 0 ? true : _ref$background, _ref$rotatable = _ref.rotatable, rotatable = _ref$rotatable === void 0 ? true : _ref$rotatable, _ref$scalable = _ref.scalable, scalable = _ref$scalable === void 0 ? true : _ref$scalable, _ref$translatable = _ref.translatable, translatable = _ref$translatable === void 0 ? true : _ref$translatable, _ref$movable = _ref.movable, movable = _ref$movable === void 0 ? true : _ref$movable, _ref$resizable = _ref.resizable, resizable = _ref$resizable === void 0 ? true : _ref$resizable, _ref$zoomable = _ref.zoomable, zoomable = _ref$zoomable === void 0 ? true : _ref$zoomable, _ref$multiple = _ref.multiple, multiple = _ref$multiple === void 0 ? false : _ref$multiple, _ref$outlined = _ref.outlined, outlined = _ref$outlined === void 0 ? true : _ref$outlined, className = _ref.className, onImageReady = _ref.onImageReady, _ref$isCircle = _ref.isCircle, isCircle = _ref$isCircle === void 0 ? false : _ref$isCircle; var canvasRef = (0, _react.useRef)(null); var selectionRef = (0, _react.useRef)(null); var imageRef = (0, _react.useRef)(null); var previousSelectionRef = (0, _react.useRef)(null); var _useState = (0, _react.useState)(false), _useState2 = (0, _slicedToArray2.default)(_useState, 2), isImageReady = _useState2[0], setIsImageReady = _useState2[1]; var _useState3 = (0, _react.useState)(false), _useState4 = (0, _slicedToArray2.default)(_useState3, 2), isCropperLoaded = _useState4[0], setIsCropperLoaded = _useState4[1]; var getCanvas = (0, _react.useCallback)(function () { return canvasRef.current; }, []); var getImage = (0, _react.useCallback)(function () { return imageRef.current; }, []); var getSelection = (0, _react.useCallback)(function () { return selectionRef.current; }, []); var getCroppedCanvas = (0, _react.useCallback)(function (options) { var selection = selectionRef.current; if (!selection) { return Promise.resolve(null); } // Check if the $toCanvas method exists (web component might not be fully initialized) if (typeof selection.$toCanvas !== 'function') { return Promise.resolve(null); } // If circular crop, we need to apply a circular mask if (isCircle) { // For circular crops, force square dimensions to maintain perfect circle var squareOptions = _objectSpread(_objectSpread({}, options), {}, { height: options === null || options === void 0 ? void 0 : options.width // Make height equal to width for a perfect circle }); return selection.$toCanvas(_objectSpread(_objectSpread({}, squareOptions), {}, { beforeDraw: function beforeDraw(context, canvas) { // For circles, ensure we have a square canvas var size = Math.min(canvas.width, canvas.height); var radius = size / 2; var centerX = size / 2; var centerY = size / 2; // Enable high-quality image rendering context.imageSmoothingEnabled = true; context.imageSmoothingQuality = 'high'; // Create a circular clipping path with anti-aliasing context.beginPath(); context.arc(centerX, centerY, radius, 0, Math.PI * 2); context.clip(); } })); } return selection.$toCanvas(options); }, [isCircle]); var fitStencilToImage = (0, _react.useCallback)(function () { var canvas = canvasRef.current; var image = imageRef.current; var selection = selectionRef.current; if (canvas && image && selection) { // Get the real time positions var canvasRect = canvas.getBoundingClientRect(); var imageRect = image.getBoundingClientRect(); // Calculate coordinates relative to the canvas requestAnimationFrame(function () { var x = imageRect.left - canvasRect.left; var y = imageRect.top - canvasRect.top; var width = imageRect.width; var height = imageRect.height; // Apply these to the selection (the stencil) if (typeof selection.$change === 'function') { selection.$change(Math.floor(x), Math.floor(y), Math.floor(width), Math.floor(height)); } }); } }, []); // Clamp position to keep crop area within image boundaries var clampPosition = (0, _react.useCallback)(function (current) { var canvas = canvasRef.current; var image = imageRef.current; if (!canvas || !image) { return null; } var canvasRect = canvas.getBoundingClientRect(); var imageRect = image.getBoundingClientRect(); var imageX = imageRect.left - canvasRect.left; var imageY = imageRect.top - canvasRect.top; var imageWidth = imageRect.width; var imageHeight = imageRect.height; var clampedX = current.x; var clampedY = current.y; // Clamp left/right if (clampedX < imageX) { clampedX = imageX; } else if (clampedX + current.width > imageX + imageWidth) { clampedX = imageX + imageWidth - current.width; } // Clamp top/bottom if (clampedY < imageY) { clampedY = imageY; } else if (clampedY + current.height > imageY + imageHeight) { clampedY = imageY + imageHeight - current.height; } return { x: clampedX, y: clampedY }; }, []); // Correct dimensions when resizing with aspect ratio constraints var correctResizeDimensions = (0, _react.useCallback)(function (current, selection) { var canvas = canvasRef.current; var image = imageRef.current; if (!canvas || !image) { return null; } var canvasRect = canvas.getBoundingClientRect(); var imageRect = image.getBoundingClientRect(); var imageX = imageRect.left - canvasRect.left; var imageY = imageRect.top - canvasRect.top; var imageWidth = imageRect.width; var imageHeight = imageRect.height; var maxWidthAvailable = imageX + imageWidth - current.x; var maxHeightAvailable = imageY + imageHeight - current.y; var correctedWidth = current.width; var correctedHeight = current.height; var hasAspectRatio = selection.aspectRatio && selection.aspectRatio > 0; if (hasAspectRatio) { var _aspectRatio = selection.aspectRatio; var heightIfMaxWidth = maxWidthAvailable / _aspectRatio; var widthIfMaxHeight = maxHeightAvailable * _aspectRatio; // Determine which axis is the limiting factor if (heightIfMaxWidth <= maxHeightAvailable) { correctedWidth = maxWidthAvailable; correctedHeight = heightIfMaxWidth; } else { correctedHeight = maxHeightAvailable; correctedWidth = widthIfMaxHeight; } } else { correctedWidth = Math.min(correctedWidth, maxWidthAvailable); correctedHeight = Math.min(correctedHeight, maxHeightAvailable); } // Ensure positive dimensions correctedWidth = Math.max(1, correctedWidth); correctedHeight = Math.max(1, correctedHeight); return { width: correctedWidth, height: correctedHeight }; }, []); // Handle move operation (position changed but size stayed same) var handleMove = (0, _react.useCallback)(function () { requestAnimationFrame(function () { var selection = selectionRef.current; if (!selection) { return; } var current = { x: selection.x || 0, y: selection.y || 0, width: selection.width || 0, height: selection.height || 0 }; var clamped = clampPosition(current); if (!clamped) { return; } if (typeof selection.$change === 'function') { selection.$change(Math.floor(clamped.x), Math.floor(clamped.y), Math.floor(current.width), Math.floor(current.height)); } previousSelectionRef.current = { width: current.width, height: current.height }; }); }, [clampPosition]); // Handle resize operation (size changed, may need aspect ratio correction) var handleResize = (0, _react.useCallback)(function () { requestAnimationFrame(function () { var selection = selectionRef.current; if (!selection) { return; } var current = { x: selection.x || 0, y: selection.y || 0, width: selection.width || 0, height: selection.height || 0 }; var corrected = correctResizeDimensions(current, selection); if (!corrected) { return; } var canvas = canvasRef.current; var image = imageRef.current; if (!canvas || !image) { return; } var canvasRect = canvas.getBoundingClientRect(); var imageRect = image.getBoundingClientRect(); var imageX = imageRect.left - canvasRect.left; var imageY = imageRect.top - canvasRect.top; var correctedX = Math.max(imageX, current.x); var correctedY = Math.max(imageY, current.y); if (typeof selection.$change === 'function') { selection.$change(Math.floor(correctedX), Math.floor(correctedY), Math.floor(corrected.width), Math.floor(corrected.height)); } previousSelectionRef.current = { width: corrected.width, height: corrected.height }; }); }, [correctResizeDimensions]); // Check if selection is within image boundaries var isSelectionOutOfBounds = (0, _react.useCallback)(function (imageX, imageY, imageWidth, imageHeight, selection) { var tolerance = 1; return selection.x < imageX - tolerance || selection.y < imageY - tolerance || selection.x + selection.width > imageX + imageWidth + tolerance || selection.y + selection.height > imageY + imageHeight + tolerance; }, []); // Detect if operation is a move or resize based on dimension change var isMovingSelection = (0, _react.useCallback)(function () { var prev = previousSelectionRef.current; var curr = selectionRef.current; if (!prev || !curr) { return false; } return prev.width === curr.width && prev.height === curr.height; }, []); var handleSelectionChange = (0, _react.useCallback)(function (event) { var canvas = canvasRef.current; var image = imageRef.current; var customEvent = event; var selection = customEvent.detail; if (!canvas || !image || !selection) { return; } var canvasRect = canvas.getBoundingClientRect(); var imageRect = image.getBoundingClientRect(); var imageX = imageRect.left - canvasRect.left; var imageY = imageRect.top - canvasRect.top; var imageWidth = imageRect.width; var imageHeight = imageRect.height; if (!isSelectionOutOfBounds(imageX, imageY, imageWidth, imageHeight, selection)) { previousSelectionRef.current = { width: selection.width, height: selection.height }; return; } if (isMovingSelection()) { handleMove(); } else { handleResize(); } }, [isSelectionOutOfBounds, isMovingSelection, handleMove, handleResize]); // Lazy load cropperjs only on the client to avoid SSR side effects (0, _react.useEffect)(function () { if (!isSSRRender()) { Promise.resolve().then(function () { return _interopRequireWildcard(require('cropperjs')); }).then(function () { return setIsCropperLoaded(true); }).catch(function () { setIsCropperLoaded(false); }); } }, []); // Auto-fit stencil to image on mount and when src changes // Only run after cropperjs has loaded (client-side only, post-hydration) (0, _react.useEffect)(function () { if (!isCropperLoaded) { return; } // eslint-disable-next-line @atlassian/perf-linting/no-chain-state-updates -- Ignored via go/ees017 (to be fixed) setIsImageReady(false); // Hide canvas while repositioning var image = imageRef.current; if (!image) { return; } // Wait for the image to be fully loaded if (typeof image.$ready === 'function') { image.$ready(function () { var timer = setTimeout(function () { fitStencilToImage(); setIsImageReady(true); onImageReady === null || onImageReady === void 0 || onImageReady(true); }, 500); // Adding a small timeout as there is a rendering issue with webkit return function () { return clearTimeout(timer); }; }); } else { // Fallback to timeout if $ready is not available var timer = setTimeout(function () { fitStencilToImage(); setIsImageReady(true); onImageReady === null || onImageReady === void 0 || onImageReady(true); }, 2000); return function () { return clearTimeout(timer); }; } }, [src, isCropperLoaded, onImageReady, fitStencilToImage]); // Attach selection change listener to enforce boundaries (only after image is ready) (0, _react.useEffect)(function () { if (!isImageReady) { return; } var selection = selectionRef.current; if (!selection) { return; } return (0, _bindEventListener.bind)(selection, { type: 'change', listener: handleSelectionChange }); }, [isImageReady, handleSelectionChange]); (0, _react.useEffect)(function () { if (!canvasRef.current || !imageRef.current) { return; } var observer = new ResizeObserver(function () { var _imageRef$current; // This forces the image to recalculate its "contain" logic // whenever the canvas wrapper changes size if (typeof ((_imageRef$current = imageRef.current) === null || _imageRef$current === void 0 ? void 0 : _imageRef$current.$center) === 'function') { var _imageRef$current2; (_imageRef$current2 = imageRef.current) === null || _imageRef$current2 === void 0 || _imageRef$current2.$center('contain'); } }); observer.observe(canvasRef.current); return function () { return observer.disconnect(); }; }, [canvasRef, imageRef]); (0, _react.useEffect)(function () { if (!canvasRef.current) { return; } var observer = new ResizeObserver(function () { var _selectionRef$current; (_selectionRef$current = selectionRef.current) === null || _selectionRef$current === void 0 || _selectionRef$current.removeAttribute('aspect-ratio'); fitStencilToImage(); }); observer.observe(canvasRef.current); return function () { return observer.disconnect(); }; }, [fitStencilToImage]); (0, _react.useImperativeHandle)(ref, function () { return { fitStencilToImage: fitStencilToImage, getCanvas: getCanvas, getCroppedCanvas: getCroppedCanvas, getImage: getImage, isImageReady: isImageReady, getSelection: getSelection }; }, [getCanvas, getCroppedCanvas, getImage, isImageReady, fitStencilToImage, getSelection]); // Inject global styles for cropper handles (0, _react.useEffect)(function () { var style = document.createElement('style'); var circularStyle = isCircle ? "\n\t\t\tcropper-selection {\n\t\t\t\toutline: none;\n\t\t\t\tborder-top: 1px solid #0052CC;\n\t\t\t\tborder-right: 1px solid #0052CC;\n\t\t\t\tborder-bottom: 1px solid #0052CC;\n\t\t\t\tborder-left: 1px solid #0052CC;\n\t\t\t\tbox-sizing: border-box;\n\t\t\t\tborder-radius: 50%;\n\t\t\t\tbox-shadow: 0 0 0 9999px rgba(255, 255, 255, 0.6);\n\t\t\t}\n\t\t" : "\n\t\t\tcropper-selection {\n\t\t\t\toutline: none;\n\t\t\t\tborder-top: 1px solid #0052CC;\n\t\t\t\tborder-right: 1px solid #0052CC;\n\t\t\t\tborder-bottom: 1px solid #0052CC;\n\t\t\t\tborder-left: 1px solid #0052CC;\n\t\t\t\tbox-sizing: border-box; \n\t\t\t}\n\t\t"; style.textContent = "\n\t\t\t\tcropper-canvas {\n\t\t\t\t\tbackground-color: #F8F8F8;\n\t\t\t\t\tbackground-image: none;\n\t\t\t\t}\n\t\t\t\tcropper-shade {\n\t\t\t\t\toutline-color: rgba(255,255,255,0.5);\n\t\t\t\t}\n\t\t\t\tcropper-handle[action=\"move\"] {\n\t\t\t\t\topacity: 0;\n\t\t\t\t}\n\t\t\t\tcropper-handle[action=\"ne-resize\"] {\n\t\t\t\t\tborder-radius: 7px 4px 4px 4px;\n\t\t\t\t\tborder-right: 7px solid rgba(255,255,255,0.6);\n\t\t\t\t\tborder-top: 7px solid rgba(255,255,255,0.6);\n\t\t\t\t\theight: 15px;\n\t\t\t\t\twidth: 15px;\n\t\t\t\t\tbackground-color: transparent;\n\t\t\t\t\ttransform: translate(-4px, 4px); \n\t\t\t\t}\n\t\t\t\tcropper-handle[action=\"ne-resize\"]::after {\n\t\t\t\t\tborder-radius: 5px 4px 4px 4px;\n\t\t\t\t\tborder-right: 5px solid #0052CC;\n\t\t\t\t\tborder-top: 5px solid #0052CC;\n\t\t\t\t\theight: 15px;\n\t\t\t\t\twidth: 15px;\n\t\t\t\t\tbackground-color: transparent;\n\t\t\t\t\ttransform: translate(-7px, -14px); \n\t\t\t\t}\n\t\t\t\tcropper-handle[action=\"nw-resize\"] {\n\t\t\t\t\tborder-radius: 4px 7px 4px 4px;\n\t\t\t\t\tborder-left: 7px solid rgba(255,255,255,0.6);\n\t\t\t\t\tborder-top: 7px solid rgba(255,255,255,0.6);\n\t\t\t\t\theight: 15px;\n\t\t\t\t\twidth: 15px;\n\t\t\t\t\tbackground-color: transparent;\n\t\t\t\t\ttransform: translate(4px, 4px); \n\t\t\t\t}\n\t\t\t\tcropper-handle[action=\"nw-resize\"]::after {\n\t\t\t\t\tborder-radius: 4px 5px 4px 4px;\n\t\t\t\t\tborder-left: 5px solid #0052CC;\n\t\t\t\t\tborder-top: 5px solid #0052CC;\n\t\t\t\t\theight: 15px;\n\t\t\t\t\twidth: 15px;\n\t\t\t\t\tbackground-color: transparent;\n\t\t\t\t\ttransform: translate(-14px, -14px); \n\t\t\t\t}\n\t\t\t\tcropper-handle[action=\"se-resize\"] {\n\t\t\t\t\tborder-radius: 4px 4px 7px 4px;\n\t\t\t\t\tborder-right: 7px solid rgba(255,255,255,0.6);\n\t\t\t\t\tborder-bottom: 7px solid rgba(255,255,255,0.6);\n\t\t\t\t\theight: 15px;\n\t\t\t\t\twidth: 15px;\n\t\t\t\t\tbackground-color: transparent;\n\t\t\t\t\ttransform: translate(-4px, -4px); \n\t\t\t\t}\n\t\t\t\tcropper-handle[action=\"se-resize\"]::after {\n\t\t\t\t\tborder-radius: 4px 4px 5px 4px;\n\t\t\t\t\tborder-right: 5px solid #0052CC;\n\t\t\t\t\tborder-bottom: 5px solid #0052CC;\n\t\t\t\t\theight: 15px;\n\t\t\t\t\twidth: 15px;\n\t\t\t\t\tbackground-color: transparent;\n\t\t\t\t\ttransform: translate(-7px, -7px); \n\t\t\t\t}\n\t\t\t\tcropper-handle[action=\"sw-resize\"] {\n\t\t\t\t\tborder-radius: 4px 4px 4px 7px;\n\t\t\t\t\tborder-left: 7px solid rgba(255,255,255,0.6);\n\t\t\t\t\tborder-bottom: 7px solid rgba(255,255,255,0.6);\n\t\t\t\t\theight: 15px;\n\t\t\t\t\twidth: 15px;\n\t\t\t\t\tbackground-color: transparent;\n\t\t\t\t\ttransform: translate(4px, -4px); \n\t\t\t\t}\n\t\t\t\tcropper-handle[action=\"sw-resize\"]::after {\n\t\t\t\t\tborder-radius: 4px 4px 4px 5px;\n\t\t\t\t\tborder-left: 5px solid #0052CC;\n\t\t\t\t\tborder-bottom: 5px solid #0052CC;\n\t\t\t\t\theight: 15px;\n\t\t\t\t\twidth: 15px;\n\t\t\t\t\tbackground-color: transparent;\n\t\t\t\t\ttransform: translate(-14px, -7px); \n\t\t\t\t}\n\t\t\t\t".concat(circularStyle, "\n\t\t\t"); document.head.appendChild(style); return function () { return style.remove(); }; }, [isCircle]); return /*#__PURE__*/_react.default.createElement(CropperCanvas, { ref: canvasRef, class: className, background: background // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , style: { opacity: isImageReady ? 1 : 0 } }, /*#__PURE__*/_react.default.createElement(CropperImage, { ref: imageRef, src: src, alt: alt, crossorigin: crossOrigin, rotatable: rotatable, scalable: scalable, translatable: translatable, "initial-center-size": "contain" }), /*#__PURE__*/_react.default.createElement(CropperShade, { x: 240, y: 5, width: 160, height: 90 }), /*#__PURE__*/_react.default.createElement(CropperSelection, { ref: selectionRef, "initial-coverage": initialCoverage, "aspect-ratio": aspectRatio, "initial-aspect-ratio": initialAspectRatio, movable: movable, resizable: resizable, zoomable: zoomable, multiple: multiple, outlined: outlined }, /*#__PURE__*/_react.default.createElement(CropperGrid, { role: "grid", hidden: true }), /*#__PURE__*/_react.default.createElement(CropperCrosshair, { hidden: true }), /*#__PURE__*/_react.default.createElement(CropperHandle, { action: "move" }), /*#__PURE__*/_react.default.createElement(CropperHandle, { action: "ne-resize" }), /*#__PURE__*/_react.default.createElement(CropperHandle, { action: "nw-resize" }), /*#__PURE__*/_react.default.createElement(CropperHandle, { action: "se-resize" }), /*#__PURE__*/_react.default.createElement(CropperHandle, { action: "sw-resize" }))); }); Cropper.displayName = 'Cropper';