From 4580bc9658f0c129a4999e44cf1804494a930077 Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Sat, 12 Jun 2021 16:20:11 +0200 Subject: [PATCH] Fix splitter (#1307) Use window for splitter resizing Signed-off-by: Tilman Vatteroth --- src/components/editor-page/editor-page.tsx | 6 +- .../editor-page/splitter/splitter.scss | 5 - .../editor-page/splitter/splitter.tsx | 160 +++++++++++++----- .../markdown-renderer/markdown-renderer.scss | 11 ++ .../replace-components/abc/abc-frame.tsx | 2 +- .../graphviz/graphviz-frame.tsx | 2 +- .../markmap/markmap-frame.tsx | 2 +- src/redux/note-details/methods.ts | 2 +- 8 files changed, 131 insertions(+), 59 deletions(-) diff --git a/src/components/editor-page/editor-page.tsx b/src/components/editor-page/editor-page.tsx index 8755a6441..4b9a7382a 100644 --- a/src/components/editor-page/editor-page.tsx +++ b/src/components/editor-page/editor-page.tsx @@ -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'} /> diff --git a/src/components/editor-page/splitter/splitter.scss b/src/components/editor-page/splitter/splitter.scss index b44b8d7d0..2af6e0756 100644 --- a/src/components/editor-page/splitter/splitter.scss +++ b/src/components/editor-page/splitter/splitter.scss @@ -6,14 +6,9 @@ .splitter { &.left { - flex: 0 1 100%; min-width: 200px; } - &.right { - flex: 1 0 200px; - } - &.separator { display: flex; } diff --git a/src/components/editor-page/splitter/splitter.tsx b/src/components/editor-page/splitter/splitter.tsx index ae70ed9be..625775843 100644 --- a/src/components/editor-page/splitter/splitter.tsx +++ b/src/components/editor-page/splitter/splitter.tsx @@ -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 = ({ 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 = ({ + 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(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) => { - 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) => { - 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 ( -
-
+
+
{left}
- +
-
{right}
+
+ {right} +
) } diff --git a/src/components/markdown-renderer/markdown-renderer.scss b/src/components/markdown-renderer/markdown-renderer.scss index 786c81a5b..df8df79c7 100644 --- a/src/components/markdown-renderer/markdown-renderer.scss +++ b/src/components/markdown-renderer/markdown-renderer.scss @@ -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; } diff --git a/src/components/markdown-renderer/replace-components/abc/abc-frame.tsx b/src/components/markdown-renderer/replace-components/abc/abc-frame.tsx index 81eb2b5f6..9af1585cc 100644 --- a/src/components/markdown-renderer/replace-components/abc/abc-frame.tsx +++ b/src/components/markdown-renderer/replace-components/abc/abc-frame.tsx @@ -28,5 +28,5 @@ export const AbcFrame: React.FC = ({ code }) => { }) }, [code]) - return
+ return
} diff --git a/src/components/markdown-renderer/replace-components/graphviz/graphviz-frame.tsx b/src/components/markdown-renderer/replace-components/graphviz/graphviz-frame.tsx index a04dd40e4..b8376bbe7 100644 --- a/src/components/markdown-renderer/replace-components/graphviz/graphviz-frame.tsx +++ b/src/components/markdown-renderer/replace-components/graphviz/graphviz-frame.tsx @@ -63,7 +63,7 @@ export const GraphvizFrame: React.FC = ({ code }) => { {error} -
+
) } diff --git a/src/components/markdown-renderer/replace-components/markmap/markmap-frame.tsx b/src/components/markdown-renderer/replace-components/markmap/markmap-frame.tsx index 85c0a13e4..bb3e0551d 100644 --- a/src/components/markdown-renderer/replace-components/markmap/markmap-frame.tsx +++ b/src/components/markdown-renderer/replace-components/markmap/markmap-frame.tsx @@ -64,7 +64,7 @@ export const MarkmapFrame: React.FC = ({ code }) => { return (
-
+
{ +export const setCheckboxInMarkdownContent = (lineInMarkdown: number, checked: boolean): void => { store.dispatch({ type: NoteDetailsActionType.SET_CHECKBOX_IN_MARKDOWN_CONTENT, checked: checked,