diff --git a/CHANGELOG.md b/CHANGELOG.md index e4e42d1b5..3a41dc02a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to ### Added - ✨(frontend) Add "Copy link to block" feature #2547 +- ✨(frontend) add word count to doc header toolbox #2549 ### Changed diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts index a95ff4974..a2545999d 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-header.spec.ts @@ -69,6 +69,8 @@ test.describe('Doc Header', () => { }) => { await createDoc(page, 'doc-update', browserName, 1); + await writeInEditor({ page, text: 'Hello Content' }); + const card = page.getByLabel( 'It is the card information about the document.', ); @@ -93,6 +95,7 @@ test.describe('Doc Header', () => { await expect( page.getByRole('menuitem', { name: 'Download' }), ).toBeVisible(); + await expect(page.getByText('Word count: 2 words').first()).toBeVisible(); }); test('it updates the title doc and check the broadcast', async ({ diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/utils.ts b/src/frontend/apps/impress/src/features/docs/doc-editor/utils.ts index e3e5e92b9..804f29e98 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/utils.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/utils.ts @@ -1,5 +1,15 @@ +import { DocsBlockNoteEditor } from './types'; + const HEX_COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; +export const getWordCount = (editor?: DocsBlockNoteEditor): number => { + const doc = editor?._tiptapEditor?.state.doc; + const text = doc ? doc.textBetween(0, doc.content.size, '\n') : ''; + const trimmed = text.trim(); + + return trimmed ? trimmed.split(/\s+/).length : 0; +}; + export const sanitizeColor = (color: string): string => { return HEX_COLOR_REGEX.test(color) ? color : randomColor(); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/DocToolBox.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/components/DocToolBox.tsx index 65d14c3bb..635be7202 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/components/DocToolBox.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/DocToolBox.tsx @@ -5,11 +5,16 @@ import { useTreeContext, } from '@gouvfr-lasuite/ui-components'; import { Present } from '@gouvfr-lasuite/ui-components/icons'; +import { announce } from '@react-aria/live-announcer'; import dynamic from 'next/dynamic'; import { useRouter } from 'next/router'; -import { useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { Text } from '@/components/Text'; +import { useEditorStore } from '@/docs/doc-editor/stores/useEditorStore'; +import { getWordCount } from '@/docs/doc-editor/utils'; +import { printDocumentWithStyles } from '@/docs/doc-export/utils_print'; import { usePresenterStore } from '@/docs/doc-presenter/stores'; import { useAuth } from '@/features/auth'; import ContentCopyIcon from '@/icons/copy.svg'; @@ -25,7 +30,6 @@ import StarIcon from '@/icons/star.svg'; import DeleteIcon from '@/icons/trash.svg'; import { useFocusStore, useResponsiveStore } from '@/stores'; -import { printDocumentWithStyles } from '../../doc-export/utils_print'; import { KEY_DOC, KEY_LIST_DOC, @@ -96,15 +100,34 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { const [isModalHistoryOpen, setIsModalHistoryOpen] = useState(false); const [isModalLeaveOpen, setIsModalLeaveOpen] = useState(false); + const { editor } = useEditorStore(); + const wordCountLabel = useMemo(() => { + if (openDropdown) { + return t('Word count: {{count}} words', { + count: getWordCount(editor), + description: + 'In the document options menu, showing the number of words in the document.', + }); + } + }, [editor, openDropdown, t]); + + useEffect(() => { + if (wordCountLabel) { + announce(wordCountLabel, 'polite'); + } + }, [wordCountLabel]); + const { restoreFocus, addLastFocus } = useFocusStore(); const { isMobile } = useResponsiveStore(); const copyDocLink = useCopyDocLink(doc.id); + const openPresenter = usePresenterStore((state) => state.open); const { mutate: duplicateDoc } = useDuplicateDoc({ onSuccess: (data) => { void router.push(`/docs/${data.id}`); }, }); + const removeFavoriteDoc = useDeleteFavoriteDoc({ listInvalidQueries: [KEY_LIST_DOC, KEY_DOC, KEY_LIST_FAVORITE_DOC], }); @@ -232,6 +255,9 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { }, isHidden: !doc.abilities.destroy, }, + { + type: 'separator', + }, ]; return ( @@ -241,6 +267,9 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { isOpen={openDropdown} shouldCloseOnInteractOutside={() => true} onOpenChange={setOpenDropdown} + bottomMessage={ + wordCountLabel && {wordCountLabel} + } >