fix(frontend): extract codemirror extension for ref update into hook

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-03-08 16:29:11 +01:00
parent f8e35e6746
commit 11b48edca1
29 changed files with 99 additions and 103 deletions

View file

@ -1,60 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentEdits } from '../editor-pane/tool-bar/formatters/types/changes'
import type { CursorSelection } from '../editor-pane/tool-bar/formatters/types/cursor-selection'
import type { EditorView } from '@codemirror/view'
import { Optional } from '@mrdrogdrog/optional'
import type { PropsWithChildren } from 'react'
import React, { createContext, useContext, useState } from 'react'
export type CodeMirrorReference = EditorView | undefined
type SetCodeMirrorReference = (value: CodeMirrorReference) => void
export type ContentFormatter = (parameters: {
currentSelection: CursorSelection
markdownContent: string
}) => [ContentEdits, CursorSelection | undefined]
type ChangeEditorContentContext = [CodeMirrorReference, SetCodeMirrorReference]
const changeEditorContentContext = createContext<ChangeEditorContentContext | undefined>(undefined)
/**
* Extracts the {@link CodeMirrorReference code mirror reference} from the parent context.
*
* @return The {@link CodeMirrorReference} from the parent context.
*/
export const useCodeMirrorReference = (): CodeMirrorReference => {
const contextContent = Optional.ofNullable(useContext(changeEditorContentContext)).orElseThrow(
() => new Error('No change content received. Did you forget to use the provider component')
)
return contextContent[0]
}
/**
* Provides a function to set the {@link CodeMirrorReference code mirror reference} in the current context.
*
* @return A function to set a {@link CodeMirrorReference code mirror reference}.
*/
export const useSetCodeMirrorReference = (): SetCodeMirrorReference => {
const contextContent = Optional.ofNullable(useContext(changeEditorContentContext)).orElseThrow(
() => new Error('No change content received. Did you forget to use the provider component')
)
return contextContent[1]
}
/**
* Provides a context for the child components that contains a ref to the current code mirror instance and a callback that posts changes to this codemirror.
*/
export const ChangeEditorContentContextProvider: React.FC<PropsWithChildren<unknown>> = ({ children }) => {
const [codeMirrorRef, setCodeMirrorRef] = useState<CodeMirrorReference>(undefined)
return (
<changeEditorContentContext.Provider value={[codeMirrorRef, setCodeMirrorRef]}>
{children}
</changeEditorContentContext.Provider>
)
}

View file

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { EditorView } from '@codemirror/view'
import { Optional } from '@mrdrogdrog/optional'
import type { PropsWithChildren } from 'react'
import React, { createContext, useContext, useState } from 'react'
type CodeMirrorReference = EditorView | undefined
const codemirrorReferenceContext = createContext<
[CodeMirrorReference, (value: CodeMirrorReference) => void] | undefined
>(undefined)
/**
* Provides the value from the {@link CodeMirrorReference code mirror reference} context.
*
* @return The {@link CodeMirrorReference} from the parent context.
*/
export const useCodemirrorReferenceContext = () => {
return Optional.ofNullable(useContext(codemirrorReferenceContext)).orElseThrow(
() => new Error('No codemirror reference received. Did you forget to use the provider component?')
)
}
/**
* Provides a context for the child components that contains a ref to the current code mirror instance and a callback that posts changes to this codemirror.
*/
export const ChangeEditorContentContextProvider: React.FC<PropsWithChildren<unknown>> = ({ children }) => {
const [codeMirrorRef, setCodeMirrorRef] = useState<CodeMirrorReference>(undefined)
return (
<codemirrorReferenceContext.Provider value={[codeMirrorRef, setCodeMirrorRef]}>
{children}
</codemirrorReferenceContext.Provider>
)
}

View file

@ -3,13 +3,18 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentEdits } from '../editor-pane/tool-bar/formatters/types/changes'
import type { CursorSelection } from '../editor-pane/tool-bar/formatters/types/cursor-selection'
import type { ContentFormatter } from './change-content-context'
import { useCodeMirrorReference } from './change-content-context'
import { useCodemirrorReferenceContext } from './codemirror-reference-context'
import type { EditorView } from '@codemirror/view'
import { Optional } from '@mrdrogdrog/optional'
import { useMemo } from 'react'
export type ContentFormatter = (parameters: {
currentSelection: CursorSelection
markdownContent: string
}) => [ContentEdits, CursorSelection | undefined]
/**
* Changes the content of the given CodeMirror view using the given formatter function.
*
@ -33,7 +38,7 @@ export const changeEditorContent = (view: EditorView, formatter: ContentFormatte
* @see changeEditorContent
*/
export const useChangeEditorContentCallback = () => {
const codeMirrorRef = useCodeMirrorReference()
const [codeMirrorRef] = useCodemirrorReferenceContext()
return useMemo(() => {
if (codeMirrorRef) {
return (callback: ContentFormatter) => changeEditorContent(codeMirrorRef, callback)

View file

@ -11,7 +11,7 @@ import { MotdModal } from '../common/motd-modal/motd-modal'
import { CommunicatorImageLightbox } from '../markdown-renderer/extensions/image/communicator-image-lightbox'
import { ExtensionEventEmitterProvider } from '../markdown-renderer/hooks/use-extension-event-emitter'
import { AppBar, AppBarMode } from './app-bar/app-bar'
import { ChangeEditorContentContextProvider } from './change-content-context/change-content-context'
import { ChangeEditorContentContextProvider } from './change-content-context/codemirror-reference-context'
import { EditorDocumentRenderer } from './editor-document-renderer/editor-document-renderer'
import { EditorPane } from './editor-pane/editor-pane'
import { useComponentsFromAppExtensions } from './editor-pane/hooks/use-components-from-app-extensions'

View file

@ -8,7 +8,6 @@ import { useBaseUrl, ORIGIN } from '../../../hooks/common/use-base-url'
import { useDarkModeState } from '../../../hooks/common/use-dark-mode-state'
import { cypressAttribute, cypressId } from '../../../utils/cypress-attribute'
import { findLanguageByCodeBlockName } from '../../markdown-renderer/extensions/base/code-block-markdown-extension/find-language-by-code-block-name'
import { useCodeMirrorReference, useSetCodeMirrorReference } from '../change-content-context/change-content-context'
import type { ScrollProps } from '../synced-scroll/scroll-props'
import styles from './extended-codemirror/codemirror.module.scss'
import { useCodeMirrorFileInsertExtension } from './hooks/code-mirror-extensions/use-code-mirror-file-insert-extension'
@ -18,6 +17,7 @@ import { useOnImageUploadFromRenderer } from './hooks/image-upload-from-renderer
import { useCodeMirrorTablePasteExtension } from './hooks/table-paste/use-code-mirror-table-paste-extension'
import { useApplyScrollState } from './hooks/use-apply-scroll-state'
import { useCursorActivityCallback } from './hooks/use-cursor-activity-callback'
import { useUpdateCodeMirrorReference } from './hooks/use-update-code-mirror-reference'
import { useAwareness } from './hooks/yjs/use-awareness'
import { useBindYTextToRedux } from './hooks/yjs/use-bind-y-text-to-redux'
import { useCodeMirrorYjsExtension } from './hooks/yjs/use-code-mirror-yjs-extension'
@ -63,16 +63,7 @@ export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMak
const spellCheckExtension = useCodeMirrorSpellCheckExtension()
const cursorActivityExtension = useCursorActivityCallback()
const codeMirrorRef = useCodeMirrorReference()
const setCodeMirrorReference = useSetCodeMirrorReference()
const updateViewContext = useMemo(() => {
return EditorView.updateListener.of((update) => {
if (codeMirrorRef !== update.view) {
setCodeMirrorReference(update.view)
}
})
}, [codeMirrorRef, setCodeMirrorReference])
const updateViewContextExtension = useUpdateCodeMirrorReference()
const yDoc = useYDoc()
const awareness = useAwareness(yDoc)
@ -102,7 +93,7 @@ export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMak
fileInsertExtension,
autocompletion(),
cursorActivityExtension,
updateViewContext,
updateViewContextExtension,
yjsExtension,
firstEditorUpdateExtension,
spellCheckExtension
@ -113,7 +104,7 @@ export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMak
tablePasteExtensions,
fileInsertExtension,
cursorActivityExtension,
updateViewContext,
updateViewContextExtension,
yjsExtension,
firstEditorUpdateExtension,
spellCheckExtension

View file

@ -8,7 +8,7 @@ import { Logger } from '../../../../../utils/logger'
import { useEditorReceiveHandler } from '../../../../render-page/window-post-message-communicator/hooks/use-editor-receive-handler'
import type { ImageUploadMessage } from '../../../../render-page/window-post-message-communicator/rendering-message'
import { CommunicationMessageType } from '../../../../render-page/window-post-message-communicator/rendering-message'
import { useCodeMirrorReference } from '../../../change-content-context/change-content-context'
import { useCodemirrorReferenceContext } from '../../../change-content-context/codemirror-reference-context'
import type { CursorSelection } from '../../tool-bar/formatters/types/cursor-selection'
import { useHandleUpload } from '../use-handle-upload'
import { findRegexMatchInText } from './find-regex-match-in-text'
@ -22,7 +22,7 @@ const imageWithPlaceholderLinkRegex = /!\[([^\]]*)]\(https:\/\/([^)]*)\)/g
* Receives {@link CommunicationMessageType.IMAGE_UPLOAD image upload events} via iframe communication and processes the attached uploads.
*/
export const useOnImageUploadFromRenderer = (): void => {
const codeMirrorReference = useCodeMirrorReference()
const [codeMirrorReference] = useCodemirrorReferenceContext()
const handleUpload = useHandleUpload()
useEditorReceiveHandler(

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Logger } from '../../../../utils/logger'
import { useCodeMirrorReference } from '../../change-content-context/change-content-context'
import { useCodemirrorReferenceContext } from '../../change-content-context/codemirror-reference-context'
import type { ScrollState } from '../../synced-scroll/scroll-props'
import { EditorView } from '@codemirror/view'
import equal from 'fast-deep-equal'
@ -37,7 +37,7 @@ const applyScrollState = (view: EditorView, scrollState: ScrollState): void => {
*/
export const useApplyScrollState = (scrollState?: ScrollState): void => {
const lastScrollPosition = useRef<ScrollState>()
const codeMirrorRef = useCodeMirrorReference()
const [codeMirrorRef] = useCodemirrorReferenceContext()
useEffect(() => {
const view = codeMirrorRef

View file

@ -7,7 +7,7 @@ import { uploadFile } from '../../../../api/media'
import { getGlobalState } from '../../../../redux'
import { supportedMimeTypes } from '../../../common/upload-image-mimetypes'
import { useUiNotifications } from '../../../notifications/ui-notification-boundary'
import type { ContentFormatter } from '../../change-content-context/change-content-context'
import type { ContentFormatter } from '../../change-content-context/use-change-editor-content-callback'
import { changeEditorContent } from '../../change-content-context/use-change-editor-content-callback'
import { replaceInContent } from '../tool-bar/formatters/replace-in-content'
import { replaceSelection } from '../tool-bar/formatters/replace-selection'

View file

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useCodemirrorReferenceContext } from '../../change-content-context/codemirror-reference-context'
import type { Extension } from '@codemirror/state'
import { EditorView } from '@codemirror/view'
import { useMemo } from 'react'
export const useUpdateCodeMirrorReference = (): Extension => {
const [codeMirrorReference, setCodeMirrorReference] = useCodemirrorReferenceContext()
return useMemo(() => {
return EditorView.updateListener.of((update) => {
if (codeMirrorReference !== update.view) {
setCodeMirrorReference(update.view)
}
})
}, [codeMirrorReference, setCodeMirrorReference])
}

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/change-content-context'
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/change-content-context'
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { prependLinesOfSelection } from '../formatters/prepend-lines-of-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/change-content-context'
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { changeCursorsToWholeLineIfNoToCursor } from '../formatters/utils/change-cursors-to-whole-line-if-no-to-cursor'
import { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/change-content-context'
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { changeCursorsToWholeLineIfNoToCursor } from '../formatters/utils/change-cursors-to-whole-line-if-no-to-cursor'
import { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/change-content-context'
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { replaceSelection } from '../formatters/replace-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/change-content-context'
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { prependLinesOfSelection } from '../formatters/prepend-lines-of-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/change-content-context'
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/change-content-context'
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { replaceSelection } from '../formatters/replace-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/change-content-context'
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { addLink } from '../formatters/add-link'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/change-content-context'
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/change-content-context'
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { addLink } from '../formatters/add-link'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/change-content-context'
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { prependLinesOfSelection } from '../formatters/prepend-lines-of-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/change-content-context'
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { prependLinesOfSelection } from '../formatters/prepend-lines-of-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/change-content-context'
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/change-content-context'
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/change-content-context'
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/change-content-context'
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/change-content-context'
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { prependLinesOfSelection } from '../formatters/prepend-lines-of-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'

View file

@ -5,7 +5,7 @@
*/
import { cypressId } from '../../../../utils/cypress-attribute'
import { UiIcon } from '../../../common/icons/ui-icon'
import type { ContentFormatter } from '../../change-content-context/change-content-context'
import type { ContentFormatter } from '../../change-content-context/use-change-editor-content-callback'
import { useChangeEditorContentCallback } from '../../change-content-context/use-change-editor-content-callback'
import React, { useCallback, useMemo } from 'react'
import { Button } from 'react-bootstrap'

View file

@ -8,7 +8,7 @@ import { Logger } from '../../../../../utils/logger'
import { UiIcon } from '../../../../common/icons/ui-icon'
import { ShowIf } from '../../../../common/show-if/show-if'
import { acceptedMimeTypes } from '../../../../common/upload-image-mimetypes'
import { useCodeMirrorReference } from '../../../change-content-context/change-content-context'
import { useCodemirrorReferenceContext } from '../../../change-content-context/codemirror-reference-context'
import { UploadInput } from '../../../sidebar/upload-input'
import { useHandleUpload } from '../../hooks/use-handle-upload'
import { extractSelectedText } from './extract-selected-text'
@ -30,7 +30,7 @@ export const UploadImageButton: React.FC = () => {
clickRef.current?.()
}, [])
const codeMirror = useCodeMirrorReference()
const [codeMirror] = useCodemirrorReferenceContext()
const handleUpload = useHandleUpload()
const onUploadImage = useCallback(