mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 11:16:31 -05:00
Fix splitter (#1307)
Use window for splitter resizing Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
6285af458a
commit
4580bc9658
8 changed files with 131 additions and 59 deletions
|
@ -10,7 +10,7 @@ import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
|
|||
import { useDocumentTitleWithNoteTitle } from '../../hooks/common/use-document-title-with-note-title'
|
||||
import { useNoteMarkdownContent } from '../../hooks/common/use-note-markdown-content'
|
||||
import {
|
||||
SetCheckboxInMarkdownContent,
|
||||
setCheckboxInMarkdownContent,
|
||||
setNoteFrontmatter,
|
||||
setNoteMarkdownContent,
|
||||
updateNoteTitleByFirstHeading
|
||||
|
@ -115,7 +115,7 @@ export const EditorPage: React.FC = () => {
|
|||
markdownContent={markdownContent}
|
||||
onMakeScrollSource={setRendererToScrollSource}
|
||||
onFirstHeadingChange={updateNoteTitleByFirstHeading}
|
||||
onTaskCheckedChange={SetCheckboxInMarkdownContent}
|
||||
onTaskCheckedChange={setCheckboxInMarkdownContent}
|
||||
onFrontmatterChange={setNoteFrontmatter}
|
||||
onScroll={onMarkdownRendererScroll}
|
||||
scrollState={scrollState.rendererScrollState}
|
||||
|
@ -142,7 +142,7 @@ export const EditorPage: React.FC = () => {
|
|||
left={leftPane}
|
||||
showRight={editorMode === EditorMode.PREVIEW || editorMode === EditorMode.BOTH}
|
||||
right={rightPane}
|
||||
containerClassName={'overflow-hidden'}
|
||||
additionalContainerClassName={'overflow-hidden'}
|
||||
/>
|
||||
<Sidebar />
|
||||
</div>
|
||||
|
|
|
@ -6,14 +6,9 @@
|
|||
|
||||
.splitter {
|
||||
&.left {
|
||||
flex: 0 1 100%;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
&.right {
|
||||
flex: 1 0 200px;
|
||||
}
|
||||
|
||||
&.separator {
|
||||
display: flex;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { ReactElement, useCallback, useRef, useState } from 'react'
|
||||
import React, { ReactElement, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { ShowIf } from '../../common/show-if/show-if'
|
||||
import { SplitDivider } from './split-divider/split-divider'
|
||||
import './splitter.scss'
|
||||
|
@ -12,70 +12,136 @@ import './splitter.scss'
|
|||
export interface SplitterProps {
|
||||
left: ReactElement
|
||||
right: ReactElement
|
||||
containerClassName?: string
|
||||
additionalContainerClassName?: string
|
||||
showLeft: boolean
|
||||
showRight: boolean
|
||||
}
|
||||
|
||||
export const Splitter: React.FC<SplitterProps> = ({ containerClassName, left, right, showLeft, showRight }) => {
|
||||
const [split, setSplit] = useState(50)
|
||||
const realSplit = Math.max(0, Math.min(100, showRight ? split : 100))
|
||||
const [doResizing, setDoResizing] = useState(false)
|
||||
/**
|
||||
* Checks if the given {@link Event} is a {@link MouseEvent}
|
||||
* @param event the event to check
|
||||
* @return {@code true} if the given event is a {@link MouseEvent}
|
||||
*/
|
||||
const isMouseEvent = (event: Event): event is MouseEvent => {
|
||||
return (event as MouseEvent).buttons !== undefined
|
||||
}
|
||||
|
||||
const isLeftMouseButtonClicked = (mouseEvent: MouseEvent): boolean => {
|
||||
return mouseEvent.buttons === 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the absolute horizontal position of the mouse or touch point from the event.
|
||||
* If no position could be found or
|
||||
*
|
||||
* @param moveEvent
|
||||
*/
|
||||
const extractHorizontalPosition = (moveEvent: MouseEvent | TouchEvent): number => {
|
||||
if (isMouseEvent(moveEvent)) {
|
||||
return moveEvent.pageX
|
||||
} else {
|
||||
return moveEvent.touches[0]?.pageX
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Left/Right splitter react component.
|
||||
*
|
||||
* @param additionalContainerClassName css classes that are added to the split container.
|
||||
* @param left the react component that should be shown on the left side.
|
||||
* @param right the react component that should be shown on the right side.
|
||||
* @param showLeft defines if the left component should be shown or hidden. Settings this prop will hide the component with css.
|
||||
* @param showRight defines if the right component should be shown or hidden. Settings this prop will hide the component with css.
|
||||
* @return the created component
|
||||
*/
|
||||
export const Splitter: React.FC<SplitterProps> = ({
|
||||
additionalContainerClassName,
|
||||
left,
|
||||
right,
|
||||
showLeft,
|
||||
showRight
|
||||
}) => {
|
||||
const [relativeSplitValue, setRelativeSplitValue] = useState(50)
|
||||
const cappedRelativeSplitValue = Math.max(0, Math.min(100, showRight ? relativeSplitValue : 100))
|
||||
const resizingInProgress = useRef(false)
|
||||
const splitContainer = useRef<HTMLDivElement>(null)
|
||||
|
||||
const recalculateSize = (mouseXPosition: number): void => {
|
||||
if (!splitContainer.current) {
|
||||
return
|
||||
}
|
||||
const x = mouseXPosition - splitContainer.current.offsetLeft
|
||||
|
||||
const newSize = x / splitContainer.current.clientWidth
|
||||
setSplit(newSize * 100)
|
||||
}
|
||||
|
||||
const stopResizing = useCallback(() => {
|
||||
setDoResizing(false)
|
||||
/**
|
||||
* Starts the splitter resizing
|
||||
*/
|
||||
const onStartResizing = useCallback(() => {
|
||||
resizingInProgress.current = true
|
||||
}, [])
|
||||
|
||||
const onMouseMove = useCallback(
|
||||
(mouseEvent: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
if (doResizing) {
|
||||
recalculateSize(mouseEvent.pageX)
|
||||
mouseEvent.preventDefault()
|
||||
/**
|
||||
* Stops the splitter resizing
|
||||
*/
|
||||
const onStopResizing = useCallback(() => {
|
||||
if (resizingInProgress.current) {
|
||||
resizingInProgress.current = false
|
||||
}
|
||||
},
|
||||
[doResizing]
|
||||
)
|
||||
}, [])
|
||||
|
||||
const onTouchMove = useCallback(
|
||||
(touchEvent: React.TouchEvent<HTMLDivElement>) => {
|
||||
if (doResizing) {
|
||||
recalculateSize(touchEvent.touches[0].pageX)
|
||||
touchEvent.preventDefault()
|
||||
/**
|
||||
* Recalculates the panel split based on the absolute mouse/touch position.
|
||||
*
|
||||
* @param moveEvent is a {@link MouseEvent} or {@link TouchEvent} that got triggered.
|
||||
*/
|
||||
const onMove = useCallback((moveEvent: MouseEvent | TouchEvent) => {
|
||||
if (!resizingInProgress.current || !splitContainer.current) {
|
||||
return
|
||||
}
|
||||
if (isMouseEvent(moveEvent) && !isLeftMouseButtonClicked(moveEvent)) {
|
||||
resizingInProgress.current = false
|
||||
moveEvent.preventDefault()
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
[doResizing]
|
||||
)
|
||||
|
||||
const onGrab = useCallback(() => setDoResizing(true), [])
|
||||
const horizontalPosition = extractHorizontalPosition(moveEvent)
|
||||
const horizontalPositionInSplitContainer = horizontalPosition - splitContainer.current.offsetLeft
|
||||
const newRelativeSize = horizontalPositionInSplitContainer / splitContainer.current.clientWidth
|
||||
setRelativeSplitValue(newRelativeSize * 100)
|
||||
moveEvent.preventDefault()
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Registers and unregisters necessary event listeners on the body so you can use the split even if the mouse isn't moving over it.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const moveHandler = onMove
|
||||
const stopResizeHandler = onStopResizing
|
||||
window.addEventListener('touchmove', moveHandler)
|
||||
window.addEventListener('mousemove', moveHandler)
|
||||
window.addEventListener('touchcancel', stopResizeHandler)
|
||||
window.addEventListener('touchend', stopResizeHandler)
|
||||
window.addEventListener('mouseup', stopResizeHandler)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('touchmove', moveHandler)
|
||||
window.removeEventListener('mousemove', moveHandler)
|
||||
window.removeEventListener('touchcancel', stopResizeHandler)
|
||||
window.removeEventListener('touchend', stopResizeHandler)
|
||||
window.removeEventListener('mouseup', stopResizeHandler)
|
||||
}
|
||||
}, [resizingInProgress, onMove, onStopResizing])
|
||||
|
||||
return (
|
||||
<div ref={splitContainer} className={`flex-fill flex-row d-flex ${additionalContainerClassName || ''}`}>
|
||||
<div
|
||||
ref={splitContainer}
|
||||
className={`flex-fill flex-row d-flex ${containerClassName || ''}`}
|
||||
onMouseUp={stopResizing}
|
||||
onTouchEnd={stopResizing}
|
||||
onMouseMove={onMouseMove}
|
||||
onTouchMove={onTouchMove}>
|
||||
<div className={`splitter left ${!showLeft ? 'd-none' : ''}`} style={{ flexBasis: `calc(${realSplit}% - 5px)` }}>
|
||||
className={`splitter left ${!showLeft ? 'd-none' : ''}`}
|
||||
style={{ width: `calc(${cappedRelativeSplitValue}% - 5px)` }}>
|
||||
{left}
|
||||
</div>
|
||||
<ShowIf condition={showLeft && showRight}>
|
||||
<div className='splitter separator'>
|
||||
<SplitDivider onGrab={onGrab} />
|
||||
<SplitDivider onGrab={onStartResizing} />
|
||||
</div>
|
||||
</ShowIf>
|
||||
<div className={`splitter right ${!showRight ? 'd-none' : ''}`}>{right}</div>
|
||||
<div
|
||||
className={`splitter right ${!showRight ? 'd-none' : ''}`}
|
||||
style={{ width: `calc(100% - ${cappedRelativeSplitValue}%)` }}>
|
||||
{right}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,6 +11,17 @@
|
|||
font-family: 'Source Sans Pro', "Twemoji", sans-serif;
|
||||
word-break: break-word;
|
||||
|
||||
.svg-container {
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
|
||||
svg {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.alert > p, .alert > ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
|
|
@ -28,5 +28,5 @@ export const AbcFrame: React.FC<AbcFrameProps> = ({ code }) => {
|
|||
})
|
||||
}, [code])
|
||||
|
||||
return <div ref={container} className={'abcjs-score bg-white text-black text-center overflow-x-auto'} />
|
||||
return <div ref={container} className={'abcjs-score bg-white text-black svg-container'} />
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ export const GraphvizFrame: React.FC<GraphvizFrameProps> = ({ code }) => {
|
|||
<ShowIf condition={!!error}>
|
||||
<Alert variant={'warning'}>{error}</Alert>
|
||||
</ShowIf>
|
||||
<div className={'text-center overflow-x-auto'} data-cy={'graphviz'} ref={container} />
|
||||
<div className={'svg-container'} data-cy={'graphviz'} ref={container} />
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@ export const MarkmapFrame: React.FC<MarkmapFrameProps> = ({ code }) => {
|
|||
|
||||
return (
|
||||
<div data-cy={'markmap'}>
|
||||
<div className={'text-center'} ref={diagramContainer} />
|
||||
<div className={'svg-container'} ref={diagramContainer} />
|
||||
<div className={'text-right button-inside'}>
|
||||
<LockButton
|
||||
locked={disablePanAndZoom}
|
||||
|
|
|
@ -48,7 +48,7 @@ export const setNoteFrontmatter = (frontmatter: NoteFrontmatter | undefined): vo
|
|||
} as SetNoteFrontmatterFromRenderingAction)
|
||||
}
|
||||
|
||||
export const SetCheckboxInMarkdownContent = (lineInMarkdown: number, checked: boolean): void => {
|
||||
export const setCheckboxInMarkdownContent = (lineInMarkdown: number, checked: boolean): void => {
|
||||
store.dispatch({
|
||||
type: NoteDetailsActionType.SET_CHECKBOX_IN_MARKDOWN_CONTENT,
|
||||
checked: checked,
|
||||
|
|
Loading…
Reference in a new issue