Replace document bar with sidebar (#937)

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
Tilman Vatteroth 2021-01-24 21:39:47 +01:00 committed by GitHub
parent 586969f368
commit 0627e0f551
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1067 additions and 604 deletions

View file

@ -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

View file

@ -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) => (

View file

@ -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')

View file

@ -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}`)

View file

@ -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",

View file

@ -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>
)
}

View file

@ -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>
)

View file

@ -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'} />

View file

@ -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}/>
}

View file

@ -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>
)
}

View file

@ -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 }

View file

@ -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%;
}

View file

@ -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>
)
}

View file

@ -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>
)
);
}

View file

@ -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>
)
}

View file

@ -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'}/>
&nbsp;
<span className={'text-primary'}>
<Trans i18nKey={'common.why'}/>
</span>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
)
}

View file

@ -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>
)
}

View file

@ -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>
)
}

View file

@ -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>
)
}

View file

@ -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>

View file

@ -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>
)
}

View file

@ -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'}>

View 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>
)
}

View file

@ -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>

View file

@ -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}

View file

@ -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}/>

View file

@ -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>
)
}

View file

@ -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>
)

View 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>
)
}

View 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 { 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>
)
}

View file

@ -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>
)
}

View 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'}/>
&nbsp;
<span className={'text-primary'}>
<Trans i18nKey={'common.why'}/>
</span>
</a>
</SidebarButton>
</SidebarMenu>
</Fragment>
)
}

View file

@ -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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
);
}

View 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>
)
}

View 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;
}

View 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;
}
}
}

View 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";

View 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
}

View 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}/>
)
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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>
)
}

View file

@ -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>
)
}

View file

@ -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;
}
}

View file

@ -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])
})
}