mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 11:16:31 -05:00
Replace document bar with sidebar (#937)
Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
parent
586969f368
commit
0627e0f551
54 changed files with 1067 additions and 604 deletions
|
@ -61,6 +61,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
|
|||
- A toggle in the editor preferences for turning ligatures on and off.
|
||||
- Easier possibility to share notes via native share-buttons on supported devices.
|
||||
- Surround selected text with a link via shortcut (ctrl+k or cmd+k).
|
||||
- A sidebar for menu options
|
||||
- Improved security by wrapping the markdown rendering into an iframe
|
||||
|
||||
### Changed
|
||||
|
|
|
@ -14,9 +14,9 @@ describe('Export', () => {
|
|||
})
|
||||
|
||||
it('Markdown', () => {
|
||||
cy.get('#editor-menu-export')
|
||||
cy.get('[data-cy="menu-export"]')
|
||||
.click()
|
||||
cy.get('a.dropdown-item > i.fa-file-text')
|
||||
cy.get('[data-cy="menu-export-markdown"]')
|
||||
.click()
|
||||
cy.get('a[download]')
|
||||
.then((anchor) => (
|
||||
|
|
|
@ -10,11 +10,11 @@ describe('Import markdown file', () => {
|
|||
})
|
||||
|
||||
it('import on blank note', () => {
|
||||
cy.get('button#editor-menu-import')
|
||||
cy.get('[data-cy="menu-import"]')
|
||||
.click()
|
||||
cy.get('.import-md-file')
|
||||
cy.get('[data-cy="menu-import-markdown"]')
|
||||
.click()
|
||||
cy.get('div[aria-labelledby="editor-menu-import"] > input[type=file]')
|
||||
cy.get('[data-cy="menu-import-markdown-input"]')
|
||||
.attachFile({ filePath: 'import.md', mimeType: 'text/markdown' })
|
||||
cy.get('.CodeMirror-code > div:nth-of-type(1) > .CodeMirror-line > span > span')
|
||||
.should('have.text', '# Some short import test file')
|
||||
|
@ -25,11 +25,11 @@ describe('Import markdown file', () => {
|
|||
it('import on note with content', () => {
|
||||
|
||||
cy.codemirrorFill('test\nabc')
|
||||
cy.get('button#editor-menu-import')
|
||||
cy.get('[data-cy="menu-import"]')
|
||||
.click()
|
||||
cy.get('.import-md-file')
|
||||
cy.get('[data-cy="menu-import-markdown"]')
|
||||
.click()
|
||||
cy.get('div[aria-labelledby="editor-menu-import"] > input[type=file]')
|
||||
cy.get('[data-cy="menu-import-markdown-input"]')
|
||||
.attachFile({ filePath: 'import.md', mimeType: 'text/markdown' })
|
||||
cy.get('.CodeMirror-code > div:nth-of-type(1) > .CodeMirror-line > span > span')
|
||||
.should('have.text', 'test')
|
||||
|
|
|
@ -104,7 +104,7 @@ describe('Toolbar Buttons', () => {
|
|||
.click()
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', `# ${testText}`)
|
||||
cy.get('.btn-toolbar [data-cy="format-heading"]')
|
||||
cy.get('.fa-header')
|
||||
.click()
|
||||
cy.get('.CodeMirror-activeline > .CodeMirror-line > span')
|
||||
.should('have.text', `## ${testText}`)
|
||||
|
|
|
@ -224,6 +224,9 @@
|
|||
"highlightedText": "Highlight"
|
||||
}
|
||||
},
|
||||
"onlineStatus": {
|
||||
"online": "Online"
|
||||
},
|
||||
"error": {
|
||||
"locked": {
|
||||
"title": "This note is locked",
|
||||
|
@ -260,6 +263,7 @@
|
|||
"switchToLight": "Switch to Light Mode"
|
||||
},
|
||||
"appBar": {
|
||||
"new": "New",
|
||||
"syncScroll": {
|
||||
"disable": "Disable sync scroll",
|
||||
"enable": "Enable sync scroll"
|
||||
|
@ -299,16 +303,11 @@
|
|||
"menu": "Menu",
|
||||
"import": "Import",
|
||||
"export": "Export",
|
||||
"new": "New",
|
||||
"shareLink": "Share link",
|
||||
"extra": "Extra",
|
||||
"revision": "Revision",
|
||||
"slideMode": "Slide Mode",
|
||||
"download": "Download",
|
||||
"help": "Help",
|
||||
"deleteNote": "Delete note",
|
||||
"permissions": "Permissions",
|
||||
"documentInfo": "Document info",
|
||||
"pinNoteToHistory": "Pin note to history",
|
||||
"pinnedToHistory": "Pinned to history"
|
||||
},
|
||||
|
@ -328,7 +327,8 @@
|
|||
},
|
||||
"export": {
|
||||
"rawHtml": "Raw HTML",
|
||||
"pdf": "PDF export is unavailable."
|
||||
"pdf": "PDF export is unavailable.",
|
||||
"markdown-file": "Markdown file"
|
||||
},
|
||||
"import": {
|
||||
"clipboard": "Clipboard",
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useCallback, useRef } from 'react'
|
||||
import { Button, Dropdown } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon'
|
||||
import { IconName } from '../fork-awesome/types'
|
||||
|
||||
export interface HiddenInputMenuEntryProps {
|
||||
type: 'dropdown' | 'button'
|
||||
acceptedFiles: string
|
||||
i18nKey: string
|
||||
icon: IconName
|
||||
onLoad: (file: File) => Promise<void>
|
||||
}
|
||||
|
||||
export const HiddenInputMenuEntry: React.FC<HiddenInputMenuEntryProps> = ({ type, acceptedFiles, i18nKey, icon, onLoad }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const fileInputReference = useRef<HTMLInputElement>(null)
|
||||
const onClick = useCallback(() => {
|
||||
const fileInput = fileInputReference.current
|
||||
if (!fileInput) {
|
||||
return
|
||||
}
|
||||
fileInput.addEventListener('change', () => {
|
||||
if (!fileInput.files || fileInput.files.length < 1) {
|
||||
return
|
||||
}
|
||||
const file = fileInput.files[0]
|
||||
onLoad(file).then(() => {
|
||||
fileInput.value = ''
|
||||
}).catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
})
|
||||
fileInput.click()
|
||||
}, [onLoad])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<input type='file' ref={fileInputReference} className='d-none' accept={acceptedFiles}/>
|
||||
{
|
||||
type === 'dropdown'
|
||||
? <Dropdown.Item className={'small import-md-file'} onClick={onClick}>
|
||||
<ForkAwesomeIcon icon={icon} className={'mx-2'}/>
|
||||
<Trans i18nKey={i18nKey}/>
|
||||
</Dropdown.Item>
|
||||
: <Button variant='light' onClick={onClick} title={t(i18nKey)}>
|
||||
<ForkAwesomeIcon icon={icon}/>
|
||||
</Button>
|
||||
}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -29,7 +29,7 @@ const UserAvatar: React.FC<UserAvatarProps> = ({ name, photo, size, additionalCl
|
|||
title={name}
|
||||
/>
|
||||
<ShowIf condition={showName}>
|
||||
<span className="mx-1">{name}</span>
|
||||
<span className="mx-1 user-line-name">{name}</span>
|
||||
</ShowIf>
|
||||
</span>
|
||||
)
|
||||
|
|
|
@ -56,7 +56,7 @@ export const AppBar: React.FC<AppBarProps> = ({ mode }) => {
|
|||
</Nav>
|
||||
<Nav className="d-flex align-items-center text-secondary">
|
||||
<Button className="mx-2" size="sm" variant="primary">
|
||||
<ForkAwesomeIcon icon="plus"/> <Trans i18nKey="editor.documentBar.new"/>
|
||||
<ForkAwesomeIcon icon="plus"/> <Trans i18nKey="editor.appBar.new"/>
|
||||
</Button>
|
||||
<ShowIf condition={!userExists}>
|
||||
<SignInButton size={'sm'} />
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { TranslatedIconButton } from '../../../common/icon-button/translated-icon-button'
|
||||
|
||||
export const PinToHistoryButton: React.FC = () => {
|
||||
useTranslation()
|
||||
|
||||
const isPinned = true
|
||||
const i18nKey = isPinned ? 'editor.documentBar.pinNoteToHistory' : 'editor.documentBar.pinnedToHistory'
|
||||
|
||||
return <TranslatedIconButton size={'sm'} className={'mx-1'} icon={'thumb-tack'} variant={'light'} i18nKey={i18nKey}/>
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import equal from 'fast-deep-equal'
|
||||
import React, { Fragment, useState } from 'react'
|
||||
import { Modal } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useFrontendBaseUrl } from '../../../../hooks/common/use-frontend-base-url'
|
||||
import { ApplicationState } from '../../../../redux'
|
||||
import { CopyableField } from '../../../common/copyable/copyable-field/copyable-field'
|
||||
import { TranslatedIconButton } from '../../../common/icon-button/translated-icon-button'
|
||||
import { CommonModal } from '../../../common/modals/common-modal'
|
||||
import { ShowIf } from '../../../common/show-if/show-if'
|
||||
import { EditorPathParams } from '../../editor'
|
||||
|
||||
export const ShareLinkButton: React.FC = () => {
|
||||
useTranslation()
|
||||
const [showShareDialog, setShowShareDialog] = useState(false)
|
||||
const noteMetadata = useSelector((state: ApplicationState) => state.documentContent.metadata, equal)
|
||||
const editorMode = useSelector((state: ApplicationState) => state.editorConfig.editorMode)
|
||||
const baseUrl = useFrontendBaseUrl()
|
||||
const { id } = useParams<EditorPathParams>()
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<TranslatedIconButton
|
||||
size={'sm'}
|
||||
className={'mx-1'}
|
||||
icon={'share'}
|
||||
variant={'light'}
|
||||
onClick={() => setShowShareDialog(true)}
|
||||
i18nKey={'editor.documentBar.shareLink'}
|
||||
/>
|
||||
<CommonModal
|
||||
show={showShareDialog}
|
||||
onHide={() => setShowShareDialog(false)}
|
||||
closeButton={true}
|
||||
titleI18nKey={'editor.modal.shareLink.title'}>
|
||||
<Modal.Body>
|
||||
<Trans i18nKey={'editor.modal.shareLink.editorDescription'}/>
|
||||
<CopyableField content={`${baseUrl}/n/${id}?${editorMode}`} nativeShareButton={true} url={`${baseUrl}/n/${id}?${editorMode}`}/>
|
||||
<ShowIf condition={noteMetadata.type === 'slide'}>
|
||||
<Trans i18nKey={'editor.modal.shareLink.slidesDescription'}/>
|
||||
<CopyableField content={`${baseUrl}/p/${id}`} nativeShareButton={true} url={`${baseUrl}/p/${id}`}/>
|
||||
</ShowIf>
|
||||
<ShowIf condition={noteMetadata.type === ''}>
|
||||
<Trans i18nKey={'editor.modal.shareLink.viewOnlyDescription'}/>
|
||||
<CopyableField content={`${baseUrl}/s/${id}`} nativeShareButton={true} url={`${baseUrl}/s/${id}`}/>
|
||||
</ShowIf>
|
||||
</Modal.Body>
|
||||
</CommonModal>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
||||
import { ActiveIndicatorStatus } from './active-indicator'
|
||||
import { UserLine } from './user-line'
|
||||
|
||||
const ConnectionIndicator: React.FC = () => {
|
||||
const userOnline = 2
|
||||
return (
|
||||
<Dropdown className="small mx-2" alignRight>
|
||||
<Dropdown.Toggle id="connection-indicator" size="sm" variant="primary" className="text-uppercase">
|
||||
<ForkAwesomeIcon icon="users" className={'mr-1'}/> {userOnline} Online
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item disabled={true} className="d-flex align-items-center p-0">
|
||||
<UserLine name="Philip Molares" photo="/img/avatar.png" color="red" status={ActiveIndicatorStatus.INACTIVE}/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item disabled={true} className="d-flex align-items-center p-0">
|
||||
<UserLine name="Philip Molares" photo="/img/avatar.png" color="blue" status={ActiveIndicatorStatus.ACTIVE}/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
export { ConnectionIndicator }
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
.user-line-color-indicator {
|
||||
border-left: 3px solid;
|
||||
min-height: 30px;
|
||||
height: 100%;
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PinToHistoryButton } from './buttons/pin-to-history-button'
|
||||
import { ShareLinkButton } from './buttons/share-link-button'
|
||||
import { ConnectionIndicator } from './connection-indicator/connection-indicator'
|
||||
import { DocumentInfoButton } from './document-info/document-info-button'
|
||||
import { EditorMenu } from './menus/editor-menu'
|
||||
import { ExportMenu } from './menus/export-menu'
|
||||
import { ImportMenu } from './menus/import-menu'
|
||||
import { PermissionButton } from './permissions/permission-button'
|
||||
import { RevisionButton } from './revisions/revision-button'
|
||||
|
||||
export interface DocumentBarProps {
|
||||
title: string
|
||||
}
|
||||
|
||||
export const DocumentBar: React.FC<DocumentBarProps> = ({ title }) => {
|
||||
useTranslation()
|
||||
|
||||
return (
|
||||
<div className={'navbar navbar-expand navbar-light bg-light'}>
|
||||
<div className="navbar-nav">
|
||||
<ShareLinkButton/>
|
||||
<DocumentInfoButton/>
|
||||
<RevisionButton/>
|
||||
<PinToHistoryButton/>
|
||||
<PermissionButton/>
|
||||
</div>
|
||||
<div className="ml-auto navbar-nav">
|
||||
<ImportMenu/>
|
||||
<ExportMenu title={title}/>
|
||||
<EditorMenu noteTitle={title}/>
|
||||
<ConnectionIndicator/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,29 +1,28 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { DateTime } from 'luxon'
|
||||
import React, { Fragment, useState } from 'react'
|
||||
import React from 'react'
|
||||
import { ListGroup, Modal } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { TranslatedIconButton } from '../../../common/icon-button/translated-icon-button'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { CommonModal } from '../../../common/modals/common-modal'
|
||||
import { DocumentInfoLine } from './document-info-line'
|
||||
import { DocumentInfoLineWithTimeMode, DocumentInfoTimeLine } from './document-info-time-line'
|
||||
import { UnitalicBoldText } from './unitalic-bold-text'
|
||||
|
||||
export const DocumentInfoButton: React.FC = () => {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
useTranslation()
|
||||
export interface DocumentInfoModalProps {
|
||||
show: boolean,
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<TranslatedIconButton size={'sm'} className={'mx-1'} icon={'line-chart'} variant={'light'} onClick={() => setShowModal(true)} i18nKey={'editor.documentBar.documentInfo'}/>
|
||||
export const DocumentInfoModal: React.FC<DocumentInfoModalProps> = ({show, onHide}) => {
|
||||
return (
|
||||
<CommonModal
|
||||
show={showModal}
|
||||
onHide={() => setShowModal(false)}
|
||||
show={show}
|
||||
onHide={onHide}
|
||||
closeButton={true}
|
||||
titleI18nKey={'editor.modal.documentInfo.title'}>
|
||||
<Modal.Body>
|
||||
|
@ -61,6 +60,5 @@ export const DocumentInfoButton: React.FC = () => {
|
|||
</ListGroup>
|
||||
</Modal.Body>
|
||||
</CommonModal>
|
||||
</Fragment>
|
||||
)
|
||||
);
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { DropdownItemWithDeletionModal } from '../../../history-page/entry-menu/dropdown-item-with-deletion-modal'
|
||||
|
||||
export interface EditorMenuProps {
|
||||
noteTitle: string
|
||||
}
|
||||
|
||||
export const EditorMenu: React.FC<EditorMenuProps> = ({ noteTitle }) => {
|
||||
useTranslation()
|
||||
|
||||
return (
|
||||
<Dropdown className={'small mx-1'} alignRight={true}>
|
||||
<Dropdown.Toggle variant='light' size='sm' id='editor-menu'>
|
||||
<Trans i18nKey={'editor.documentBar.menu'}/>
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
<DropdownItemWithDeletionModal
|
||||
itemI18nKey={'landing.history.menu.deleteNote'}
|
||||
modalButtonI18nKey={'editor.modal.deleteNote.button'}
|
||||
modalIcon={'trash'}
|
||||
modalQuestionI18nKey={'editor.modal.deleteNote.question'}
|
||||
modalTitleI18nKey={'editor.modal.deleteNote.title'}
|
||||
modalWarningI18nKey={'editor.modal.deleteNote.warning'}
|
||||
noteTitle={noteTitle}
|
||||
className={'small'}
|
||||
onConfirm={() => console.log('deleted')}/>
|
||||
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import links from '../../../../links.json'
|
||||
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
||||
import { MarkdownExportDropdownItem } from './export/markdown'
|
||||
|
||||
export interface ExportMenuProps {
|
||||
title: string
|
||||
}
|
||||
|
||||
export const ExportMenu: React.FC<ExportMenuProps> = ({ title }) => {
|
||||
useTranslation()
|
||||
return (
|
||||
<Dropdown className='small mx-1' alignRight={true}>
|
||||
<Dropdown.Toggle variant='light' size='sm' id='editor-menu-export' className=''>
|
||||
<Trans i18nKey='editor.documentBar.export'/>
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Header>
|
||||
<Trans i18nKey='common.export'/>
|
||||
</Dropdown.Header>
|
||||
<Dropdown.Item className='small'>
|
||||
<ForkAwesomeIcon icon='dropbox' className={'mx-2'}/>
|
||||
Dropbox
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item className='small'>
|
||||
<ForkAwesomeIcon icon='github' className={'mx-2'}/>
|
||||
Gist
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item className='small'>
|
||||
<ForkAwesomeIcon icon='gitlab' className={'mx-2'}/>
|
||||
Gitlab Snippet
|
||||
</Dropdown.Item>
|
||||
|
||||
<Dropdown.Divider/>
|
||||
|
||||
<Dropdown.Header>
|
||||
<Trans i18nKey='editor.documentBar.download'/>
|
||||
</Dropdown.Header>
|
||||
<MarkdownExportDropdownItem title={title} />
|
||||
<Dropdown.Item className='small'>
|
||||
<ForkAwesomeIcon icon='file-code-o' className={'mx-2'}/>
|
||||
HTML
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item className='small'>
|
||||
<ForkAwesomeIcon icon='file-code-o' className={'mx-2'}/>
|
||||
<Trans i18nKey='editor.export.rawHtml'/>
|
||||
</Dropdown.Item>
|
||||
|
||||
<Dropdown.Divider/>
|
||||
|
||||
<Dropdown.Item className='small text-muted' dir={'auto'} href={links.faq} target={'_blank'} rel='noopener noreferrer'>
|
||||
<ForkAwesomeIcon icon='file-pdf-o' className={'mx-2'}/>
|
||||
<Trans i18nKey={'editor.export.pdf'}/>
|
||||
|
||||
<span className={'text-primary'}>
|
||||
<Trans i18nKey={'common.why'}/>
|
||||
</span>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { ApplicationState } from '../../../../../redux'
|
||||
import { download } from '../../../../common/download/download'
|
||||
import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon'
|
||||
|
||||
export interface MarkdownExportDropdownItemProps {
|
||||
title: string
|
||||
}
|
||||
|
||||
export const MarkdownExportDropdownItem: React.FC<MarkdownExportDropdownItemProps> = ({ title }) => {
|
||||
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
|
||||
|
||||
return (
|
||||
<Dropdown.Item className='small' onClick={() => download(markdownContent, `${title}.md`, 'text/markdown')}>
|
||||
<ForkAwesomeIcon icon='file-text' className={'mx-2'}/>
|
||||
Markdown
|
||||
</Dropdown.Item>
|
||||
)
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { ApplicationState } from '../../../../redux'
|
||||
import { setDocumentContent } from '../../../../redux/document-content/methods'
|
||||
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
||||
import { HiddenInputMenuEntry } from '../../../common/hidden-input-menu-entry/hidden-input-menu-entry'
|
||||
|
||||
export const ImportMenu: React.FC = () => {
|
||||
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
|
||||
|
||||
const onImportMarkdown = useCallback((file: File) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const fileReader = new FileReader()
|
||||
fileReader.addEventListener('load', () => {
|
||||
const newContent = fileReader.result as string
|
||||
if (markdownContent.length === 0) {
|
||||
setDocumentContent(newContent)
|
||||
} else {
|
||||
setDocumentContent(markdownContent + '\n' + newContent)
|
||||
}
|
||||
})
|
||||
fileReader.addEventListener('loadend', () => {
|
||||
resolve()
|
||||
})
|
||||
fileReader.addEventListener('error', (error) => {
|
||||
reject(error)
|
||||
})
|
||||
fileReader.readAsText(file)
|
||||
})
|
||||
}, [markdownContent])
|
||||
|
||||
return (
|
||||
<Dropdown className='small mx-1' alignRight={true}>
|
||||
<Dropdown.Toggle variant='light' size='sm' id='editor-menu-import' className=''>
|
||||
<Trans i18nKey='editor.documentBar.import'/>
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item className='small'>
|
||||
<ForkAwesomeIcon icon='dropbox' className={'mx-2'}/>
|
||||
Dropbox
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item className='small'>
|
||||
<ForkAwesomeIcon icon='github' className={'mx-2'}/>
|
||||
Gist
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item className='small'>
|
||||
<ForkAwesomeIcon icon='gitlab' className={'mx-2'}/>
|
||||
Gitlab Snippet
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item className='small'>
|
||||
<ForkAwesomeIcon icon='clipboard' className={'mx-2'}/>
|
||||
<Trans i18nKey='editor.import.clipboard'/>
|
||||
</Dropdown.Item>
|
||||
<HiddenInputMenuEntry
|
||||
type={'dropdown'}
|
||||
acceptedFiles={'.md, text/markdown, text/plain'}
|
||||
i18nKey={'editor.import.file'}
|
||||
icon={'file-text-o'}
|
||||
onLoad={onImportMarkdown}
|
||||
/>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useState } from 'react'
|
||||
import { TranslatedIconButton } from '../../../common/icon-button/translated-icon-button'
|
||||
import { PermissionModal } from './permission-modal'
|
||||
|
||||
export const PermissionButton: React.FC = () => {
|
||||
const [showPermissionModal, setShowPermissionModal] = useState(false)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<TranslatedIconButton size={'sm'} className={'mx-1'} icon={'lock'} variant={'light'} onClick={() => setShowPermissionModal(true)} i18nKey={'editor.documentBar.permissions'}/>
|
||||
<PermissionModal show={showPermissionModal} onChangeShow={setShowPermissionModal}/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -16,7 +16,7 @@ import { PermissionList } from './permission-list'
|
|||
|
||||
export interface PermissionsModalProps {
|
||||
show: boolean,
|
||||
onChangeShow: (newShow: boolean) => void
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
export interface Principal {
|
||||
|
@ -59,7 +59,7 @@ const permissionsApiResponse: NotePermissions = {
|
|||
}]
|
||||
}
|
||||
|
||||
export const PermissionModal: React.FC<PermissionsModalProps> = ({ show, onChangeShow }) => {
|
||||
export const PermissionModal: React.FC<PermissionsModalProps> = ({ show, onHide }) => {
|
||||
useTranslation()
|
||||
const [error, setError] = useState(false)
|
||||
const [userList, setUserList] = useState<Principal[]>([])
|
||||
|
@ -123,7 +123,7 @@ export const PermissionModal: React.FC<PermissionsModalProps> = ({ show, onChang
|
|||
return (
|
||||
<CommonModal
|
||||
show={show}
|
||||
onHide={() => onChangeShow(false)}
|
||||
onHide={onHide}
|
||||
closeButton={true}
|
||||
titleI18nKey={'editor.modal.permissions.title'}>
|
||||
<Modal.Body>
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useState } from 'react'
|
||||
import { TranslatedIconButton } from '../../../common/icon-button/translated-icon-button'
|
||||
import { RevisionModal } from './revision-modal'
|
||||
|
||||
export const RevisionButton: React.FC = () => {
|
||||
const [show, setShow] = useState(false)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<TranslatedIconButton size={'sm'} className={'mx-1'} icon={'history'} variant={'light'} i18nKey={'editor.documentBar.revision'} onClick={() => setShow(true)}/>
|
||||
<RevisionModal show={show} onHide={() => setShow(false)} titleI18nKey={'editor.modal.revision.title'} icon={'history'}/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -15,13 +15,18 @@ import { Revision, RevisionListEntry } from '../../../../api/revisions/types'
|
|||
import { UserResponse } from '../../../../api/users/types'
|
||||
import { useIsDarkModeActivated } from '../../../../hooks/common/use-is-dark-mode-activated'
|
||||
import { ApplicationState } from '../../../../redux'
|
||||
import { CommonModal, CommonModalProps } from '../../../common/modals/common-modal'
|
||||
import { CommonModal } from '../../../common/modals/common-modal'
|
||||
import { ShowIf } from '../../../common/show-if/show-if'
|
||||
import { RevisionModalListEntry } from './revision-modal-list-entry'
|
||||
import './revision-modal.scss'
|
||||
import { downloadRevision, getUserDataForRevision } from './utils'
|
||||
|
||||
export const RevisionModal: React.FC<CommonModalProps> = ({ show, onHide, icon, titleI18nKey }) => {
|
||||
export interface PermissionsModalProps {
|
||||
show: boolean,
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
export const RevisionModal: React.FC<PermissionsModalProps> = ({ show, onHide }) => {
|
||||
useTranslation()
|
||||
const [revisions, setRevisions] = useState<RevisionListEntry[]>([])
|
||||
const [selectedRevisionTimestamp, setSelectedRevisionTimestamp] = useState<number | null>(null)
|
||||
|
@ -56,7 +61,7 @@ export const RevisionModal: React.FC<CommonModalProps> = ({ show, onHide, icon,
|
|||
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
|
||||
|
||||
return (
|
||||
<CommonModal show={show} onHide={onHide} titleI18nKey={titleI18nKey} icon={icon} closeButton={true} size={'xl'} additionalClasses='revision-modal'>
|
||||
<CommonModal show={show} onHide={onHide} titleI18nKey={'editor.modal.revision.title'} icon={'history'} closeButton={true} size={'xl'} additionalClasses='revision-modal'>
|
||||
<Modal.Body>
|
||||
<Row>
|
||||
<Col lg={4} className={'scroll-col'}>
|
||||
|
|
53
src/components/editor/document-bar/share/share-modal.tsx
Normal file
53
src/components/editor/document-bar/share/share-modal.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import equal from 'fast-deep-equal'
|
||||
import React from 'react'
|
||||
import { Modal } from 'react-bootstrap'
|
||||
import { useTranslation , Trans } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useFrontendBaseUrl } from '../../../../hooks/common/use-frontend-base-url'
|
||||
import { ApplicationState } from '../../../../redux'
|
||||
import { CopyableField } from '../../../common/copyable/copyable-field/copyable-field'
|
||||
import { CommonModal } from '../../../common/modals/common-modal'
|
||||
import { ShowIf } from '../../../common/show-if/show-if'
|
||||
import { EditorPathParams } from '../../editor'
|
||||
|
||||
export interface ShareModalProps {
|
||||
show: boolean,
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
export const ShareModal: React.FC<ShareModalProps> = ({ show, onHide }) => {
|
||||
useTranslation()
|
||||
const noteMetadata = useSelector((state: ApplicationState) => state.documentContent.metadata, equal)
|
||||
const editorMode = useSelector((state: ApplicationState) => state.editorConfig.editorMode)
|
||||
const baseUrl = useFrontendBaseUrl()
|
||||
const { id } = useParams<EditorPathParams>()
|
||||
|
||||
return (
|
||||
<CommonModal
|
||||
show={show}
|
||||
onHide={onHide}
|
||||
closeButton={true}
|
||||
titleI18nKey={'editor.modal.shareLink.title'}>
|
||||
<Modal.Body>
|
||||
<Trans i18nKey={'editor.modal.shareLink.editorDescription'}/>
|
||||
<CopyableField content={`${baseUrl}/n/${id}?${editorMode}`} nativeShareButton={true}
|
||||
url={`${baseUrl}/n/${id}?${editorMode}`}/>
|
||||
<ShowIf condition={noteMetadata.type === 'slide'}>
|
||||
<Trans i18nKey={'editor.modal.shareLink.slidesDescription'}/>
|
||||
<CopyableField content={`${baseUrl}/p/${id}`} nativeShareButton={true} url={`${baseUrl}/p/${id}`}/>
|
||||
</ShowIf>
|
||||
<ShowIf condition={noteMetadata.type === ''}>
|
||||
<Trans i18nKey={'editor.modal.shareLink.viewOnlyDescription'}/>
|
||||
<CopyableField content={`${baseUrl}/s/${id}`} nativeShareButton={true} url={`${baseUrl}/s/${id}`}/>
|
||||
</ShowIf>
|
||||
</Modal.Body>
|
||||
</CommonModal>
|
||||
)
|
||||
}
|
|
@ -51,7 +51,7 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
|
|||
const [tocAst, setTocAst] = useState<TocAst>()
|
||||
const internalDocumentRenderPaneRef = useRef<HTMLDivElement>()
|
||||
const { width } = useResizeObserver({ ref: internalDocumentRenderPaneRef.current })
|
||||
const realWidth = width || 0
|
||||
const realWidth = width ?? 0
|
||||
const rendererRef = useRef<HTMLDivElement | null>(null)
|
||||
const changeLineMarker = useAdaptedLineMarkerCallback(documentRenderPaneRef, rendererRef, onLineMarkerPositionChanged)
|
||||
const setContainerReference = useCallback((instance: HTMLDivElement | null) => {
|
||||
|
@ -84,22 +84,24 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
|
|||
</div>
|
||||
|
||||
<div className={'col-md pt-4'}>
|
||||
<ShowIf condition={realWidth >= 1280 && !!tocAst}>
|
||||
<TableOfContents ast={tocAst as TocAst} className={'sticky'} baseUrl={baseUrl}/>
|
||||
</ShowIf>
|
||||
<ShowIf condition={realWidth < 1280 && !!tocAst}>
|
||||
<div className={'markdown-toc-sidebar-button'}>
|
||||
<Dropdown drop={'up'}>
|
||||
<Dropdown.Toggle id="toc-overlay-button" variant={'secondary'} className={'no-arrow'}>
|
||||
<ForkAwesomeIcon icon={'bars'}/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<div className={'p-2'}>
|
||||
<TableOfContents ast={tocAst as TocAst} baseUrl={baseUrl}/>
|
||||
</div>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<ShowIf condition={!!tocAst}>
|
||||
<ShowIf condition={realWidth >= 1280}>
|
||||
<TableOfContents ast={tocAst as TocAst} className={'sticky'} baseUrl={baseUrl}/>
|
||||
</ShowIf>
|
||||
<ShowIf condition={realWidth < 1280}>
|
||||
<div className={'markdown-toc-sidebar-button'}>
|
||||
<Dropdown drop={'up'}>
|
||||
<Dropdown.Toggle id="toc-overlay-button" variant={'secondary'} className={'no-arrow'}>
|
||||
<ForkAwesomeIcon icon={'list-ol'}/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<div className={'p-2'}>
|
||||
<TableOfContents ast={tocAst as TocAst} baseUrl={baseUrl}/>
|
||||
</div>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</ShowIf>
|
||||
</ShowIf>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -203,9 +203,7 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
|
|||
return (
|
||||
<div className={'d-flex flex-column h-100 position-relative'} onMouseEnter={onMakeScrollSource}>
|
||||
<MaxLengthWarningModal show={showMaxLengthWarning} onHide={onMaxLengthHide} maxLength={maxLength}/>
|
||||
<ToolBar
|
||||
editor={editor}
|
||||
/>
|
||||
<ToolBar editor={editor}/>
|
||||
<ControlledCodeMirror
|
||||
className={`overflow-hidden w-100 flex-fill ${ligaturesEnabled ? '' : 'no-ligatures'}`}
|
||||
value={content}
|
||||
|
|
|
@ -5,16 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
*/
|
||||
|
||||
import { Editor } from 'codemirror'
|
||||
import React, { useCallback } from 'react'
|
||||
import React from 'react'
|
||||
import { Button, ButtonGroup, ButtonToolbar } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
||||
import { HiddenInputMenuEntry } from '../../../common/hidden-input-menu-entry/hidden-input-menu-entry'
|
||||
import { handleUpload } from '../upload-handler'
|
||||
import { EditorPreferences } from './editor-preferences/editor-preferences'
|
||||
import { EmojiPickerButton } from './emoji-picker/emoji-picker-button'
|
||||
import { TablePickerButton } from './table-picker/table-picker-button'
|
||||
import './tool-bar.scss'
|
||||
import { UploadImageButton } from './upload-image-button'
|
||||
import {
|
||||
addCodeFences,
|
||||
addCollapsableBlock,
|
||||
|
@ -34,22 +33,14 @@ import {
|
|||
superscriptSelection,
|
||||
underlineSelection
|
||||
} from './utils/toolbarButtonUtils'
|
||||
import { supportedMimeTypesJoined } from './utils/upload-image-mimetypes'
|
||||
|
||||
export interface ToolBarProps {
|
||||
editor: Editor | undefined
|
||||
editor?: Editor
|
||||
}
|
||||
|
||||
export const ToolBar: React.FC<ToolBarProps> = ({ editor }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onUploadImage = useCallback((file: File) => {
|
||||
if (editor) {
|
||||
handleUpload(file, editor)
|
||||
}
|
||||
return Promise.resolve()
|
||||
}, [editor])
|
||||
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
|
@ -103,13 +94,7 @@ export const ToolBar: React.FC<ToolBarProps> = ({ editor }) => {
|
|||
<Button data-cy={'format-image'} variant='light' onClick={() => addImage(editor)} title={t('editor.editorToolbar.image')}>
|
||||
<ForkAwesomeIcon icon="picture-o"/>
|
||||
</Button>
|
||||
<HiddenInputMenuEntry
|
||||
type={'button'}
|
||||
acceptedFiles={supportedMimeTypesJoined}
|
||||
i18nKey={'editor.editorToolbar.uploadImage'}
|
||||
icon={'upload'}
|
||||
onLoad={onUploadImage}
|
||||
/>
|
||||
<UploadImageButton editor={editor}/>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup className={'mx-1 flex-wrap'}>
|
||||
<TablePickerButton editor={editor}/>
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Editor } from 'codemirror'
|
||||
import React, { Fragment, useCallback, useRef } from 'react'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
||||
import { UploadInput } from '../../sidebar/upload-input'
|
||||
import { handleUpload } from '../upload-handler'
|
||||
import { supportedMimeTypesJoined } from './utils/upload-image-mimetypes'
|
||||
|
||||
export interface UploadImageButtonProps {
|
||||
editor?: Editor
|
||||
}
|
||||
|
||||
export const UploadImageButton: React.FC<UploadImageButtonProps> = ({ editor }) => {
|
||||
const { t } = useTranslation()
|
||||
const clickRef = useRef<(() => void)>()
|
||||
const buttonClick = useCallback(() => {
|
||||
clickRef.current?.()
|
||||
}, [])
|
||||
|
||||
const onUploadImage = useCallback((file: File) => {
|
||||
if (editor) {
|
||||
handleUpload(file, editor)
|
||||
}
|
||||
return Promise.resolve()
|
||||
}, [editor])
|
||||
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Button variant='light' onClick={buttonClick} title={t('editor.editorToolbar.uploadImage')}>
|
||||
<ForkAwesomeIcon icon={'upload'}/>
|
||||
</Button>
|
||||
<UploadInput onLoad={onUploadImage} acceptedFiles={supportedMimeTypesJoined} onClickRef={clickRef}/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
@ -18,12 +18,12 @@ import { extractNoteTitle } from '../common/document-title/note-title-extractor'
|
|||
import { MotdBanner } from '../common/motd-banner/motd-banner'
|
||||
import { AppBar, AppBarMode } from './app-bar/app-bar'
|
||||
import { EditorMode } from './app-bar/editor-view-mode'
|
||||
import { DocumentBar } from './document-bar/document-bar'
|
||||
import { DocumentIframe } from './document-renderer-pane/document-iframe'
|
||||
import { EditorPane } from './editor-pane/editor-pane'
|
||||
import { editorTestContent } from './editorTestContent'
|
||||
import { useViewModeShortcuts } from './hooks/useViewModeShortcuts'
|
||||
import { DualScrollState, ScrollState } from './scroll/scroll-props'
|
||||
import { Sidebar } from './sidebar/sidebar'
|
||||
import { Splitter } from './splitter/splitter'
|
||||
import { YAMLMetaData } from './yaml-metadata/yaml-metadata'
|
||||
|
||||
|
@ -125,36 +125,41 @@ export const Editor: React.FC = () => {
|
|||
scrollSource.current = ScrollSource.RENDERER
|
||||
}, [])
|
||||
|
||||
const setEditorToScrollSource = useCallback(() => {
|
||||
scrollSource.current = ScrollSource.EDITOR
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<MotdBanner/>
|
||||
<div className={'d-flex flex-column vh-100'}>
|
||||
<AppBar mode={AppBarMode.EDITOR}/>
|
||||
<DocumentBar title={documentTitle}/>
|
||||
<Splitter
|
||||
showLeft={editorMode === EditorMode.EDITOR || editorMode === EditorMode.BOTH}
|
||||
left={
|
||||
<EditorPane
|
||||
onContentChange={setDocumentContent}
|
||||
content={markdownContent}
|
||||
scrollState={scrollState.editorScrollState}
|
||||
onScroll={onEditorScroll}
|
||||
onMakeScrollSource={() => (scrollSource.current = ScrollSource.EDITOR)}
|
||||
/>
|
||||
}
|
||||
showRight={editorMode === EditorMode.PREVIEW || (editorMode === EditorMode.BOTH)}
|
||||
right={
|
||||
<DocumentIframe markdownContent={markdownContent}
|
||||
onMakeScrollSource={setRendererToScrollSource}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onTaskCheckedChange={onTaskCheckedChange}
|
||||
onMetadataChange={onMetadataChange}
|
||||
onScroll={onMarkdownRendererScroll}
|
||||
wide={editorMode === EditorMode.PREVIEW}
|
||||
scrollState={scrollState.rendererScrollState}
|
||||
/>
|
||||
}
|
||||
containerClassName={'overflow-hidden'}/>
|
||||
<div className={"flex-fill d-flex h-100 w-100 overflow-hidden flex-row"}>
|
||||
<Splitter
|
||||
showLeft={editorMode === EditorMode.EDITOR || editorMode === EditorMode.BOTH}
|
||||
left={
|
||||
<EditorPane
|
||||
onContentChange={setDocumentContent}
|
||||
content={markdownContent}
|
||||
scrollState={scrollState.editorScrollState}
|
||||
onScroll={onEditorScroll}
|
||||
onMakeScrollSource={setEditorToScrollSource}/>
|
||||
}
|
||||
showRight={editorMode === EditorMode.PREVIEW || editorMode === EditorMode.BOTH}
|
||||
right={
|
||||
<DocumentIframe
|
||||
markdownContent={markdownContent}
|
||||
onMakeScrollSource={setRendererToScrollSource}
|
||||
onFirstHeadingChange={onFirstHeadingChange}
|
||||
onTaskCheckedChange={onTaskCheckedChange}
|
||||
onMetadataChange={onMetadataChange}
|
||||
onScroll={onMarkdownRendererScroll}
|
||||
wide={editorMode === EditorMode.PREVIEW}
|
||||
scrollState={scrollState.rendererScrollState}/>
|
||||
}
|
||||
containerClassName={'overflow-hidden'}/>
|
||||
<Sidebar/>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
|
|
38
src/components/editor/sidebar/delete-note-sidebar-entry.tsx
Normal file
38
src/components/editor/sidebar/delete-note-sidebar-entry.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { DeletionModal } from '../../common/modals/deletion-modal'
|
||||
import { SidebarButton } from './sidebar-button'
|
||||
import { SpecificSidebarEntryProps } from './types'
|
||||
|
||||
export const DeleteNoteSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ hide, className }) => {
|
||||
useTranslation()
|
||||
const [showDialog, setShowDialog] = useState(false)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SidebarButton icon={"trash"} className={className} hide={hide} onClick={() => setShowDialog(true)}>
|
||||
<Trans i18nKey={'landing.history.menu.deleteNote'}/>
|
||||
</SidebarButton>
|
||||
<DeletionModal
|
||||
onConfirm={() => setShowDialog(false)}
|
||||
deletionButtonI18nKey={'editor.modal.deleteNote.button'}
|
||||
show={showDialog}
|
||||
onHide={() => setShowDialog(false)}
|
||||
titleI18nKey={'editor.modal.deleteNote.title'}>
|
||||
<h5><Trans i18nKey={'editor.modal.deleteNote.question'}/></h5>
|
||||
<ul>
|
||||
<li> noteTitle</li>
|
||||
</ul>
|
||||
<h6>
|
||||
<Trans i18nKey={'editor.modal.deleteNote.warning'}/>
|
||||
</h6>
|
||||
</DeletionModal>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { DocumentInfoModal } from '../document-bar/document-info/document-info-modal'
|
||||
import { SidebarButton } from './sidebar-button'
|
||||
import { SpecificSidebarEntryProps } from './types'
|
||||
|
||||
export const DocumentInfoSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({className, hide}) => {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
useTranslation()
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SidebarButton hide={hide} className={className} icon={"line-chart"} onClick={() => setShowModal(true)}>
|
||||
<Trans i18nKey={'editor.modal.documentInfo.title'} />
|
||||
</SidebarButton>
|
||||
<DocumentInfoModal show={showModal} onHide={() => setShowModal(false)}/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
import { useSelector } from 'react-redux'
|
||||
import { ApplicationState } from '../../../redux'
|
||||
import { download } from '../../common/download/download'
|
||||
import { SidebarButton } from './sidebar-button'
|
||||
|
||||
export const ExportMarkdownSidebarEntry: React.FC = () => {
|
||||
useTranslation()
|
||||
|
||||
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
|
||||
const onClick = useCallback(() => {
|
||||
download(markdownContent, `title.md`, 'text/markdown') //todo: replace hard coded title with redux
|
||||
}, [markdownContent])
|
||||
|
||||
return (
|
||||
<SidebarButton data-cy={"menu-export-markdown"} onClick={onClick} icon={'file-text'}>
|
||||
<Trans i18nKey={'editor.export.markdown-file'}/>
|
||||
</SidebarButton>
|
||||
)
|
||||
}
|
64
src/components/editor/sidebar/export-menu-sidebar-menu.tsx
Normal file
64
src/components/editor/sidebar/export-menu-sidebar-menu.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useCallback } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import links from '../../../links.json'
|
||||
import { ExportMarkdownSidebarEntry } from './export-markdown-sidebar-entry'
|
||||
import { SidebarButton } from './sidebar-button'
|
||||
import { SidebarMenu } from './sidebar-menu'
|
||||
import { DocumentSidebarMenuSelection, SpecificSidebarMenuProps } from './types'
|
||||
|
||||
export const ExportMenuSidebarMenu: React.FC<SpecificSidebarMenuProps> = (
|
||||
{
|
||||
className,
|
||||
menuId,
|
||||
onClick,
|
||||
selectedMenuId
|
||||
}) => {
|
||||
useTranslation()
|
||||
|
||||
const hide = selectedMenuId !== DocumentSidebarMenuSelection.NONE && selectedMenuId !== menuId
|
||||
const expand = selectedMenuId === menuId
|
||||
const onClickHandler = useCallback(() => {
|
||||
onClick(menuId)
|
||||
}, [menuId, onClick])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SidebarButton data-cy={"menu-export"} hide={hide} icon={expand ? "arrow-left" : "cloud-download"}
|
||||
className={className} onClick={onClickHandler}>
|
||||
<Trans i18nKey={'editor.documentBar.export'}/>
|
||||
</SidebarButton>
|
||||
<SidebarMenu expand={expand}>
|
||||
<SidebarButton icon={"github"}>
|
||||
Gist
|
||||
</SidebarButton>
|
||||
<SidebarButton icon={"gitlab"}>
|
||||
Gitlab Snippet
|
||||
</SidebarButton>
|
||||
|
||||
<ExportMarkdownSidebarEntry/>
|
||||
|
||||
<SidebarButton icon={"file-code-o"}>
|
||||
HTML
|
||||
</SidebarButton>
|
||||
<SidebarButton icon={"file-code-o"}>
|
||||
<Trans i18nKey='editor.export.rawHtml'/>
|
||||
</SidebarButton>
|
||||
<SidebarButton icon={"file-pdf-o"}>
|
||||
<a className='small text-muted' dir={'auto'} href={links.faq} target={'_blank'} rel='noopener noreferrer'>
|
||||
<Trans i18nKey={'editor.export.pdf'}/>
|
||||
|
||||
<span className={'text-primary'}>
|
||||
<Trans i18nKey={'common.why'}/>
|
||||
</span>
|
||||
</a>
|
||||
</SidebarButton>
|
||||
</SidebarMenu>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useCallback, useRef } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { ApplicationState } from '../../../redux'
|
||||
import { setDocumentContent } from '../../../redux/document-content/methods'
|
||||
import { SidebarButton } from './sidebar-button'
|
||||
import { UploadInput } from './upload-input'
|
||||
|
||||
export const ImportMarkdownSidebarEntry: React.FC = () => {
|
||||
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
|
||||
useTranslation()
|
||||
|
||||
const onImportMarkdown = useCallback((file: File) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const fileReader = new FileReader()
|
||||
fileReader.addEventListener('load', () => {
|
||||
const newContent = fileReader.result as string
|
||||
if (markdownContent.length === 0) {
|
||||
setDocumentContent(newContent)
|
||||
} else {
|
||||
setDocumentContent(markdownContent + '\n' + newContent)
|
||||
}
|
||||
})
|
||||
fileReader.addEventListener('loadend', () => {
|
||||
resolve()
|
||||
})
|
||||
fileReader.addEventListener('error', (error) => {
|
||||
reject(error)
|
||||
})
|
||||
fileReader.readAsText(file)
|
||||
})
|
||||
}, [markdownContent])
|
||||
|
||||
const clickRef = useRef<(() => void)>()
|
||||
const buttonClick = useCallback(() => {
|
||||
clickRef.current?.();
|
||||
},[]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SidebarButton data-cy={"menu-import-markdown"} icon={"file-text-o"} onClick={buttonClick}>
|
||||
<Trans i18nKey={'editor.import.file'}/>
|
||||
</SidebarButton>
|
||||
<UploadInput onLoad={onImportMarkdown} data-cy={"menu-import-markdown-input"} acceptedFiles={'.md, text/markdown, text/plain'} onClickRef={clickRef}/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
50
src/components/editor/sidebar/import-menu-sidebar-menu.tsx
Normal file
50
src/components/editor/sidebar/import-menu-sidebar-menu.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useCallback } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { ImportMarkdownSidebarEntry } from './import-markdown-sidebar-entry'
|
||||
import { SidebarButton } from './sidebar-button'
|
||||
import { SidebarMenu } from './sidebar-menu'
|
||||
import { DocumentSidebarMenuSelection, SpecificSidebarMenuProps } from './types'
|
||||
|
||||
export const ImportMenuSidebarMenu: React.FC<SpecificSidebarMenuProps> = (
|
||||
{
|
||||
className,
|
||||
menuId,
|
||||
onClick,
|
||||
selectedMenuId
|
||||
}) => {
|
||||
|
||||
useTranslation()
|
||||
|
||||
const hide = selectedMenuId !== DocumentSidebarMenuSelection.NONE && selectedMenuId !== menuId
|
||||
const expand = selectedMenuId === menuId
|
||||
const onClickHandler = useCallback(() => {
|
||||
onClick(menuId)
|
||||
}, [menuId, onClick])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SidebarButton data-cy={"menu-import"} hide={hide} icon={expand ? "arrow-left" : "cloud-upload"}
|
||||
className={className} onClick={onClickHandler}>
|
||||
<Trans i18nKey={'editor.documentBar.import'}/>
|
||||
</SidebarButton>
|
||||
<SidebarMenu expand={expand}>
|
||||
<SidebarButton icon={"github"}>
|
||||
Gist
|
||||
</SidebarButton>
|
||||
<SidebarButton icon={"gitlab"}>
|
||||
Gitlab Snippet
|
||||
</SidebarButton>
|
||||
<SidebarButton icon={"clipboard"}>
|
||||
<Trans i18nKey={'editor.import.clipboard'}/>
|
||||
</SidebarButton>
|
||||
<ImportMarkdownSidebarEntry/>
|
||||
</SidebarMenu>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
25
src/components/editor/sidebar/permissions-sidebar-entry.tsx
Normal file
25
src/components/editor/sidebar/permissions-sidebar-entry.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { PermissionModal } from '../document-bar/permissions/permission-modal'
|
||||
import { SidebarButton } from './sidebar-button'
|
||||
import { SpecificSidebarEntryProps } from './types'
|
||||
|
||||
export const PermissionsSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({className, hide}) => {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
useTranslation()
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SidebarButton hide={hide} className={className} icon={"lock"} onClick={() => setShowModal(true)}>
|
||||
<Trans i18nKey={'editor.modal.permissions.title'}/>
|
||||
</SidebarButton>
|
||||
<PermissionModal show={showModal} onHide={() => setShowModal(false)}/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
23
src/components/editor/sidebar/pin-note-sidebar-entry.tsx
Normal file
23
src/components/editor/sidebar/pin-note-sidebar-entry.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { SidebarButton } from './sidebar-button'
|
||||
import { SpecificSidebarEntryProps } from './types'
|
||||
|
||||
export const PinNoteSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({ className, hide }) => {
|
||||
useTranslation()
|
||||
|
||||
const isPinned = true
|
||||
const i18nKey = isPinned ? 'editor.documentBar.pinNoteToHistory' : 'editor.documentBar.pinnedToHistory'
|
||||
|
||||
return (
|
||||
<SidebarButton icon={'thumb-tack'} className={className} hide={hide}>
|
||||
<Trans i18nKey={i18nKey}/>
|
||||
</SidebarButton>
|
||||
)
|
||||
}
|
24
src/components/editor/sidebar/revision-sidebar-entry.tsx
Normal file
24
src/components/editor/sidebar/revision-sidebar-entry.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useState } from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { RevisionModal } from '../document-bar/revisions/revision-modal'
|
||||
import { SidebarButton } from './sidebar-button'
|
||||
import { SpecificSidebarEntryProps } from './types'
|
||||
|
||||
export const RevisionSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({className, hide}) => {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SidebarButton hide={hide} className={className} icon={"history"} onClick={() => setShowModal(true)}>
|
||||
<Trans i18nKey={'editor.modal.revision.title'}/>
|
||||
</SidebarButton>
|
||||
<RevisionModal show={showModal} onHide={() => setShowModal(false)}/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
25
src/components/editor/sidebar/share-sidebar-entry.tsx
Normal file
25
src/components/editor/sidebar/share-sidebar-entry.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { ShareModal } from '../document-bar/share/share-modal'
|
||||
import { SidebarButton } from './sidebar-button'
|
||||
import { SpecificSidebarEntryProps } from './types'
|
||||
|
||||
export const ShareSidebarEntry: React.FC<SpecificSidebarEntryProps> = ({className, hide}) => {
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
useTranslation()
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SidebarButton hide={hide} className={className} icon={"share"} onClick={() => setShowModal(true)}>
|
||||
<Trans i18nKey={'editor.modal.shareLink.title'}/>
|
||||
</SidebarButton>
|
||||
<ShareModal show={showModal} onHide={() => setShowModal(false)}/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
29
src/components/editor/sidebar/sidebar-button.tsx
Normal file
29
src/components/editor/sidebar/sidebar-button.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
|
||||
import { IconName } from '../../common/fork-awesome/types'
|
||||
import { ShowIf } from '../../common/show-if/show-if'
|
||||
import { SidebarEntryProps } from './types'
|
||||
|
||||
export type SidebarEntryVariant = "primary"
|
||||
|
||||
export const SidebarButton: React.FC<SidebarEntryProps> = ({children, icon, className, variant,buttonRef, hide, ...props }) => {
|
||||
return (
|
||||
<button ref={buttonRef}
|
||||
className={`sidebar-entry ${hide ? 'hide' : ''} ${variant ? `sidebar-entry-${variant}` : ''} ${className ?? ''}`} {...props} >
|
||||
<ShowIf condition={!!icon}>
|
||||
<span className={'sidebar-icon'}>
|
||||
<ForkAwesomeIcon icon={icon as IconName}/>
|
||||
</span>
|
||||
</ShowIf>
|
||||
<span className={'sidebar-text'}>
|
||||
{children}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
18
src/components/editor/sidebar/sidebar-menu.tsx
Normal file
18
src/components/editor/sidebar/sidebar-menu.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { SidebarMenuProps } from './types'
|
||||
|
||||
export const SidebarMenu: React.FC<SidebarMenuProps> = ({children, expand}) => {
|
||||
return (
|
||||
<div className={`sidebar-menu ${expand ? 'show' : ''}`}>
|
||||
<div className={`d-flex flex-column`}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
55
src/components/editor/sidebar/sidebar.tsx
Normal file
55
src/components/editor/sidebar/sidebar.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
import { useClickAway } from 'react-use'
|
||||
import { DeleteNoteSidebarEntry } from './delete-note-sidebar-entry'
|
||||
import { DocumentInfoSidebarEntry } from './document-info-sidebar-entry'
|
||||
import { ExportMenuSidebarMenu } from './export-menu-sidebar-menu'
|
||||
import { ImportMenuSidebarMenu } from './import-menu-sidebar-menu'
|
||||
import { PermissionsSidebarEntry } from './permissions-sidebar-entry'
|
||||
import { PinNoteSidebarEntry } from './pin-note-sidebar-entry'
|
||||
import { RevisionSidebarEntry } from './revision-sidebar-entry'
|
||||
import { ShareSidebarEntry } from './share-sidebar-entry'
|
||||
import "./style/theme.scss"
|
||||
import { DocumentSidebarMenuSelection } from './types'
|
||||
import { UsersOnlineSidebarMenu } from './users-online-sidebar-menu/users-online-sidebar-menu'
|
||||
|
||||
export const Sidebar: React.FC = () => {
|
||||
|
||||
const sideBarRef = useRef<HTMLDivElement>(null)
|
||||
const [selectedMenu, setSelectedMenu] = useState<DocumentSidebarMenuSelection>(DocumentSidebarMenuSelection.NONE)
|
||||
|
||||
useClickAway(sideBarRef, () => {
|
||||
setSelectedMenu(DocumentSidebarMenuSelection.NONE)
|
||||
})
|
||||
|
||||
const toggleValue = useCallback((toggleValue: DocumentSidebarMenuSelection): void => {
|
||||
const newValue = selectedMenu === toggleValue ? DocumentSidebarMenuSelection.NONE : toggleValue
|
||||
setSelectedMenu(newValue)
|
||||
}, [selectedMenu])
|
||||
|
||||
const selectionIsNotNone = selectedMenu !== DocumentSidebarMenuSelection.NONE
|
||||
|
||||
return (
|
||||
<div className="slide-sidebar">
|
||||
<div ref={sideBarRef} className={`sidebar-inner ${selectionIsNotNone ? 'show' : ''}`}>
|
||||
<UsersOnlineSidebarMenu menuId={DocumentSidebarMenuSelection.USERS_ONLINE}
|
||||
selectedMenuId={selectedMenu} onClick={toggleValue}/>
|
||||
<DocumentInfoSidebarEntry hide={selectionIsNotNone}/>
|
||||
<RevisionSidebarEntry hide={selectionIsNotNone}/>
|
||||
<PermissionsSidebarEntry hide={selectionIsNotNone}/>
|
||||
<ImportMenuSidebarMenu menuId={DocumentSidebarMenuSelection.IMPORT}
|
||||
selectedMenuId={selectedMenu} onClick={toggleValue}/>
|
||||
<ExportMenuSidebarMenu menuId={DocumentSidebarMenuSelection.EXPORT}
|
||||
selectedMenuId={selectedMenu} onClick={toggleValue}/>
|
||||
<ShareSidebarEntry hide={selectionIsNotNone}/>
|
||||
<DeleteNoteSidebarEntry hide={selectionIsNotNone}/>
|
||||
<PinNoteSidebarEntry hide={selectionIsNotNone}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
49
src/components/editor/sidebar/style/colors.scss
Normal file
49
src/components/editor/sidebar/style/colors.scss
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
.slide-sidebar {
|
||||
background: $body-bg;
|
||||
|
||||
.sidebar-menu>div {
|
||||
background: $sidebar-menu-bg;
|
||||
box-shadow: inset 0 7px 7px -6px $sidebar-menu-shadow;
|
||||
}
|
||||
|
||||
.sidebar-inner {
|
||||
background: $body-bg;
|
||||
}
|
||||
|
||||
.sidebar-entry {
|
||||
color: $dark;
|
||||
|
||||
&:hover {
|
||||
background: $entry-hover-bg;
|
||||
color: $dark;
|
||||
}
|
||||
|
||||
&.sidebar-entry-primary {
|
||||
background: $primary;
|
||||
color: $white;
|
||||
|
||||
&:hover {
|
||||
color: $primary;
|
||||
background: $entry-hover-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin text-stroke($shadow-color) {
|
||||
text-shadow: 0px 0px 1px $shadow-color,
|
||||
1px 0px 1px $shadow-color,
|
||||
0px 1px 1px $shadow-color,
|
||||
-1px 0px 1px $shadow-color,
|
||||
0px -1px 1px $shadow-color,
|
||||
1px 1px 1px $shadow-color,
|
||||
-1px -1px 1px $shadow-color,
|
||||
-1px 1px 1px $shadow-color,
|
||||
1px -1px 1px $shadow-color;
|
||||
}
|
100
src/components/editor/sidebar/style/layout.scss
Normal file
100
src/components/editor/sidebar/style/layout.scss
Normal file
|
@ -0,0 +1,100 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
|
||||
$height: 40px;
|
||||
$menu-width: 175px;
|
||||
|
||||
.slide-sidebar {
|
||||
flex: 0 0 $height;
|
||||
position: relative;
|
||||
|
||||
.sidebar-inner {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow-y: auto;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
width: $menu-width;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transition: left 0.3s;
|
||||
box-shadow: 0 0 0px rgba(0, 0, 0, 0.15);
|
||||
|
||||
&:hover, &.show {
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
|
||||
left: (-$menu-width + $height);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
transition: height 0.2s, flex-basis 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
flex: 0 1 0;
|
||||
height: 0;
|
||||
|
||||
&.show {
|
||||
height: 100%;
|
||||
flex-basis: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-entry {
|
||||
height: $height;
|
||||
flex: 0 0 $height;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: solid 1px rgba(0, 0, 0, 0.15);
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
transition: height 0.2s, flex-basis 0.2s;
|
||||
overflow: hidden;
|
||||
|
||||
&.hide {
|
||||
flex-basis: 0;
|
||||
height: 0px;
|
||||
border-width: 0px;
|
||||
|
||||
.sidebar-icon {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sidebar-text {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-icon {
|
||||
transition: opacity 0.2s;
|
||||
opacity: 1;
|
||||
height: $height;
|
||||
width: $height;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
flex: 0 0 40px;
|
||||
}
|
||||
|
||||
.sidebar-text {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s;
|
||||
text-align: left;
|
||||
flex: 1 1 0;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
}
|
21
src/components/editor/sidebar/style/theme.scss
Normal file
21
src/components/editor/sidebar/style/theme.scss
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
body.dark {
|
||||
@import "../../../../style/variables.dark";
|
||||
$entry-hover-bg: lighten($body-bg, 10%);
|
||||
$sidebar-menu-bg: $body-bg;
|
||||
$sidebar-menu-shadow: #1d1d1d;
|
||||
@import "colors";
|
||||
}
|
||||
|
||||
@import "../../../../style/variables.light";
|
||||
$entry-hover-bg: darken($body-bg, 10%);
|
||||
$sidebar-menu-bg: $body-bg;
|
||||
$sidebar-menu-shadow: #bbbbbb;
|
||||
@import "colors";
|
||||
|
||||
@import "layout";
|
43
src/components/editor/sidebar/types.ts
Normal file
43
src/components/editor/sidebar/types.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { RefObject } from 'react'
|
||||
import { IconName } from '../../common/fork-awesome/types'
|
||||
import { SidebarEntryVariant } from './sidebar-button'
|
||||
|
||||
export interface SpecificSidebarEntryProps {
|
||||
className?: string
|
||||
hide?: boolean
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
export interface SidebarEntryProps {
|
||||
icon?: IconName
|
||||
variant?: SidebarEntryVariant
|
||||
buttonRef?: RefObject<HTMLButtonElement>
|
||||
hide?: boolean
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
"data-cy"?: string
|
||||
}
|
||||
|
||||
export interface SidebarMenuProps {
|
||||
expand?: boolean
|
||||
}
|
||||
|
||||
export enum DocumentSidebarMenuSelection {
|
||||
NONE,
|
||||
USERS_ONLINE,
|
||||
IMPORT,
|
||||
EXPORT
|
||||
}
|
||||
|
||||
export interface SpecificSidebarMenuProps {
|
||||
className?: string
|
||||
onClick: (menuId: DocumentSidebarMenuSelection) => void
|
||||
selectedMenuId: DocumentSidebarMenuSelection
|
||||
menuId: DocumentSidebarMenuSelection
|
||||
}
|
44
src/components/editor/sidebar/upload-input.tsx
Normal file
44
src/components/editor/sidebar/upload-input.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { MutableRefObject, useCallback, useEffect, useRef } from 'react'
|
||||
|
||||
export interface UploadInputProps {
|
||||
onLoad: (file: File) => Promise<void>
|
||||
acceptedFiles: string
|
||||
onClickRef: MutableRefObject<(() => void) | undefined>
|
||||
"data-cy"?: string
|
||||
}
|
||||
|
||||
export const UploadInput: React.FC<UploadInputProps> = ({ onLoad, acceptedFiles, onClickRef, ...props }) => {
|
||||
const fileInputReference = useRef<HTMLInputElement>(null)
|
||||
const onClick = useCallback(() => {
|
||||
const fileInput = fileInputReference.current
|
||||
if (!fileInput) {
|
||||
return
|
||||
}
|
||||
fileInput.addEventListener('change', () => {
|
||||
if (!fileInput.files || fileInput.files.length < 1) {
|
||||
return
|
||||
}
|
||||
const file = fileInput.files[0]
|
||||
onLoad(file).then(() => {
|
||||
fileInput.value = ''
|
||||
}).catch((error) => {
|
||||
console.error(error)
|
||||
})
|
||||
})
|
||||
fileInput.click()
|
||||
}, [onLoad])
|
||||
|
||||
useEffect(() => {
|
||||
onClickRef.current = onClick
|
||||
})
|
||||
|
||||
return (
|
||||
<input data-cy={props["data-cy"]} type='file' ref={fileInputReference} className='d-none' accept={acceptedFiles}/>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*!
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
body.dark {
|
||||
@import "../../../../style/variables.dark";
|
||||
$entry-hover-bg: lighten($body-bg, 10%);
|
||||
$sidebar-menu-bg: $body-bg;
|
||||
$sidebar-menu-shadow: #1d1d1d;
|
||||
@import "../style/colors";
|
||||
}
|
||||
|
||||
@import "../../../../style/variables.light";
|
||||
$entry-hover-bg: darken($body-bg, 10%);
|
||||
$sidebar-menu-bg: $body-bg;
|
||||
$sidebar-menu-shadow: #bbbbbb;
|
||||
@import "../style/colors";
|
||||
|
||||
.slide-sidebar .sidebar-entry.online-entry {
|
||||
&:hover {
|
||||
.sidebar-icon:after {
|
||||
color: $primary;
|
||||
$shadow-color: #ffffff;
|
||||
@include text-stroke(#ffffff);
|
||||
}
|
||||
}
|
||||
|
||||
--users-online: '0';
|
||||
|
||||
.sidebar-icon:after {
|
||||
@include text-stroke($primary);
|
||||
content: var(--users-online);
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
bottom: 3px;
|
||||
font-size: 0.9rem;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
.user-line-color-indicator {
|
||||
border-left: 3px solid;
|
||||
min-height: 30px;
|
||||
height: 100%;
|
||||
flex: 0 0 3px;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
flex: 0 0 20px;
|
||||
}
|
||||
|
||||
.user-line-name {
|
||||
text-overflow: ellipsis;
|
||||
flex: 1 1 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.active-indicator-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex: 0 0 20px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react'
|
||||
import React from 'react'
|
||||
import { UserAvatar } from '../../../common/user-avatar/user-avatar'
|
||||
import { ActiveIndicator, ActiveIndicatorStatus } from './active-indicator'
|
||||
import './user-line.scss'
|
||||
|
@ -18,10 +18,12 @@ export interface UserLineProps {
|
|||
|
||||
export const UserLine: React.FC<UserLineProps> = ({ name, photo, color, status }) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={'d-flex align-items-center h-100 w-100'}>
|
||||
<div className='d-inline-flex align-items-bottom user-line-color-indicator' style={{ borderLeftColor: color }}/>
|
||||
<UserAvatar photo={photo} name={name} additionalClasses={'mx-2'}/>
|
||||
<ActiveIndicator status={status} />
|
||||
</Fragment>
|
||||
<UserAvatar photo={photo} name={name} additionalClasses={'flex-fill overflow-hidden px-2 text-nowrap w-100'}/>
|
||||
<div className={"active-indicator-container"}>
|
||||
<ActiveIndicator status={status} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { SidebarButton } from '../sidebar-button'
|
||||
import { SidebarMenu } from '../sidebar-menu'
|
||||
import { DocumentSidebarMenuSelection, SpecificSidebarMenuProps } from '../types'
|
||||
import { ActiveIndicatorStatus } from './active-indicator'
|
||||
import './online-counter.scss'
|
||||
import { UserLine } from './user-line'
|
||||
|
||||
export const UsersOnlineSidebarMenu: React.FC<SpecificSidebarMenuProps> = ({
|
||||
className,
|
||||
menuId,
|
||||
onClick,
|
||||
selectedMenuId
|
||||
}) => {
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const [counter] = useState(2)
|
||||
useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
const value = `${counter}`
|
||||
buttonRef.current?.style.setProperty('--users-online', `"${value}"`)
|
||||
}, [counter])
|
||||
|
||||
const hide = selectedMenuId !== DocumentSidebarMenuSelection.NONE && selectedMenuId !== menuId
|
||||
const expand = selectedMenuId === menuId
|
||||
const onClickHandler = useCallback(() => {
|
||||
onClick(menuId)
|
||||
}, [menuId, onClick])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<SidebarButton hide={hide} buttonRef={buttonRef} onClick={onClickHandler} icon={expand ? "arrow-left" : "users"}
|
||||
variant={'primary'} className={`online-entry ${className ?? ''}`}>
|
||||
<Trans i18nKey={'editor.onlineStatus.online'}/>
|
||||
</SidebarButton>
|
||||
<SidebarMenu expand={expand}>
|
||||
<SidebarButton>
|
||||
<UserLine name="Philip Molares" photo="/img/avatar.png" color="red" status={ActiveIndicatorStatus.INACTIVE}/>
|
||||
</SidebarButton>
|
||||
<SidebarButton>
|
||||
<UserLine name="Tilman Vatteroth" photo="/img/avatar.png" color="blue" status={ActiveIndicatorStatus.ACTIVE}/>
|
||||
</SidebarButton>
|
||||
</SidebarMenu>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -16,19 +16,21 @@
|
|||
top: 0;
|
||||
}
|
||||
|
||||
padding: 2px;
|
||||
|
||||
> ul > li {
|
||||
> a {
|
||||
padding: 4px 20px;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
> ul > li {
|
||||
> a {
|
||||
padding: 1px 0 1px 30px;
|
||||
padding: 1px 0 1px 20px;
|
||||
}
|
||||
|
||||
> ul > li {
|
||||
> a {
|
||||
padding: 1px 0 1px 38px;
|
||||
padding: 1px 0 1px 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,13 +64,13 @@
|
|||
}
|
||||
|
||||
.markdown-toc-sidebar-button {
|
||||
position: fixed;
|
||||
right: 40px;
|
||||
bottom: 30px;
|
||||
position: fixed;
|
||||
right: 70px;
|
||||
bottom: 30px;
|
||||
|
||||
& > .dropup {
|
||||
position: sticky;
|
||||
bottom: 20px;
|
||||
right: 0;
|
||||
}
|
||||
& > .dropup {
|
||||
position: sticky;
|
||||
bottom: 20px;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
*/
|
||||
|
||||
import equal from 'fast-deep-equal'
|
||||
import { RefObject, useEffect, useRef } from 'react'
|
||||
import { TocAst } from 'markdown-it-toc-done-right'
|
||||
import { RefObject, useEffect, useRef } from 'react'
|
||||
|
||||
export const usePostTocAstOnChange = (tocAst: RefObject<TocAst|undefined>, onTocChange?: (ast: TocAst) => void): void => {
|
||||
const lastTocAst = useRef<TocAst>()
|
||||
|
@ -15,5 +15,5 @@ export const usePostTocAstOnChange = (tocAst: RefObject<TocAst|undefined>, onToc
|
|||
lastTocAst.current = tocAst.current
|
||||
onTocChange(tocAst.current)
|
||||
}
|
||||
}, [onTocChange, tocAst])
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue