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 ~