Fix splitter (#1307)

Use window for splitter resizing

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2021-06-12 16:20:11 +02:00 committed by GitHub
parent 6285af458a
commit 4580bc9658
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 131 additions and 59 deletions

View file

@ -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>

View file

@ -6,14 +6,9 @@
.splitter {
&.left {
flex: 0 1 100%;
min-width: 200px;
}
&.right {
flex: 1 0 200px;
}
&.separator {
display: flex;
}

View file

@ -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()
}
},
[doResizing]
)
/**
* Stops the splitter resizing
*/
const onStopResizing = useCallback(() => {
if (resizingInProgress.current) {
resizingInProgress.current = false
}
}, [])
const onTouchMove = useCallback(
(touchEvent: React.TouchEvent<HTMLDivElement>) => {
if (doResizing) {
recalculateSize(touchEvent.touches[0].pageX)
touchEvent.preventDefault()
}
},
[doResizing]
)
/**
* 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
}
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 ${containerClassName || ''}`}
onMouseUp={stopResizing}
onTouchEnd={stopResizing}
onMouseMove={onMouseMove}
onTouchMove={onTouchMove}>
<div className={`splitter left ${!showLeft ? 'd-none' : ''}`} style={{ flexBasis: `calc(${realSplit}% - 5px)` }}>
<div ref={splitContainer} className={`flex-fill flex-row d-flex ${additionalContainerClassName || ''}`}>
<div
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>
)
}

View file

@ -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;
}

View file

@ -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'} />
}

View file

@ -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>
)
}

View file

@ -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}

View file

@ -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,