Draggable menu (React)

4+

Меню, созданное React, я отделил от основного меню. Для разнообразия сделал там три кнопки, переключающими отображение разного содержимого в одном окне.

Первое, что мне было интересно сделать это перемещение окна. Для этого я создал div в виде полосы в верхней части окна. При нажатии на неё активируется Drag Mode, при отпускании клавиши мыши над всем окном Matcap режим отключается.

<div className="MatCap" onMouseUp={disableDragMode} style={ {left:newX, top:newY} }>
            <div className='dragLine' onMouseDown={enableDragMode} > …

Функция enableDragMode переключает state

    const [dragModeEnabled, setDragMode ] = useState(false);

и устанавливает точку, в который произошел клик

const [clickPoint, setClickPoint] = useState({x: undefined, y: undefined});

    function enableDragMode () {
        if( event.buttons !== 1 ) return;
        let blockStyle = getComputedStyle(event.target.parentNode.parentNode)
        let blockOffset = {x: blockStyle.left.replace('px',''), y: blockStyle.top.replace('px','')};
        let point = {x: event.offsetX, y: event.offsetY, blockOffset: blockOffset}
        setClickPoint( point );
        setDragMode(true);
        event.stopPropagation()
        event.preventDefault()
    }

При переключении режима в useEffect добавляется или удаляется listener на событие движения мыши:

useEffect(() => {
        if( dragModeEnabled ) { document.addEventListener( "mousemove", onMouseMove, false) }
         return function cleanup() {
            document.removeEventListener( "mousemove", onMouseMove, false)
        }
    }, [dragModeEnabled] );

При перемещении мыши с зажатой левой клавишей происходит изменение координат div.

const [newX, setNewX] = useState('0px');
    const [newY, setNewY] = useState('30px');

 Так же добавлены проверки, чтобы div не мог двигаться дальше краев окна.

function onMouseMove () {
        if( event.pageX < 20 || event.pageX > document.documentElement.clientWidth-20 || event.pageY < 20 || event.pageY > document.documentElement.clientHeight-20 ){
            return;
        }
        if( event.buttons !== 1 ) {
            setDragMode(false);
            return;
        }
        moveAt(event.pageX, event.pageY);
    }

    function moveAt(pageX, pageY) {
        setNewX( pageX - clickPoint.x - clickPoint.blockOffset.x + 'px');
        setNewY( pageY - clickPoint.y - clickPoint.blockOffset.y + 'px');
    }

Полный код компонента Matcap.js

import React, { useState, useEffect, useContext } from 'react';
import './Matcap.css';
import { MatcapContext } from '../../../ReactPanel';

let headers = {
    'MatCaps': "Select Matcap",
    'Textures': "Select Texture",
    'Models': "Select Model"
}

function Matcap( props ) {

    const [dragModeEnabled, setDragMode ] = useState(false);
    const [newX, setNewX] = useState('0px');
    const [newY, setNewY] = useState('30px');
    const [ cardsDiv, setCardsDiv ] = useState(null);
    const [clickPoint, setClickPoint] = useState({x: undefined, y: undefined});
    const selectedCard = useContext( MatcapContext )

    useEffect(() => {
        if( dragModeEnabled ) { document.addEventListener( "mousemove", onMouseMove, false) }
         return function cleanup() {
            document.removeEventListener( "mousemove", onMouseMove, false)
        }
    }, [dragModeEnabled] );

    useEffect( () => {
        setCardsDiv( props.cardsDiv );
    }, [] )

    function clickUseButton () {
        appState.changeAppState( 'matcapChanged', selectedCard.src );
    }
    
    function changeCheckbox( e ) {
        console.log('changeCheckbox', e.target.checked );
    }
    
    function setPanelInvisible() {
        props.setPanel("")
    }

    function enableDragMode () {
        if( event.buttons !== 1 ) return;
        let blockStyle = getComputedStyle(event.target.parentNode.parentNode)
        let blockOffset = {x: blockStyle.left.replace('px',''), y: blockStyle.top.replace('px','')};
        let point = {x: event.offsetX, y: event.offsetY, blockOffset: blockOffset}
        setClickPoint( point );
        setDragMode(true);
        event.stopPropagation()
        event.preventDefault()
    }

    function onMouseMove () {
        if( event.pageX < 20 || event.pageX > document.documentElement.clientWidth-20 || event.pageY < 20 || event.pageY > document.documentElement.clientHeight-20 ){
            return;
        }
        if( event.buttons !== 1 ) {
            setDragMode(false);
            return;
        }
        moveAt(event.pageX, event.pageY);
    }

    function moveAt(pageX, pageY) {
        setNewX( pageX - clickPoint.x - clickPoint.blockOffset.x + 'px');
        setNewY( pageY - clickPoint.y - clickPoint.blockOffset.y + 'px');
    }

    function disableDragMode() {
        setDragMode(false);
    }

    return (
        <div className="MatCap" onMouseUp={disableDragMode} style={ {left:newX, top:newY} }>
            <div className='dragLine' onMouseDown={enableDragMode} >
                {headers[props.panel]}
                <button className="addPanel_CloseButton" onClick={ setPanelInvisible }>
                    X
                </button>
            </div>
            <div className="HeadMatCab">
                <div className="matCapPreview">
                    {props.panel === "MatCaps" &amp;&amp; selectedCard.img ? <img className="cardImgSelected" src = {selectedCard.img}></img> : "" }
                </div>
                <div className="HeadMatCabButtons">
                    <label>Preview
                        <input
                        type='checkbox'
                        name="checkbox"
                        onChange={ changeCheckbox }
                        />
                    </label>
                    <button
                        className='addPanel_Buttons'
                        onClick={ clickUseButton }>
                            Use
                    </button>
                </div>
            </div>
            { props.panel === "MatCaps" &amp;&amp; cardsDiv }
        </div>
    );
}

export default Matcap;

4+