mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 19:26: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 { useDocumentTitleWithNoteTitle } from '../../hooks/common/use-document-title-with-note-title'
|
||||||
import { useNoteMarkdownContent } from '../../hooks/common/use-note-markdown-content'
|
import { useNoteMarkdownContent } from '../../hooks/common/use-note-markdown-content'
|
||||||
import {
|
import {
|
||||||
SetCheckboxInMarkdownContent,
|
setCheckboxInMarkdownContent,
|
||||||
setNoteFrontmatter,
|
setNoteFrontmatter,
|
||||||
setNoteMarkdownContent,
|
setNoteMarkdownContent,
|
||||||
updateNoteTitleByFirstHeading
|
updateNoteTitleByFirstHeading
|
||||||
|
@ -115,7 +115,7 @@ export const EditorPage: React.FC = () => {
|
||||||
markdownContent={markdownContent}
|
markdownContent={markdownContent}
|
||||||
onMakeScrollSource={setRendererToScrollSource}
|
onMakeScrollSource={setRendererToScrollSource}
|
||||||
onFirstHeadingChange={updateNoteTitleByFirstHeading}
|
onFirstHeadingChange={updateNoteTitleByFirstHeading}
|
||||||
onTaskCheckedChange={SetCheckboxInMarkdownContent}
|
onTaskCheckedChange={setCheckboxInMarkdownContent}
|
||||||
onFrontmatterChange={setNoteFrontmatter}
|
onFrontmatterChange={setNoteFrontmatter}
|
||||||
onScroll={onMarkdownRendererScroll}
|
onScroll={onMarkdownRendererScroll}
|
||||||
scrollState={scrollState.rendererScrollState}
|
scrollState={scrollState.rendererScrollState}
|
||||||
|
@ -142,7 +142,7 @@ export const EditorPage: React.FC = () => {
|
||||||
left={leftPane}
|
left={leftPane}
|
||||||
showRight={editorMode === EditorMode.PREVIEW || editorMode === EditorMode.BOTH}
|
showRight={editorMode === EditorMode.PREVIEW || editorMode === EditorMode.BOTH}
|
||||||
right={rightPane}
|
right={rightPane}
|
||||||
containerClassName={'overflow-hidden'}
|
additionalContainerClassName={'overflow-hidden'}
|
||||||
/>
|
/>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,14 +6,9 @@
|
||||||
|
|
||||||
.splitter {
|
.splitter {
|
||||||
&.left {
|
&.left {
|
||||||
flex: 0 1 100%;
|
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.right {
|
|
||||||
flex: 1 0 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.separator {
|
&.separator {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* 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 { ShowIf } from '../../common/show-if/show-if'
|
||||||
import { SplitDivider } from './split-divider/split-divider'
|
import { SplitDivider } from './split-divider/split-divider'
|
||||||
import './splitter.scss'
|
import './splitter.scss'
|
||||||
|
@ -12,70 +12,136 @@ import './splitter.scss'
|
||||||
export interface SplitterProps {
|
export interface SplitterProps {
|
||||||
left: ReactElement
|
left: ReactElement
|
||||||
right: ReactElement
|
right: ReactElement
|
||||||
containerClassName?: string
|
additionalContainerClassName?: string
|
||||||
showLeft: boolean
|
showLeft: boolean
|
||||||
showRight: boolean
|
showRight: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Splitter: React.FC<SplitterProps> = ({ containerClassName, left, right, showLeft, showRight }) => {
|
/**
|
||||||
const [split, setSplit] = useState(50)
|
* Checks if the given {@link Event} is a {@link MouseEvent}
|
||||||
const realSplit = Math.max(0, Math.min(100, showRight ? split : 100))
|
* @param event the event to check
|
||||||
const [doResizing, setDoResizing] = useState(false)
|
* @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 splitContainer = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const recalculateSize = (mouseXPosition: number): void => {
|
/**
|
||||||
if (!splitContainer.current) {
|
* Starts the splitter resizing
|
||||||
return
|
*/
|
||||||
}
|
const onStartResizing = useCallback(() => {
|
||||||
const x = mouseXPosition - splitContainer.current.offsetLeft
|
resizingInProgress.current = true
|
||||||
|
|
||||||
const newSize = x / splitContainer.current.clientWidth
|
|
||||||
setSplit(newSize * 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopResizing = useCallback(() => {
|
|
||||||
setDoResizing(false)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onMouseMove = useCallback(
|
/**
|
||||||
(mouseEvent: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
* Stops the splitter resizing
|
||||||
if (doResizing) {
|
*/
|
||||||
recalculateSize(mouseEvent.pageX)
|
const onStopResizing = useCallback(() => {
|
||||||
mouseEvent.preventDefault()
|
if (resizingInProgress.current) {
|
||||||
|
resizingInProgress.current = false
|
||||||
}
|
}
|
||||||
},
|
}, [])
|
||||||
[doResizing]
|
|
||||||
)
|
|
||||||
|
|
||||||
const onTouchMove = useCallback(
|
/**
|
||||||
(touchEvent: React.TouchEvent<HTMLDivElement>) => {
|
* Recalculates the panel split based on the absolute mouse/touch position.
|
||||||
if (doResizing) {
|
*
|
||||||
recalculateSize(touchEvent.touches[0].pageX)
|
* @param moveEvent is a {@link MouseEvent} or {@link TouchEvent} that got triggered.
|
||||||
touchEvent.preventDefault()
|
*/
|
||||||
|
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 (
|
return (
|
||||||
|
<div ref={splitContainer} className={`flex-fill flex-row d-flex ${additionalContainerClassName || ''}`}>
|
||||||
<div
|
<div
|
||||||
ref={splitContainer}
|
className={`splitter left ${!showLeft ? 'd-none' : ''}`}
|
||||||
className={`flex-fill flex-row d-flex ${containerClassName || ''}`}
|
style={{ width: `calc(${cappedRelativeSplitValue}% - 5px)` }}>
|
||||||
onMouseUp={stopResizing}
|
|
||||||
onTouchEnd={stopResizing}
|
|
||||||
onMouseMove={onMouseMove}
|
|
||||||
onTouchMove={onTouchMove}>
|
|
||||||
<div className={`splitter left ${!showLeft ? 'd-none' : ''}`} style={{ flexBasis: `calc(${realSplit}% - 5px)` }}>
|
|
||||||
{left}
|
{left}
|
||||||
</div>
|
</div>
|
||||||
<ShowIf condition={showLeft && showRight}>
|
<ShowIf condition={showLeft && showRight}>
|
||||||
<div className='splitter separator'>
|
<div className='splitter separator'>
|
||||||
<SplitDivider onGrab={onGrab} />
|
<SplitDivider onGrab={onStartResizing} />
|
||||||
</div>
|
</div>
|
||||||
</ShowIf>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,17 @@
|
||||||
font-family: 'Source Sans Pro', "Twemoji", sans-serif;
|
font-family: 'Source Sans Pro', "Twemoji", sans-serif;
|
||||||
word-break: break-word;
|
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 {
|
.alert > p, .alert > ul {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,5 +28,5 @@ export const AbcFrame: React.FC<AbcFrameProps> = ({ code }) => {
|
||||||
})
|
})
|
||||||
}, [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}>
|
<ShowIf condition={!!error}>
|
||||||
<Alert variant={'warning'}>{error}</Alert>
|
<Alert variant={'warning'}>{error}</Alert>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<div className={'text-center overflow-x-auto'} data-cy={'graphviz'} ref={container} />
|
<div className={'svg-container'} data-cy={'graphviz'} ref={container} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,7 @@ export const MarkmapFrame: React.FC<MarkmapFrameProps> = ({ code }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-cy={'markmap'}>
|
<div data-cy={'markmap'}>
|
||||||
<div className={'text-center'} ref={diagramContainer} />
|
<div className={'svg-container'} ref={diagramContainer} />
|
||||||
<div className={'text-right button-inside'}>
|
<div className={'text-right button-inside'}>
|
||||||
<LockButton
|
<LockButton
|
||||||
locked={disablePanAndZoom}
|
locked={disablePanAndZoom}
|
||||||
|
|
|
@ -48,7 +48,7 @@ export const setNoteFrontmatter = (frontmatter: NoteFrontmatter | undefined): vo
|
||||||
} as SetNoteFrontmatterFromRenderingAction)
|
} as SetNoteFrontmatterFromRenderingAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SetCheckboxInMarkdownContent = (lineInMarkdown: number, checked: boolean): void => {
|
export const setCheckboxInMarkdownContent = (lineInMarkdown: number, checked: boolean): void => {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: NoteDetailsActionType.SET_CHECKBOX_IN_MARKDOWN_CONTENT,
|
type: NoteDetailsActionType.SET_CHECKBOX_IN_MARKDOWN_CONTENT,
|
||||||
checked: checked,
|
checked: checked,
|
||||||
|
|
Loading…
Reference in a new issue