mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-20 10:15:17 -04:00
Extract editor code into hooks (#1531)
* Extract code into hooks Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
73352a7b08
commit
6adb63967b
6 changed files with 218 additions and 120 deletions
|
@ -4,23 +4,24 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Editor, EditorChange, EditorConfiguration, ScrollInfo } from 'codemirror'
|
import { Editor, EditorChange } from 'codemirror'
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useRef, useState } from 'react'
|
||||||
import { Controlled as ControlledCodeMirror } from 'react-codemirror2'
|
import { Controlled as ControlledCodeMirror } from 'react-codemirror2'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { MaxLengthWarningModal } from '../editor-modals/max-length-warning-modal'
|
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 { allHinters, findWordAtCursor } from './autocompletion'
|
||||||
import './editor-pane.scss'
|
import './editor-pane.scss'
|
||||||
import { defaultKeyMap } from './key-map'
|
|
||||||
import { createStatusInfo, defaultState, StatusBar, StatusBarInfo } from './status-bar/status-bar'
|
import { createStatusInfo, defaultState, StatusBar, StatusBarInfo } from './status-bar/status-bar'
|
||||||
import { ToolBar } from './tool-bar/tool-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 { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
import './codemirror-imports'
|
import './codemirror-imports'
|
||||||
import { setNoteContent } from '../../../redux/note-details/methods'
|
import { setNoteContent } from '../../../redux/note-details/methods'
|
||||||
import { useNoteMarkdownContent } from '../../../hooks/common/use-note-markdown-content'
|
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) => {
|
const onChange = (editor: Editor) => {
|
||||||
for (const hinter of allHinters) {
|
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<ScrollProps> = ({ scrollState, onScroll, onMakeScrollSource }) => {
|
export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMakeScrollSource }) => {
|
||||||
const markdownContent = useNoteMarkdownContent()
|
const markdownContent = useNoteMarkdownContent()
|
||||||
const { t } = useTranslation()
|
|
||||||
const maxLength = useApplicationState((state) => state.config.maxDocumentLength)
|
const maxLength = useApplicationState((state) => state.config.maxDocumentLength)
|
||||||
const smartPasteEnabled = useApplicationState((state) => state.editorConfig.smartPaste)
|
|
||||||
const [showMaxLengthWarning, setShowMaxLengthWarning] = useState(false)
|
const [showMaxLengthWarning, setShowMaxLengthWarning] = useState(false)
|
||||||
const maxLengthWarningAlreadyShown = useRef(false)
|
const maxLengthWarningAlreadyShown = useRef(false)
|
||||||
const [editor, setEditor] = useState<Editor>()
|
const [editor, setEditor] = useState<Editor>()
|
||||||
const [statusBarInfo, setStatusBarInfo] = useState<StatusBarInfo>(defaultState)
|
const [statusBarInfo, setStatusBarInfo] = useState<StatusBarInfo>(defaultState)
|
||||||
const editorPreferences = useApplicationState((state) => state.editorConfig.preferences)
|
|
||||||
const ligaturesEnabled = useApplicationState((state) => state.editorConfig.ligatures)
|
const ligaturesEnabled = useApplicationState((state) => state.editorConfig.ligatures)
|
||||||
|
|
||||||
const lastScrollPosition = useRef<number>()
|
const onPaste = useOnEditorPasteCallback()
|
||||||
const [editorScroll, setEditorScroll] = useState<ScrollInfo>()
|
const onEditorScroll = useOnEditorScroll(onScroll)
|
||||||
const onEditorScroll = useCallback((editor: Editor, data: ScrollInfo) => setEditorScroll(data), [])
|
useApplyScrollState(editor, scrollState)
|
||||||
|
|
||||||
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 onBeforeChange = useCallback(
|
const onBeforeChange = useCallback(
|
||||||
(editor: Editor, data: EditorChange, value: string) => {
|
(editor: Editor, data: EditorChange, value: string) => {
|
||||||
|
@ -140,56 +80,9 @@ export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMak
|
||||||
[maxLength]
|
[maxLength]
|
||||||
)
|
)
|
||||||
|
|
||||||
const onDrop = useCallback((dropEditor: Editor, event: DropEvent) => {
|
const onDrop = useOnEditorFileDrop()
|
||||||
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 onMaxLengthHide = useCallback(() => setShowMaxLengthWarning(false), [])
|
const onMaxLengthHide = useCallback(() => setShowMaxLengthWarning(false), [])
|
||||||
|
const codeMirrorOptions = useCodeMirrorOptions()
|
||||||
const codeMirrorOptions: EditorConfiguration = useMemo<EditorConfiguration>(
|
|
||||||
() => ({
|
|
||||||
...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]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'d-flex flex-column h-100 position-relative'} onMouseEnter={onMakeScrollSource}>
|
<div className={'d-flex flex-column h-100 position-relative'} onMouseEnter={onMakeScrollSource}>
|
||||||
|
|
|
@ -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<number>()
|
||||||
|
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])
|
||||||
|
}
|
|
@ -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<EditorConfiguration>(
|
||||||
|
() => ({
|
||||||
|
...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]
|
||||||
|
)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
}
|
|
@ -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]
|
||||||
|
)
|
||||||
|
}
|
|
@ -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<ScrollState>()
|
||||||
|
|
||||||
|
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]
|
||||||
|
)
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue