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

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback' 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 { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { TypeBold as IconTypeBold } from 'react-bootstrap-icons' import { TypeBold as IconTypeBold } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const BoldButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection }) => { const formatter: ContentFormatter = useCallback(({ currentSelection }) => {
return wrapSelection(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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback' 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 { prependLinesOfSelection } from '../formatters/prepend-lines-of-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { CheckSquare as IconCheckSquare } from 'react-bootstrap-icons' import { CheckSquare as IconCheckSquare } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const CheckListButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => { const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => {
return prependLinesOfSelection(markdownContent, currentSelection, () => `- [ ] `) 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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback' 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 { changeCursorsToWholeLineIfNoToCursor } from '../formatters/utils/change-cursors-to-whole-line-if-no-to-cursor'
import { wrapSelection } from '../formatters/wrap-selection' import { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { Code as IconCode } from 'react-bootstrap-icons' import { Code as IconCode } from 'react-bootstrap-icons'
@ -17,5 +17,5 @@ export const CodeFenceButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => { const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => {
return wrapSelection(changeCursorsToWholeLineIfNoToCursor(markdownContent, currentSelection), '```\n', '\n```') 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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback' 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 { changeCursorsToWholeLineIfNoToCursor } from '../formatters/utils/change-cursors-to-whole-line-if-no-to-cursor'
import { wrapSelection } from '../formatters/wrap-selection' import { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { ArrowsCollapse as IconArrowsCollapse } from 'react-bootstrap-icons' import { ArrowsCollapse as IconArrowsCollapse } from 'react-bootstrap-icons'
@ -21,5 +21,5 @@ export const CollapsibleBlockButton: React.FC = () => {
'\n:::\n' '\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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback' 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 { replaceSelection } from '../formatters/replace-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { ChatDots as IconChatDots } from 'react-bootstrap-icons' import { ChatDots as IconChatDots } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const CommentButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection }) => { const formatter: ContentFormatter = useCallback(({ currentSelection }) => {
return replaceSelection({ from: currentSelection.to ?? currentSelection.from }, '> []', true) 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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback' 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 { prependLinesOfSelection } from '../formatters/prepend-lines-of-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { TypeH1 as IconTypeH1 } from 'react-bootstrap-icons' import { TypeH1 as IconTypeH1 } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const HeaderLevelButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => { const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => {
return prependLinesOfSelection(markdownContent, currentSelection, (line) => (line.startsWith('#') ? `#` : `# `)) 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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback' 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 { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { Eraser as IconEraser } from 'react-bootstrap-icons' import { Eraser as IconEraser } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const HighlightButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection }) => { const formatter: ContentFormatter = useCallback(({ currentSelection }) => {
return wrapSelection(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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback' 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 { replaceSelection } from '../formatters/replace-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { DashLg as IconDashLg } from 'react-bootstrap-icons' import { DashLg as IconDashLg } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const HorizontalLineButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection }) => { const formatter: ContentFormatter = useCallback(({ currentSelection }) => {
return replaceSelection({ from: currentSelection.to ?? currentSelection.from }, '----\n', true) 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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback' 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 { addLink } from '../formatters/add-link'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { Image as IconImage } from 'react-bootstrap-icons' import { Image as IconImage } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const ImageLinkButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => { const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => {
return addLink(markdownContent, currentSelection, '!') 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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback' 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 { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { TypeItalic as IconTypeItalic } from 'react-bootstrap-icons' import { TypeItalic as IconTypeItalic } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const ItalicButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection }) => { const formatter: ContentFormatter = useCallback(({ currentSelection }) => {
return wrapSelection(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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback' 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 { addLink } from '../formatters/add-link'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { Link as IconLink } from 'react-bootstrap-icons' import { Link as IconLink } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const LinkButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => { const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => {
return addLink(markdownContent, currentSelection) 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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback' 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 { prependLinesOfSelection } from '../formatters/prepend-lines-of-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { ListOl as IconListOl } from 'react-bootstrap-icons' import { ListOl as IconListOl } from 'react-bootstrap-icons'
@ -20,5 +20,5 @@ export const OrderedListButton: React.FC = () => {
(line, lineIndexInBlock) => `${lineIndexInBlock + 1}. ` (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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback' 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 { prependLinesOfSelection } from '../formatters/prepend-lines-of-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { Quote as IconQuote } from 'react-bootstrap-icons' import { Quote as IconQuote } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const QuotesButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => { const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => {
return prependLinesOfSelection(markdownContent, currentSelection, () => `> `) 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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback' 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 { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { TypeStrikethrough as IconTypeStrikethrough } from 'react-bootstrap-icons' import { TypeStrikethrough as IconTypeStrikethrough } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const StrikethroughButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection }) => { const formatter: ContentFormatter = useCallback(({ currentSelection }) => {
return wrapSelection(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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback' 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 { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { Subscript as IconSubscript } from 'react-bootstrap-icons' import { Subscript as IconSubscript } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const SubscriptButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection }) => { const formatter: ContentFormatter = useCallback(({ currentSelection }) => {
return wrapSelection(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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback' 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 { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { Superscript as IconSuperscript } from 'react-bootstrap-icons' import { Superscript as IconSuperscript } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const SuperscriptButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection }) => { const formatter: ContentFormatter = useCallback(({ currentSelection }) => {
return wrapSelection(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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback' 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 { wrapSelection } from '../formatters/wrap-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { TypeUnderline as IconTypeUnderline } from 'react-bootstrap-icons' import { TypeUnderline as IconTypeUnderline } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const UnderlineButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection }) => { const formatter: ContentFormatter = useCallback(({ currentSelection }) => {
return wrapSelection(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 * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import type { ContentFormatter } from '../../../change-content-context/use-change-editor-content-callback' 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 { prependLinesOfSelection } from '../formatters/prepend-lines-of-selection'
import { ToolbarButton } from '../toolbar-button'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { List as IconList } from 'react-bootstrap-icons' import { List as IconList } from 'react-bootstrap-icons'
@ -16,5 +16,5 @@ export const UnorderedListButton: React.FC = () => {
const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => { const formatter: ContentFormatter = useCallback(({ currentSelection, markdownContent }) => {
return prependLinesOfSelection(markdownContent, currentSelection, () => `- `) 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 * 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 { useChangeEditorContentCallback } from '../../../change-content-context/use-change-editor-content-callback'
import { replaceSelection } from '../formatters/replace-selection' import { replaceSelection } from '../formatters/replace-selection'
import { ToolbarButton } from '../toolbar-button'
import { EmojiPickerPopover } from './emoji-picker-popover' import { EmojiPickerPopover } from './emoji-picker-popover'
import styles from './emoji-picker.module.scss' import styles from './emoji-picker.module.scss'
import { extractEmojiShortCode } from './extract-emoji-short-code' import { extractEmojiShortCode } from './extract-emoji-short-code'
import type { EmojiClickEventDetail } from 'emoji-picker-element/shared' import type { EmojiClickEventDetail } from 'emoji-picker-element/shared'
import React, { Fragment, useCallback, useRef, useState } from 'react' 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 { EmojiSmile as IconEmojiSmile } from 'react-bootstrap-icons'
import type { OverlayInjectedProps } from 'react-bootstrap/Overlay' import type { OverlayInjectedProps } from 'react-bootstrap/Overlay'
import { useTranslation } from 'react-i18next'
/** /**
* Renders a button to open the emoji picker. * Renders a button to open the emoji picker.
* @see EmojiPickerPopover * @see EmojiPickerPopover
*/ */
export const EmojiPickerButton: React.FC = () => { export const EmojiPickerButton: React.FC = () => {
const { t } = useTranslation()
const [showEmojiPicker, setShowEmojiPicker] = useState(false) const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const changeEditorContent = useChangeEditorContentCallback() const changeEditorContent = useChangeEditorContentCallback()
const buttonRef = useRef(null) const buttonRef = useRef<HTMLButtonElement>(null)
const onEmojiSelected = useCallback( const onEmojiSelected = useCallback(
(emojiClickEvent: EmojiClickEventDetail) => { (emojiClickEvent: EmojiClickEventDetail) => {
@ -57,15 +54,7 @@ export const EmojiPickerButton: React.FC = () => {
offset={[0, 0]}> offset={[0, 0]}>
{createPopoverElement} {createPopoverElement}
</Overlay> </Overlay>
<Button <ToolbarButton i18nKey={'emoji'} icon={IconEmojiSmile} onClick={showPicker} buttonRef={buttonRef}></ToolbarButton>
{...cypressId('show-emoji-picker')}
variant='light'
onClick={showPicker}
title={t('editor.editorToolbar.emoji') ?? undefined}
disabled={!changeEditorContent}
ref={buttonRef}>
<UiIcon icon={IconEmojiSmile} />
</Button>
</Fragment> </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 * 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 { useChangeEditorContentCallback } from '../../../change-content-context/use-change-editor-content-callback'
import { replaceSelection } from '../formatters/replace-selection' import { replaceSelection } from '../formatters/replace-selection'
import { ToolbarButton } from '../toolbar-button'
import { createMarkdownTable } from './create-markdown-table' import { createMarkdownTable } from './create-markdown-table'
import { CustomTableSizeModal } from './custom-table-size-modal' import { CustomTableSizeModal } from './custom-table-size-modal'
import './table-picker.module.scss' import './table-picker.module.scss'
import { TableSizePickerPopover } from './table-size-picker-popover' import { TableSizePickerPopover } from './table-size-picker-popover'
import React, { Fragment, useCallback, useMemo, useRef, useState } from 'react' import React, { Fragment, useCallback, useRef, useState } from 'react'
import { Button, Overlay } from 'react-bootstrap' import { Overlay } from 'react-bootstrap'
import { Table as IconTable } from 'react-bootstrap-icons' import { Table as IconTable } from 'react-bootstrap-icons'
import type { OverlayInjectedProps } from 'react-bootstrap/Overlay' import type { OverlayInjectedProps } from 'react-bootstrap/Overlay'
import { useTranslation } from 'react-i18next'
enum PickerMode { enum PickerMode {
INVISIBLE, INVISIBLE,
@ -27,7 +25,6 @@ enum PickerMode {
* Toggles the visibility of a table size picker overlay and inserts the result into the editor. * Toggles the visibility of a table size picker overlay and inserts the result into the editor.
*/ */
export const TablePickerButton: React.FC = () => { export const TablePickerButton: React.FC = () => {
const { t } = useTranslation()
const [pickerMode, setPickerMode] = useState<PickerMode>(PickerMode.INVISIBLE) const [pickerMode, setPickerMode] = useState<PickerMode>(PickerMode.INVISIBLE)
const onDismiss = useCallback(() => setPickerMode(PickerMode.INVISIBLE), []) const onDismiss = useCallback(() => setPickerMode(PickerMode.INVISIBLE), [])
const onShowModal = useCallback(() => setPickerMode(PickerMode.CUSTOM), []) const onShowModal = useCallback(() => setPickerMode(PickerMode.CUSTOM), [])
@ -42,7 +39,6 @@ export const TablePickerButton: React.FC = () => {
[changeEditorContent] [changeEditorContent]
) )
const tableTitle = useMemo(() => t('editor.editorToolbar.table.titleWithoutSize'), [t])
const button = useRef(null) const button = useRef(null)
const toggleOverlayVisibility = useCallback(() => { const toggleOverlayVisibility = useCallback(() => {
setPickerMode((oldPickerMode) => (oldPickerMode === PickerMode.INVISIBLE ? PickerMode.GRID : PickerMode.INVISIBLE)) setPickerMode((oldPickerMode) => (oldPickerMode === PickerMode.INVISIBLE ? PickerMode.GRID : PickerMode.INVISIBLE))
@ -71,15 +67,12 @@ export const TablePickerButton: React.FC = () => {
return ( return (
<Fragment> <Fragment>
<Button <ToolbarButton
{...cypressId('table-size-picker-button')} i18nKey={'table.titleWithoutSize'}
variant='light' icon={IconTable}
onClick={toggleOverlayVisibility} onClick={toggleOverlayVisibility}
title={tableTitle} buttonRef={button}
ref={button} />
disabled={!changeEditorContent}>
<UiIcon icon={IconTable} />
</Button>
<Overlay <Overlay
target={button.current} target={button.current}
onHide={onOverlayHide} onHide={onOverlayHide}

View file

@ -4,10 +4,16 @@
* SPDX-License-Identifier: AGPL-3.0-only * 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; margin: 1px;
border-radius: 2px; border-radius: 2px;
border: solid 1px var(--bs-dark); border: solid 1px;
} }
.table-container { .table-container {

View file

@ -3,6 +3,7 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { concatCssClasses } from '../../../../../utils/concat-css-classes'
import { cypressAttribute, cypressId } from '../../../../../utils/cypress-attribute' import { cypressAttribute, cypressId } from '../../../../../utils/cypress-attribute'
import { UiIcon } from '../../../../common/icons/ui-icon' import { UiIcon } from '../../../../common/icons/ui-icon'
import { createNumberRangeArray } from '../../../../common/number-range/number-range' import { createNumberRangeArray } from '../../../../common/number-range/number-range'
@ -55,7 +56,7 @@ export const TableSizePickerPopover = React.forwardRef<HTMLDivElement, TableSize
return ( return (
<div <div
key={`${row}_${col}`} 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('selected', selected ? 'true' : 'false')}
{...cypressAttribute('col', `${col + 1}`)} {...cypressAttribute('col', `${col + 1}`)}
{...cypressAttribute('row', `${row + 1}`)} {...cypressAttribute('row', `${row + 1}`)}
@ -70,7 +71,7 @@ export const TableSizePickerPopover = React.forwardRef<HTMLDivElement, TableSize
) )
return ( 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> <Popover.Header>
<TableSizeText tableSize={tableSize} /> <TableSizeText tableSize={tableSize} />
</Popover.Header> </Popover.Header>

View file

@ -5,9 +5,8 @@
*/ */
import { cypressId } from '../../../../utils/cypress-attribute' import { cypressId } from '../../../../utils/cypress-attribute'
import { UiIcon } from '../../../common/icons/ui-icon' import { UiIcon } from '../../../common/icons/ui-icon'
import type { ContentFormatter } from '../../change-content-context/use-change-editor-content-callback' import type { PropsWithChildren, RefObject } from 'react'
import { useChangeEditorContentCallback } from '../../change-content-context/use-change-editor-content-callback' import React, { useMemo } from 'react'
import React, { useCallback, useMemo } from 'react'
import { Button } from 'react-bootstrap' import { Button } from 'react-bootstrap'
import type { Icon } from 'react-bootstrap-icons' import type { Icon } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -15,33 +14,41 @@ import { useTranslation } from 'react-i18next'
export interface ToolbarButtonProps { export interface ToolbarButtonProps {
i18nKey: string i18nKey: string
icon: Icon 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 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 iconName An icon that is shown in the button
* @param formatter The formatter function changes the editor content on click * @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 { t } = useTranslation('', { keyPrefix: 'editor.editorToolbar' })
const changeEditorContent = useChangeEditorContentCallback()
const onClick = useCallback(() => {
changeEditorContent?.(formatter)
}, [formatter, changeEditorContent])
const title = useMemo(() => t(i18nKey), [i18nKey, t]) const title = useMemo(() => t(i18nKey), [i18nKey, t])
return ( return (
<Button <Button
variant='light' variant={'light'}
onClick={onClick} onClick={onClick}
title={title} title={title}
disabled={!changeEditorContent} ref={buttonRef}
disabled={disabled}
{...cypressId('toolbar.' + i18nKey)}> {...cypressId('toolbar.' + i18nKey)}>
<UiIcon icon={icon} /> <UiIcon icon={icon} />
{children}
</Button> </Button>
) )
} }

View file

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