mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-21 17:26:29 -05:00
feature: add button to open cheatsheet in a new tab
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
438a5466e0
commit
9b9eafc948
7 changed files with 108 additions and 39 deletions
|
@ -645,6 +645,7 @@
|
||||||
"cheatsheet": {
|
"cheatsheet": {
|
||||||
"button": "Open Cheatsheet",
|
"button": "Open Cheatsheet",
|
||||||
"modal":{
|
"modal":{
|
||||||
|
"popup": "Open in new tab",
|
||||||
"title": "Cheatsheet",
|
"title": "Cheatsheet",
|
||||||
"headlines": {
|
"headlines": {
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
|
|
|
@ -15,7 +15,6 @@ import type { Icon } from 'react-bootstrap-icons'
|
||||||
|
|
||||||
export interface IconButtonProps extends ButtonProps, PropsWithDataTestId {
|
export interface IconButtonProps extends ButtonProps, PropsWithDataTestId {
|
||||||
icon: Icon
|
icon: Icon
|
||||||
onClick?: () => void
|
|
||||||
border?: boolean
|
border?: boolean
|
||||||
iconSize?: number | string
|
iconSize?: number | string
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,8 @@ import { cypressId } from '../../../utils/cypress-attribute'
|
||||||
import { testId } from '../../../utils/test-id'
|
import { testId } from '../../../utils/test-id'
|
||||||
import { UiIcon } from '../icons/ui-icon'
|
import { UiIcon } from '../icons/ui-icon'
|
||||||
import { ShowIf } from '../show-if/show-if'
|
import { ShowIf } from '../show-if/show-if'
|
||||||
import type { PropsWithChildren } from 'react'
|
import type { PropsWithChildren, ReactElement } from 'react'
|
||||||
import React, { useMemo } from 'react'
|
import React, { Fragment, useMemo } from 'react'
|
||||||
import { Modal } from 'react-bootstrap'
|
import { Modal } from 'react-bootstrap'
|
||||||
import type { Icon } from 'react-bootstrap-icons'
|
import type { Icon } from 'react-bootstrap-icons'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
@ -26,6 +26,7 @@ export interface ModalContentProps {
|
||||||
titleIcon?: Icon
|
titleIcon?: Icon
|
||||||
modalSize?: 'lg' | 'sm' | 'xl'
|
modalSize?: 'lg' | 'sm' | 'xl'
|
||||||
additionalClasses?: string
|
additionalClasses?: string
|
||||||
|
additionalTitleElement?: ReactElement
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CommonModalProps = PropsWithDataCypressId & ModalVisibilityProps & ModalContentProps
|
export type CommonModalProps = PropsWithDataCypressId & ModalVisibilityProps & ModalContentProps
|
||||||
|
@ -42,6 +43,7 @@ export type CommonModalProps = PropsWithDataCypressId & ModalVisibilityProps & M
|
||||||
* @param modalSize The modal size
|
* @param modalSize The modal size
|
||||||
* @param children The children to render into the modal.
|
* @param children The children to render into the modal.
|
||||||
* @param titleIsI18nKey If the title is a i18n key and should be used as such
|
* @param titleIsI18nKey If the title is a i18n key and should be used as such
|
||||||
|
* @param additionalTitleElement additional optional element that should be shown in the header
|
||||||
* @param props Additional props directly given to the modal
|
* @param props Additional props directly given to the modal
|
||||||
*/
|
*/
|
||||||
export const CommonModal: React.FC<PropsWithChildren<CommonModalProps>> = ({
|
export const CommonModal: React.FC<PropsWithChildren<CommonModalProps>> = ({
|
||||||
|
@ -54,6 +56,7 @@ export const CommonModal: React.FC<PropsWithChildren<CommonModalProps>> = ({
|
||||||
additionalClasses,
|
additionalClasses,
|
||||||
modalSize,
|
modalSize,
|
||||||
children,
|
children,
|
||||||
|
additionalTitleElement,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
@ -77,6 +80,7 @@ export const CommonModal: React.FC<PropsWithChildren<CommonModalProps>> = ({
|
||||||
<UiIcon icon={titleIcon} nbsp={true} />
|
<UiIcon icon={titleIcon} nbsp={true} />
|
||||||
{titleElement}
|
{titleElement}
|
||||||
</Modal.Title>
|
</Modal.Title>
|
||||||
|
{additionalTitleElement ?? <Fragment />}
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
{children}
|
{children}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -6,10 +6,10 @@
|
||||||
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
|
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
|
||||||
import { cypressId } from '../../../../utils/cypress-attribute'
|
import { cypressId } from '../../../../utils/cypress-attribute'
|
||||||
import { CommonModal } from '../../../common/modals/common-modal'
|
import { CommonModal } from '../../../common/modals/common-modal'
|
||||||
import { CheatsheetModalBody } from './cheatsheet-modal-body'
|
import { CheatsheetContent } from './cheatsheet-content'
|
||||||
|
import { CheatsheetInNewTabButton } from './cheatsheet-in-new-tab-button'
|
||||||
import React, { Fragment } from 'react'
|
import React, { Fragment } from 'react'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button, Modal } from 'react-bootstrap'
|
||||||
import { QuestionCircle as IconQuestionCircle } from 'react-bootstrap-icons'
|
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,12 +32,18 @@ export const CheatsheetButton: React.FC = () => {
|
||||||
</Button>
|
</Button>
|
||||||
<CommonModal
|
<CommonModal
|
||||||
modalSize={'xl'}
|
modalSize={'xl'}
|
||||||
titleIcon={IconQuestionCircle}
|
|
||||||
show={modalVisibility}
|
show={modalVisibility}
|
||||||
onHide={closeModal}
|
onHide={closeModal}
|
||||||
showCloseButton={true}
|
showCloseButton={true}
|
||||||
titleI18nKey={'cheatsheet.modal.title'}>
|
titleI18nKey={'cheatsheet.modal.title'}
|
||||||
<CheatsheetModalBody />
|
additionalTitleElement={
|
||||||
|
<div className={'d-flex flex-row-reverse w-100 mx-2'}>
|
||||||
|
<CheatsheetInNewTabButton onClick={closeModal} />
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<Modal.Body>
|
||||||
|
<CheatsheetContent />
|
||||||
|
</Modal.Body>
|
||||||
</CommonModal>
|
</CommonModal>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,13 +10,13 @@ import { CategoryAccordion } from './category-accordion'
|
||||||
import { CheatsheetEntryPane } from './cheatsheet-entry-pane'
|
import { CheatsheetEntryPane } from './cheatsheet-entry-pane'
|
||||||
import { TopicSelection } from './topic-selection'
|
import { TopicSelection } from './topic-selection'
|
||||||
import React, { useCallback, useMemo, useState } from 'react'
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
import { Col, ListGroup, Modal, Row } from 'react-bootstrap'
|
import { Col, ListGroup, Row } from 'react-bootstrap'
|
||||||
import { Trans } from 'react-i18next'
|
import { Trans } from 'react-i18next'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the tab content for the cheatsheet.
|
* Renders the tab content for the cheatsheet.
|
||||||
*/
|
*/
|
||||||
export const CheatsheetModalBody: React.FC = () => {
|
export const CheatsheetContent: React.FC = () => {
|
||||||
const [selectedExtension, setSelectedExtension] = useState<CheatsheetExtension>()
|
const [selectedExtension, setSelectedExtension] = useState<CheatsheetExtension>()
|
||||||
const [selectedEntry, setSelectedEntry] = useState<CheatsheetEntry>()
|
const [selectedEntry, setSelectedEntry] = useState<CheatsheetEntry>()
|
||||||
|
|
||||||
|
@ -31,35 +31,29 @@ export const CheatsheetModalBody: React.FC = () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal.Body>
|
<Row className={`mt-2`}>
|
||||||
<Row className={`mt-2`}>
|
<Col xs={3}>
|
||||||
<Col xs={3}>
|
<CategoryAccordion extensions={extensions} selectedEntry={selectedExtension} onStateChange={changeExtension} />
|
||||||
<CategoryAccordion
|
</Col>
|
||||||
extensions={extensions}
|
<Col xs={9}>
|
||||||
selectedEntry={selectedExtension}
|
<ListGroup>
|
||||||
onStateChange={changeExtension}
|
<TopicSelection
|
||||||
|
extension={selectedExtension}
|
||||||
|
selectedEntry={selectedEntry}
|
||||||
|
setSelectedEntry={setSelectedEntry}
|
||||||
/>
|
/>
|
||||||
</Col>
|
{selectedEntry !== undefined ? (
|
||||||
<Col xs={9}>
|
<CheatsheetEntryPane
|
||||||
<ListGroup>
|
rootI18nKey={isCheatsheetGroup(selectedExtension) ? selectedExtension.i18nKey : undefined}
|
||||||
<TopicSelection
|
extension={selectedEntry}
|
||||||
extension={selectedExtension}
|
|
||||||
selectedEntry={selectedEntry}
|
|
||||||
setSelectedEntry={setSelectedEntry}
|
|
||||||
/>
|
/>
|
||||||
{selectedEntry !== undefined ? (
|
) : (
|
||||||
<CheatsheetEntryPane
|
<span>
|
||||||
rootI18nKey={isCheatsheetGroup(selectedExtension) ? selectedExtension.i18nKey : undefined}
|
<Trans i18nKey={'cheatsheet.modal.noSelection'}></Trans>
|
||||||
extension={selectedEntry}
|
</span>
|
||||||
/>
|
)}
|
||||||
) : (
|
</ListGroup>
|
||||||
<span>
|
</Col>
|
||||||
<Trans i18nKey={'cheatsheet.modal.noSelection'}></Trans>
|
</Row>
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</ListGroup>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Modal.Body>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { IconButton } from '../../../common/icon-button/icon-button'
|
||||||
|
import type { MouseEvent } from 'react'
|
||||||
|
import React, { useCallback } from 'react'
|
||||||
|
import { BoxArrowUpRight } from 'react-bootstrap-icons'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
export interface CheatsheetInNewTabButtonProps {
|
||||||
|
onClick?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a button that opens the cheatsheet in a new tab.
|
||||||
|
*/
|
||||||
|
export const CheatsheetInNewTabButton: React.FC<CheatsheetInNewTabButtonProps> = ({ onClick }) => {
|
||||||
|
const openPopUp = useCallback(
|
||||||
|
(event: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
|
window.open('/cheatsheet', '_blank', 'menubar=no,width=1000,height=600,location=no,toolbar=no')
|
||||||
|
onClick?.()
|
||||||
|
},
|
||||||
|
[onClick]
|
||||||
|
)
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
size={'sm'}
|
||||||
|
iconSize={1.2}
|
||||||
|
href={'/cheatsheet'}
|
||||||
|
onClick={openPopUp}
|
||||||
|
icon={BoxArrowUpRight}
|
||||||
|
className={'p-2 border-0'}
|
||||||
|
variant={'outline-dark'}
|
||||||
|
target={'_blank'}
|
||||||
|
title={t('cheatsheet.modal.popup') ?? ''}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
21
frontend/src/pages/cheatsheet.tsx
Normal file
21
frontend/src/pages/cheatsheet.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { CheatsheetContent } from '../components/editor-page/app-bar/cheatsheet/cheatsheet-content'
|
||||||
|
import { useApplyDarkMode } from '../hooks/common/use-apply-dark-mode'
|
||||||
|
import type { NextPage } from 'next'
|
||||||
|
import { Container } from 'react-bootstrap'
|
||||||
|
|
||||||
|
const CheatsheetPage: NextPage = () => {
|
||||||
|
useApplyDarkMode()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<CheatsheetContent></CheatsheetContent>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CheatsheetPage
|
Loading…
Reference in a new issue