diff --git a/src/components/editor-page/editor-pane/editor-pane.tsx b/src/components/editor-page/editor-pane/editor-pane.tsx index 8dd721d3e..f163d33f4 100644 --- a/src/components/editor-page/editor-pane/editor-pane.tsx +++ b/src/components/editor-page/editor-pane/editor-pane.tsx @@ -4,23 +4,24 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Editor, EditorChange, EditorConfiguration, ScrollInfo } from 'codemirror' -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { Editor, EditorChange } from 'codemirror' +import React, { useCallback, useRef, useState } from 'react' import { Controlled as ControlledCodeMirror } from 'react-codemirror2' -import { useTranslation } from 'react-i18next' import { MaxLengthWarningModal } from '../editor-modals/max-length-warning-modal' -import { ScrollProps, ScrollState } from '../synced-scroll/scroll-props' +import { ScrollProps } from '../synced-scroll/scroll-props' import { allHinters, findWordAtCursor } from './autocompletion' import './editor-pane.scss' -import { defaultKeyMap } from './key-map' import { createStatusInfo, defaultState, StatusBar, StatusBarInfo } from './status-bar/status-bar' import { ToolBar } from './tool-bar/tool-bar' -import { handleUpload } from './upload-handler' -import { handleFilePaste, handleTablePaste, PasteEvent } from './tool-bar/utils/pasteHandlers' import { useApplicationState } from '../../../hooks/common/use-application-state' import './codemirror-imports' import { setNoteContent } from '../../../redux/note-details/methods' import { useNoteMarkdownContent } from '../../../hooks/common/use-note-markdown-content' +import { useCodeMirrorOptions } from './hooks/use-code-mirror-options' +import { useOnEditorPasteCallback } from './hooks/use-on-editor-paste-callback' +import { useOnEditorFileDrop } from './hooks/use-on-editor-file-drop' +import { useOnEditorScroll } from './hooks/use-on-editor-scroll' +import { useApplyScrollState } from './hooks/use-apply-scroll-state' const onChange = (editor: Editor) => { for (const hinter of allHinters) { @@ -37,79 +38,18 @@ const onChange = (editor: Editor) => { } } -interface DropEvent { - pageX: number - pageY: number - dataTransfer: { - files: FileList - effectAllowed: string - } | null - preventDefault: () => void -} - export const EditorPane: React.FC = ({ scrollState, onScroll, onMakeScrollSource }) => { const markdownContent = useNoteMarkdownContent() - const { t } = useTranslation() const maxLength = useApplicationState((state) => state.config.maxDocumentLength) - const smartPasteEnabled = useApplicationState((state) => state.editorConfig.smartPaste) const [showMaxLengthWarning, setShowMaxLengthWarning] = useState(false) const maxLengthWarningAlreadyShown = useRef(false) const [editor, setEditor] = useState() const [statusBarInfo, setStatusBarInfo] = useState(defaultState) - const editorPreferences = useApplicationState((state) => state.editorConfig.preferences) const ligaturesEnabled = useApplicationState((state) => state.editorConfig.ligatures) - const lastScrollPosition = useRef() - const [editorScroll, setEditorScroll] = useState() - const onEditorScroll = useCallback((editor: Editor, data: ScrollInfo) => setEditorScroll(data), []) - - const onPaste = useCallback( - (pasteEditor: Editor, event: PasteEvent) => { - if (!event || !event.clipboardData) { - return - } - if (smartPasteEnabled) { - const tableInserted = handleTablePaste(event, pasteEditor) - if (tableInserted) { - return - } - } - handleFilePaste(event, pasteEditor) - }, - [smartPasteEnabled] - ) - - useEffect(() => { - if (!editor || !onScroll || !editorScroll) { - return - } - const line = editor.lineAtHeight(editorScroll.top, 'local') - const startYOfLine = editor.heightAtLine(line, 'local') - const lineInfo = editor.lineInfo(line) - if (lineInfo === null) { - return - } - const heightOfLine = (lineInfo.handle as { height: number }).height - const percentageRaw = Math.max(editorScroll.top - startYOfLine, 0) / heightOfLine - const percentage = Math.floor(percentageRaw * 100) - - const newScrollState: ScrollState = { firstLineInView: line + 1, scrolledPercentage: percentage } - onScroll(newScrollState) - }, [editor, editorScroll, onScroll]) - - useEffect(() => { - if (!editor || !scrollState) { - return - } - const startYOfLine = editor.heightAtLine(scrollState.firstLineInView - 1, 'local') - const heightOfLine = (editor.lineInfo(scrollState.firstLineInView - 1).handle as { height: number }).height - const newPositionRaw = startYOfLine + (heightOfLine * scrollState.scrolledPercentage) / 100 - const newPosition = Math.floor(newPositionRaw) - if (newPosition !== lastScrollPosition.current) { - lastScrollPosition.current = newPosition - editor.scrollTo(0, newPosition) - } - }, [editor, scrollState]) + const onPaste = useOnEditorPasteCallback() + const onEditorScroll = useOnEditorScroll(onScroll) + useApplyScrollState(editor, scrollState) const onBeforeChange = useCallback( (editor: Editor, data: EditorChange, value: string) => { @@ -140,56 +80,9 @@ export const EditorPane: React.FC = ({ scrollState, onScroll, onMak [maxLength] ) - const onDrop = useCallback((dropEditor: Editor, event: DropEvent) => { - if ( - event && - dropEditor && - event.pageX && - event.pageY && - event.dataTransfer && - event.dataTransfer.files && - event.dataTransfer.files.length >= 1 - ) { - event.preventDefault() - const top: number = event.pageY - const left: number = event.pageX - const newCursor = dropEditor.coordsChar({ top, left }, 'page') - dropEditor.setCursor(newCursor) - const files: FileList = event.dataTransfer.files - handleUpload(files[0], dropEditor) - } - }, []) - + const onDrop = useOnEditorFileDrop() const onMaxLengthHide = useCallback(() => setShowMaxLengthWarning(false), []) - - const codeMirrorOptions: EditorConfiguration = useMemo( - () => ({ - ...editorPreferences, - mode: 'gfm', - viewportMargin: 20, - styleActiveLine: true, - lineNumbers: true, - lineWrapping: true, - showCursorWhenSelecting: true, - highlightSelectionMatches: true, - inputStyle: 'textarea', - matchBrackets: true, - autoCloseBrackets: true, - matchTags: { - bothTags: true - }, - autoCloseTags: true, - foldGutter: true, - gutters: ['CodeMirror-linenumbers', 'authorship-gutters', 'CodeMirror-foldgutter'], - extraKeys: defaultKeyMap, - flattenSpans: true, - addModeClass: true, - autoRefresh: true, - // otherCursors: true, - placeholder: t('editor.placeholder') - }), - [t, editorPreferences] - ) + const codeMirrorOptions = useCodeMirrorOptions() return (
diff --git a/src/components/editor-page/editor-pane/hooks/use-apply-scroll-state.ts b/src/components/editor-page/editor-pane/hooks/use-apply-scroll-state.ts new file mode 100644 index 000000000..243b0e637 --- /dev/null +++ b/src/components/editor-page/editor-pane/hooks/use-apply-scroll-state.ts @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { useEffect, useRef } from 'react' +import { Editor } from 'codemirror' +import { ScrollState } from '../../synced-scroll/scroll-props' + +/** + * Monitors the given scroll state and scrolls the editor to the state if changed. + * + * @param editor The editor that should be manipulated + * @param scrollState The scroll state that should be monitored + */ +export const useApplyScrollState = (editor?: Editor, scrollState?: ScrollState): void => { + const lastScrollPosition = useRef() + useEffect(() => { + if (!editor || !scrollState) { + return + } + const startYOfLine = editor.heightAtLine(scrollState.firstLineInView - 1, 'local') + const heightOfLine = (editor.lineInfo(scrollState.firstLineInView - 1).handle as { height: number }).height + const newPositionRaw = startYOfLine + (heightOfLine * scrollState.scrolledPercentage) / 100 + const newPosition = Math.floor(newPositionRaw) + if (newPosition !== lastScrollPosition.current) { + lastScrollPosition.current = newPosition + editor.scrollTo(0, newPosition) + } + }, [editor, scrollState]) +} diff --git a/src/components/editor-page/editor-pane/hooks/use-code-mirror-options.ts b/src/components/editor-page/editor-pane/hooks/use-code-mirror-options.ts new file mode 100644 index 000000000..2452e7489 --- /dev/null +++ b/src/components/editor-page/editor-pane/hooks/use-code-mirror-options.ts @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { EditorConfiguration } from 'codemirror' +import { useMemo } from 'react' +import { defaultKeyMap } from '../key-map' +import { useApplicationState } from '../../../../hooks/common/use-application-state' +import { useTranslation } from 'react-i18next' + +/** + * Generates the configuration for a CodeMirror instance. + */ +export const useCodeMirrorOptions = (): EditorConfiguration => { + const editorPreferences = useApplicationState((state) => state.editorConfig.preferences) + const { t } = useTranslation() + + return useMemo( + () => ({ + ...editorPreferences, + mode: 'gfm', + viewportMargin: 20, + styleActiveLine: true, + lineNumbers: true, + lineWrapping: true, + showCursorWhenSelecting: true, + highlightSelectionMatches: true, + inputStyle: 'textarea', + matchBrackets: true, + autoCloseBrackets: true, + matchTags: { + bothTags: true + }, + autoCloseTags: true, + foldGutter: true, + gutters: ['CodeMirror-linenumbers', 'authorship-gutters', 'CodeMirror-foldgutter'], + extraKeys: defaultKeyMap, + flattenSpans: true, + addModeClass: true, + autoRefresh: true, + // otherCursors: true, + placeholder: t('editor.placeholder') + }), + [t, editorPreferences] + ) +} diff --git a/src/components/editor-page/editor-pane/hooks/use-on-editor-file-drop.ts b/src/components/editor-page/editor-pane/hooks/use-on-editor-file-drop.ts new file mode 100644 index 000000000..052de4e82 --- /dev/null +++ b/src/components/editor-page/editor-pane/hooks/use-on-editor-file-drop.ts @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { useCallback } from 'react' +import { Editor } from 'codemirror' +import { handleUpload } from '../upload-handler' +import { DomEvent } from 'react-codemirror2' + +interface DropEvent { + pageX: number + pageY: number + dataTransfer: { + files: FileList + effectAllowed: string + } | null + preventDefault: () => void +} + +/** + * Creates a callback that is used to process file drops on the code mirror editor + * + * @return the code mirror callback + */ +export const useOnEditorFileDrop = (): DomEvent => { + return useCallback((dropEditor: Editor, event: DropEvent) => { + if ( + event && + dropEditor && + event.pageX && + event.pageY && + event.dataTransfer && + event.dataTransfer.files && + event.dataTransfer.files.length >= 1 + ) { + event.preventDefault() + const newCursor = dropEditor.coordsChar({ top: event.pageY, left: event.pageX }, 'page') + dropEditor.setCursor(newCursor) + const files: FileList = event.dataTransfer.files + handleUpload(files[0], dropEditor) + } + }, []) +} diff --git a/src/components/editor-page/editor-pane/hooks/use-on-editor-paste-callback.ts b/src/components/editor-page/editor-pane/hooks/use-on-editor-paste-callback.ts new file mode 100644 index 000000000..8f5f7b68c --- /dev/null +++ b/src/components/editor-page/editor-pane/hooks/use-on-editor-paste-callback.ts @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { useCallback } from 'react' +import { Editor } from 'codemirror' +import { handleFilePaste, handleTablePaste, PasteEvent } from '../tool-bar/utils/pasteHandlers' +import { useApplicationState } from '../../../../hooks/common/use-application-state' +import { DomEvent } from 'react-codemirror2' + +/** + * Creates a callback that handles the table or file paste action in code mirror. + * + * @return the created callback + */ +export const useOnEditorPasteCallback = (): DomEvent => { + const smartPasteEnabled = useApplicationState((state) => state.editorConfig.smartPaste) + + return useCallback( + (pasteEditor: Editor, event: PasteEvent) => { + if (!event || !event.clipboardData) { + return + } + if (smartPasteEnabled && handleTablePaste(event, pasteEditor)) { + return + } + handleFilePaste(event, pasteEditor) + }, + [smartPasteEnabled] + ) +} diff --git a/src/components/editor-page/editor-pane/hooks/use-on-editor-scroll.ts b/src/components/editor-page/editor-pane/hooks/use-on-editor-scroll.ts new file mode 100644 index 000000000..3d1a38464 --- /dev/null +++ b/src/components/editor-page/editor-pane/hooks/use-on-editor-scroll.ts @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { DomEvent } from 'react-codemirror2' +import { useCallback, useEffect, useState } from 'react' +import { Editor, ScrollInfo } from 'codemirror' +import { ScrollState } from '../../synced-scroll/scroll-props' + +/** + * Creates a callback for the scroll binding of the code mirror editor. + * It calculates a {@link ScrollState} and posts it on change. + * + * @param onScroll The callback that is used to post the {@link ScrolLState}. + * @return The callback for the code mirror scroll binding. + */ +export const useOnEditorScroll = (onScroll?: (scrollState: ScrollState) => void): DomEvent => { + const [editorScrollState, setEditorScrollState] = useState() + + useEffect(() => { + if (onScroll && editorScrollState) { + onScroll(editorScrollState) + } + }, [editorScrollState, onScroll]) + + return useCallback( + (editor: Editor, scrollInfo: ScrollInfo) => { + if (!editor || !onScroll || !scrollInfo) { + return + } + const line = editor.lineAtHeight(scrollInfo.top, 'local') + const startYOfLine = editor.heightAtLine(line, 'local') + const lineInfo = editor.lineInfo(line) + if (lineInfo === null) { + return + } + const heightOfLine = (lineInfo.handle as { height: number }).height + const percentageRaw = Math.max(scrollInfo.top - startYOfLine, 0) / heightOfLine + const percentage = Math.floor(percentageRaw * 100) + + setEditorScrollState({ firstLineInView: line + 1, scrolledPercentage: percentage }) + }, + [onScroll] + ) +}