mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
feat: optimize resize and enable drag on Firefox
This commit is contained in:
parent
86a030f308
commit
193a8f48a1
3 changed files with 102 additions and 85 deletions
|
|
@ -1,10 +1,10 @@
|
|||
import { Icon } from 'components/Icon'
|
||||
import * as React from 'react'
|
||||
import { Size } from './SideBarBodyWrapper'
|
||||
import { Size2D } from './SideBarBodyWrapper'
|
||||
|
||||
type Props = {
|
||||
size: Size
|
||||
onResize(size: Size): void
|
||||
size: Size2D
|
||||
onResize(size: Size2D): void
|
||||
onResetSize?(): void
|
||||
onResizeStateChange?(state: ResizeState): void
|
||||
style?: React.CSSProperties
|
||||
|
|
@ -12,55 +12,13 @@ type Props = {
|
|||
|
||||
type ResizeState = 'idle' | 'resizing'
|
||||
|
||||
export function HorizontalResizeHandler({
|
||||
onResize,
|
||||
onResetSize,
|
||||
onResizeStateChange,
|
||||
size,
|
||||
style,
|
||||
}: Props) {
|
||||
const pointerDown = React.useRef(false)
|
||||
const startX = React.useRef(0)
|
||||
const baseSize = React.useRef(size)
|
||||
const latestPropSize = React.useRef(size)
|
||||
|
||||
React.useEffect(() => {
|
||||
latestPropSize.current = size
|
||||
}, [size])
|
||||
|
||||
const onPointerDown = React.useCallback(({ clientX }: React.MouseEvent) => {
|
||||
startX.current = clientX
|
||||
pointerDown.current = true
|
||||
baseSize.current = latestPropSize.current
|
||||
onResizeStateChange?.('resizing')
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
const onPointerMove = ({ clientX }: MouseEvent) => {
|
||||
if (!pointerDown.current) return
|
||||
const shift = clientX - startX.current
|
||||
onResize(baseSize.current + shift)
|
||||
}
|
||||
window.addEventListener('mousemove', onPointerMove)
|
||||
return () => window.removeEventListener('mousemove', onPointerMove)
|
||||
}, [onResize])
|
||||
|
||||
React.useEffect(() => {
|
||||
const onPointerUp = () => {
|
||||
if (pointerDown.current) {
|
||||
pointerDown.current = false
|
||||
baseSize.current = latestPropSize.current
|
||||
onResizeStateChange?.('idle')
|
||||
}
|
||||
}
|
||||
window.addEventListener('mouseup', onPointerUp)
|
||||
return () => window.removeEventListener('mouseup', onPointerUp)
|
||||
}, [])
|
||||
export function ResizeHandler({ onResize, onResetSize, onResizeStateChange, size, style }: Props) {
|
||||
const { onPointerDown } = useResizeHandler(size, onResize, { onResizeStateChange })
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'gitako-resize-handler'}
|
||||
onMouseDown={onPointerDown}
|
||||
onPointerDown={onPointerDown}
|
||||
onDoubleClick={onResetSize}
|
||||
style={style}
|
||||
>
|
||||
|
|
@ -68,3 +26,61 @@ export function HorizontalResizeHandler({
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function useResizeHandler(
|
||||
size: Size2D,
|
||||
onResize: (size: Size2D) => void,
|
||||
{
|
||||
onResizeStateChange,
|
||||
onClick,
|
||||
}: Partial<{
|
||||
onResizeStateChange: (state: ResizeState) => void
|
||||
onClick: (e: PointerEvent) => void
|
||||
}> = {},
|
||||
) {
|
||||
const pointerDown = React.useRef(false)
|
||||
const pointerMoved = React.useRef(false)
|
||||
const initialSizeRef = React.useRef([0, 0])
|
||||
const baseSize = React.useRef(size)
|
||||
const latestPropSize = React.useRef(size)
|
||||
|
||||
React.useEffect(() => {
|
||||
latestPropSize.current = size
|
||||
}, [size])
|
||||
|
||||
React.useEffect(() => {
|
||||
const onPointerMove = ({ clientX, clientY }: PointerEvent) => {
|
||||
if (!pointerDown.current) return
|
||||
pointerMoved.current = true
|
||||
const [x, y] = baseSize.current
|
||||
onResize([x + clientX - initialSizeRef.current[0], y + clientY - initialSizeRef.current[1]])
|
||||
}
|
||||
window.addEventListener('pointermove', onPointerMove)
|
||||
return () => window.removeEventListener('pointermove', onPointerMove)
|
||||
}, [onResize])
|
||||
|
||||
React.useEffect(() => {
|
||||
const onPointerUp = (e: PointerEvent) => {
|
||||
if (pointerDown.current) {
|
||||
pointerDown.current = false
|
||||
|
||||
if (!pointerMoved.current) onClick?.(e)
|
||||
pointerMoved.current = false
|
||||
|
||||
baseSize.current = latestPropSize.current
|
||||
onResizeStateChange?.('idle')
|
||||
}
|
||||
}
|
||||
window.addEventListener('pointerup', onPointerUp)
|
||||
return () => window.removeEventListener('pointerup', onPointerUp)
|
||||
}, [])
|
||||
|
||||
const onPointerDown = React.useCallback(({ clientX, clientY }: React.PointerEvent) => {
|
||||
pointerDown.current = true
|
||||
initialSizeRef.current = [clientX, clientY]
|
||||
baseSize.current = latestPropSize.current
|
||||
onResizeStateChange?.('resizing')
|
||||
}, [])
|
||||
|
||||
return { onPointerDown }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { HorizontalResizeHandler } from 'components/ResizeHandler'
|
||||
import { ResizeHandler } from 'components/ResizeHandler'
|
||||
import { useConfigs } from 'containers/ConfigsContext'
|
||||
import * as React from 'react'
|
||||
import { useDebounce, useWindowSize } from 'react-use'
|
||||
|
|
@ -7,7 +7,8 @@ import { cx } from 'utils/cx'
|
|||
import { setCSSVariable } from 'utils/DOMHelper'
|
||||
import * as features from 'utils/features'
|
||||
|
||||
export type Size = number
|
||||
type Size = number
|
||||
export type Size2D = [Size, Size]
|
||||
type Props = {
|
||||
baseSize: Size
|
||||
className?: string
|
||||
|
|
@ -67,7 +68,7 @@ export function SideBarBodyWrapper({
|
|||
const onResize = React.useMemo(() => {
|
||||
let sizeToApply: number,
|
||||
applied = true
|
||||
return (size: number) => {
|
||||
return ([size]: number[]) => {
|
||||
// do NOT merge this with the above similar effect, side bar will jump otherwise
|
||||
sizeToApply = getSafeSize(size, width)
|
||||
setSize(sizeToApply)
|
||||
|
|
@ -94,6 +95,7 @@ export function SideBarBodyWrapper({
|
|||
[onLeave],
|
||||
)
|
||||
|
||||
const newLocal: [number, number] = React.useMemo(() => [size, size], [size])
|
||||
return (
|
||||
<div
|
||||
ref={bodyWrapperRef}
|
||||
|
|
@ -102,7 +104,7 @@ export function SideBarBodyWrapper({
|
|||
>
|
||||
<div className={'gitako-side-bar-body-wrapper-content'}>{children}</div>
|
||||
{features.resize && (
|
||||
<HorizontalResizeHandler
|
||||
<ResizeHandler
|
||||
onResize={onResize}
|
||||
onResetSize={() => {
|
||||
setSize(defaultConfigs.sideBarWidth)
|
||||
|
|
@ -111,7 +113,7 @@ export function SideBarBodyWrapper({
|
|||
onResizeStateChange={state => {
|
||||
blockLeaveRef.current = state === 'resizing'
|
||||
}}
|
||||
size={size}
|
||||
size={newLocal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,30 +4,31 @@ import * as React from 'react'
|
|||
import { useDebounce, useWindowSize } from 'react-use'
|
||||
import { cx } from 'utils/cx'
|
||||
import { Icon } from './Icon'
|
||||
import { useResizeHandler } from './ResizeHandler'
|
||||
|
||||
type Props = {
|
||||
error?: string | null
|
||||
className?: React.HTMLAttributes<HTMLButtonElement>['className']
|
||||
onHover?: React.HTMLAttributes<HTMLButtonElement>['onMouseEnter']
|
||||
} & Pick<React.HTMLAttributes<HTMLButtonElement>, 'onClick'>
|
||||
onClick?: (e: PointerEvent) => void
|
||||
}
|
||||
|
||||
const buttonHeight = 42
|
||||
|
||||
function getSafeDistance(y: number, height: number) {
|
||||
return Math.max(0, Math.min(y, height - buttonHeight))
|
||||
}
|
||||
|
||||
export function ToggleShowButton({ error, className, onClick, onHover }: Props) {
|
||||
const ref = React.useRef<HTMLDivElement>(null)
|
||||
const config = useConfigs()
|
||||
const [distance, setDistance] = React.useState(config.value.toggleButtonVerticalDistance)
|
||||
const { height } = useWindowSize()
|
||||
const buttonHeight = 42
|
||||
React.useEffect(() => {
|
||||
// make sure it is inside viewport
|
||||
if (height - buttonHeight < distance) {
|
||||
setDistance(Math.max(0, height - buttonHeight))
|
||||
}
|
||||
const safeDistance = getSafeDistance(distance, height)
|
||||
if (safeDistance !== distance) setDistance(safeDistance)
|
||||
}, [height, distance])
|
||||
React.useLayoutEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.style.top = distance + 'px'
|
||||
}
|
||||
}, [distance])
|
||||
|
||||
// updating context
|
||||
useDebounce(
|
||||
|
|
@ -36,28 +37,34 @@ export function ToggleShowButton({ error, className, onClick, onHover }: Props)
|
|||
[distance],
|
||||
)
|
||||
|
||||
const toggleIconMode = config.value.toggleButtonContent
|
||||
React.useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.style.top = distance + 'px'
|
||||
}
|
||||
}, [])
|
||||
const { onPointerDown } = useResizeHandler(
|
||||
[distance, distance],
|
||||
([, y]) => {
|
||||
const distance = getSafeDistance(y, height)
|
||||
setDistance(distance)
|
||||
if (ref.current) {
|
||||
ref.current.style.top = distance + 'px'
|
||||
}
|
||||
},
|
||||
{ onClick },
|
||||
)
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cx('gitako-toggle-show-button-wrapper', className)}>
|
||||
<button
|
||||
className={cx('gitako-toggle-show-button', {
|
||||
error,
|
||||
})}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onHover}
|
||||
draggable
|
||||
onDragStart={event => {
|
||||
hideDragPreview(event)
|
||||
}}
|
||||
onDrag={e => {
|
||||
if (e.clientY !== 0) {
|
||||
// It will be 0 when release pointer
|
||||
setDistance(e.clientY - buttonHeight / 2)
|
||||
}
|
||||
}}
|
||||
onPointerEnter={onHover}
|
||||
onPointerDown={onPointerDown}
|
||||
title={'Gitako (draggable)'}
|
||||
>
|
||||
{toggleIconMode === 'octoface' ? (
|
||||
{config.value.toggleButtonContent === 'octoface' ? (
|
||||
<Icon className={'octoface-icon'} type={'octoface'} />
|
||||
) : (
|
||||
<img className={'tentacle'} draggable={false} src={iconURL} />
|
||||
|
|
@ -67,11 +74,3 @@ export function ToggleShowButton({ error, className, onClick, onHover }: Props)
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function hideDragPreview(event: React.DragEvent<HTMLButtonElement>) {
|
||||
const img = new Image()
|
||||
const EMPTY_IMAGE_BASE64 =
|
||||
'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='
|
||||
img.src = EMPTY_IMAGE_BASE64
|
||||
event.dataTransfer.setDragImage(img, 0, 0)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue