diff --git a/src/components/editor-page/editor-pane/editor-pane.tsx b/src/components/editor-page/editor-pane/editor-pane.tsx index e5bf1e980..f61cf20f1 100644 --- a/src/components/editor-page/editor-pane/editor-pane.tsx +++ b/src/components/editor-page/editor-pane/editor-pane.tsx @@ -5,14 +5,12 @@ */ import type { Editor, EditorChange } from 'codemirror' -import React, { useCallback, useRef, useState } from 'react' +import React, { useCallback, useState } from 'react' import { Controlled as ControlledCodeMirror } from 'react-codemirror2' -import { MaxLengthWarningModal } from '../editor-modals/max-length-warning-modal' import type { ScrollProps } from '../synced-scroll/scroll-props' import { allHinters, findWordAtCursor } from './autocompletion' import './editor-pane.scss' -import type { StatusBarInfo } from './status-bar/status-bar' -import { createStatusInfo, defaultState, StatusBar } from './status-bar/status-bar' +import { StatusBar } from './status-bar/status-bar' import { ToolBar } from './tool-bar/tool-bar' import { useApplicationState } from '../../../hooks/common/use-application-state' import './codemirror-imports' @@ -23,6 +21,8 @@ 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' +import { MaxLengthWarning } from './max-length-warning/max-length-warning' +import { useCreateStatusBarInfo } from './hooks/use-create-status-bar-info' const onChange = (editor: Editor) => { for (const hinter of allHinters) { @@ -41,53 +41,34 @@ const onChange = (editor: Editor) => { export const EditorPane: React.FC = ({ scrollState, onScroll, onMakeScrollSource }) => { const markdownContent = useNoteMarkdownContent() - const maxLength = useApplicationState((state) => state.config.maxDocumentLength) - const [showMaxLengthWarning, setShowMaxLengthWarning] = useState(false) - const maxLengthWarningAlreadyShown = useRef(false) + const [editor, setEditor] = useState() - const [statusBarInfo, setStatusBarInfo] = useState(defaultState) const ligaturesEnabled = useApplicationState((state) => state.editorConfig.ligatures) const onPaste = useOnEditorPasteCallback() const onEditorScroll = useOnEditorScroll(onScroll) useApplyScrollState(editor, scrollState) - const onBeforeChange = useCallback( - (editor: Editor, data: EditorChange, value: string) => { - if (value.length > maxLength && !maxLengthWarningAlreadyShown.current) { - setShowMaxLengthWarning(true) - maxLengthWarningAlreadyShown.current = true - } - if (value.length <= maxLength) { - maxLengthWarningAlreadyShown.current = false - } - setNoteContent(value) - }, - [maxLength] - ) + const onBeforeChange = useCallback((editor: Editor, data: EditorChange, value: string) => { + setNoteContent(value) + }, []) + + const [statusBarInfo, updateStatusBarInfo] = useCreateStatusBarInfo() const onEditorDidMount = useCallback( (mountedEditor: Editor) => { - setStatusBarInfo(createStatusInfo(mountedEditor, maxLength)) + updateStatusBarInfo(mountedEditor) setEditor(mountedEditor) }, - [maxLength] - ) - - const onCursorActivity = useCallback( - (editorWithActivity: Editor) => { - setStatusBarInfo(createStatusInfo(editorWithActivity, maxLength)) - }, - [maxLength] + [updateStatusBarInfo] ) const onDrop = useOnEditorFileDrop() - const onMaxLengthHide = useCallback(() => setShowMaxLengthWarning(false), []) const codeMirrorOptions = useCodeMirrorOptions() return (
- + = ({ scrollState, onScroll, onMak onChange={onChange} onPaste={onPaste} onDrop={onDrop} - onCursorActivity={onCursorActivity} + onCursorActivity={updateStatusBarInfo} editorDidMount={onEditorDidMount} onBeforeChange={onBeforeChange} onScroll={onEditorScroll} /> - +
) } diff --git a/src/components/editor-page/editor-pane/hooks/use-create-status-bar-info.ts b/src/components/editor-page/editor-pane/hooks/use-create-status-bar-info.ts new file mode 100644 index 000000000..822cc7683 --- /dev/null +++ b/src/components/editor-page/editor-pane/hooks/use-create-status-bar-info.ts @@ -0,0 +1,38 @@ +/* + * 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 { defaultState } from '../status-bar/status-bar' +import type { Editor } from 'codemirror' +import { useCallback, useState } 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: StatusBarInfo, + updateStatusBarInfo: (editor: Editor) => void +] => { + const maxDocumentLength = useApplicationState((state) => state.config.maxDocumentLength) + const [statusBarInfo, setStatusBarInfo] = useState(defaultState) + + const updateStatusBarInfo = useCallback( + (editor: Editor): void => { + setStatusBarInfo({ + position: editor.getCursor(), + charactersInDocument: editor.getValue().length, + remainingCharacters: maxDocumentLength - editor.getValue().length, + linesInDocument: editor.lineCount(), + selectedColumns: editor.getSelection().length, + selectedLines: editor.getSelection().split('\n').length + }) + }, + [maxDocumentLength] + ) + + return [statusBarInfo, updateStatusBarInfo] +} diff --git a/src/components/editor-page/editor-modals/max-length-warning-modal.tsx b/src/components/editor-page/editor-pane/max-length-warning/max-length-warning-modal.tsx similarity index 54% rename from src/components/editor-page/editor-modals/max-length-warning-modal.tsx rename to src/components/editor-page/editor-pane/max-length-warning/max-length-warning-modal.tsx index 30c37d63b..f6988496e 100644 --- a/src/components/editor-page/editor-modals/max-length-warning-modal.tsx +++ b/src/components/editor-page/editor-pane/max-length-warning/max-length-warning-modal.tsx @@ -7,16 +7,20 @@ import React from 'react' import { Button, Modal } from 'react-bootstrap' import { Trans, useTranslation } from 'react-i18next' -import type { ModalVisibilityProps } from '../../common/modals/common-modal' -import { CommonModal } from '../../common/modals/common-modal' -import { cypressId } from '../../../utils/cypress-attribute' +import type { ModalVisibilityProps } from '../../../common/modals/common-modal' +import { CommonModal } from '../../../common/modals/common-modal' +import { cypressId } from '../../../../utils/cypress-attribute' +import { useApplicationState } from '../../../../hooks/common/use-application-state' -export interface MaxLengthWarningModalProps extends ModalVisibilityProps { - maxLength: number -} - -export const MaxLengthWarningModal: React.FC = ({ show, onHide, maxLength }) => { +/** + * Shows a modal that informs the user that the document is too long. + * + * @param show is {@code true} if the modal should be shown + * @param onHide gets called if the modal was closed + */ +export const MaxLengthWarningModal: React.FC = ({ show, onHide }) => { useTranslation() + const maxDocumentLength = useApplicationState((state) => state.config.maxDocumentLength) return ( = ({ sh title={'editor.error.limitReached.title'} showCloseButton={true}> - + diff --git a/src/components/editor-page/editor-pane/max-length-warning/max-length-warning.tsx b/src/components/editor-page/editor-pane/max-length-warning/max-length-warning.tsx new file mode 100644 index 000000000..a13cdf436 --- /dev/null +++ b/src/components/editor-page/editor-pane/max-length-warning/max-length-warning.tsx @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import React, { useCallback, useEffect, useRef, useState } from 'react' +import { MaxLengthWarningModal } from './max-length-warning-modal' +import { useNoteMarkdownContent } from '../../../../hooks/common/use-note-markdown-content' +import { useApplicationState } from '../../../../hooks/common/use-application-state' + +/** + * Watches the length of the document and shows a warning modal to the user if the document length exceeds the configured value. + */ +export const MaxLengthWarning: React.FC = () => { + const [showMaxLengthWarningModal, setShowMaxLengthWarningModal] = useState(false) + const maxLengthWarningAlreadyShown = useRef(false) + const maxDocumentLength = useApplicationState((state) => state.config.maxDocumentLength) + const hideWarning = useCallback(() => setShowMaxLengthWarningModal(false), []) + const markdownContent = useNoteMarkdownContent() + + useEffect(() => { + if (markdownContent.length > maxDocumentLength && !maxLengthWarningAlreadyShown.current) { + setShowMaxLengthWarningModal(true) + maxLengthWarningAlreadyShown.current = true + } + if (markdownContent.length <= maxDocumentLength) { + maxLengthWarningAlreadyShown.current = false + } + }, [markdownContent, maxDocumentLength]) + + return +} diff --git a/src/components/editor-page/editor-pane/status-bar/cursor-position-info.tsx b/src/components/editor-page/editor-pane/status-bar/cursor-position-info.tsx new file mode 100644 index 000000000..2588f66f1 --- /dev/null +++ b/src/components/editor-page/editor-pane/status-bar/cursor-position-info.tsx @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import React, { useMemo } from 'react' +import { Trans } from 'react-i18next' +import type { Position } from 'codemirror' + +export interface CursorPositionInfoProps { + cursorPosition: Position +} + +/** + * Renders a translated text that shows the given cursor position. + * + * @param cursorPosition The cursor position that should be included + */ +export const CursorPositionInfo: React.FC = ({ cursorPosition }) => { + const translationOptions = useMemo( + () => ({ + line: cursorPosition.line + 1, + columns: cursorPosition.ch + 1 + }), + [cursorPosition.ch, cursorPosition.line] + ) + + return ( + + + + ) +} diff --git a/src/components/editor-page/editor-pane/status-bar/number-of-lines-in-document-info.tsx b/src/components/editor-page/editor-pane/status-bar/number-of-lines-in-document-info.tsx new file mode 100644 index 000000000..5f9ea5926 --- /dev/null +++ b/src/components/editor-page/editor-pane/status-bar/number-of-lines-in-document-info.tsx @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import React, { useMemo } from 'react' +import { Trans, useTranslation } from 'react-i18next' + +export interface LinesInDocumentInfoProps { + numberOfLinesInDocument: number +} + +/** + * Renders a translated text that shows the number of lines in the document. + * + * @param linesInDocument The number of lines in the document + */ +export const NumberOfLinesInDocumentInfo: React.FC = ({ numberOfLinesInDocument }) => { + useTranslation() + + const translationOptions = useMemo(() => ({ lines: numberOfLinesInDocument }), [numberOfLinesInDocument]) + + return ( + + + + ) +} diff --git a/src/components/editor-page/editor-pane/status-bar/remaining-characters-info.tsx b/src/components/editor-page/editor-pane/status-bar/remaining-characters-info.tsx new file mode 100644 index 000000000..db49339eb --- /dev/null +++ b/src/components/editor-page/editor-pane/status-bar/remaining-characters-info.tsx @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import React, { useMemo } from 'react' +import { cypressId } from '../../../../utils/cypress-attribute' +import { Trans, useTranslation } from 'react-i18next' + +export interface LengthInfoProps { + remainingCharacters: number + charactersInDocument: number +} + +/** + * Renders a translated text that shows the number of remaining characters. + * + * @param remainingCharacters The number of characters that are still available in this document + * @param charactersInDocument The total number of characters in the document + */ +export const RemainingCharactersInfo: React.FC = ({ remainingCharacters, charactersInDocument }) => { + const { t } = useTranslation() + + const remainingCharactersClass = useMemo(() => { + if (remainingCharacters <= 0) { + return 'text-danger' + } else if (remainingCharacters <= 100) { + return 'text-warning' + } else { + return '' + } + }, [remainingCharacters]) + + const lengthTooltip = useMemo(() => { + if (remainingCharacters === 0) { + return t('editor.statusBar.lengthTooltip.maximumReached') + } else if (remainingCharacters < 0) { + return t('editor.statusBar.lengthTooltip.exceeded', { exceeded: -remainingCharacters }) + } else { + return t('editor.statusBar.lengthTooltip.remaining', { remaining: remainingCharacters }) + } + }, [remainingCharacters, t]) + + const translationOptions = useMemo(() => ({ length: charactersInDocument }), [charactersInDocument]) + + return ( + + + + ) +} diff --git a/src/components/editor-page/editor-pane/status-bar/selection-info.tsx b/src/components/editor-page/editor-pane/status-bar/selection-info.tsx new file mode 100644 index 000000000..6bdda3b82 --- /dev/null +++ b/src/components/editor-page/editor-pane/status-bar/selection-info.tsx @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import React, { useMemo } from 'react' +import { Trans, useTranslation } from 'react-i18next' + +export interface SelectionInfoProps { + count: number + translationKey: 'column' | 'line' +} + +/** + * Renders a translated text that shows the number of selected columns or lines. + * + * @param count The number that should be included in the text + * @param translationKey Defines if the text for selected columns or lines should be used + */ +export const SelectionInfo: React.FC = ({ count, translationKey }) => { + useTranslation() + + const countTranslationOptions = useMemo(() => ({ count: count }), [count]) + + console.log(count, translationKey) + return ( + + + + ) +} diff --git a/src/components/editor-page/editor-pane/status-bar/separator-dash.tsx b/src/components/editor-page/editor-pane/status-bar/separator-dash.tsx new file mode 100644 index 000000000..f25792db8 --- /dev/null +++ b/src/components/editor-page/editor-pane/status-bar/separator-dash.tsx @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import React, { Fragment } from 'react' + +/** + * Renders a dash without breaking spaces around. + */ +export const SeparatorDash: React.FC = () => { + return  –  +} diff --git a/src/components/editor-page/editor-pane/status-bar/status-bar.tsx b/src/components/editor-page/editor-pane/status-bar/status-bar.tsx index a291c75ab..eb1b0080d 100644 --- a/src/components/editor-page/editor-pane/status-bar/status-bar.tsx +++ b/src/components/editor-page/editor-pane/status-bar/status-bar.tsx @@ -4,12 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import type { Editor, Position } from 'codemirror' -import React, { useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import { ShowIf } from '../../../common/show-if/show-if' +import type { Position } from 'codemirror' +import React from 'react' import './status-bar.scss' -import { cypressId } from '../../../../utils/cypress-attribute' +import { RemainingCharactersInfo } from './remaining-characters-info' +import { NumberOfLinesInDocumentInfo } from './number-of-lines-in-document-info' +import { CursorPositionInfo } from './cursor-position-info' +import { SelectionInfo } from './selection-info' +import { ShowIf } from '../../../common/show-if/show-if' +import { SeparatorDash } from './separator-dash' export interface StatusBarInfo { position: Position @@ -29,57 +32,36 @@ export const defaultState: StatusBarInfo = { remainingCharacters: 0 } -export const createStatusInfo = (editor: Editor, maxDocumentLength: number): StatusBarInfo => ({ - position: editor.getCursor(), - charactersInDocument: editor.getValue().length, - remainingCharacters: maxDocumentLength - editor.getValue().length, - linesInDocument: editor.lineCount(), - selectedColumns: editor.getSelection().length, - selectedLines: editor.getSelection().split('\n').length -}) - -export const StatusBar: React.FC = ({ - position, - selectedColumns, - selectedLines, - charactersInDocument, - linesInDocument, - remainingCharacters -}) => { - const { t } = useTranslation() - - const getLengthTooltip = useMemo(() => { - if (remainingCharacters === 0) { - return t('editor.statusBar.lengthTooltip.maximumReached') - } - if (remainingCharacters < 0) { - return t('editor.statusBar.lengthTooltip.exceeded', { exceeded: -remainingCharacters }) - } - return t('editor.statusBar.lengthTooltip.remaining', { remaining: remainingCharacters }) - }, [remainingCharacters, t]) +export interface StatusBarProps { + statusBarInfo: StatusBarInfo +} +/** + * Shows additional information about the document length and the current selection. + * + * @param statusBarInfo The information to show + */ +export const StatusBar: React.FC = ({ statusBarInfo }) => { return (
- {t('editor.statusBar.cursor', { line: position.line + 1, columns: position.ch + 1 })} - - -  – {t('editor.statusBar.selection.column', { count: selectedColumns })} - - 1}> -  – {t('editor.statusBar.selection.line', { count: selectedLines })} - + + 0}> + + + + 1}> + +
- {t('editor.statusBar.lines', { lines: linesInDocument })} -  –  - - {t('editor.statusBar.length', { length: charactersInDocument })} - + + +
)