@@ -3,9 +3,13 @@ import { flip, offset, shift } from "@floating-ui/react";
33import { ComponentProps , FC , useMemo } from "react" ;
44
55import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js" ;
6+ import { useCreateBlockNote } from "../../hooks/useCreateBlockNote.js" ;
67import { useExtension , useExtensionState } from "../../hooks/useExtension.js" ;
8+ import { useDictionary } from "../../i18n/dictionary.js" ;
79import { FloatingUIOptions } from "../Popovers/FloatingUIOptions.js" ;
810import { PositionPopover } from "../Popovers/PositionPopover.js" ;
11+ import { confirmDiscardUnsavedComment } from "./confirmDiscardUnsavedComment.js" ;
12+ import { defaultCommentEditorSchema } from "./defaultCommentEditorSchema.js" ;
913import { Thread } from "./Thread.js" ;
1014import { useThreads } from "./useThreads.js" ;
1115
@@ -24,8 +28,10 @@ export default function FloatingThreadController(props: {
2428 portalElement ?: HTMLElement | null ;
2529} ) {
2630 const editor = useBlockNoteEditor < any , any , any > ( ) ;
31+ const dict = useDictionary ( ) ;
2732
2833 const comments = useExtension ( CommentsExtension ) ;
34+
2935 const selectedThread = useExtensionState ( CommentsExtension , {
3036 editor,
3137 selector : ( state ) =>
@@ -37,6 +43,24 @@ export default function FloatingThreadController(props: {
3743 : undefined ,
3844 } ) ;
3945
46+ // The editor used to compose a reply. We own it here (rather than in
47+ // `Thread`) so the dismiss handler below can check whether the user has typed
48+ // anything before discarding it. A fresh editor is created for each thread,
49+ // so it always starts empty.
50+ const newCommentEditor = useCreateBlockNote (
51+ {
52+ trailingBlock : false ,
53+ dictionary : {
54+ ...dict ,
55+ placeholders : {
56+ emptyDocument : dict . placeholders . comment_reply ,
57+ } ,
58+ } ,
59+ schema : comments . commentEditorSchema || defaultCommentEditorSchema ,
60+ } ,
61+ [ selectedThread ?. id ] ,
62+ ) ;
63+
4064 const threads = useThreads ( ) ;
4165
4266 const thread = useMemo (
@@ -52,11 +76,24 @@ export default function FloatingThreadController(props: {
5276 // Needed as hooks like `useDismiss` call `onOpenChange` to change the
5377 // open state.
5478 onOpenChange : ( open , _event , reason ) => {
55- if ( reason === "escape-key" ) {
56- editor . focus ( ) ;
57- }
58-
5979 if ( ! open ) {
80+ // If the user has typed an unsaved reply, ask for confirmation
81+ // before discarding it (e.g. when clicking outside the card).
82+ // Otherwise the unsaved reply is lost.
83+ if (
84+ ! confirmDiscardUnsavedComment ( {
85+ hasUnsavedContent : ! newCommentEditor . isEmpty ,
86+ confirmBeforeDiscard : comments . confirmBeforeDiscard ,
87+ message : dict . comments . discard_pending_comment ,
88+ } )
89+ ) {
90+ // Keep the thread open so the user can finish their reply.
91+ return ;
92+ }
93+
94+ if ( reason === "escape-key" ) {
95+ editor . focus ( ) ;
96+ }
6097 comments . selectThread ( undefined ) ;
6198 }
6299 } ,
@@ -75,7 +112,14 @@ export default function FloatingThreadController(props: {
75112 ...props . floatingUIOptions ?. elementProps ,
76113 } ,
77114 } ) ,
78- [ comments , editor , props . floatingUIOptions , selectedThread ] ,
115+ [
116+ comments ,
117+ dict ,
118+ editor ,
119+ newCommentEditor ,
120+ props . floatingUIOptions ,
121+ selectedThread ,
122+ ] ,
79123 ) ;
80124
81125 // nice to have improvements:
@@ -89,7 +133,13 @@ export default function FloatingThreadController(props: {
89133 portalElement = { props . portalElement }
90134 { ...floatingUIOptions }
91135 >
92- { thread && < Component thread = { thread } selected = { true } /> }
136+ { thread && (
137+ < Component
138+ thread = { thread }
139+ selected = { true }
140+ newCommentEditor = { newCommentEditor }
141+ />
142+ ) }
93143 </ PositionPopover >
94144 ) ;
95145}
0 commit comments