Many admin backs support drag-and-drop to adjust the width of the left menu to improve the user experience.

BTW, record the screen generated GIF plug-in is the ~ chrome.google.com/webstore/de…

This article describes the implementation in detail.

algorithm

The algorithm is: start to drag the dividing line, monitor cursor movement, record the horizontal position of the cursor, set the menu width for the current width plus the difference between the current horizontal position of the cursor and the previous horizontal position, end of the drag. The flow chart is as follows:

Now, we use React to implement it step by step.

The specific implementation

1 Start dragging

On the drag line, press the mouse to start dragging. At this point, record the horizontal position of the cursor.

// Cursor horizontal position
const [clientX, setClientX] = useState(0)
// Whether you are dragging
const [isResizing, setIsResizing] = useState(false)
const handleStartResize = useCallback((e: React.MouseEvent<HTMLElement>) = > {
    setClientX(e.clientX)
    setIsResizing(true)}, [])return (
  <div
    className={s.resizeHandle}
    onMouseDown={handleStartResize}
  ></div>
)
Copy the code

Note: To make it easier for the user to start dragging, make the drag line wider. Such as:

.resizeHandle {
  width: 10px;
  cursor: col-resize;
  / *... * /
}
Copy the code

2 Set the width of the menu

Monitor cursor movement:

useEffect(() = > {
  document.addEventListener('mousemove', handleResize)
  return () = > {
    document.removeEventListener('mousemove', handleResize)
  }
}, [handleResize])
Copy the code

Record cursor horizontal position, set menu width:

// Left width
const [menuWidth, setMenuWidth] = useState(calculatedMenuWidth);
const handleStartResize = useCallback((e) = > {
  if(! isResizing) {return
  }
  const offset = e.clientX - clientX
  const width = menuWidth + offset
  setMenuWidth(width)},menuWidth.clientX])

return (
  <div className={s.app}>
    <div
      className={s.menu}
      style={{ width: `${menuWidth}px`}} > menu < div className = {s.r esizeHandle} / > < / div > < div > right < / div > < / div >)
Copy the code

The mousemove event will be triggered frequently while dragging. To improve performance, we will do a shake proofing:

import { useDebounceFn } from 'ahooks'
const { run: didHandleResize } = useDebounceFn((e) = > {
  /* Resize business logic */
}
consthandleResize = useCallback(didHandleResize, [...] )Copy the code

3 End the drag and drop

Lift the mouse to end the drag and drop.

const handleStopResizing = useCallback(() = > {
  setIsResizing(false)
}, [prevUserSelectStyle])

useEffect(() = > {
  document.addEventListener('mouseup', handleStopResize)
  return () = > {
    document.removeEventListener('mouseup', handleStopResize)
  }
}, [handleStopResize])
Copy the code

The above achieved the core functionality, but used in a production environment, requires a better user experience. Now let’s do some optimization.

To optimize the

Limit the width of the menu

Menus that are too wide or too narrow will result in poor display. Therefore, set the maximum and minimum width of the limit menu.

const MENU_MIN = 200;
const MENU_MAX = 600;

const handleResize = useCallback(offset= > {
  let res = menuWidth + offset;
  if (res < MENU_MIN) {
    res = MENU_MIN;
  }
  if (res > MENU_MAX) {
    res = MENU_MAX;
  }
  setMenuWidth(res);
}, [menuWidth]);
Copy the code

Menu width to store Settings

After the user sets the width of the menu, the page refreshes and the width of the menu remains the same as before.

const MENU_DEFAULT = 300

const [menuWidth, setMenuWidth] = useState(localStorage.getItem('app-left-menu-width') || MENU_DEFAULT)

const handleResize = useCallback(offset= > {
  const res = ...
  setMenuWidth(res)
  localStorage.setItem('app-left-menu-width'.`${res}`);
}, [])

Copy the code

Optimize end drag judgment

If mouseup bubbling is disabled in the iframe or the element when the user lifts the mouse, the event will not bubble over the Document, rendering the end-of-drag judgment invalid.

Solution: As you drag, put a mask over the page to cover the elements below. When the user mouseup, which is actually on the mask element, always bubbles up onto the Document. JS:

{isResizing && <div className={s.mask} />}
Copy the code

CSS:

.mask {
  position: fixed;
  z-index: 98;
  left: 0;
  top: 0;
  height: 100%;
  width: 100%;
}
Copy the code

Deselect text while dragging

Drag to select text as it passes text. The following code removes the check:

const [prevUserSelectStyle, setPrevUserSelectStyle] = useState(getComputedStyle(document.body).userSelect)

const handleStartResize = useCallback((e: React.MouseEvent<HTMLElement>) = > {
  setPrevUserSelectStyle(getComputedStyle(document.body).userSelect)
  document.body.style.userSelect = 'none'
  / /...}, [...]. )const handleStopResize = useCallback(() = > {
  document.body.style.userSelect = prevUserSelectStyle
  // ...}, [...]. )Copy the code

The complete code

I’ve abstracted the ability to adjust the width of an element into a component. The calling code for the component is as follows:

import {useState, useCallback} from 'react'
import ResizeWidth from './components/resize-width/index.tsx'
import s from './App.module.scss';

const MENU_MIN = 200
const MENU_MAX = 600
const MENU_DEFAULT = 300
function App() {
  const storedMenuWidth = localStorage.getItem('app-left-menu-width')
  const calculatedMenuWidth = storedMenuWidth ? parseInt(storedMenuWidth, 10) : MENU_DEFAULT
  const [menuWidth, setMenuWidth] = useState(calculatedMenuWidth)
  const [expandLeftWidth, setExpandLeftWidth] = useState(calculatedMenuWidth)
 
  const handleResize = useCallback(offset= > {
    let res = menuWidth + offset
    if (res < MENU_MIN) {
      res = MENU_MIN
    }
    if (res > MENU_MAX) {
      res = MENU_MAX
    }
    setMenuWidth(res)
    setExpandLeftWidth(res)
    localStorage.setItem('app-left-menu-width'.`${res}`)
  }, [menuWidth])

  const handleToggleExpand = useCallback((isExpend) = > {
    setMenuWidth(isExpend ? expandLeftWidth : 0)
  }, [expandLeftWidth])

  return (
    <div
      className={s.app}
    >
      <div
        className={s.menu}
        style={{ width:` ${menuWidth}px`}} >
        <ResizeWidth
          onResize={handleResize}
          onToggleExpand={handleToggleExpand}
        />
      </div>
      <div className={s.main}>
      </div>
    </div>)}export default App
Copy the code

The component code is as follows:

import React, { FC, useCallback, useEffect, useState } from 'react'
import { useDebounceFn } from 'ahooks'
import cn from 'classnames'
import s from './style.module.scss'

export interface IResizeWidthProps {
  onResize: (offset: number) = > void
  onToggleExpand: (isExpand: boolean) = > void
}

const ResizeWidth: FC<IResizeWidthProps> = ({ onToggleExpand, onResize }) = > {
  const [clientX, setClientX] = useState(0)
  const [isResizing, setIsResizing] = useState(false)
  const [prevUserSelectStyle, setPrevUserSelectStyle] = useState(getComputedStyle(document.body).userSelect)

  const handleStartResize = useCallback((e: React.MouseEvent<HTMLElement>) = > {
    setClientX(e.clientX)
    setIsResizing(true)
    setPrevUserSelectStyle(getComputedStyle(document.body).userSelect)
    document.body.style.userSelect = 'none'
  }, [])

  const handleStopResize = useCallback(() = > {
    setIsResizing(false)
    document.body.style.userSelect = prevUserSelectStyle
  }, [prevUserSelectStyle])

  const { run: didHandleResize } = useDebounceFn((e) = > {
    if(! isResizing) {return
    }
    const offset = e.clientX - clientX
    setClientX(e.clientX)
    onResize(offset)
  }, {
    wait: 0,})const handleResize = useCallback(didHandleResize, [isResizing, clientX, didHandleResize])

  useEffect(() = > {
    document.addEventListener('mouseup', handleStopResize)
    document.addEventListener('mousemove', handleResize)
    return () = > {
      document.removeEventListener('mouseup', handleStopResize)
      document.removeEventListener('mousemove', handleResize)
    }
  }, [handleStopResize, handleResize])


  const [isExpand, setIsExpand] = useState(true)
  const handleToggleExpand = () = > {
    constnext = ! isExpand onToggleExpand(next) setIsExpand(next) }return (
    <div
      className={s.resizeHandle}
      onMouseDown={handleStartResize}
    >
      <div className={cn(s.toggleBtn, !isExpand && s.fold)} onClick={handleToggleExpand} />
      {isResizing && <div className={s.mask} />}
    </div>)}export default React.memo(ResizeWidth)
Copy the code

Follow the public account: front-end GoGoGo, help you promotion and salary ~