mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-22 11:15:23 -04:00
feat(editor): re-add editor mode buttons (edit/both/view)
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
f30f0d8e51
commit
f9b6f6851b
11 changed files with 139 additions and 393 deletions
|
@ -10,6 +10,7 @@ import { NoteTitleElement } from '../../../../../components/layout/app-bar/app-b
|
||||||
import { BaseAppBar } from '../../../../../components/layout/app-bar/base-app-bar'
|
import { BaseAppBar } from '../../../../../components/layout/app-bar/base-app-bar'
|
||||||
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
|
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { EditorModeExtendedAppBar } from './editor-mode-extended-app-bar'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the EditorAppBar that extends the {@link BaseAppBar} with the note title or realtime connection alert.
|
* Renders the EditorAppBar that extends the {@link BaseAppBar} with the note title or realtime connection alert.
|
||||||
|
@ -22,15 +23,15 @@ export const EditorAppBar: React.FC = () => {
|
||||||
return <BaseAppBar />
|
return <BaseAppBar />
|
||||||
} else if (isSynced) {
|
} else if (isSynced) {
|
||||||
return (
|
return (
|
||||||
<BaseAppBar>
|
<EditorModeExtendedAppBar>
|
||||||
<NoteTitleElement />
|
<NoteTitleElement />
|
||||||
</BaseAppBar>
|
</EditorModeExtendedAppBar>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<BaseAppBar>
|
<EditorModeExtendedAppBar>
|
||||||
<RealtimeConnectionAlert />
|
<RealtimeConnectionAlert />
|
||||||
</BaseAppBar>
|
</EditorModeExtendedAppBar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import type { PropsWithChildren } from 'react'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import React, { Fragment } from 'react'
|
||||||
|
import { BaseAppBar } from '../../../../../components/layout/app-bar/base-app-bar'
|
||||||
|
import { ButtonGroup } from 'react-bootstrap'
|
||||||
|
import { Eye as IconEye, FileText as IconFileText, WindowSplit as IconWindowSplit } from 'react-bootstrap-icons'
|
||||||
|
import { IconButton } from '../../../../../components/common/icon-button/icon-button'
|
||||||
|
import { setEditorSplitPosition } from '../../../../../redux/editor-config/methods'
|
||||||
|
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
|
||||||
|
import { useTranslatedText } from '../../../../../hooks/common/use-translated-text'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended AppBar for the editor mode that includes buttons to switch between the different editor modes
|
||||||
|
*/
|
||||||
|
export const EditorModeExtendedAppBar: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
|
const splitValue = useApplicationState((state) => state.editorConfig.splitPosition)
|
||||||
|
|
||||||
|
const onClickEditorOnly = useCallback(() => {
|
||||||
|
setEditorSplitPosition(100)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onClickBothViews = useCallback(() => {
|
||||||
|
setEditorSplitPosition(50)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onClickViewOnly = useCallback(() => {
|
||||||
|
setEditorSplitPosition(0)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const titleEditorOnly = useTranslatedText('editor.viewMode.edit')
|
||||||
|
const titleBothViews = useTranslatedText('editor.viewMode.both')
|
||||||
|
const titleViewOnly = useTranslatedText('editor.viewMode.view')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseAppBar
|
||||||
|
additionalContentLeft={
|
||||||
|
<Fragment>
|
||||||
|
<ButtonGroup>
|
||||||
|
<IconButton
|
||||||
|
icon={IconFileText}
|
||||||
|
title={titleEditorOnly}
|
||||||
|
onClick={onClickEditorOnly}
|
||||||
|
variant={splitValue === 100 ? 'secondary' : 'outline-secondary'}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={IconWindowSplit}
|
||||||
|
title={titleBothViews}
|
||||||
|
onClick={onClickBothViews}
|
||||||
|
variant={splitValue > 0 && splitValue < 100 ? 'secondary' : 'outline-secondary'}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={IconEye}
|
||||||
|
title={titleViewOnly}
|
||||||
|
onClick={onClickViewOnly}
|
||||||
|
variant={splitValue === 0 ? 'secondary' : 'outline-secondary'}
|
||||||
|
/>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Fragment>
|
||||||
|
}>
|
||||||
|
{children}
|
||||||
|
</BaseAppBar>
|
||||||
|
)
|
||||||
|
}
|
|
@ -59,183 +59,6 @@ exports[`Splitter resize can change size with mouse 1`] = `
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Splitter resize can change size with mouse 2`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="flex-fill flex-row d-flex "
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="left"
|
|
||||||
style="width: calc(50% - 5px);"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="inner"
|
|
||||||
>
|
|
||||||
left
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="divider"
|
|
||||||
data-testid="splitter-divider"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="middle"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="buttons"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="btn btn-light"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
BootstrapIconMock_ArrowLeft
|
|
||||||
</button>
|
|
||||||
<span
|
|
||||||
class="grabber"
|
|
||||||
>
|
|
||||||
BootstrapIconMock_ArrowLeftRight
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
class="btn btn-light"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
BootstrapIconMock_ArrowRight
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="right"
|
|
||||||
style="width: calc(100% - 50%);"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="inner"
|
|
||||||
>
|
|
||||||
right
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Splitter resize can change size with mouse 3`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="flex-fill flex-row d-flex "
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="left"
|
|
||||||
style="width: calc(50% - 5px);"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="inner"
|
|
||||||
>
|
|
||||||
left
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="divider"
|
|
||||||
data-testid="splitter-divider"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="middle"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="buttons"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="btn btn-light"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
BootstrapIconMock_ArrowLeft
|
|
||||||
</button>
|
|
||||||
<span
|
|
||||||
class="grabber"
|
|
||||||
>
|
|
||||||
BootstrapIconMock_ArrowLeftRight
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
class="btn btn-light"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
BootstrapIconMock_ArrowRight
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="right"
|
|
||||||
style="width: calc(100% - 50%);"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="inner"
|
|
||||||
>
|
|
||||||
right
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Splitter resize can change size with mouse 4`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="flex-fill flex-row d-flex "
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="left"
|
|
||||||
style="width: calc(50% - 5px);"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="inner"
|
|
||||||
>
|
|
||||||
left
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="divider"
|
|
||||||
data-testid="splitter-divider"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="middle"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="buttons"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="btn btn-light"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
BootstrapIconMock_ArrowLeft
|
|
||||||
</button>
|
|
||||||
<span
|
|
||||||
class="grabber"
|
|
||||||
>
|
|
||||||
BootstrapIconMock_ArrowLeftRight
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
class="btn btn-light"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
BootstrapIconMock_ArrowRight
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="right"
|
|
||||||
style="width: calc(100% - 50%);"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="inner"
|
|
||||||
>
|
|
||||||
right
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Splitter resize can change size with touch 1`] = `
|
exports[`Splitter resize can change size with touch 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
|
@ -471,180 +294,3 @@ exports[`Splitter resize can change size with touch 4`] = `
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Splitter resize can react to shortcuts 1`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="flex-fill flex-row d-flex "
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="left"
|
|
||||||
style="width: calc(0% - 5px);"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="inner"
|
|
||||||
>
|
|
||||||
left
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="divider"
|
|
||||||
data-testid="splitter-divider"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="middle shift-right"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="buttons"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="btn btn-secondary"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
BootstrapIconMock_ArrowLeft
|
|
||||||
</button>
|
|
||||||
<span
|
|
||||||
class="grabber"
|
|
||||||
>
|
|
||||||
BootstrapIconMock_ArrowLeftRight
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
class="btn btn-light"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
BootstrapIconMock_ArrowRight
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="right"
|
|
||||||
style="width: calc(100% - 0%);"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="inner"
|
|
||||||
>
|
|
||||||
right
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Splitter resize can react to shortcuts 2`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="flex-fill flex-row d-flex "
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="left"
|
|
||||||
style="width: calc(100% - 5px);"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="inner"
|
|
||||||
>
|
|
||||||
left
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="divider"
|
|
||||||
data-testid="splitter-divider"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="middle shift-left"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="buttons"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="btn btn-light"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
BootstrapIconMock_ArrowLeft
|
|
||||||
</button>
|
|
||||||
<span
|
|
||||||
class="grabber"
|
|
||||||
>
|
|
||||||
BootstrapIconMock_ArrowLeftRight
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
class="btn btn-secondary"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
BootstrapIconMock_ArrowRight
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="right"
|
|
||||||
style="width: calc(100% - 100%);"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="inner"
|
|
||||||
>
|
|
||||||
right
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Splitter resize can react to shortcuts 3`] = `
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="flex-fill flex-row d-flex "
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="left"
|
|
||||||
style="width: calc(50% - 5px);"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="inner"
|
|
||||||
>
|
|
||||||
left
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="divider"
|
|
||||||
data-testid="splitter-divider"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="middle"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="buttons"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="btn btn-light"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
BootstrapIconMock_ArrowLeft
|
|
||||||
</button>
|
|
||||||
<span
|
|
||||||
class="grabber"
|
|
||||||
>
|
|
||||||
BootstrapIconMock_ArrowLeftRight
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
class="btn btn-light"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
BootstrapIconMock_ArrowRight
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="right"
|
|
||||||
style="width: calc(100% - 50%);"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="inner"
|
|
||||||
>
|
|
||||||
right
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
|
@ -1,30 +1,29 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
import { setEditorSplitPosition } from '../../../../redux/editor-config/methods'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds global keyboard shortcuts for setting the split value.
|
* Binds global keyboard shortcuts for setting the split value.
|
||||||
*
|
|
||||||
* @param setRelativeSplitValue A function that is used to set the split value
|
|
||||||
*/
|
*/
|
||||||
export const useKeyboardShortcuts = (setRelativeSplitValue: (value: number) => void) => {
|
export const useKeyboardShortcuts = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const shortcutHandler = (event: KeyboardEvent): void => {
|
const shortcutHandler = (event: KeyboardEvent): void => {
|
||||||
if (event.ctrlKey && event.altKey && event.key === 'b') {
|
if (event.ctrlKey && event.altKey && event.key === 'b') {
|
||||||
setRelativeSplitValue(50)
|
setEditorSplitPosition(50)
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.ctrlKey && event.altKey && event.key === 'v') {
|
if (event.ctrlKey && event.altKey && event.key === 'v') {
|
||||||
setRelativeSplitValue(0)
|
setEditorSplitPosition(0)
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.ctrlKey && event.altKey && (event.key === 'e' || event.key === '€')) {
|
if (event.ctrlKey && event.altKey && (event.key === 'e' || event.key === '€')) {
|
||||||
setRelativeSplitValue(100)
|
setEditorSplitPosition(100)
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,5 +32,5 @@ export const useKeyboardShortcuts = (setRelativeSplitValue: (value: number) => v
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', shortcutHandler, false)
|
document.removeEventListener('keydown', shortcutHandler, false)
|
||||||
}
|
}
|
||||||
}, [setRelativeSplitValue])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { Splitter } from './splitter'
|
import { Splitter } from './splitter'
|
||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { Mock } from 'ts-mockery'
|
import { Mock } from 'ts-mockery'
|
||||||
|
import * as EditorConfigModule from '../../../redux/editor-config/methods'
|
||||||
|
import { mockAppState } from '../../../test-utils/mock-app-state'
|
||||||
|
import type { EditorConfig } from '../../../redux/editor-config/types'
|
||||||
|
|
||||||
|
jest.mock('../../../hooks/common/use-application-state')
|
||||||
|
jest.mock('../../../redux/editor-config/methods')
|
||||||
|
|
||||||
|
const setEditorSplitPosition = jest.spyOn(EditorConfigModule, 'setEditorSplitPosition').mockReturnValue()
|
||||||
|
|
||||||
describe('Splitter', () => {
|
describe('Splitter', () => {
|
||||||
describe('resize', () => {
|
describe('resize', () => {
|
||||||
|
@ -15,16 +23,22 @@ describe('Splitter', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can react to shortcuts', () => {
|
it('can react to shortcuts', () => {
|
||||||
const view = render(<Splitter left={<>left</>} right={<>right</>} />)
|
render(<Splitter left={<>left</>} right={<>right</>} />)
|
||||||
|
|
||||||
fireEvent.keyDown(document, Mock.of<KeyboardEvent>({ ctrlKey: true, altKey: true, key: 'v' }))
|
fireEvent.keyDown(document, Mock.of<KeyboardEvent>({ ctrlKey: true, altKey: true, key: 'v' }))
|
||||||
expect(view.container).toMatchSnapshot()
|
expect(setEditorSplitPosition).toHaveBeenCalledWith(0)
|
||||||
|
|
||||||
fireEvent.keyDown(document, Mock.of<KeyboardEvent>({ ctrlKey: true, altKey: true, key: 'e' }))
|
fireEvent.keyDown(document, Mock.of<KeyboardEvent>({ ctrlKey: true, altKey: true, key: 'e' }))
|
||||||
expect(view.container).toMatchSnapshot()
|
expect(setEditorSplitPosition).toHaveBeenCalledWith(100)
|
||||||
|
|
||||||
fireEvent.keyDown(document, Mock.of<KeyboardEvent>({ ctrlKey: true, altKey: true, key: 'b' }))
|
fireEvent.keyDown(document, Mock.of<KeyboardEvent>({ ctrlKey: true, altKey: true, key: 'b' }))
|
||||||
expect(view.container).toMatchSnapshot()
|
expect(setEditorSplitPosition).toHaveBeenCalledWith(50)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can change size with mouse', async () => {
|
it('can change size with mouse', async () => {
|
||||||
|
mockAppState({
|
||||||
|
editorConfig: { splitPosition: 50 } as EditorConfig
|
||||||
|
})
|
||||||
const view = render(<Splitter left={<>left</>} right={<>right</>} />)
|
const view = render(<Splitter left={<>left</>} right={<>right</>} />)
|
||||||
expect(view.container).toMatchSnapshot()
|
expect(view.container).toMatchSnapshot()
|
||||||
const divider = await screen.findByTestId('splitter-divider')
|
const divider = await screen.findByTestId('splitter-divider')
|
||||||
|
@ -32,15 +46,15 @@ describe('Splitter', () => {
|
||||||
fireEvent.mouseDown(divider, {})
|
fireEvent.mouseDown(divider, {})
|
||||||
fireEvent.mouseMove(window, Mock.of<MouseEvent>({ buttons: 1, clientX: 1920 }))
|
fireEvent.mouseMove(window, Mock.of<MouseEvent>({ buttons: 1, clientX: 1920 }))
|
||||||
fireEvent.mouseUp(window)
|
fireEvent.mouseUp(window)
|
||||||
expect(view.container).toMatchSnapshot()
|
expect(setEditorSplitPosition).toHaveBeenCalledWith(100)
|
||||||
|
|
||||||
fireEvent.mouseDown(divider, {})
|
fireEvent.mouseDown(divider, {})
|
||||||
fireEvent.mouseMove(window, Mock.of<MouseEvent>({ buttons: 1, clientX: 0 }))
|
fireEvent.mouseMove(window, Mock.of<MouseEvent>({ buttons: 1, clientX: 0 }))
|
||||||
fireEvent.mouseUp(window)
|
fireEvent.mouseUp(window)
|
||||||
expect(view.container).toMatchSnapshot()
|
expect(setEditorSplitPosition).toHaveBeenCalledWith(0)
|
||||||
|
|
||||||
fireEvent.mouseMove(window, Mock.of<MouseEvent>({ buttons: 1, clientX: 1920 }))
|
fireEvent.mouseMove(window, Mock.of<MouseEvent>({ buttons: 1, clientX: 1920 }))
|
||||||
expect(view.container).toMatchSnapshot()
|
expect(setEditorSplitPosition).toHaveBeenCalledWith(100)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can change size with touch', async () => {
|
it('can change size with touch', async () => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -8,6 +8,8 @@ import { DividerButtonsShift, SplitDivider } from './split-divider/split-divider
|
||||||
import styles from './splitter.module.scss'
|
import styles from './splitter.module.scss'
|
||||||
import type { MouseEvent, ReactElement, TouchEvent } from 'react'
|
import type { MouseEvent, ReactElement, TouchEvent } from 'react'
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { useApplicationState } from '../../../hooks/common/use-application-state'
|
||||||
|
import { setEditorSplitPosition } from '../../../redux/editor-config/methods'
|
||||||
|
|
||||||
export interface SplitterProps {
|
export interface SplitterProps {
|
||||||
left?: ReactElement
|
left?: ReactElement
|
||||||
|
@ -54,7 +56,7 @@ const SNAP_PERCENTAGE = 10
|
||||||
* @return the created component
|
* @return the created component
|
||||||
*/
|
*/
|
||||||
export const Splitter: React.FC<SplitterProps> = ({ additionalContainerClassName, left, right }) => {
|
export const Splitter: React.FC<SplitterProps> = ({ additionalContainerClassName, left, right }) => {
|
||||||
const [relativeSplitValue, setRelativeSplitValue] = useState(50)
|
const relativeSplitValue = useApplicationState((state) => state.editorConfig.splitPosition)
|
||||||
const [resizingInProgress, setResizingInProgress] = useState(false)
|
const [resizingInProgress, setResizingInProgress] = useState(false)
|
||||||
const adjustedRelativeSplitValue = useMemo(() => Math.min(100, Math.max(0, relativeSplitValue)), [relativeSplitValue])
|
const adjustedRelativeSplitValue = useMemo(() => Math.min(100, Math.max(0, relativeSplitValue)), [relativeSplitValue])
|
||||||
const splitContainer = useRef<HTMLDivElement>(null)
|
const splitContainer = useRef<HTMLDivElement>(null)
|
||||||
|
@ -73,19 +75,19 @@ export const Splitter: React.FC<SplitterProps> = ({ additionalContainerClassName
|
||||||
setResizingInProgress(false)
|
setResizingInProgress(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves the splitter to the left or right side if the relative split value is close to the edges.
|
||||||
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!resizingInProgress) {
|
if (!resizingInProgress) {
|
||||||
setRelativeSplitValue((value) => {
|
if (relativeSplitValue < SNAP_PERCENTAGE && relativeSplitValue > 0) {
|
||||||
if (value < SNAP_PERCENTAGE) {
|
setEditorSplitPosition(0)
|
||||||
return 0
|
}
|
||||||
}
|
if (relativeSplitValue > 100 - SNAP_PERCENTAGE && relativeSplitValue < 100) {
|
||||||
if (value > 100 - SNAP_PERCENTAGE) {
|
setEditorSplitPosition(100)
|
||||||
return 100
|
}
|
||||||
}
|
|
||||||
return value
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}, [resizingInProgress])
|
}, [resizingInProgress, relativeSplitValue])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recalculates the panel split based on the absolute mouse/touch position.
|
* Recalculates the panel split based on the absolute mouse/touch position.
|
||||||
|
@ -109,17 +111,17 @@ export const Splitter: React.FC<SplitterProps> = ({ additionalContainerClassName
|
||||||
const horizontalPositionInSplitContainer = horizontalPosition - splitContainer.current.offsetLeft
|
const horizontalPositionInSplitContainer = horizontalPosition - splitContainer.current.offsetLeft
|
||||||
const newRelativeSize = horizontalPositionInSplitContainer / splitContainer.current.clientWidth
|
const newRelativeSize = horizontalPositionInSplitContainer / splitContainer.current.clientWidth
|
||||||
const number = newRelativeSize * 100
|
const number = newRelativeSize * 100
|
||||||
setRelativeSplitValue(number)
|
setEditorSplitPosition(number)
|
||||||
moveEvent.preventDefault()
|
moveEvent.preventDefault()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onLeftButtonClick = useCallback(() => {
|
const onLeftButtonClick = useCallback(() => {
|
||||||
setRelativeSplitValue((value) => (value === 100 ? 50 : 0))
|
setEditorSplitPosition(relativeSplitValue === 100 ? 50 : 0)
|
||||||
}, [])
|
}, [relativeSplitValue])
|
||||||
|
|
||||||
const onRightButtonClick = useCallback(() => {
|
const onRightButtonClick = useCallback(() => {
|
||||||
setRelativeSplitValue((value) => (value === 0 ? 50 : 100))
|
setEditorSplitPosition(relativeSplitValue === 0 ? 50 : 100)
|
||||||
}, [])
|
}, [relativeSplitValue])
|
||||||
|
|
||||||
const dividerButtonsShift = useMemo(() => {
|
const dividerButtonsShift = useMemo(() => {
|
||||||
if (relativeSplitValue === 0) {
|
if (relativeSplitValue === 0) {
|
||||||
|
@ -131,7 +133,7 @@ export const Splitter: React.FC<SplitterProps> = ({ additionalContainerClassName
|
||||||
}
|
}
|
||||||
}, [relativeSplitValue])
|
}, [relativeSplitValue])
|
||||||
|
|
||||||
useKeyboardShortcuts(setRelativeSplitValue)
|
useKeyboardShortcuts()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -16,10 +16,14 @@ import React from 'react'
|
||||||
import { Nav, Navbar } from 'react-bootstrap'
|
import { Nav, Navbar } from 'react-bootstrap'
|
||||||
import { cypressId } from '../../../utils/cypress-attribute'
|
import { cypressId } from '../../../utils/cypress-attribute'
|
||||||
|
|
||||||
|
export interface BaseAppBarProps {
|
||||||
|
additionalContentLeft?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the base app bar with branding, help, settings user elements.
|
* Renders the base app bar with branding, help, settings user elements.
|
||||||
*/
|
*/
|
||||||
export const BaseAppBar: React.FC<PropsWithChildren> = ({ children }) => {
|
export const BaseAppBar: React.FC<PropsWithChildren<BaseAppBarProps>> = ({ children, additionalContentLeft }) => {
|
||||||
return (
|
return (
|
||||||
<Navbar
|
<Navbar
|
||||||
expand={true}
|
expand={true}
|
||||||
|
@ -27,6 +31,7 @@ export const BaseAppBar: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
{...cypressId('base-app-bar')}>
|
{...cypressId('base-app-bar')}>
|
||||||
<Nav className={`align-items-center justify-content-start gap-2 flex-grow-1 ${styles.side}`}>
|
<Nav className={`align-items-center justify-content-start gap-2 flex-grow-1 ${styles.side}`}>
|
||||||
<BrandingElement />
|
<BrandingElement />
|
||||||
|
{additionalContentLeft}
|
||||||
</Nav>
|
</Nav>
|
||||||
<Nav className={`align-items-center flex-fill overflow-hidden px-4 ${styles.center}`}>{children}</Nav>
|
<Nav className={`align-items-center flex-fill overflow-hidden px-4 ${styles.center}`}>{children}</Nav>
|
||||||
<Nav className={`align-items-stretch justify-content-end flex-grow-1 ${styles.side} h-100 py-1`}>
|
<Nav className={`align-items-stretch justify-content-end flex-grow-1 ${styles.side} h-100 py-1`}>
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
import type { EditorConfig } from './types'
|
import type { EditorConfig } from './types'
|
||||||
|
|
||||||
export const initialState: EditorConfig = {
|
export const initialState: EditorConfig = {
|
||||||
|
splitPosition: 50,
|
||||||
ligatures: true,
|
ligatures: true,
|
||||||
syncScroll: true,
|
syncScroll: true,
|
||||||
smartPaste: true,
|
smartPaste: true,
|
||||||
|
|
|
@ -12,6 +12,12 @@ import { Logger } from '../../utils/logger'
|
||||||
|
|
||||||
const log = new Logger('Redux > EditorConfig')
|
const log = new Logger('Redux > EditorConfig')
|
||||||
|
|
||||||
|
export const setEditorSplitPosition = (splitPosition: number): void => {
|
||||||
|
const action = editorConfigActionsCreator.setSplitPosition(splitPosition)
|
||||||
|
store.dispatch(action)
|
||||||
|
saveToLocalStorage()
|
||||||
|
}
|
||||||
|
|
||||||
export const setEditorSyncScroll = (syncScroll: boolean): void => {
|
export const setEditorSyncScroll = (syncScroll: boolean): void => {
|
||||||
const action = editorConfigActionsCreator.setSyncScroll(syncScroll)
|
const action = editorConfigActionsCreator.setSyncScroll(syncScroll)
|
||||||
store.dispatch(action)
|
store.dispatch(action)
|
||||||
|
|
|
@ -12,6 +12,9 @@ const editorConfigSlice = createSlice({
|
||||||
name: 'editorConfig',
|
name: 'editorConfig',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
|
setSplitPosition: (state, action: PayloadAction<EditorConfig['splitPosition']>) => {
|
||||||
|
state.splitPosition = action.payload
|
||||||
|
},
|
||||||
setSyncScroll: (state, action: PayloadAction<EditorConfig['syncScroll']>) => {
|
setSyncScroll: (state, action: PayloadAction<EditorConfig['syncScroll']>) => {
|
||||||
state.syncScroll = action.payload
|
state.syncScroll = action.payload
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
export interface EditorConfig {
|
export interface EditorConfig {
|
||||||
|
splitPosition: number
|
||||||
syncScroll: boolean
|
syncScroll: boolean
|
||||||
ligatures: boolean
|
ligatures: boolean
|
||||||
smartPaste: boolean
|
smartPaste: boolean
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue