UNPKG

expo-dynamic-image-crop

Version:

Dynamic image cropping component for Expo/React Native with free-form and fixed aspect ratio support

161 lines (160 loc) 6.53 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { useContext, useState } from "react"; import * as ImageManipulator from "expo-image-manipulator"; import { useCallback, useEffect } from "react"; import { Modal, StatusBar, StyleSheet, View, } from "react-native"; import { EditorContext } from "./context/editor"; import { ControlBar } from "./ControlBar"; import { EditingWindow } from "./EditingWindow"; import { Processing } from "./Processing"; import { useEditorStore } from "./store"; import { usePerformCrop } from "./hooks/usePerformCrop"; function ImageEditorCore(props) { const { minimumCropDimensions = { width: 100, height: 100 }, fixedAspectRatio = 0.66666666666, dynamicCrop = false, onEditingCancel, onEditingComplete, imageUri = null, processingComponent, editorOptions, customControlBar, } = props; const options = useEditorStore((s) => s.editorOptions); const setOptions = useEditorStore((s) => s.setEditorOptions); const imageData = useEditorStore((s) => s.imageData); const setImageData = useEditorStore((s) => s.setImageData); const setReady = useEditorStore((s) => s.setReady); const setEditingMode = useEditorStore((s) => s.setEditingMode); const setProcessing = useEditorStore((s) => s.setProcessing); const isEdit = useEditorStore((s) => s.isEdit); const resetEditorStore = useEditorStore((s) => s.resetEditorStore); const initialize = useCallback(async () => { setProcessing(true); if (imageUri) { const { width: pickerWidth, height: pickerHeight } = await ImageManipulator.manipulateAsync(imageUri, []); setImageData({ uri: imageUri, width: pickerWidth, height: pickerHeight, }); setReady(true); setProcessing(false); } }, [imageUri, setImageData, setProcessing, setReady]); const onBackPress = () => { if (!isEdit) { onEditingCancel(); resetEditorStore(); } else { setProcessing(true); initialize().then(() => { setEditingMode("crop"); setProcessing(false); }); } }; const onSave = () => { onEditingComplete(imageData); resetEditorStore(); }; useEffect(() => { initialize().then(setCustomStyles).catch(console.error); // eslint-disable-next-line react-hooks/exhaustive-deps }, [imageUri]); function setCustomStyles() { if (editorOptions) { const custom = Object.assign({}, options); Object.entries(editorOptions).forEach(([key, value]) => { if (key) { // @ts-ignore if (typeof custom[key] === "object" && custom[key] !== null) { // @ts-ignore custom[key] = { ...custom[key], ...value }; } else { // @ts-ignore custom[key] = value; } } }); setOptions(custom); } } return (_jsxs(EditorContext.Provider, { value: { minimumCropDimensions, fixedAspectRatio, dynamicCrop, onBackPress, onSave, imageUri, }, children: [_jsx(StatusBar, { hidden: true }), _jsx(ImageEditorView, { processingComponent: processingComponent, customControlBar: customControlBar })] })); } export function ImageEditorView({ processingComponent, customControlBar, }) { const ready = useEditorStore((s) => s.ready); const processing = useEditorStore((s) => s.processing); const isEdit = useEditorStore((s) => s.isEdit); const setIsEdit = useEditorStore((s) => s.setIsEdit); const { backgroundColor, controlBar } = useEditorStore((s) => s.editorOptions); const { onBackPress, onSave } = useContext(EditorContext); const performCrop = usePerformCrop(); // Create actions object for custom control bar const controlBarActions = { onCancel: () => { onBackPress(); setIsEdit(false); }, onCrop: async () => { await performCrop(); setIsEdit(true); }, onSave: onSave, onBack: () => { onBackPress(); setIsEdit(false); }, isEdit, }; return (_jsxs(_Fragment, { children: [ready && (_jsx(View, { style: [styles.container, { backgroundColor }], children: customControlBar ? (_jsxs(_Fragment, { children: [customControlBar(controlBarActions), _jsx(EditingWindow, {})] })) : (_jsxs(_Fragment, { children: [controlBar?.position === "top" && _jsx(ControlBar, {}), _jsx(EditingWindow, {}), controlBar?.position === "bottom" && _jsx(ControlBar, {})] })) })), processing && _jsx(Processing, { customComponent: processingComponent })] })); } /** * Main ImageEditor component with optional modal wrapper * * @param props - ImageEditorProps * @param props.useModal - Whether to wrap editor in a modal (default: true) * @param props.isVisible - Modal visibility (required when useModal is true) * * @example * // With modal (default behavior) * <ImageEditor isVisible={showEditor} imageUri={uri} ... /> * * @example * // Inline without modal * <ImageEditor useModal={false} imageUri={uri} ... /> */ export function ImageEditor({ isVisible, useModal = true, ...props }) { const [open, setOpen] = useState(false); const resetEditorStore = useEditorStore((s) => s.resetEditorStore); useEffect(() => { if (useModal && isVisible !== undefined) { if (isVisible) { setTimeout(() => { setOpen(true); }, 600); } else { resetEditorStore(); setTimeout(() => { setOpen(false); }, 100); } } }, [isVisible, useModal, resetEditorStore]); // Inline mode: render directly without modal if (!useModal) { return _jsx(ImageEditorCore, { ...props }); } // Modal mode: wrap in modal (default behavior) return (_jsx(Modal, { visible: open, style: styles.modalContainer, children: _jsx(ImageEditorCore, { ...props }) })); } const styles = StyleSheet.create({ container: { flex: 1, }, modalContainer: { flex: 1, zIndex: 1, }, });