toggle-corp/react-store

View on GitHub
components/View/Resizable/index.js

Summary

Maintainability
A
0 mins
Test Coverage
import PropTypes from 'prop-types';
import React from 'react';

/*
eslint css-modules/no-unused-class: [
    1,
    { markAsUsed: ['resizable-h', 'resizable-v'], camelCase: true }
]
*/
import styles from './styles.scss';

const propTypes = {
    className: PropTypes.string,

    firstChild: PropTypes.node,
    firstContainerClassName: PropTypes.string,

    secondChild: PropTypes.node,
    secondContainerClassName: PropTypes.string,

    separatorClassName: PropTypes.string,

    resizableClassName: PropTypes.string.isRequired,
    getInitialSize: PropTypes.func.isRequired,
    calculateDifference: PropTypes.func.isRequired,
    resizeContainers: PropTypes.func.isRequired,

    onResize: PropTypes.func,

    disabled: PropTypes.bool,
};

const defaultProps = {
    className: '',
    disabled: false,
    firstChild: null,
    secondChild: null,
    separatorClassName: '',
    firstContainerClassName: '',
    secondContainerClassName: '',
    onResize: undefined,
};

export default class Resizable extends React.PureComponent {
    static propTypes = propTypes;

    static defaultProps = defaultProps;

    static getMousePosition = e => ({
        x: e.clientX,
        y: e.clientY,
    })

    static isContainerClicked = (e, container) => {
        if (!container) {
            return false;
        }
        const rc = container.getBoundingClientRect();
        const { x, y } = Resizable.getMousePosition(e);

        return (
            x >= rc.left && x <= rc.right
            && y >= rc.top && y <= rc.bottom
        );
    }

    static syncResizeClassName = (container, resizing) => {
        if (!container) {
            return;
        }

        const resizeClassName = styles.resizing;
        const classNames = container.className.split(' ');
        const classNameIndex = classNames.findIndex(d => d === resizeClassName);

        if (resizing) {
            if (classNameIndex === -1) {
                classNames.push(resizeClassName);
            }
        } else if (classNameIndex !== -1) {
            classNames.splice(classNameIndex, 1);
        }

        // eslint-disable-next-line no-param-reassign
        container.className = classNames.join(' ');
    }

    constructor(props) {
        super(props);

        this.resizing = false;
        this.lastMousePosition = {};
        this.state = {
            dragging: false,
        };
    }

    componentDidMount() {
        window.addEventListener('mousedown', this.handleMouseDown);
        window.addEventListener('mouseup', this.handleMouseUp);
        window.addEventListener('mousemove', this.handleMouseMove);
    }

    componentWillUnmount() {
        window.removeEventListener('mousedown', this.handleMouseDown);
        window.removeEventListener('mouseup', this.handleMouseUp);
        window.removeEventListener('mousemove', this.handleMouseMove);
    }

    getSeparatorClassName = () => {
        const { separatorClassName } = this.props;
        const classNames = [
            separatorClassName,
            'separator',
            styles.separator,
        ];

        return classNames.join(' ');
    }

    getOverlayClassName = () => {
        const classNames = [
            'overlay',
            styles.overlay,
        ];
        return classNames.join(' ');
    }

    getClassName = () => {
        const { className, resizableClassName } = this.props;
        const classNames = [
            className,
            resizableClassName,
            styles[resizableClassName],
        ];

        return classNames.join(' ');
    }

    getFirstContainerClassName = () => {
        const { firstContainerClassName } = this.props;
        const classNames = [
            firstContainerClassName,
            'first',
            styles.first,
        ];

        return classNames.join(' ');
    }

    getSecondContainerClassName = () => {
        const { secondContainerClassName } = this.props;
        const classNames = [
            secondContainerClassName,
            'second',
            styles.second,
        ];

        return classNames.join(' ');
    }

    handleMouseDown = (e) => {
        const clickOnSeparator = Resizable.isContainerClicked(e, this.separator);

        if (clickOnSeparator) {
            Resizable.syncResizeClassName(this.container, true);

            this.resizing = true;
            this.lastMousePosition = Resizable.getMousePosition(e);
            this.initialSize = this.props.getInitialSize(this.firstContainer);
            this.setState({ dragging: true });
        } else {
            this.resizing = false;
        }
    }

    handleMouseUp = (e) => {
        if (this.resizing) {
            const mousePosition = Resizable.getMousePosition(e);
            const dx = this.props.calculateDifference(mousePosition, this.lastMousePosition);

            this.props.resizeContainers(
                this.firstContainer,
                this.secondContainer,
                this.initialSize + dx,
            );
            Resizable.syncResizeClassName(this.container, false);

            this.lastMousePosition = {};
            this.resizing = false;

            if (this.props.onResize) {
                this.props.onResize();
            }
            this.setState({ dragging: false });
        }
    }

    handleMouseMove = (e) => {
        if (this.resizing) {
            const mousePosition = Resizable.getMousePosition(e);
            const dx = this.props.calculateDifference(mousePosition, this.lastMousePosition);

            this.props.resizeContainers(
                this.firstContainer,
                this.secondContainer,
                this.initialSize + dx,
            );
        }
    }

    render() {
        const {
            firstChild,
            secondChild,
            disabled,
        } = this.props;
        const { dragging } = this.state;

        return (
            <div
                ref={(el) => { this.container = el; }}
                className={this.getClassName()}
            >
                <div
                    ref={(el) => { this.firstContainer = el; }}
                    className={this.getFirstContainerClassName()}
                >
                    { firstChild }
                    { dragging && (
                        <div className={this.getOverlayClassName()} />
                    )}
                </div>
                <div
                    ref={(el) => { this.secondContainer = el; }}
                    className={this.getSecondContainerClassName()}
                >
                    { secondChild }
                    { !disabled && (
                        <div
                            ref={(el) => { this.separator = el; }}
                            className={this.getSeparatorClassName()}
                        />
                    )}
                    { dragging && (
                        <div className={this.getOverlayClassName()} />
                    )}
                </div>
            </div>
        );
    }
}