From a070f563399a44e2781f487a83ee81c013b7c942 Mon Sep 17 00:00:00 2001 From: ZouicheOmar Date: Fri, 18 Apr 2025 17:09:05 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20add=20custom=20callout=20?= =?UTF-8?q?block=20to=20editor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a custom block to the editor, the callout block. --- CHANGELOG.md | 4 + .../__tests__/app-impress/doc-editor.spec.ts | 37 ++++ .../doc-editor/components/BlockNoteEditor.tsx | 3 +- .../components/BlockNoteSuggestionMenu.tsx | 6 +- .../BlockNoteToolBar/BlockNoteToolbar.tsx | 7 +- .../components/custom-blocks/CalloutBlock.tsx | 166 ++++++++++++++++++ .../components/custom-blocks/index.ts | 1 + .../src/features/docs/doc-editor/styles.tsx | 17 ++ .../apps/impress/src/i18n/translations.json | 8 + 9 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/CalloutBlock.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index c0fb0b0d..92de297c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ and this project adheres to ## [Unreleased] +## Added + +✨ Add a custom callout block to the editor #892 + ## [3.2.1] - 2025-05-06 ## Fixed diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts index 09f0cde2..d1a8d856 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts @@ -452,4 +452,41 @@ test.describe('Doc Editor', () => { const svgBuffer = await cs.toBuffer(await download.createReadStream()); expect(svgBuffer.toString()).toContain('Hello svg'); }); + + test('it checks if callout custom block', async ({ page, browserName }) => { + await createDoc(page, 'doc-toolbar', browserName, 1); + + const editor = page.locator('.ProseMirror'); + await editor.click(); + await page.locator('.bn-block-outer').last().fill('/'); + await page.getByText('Add a callout block').click(); + + const calloutBlock = page + .locator('div[data-content-type="callout"]') + .first(); + + await expect(calloutBlock).toBeVisible(); + + await calloutBlock.locator('.inline-content').fill('example text'); + + await expect(page.locator('.bn-block').first()).toHaveAttribute( + 'data-background-color', + 'yellow', + ); + + const emojiButton = calloutBlock.getByRole('button'); + await expect(emojiButton).toHaveText('💡'); + await emojiButton.click(); + await page.locator('button[aria-label="⚠️"]').click(); + await expect(emojiButton).toHaveText('⚠️'); + + await page.locator('.bn-side-menu > button').last().click(); + await page.locator('.mantine-Menu-dropdown > button').last().click(); + await page.locator('.bn-color-picker-dropdown > button').last().click(); + + await expect(page.locator('.bn-block').first()).toHaveAttribute( + 'data-background-color', + 'pink', + ); + }); }); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx index 1f401819..a927578c 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx @@ -27,12 +27,13 @@ import { randomColor } from '../utils'; import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu'; import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar'; -import { DividerBlock } from './custom-blocks'; +import { CalloutBlock, DividerBlock } from './custom-blocks'; export const blockNoteSchema = withPageBreak( BlockNoteSchema.create({ blockSpecs: { ...defaultBlockSpecs, + callout: CalloutBlock, divider: DividerBlock, }, }), diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteSuggestionMenu.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteSuggestionMenu.tsx index 4c90ba5f..3122b1c1 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteSuggestionMenu.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteSuggestionMenu.tsx @@ -11,7 +11,10 @@ import { useTranslation } from 'react-i18next'; import { DocsBlockSchema } from '../types'; -import { getDividerReactSlashMenuItems } from './custom-blocks'; +import { + getCalloutReactSlashMenuItems, + getDividerReactSlashMenuItems, +} from './custom-blocks'; export const BlockNoteSuggestionMenu = () => { const editor = useBlockNoteEditor(); @@ -25,6 +28,7 @@ export const BlockNoteSuggestionMenu = () => { combineByGroup( getDefaultReactSlashMenuItems(editor), getPageBreakReactSlashMenuItems(editor), + getCalloutReactSlashMenuItems(editor, t, basicBlocksName), getDividerReactSlashMenuItems(editor, t, basicBlocksName), ), query, diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteToolBar/BlockNoteToolbar.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteToolBar/BlockNoteToolbar.tsx index a250741f..d59a09ab 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteToolBar/BlockNoteToolbar.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteToolBar/BlockNoteToolbar.tsx @@ -6,9 +6,12 @@ import { useDictionary, } from '@blocknote/react'; import React, { JSX, useCallback, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useConfig } from '@/core/config/api'; +import { getCalloutFormattingToolbarItems } from '../custom-blocks'; + import { AIGroupButton } from './AIButton'; import { FileDownloadButton } from './FileDownloadButton'; import { MarkdownButton } from './MarkdownButton'; @@ -18,11 +21,13 @@ export const BlockNoteToolbar = () => { const dict = useDictionary(); const [confirmOpen, setIsConfirmOpen] = useState(false); const [onConfirm, setOnConfirm] = useState<() => void | Promise>(); + const { t } = useTranslation(); const { data: conf } = useConfig(); const toolbarItems = useMemo(() => { const toolbarItems = getFormattingToolbarItems([ ...blockTypeSelectItems(dict), + getCalloutFormattingToolbarItems(t), ]); const fileDownloadButtonIndex = toolbarItems.findIndex( (item) => @@ -46,7 +51,7 @@ export const BlockNoteToolbar = () => { } return toolbarItems as JSX.Element[]; - }, [dict]); + }, [dict, t]); const formattingToolbar = useCallback(() => { return ( diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/CalloutBlock.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/CalloutBlock.tsx new file mode 100644 index 00000000..bbb0850c --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/CalloutBlock.tsx @@ -0,0 +1,166 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +import { defaultProps, insertOrUpdateBlock } from '@blocknote/core'; +import { BlockTypeSelectItem, createReactBlockSpec } from '@blocknote/react'; +import { TFunction } from 'i18next'; +import React, { useEffect, useState } from 'react'; +import { css } from 'styled-components'; + +import { Box, BoxButton, Icon } from '@/components'; + +import { DocsBlockNoteEditor } from '../../types'; +import { EmojiPicker } from '../EmojiPicker'; + +const calloutCustom = [ + { + name: 'Callout', + id: 'callout', + emojis: [ + 'bulb', + 'point_right', + 'point_up', + 'ok_hand', + 'key', + 'construction', + 'warning', + 'fire', + 'pushpin', + 'scissors', + 'question', + 'no_entry', + 'no_entry_sign', + 'alarm_clock', + 'phone', + 'rotating_light', + 'recycle', + 'white_check_mark', + 'lock', + 'paperclip', + 'book', + 'speaking_head_in_silhouette', + 'arrow_right', + 'loudspeaker', + 'hammer_and_wrench', + 'gear', + ], + }, +]; + +const calloutCategories = [ + 'callout', + 'people', + 'nature', + 'foods', + 'activity', + 'places', + 'flags', + 'objects', + 'symbols', +]; + +export const CalloutBlock = createReactBlockSpec( + { + type: 'callout', + propSchema: { + textAlignment: defaultProps.textAlignment, + backgroundColor: defaultProps.backgroundColor, + emoji: { default: '💡' }, + }, + content: 'inline', + }, + { + render: ({ block, editor, contentRef }) => { + const [openEmojiPicker, setOpenEmojiPicker] = useState(false); + + const toggleEmojiPicker = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setOpenEmojiPicker(!openEmojiPicker); + }; + + const onClickOutside = () => setOpenEmojiPicker(false); + + const onEmojiSelect = ({ native }: { native: string }) => { + editor.updateBlock(block, { props: { emoji: native } }); + setOpenEmojiPicker(false); + }; + + // Temporary: sets a yellow background color to a callout block when added by + // the user, while keeping the colors menu on the drag handler usable for + // this custom block. + useEffect(() => { + if ( + !block.content.length && + block.props.backgroundColor === 'default' + ) { + editor.updateBlock(block, { props: { backgroundColor: 'yellow' } }); + } + }, [block, editor]); + + return ( + + + {block.props.emoji} + + + {openEmojiPicker && ( + + )} + + + ); + }, + }, +); + +export const getCalloutReactSlashMenuItems = ( + editor: DocsBlockNoteEditor, + t: TFunction<'translation', undefined>, + group: string, +) => [ + { + title: t('Callout'), + onItemClick: () => { + insertOrUpdateBlock(editor, { + type: 'callout', + }); + }, + aliases: ['callout', 'encadré', 'hervorhebung', 'benadrukken'], + group, + icon: , + subtext: t('Add a callout block'), + }, +]; + +export const getCalloutFormattingToolbarItems = ( + t: TFunction<'translation', undefined>, +): BlockTypeSelectItem => ({ + name: t('Callout'), + type: 'callout', + icon: () => , + isSelected: (block) => block.type === 'callout', +}); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/index.ts b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/index.ts index 70bc3fae..34a8c459 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-blocks/index.ts @@ -1 +1,2 @@ +export * from './CalloutBlock'; export * from './DividerBlock'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx index 8a96c86b..da02458e 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/styles.tsx @@ -61,6 +61,23 @@ export const cssEditor = (readonly: boolean) => css` height: 38px; } + /** + * Callout, Paragraph and Heading blocks + */ + .bn-block { + border-radius: var(--c--theme--spacings--3xs); + } + + .bn-block-outer { + border-radius: var(--c--theme--spacings--3xs); + } + + .bn-block-content[data-content-type='paragraph'], + .bn-block-content[data-content-type='heading'] { + padding: var(--c--theme--spacings--3xs) var(--c--theme--spacings--3xs); + border-radius: var(--c--theme--spacings--3xs); + } + h1 { font-size: 1.875rem; } diff --git a/src/frontend/apps/impress/src/i18n/translations.json b/src/frontend/apps/impress/src/i18n/translations.json index 72b981bd..fb906dea 100644 --- a/src/frontend/apps/impress/src/i18n/translations.json +++ b/src/frontend/apps/impress/src/i18n/translations.json @@ -53,6 +53,7 @@ "Accessible to anyone": "Für alle zugänglich", "Accessible to authenticated users": "Für authentifizierte Benutzer zugänglich", "Add": "Hinzufügen", + "Add a callout block": "Hebt schrift hervor", "Add a horizontal line": "Waagerechte Linie einfügen", "Address:": "Anschrift:", "Administrator": "Administrator", @@ -67,6 +68,7 @@ "Available soon": "Bald verfügbar", "Banner image": "Bannerbild", "Beautify": "Verschönern", + "Callout": "Hervorhebung", "Can't load this page, please check your internet connection.": "Diese Seite kann nicht geladen werden. Bitte überprüfen Sie Ihre Internetverbindung.", "Cancel": "Abbrechen", "Close the modal": "Pop up schliessen", @@ -284,6 +286,7 @@ "Accessible to anyone": "Accesible para todos", "Accessible to authenticated users": "Accesible a usuarios autenticados", "Add": "Añadir", + "Add a callout block": "Resaltar el texto para que destaque", "Add a horizontal line": "Añadir una línea horizontal", "Address:": "Dirección:", "Administrator": "Administrador", @@ -298,6 +301,7 @@ "Available soon": "Próximamente disponible", "Banner image": "Imagen de portada", "Beautify": "Embellecer", + "Callout": "Destacado", "Can't load this page, please check your internet connection.": "No se puede cargar esta página, por favor compruebe su conexión a Internet.", "Cancel": "Cancelar", "Close the modal": "Cerrar modal", @@ -508,6 +512,7 @@ "Accessible to authenticated users": "Accessible aux utilisateurs authentifiés", "Add": "Ajouter", "Add a horizontal line": "Ajouter une ligne horizontale", + "Add a callout block": "Faites ressortir du texte pour le mettre en évidence", "Address:": "Adresse :", "Administrator": "Administrateur", "All docs": "Tous les documents", @@ -521,6 +526,7 @@ "Available soon": "Disponible prochainement", "Banner image": "Image de la bannière", "Beautify": "Embellir", + "Callout": "Encadré", "Can't load this page, please check your internet connection.": "Impossible de charger cette page, veuillez vérifier votre connexion Internet.", "Cancel": "Annuler", "Close the modal": "Fermer la modale", @@ -914,6 +920,7 @@ "Accessible to anyone": "Toegankelijk voor iedereen", "Accessible to authenticated users": "Toegankelijk voor geauthentiseerde gebruikers", "Add": "Voeg toe", + "Add a callout block": "Laat je tekst opvallen", "Add a horizontal line": "Voeg horizontale lijn toe", "Address:": "Adres:", "Administrator": "Administrator", @@ -928,6 +935,7 @@ "Available soon": "Binnenkort beschikbaar", "Banner image": "Banner afbeelding", "Beautify": "Maak mooier", + "Callout": "Benadrukken", "Can't load this page, please check your internet connection.": "Kan deze pagina niet laden. Controleer je internetverbinding.", "Cancel": "Breek af", "Close the modal": "Sluit het venster",