mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-21 18:55:19 -04:00
Upgrade to CodeMirror 6 (#1787)
Upgrade to CodeMirror 6 Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
1a09bfa5f1
commit
6a6f6105b9
103 changed files with 1906 additions and 2615 deletions
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { handleUpload } from '../../upload-handler'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import type { Extension } from '@codemirror/state'
|
||||
import Optional from 'optional-js'
|
||||
|
||||
/**
|
||||
* Creates a callback that is used to process file drops on the code mirror editor
|
||||
*
|
||||
* @return the code mirror callback
|
||||
*/
|
||||
export const useCodeMirrorFileDropExtension = (): Extension => {
|
||||
const onDrop = useCallback((event: DragEvent, view: EditorView): void => {
|
||||
if (!event.pageX || !event.pageY) {
|
||||
return
|
||||
}
|
||||
Optional.ofNullable(event.dataTransfer?.files)
|
||||
.filter((files) => files.length > 0)
|
||||
.ifPresent((files) => {
|
||||
event.preventDefault()
|
||||
const newCursor = view.posAtCoords({ y: event.pageY, x: event.pageX })
|
||||
if (newCursor === null) {
|
||||
return
|
||||
}
|
||||
handleUpload(files[0], { from: newCursor })
|
||||
})
|
||||
}, [])
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
EditorView.domEventHandlers({
|
||||
drop: onDrop
|
||||
}),
|
||||
[onDrop]
|
||||
)
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { handleFilePaste, handleTablePaste } from '../../tool-bar/utils/pasteHandlers'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import type { Extension } from '@codemirror/state'
|
||||
|
||||
/**
|
||||
* Creates a {@link Extension code mirror extension} that handles the table or file paste action.
|
||||
*
|
||||
* @return the created {@link Extension code mirror extension}
|
||||
*/
|
||||
export const useCodeMirrorPasteExtension = (): Extension => {
|
||||
return useMemo(
|
||||
() =>
|
||||
EditorView.domEventHandlers({
|
||||
paste: (event: ClipboardEvent) => {
|
||||
const clipboardData = event.clipboardData
|
||||
if (!clipboardData) {
|
||||
return
|
||||
}
|
||||
if (handleTablePaste(clipboardData) || handleFilePaste(clipboardData)) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
}),
|
||||
[]
|
||||
)
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import type { ScrollState } from '../../../synced-scroll/scroll-props'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import type { Extension } from '@codemirror/state'
|
||||
|
||||
export type OnScrollCallback = ((scrollState: ScrollState) => void) | undefined
|
||||
|
||||
/**
|
||||
* Extracts the {@link ScrollState scroll state} from the given {@link EditorView editor view}.
|
||||
*
|
||||
* @param view The {@link EditorView editor view} whose scroll state should be extracted.
|
||||
*/
|
||||
export const extractScrollState = (view: EditorView): ScrollState => {
|
||||
const state = view.state
|
||||
const scrollTop = view.scrollDOM.scrollTop
|
||||
const lineBlockAtHeight = view.lineBlockAtHeight(scrollTop)
|
||||
const line = state.doc.lineAt(lineBlockAtHeight.from)
|
||||
const percentageRaw = (scrollTop - lineBlockAtHeight.top) / lineBlockAtHeight.height
|
||||
const scrolledPercentage = Math.floor(percentageRaw * 100)
|
||||
return {
|
||||
firstLineInView: line.number,
|
||||
scrolledPercentage
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a code mirror extension for the scroll binding.
|
||||
* It calculates a {@link ScrollState} and posts it on change.
|
||||
*
|
||||
* @param onScroll The callback that is used to post {@link ScrollState scroll states} when the editor view is scrolling.
|
||||
* @return The extensions that watches the scrolling in the editor.
|
||||
*/
|
||||
export const useCodeMirrorScrollWatchExtension = (onScroll: OnScrollCallback): Extension => {
|
||||
const onEditorScroll = useCallback(
|
||||
(view: EditorView) => {
|
||||
if (!onScroll || !view) {
|
||||
return undefined
|
||||
}
|
||||
onScroll(extractScrollState(view))
|
||||
},
|
||||
[onScroll]
|
||||
)
|
||||
return useMemo(
|
||||
() =>
|
||||
EditorView.domEventHandlers({
|
||||
scroll: (event, view) => onEditorScroll(view)
|
||||
}),
|
||||
[onEditorScroll]
|
||||
)
|
||||
}
|
|
@ -6,8 +6,24 @@
|
|||
|
||||
import type { MutableRefObject } from 'react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import type { Editor } from 'codemirror'
|
||||
import type { ScrollState } from '../../synced-scroll/scroll-props'
|
||||
import type { ReactCodeMirrorRef } from '@uiw/react-codemirror'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import equal from 'fast-deep-equal'
|
||||
|
||||
/**
|
||||
* Applies the given {@link ScrollState scroll state} to the given {@link EditorView code mirror editor view}.
|
||||
*
|
||||
* @param view The {@link EditorView view} that should be scrolled
|
||||
* @param scrollState The {@link ScrollState scroll state} that should be applied
|
||||
*/
|
||||
export const applyScrollState = (view: EditorView, scrollState: ScrollState): void => {
|
||||
const line = view.state.doc.line(scrollState.firstLineInView)
|
||||
const lineBlock = view.lineBlockAt(line.from)
|
||||
const margin = Math.floor(lineBlock.height * scrollState.scrolledPercentage) / 100
|
||||
const stateEffect = EditorView.scrollIntoView(line.from, { y: 'start', yMargin: -margin })
|
||||
view.dispatch({ effects: [stateEffect] })
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors the given scroll state and scrolls the editor to the state if changed.
|
||||
|
@ -16,22 +32,21 @@ import type { ScrollState } from '../../synced-scroll/scroll-props'
|
|||
* @param scrollState The scroll state that should be monitored
|
||||
*/
|
||||
export const useApplyScrollState = (
|
||||
editorRef: MutableRefObject<Editor | undefined>,
|
||||
editorRef: MutableRefObject<ReactCodeMirrorRef | null>,
|
||||
scrollState?: ScrollState
|
||||
): void => {
|
||||
const lastScrollPosition = useRef<number>()
|
||||
const lastScrollPosition = useRef<ScrollState>()
|
||||
|
||||
useEffect(() => {
|
||||
const editor = editorRef.current
|
||||
if (!editor || !scrollState) {
|
||||
const view = editorRef.current?.view
|
||||
if (!view || !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)
|
||||
|
||||
if (equal(scrollState, lastScrollPosition.current)) {
|
||||
return
|
||||
}
|
||||
applyScrollState(view, scrollState)
|
||||
lastScrollPosition.current = scrollState
|
||||
}, [editorRef, scrollState])
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { RefObject } from 'react'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import type { Extension } from '@codemirror/state'
|
||||
|
||||
/**
|
||||
* Creates a {@link RefObject<boolean> reference} that contains the information if the editor is currently focused or not.
|
||||
*
|
||||
* @returns The reference and the necessary {@link Extension code mirror extension} that receives the focus and blur events
|
||||
*/
|
||||
export const useCodeMirrorFocusReference = (): [Extension, RefObject<boolean>] => {
|
||||
const focusReference = useRef<boolean>(false)
|
||||
const codeMirrorExtension = useMemo(
|
||||
() =>
|
||||
EditorView.domEventHandlers({
|
||||
blur: () => {
|
||||
focusReference.current = false
|
||||
},
|
||||
focus: () => {
|
||||
focusReference.current = true
|
||||
}
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
return [codeMirrorExtension, focusReference]
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { EditorConfiguration } from 'codemirror'
|
||||
import { useMemo } from 'react'
|
||||
import { createDefaultKeyMap } 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: createDefaultKeyMap(),
|
||||
flattenSpans: true,
|
||||
addModeClass: true,
|
||||
autoRefresh: true,
|
||||
// otherCursors: true,
|
||||
placeholder: t('editor.placeholder')
|
||||
}),
|
||||
[t, editorPreferences]
|
||||
)
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { StatusBarInfo } from '../status-bar/status-bar'
|
||||
import { useMemo } from 'react'
|
||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||
|
||||
/**
|
||||
* Provides a {@link StatusBarInfo} object and a function that can update this object using a {@link CodeMirror code mirror instance}.
|
||||
*/
|
||||
export const useCreateStatusBarInfo = (): StatusBarInfo => {
|
||||
const maxDocumentLength = useApplicationState((state) => state.config.maxDocumentLength)
|
||||
const selection = useApplicationState((state) => state.noteDetails.selection)
|
||||
const markdownContent = useApplicationState((state) => state.noteDetails.markdownContent)
|
||||
const markdownContentLines = useApplicationState((state) => state.noteDetails.markdownContentLines)
|
||||
|
||||
return useMemo(() => {
|
||||
const startCharacter = selection.from.character
|
||||
const endCharacter = selection.to?.character ?? 0
|
||||
const startLine = selection.from.line
|
||||
const endLine = selection.to?.line ?? 0
|
||||
|
||||
return {
|
||||
position: { line: startLine, character: startCharacter },
|
||||
charactersInDocument: markdownContent.length,
|
||||
remainingCharacters: maxDocumentLength - markdownContent.length,
|
||||
linesInDocument: markdownContentLines.length,
|
||||
selectedColumns: endCharacter - startCharacter,
|
||||
selectedLines: endLine - startLine
|
||||
}
|
||||
}, [markdownContent.length, markdownContentLines.length, maxDocumentLength, selection])
|
||||
}
|
|
@ -4,27 +4,36 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Editor } from 'codemirror'
|
||||
import { useCallback } from 'react'
|
||||
import type { CursorPosition } from '../../../../redux/editor/types'
|
||||
import type { RefObject } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { updateCursorPositions } from '../../../../redux/note-details/methods'
|
||||
import type { ViewUpdate } from '@codemirror/view'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { Logger } from '../../../../utils/logger'
|
||||
import type { Extension } from '@codemirror/state'
|
||||
|
||||
const logger = new Logger('useCursorActivityCallback')
|
||||
|
||||
/**
|
||||
* Provides a callback for codemirror that handles cursor changes
|
||||
*
|
||||
* @return the generated callback
|
||||
*/
|
||||
export const useCursorActivityCallback = (): ((editor: Editor) => void) => {
|
||||
return useCallback((editor) => {
|
||||
const firstSelection = editor.listSelections()[0]
|
||||
if (firstSelection === undefined) {
|
||||
return
|
||||
}
|
||||
const start: CursorPosition = { line: firstSelection.from().line, character: firstSelection.from().ch }
|
||||
const end: CursorPosition = { line: firstSelection.to().line, character: firstSelection.to().ch }
|
||||
updateCursorPositions({
|
||||
from: start,
|
||||
to: start.line === end.line && start.character === end.character ? undefined : end
|
||||
})
|
||||
}, [])
|
||||
export const useCursorActivityCallback = (editorFocused: RefObject<boolean>): Extension => {
|
||||
return useMemo(
|
||||
() =>
|
||||
EditorView.updateListener.of((viewUpdate: ViewUpdate): void => {
|
||||
if (!editorFocused.current) {
|
||||
logger.debug("Don't post updated cursor because editor isn't focused")
|
||||
return
|
||||
}
|
||||
const firstSelection = viewUpdate.state.selection.main
|
||||
const newCursorPos = {
|
||||
from: firstSelection.from,
|
||||
to: firstSelection.to === firstSelection.from ? undefined : firstSelection.to
|
||||
}
|
||||
updateCursorPositions(newCursorPos)
|
||||
}),
|
||||
[editorFocused]
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||
|
||||
export interface LineBasedPosition {
|
||||
line: number
|
||||
character: number
|
||||
}
|
||||
|
||||
const calculateLineBasedPosition = (absolutePosition: number, lineStartIndexes: number[]): LineBasedPosition => {
|
||||
const foundLineIndex = lineStartIndexes.findIndex((startIndex) => absolutePosition < startIndex)
|
||||
const line = foundLineIndex === -1 ? lineStartIndexes.length - 1 : foundLineIndex - 1
|
||||
return {
|
||||
line: line,
|
||||
character: absolutePosition - lineStartIndexes[line]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the line+character based position of the to cursor, if available.
|
||||
*/
|
||||
export const useLineBasedToPosition = (): LineBasedPosition | undefined => {
|
||||
const lineStartIndexes = useApplicationState((state) => state.noteDetails.markdownContent.lineStartIndexes)
|
||||
const selection = useApplicationState((state) => state.noteDetails.selection)
|
||||
|
||||
return useMemo(() => {
|
||||
const to = selection.to
|
||||
if (to === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return calculateLineBasedPosition(to, lineStartIndexes)
|
||||
}, [selection.to, lineStartIndexes])
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the line+character based position of the from cursor.
|
||||
*/
|
||||
export const useLineBasedFromPosition = (): LineBasedPosition => {
|
||||
const lineStartIndexes = useApplicationState((state) => state.noteDetails.markdownContent.lineStartIndexes)
|
||||
const selection = useApplicationState((state) => state.noteDetails.selection)
|
||||
|
||||
return useMemo(() => {
|
||||
return calculateLineBasedPosition(selection.from, lineStartIndexes)
|
||||
}, [selection.from, lineStartIndexes])
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useMemo, useRef } from 'react'
|
||||
import type { ScrollState } from '../../synced-scroll/scroll-props'
|
||||
import { extractScrollState } from './code-mirror-extensions/use-code-mirror-scroll-watch-extension'
|
||||
import { applyScrollState } from './use-apply-scroll-state'
|
||||
import { store } from '../../../../redux'
|
||||
import type { Extension } from '@codemirror/state'
|
||||
import { Logger } from '../../../../utils/logger'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
|
||||
const logger = new Logger('useOffScreenScrollProtection')
|
||||
|
||||
/**
|
||||
* If the editor content changes while the editor isn't focused then the editor starts jumping around.
|
||||
* This extension fixes this behaviour by saving the scroll state when the editor looses focus and applies it on content changes.
|
||||
*
|
||||
* @returns necessary {@link Extension code mirror extensions} to provide the functionality
|
||||
*/
|
||||
export const useOffScreenScrollProtection = (): Extension[] => {
|
||||
const offFocusScrollState = useRef<ScrollState>()
|
||||
|
||||
return useMemo(() => {
|
||||
const saveOffFocusScrollStateExtension = EditorView.domEventHandlers({
|
||||
blur: (event, view) => {
|
||||
offFocusScrollState.current = extractScrollState(view)
|
||||
logger.debug('Save off-focus scroll state', offFocusScrollState.current)
|
||||
},
|
||||
focus: () => {
|
||||
offFocusScrollState.current = undefined
|
||||
}
|
||||
})
|
||||
|
||||
const changeExtension = EditorView.updateListener.of((update) => {
|
||||
const view = update.view
|
||||
const scrollState = offFocusScrollState.current
|
||||
if (!scrollState || !update.docChanged) {
|
||||
return
|
||||
}
|
||||
logger.debug('Apply off-focus scroll state', scrollState)
|
||||
applyScrollState(view, scrollState)
|
||||
const selection = store.getState().noteDetails.selection
|
||||
view.dispatch(
|
||||
view.state.update({
|
||||
selection: {
|
||||
anchor: selection.from,
|
||||
head: selection.to
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
return [saveOffFocusScrollStateExtension, changeExtension]
|
||||
}, [])
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import type { Editor } from 'codemirror'
|
||||
import { handleUpload } from '../upload-handler'
|
||||
import type { 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])
|
||||
}
|
||||
}, [])
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react'
|
||||
import type { Editor } from 'codemirror'
|
||||
import type { PasteEvent } from '../tool-bar/utils/pasteHandlers'
|
||||
import { handleFilePaste, handleTablePaste } from '../tool-bar/utils/pasteHandlers'
|
||||
import type { 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 => {
|
||||
return useCallback((pasteEditor: Editor, event: PasteEvent) => {
|
||||
if (!event || !event.clipboardData) {
|
||||
return
|
||||
}
|
||||
if (handleTablePaste(event) || handleFilePaste(event)) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
}, [])
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { DomEvent } from 'react-codemirror2'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import type { Editor, ScrollInfo } from 'codemirror'
|
||||
import type { 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]
|
||||
)
|
||||
}
|
|
@ -36,7 +36,7 @@ export const useOnImageUploadFromRenderer = (): void => {
|
|||
.then((blob) => {
|
||||
const file = new File([blob], fileName, { type: blob.type })
|
||||
const { cursorSelection, alt, title } = Optional.ofNullable(lineIndex)
|
||||
.map((actualLineIndex) => findPlaceholderInMarkdownContent(actualLineIndex, placeholderIndexInLine))
|
||||
.flatMap((actualLineIndex) => findPlaceholderInMarkdownContent(actualLineIndex, placeholderIndexInLine))
|
||||
.orElseGet(() => ({}))
|
||||
handleUpload(file, cursorSelection, alt, title)
|
||||
})
|
||||
|
@ -58,26 +58,25 @@ export interface ExtractResult {
|
|||
* @param replacementIndexInLine If multiple image placeholders are present in the target line then this number describes the index of the wanted placeholder.
|
||||
* @return the calculated start and end position or undefined if no position could be determined
|
||||
*/
|
||||
const findPlaceholderInMarkdownContent = (lineIndex: number, replacementIndexInLine = 0): ExtractResult | undefined => {
|
||||
const currentMarkdownContentLines = getGlobalState().noteDetails.markdownContent.split('\n')
|
||||
const lineAtIndex = currentMarkdownContentLines[lineIndex]
|
||||
if (lineAtIndex === undefined) {
|
||||
return
|
||||
}
|
||||
return findImagePlaceholderInLine(currentMarkdownContentLines[lineIndex], lineIndex, replacementIndexInLine)
|
||||
const findPlaceholderInMarkdownContent = (lineIndex: number, replacementIndexInLine = 0): Optional<ExtractResult> => {
|
||||
const noteDetails = getGlobalState().noteDetails
|
||||
const currentMarkdownContentLines = noteDetails.markdownContent.lines
|
||||
return Optional.ofNullable(noteDetails.markdownContent.lineStartIndexes[lineIndex]).map((startIndexOfLine) =>
|
||||
findImagePlaceholderInLine(currentMarkdownContentLines[lineIndex], startIndexOfLine, replacementIndexInLine)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find the right image placeholder in the given line.
|
||||
*
|
||||
* @param line The line that should be inspected
|
||||
* @param lineIndex The index of the line in the document
|
||||
* @param startIndexOfLine The absolute start index of the line in the document
|
||||
* @param replacementIndexInLine If multiple image placeholders are present in the target line then this number describes the index of the wanted placeholder.
|
||||
* @return the calculated start and end position or undefined if no position could be determined
|
||||
*/
|
||||
const findImagePlaceholderInLine = (
|
||||
line: string,
|
||||
lineIndex: number,
|
||||
startIndexOfLine: number,
|
||||
replacementIndexInLine = 0
|
||||
): ExtractResult | undefined => {
|
||||
const startOfImageTag = findRegexMatchInText(line, imageWithPlaceholderLinkRegex, replacementIndexInLine)
|
||||
|
@ -85,16 +84,12 @@ const findImagePlaceholderInLine = (
|
|||
return
|
||||
}
|
||||
|
||||
const from = startIndexOfLine + startOfImageTag.index
|
||||
const to = from + startOfImageTag[0].length
|
||||
return {
|
||||
cursorSelection: {
|
||||
from: {
|
||||
character: startOfImageTag.index,
|
||||
line: lineIndex
|
||||
},
|
||||
to: {
|
||||
character: startOfImageTag.index + startOfImageTag[0].length,
|
||||
line: lineIndex
|
||||
}
|
||||
from,
|
||||
to
|
||||
},
|
||||
alt: startOfImageTag[1],
|
||||
title: startOfImageTag[2]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue