diff --git a/packages/core/src/i18n/locales/ar.ts b/packages/core/src/i18n/locales/ar.ts index 37abc3e30b..656b818437 100644 --- a/packages/core/src/i18n/locales/ar.ts +++ b/packages/core/src/i18n/locales/ar.ts @@ -370,6 +370,7 @@ export const ar: Dictionary = { save_button_text: "حفظ", cancel_button_text: "إلغاء", deleted_reference_text: "تم حذف المحتوى الأصلي", + discard_pending_comment: "هل أنت متأكد أنك تريد تجاهل هذا التعليق؟", actions: { add_reaction: "أضف تفاعلًا", resolve: "حل", diff --git a/packages/core/src/i18n/locales/de.ts b/packages/core/src/i18n/locales/de.ts index 40944212b3..69a0e1be2b 100644 --- a/packages/core/src/i18n/locales/de.ts +++ b/packages/core/src/i18n/locales/de.ts @@ -404,6 +404,7 @@ export const de: Dictionary = { save_button_text: "Speichern", cancel_button_text: "Abbrechen", deleted_reference_text: "Originalinhalt gelöscht", + discard_pending_comment: "Möchten Sie diesen Kommentar wirklich verwerfen?", actions: { add_reaction: "Reaktion hinzufügen", resolve: "Lösen", diff --git a/packages/core/src/i18n/locales/en.ts b/packages/core/src/i18n/locales/en.ts index 5a9968eab2..6393d29b04 100644 --- a/packages/core/src/i18n/locales/en.ts +++ b/packages/core/src/i18n/locales/en.ts @@ -385,6 +385,7 @@ export const en = { save_button_text: "Save", cancel_button_text: "Cancel", deleted_reference_text: "Original content deleted", + discard_pending_comment: "Are you sure you want to discard this comment?", actions: { add_reaction: "Add reaction", resolve: "Resolve", diff --git a/packages/core/src/i18n/locales/es.ts b/packages/core/src/i18n/locales/es.ts index 4757d9784f..b1ce7a9c15 100644 --- a/packages/core/src/i18n/locales/es.ts +++ b/packages/core/src/i18n/locales/es.ts @@ -383,6 +383,7 @@ export const es: Dictionary = { save_button_text: "Guardar", cancel_button_text: "Cancelar", deleted_reference_text: "Contenido original eliminado", + discard_pending_comment: "¿Seguro que quieres descartar este comentario?", actions: { add_reaction: "Agregar reacción", resolve: "Resolver", diff --git a/packages/core/src/i18n/locales/fa.ts b/packages/core/src/i18n/locales/fa.ts index c9c67c1fee..89ea0cb3b7 100644 --- a/packages/core/src/i18n/locales/fa.ts +++ b/packages/core/src/i18n/locales/fa.ts @@ -353,6 +353,7 @@ export const fa = { save_button_text: "ذخیره", cancel_button_text: "لغو", deleted_reference_text: "محتوای اصلی حذف شد", + discard_pending_comment: "آیا مطمئن هستید که می‌خواهید این دیدگاه را نادیده بگیرید؟", actions: { add_reaction: "افزودن واکنش", resolve: "حل کردن", diff --git a/packages/core/src/i18n/locales/fr.ts b/packages/core/src/i18n/locales/fr.ts index b05d346409..b977e01265 100644 --- a/packages/core/src/i18n/locales/fr.ts +++ b/packages/core/src/i18n/locales/fr.ts @@ -431,6 +431,7 @@ export const fr: Dictionary = { save_button_text: "Enregistrer", cancel_button_text: "Annuler", deleted_reference_text: "Contenu d'origine supprimé", + discard_pending_comment: "Voulez-vous vraiment abandonner ce commentaire ?", actions: { add_reaction: "Ajouter une réaction", resolve: "Résoudre", diff --git a/packages/core/src/i18n/locales/he.ts b/packages/core/src/i18n/locales/he.ts index 797831460c..da8b7d7dee 100644 --- a/packages/core/src/i18n/locales/he.ts +++ b/packages/core/src/i18n/locales/he.ts @@ -385,6 +385,7 @@ export const he: Dictionary = { save_button_text: "שמור", cancel_button_text: "בטל", deleted_reference_text: "התוכן המקורי נמחק", + discard_pending_comment: "האם אתה בטוח שברצונך לבטל את התגובה הזו?", actions: { add_reaction: "הוסף תגובה", resolve: "סמן כפתור", diff --git a/packages/core/src/i18n/locales/hr.ts b/packages/core/src/i18n/locales/hr.ts index c2081599cc..bcf57017fc 100644 --- a/packages/core/src/i18n/locales/hr.ts +++ b/packages/core/src/i18n/locales/hr.ts @@ -398,6 +398,7 @@ export const hr: Dictionary = { save_button_text: "Spremi", cancel_button_text: "Odustani", deleted_reference_text: "Originalni sadržaj je obrisan", + discard_pending_comment: "Jeste li sigurni da želite odbaciti ovaj komentar?", actions: { add_reaction: "Dodaj reakciju", resolve: "Riješi", diff --git a/packages/core/src/i18n/locales/is.ts b/packages/core/src/i18n/locales/is.ts index fcde471e56..fd308004e2 100644 --- a/packages/core/src/i18n/locales/is.ts +++ b/packages/core/src/i18n/locales/is.ts @@ -398,6 +398,7 @@ export const is: Dictionary = { save_button_text: "Vista", cancel_button_text: "Hætta", deleted_reference_text: "Upprunalegu efni eytt", + discard_pending_comment: "Ertu viss um að þú viljir henda þessari athugasemd?", actions: { add_reaction: "Bæta við viðbrögðum", resolve: "Leysa", diff --git a/packages/core/src/i18n/locales/it.ts b/packages/core/src/i18n/locales/it.ts index 4053581107..d093b9da40 100644 --- a/packages/core/src/i18n/locales/it.ts +++ b/packages/core/src/i18n/locales/it.ts @@ -407,6 +407,7 @@ export const it: Dictionary = { save_button_text: "Salva", cancel_button_text: "Annulla", deleted_reference_text: "Contenuto originale eliminato", + discard_pending_comment: "Vuoi davvero eliminare questo commento?", actions: { add_reaction: "Aggiungi reazione", resolve: "Risolvi", diff --git a/packages/core/src/i18n/locales/ja.ts b/packages/core/src/i18n/locales/ja.ts index ce5ba87a77..3e9e895761 100644 --- a/packages/core/src/i18n/locales/ja.ts +++ b/packages/core/src/i18n/locales/ja.ts @@ -425,6 +425,7 @@ export const ja: Dictionary = { save_button_text: "保存", cancel_button_text: "キャンセル", deleted_reference_text: "元のコンテンツが削除されました", + discard_pending_comment: "このコメントを破棄してもよろしいですか?", actions: { add_reaction: "リアクションを追加", resolve: "解決", diff --git a/packages/core/src/i18n/locales/ko.ts b/packages/core/src/i18n/locales/ko.ts index 53a5def39e..7abf93990f 100644 --- a/packages/core/src/i18n/locales/ko.ts +++ b/packages/core/src/i18n/locales/ko.ts @@ -398,6 +398,7 @@ export const ko: Dictionary = { save_button_text: "저장", cancel_button_text: "취소", deleted_reference_text: "원본 콘텐츠 삭제됨", + discard_pending_comment: "이 댓글을 삭제하시겠습니까?", actions: { add_reaction: "반응 추가", resolve: "해결", diff --git a/packages/core/src/i18n/locales/nl.ts b/packages/core/src/i18n/locales/nl.ts index a1bff3fc6b..96a99da759 100644 --- a/packages/core/src/i18n/locales/nl.ts +++ b/packages/core/src/i18n/locales/nl.ts @@ -385,6 +385,7 @@ export const nl: Dictionary = { save_button_text: "Opslaan", cancel_button_text: "Annuleren", deleted_reference_text: "Originele inhoud verwijderd", + discard_pending_comment: "Weet je zeker dat je deze reactie wilt verwijderen?", actions: { add_reaction: "Reactie toevoegen", resolve: "Oplossen", diff --git a/packages/core/src/i18n/locales/no.ts b/packages/core/src/i18n/locales/no.ts index 5d518d116b..01377a54f5 100644 --- a/packages/core/src/i18n/locales/no.ts +++ b/packages/core/src/i18n/locales/no.ts @@ -402,6 +402,7 @@ export const no: Dictionary = { save_button_text: "Lagre", cancel_button_text: "Avbryt", deleted_reference_text: "Originalt innhold slettet", + discard_pending_comment: "Er du sikker på at du vil forkaste denne kommentaren?", actions: { add_reaction: "Legg til reaksjon", resolve: "Løs", diff --git a/packages/core/src/i18n/locales/pl.ts b/packages/core/src/i18n/locales/pl.ts index 614f64e9f2..7fda96397f 100644 --- a/packages/core/src/i18n/locales/pl.ts +++ b/packages/core/src/i18n/locales/pl.ts @@ -376,6 +376,7 @@ export const pl: Dictionary = { save_button_text: "Zapisz", cancel_button_text: "Anuluj", deleted_reference_text: "Oryginalna treść usunięta", + discard_pending_comment: "Czy na pewno chcesz odrzucić ten komentarz?", actions: { add_reaction: "Dodaj reakcję", resolve: "Rozwiąż", diff --git a/packages/core/src/i18n/locales/pt.ts b/packages/core/src/i18n/locales/pt.ts index c12c94012e..fcf25eb3bd 100644 --- a/packages/core/src/i18n/locales/pt.ts +++ b/packages/core/src/i18n/locales/pt.ts @@ -377,6 +377,7 @@ export const pt: Dictionary = { save_button_text: "Salvar", cancel_button_text: "Cancelar", deleted_reference_text: "Conteúdo original excluído", + discard_pending_comment: "Tem certeza de que deseja descartar este comentário?", actions: { add_reaction: "Adicionar reação", resolve: "Resolver", diff --git a/packages/core/src/i18n/locales/ru.ts b/packages/core/src/i18n/locales/ru.ts index 2982c8f5f6..ae197f4810 100644 --- a/packages/core/src/i18n/locales/ru.ts +++ b/packages/core/src/i18n/locales/ru.ts @@ -428,6 +428,7 @@ export const ru: Dictionary = { save_button_text: "Сохранить", cancel_button_text: "Отменить", deleted_reference_text: "Исходный контент удалён", + discard_pending_comment: "Вы уверены, что хотите отменить этот комментарий?", actions: { add_reaction: "Добавить реакцию", resolve: "Решить", diff --git a/packages/core/src/i18n/locales/sk.ts b/packages/core/src/i18n/locales/sk.ts index c24974f392..e203ccd5fe 100644 --- a/packages/core/src/i18n/locales/sk.ts +++ b/packages/core/src/i18n/locales/sk.ts @@ -383,6 +383,7 @@ export const sk = { save_button_text: "Uložiť", cancel_button_text: "Zrušiť", deleted_reference_text: "Pôvodný obsah odstránený", + discard_pending_comment: "Naozaj chcete zahodiť tento komentár?", actions: { add_reaction: "Pridať reakciu", resolve: "Vyriešiť", diff --git a/packages/core/src/i18n/locales/uk.ts b/packages/core/src/i18n/locales/uk.ts index a5d7d8f9af..e0a743511d 100644 --- a/packages/core/src/i18n/locales/uk.ts +++ b/packages/core/src/i18n/locales/uk.ts @@ -409,6 +409,7 @@ export const uk: Dictionary = { save_button_text: "Зберегти", cancel_button_text: "Скасувати", deleted_reference_text: "Оригінальний вміст видалено", + discard_pending_comment: "Ви впевнені, що хочете відхилити цей коментар?", actions: { add_reaction: "Додати реакцію", resolve: "Вирішити", diff --git a/packages/core/src/i18n/locales/uz.ts b/packages/core/src/i18n/locales/uz.ts index ffc8d04ac6..3227112678 100644 --- a/packages/core/src/i18n/locales/uz.ts +++ b/packages/core/src/i18n/locales/uz.ts @@ -418,6 +418,7 @@ export const uz: Dictionary = { save_button_text: "Saqlash", cancel_button_text: "Bekor qilish", deleted_reference_text: "Asl tarkib o‘chirildi", + discard_pending_comment: "Haqiqatan ham bu izohni bekor qilmoqchimisiz?", actions: { add_reaction: "Reaksiya qo‘shish", resolve: "Hal qilish", diff --git a/packages/core/src/i18n/locales/vi.ts b/packages/core/src/i18n/locales/vi.ts index cbe0e5e628..c4ba2d36b0 100644 --- a/packages/core/src/i18n/locales/vi.ts +++ b/packages/core/src/i18n/locales/vi.ts @@ -384,6 +384,7 @@ export const vi: Dictionary = { save_button_text: "Lưu", cancel_button_text: "Hủy", deleted_reference_text: "Nội dung gốc đã bị xóa", + discard_pending_comment: "Bạn có chắc chắn muốn hủy bình luận này không?", actions: { add_reaction: "Thêm phản ứng", resolve: "Giải quyết", diff --git a/packages/core/src/i18n/locales/zh-tw.ts b/packages/core/src/i18n/locales/zh-tw.ts index b64912255f..08a72e3586 100644 --- a/packages/core/src/i18n/locales/zh-tw.ts +++ b/packages/core/src/i18n/locales/zh-tw.ts @@ -426,6 +426,7 @@ export const zhTW: Dictionary = { save_button_text: "儲存", cancel_button_text: "取消", deleted_reference_text: "原始內容已刪除", + discard_pending_comment: "確定要捨棄此評論嗎?", actions: { add_reaction: "新增回應", resolve: "解決", diff --git a/packages/core/src/i18n/locales/zh.ts b/packages/core/src/i18n/locales/zh.ts index ba5a2fe73b..ed6c7e0bb7 100644 --- a/packages/core/src/i18n/locales/zh.ts +++ b/packages/core/src/i18n/locales/zh.ts @@ -426,6 +426,7 @@ export const zh: Dictionary = { save_button_text: "保存", cancel_button_text: "取消", deleted_reference_text: "原始内容已删除", + discard_pending_comment: "确定要放弃此评论吗?", actions: { add_reaction: "添加反应", resolve: "解决", diff --git a/packages/react/src/components/Comments/FloatingComposer.tsx b/packages/react/src/components/Comments/FloatingComposer.tsx index c3517b27c0..827ddb69b6 100644 --- a/packages/react/src/components/Comments/FloatingComposer.tsx +++ b/packages/react/src/components/Comments/FloatingComposer.tsx @@ -1,4 +1,5 @@ import { + BlockNoteEditor, BlockSchema, DefaultBlockSchema, DefaultInlineContentSchema, @@ -9,19 +10,16 @@ import { StyleSchema, } from "@blocknote/core"; import { CommentsExtension } from "@blocknote/core/comments"; +import { TextSelection } from "@tiptap/pm/state"; import { memo, useCallback } from "react"; - import { Components, useComponentsContext, } from "../../editor/ComponentsContext.js"; -import { useCreateBlockNote } from "../../hooks/useCreateBlockNote.js"; +import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js"; import { useExtension } from "../../hooks/useExtension.js"; import { useDictionary } from "../../i18n/dictionary.js"; import { CommentEditor } from "./CommentEditor.js"; -import { defaultCommentEditorSchema } from "./defaultCommentEditorSchema.js"; -import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js"; -import { TextSelection } from "@tiptap/pm/state"; type FloatingComposerActionsProps = { isFocused: boolean; @@ -59,25 +57,22 @@ export function FloatingComposer< B extends BlockSchema = DefaultBlockSchema, I extends InlineContentSchema = DefaultInlineContentSchema, S extends StyleSchema = DefaultStyleSchema, ->() { +>(props: { + /** + * The (empty) editor used to compose the new comment. Created and owned by + * the `FloatingComposerController`, so it can check for unsaved text before + * the composer is dismissed. + */ + newCommentEditor: BlockNoteEditor; +}) { const editor = useBlockNoteEditor(); + const newCommentEditor = props.newCommentEditor; const comments = useExtension(CommentsExtension); const Components = useComponentsContext()!; const dict = useDictionary(); - const newCommentEditor = useCreateBlockNote({ - trailingBlock: false, - dictionary: { - ...dict, - placeholders: { - emptyDocument: dict.placeholders.new_comment, - }, - }, - schema: comments.commentEditorSchema || defaultCommentEditorSchema, - }); - const onSave = useCallback(async () => { // (later) For REST API, we should implement a loading state and error state await comments.createThread({ diff --git a/packages/react/src/components/Comments/FloatingComposerController.tsx b/packages/react/src/components/Comments/FloatingComposerController.tsx index 0c41402d66..82dae1fe5a 100644 --- a/packages/react/src/components/Comments/FloatingComposerController.tsx +++ b/packages/react/src/components/Comments/FloatingComposerController.tsx @@ -11,10 +11,13 @@ import { flip, offset, shift } from "@floating-ui/react"; import { ComponentProps, FC, useMemo } from "react"; import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js"; +import { useCreateBlockNote } from "../../hooks/useCreateBlockNote.js"; import { useEditorState } from "../../hooks/useEditorState.js"; import { useExtension, useExtensionState } from "../../hooks/useExtension.js"; +import { useDictionary } from "../../i18n/dictionary.js"; import { FloatingUIOptions } from "../Popovers/FloatingUIOptions.js"; import { PositionPopover } from "../Popovers/PositionPopover.js"; +import { defaultCommentEditorSchema } from "./defaultCommentEditorSchema.js"; import { FloatingComposer } from "./FloatingComposer.js"; export default function FloatingComposerController< @@ -32,6 +35,7 @@ export default function FloatingComposerController< portalElement?: HTMLElement | null; }) { const editor = useBlockNoteEditor(); + const dict = useDictionary(); const comments = useExtension(CommentsExtension); @@ -40,6 +44,24 @@ export default function FloatingComposerController< selector: (state) => state.pendingComment, }); + // The editor used to compose a new comment. We own it here (rather than in + // `FloatingComposer`) so that the dismiss handler below can check whether the + // user has typed anything before discarding it. A fresh editor is created for + // each pending comment, so it always starts empty. + const newCommentEditor = useCreateBlockNote( + { + trailingBlock: false, + dictionary: { + ...dict, + placeholders: { + emptyDocument: dict.placeholders.new_comment, + }, + }, + schema: comments.commentEditorSchema || defaultCommentEditorSchema, + }, + [pendingComment], + ); + const position = useEditorState({ editor, selector: ({ editor }) => @@ -60,6 +82,16 @@ export default function FloatingComposerController< // open state. onOpenChange: (open) => { if (!open) { + // If the user has typed a comment that hasn't been saved yet, ask + // for confirmation before discarding it (e.g. when clicking + // outside the composer). Otherwise the unsaved comment is lost. + if ( + !newCommentEditor.isEmpty && + !window.confirm(dict.comments.discard_pending_comment) + ) { + // Keep the composer open so the user can continue editing. + return; + } comments.stopPendingComment(); editor.focus(); } @@ -78,7 +110,14 @@ export default function FloatingComposerController< ...props.floatingUIOptions?.elementProps, }, }), - [comments, editor, pendingComment, props.floatingUIOptions], + [ + comments, + dict, + editor, + newCommentEditor, + pendingComment, + props.floatingUIOptions, + ], ); // nice to have improvements would be: @@ -93,7 +132,7 @@ export default function FloatingComposerController< portalElement={props.portalElement} {...floatingUIOptions} > - + ); }