Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                
              
            
!

CSS

              
                html {
    overflow: scroll;
}

.cropper {
    position: relative;
    min-width: 100px;
}
.cropper__image {
    width: 100%;
    height: auto;
}
.cropper__cover {
    position: absolute;
    top: 0;
    left: 0;
}
.cropper__mask {
    fill: #333;
    fill-opacity: 0.7;
    cursor: crosshair;
}
.cropper__porthole {
    position: absolute;
    cursor: move;
}
.cropper__handle {
    position: absolute;
    width: 10px;
    height: 10px;
    background-color: white;
    border-radius: 2px;
    border: 1px solid #333;
    opacity: 0.8;
}
.cropper__handle:before {
    content: ' ';
    position: absolute;
    width: 40px;
    height: 40px;
    background: white;
    opacity: 0.2;
    z-index: -1;
}
.cropper__handle--ne {
    top: -5px;
    right: -5px;
    cursor: ne-resize;
}
.cropper__handle--ne:before {
    bottom: 0;
    left: 0;
    border-radius: 50% 50% 50% 0;
}
.cropper__handle--se {
    bottom: -5px;
    right: -5px;
    cursor: se-resize;
}
.cropper__handle--se:before {
    top: 0;
    left: 0;
    border-radius: 0 50% 50% 50%;
}
.cropper__handle--sw {
    bottom: -5px;
    left: -5px;
    cursor: sw-resize;
}
.cropper__handle--sw:before {
    top: 0;
    right: 0;
    border-radius: 50% 0 50% 50%;
}
.cropper__handle--nw {
    top: -5px;
    left: -5px;
    cursor: nw-resize;
}
.cropper__handle--nw:before {
    bottom: 0;
    right: 0;
    border-radius: 50% 50% 0 50%;
}
.cropper__ants {
    fill-opacity: 0;
    stroke: white;
    stroke-dasharray: 8;
    stroke-width: 2;
    
    -webkit-animation: croppermarchingants 30s linear infinite;
    -webkit-animation-fill-mode: forwards;
    -moz-animation: croppermarchingants 30s linear infinite;
    -moz-animation-fill-mode: forwards;
    animation: croppermarchingants 30s linear infinite;
    animation-fill-mode: forwards;
}
.cropper__ants--dark {
    fill-opacity: 0;
    stroke: #333;
    stroke-width: 2;
}
@-webkit-keyframes croppermarchingants  {
  to { stroke-dashoffset: 100%; }
}
@-moz-keyframes croppermarchingants  {
  to { stroke-dashoffset: 100%; }
}
@keyframes croppermarchingants {
  to { stroke-dashoffset: 100%; }
}
              
            
!

JS

              
                /**
 * TODO:
 * ability to deselect
 * keyboard nudge?
 * prop option for extended handles on or off?
 * onImageLoaded callback props
 */

/**
 * #### BEGIN APP-SPECIFIC CODE (not included in reusable component)
 */

class App extends React.Component {
    state = {
        previewUri: '',
        cropMask: undefined
    };

    _handleCrop(cropMask) {
        this.setState({cropMask});
        if (cropMask) {
            getCroppedImageUrl(TEST_IMAGE_URL, cropMask)
                .then((result) => {
                    this.setState({
                        previewUri: result
                    });
                });
        } else {
            this.setState({previewUri: ''})
        }
    }

    render() {
        let {previewUri, cropMask} = this.state;
        let preview = null;
        let actualAspectRatio = cropMask
            ? cropMask.width / cropMask.height
            : 'None';
        let requestedAspectRatio = 2/1; // 16/9;
        let maxChunkSize = 30;
        
        if (previewUri) {
            preview = (
                <img
                    src={previewUri}
                    alt="Crop Preview"
                    style={{ maxWidth: '100%', border: '1px solid black' }}
                />
            );
        }
        
        // "https://i.imgur.com/aZtvTw5.png"
                    // defaultCropMask={{
                    //     x: 100,
                    //     y: 100,
                    //     width: 100,
                    //     height: 100
                    // }}
        return (
            <div>
                <Cropper
                    src={TEST_IMAGE_URL}
                    onCrop={this._handleCrop.bind(this)}
                    aspectRatio={requestedAspectRatio}
                    aspectRatioMaxChunkSize={maxChunkSize}
                    aggressiveCallbacks={false}
                />
                <div>
                    <table border="0">
                        <tr>
                            <td>Requested aspect ratio:</td>
                            <td>{requestedAspectRatio}</td>
                        </tr>
                        <tr>
                            <td>Actual aspect ratio:</td>
                            <td>{actualAspectRatio}</td>
                        </tr>
                    </table>
                </div>
                <div>{JSON.stringify(cropMask)}</div>
                {preview}
            </div>
        );
    }
}

const preloadImage = (url, crossOrigin = 'anonymous') => (
    new Promise((resolve, reject) => {
        let img = new Image();

        img.crossOrigin = crossOrigin;
        img.onload = () => resolve(img);
        img.onerror = reject;
        img.setAttribute('src', url);
    })
);

const getCroppedImageUrl = (url, cropMask) => (
    preloadImage(url)
        .then((image) => {
            let canvas = document.createElement('canvas');
            canvas.setAttribute('width', cropMask.width);
            canvas.setAttribute('height', cropMask.height);

            let context = canvas.getContext('2d');
            context.drawImage(image,
                cropMask.x, cropMask.y, cropMask.width, cropMask.height,
                0, 0, cropMask.width, cropMask.height
            );
            return canvas.toDataURL('image/png');
        })
);

/**
 * #### BEGIN COMPONENT CODE
 */

const CROP_HANDLE_NE = 'crop-handle-ne';
const CROP_HANDLE_SE = 'crop-handle-se';
const CROP_HANDLE_SW = 'crop-handle-sw';
const CROP_HANDLE_NW = 'crop-handle-nw';
const CROP_HANDLE_DIRECTIONS = [
    CROP_HANDLE_NE,
    CROP_HANDLE_SE,
    CROP_HANDLE_SW,
    CROP_HANDLE_NW
];
const MINIMUM_CROP_DIMENSION = 30;
const DIRECTION_TO_CLASSNAME_MAP = {
    [CROP_HANDLE_NE]: 'cropper__handle--ne',
    [CROP_HANDLE_SE]: 'cropper__handle--se',
    [CROP_HANDLE_SW]: 'cropper__handle--sw',
    [CROP_HANDLE_NW]: 'cropper__handle--nw'
};

// const CropperPorthole = ({scaledCropMask, onPortholeDown, onHandleDown}) => {
//     if (!scaledCropMask) {
//         return null;
//     }
//     let handles = CROP_HANDLE_DIRECTIONS.map((direction) => (
//         <div
//             key={direction}
//             className={'cropper__handle ' + DIRECTION_TO_CLASSNAME_MAP[direction]}
//             data-handle="true"
//             onMouseDown={onHandleDown}
//             onTouchStart={onHandleDown}
//         />
//     ));

//     return (
//         <div
//             className="cropper__porthole"
//             style={pxToStyles(scaledCropMask)}
//             onMouseDown={onPortholeDown}
//             onTouchStart={onPortholeDown}
//         >{handles}</div>
//     );
// };

class Cropper extends React.Component {
    static propTypes = {
        src: React.PropTypes.string.isRequired,
        onCrop: React.PropTypes.func.isRequired,

        defaultCropMask: React.PropTypes.shape({
            x: React.PropTypes.number.isRequired,
            y: React.PropTypes.number.isRequired,
            width: React.PropTypes.number.isRequired,
            height: React.PropTypes.number.isRequired
        }),
        crossOrigin: React.PropTypes.string,
        aspectRatio: React.PropTypes.number,
        aspectRatioMaxChunkSize: React.PropTypes.number,
        aggressiveCallbacks: React.PropTypes.bool,
        altText: React.PropTypes.string
    };

    static defaultProps = {
        crossOrigin: 'anonymous',
        aggressiveCallbacks: false,
        altText: '',
        aspectRatioMaxChunkSize: 10
    };

    state = {
        naturalWidth: 0,
        naturalHeight: 0,
        cropMask: undefined,
        scale: 1
    };

    componentWillMount() {
        this._handleWindowSize = this._handleWindowSize.bind(this);

        addWindowHandler('resize', this._handleWindowSize);
    }

    componentWillUnmount() {
        removeWindowHandler('resize', this._handleWindowSize);
    }

    _handleWindowSize() {
        this._setScale();
    }

    _handleImageLoaded(event) {
        let {
            onCrop,
            aspectRatio,
            defaultCropMask: cropMask,
            aspectRatioMaxChunkSize
        } = this.props;
        let {naturalWidth, naturalHeight} = this.refs.image;

        if (cropMask) {
            cropMask = constrainCropMask(
                naturalWidth,
                naturalHeight,
                cropMask,
                aspectRatio,
                aspectRatioMaxChunkSize
            );
        } else {
            cropMask = centerCropMask(
                naturalWidth,
                naturalHeight,
                constrainCropMask(
                    naturalWidth,
                    naturalHeight, 
                    getDefaultCropMask(naturalWidth, naturalHeight),
                    aspectRatio,
                    aspectRatioMaxChunkSize
                )
            );
        }

        this.setState({
            naturalWidth,
            naturalHeight,
            cropMask
        });
        this._setScale();
        onCrop(cropMask);
    }

    _getScale() {
        let scale = 1;

        // window could be scaled before image loads
        if (this.refs.image) {
            let {clientWidth, naturalWidth} = this.refs.image;

            scale = getScale(clientWidth, naturalWidth);
        }
        return scale;
    }

    _setScale() {
        this.setState({
            scale: this._getScale()
        });
    }

    _handlePortholeMouseDown(event) {
        if (event.target.getAttribute('data-handle')) {
            return;
        }

        event.preventDefault();
        
        let {cropMask} = this.state;
        let handleMouseMove = this._handlePortholeMouseMove.bind(
            this,
            getEventCoords(event),
            cropMask
        );
        let handleMouseUp = this._handlePortholeMouseUp.bind(this);

        // if the user dragged the mouse into some other context (like a frame) then mouseup,
        // it's possible they could get in a state in which they're still moving things.
        // clicking should release their drag action.
        if (this._finishMoveAction) {
            this._finishMoveAction();
        }
        this._finishMoveAction = () => {
            removeMoveHandler(handleMouseMove);
            removeActionEndHandler(handleMouseUp);
            this._finishMoveAction = () => {};

            let {onCrop} = this.props;
            let {cropMask} = this.state;

            // invoke oncrop here in case the scenario above plays out.
            // when they click to release the drag action, invoke their callback.
            // if we do this in the mouseUp, it could be missed if they mouseUp in
            // a different context.
            onCrop(cropMask);
        };

        addMoveHandler(handleMouseMove);
        addActionEndHandler(handleMouseUp);
    }

    _handlePortholeMouseMove(dragStartPosition, dragStartCrop, event) {
        event.preventDefault();

        let {
            aspectRatio,
            aggressiveCallbacks,
            onCrop
        } = this.props;
        let {naturalWidth, naturalHeight, scale} = this.state;
        let unconstrainedNextCropMask = getNextMovedCropMask(
            dragStartPosition,
            dragStartCrop,
            getEventCoords(event),
            scale
        );
        let cropMask = constrainCropMaskXY(
            naturalWidth,
            naturalHeight,
            unconstrainedNextCropMask
        );

        this.setState({cropMask});
        if (aggressiveCallbacks) {
            onCrop(cropMask);
        }
    }

    _handlePortholeMouseUp(event) {
        event.preventDefault();
        this._finishMoveAction();
    }

    _handleHandleMouseDown(direction, event) {
        event.preventDefault();

        let {cropMask} = this.state;
        let handleMouseMove = this._handleHandleMouseMove.bind(
            this,
            getEventCoords(event),
            cropMask,
            direction
        );
        let handleMouseUp = this._handleHandleMouseUp.bind(this);

        // if the user dragged the mouse into some other context (like a frame) then mouseup,
        // it's possible they could get in a state in which they're still moving things.
        // clicking should release their resize action.
        if (this._finishSizeAction) {
            this._finishSizeAction();
        }
        this._finishSizeAction = () => {
            removeMoveHandler(handleMouseMove);
            removeActionEndHandler(handleMouseUp);
            this._finishSizeAction = () => {};

            let {onCrop} = this.props;
            let {cropMask} = this.state;

            // invoke oncrop here in case the scenario above plays out.
            // when they click to release the resize action, invoke their callback.
            // if we do this in the mouseUp, it could be missed if they mouseUp in
            // a different context.
            onCrop(cropMask);
        };
        addMoveHandler(handleMouseMove);
        addActionEndHandler(handleMouseUp);
    }

    _handleHandleMouseMove(dragStartPosition, dragStartCrop, direction, event) {
        event.preventDefault();

        let {
            aspectRatio,
            aggressiveCallbacks,
            onCrop,
            aspectRatioMaxChunkSize
        } = this.props;
        let {
            pageX: startPageX,
            pageY: startPageY
        } = dragStartPosition;
        let {
            pageX: newPageX,
            pageY: newPageY
        } = getEventCoords(event);
        let {x,y,width,height} = dragStartCrop;
        let {naturalWidth, naturalHeight, scale} = this.state;
        let invertWidth = direction === CROP_HANDLE_SW || direction === CROP_HANDLE_NW;
        let invertHeight = direction === CROP_HANDLE_NW || direction === CROP_HANDLE_NE;
        let widthAdjustment = ((startPageX - newPageX) * scale) * (invertWidth ? -1 : 1);
        let heightAdjustment = ((startPageY - newPageY) * scale) * (invertHeight ? -1 : 1);
        let adjustedWidth = Math.floor(width - widthAdjustment);
        let adjustedHeight = Math.floor(height - heightAdjustment);
        // the following prevents the box from getting larger when it hits the edge (still needs tweaking for resizing off the bottom)
        if (adjustedWidth + x > naturalWidth) {
            adjustedWidth = naturalWidth - x;
        }
        if (adjustedHeight + y > naturalHeight) {
            adjustedHeight = naturalHeight - y;
        }
        // the preceding prevents the box from getting larger when it hits the edge
        let sizeConstrainedMask = constrainCropMask(
            naturalWidth,
            naturalHeight,
            {
                x,
                y,
                width: adjustedWidth,
                height: adjustedHeight
            },
            aspectRatio,
            aspectRatioMaxChunkSize
        );
        
        let xAdjustment = invertWidth
            ? sizeConstrainedMask.width - width
            : 0;
        let yAdjustment = invertHeight
            ? sizeConstrainedMask.height - height
            : 0;
        let cropMask = constrainCropMaskXY(
            naturalWidth,
            naturalHeight,
            {
                ...sizeConstrainedMask,
                x: x - xAdjustment,
                y: y - yAdjustment
            }
        );

        this.setState({cropMask});
        if (aggressiveCallbacks) {
            onCrop(cropMask);
        }
    }

    _handleHandleMouseUp(event) {
        event.preventDefault();
        this._finishSizeAction();
    }

    render() {
        let {
            src,
            crossOrigin,
            altText
        } = this.props;
        let {
            naturalWidth,
            naturalHeight,
            cropMask
        } = this.state;
        let svgCropMask;
        let porthole;
        let scale = this._getScale();

        // image is loaded and we have a mask to show
        if (naturalWidth && naturalHeight && cropMask) {
            let scaledCropMask = getScaledCropMask(cropMask, scale);

            svgCropMask = (
                <svg
                    width="100%"
                    height="100%"
                    xmlns="http://www.w3.org/2000/svg"
                    className="cropper__cover"
                >
                    <polygon
                        className="cropper__mask"
                        points={getCropMaskPolygon(naturalWidth, naturalHeight, cropMask, scale)}
                    />
                    <rect
                        className="cropper__ants--dark"
                        {...scaledCropMask}
                    />
                    <rect
                        className="cropper__ants"
                        {...scaledCropMask}
                    />
                </svg>
            );
            
            let handles = CROP_HANDLE_DIRECTIONS.map((direction) => (
                <div
                    className={'cropper__handle ' + DIRECTION_TO_CLASSNAME_MAP[direction]}
                    data-handle="true"
                    onMouseDown={this._handleHandleMouseDown.bind(this, direction)}
                    onTouchStart={this._handleHandleMouseDown.bind(this, direction)}
                />
            ));
            
            porthole = (
                <div
                    className="cropper__porthole"
                    style={pxToStyles(scaledCropMask)}
                    onMouseDown={this._handlePortholeMouseDown.bind(this)}
                    onTouchStart={this._handlePortholeMouseDown.bind(this)}
                >{handles}</div>
            );
        }

/* viewBox={`0 0 ${naturalWidth} ${naturalHeight}`} */
/*
                    <polygon
                        className="cropper__ants"
                        points={getMarchingAntsPolygon(cropMask, scale)}
                        onMouseDown={this._handlePortholeMouseDown.bind(this)}
                    />
*/
        
        return (
            <div className="cropper">
                {svgCropMask}
                {porthole}
                <img
                    src={src}
                    alt={altText}
                    className="cropper__image"
                    crossOrigin={crossOrigin}
                    ref="image"
                    onLoad={this._handleImageLoaded.bind(this)}
                />
            </div>
        );
    }
}

// const getHandlePosition = ({x,y,width,height}, direction) => {
//     const size = 20;
//     const halfSize = Math.floor(size / 2);
//     const dim = ({x, y}) => ({x, y, width: size, height: size});

//     switch (direction) {
//         case CROP_HANDLE_NE:
//             return dim({
//                 x: x + width - halfSize,
//                 y: y - halfSize
//             });
//         case CROP_HANDLE_SE:
//             return dim({
//                 x: x + width - halfSize,
//                 y: y + height - halfSize
//             });
//         case CROP_HANDLE_SW:
//             return dim({
//                 x: x - halfSize,
//                 y: y + height - halfSize
//             });
//         case CROP_HANDLE_NW:
//             return dim({
//                 x: x - halfSize,
//                 y: y - halfSize
//             });
//     };
// }

const getScaledCropMask = ({x,y,width,height}, scale = 1) => ({
    x: Math.round(x / scale),
    y: Math.round(y / scale),
    width: Math.round(width / scale),
    height: Math.round(height / scale)
});

const pxToStyles = ({x,y,width,height}) => ({
    left: `${x}px`,
    top: `${y}px`,
    width: `${width}px`,
    height: `${height}px`
});

// const getMarchingAntsPolygon = ({x,y,width,height}, scale = 1) => {
//     let top = Math.round(y / scale);
//     let left = Math.round(x / scale);
//     let bottom = Math.round((y + height) / scale);
//     let right = Math.round((x + width) / scale);

//     return [
//         `${left},${top}`,
//         `${right},${top}`,
//         `${right},${bottom}`,
//         `${left},${bottom}`,
//         `${left},${top}`
//     ];
// };

const getCropMaskPolygon = (
    naturalWidth,
    naturalHeight,
    cropMask,
    scale = 1
) => {
    let width = Math.round(naturalWidth / scale);
    let height = Math.round(naturalHeight / scale);
    let top = Math.round(cropMask.y / scale);
    let left = Math.round(cropMask.x / scale);
    let bottom = Math.round((cropMask.y + cropMask.height) / scale);
    let right = Math.round((cropMask.x + cropMask.width) / scale);

    return [
        '0,0',
        `${left},0`,
        `${left},${bottom}`,
        `${right},${bottom}`,
        `${right},${top}`,
        `${left},${top}`,
        `${left},0`,
        `${width},0`,
        `${width},${height}`,
        `0,${height}`,
        '0,0'
    ].join(' ');
};

const getDefaultCropMask = (width, height, multiplier = 0.1) => {
    let percentWidth = Math.floor(width * multiplier);
    let percentHeight = Math.floor(height * multiplier);

    return {
        x: percentWidth,
        y: percentHeight,
        width: width - (percentWidth * 2),
        height: height - (percentHeight * 2)
    };
};

const getScale = (currentWidth, naturalWidth) => naturalWidth / currentWidth;

const getNextMovedCropMask = (dragStartPosition, dragStartCrop, newPosition, scale) => {
    let {
        pageX: startPageX,
        pageY: startPageY
    } = dragStartPosition;
    let {
        pageX: newPageX,
        pageY: newPageY
    } = newPosition;
    let {x,y,width,height} = dragStartCrop;

    return {
        x: Math.floor(x - ((startPageX - newPageX) * scale)),
        y: Math.floor(y - ((startPageY - newPageY) * scale)),
        width,
        height
    };
};

const correctAspectRatio = (width, aspectRatio, naturalWidth, naturalHeight, maxAcceptableChunk = 10) => {
    let getDimensions = (width) => {
        let height = width / aspectRatio;
        return {width, height, intOffset: height % 1}
    }
    let potentialDimensions = [];
    let preliminaryHeight = width / aspectRatio;

    if (preliminaryHeight > naturalHeight) {
        width = Math.floor(naturalHeight * aspectRatio);
        if (width > naturalWidth) {
            // this should only ever shrink the width because we generated a height from the width that was too tall and therefore we must LOWER the width to lower the height.
            // this function assumes the implementer has provided a valid width to start, so the width it just shrank should still be valid.
            throw new Error('not really sure what happened here, need to give it some thought.  feel free to report a bug!')
        }
    }

    for (let i = 0; i <= maxAcceptableChunk; i++) {
        potentialDimensions.push(getDimensions(width - i));
        if (potentialDimensions[i].intOffset === 0) {
            return potentialDimensions[i];
        }
    }

    // couldn't find a perfect integer, find the one that's closest
    // could improve this by sorting instead?
//     let smallestIntOffset = Math.min.apply(Math, potentialDimensions.map((o) => o.intOffset));
//     let dimensionWithSmallestOffset = potentialDimensions.find((dim) => dim.intOffset === smallestIntOffset);

//     return dimensionWithSmallestOffset;

    // run a test against these to see which is fastest
    let sorted = potentialDimensions.sort(
        ({intOffset: a}, {intOffset: b}) => (a < b ? -1 : (a > b ? 1 : 0))
    );
    return sorted[0];
};

const constrainCropMaskXY = (
    naturalWidth,
    naturalHeight,
    {x,y,width,height}
) => {
    if (x + width > naturalWidth) {
        x = naturalWidth - width;
    }
    if (y + height > naturalHeight) {
        y = naturalHeight - height;
    }
    return {
        x: Math.floor(Math.max(0, x)),
        y: Math.floor(Math.max(0, y)),
        width,
        height
    };
};

const constrainCropMask = (
    naturalWidth,
    naturalHeight,
    {x,y,width,height},
    aspectRatio,
    maxChunkSize
) => {
    width = Math.max(
        MINIMUM_CROP_DIMENSION,
        Math.min(naturalWidth, width)
    );
    height = Math.max(
        MINIMUM_CROP_DIMENSION,
        Math.min(naturalHeight, height)
    );
    if (aspectRatio) {
        // http://stackoverflow.com/questions/8038605/resizing-while-maintaining-aspect-ratio-and-rounding-to-even-integers
        let corrected = correctAspectRatio(
            width,
            aspectRatio,
            naturalWidth,
            naturalHeight,
            maxChunkSize
        );
        width = corrected.width;
        height = corrected.height;
    }
    width = Math.floor(width);
    height = Math.floor(height);

    return {
        ...constrainCropMaskXY(naturalWidth, naturalHeight, {x,y,width,height}),
        width,
        height
    };
};

const centerCropMask = (
    naturalWidth,
    naturalHeight,
    {x,y,width,height}
) => ({
    x: Math.floor((naturalWidth - width) / 2),
    y: Math.floor((naturalHeight - height) / 2),
    width,
    height
})

const getEventCoords = ({touches, pageX, pageY}) => {
    return touches ? {
        pageX: touches[0].pageX,
        pageY: touches[0].pageY
    } : {pageX, pageY};
};

const HAS_WINDOW = typeof(window) !== 'undefined';

const addWindowHandler = (eventName, handler) => {
    if (HAS_WINDOW) {
        window.addEventListener(eventName, handler);
    }
};
const removeWindowHandler = (eventName, handler) => {
    if (HAS_WINDOW) {
        window.removeEventListener(eventName, handler);
    }
};
const addMoveHandler = (handler) => {
    addWindowHandler('mousemove', handler);
    addWindowHandler('touchmove', handler);
};
const removeMoveHandler = (handler) => {
    removeWindowHandler('mousemove', handler);
    removeWindowHandler('touchmove', handler);
};
const addActionEndHandler = (handler) => {
    addWindowHandler('mouseup', handler);
    addWindowHandler('touchend', handler);
};
const removeActionEndHandler = (handler) => {
    removeWindowHandler('mouseup', handler);
    removeWindowHandler('touchend', handler);
};

mountOnLoad(
    <App />,
    document.body
);
              
            
!
999px

Console