refactor: extract visual part of the toolbar-button component and use it in all buttons components

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-05-31 20:14:37 +02:00
parent 3a5ae8df3a
commit 14ba7ea9ce
26 changed files with 127 additions and 109 deletions

View file

@ -29,8 +29,8 @@ describe('File upload', () => {
})
it('via button', () => {
cy.getByCypressId('editor-pane').should('have.attr', 'data-cypress-editor-ready', 'true')
cy.getByCypressId('editor-toolbar-upload-image-button').should('be.visible')
cy.getByCypressId('editor-toolbar-upload-image-input').selectFile(
cy.getByCypressId('toolbar.uploadImage').should('be.visible')
cy.getByCypressId('toolbar.uploadImage.input').selectFile(
{
contents: '@demoImage',
fileName: 'demo.png',
@ -80,8 +80,8 @@ describe('File upload', () => {
statusCode: 400
}
)
cy.getByCypressId('editor-toolbar-upload-image-button').should('be.visible')
cy.getByCypressId('editor-toolbar-upload-image-input').selectFile(
cy.getByCypressId('toolbar.uploadImage').should('be.visible')
cy.getByCypressId('toolbar.uploadImage.input').selectFile(
{
contents: '@demoImage',
fileName: 'demo.png',

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { FormatterToolbarButton } from '../formatter-toolbar-button'
import { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'
import { TypeBold as IconTypeBold } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const BoldButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection }) => {
return wrapSelection(currentSelection, '**', '**')
}, [])
return <ToolbarButton i18nKey={'bold'} icon={IconTypeBold} formatter={formatter}></ToolbarButton>
return <FormatterToolbarButton i18nKey={'bold'} icon={IconTypeBold} formatter={formatter} />
}

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { FormatterToolbarButton } from '../formatter-toolbar-button'
import { prependLinesOfSelection } from '../formatters/prepend-lines-of-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'
import { CheckSquare as IconCheckSquare } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const CheckListButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => {
return prependLinesOfSelection(markdownContent, currentSelection, () => `- [ ] `)
}, [])
return <ToolbarButton i18nKey={'checkList'} icon={IconCheckSquare} formatter={formatter}></ToolbarButton>
return <FormatterToolbarButton i18nKey={'checkList'} icon={IconCheckSquare} formatter={formatter} />
}

View file

@ -4,9 +4,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { FormatterToolbarButton } from '../formatter-toolbar-button'
import { changeCursorsToWholeLineIfNoToCursor } from '../formatters/utils/change-cursors-to-whole-line-if-no-to-cursor'
import { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'
import { Code as IconCode } from 'react-bootstrap-icons'
@ -17,5 +17,5 @@ export const CodeFenceButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => {
return wrapSelection(changeCursorsToWholeLineIfNoToCursor(markdownContent, currentSelection), '```\n', '\n```')
}, [])
return <ToolbarButton i18nKey={'code'} icon={IconCode} formatter={formatter}></ToolbarButton>
return <FormatterToolbarButton i18nKey={'code'} icon={IconCode} formatter={formatter} />
}

View file

@ -4,9 +4,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { FormatterToolbarButton } from '../formatter-toolbar-button'
import { changeCursorsToWholeLineIfNoToCursor } from '../formatters/utils/change-cursors-to-whole-line-if-no-to-cursor'
import { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'
import { ArrowsCollapse as IconArrowsCollapse } from 'react-bootstrap-icons'
@ -21,5 +21,5 @@ export const CollapsibleBlockButton: React.FC = () => {
'\n:::\n'
)
}, [])
return <ToolbarButton i18nKey={'collapsibleBlock'} icon={IconArrowsCollapse} formatter={formatter}></ToolbarButton>
return <FormatterToolbarButton i18nKey={'collapsibleBlock'} icon={IconArrowsCollapse} formatter={formatter} />
}

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { FormatterToolbarButton } from '../formatter-toolbar-button'
import { replaceSelection } from '../formatters/replace-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'
import { ChatDots as IconChatDots } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const CommentButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection }) => {
return replaceSelection({ from: currentSelection.to ?? currentSelection.from }, '> []', true)
}, [])
return <ToolbarButton i18nKey={'comment'} icon={IconChatDots} formatter={formatter}></ToolbarButton>
return <FormatterToolbarButton i18nKey={'comment'} icon={IconChatDots} formatter={formatter} />
}

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { FormatterToolbarButton } from '../formatter-toolbar-button'
import { prependLinesOfSelection } from '../formatters/prepend-lines-of-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'
import { TypeH1 as IconTypeH1 } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const HeaderLevelButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => {
return prependLinesOfSelection(markdownContent, currentSelection, (line) => (line.startsWith('#') ? `#` : `# `))
}, [])
return <ToolbarButton i18nKey={'header'} icon={IconTypeH1} formatter={formatter}></ToolbarButton>
return <FormatterToolbarButton i18nKey={'header'} icon={IconTypeH1} formatter={formatter} />
}

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { FormatterToolbarButton } from '../formatter-toolbar-button'
import { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'
import { Eraser as IconEraser } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const HighlightButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection }) => {
return wrapSelection(currentSelection, '==', '==')
}, [])
return <ToolbarButton i18nKey={'highlight'} icon={IconEraser} formatter={formatter}></ToolbarButton>
return <FormatterToolbarButton i18nKey={'highlight'} icon={IconEraser} formatter={formatter} />
}

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { FormatterToolbarButton } from '../formatter-toolbar-button'
import { replaceSelection } from '../formatters/replace-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'
import { DashLg as IconDashLg } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const HorizontalLineButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection }) => {
return replaceSelection({ from: currentSelection.to ?? currentSelection.from }, '----\n', true)
}, [])
return <ToolbarButton i18nKey={'horizontalLine'} icon={IconDashLg} formatter={formatter}></ToolbarButton>
return <FormatterToolbarButton i18nKey={'horizontalLine'} icon={IconDashLg} formatter={formatter} />
}

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { FormatterToolbarButton } from '../formatter-toolbar-button'
import { addLink } from '../formatters/add-link'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'
import { Image as IconImage } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const ImageLinkButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => {
return addLink(markdownContent, currentSelection, '!')
}, [])
return <ToolbarButton i18nKey={'imageLink'} icon={IconImage} formatter={formatter}></ToolbarButton>
return <FormatterToolbarButton i18nKey={'imageLink'} icon={IconImage} formatter={formatter} />
}

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { FormatterToolbarButton } from '../formatter-toolbar-button'
import { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'
import { TypeItalic as IconTypeItalic } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const ItalicButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection }) => {
return wrapSelection(currentSelection, '*', '*')
}, [])
return <ToolbarButton i18nKey={'italic'} icon={IconTypeItalic} formatter={formatter}></ToolbarButton>
return <FormatterToolbarButton i18nKey={'italic'} icon={IconTypeItalic} formatter={formatter} />
}

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { FormatterToolbarButton } from '../formatter-toolbar-button'
import { addLink } from '../formatters/add-link'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'
import { Link as IconLink } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const LinkButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => {
return addLink(markdownContent, currentSelection)
}, [])
return <ToolbarButton i18nKey={'link'} icon={IconLink} formatter={formatter}></ToolbarButton>
return <FormatterToolbarButton i18nKey={'link'} icon={IconLink} formatter={formatter} />
}

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { FormatterToolbarButton } from '../formatter-toolbar-button'
import { prependLinesOfSelection } from '../formatters/prepend-lines-of-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'
import { ListOl as IconListOl } from 'react-bootstrap-icons'
@ -20,5 +20,5 @@ export const OrderedListButton: React.FC = () => {
(line, lineIndexInBlock) => `${lineIndexInBlock + 1}. `
)
}, [])
return <ToolbarButton i18nKey={'orderedList'} icon={IconListOl} formatter={formatter}></ToolbarButton>
return <FormatterToolbarButton i18nKey={'orderedList'} icon={IconListOl} formatter={formatter} />
}

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { FormatterToolbarButton } from '../formatter-toolbar-button'
import { prependLinesOfSelection } from '../formatters/prepend-lines-of-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'
import { Quote as IconQuote } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const QuotesButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => {
return prependLinesOfSelection(markdownContent, currentSelection, () => `> `)
}, [])
return <ToolbarButton i18nKey={'blockquote'} icon={IconQuote} formatter={formatter}></ToolbarButton>
return <FormatterToolbarButton i18nKey={'blockquote'} icon={IconQuote} formatter={formatter} />
}

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { FormatterToolbarButton } from '../formatter-toolbar-button'
import { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'
import { TypeStrikethrough as IconTypeStrikethrough } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const StrikethroughButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection }) => {
return wrapSelection(currentSelection, '~~', '~~')
}, [])
return <ToolbarButton i18nKey={'strikethrough'} icon={IconTypeStrikethrough} formatter={formatter}></ToolbarButton>
return <FormatterToolbarButton i18nKey={'strikethrough'} icon={IconTypeStrikethrough} formatter={formatter} />
}

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { FormatterToolbarButton } from '../formatter-toolbar-button'
import { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'
import { Subscript as IconSubscript } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const SubscriptButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection }) => {
return wrapSelection(currentSelection, '~', '~')
}, [])
return <ToolbarButton i18nKey={'subscript'} icon={IconSubscript} formatter={formatter}></ToolbarButton>
return <FormatterToolbarButton i18nKey={'subscript'} icon={IconSubscript} formatter={formatter} />
}

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { FormatterToolbarButton } from '../formatter-toolbar-button'
import { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'
import { Superscript as IconSuperscript } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const SuperscriptButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection }) => {
return wrapSelection(currentSelection, '^', '^')
}, [])
return <ToolbarButton i18nKey={'superscript'} icon={IconSuperscript} formatter={formatter}></ToolbarButton>
return <FormatterToolbarButton i18nKey={'superscript'} icon={IconSuperscript} formatter={formatter} />
}

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { FormatterToolbarButton } from '../formatter-toolbar-button'
import { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'
import { TypeUnderline as IconTypeUnderline } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const UnderlineButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection }) => {
return wrapSelection(currentSelection, '++', '++')
}, [])
return <ToolbarButton i18nKey={'underline'} icon={IconTypeUnderline} formatter={formatter}></ToolbarButton>
return <FormatterToolbarButton i18nKey={'underline'} icon={IconTypeUnderline} formatter={formatter} />
}

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback'
import { FormatterToolbarButton } from '../formatter-toolbar-button'
import { prependLinesOfSelection } from '../formatters/prepend-lines-of-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react'
import { List as IconList } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const UnorderedListButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => {
return prependLinesOfSelection(markdownContent, currentSelection, () => `- `)
}, [])
return <ToolbarButton i18nKey={'unorderedList'} icon={IconList} formatter={formatter}></ToolbarButton>
return <FormatterToolbarButton i18nKey={'unorderedList'} icon={IconList} formatter={formatter} />
}

View file

@ -3,29 +3,26 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { cypressId } from '../../../../../utils/cypress-attribute'
import { UiIcon } from '../../../../common/icons/ui-icon'
import { useChangeEditorContentCallback } from '../../../change-content-context/use-change-editor-content-callback'
import { replaceSelection } from '../formatters/replace-selection'
import { ToolbarButton } from '../toolbar-button'
import { EmojiPickerPopover } from './emoji-picker-popover'
import styles from './emoji-picker.module.scss'
import { extractEmojiShortCode } from './extract-emoji-short-code'
import type { EmojiClickEventDetail } from 'emoji-picker-element/shared'
import React, { Fragment, useCallback, useRef, useState } from 'react'
import { Button, Overlay } from 'react-bootstrap'
import { Overlay } from 'react-bootstrap'
import { EmojiSmile as IconEmojiSmile } from 'react-bootstrap-icons'
import type { OverlayInjectedProps } from 'react-bootstrap/Overlay'
import { useTranslation } from 'react-i18next'
/**
* Renders a button to open the emoji picker.
* @see EmojiPickerPopover
*/
export const EmojiPickerButton: React.FC = () => {
const { t } = useTranslation()
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const changeEditorContent = useChangeEditorContentCallback()
const buttonRef = useRef(null)
const buttonRef = useRef<HTMLButtonElement>(null)
const onEmojiSelected = useCallback(
(emojiClickEvent: EmojiClickEventDetail) => {
@ -57,15 +54,7 @@ export const EmojiPickerButton: React.FC = () => {
offset={[0, 0]}>
{createPopoverElement}
</Overlay>
<Button
{...cypressId('show-emoji-picker')}
variant='light'
onClick={showPicker}
title={t('editor.editorToolbar.emoji') ?? undefined}
disabled={!changeEditorContent}
ref={buttonRef}>
<UiIcon icon={IconEmojiSmile} />
</Button>
<ToolbarButton i18nKey={'emoji'} icon={IconEmojiSmile} onClick={showPicker} buttonRef={buttonRef}></ToolbarButton>
</Fragment>
)
}

View file

@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ContentFormatter } from '../../change-content-context/use-change-editor-content-callback'
import { useChangeEditorContentCallback } from '../../change-content-context/use-change-editor-content-callback'
import type { ToolbarButtonProps } from './toolbar-button'
import { ToolbarButton } from './toolbar-button'
import React, { useCallback } from 'react'
export interface FormatterToolbarButtonProps extends Omit<ToolbarButtonProps, 'onClick' | 'disabled'> {
formatter: ContentFormatter
}
/**
* Renders a button for the editor toolbar that formats the content using a given formatter function.
*
* @param i18nKey Used to generate a title for the button by interpreting it as translation key in the i18n-namespace `editor.editorToolbar`-
* @param iconName A fork awesome icon name that is shown in the button
* @param formatter The formatter function changes the editor content on click
*/
export const FormatterToolbarButton: React.FC<FormatterToolbarButtonProps> = ({ i18nKey, icon, formatter }) => {
const changeEditorContent = useChangeEditorContentCallback()
const onClick = useCallback(() => {
changeEditorContent?.(formatter)
}, [formatter, changeEditorContent])
return <ToolbarButton i18nKey={i18nKey} icon={icon} onClick={onClick} disabled={!changeEditorContent} />
}

View file

@ -3,19 +3,17 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { cypressId } from '../../../../../utils/cypress-attribute'
import { UiIcon } from '../../../../common/icons/ui-icon'
import { useChangeEditorContentCallback } from '../../../change-content-context/use-change-editor-content-callback'
import { replaceSelection } from '../formatters/replace-selection'
import { ToolbarButton } from '../toolbar-button'
import { createMarkdownTable } from './create-markdown-table'
import { CustomTableSizeModal } from './custom-table-size-modal'
import './table-picker.module.scss'
import { TableSizePickerPopover } from './table-size-picker-popover'
import React, { Fragment, useCallback, useMemo, useRef, useState } from 'react'
import { Button, Overlay } from 'react-bootstrap'
import React, { Fragment, useCallback, useRef, useState } from 'react'
import { Overlay } from 'react-bootstrap'
import { Table as IconTable } from 'react-bootstrap-icons'
import type { OverlayInjectedProps } from 'react-bootstrap/Overlay'
import { useTranslation } from 'react-i18next'
enum PickerMode {
INVISIBLE,
@ -27,7 +25,6 @@ enum PickerMode {
* Toggles the visibility of a table size picker overlay and inserts the result into the editor.
*/
export const TablePickerButton: React.FC = () => {
const { t } = useTranslation()
const [pickerMode, setPickerMode] = useState<PickerMode>(PickerMode.INVISIBLE)
const onDismiss = useCallback(() => setPickerMode(PickerMode.INVISIBLE), [])
const onShowModal = useCallback(() => setPickerMode(PickerMode.CUSTOM), [])
@ -42,7 +39,6 @@ export const TablePickerButton: React.FC = () => {
[changeEditorContent]
)
const tableTitle = useMemo(() => t('editor.editorToolbar.table.titleWithoutSize'), [t])
const button = useRef(null)
const toggleOverlayVisibility = useCallback(() => {
setPickerMode((oldPickerMode) => (oldPickerMode === PickerMode.INVISIBLE ? PickerMode.GRID : PickerMode.INVISIBLE))
@ -71,15 +67,12 @@ export const TablePickerButton: React.FC = () => {
return (
<Fragment>
<Button
{...cypressId('table-size-picker-button')}
variant='light'
<ToolbarButton
i18nKey={'table.titleWithoutSize'}
icon={IconTable}
onClick={toggleOverlayVisibility}
title={tableTitle}
ref={button}
disabled={!changeEditorContent}>
<UiIcon icon={IconTable} />
</Button>
buttonRef={button}
/>
<Overlay
target={button.current}
onHide={onOverlayHide}

View file

@ -4,10 +4,16 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
.table-cell {
.cell {
&.selected {
background: var(--bs-primary);
border-color: transparent;
}
background: transparent;
border-color: var(--bs-secondary);
margin: 1px;
border-radius: 2px;
border: solid 1px var(--bs-dark);
border: solid 1px;
}
.table-container {

View file

@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { concatCssClasses } from '../../../../../utils/concat-css-classes'
import { cypressAttribute, cypressId } from '../../../../../utils/cypress-attribute'
import { UiIcon } from '../../../../common/icons/ui-icon'
import { createNumberRangeArray } from '../../../../common/number-range/number-range'
@ -55,7 +56,7 @@ export const TableSizePickerPopover = React.forwardRef<HTMLDivElement, TableSize
return (
<div
key={`${row}_${col}`}
className={`${styles['table-cell']} ${selected ? 'bg-primary border-primary' : ''}`}
className={concatCssClasses(styles.cell, { [styles.selected]: selected })}
{...cypressAttribute('selected', selected ? 'true' : 'false')}
{...cypressAttribute('col', `${col + 1}`)}
{...cypressAttribute('row', `${row + 1}`)}
@ -70,7 +71,7 @@ export const TableSizePickerPopover = React.forwardRef<HTMLDivElement, TableSize
)
return (
<Popover ref={ref} {...cypressId('table-size-picker-popover')} className={`bg-light`} {...props}>
<Popover ref={ref} {...cypressId('table-size-picker-popover')} className={`bg-body`} {...props}>
<Popover.Header>
<TableSizeText tableSize={tableSize} />
</Popover.Header>

View file

@ -5,9 +5,8 @@
*/
import { cypressId } from '../../../../utils/cypress-attribute'
import { UiIcon } from '../../../common/icons/ui-icon'
import type { ContentFormatter } from '../../change-content-context/use-change-editor-content-callback'
import { useChangeEditorContentCallback } from '../../change-content-context/use-change-editor-content-callback'
import React, { useCallback, useMemo } from 'react'
import type { PropsWithChildren, RefObject } from 'react'
import React, { useMemo } from 'react'
import { Button } from 'react-bootstrap'
import type { Icon } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
@ -15,33 +14,41 @@ import { useTranslation } from 'react-i18next'
export interface ToolbarButtonProps {
i18nKey: string
icon: Icon
formatter: ContentFormatter
onClick: () => void
disabled?: boolean
buttonRef?: RefObject<HTMLButtonElement>
}
/**
* Renders a button for the editor toolbar that formats the content using a given formatter function.
* Renders a button for the editor toolbar.
*
* @param i18nKey Used to generate a title for the button by interpreting it as translation key in the i18n-namespace `editor.editorToolbar`-
* @param iconName A fork awesome icon name that is shown in the button
* @param formatter The formatter function changes the editor content on click
* @param iconName An icon that is shown in the button
* @param onClick A callback that is executed on click
* @param disabled Defines if the button is disabled
* @param buttonRef A reference to the button element
*/
export const ToolbarButton: React.FC<ToolbarButtonProps> = ({ i18nKey, icon, formatter }) => {
export const ToolbarButton: React.FC<PropsWithChildren<ToolbarButtonProps>> = ({
i18nKey,
icon,
onClick,
disabled = false,
buttonRef,
children
}) => {
const { t } = useTranslation('', { keyPrefix: 'editor.editorToolbar' })
const changeEditorContent = useChangeEditorContentCallback()
const onClick = useCallback(() => {
changeEditorContent?.(formatter)
}, [formatter, changeEditorContent])
const title = useMemo(() => t(i18nKey), [i18nKey, t])
return (
<Button
variant='light'
variant={'light'}
onClick={onClick}
title={title}
disabled={!changeEditorContent}
ref={buttonRef}
disabled={disabled}
{...cypressId('toolbar.' + i18nKey)}>
<UiIcon icon={icon} />
{children}
</Button>
)
}

View file

@ -5,18 +5,16 @@
*/
import { cypressId } from '../../../../../utils/cypress-attribute'
import { Logger } from '../../../../../utils/logger'
import { UiIcon } from '../../../../common/icons/ui-icon'
import { ShowIf } from '../../../../common/show-if/show-if'
import { acceptedMimeTypes } from '../../../../common/upload-image-mimetypes'
import { UploadInput } from '../../../../common/upload-input'
import { useCodemirrorReferenceContext } from '../../../change-content-context/codemirror-reference-context'
import { useHandleUpload } from '../../hooks/use-handle-upload'
import { ToolbarButton } from '../toolbar-button'
import { extractSelectedText } from './extract-selected-text'
import { Optional } from '@mrdrogdrog/optional'
import React, { Fragment, useCallback, useRef } from 'react'
import { Button } from 'react-bootstrap'
import { Upload as IconUpload } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
const logger = new Logger('Upload image button')
@ -24,7 +22,6 @@ const logger = new Logger('Upload image button')
* Shows a button that uploads a chosen file to the backend and adds the link to the note.
*/
export const UploadImageButton: React.FC = () => {
const { t } = useTranslation()
const clickRef = useRef<() => void>()
const buttonClick = useCallback(() => {
clickRef.current?.()
@ -49,22 +46,16 @@ export const UploadImageButton: React.FC = () => {
return (
<Fragment>
<Button
variant='light'
onClick={buttonClick}
disabled={!codeMirror}
title={t('editor.editorToolbar.uploadImage') ?? undefined}
{...cypressId('editor-toolbar-upload-image-button')}>
<UiIcon icon={IconUpload} />
</Button>
<ToolbarButton i18nKey={'uploadImage'} icon={IconUpload} onClick={buttonClick}>
<ShowIf condition={!!codeMirror}>
<UploadInput
onLoad={onUploadImage}
allowedFileTypes={acceptedMimeTypes}
onClickRef={clickRef}
{...cypressId('editor-toolbar-upload-image-input')}
{...cypressId('toolbar.uploadImage.input')}
/>
</ShowIf>
</ToolbarButton>
</Fragment>
)
}