@@ -28,12 +28,18 @@ import NoteEditView, { type NoteEditHandle } from "./NoteEditView";
2828import NoteColorPicker from "./NoteColorPicker" ;
2929// 批 3(用户问题9):CM 内容底部留白与阅读/textarea 同源(末行可滚离底边)
3030import { BOTTOM_BREATHER_CSS } from "../utils/contentBreather" ;
31+ // 批 8(REQ-317):编辑态选区右键菜单——插入计划纯函数 + 共享菜单组件
32+ import { planTaskLineInsert , type SelectionActionId , type SelectionNoteAction } from "../utils/noteSelectionMenu" ;
33+ import SelectionActionMenu from "./note-selection/SelectionActionMenu" ;
3134
3235interface Props {
3336 note : Note ;
3437 onCancel : ( ) => void ;
3538 /** 图片点击放大回调(透传 NotesPage 的 ImagePreviewOverlay) */
3639 onImageOpen ?: ( url : string , title ?: string ) => void ;
40+ /** 批 8(REQ-317):选区菜单行动类动作上抛(转问题/模型卡预填——复制/
41+ * 全选/加入行动就地执行,行动类由 NotesPage 层对话框/命令处理) */
42+ onSelectionAction ?: ( action : SelectionNoteAction , text : string ) => void ;
3743}
3844
3945const TOOLBAR_BTN : React . CSSProperties = {
@@ -51,7 +57,7 @@ const editorTheme = EditorView.theme({
5157} ) ;
5258
5359const RichEditorView = forwardRef < NoteEditHandle , Props > ( function RichEditorView (
54- { note, onCancel, onImageOpen } , ref ,
60+ { note, onCancel, onImageOpen, onSelectionAction } , ref ,
5561) {
5662 const [ title , setTitle ] = useState ( note . title ) ;
5763 // CM 非受控持有正文;content state 仅作草稿恢复时的外部 doc 同步源
@@ -62,17 +68,23 @@ const RichEditorView = forwardRef<NoteEditHandle, Props>(function RichEditorView
6268 const [ draftPrompt , setDraftPrompt ] = useState < { title : string ; content : string } | null > ( null ) ;
6369 // v0.16.1:荧光笔色板弹层开合(受控——选色/默认黄/点击外部关闭)
6470 const [ highlightOpen , setHighlightOpen ] = useState ( false ) ;
71+ // 批 8(REQ-317):编辑态选区右键菜单态(CM 选区文本快照——点击动作不再
72+ // 依赖“点击瞬间选区仍在”,快照语义与阅读态 SelectionActionMenu 一致)
73+ const [ selMenu , setSelMenu ] = useState < { x : number ; y : number ; text : string } | null > ( null ) ;
6574
6675 // refs 快照(卸载/定时器闭包取最新值,防 state 闭包过期——同 NoteEditView)
6776 const titleRef = useRef ( title ) ;
6877 const contentRef = useRef ( content ) ;
6978 // 内联回调 ref:CM extensions 构造后不可更新,keymap/plugin 闭包必须经 ref 取最新
7079 const onCancelRef = useRef ( onCancel ) ;
7180 const onImageOpenRef = useRef ( onImageOpen ) ;
81+ // 批 8:onSelectionAction 同 ref 模式(CM extension 与菜单动作闭包取最新)
82+ const onSelectionActionRef = useRef ( onSelectionAction ) ;
7283 useEffect ( ( ) => { titleRef . current = title ; } , [ title ] ) ;
7384 useEffect ( ( ) => { contentRef . current = content ; } , [ content ] ) ;
7485 useEffect ( ( ) => { onCancelRef . current = onCancel ; } , [ onCancel ] ) ;
7586 useEffect ( ( ) => { onImageOpenRef . current = onImageOpen ; } , [ onImageOpen ] ) ;
87+ useEffect ( ( ) => { onSelectionActionRef . current = onSelectionAction ; } , [ onSelectionAction ] ) ;
7688
7789 // ── v0.15 剪贴板图片:粘贴即落盘(import_note_image_b64)+ 插入相对引用 ──
7890 const onInsertImage = useCallback ( ( rel : string ) => {
@@ -125,6 +137,40 @@ const RichEditorView = forwardRef<NoteEditHandle, Props>(function RichEditorView
125137 autosave . scheduleDraftWrite ( ) ;
126138 } , [ autosave . markDirty , autosave . scheduleDraftWrite ] ) ;
127139
140+ // 批 8(REQ-317):编辑态选区动作就地执行(复制已在菜单内用快照文本完成)
141+ const runSelAction = ( action : Exclude < SelectionActionId , "copy" > ) => {
142+ const view = viewRef . current ;
143+ const snapshotText = selMenu ?. text ?? "" ;
144+ if ( action === "selectAll" ) {
145+ // 全选=CM 既有 select-all 语义(basicSetup Mod-a 同命令路径:主选区覆盖
146+ // 全 doc)——不引 @codemirror/commands(无直接依赖),dispatch 等价
147+ if ( ! view ) return ;
148+ view . dispatch ( {
149+ selection : { anchor : 0 , head : view . state . doc . length } ,
150+ scrollIntoView : true ,
151+ } ) ;
152+ view . focus ( ) ;
153+ return ;
154+ }
155+ if ( action === "addTask" ) {
156+ // 加入行动:以「选区结束处所在行」为锚插入 `- [ ] <快照文本>` 独立任务行;
157+ // dispatch → 既有 onChange → 自动保存/草稿/任务索引重扫通道(不 bypass)
158+ if ( ! view || ! snapshotText ) return ;
159+ const plan = planTaskLineInsert ( view . state . doc . toString ( ) , view . state . selection . main . to , snapshotText ) ;
160+ if ( ! plan ) return ;
161+ view . dispatch ( {
162+ changes : { from : plan . from , insert : plan . insert } ,
163+ selection : { anchor : plan . from + plan . insert . length } ,
164+ userEvent : "input.addtask" ,
165+ } ) ;
166+ view . focus ( ) ;
167+ return ;
168+ }
169+ if ( snapshotText && onSelectionActionRef . current ) {
170+ onSelectionActionRef . current ( action , snapshotText ) ;
171+ }
172+ } ;
173+
128174 const extensions = useMemo ( ( ) => [
129175 basicSetup ,
130176 markdown ( ) ,
@@ -136,6 +182,23 @@ const RichEditorView = forwardRef<NoteEditHandle, Props>(function RichEditorView
136182 ] ) ,
137183 // v0.15:粘贴图片 → 拦截 + 落盘 + 插入引用(文字粘贴走默认行为)
138184 EditorView . domEventHandlers ( { paste : ( e ) => handleImagePaste ( e ) } ) ,
185+ // 批 8(REQ-317):编辑态选区右键菜单——CM 主选区非空(sliceDoc 非空串)
186+ // 才弹菜单(右键落点通常已由 CM mousedown 语义定选区);空选区维持现状
187+ // (原生菜单已被全局抑制→静默,与阅读态“空选区不弹”取最小一致)
188+ EditorView . domEventHandlers ( {
189+ // domEventHandlers 回调签名为 (event, view)——事件在前、视图在后(与
190+ // keymap 的 (view, event) 相反),paste 处理同型单参可用
191+ contextmenu : ( e , view ) => {
192+ const main = view . state . selection . main ;
193+ if ( main . empty ) return false ;
194+ const t = view . state . sliceDoc ( main . from , main . to ) ;
195+ if ( ! t . trim ( ) ) return false ;
196+ e . preventDefault ( ) ;
197+ e . stopPropagation ( ) ; // 自绘菜单范式:到 window 前截停(原生兜底同在)
198+ setSelMenu ( { x : e . clientX , y : e . clientY , text : t } ) ;
199+ return true ;
200+ } ,
201+ } ) ,
139202 imageDecorationPlugin ( { noteId : note . id , onOpen : ( url , t ) => onImageOpenRef . current ?.( url , t ) } ) ,
140203 editorTheme ,
141204 ] , [ note . id , autosave . saveVersioned , autosave . flushLatestRef , handleImagePaste ] ) ;
@@ -316,6 +379,19 @@ const RichEditorView = forwardRef<NoteEditHandle, Props>(function RichEditorView
316379 < div ref = { containerRef } style = { { flex : 1 , overflow : "hidden" } } />
317380
318381 { status && < p style = { { padding : "4px 16px" , fontSize : 12 , color : "#047857" } } > { status } </ p > }
382+
383+ { /* 批 8(REQ-317):编辑态选区右键菜单(复制就地、全选=CM select-all、
384+ 加入行动=选区内容插任务行走既有保存通道、行动类上抛 NotesPage) */ }
385+ { selMenu && (
386+ < SelectionActionMenu
387+ mode = "editing"
388+ x = { selMenu . x }
389+ y = { selMenu . y }
390+ text = { selMenu . text }
391+ onClose = { ( ) => setSelMenu ( null ) }
392+ onAction = { runSelAction }
393+ />
394+ ) }
319395 </ div >
320396 ) ;
321397} ) ;
0 commit comments