All files react-cropper.tsx

91.43% Statements 32/35
85.71% Branches 24/28
88.89% Functions 8/9
91.18% Lines 31/34

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126                                                          1x 2x 2x 2x 2x 2x 2x           1x 8x   8x 8x 16x   14x 2x   12x         8x     1x                               8x 8x 8x 8x 8x 6x 5x                   5x           6x 5x             8x 8x 7x       8x                            
import React, {useEffect, useRef} from 'react';
import Cropper from 'cropperjs';
 
interface ReactCropperElement extends HTMLImageElement {
    cropper: Cropper;
}
 
type ReactCropperRef =
    | ((instance: HTMLImageElement | ReactCropperElement | null) => void)
    | React.MutableRefObject<HTMLImageElement | ReactCropperElement | null>
    | null;
 
interface ReactCropperDefaultOptions {
    scaleX?: number;
    scaleY?: number;
    enable?: boolean;
    zoomTo?: number;
    rotateTo?: number;
}
 
interface ReactCropperProps
    extends ReactCropperDefaultOptions,
        Cropper.Options<HTMLImageElement>,
        Omit<React.HTMLProps<HTMLImageElement>, 'data' | 'ref' | 'crossOrigin'> {
    crossOrigin?: '' | 'anonymous' | 'use-credentials' | undefined;
    on?: (eventName: string, callback: () => void | Promise<void>) => void | Promise<void>;
    onInitialized?: (instance: Cropper) => void | Promise<void>;
}
 
const applyDefaultOptions = (cropper: Cropper, options: ReactCropperDefaultOptions = {}): void => {
    const {enable = true, scaleX = 1, scaleY = 1, zoomTo = 0, rotateTo = 0} = options;
    enable ? cropper.enable() : cropper.disable();
    cropper.scaleX(scaleX);
    cropper.scaleY(scaleY);
    cropper.rotateTo(rotateTo);
    zoomTo > 0 && cropper.zoomTo(zoomTo);
};
 
/**
 * sourced from: https://itnext.io/reusing-the-ref-from-forwardref-with-react-hooks-4ce9df693dd
 */
const useCombinedRefs = (...refs: ReactCropperRef[]): React.RefObject<ReactCropperElement> => {
    const targetRef = useRef<ReactCropperElement>(null);
 
    React.useEffect(() => {
        refs.forEach((ref) => {
            if (!ref) return;
 
            if (typeof ref === 'function') {
                ref(targetRef.current);
            } else {
                ref.current = targetRef.current;
            }
        });
    }, [refs]);
 
    return targetRef;
};
 
const ReactCropper = React.forwardRef<ReactCropperElement | HTMLImageElement, ReactCropperProps>(({...props}, ref) => {
    const {
        dragMode = 'crop',
        src,
        style,
        className,
        crossOrigin,
        scaleX,
        scaleY,
        enable,
        zoomTo,
        rotateTo,
        alt = 'picture',
        ready,
        onInitialized,
        ...rest
    } = props;
    const defaultOptions: ReactCropperDefaultOptions = {scaleY, scaleX, enable, zoomTo, rotateTo};
    const innerRef = useRef<HTMLImageElement>(null);
    const combinedRef = useCombinedRefs(ref, innerRef);
    useEffect(() => {
        if (combinedRef.current !== null) {
            const cropper = new Cropper(combinedRef.current, {
                dragMode,
                ...rest,
                ready: (e) => {
                    if (e.currentTarget !== null) {
                        applyDefaultOptions(e.currentTarget.cropper, defaultOptions);
                    }
                    ready && ready(e);
                },
            });
            onInitialized && onInitialized(cropper);
        }
 
        /**
         * destroy cropper on un-mount
         */
        return () => {
            combinedRef.current?.cropper?.destroy();
        };
    }, [combinedRef]);
 
    /**
     * re-render when src changes
     */
    useEffect(() => {
        if (combinedRef.current?.cropper && typeof src !== 'undefined') {
            combinedRef.current.cropper.reset().clear().replace(src);
        }
    }, [src]);
 
    return (
        <div style={style} className={className}>
            <img
                crossOrigin={crossOrigin}
                src={src}
                alt={alt}
                style={{opacity: 0, maxWidth: '100%'}}
                ref={combinedRef}
            />
        </div>
    );
});
 
export {ReactCropper, ReactCropperProps, ReactCropperElement, applyDefaultOptions};