Skip to content

Commit 7c75908

Browse files
feat(notes): rework note editor with context menu and editable tables (#853)
* feat(notes): add formatting context menu to note editor Right-click menu with Format, Paragraph and Insert submenus reusing existing CodeMirror markdown commands. Disabled in preview mode. * feat(notes): localize table add column/row controls * refactor(notes): extract markdown table parser into tableParser module * feat(notes): inline-edit tables with add column/row controls Render markdown tables as an interactive block widget in live preview: cells are edited in place (committed on blur/Enter/Tab), and hover gutters add a column to the right or a row below. Clamp the table range to the actual rows so trailing text is never hidden or overwritten, and add click strips to place the cursor before/after the table. * fix(notes): restore list menu commands * fix(notes): improve table exit click area * fix(notes): improve table keyboard navigation * fix(notes): stabilize table keyboard entry * fix(notes): preserve table boundary while typing * feat(notes): add table row and column drag reorder * fix(notes): refine table drag handles * fix(notes): polish table drag handle movement * fix(notes): select table cell text on focus * fix(notes): size table columns by content * fix(notes): remove table add button tooltips * fix(notes): improve table tab navigation * feat(notes): rework inline formatting on syntax tree * feat(notes): edit table cells with nested editors * feat(notes): add table cell menu commands and tabular paste * fix(notes): paste excel data as table * chore: update gitignore * feat(notes): scroll table and keep drag overlay aligned on column drag * fix(notes): keep editor layout and table separation on table paste * fix(notes): allow drag selection over table dead zones * feat(notes): keep markup rendered while mouse selection is dragging * fix(notes): scroll table edge cells fully into view * fix(notes): adjust editor context menu spacing * fix(notes): apply review fixes to table editing and reveal selection * refactor(notes): reduce table navigation and reveal selection overhead
1 parent 5a88cf8 commit 7c75908

27 files changed

Lines changed: 5156 additions & 458 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ docs/superpowers
1717
docs/releases
1818
docs/website/.vitepress/cache
1919
docs/website/.vitepress/dist
20+
docs/strategy

src/main/i18n/locales/en_US/ui.json

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,52 @@
357357
"symbols": "Symbols",
358358
"selectedMultiple": "{{count}} Notes Selected",
359359
"noSelected": "No Note Selected",
360+
"editor": {
361+
"menu": {
362+
"format": {
363+
"title": "Format",
364+
"bold": "Bold",
365+
"italic": "Italic",
366+
"strikethrough": "Strikethrough",
367+
"highlight": "Highlight",
368+
"code": "Code",
369+
"link": "Link",
370+
"clearFormatting": "Clear formatting"
371+
},
372+
"paragraph": {
373+
"title": "Paragraph",
374+
"bulletList": "Bullet list",
375+
"numberedList": "Numbered list",
376+
"taskList": "Task list",
377+
"heading": "Heading {{level}}",
378+
"body": "Body",
379+
"quote": "Quote"
380+
},
381+
"insert": {
382+
"title": "Insert",
383+
"table": "Table",
384+
"callout": "Callout",
385+
"horizontalRule": "Horizontal rule",
386+
"codeBlock": "Code block"
387+
},
388+
"table": {
389+
"title": "Table",
390+
"insertRowAbove": "Insert row above",
391+
"insertRowBelow": "Insert row below",
392+
"deleteRow": "Delete row",
393+
"insertColumnLeft": "Insert column left",
394+
"insertColumnRight": "Insert column right",
395+
"deleteColumn": "Delete column",
396+
"alignLeft": "Align left",
397+
"alignCenter": "Align center",
398+
"alignRight": "Align right"
399+
}
400+
},
401+
"table": {
402+
"addColumn": "Add column to the right",
403+
"addRow": "Add row below"
404+
}
405+
},
360406
"tasks": {
361407
"title": "Tasks",
362408
"today": "Today",

src/main/i18n/locales/ru_RU/ui.json

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,52 @@
358358
"symbols": "Символы",
359359
"selectedMultiple": "Выбрано заметок: {{count}}",
360360
"noSelected": "Нет выбранных заметок",
361+
"editor": {
362+
"menu": {
363+
"format": {
364+
"title": "Форматирование",
365+
"bold": "Полужирный",
366+
"italic": "Курсив",
367+
"strikethrough": "Зачёркнутый",
368+
"highlight": "Выделение",
369+
"code": "Код",
370+
"link": "Ссылка",
371+
"clearFormatting": "Очистить форматирование"
372+
},
373+
"paragraph": {
374+
"title": "Абзац",
375+
"bulletList": "Маркированный список",
376+
"numberedList": "Нумерованный список",
377+
"taskList": "Список задач",
378+
"heading": "Заголовок {{level}}",
379+
"body": "Обычный текст",
380+
"quote": "Цитата"
381+
},
382+
"insert": {
383+
"title": "Вставить",
384+
"table": "Таблица",
385+
"callout": "Выноска",
386+
"horizontalRule": "Горизонтальная линия",
387+
"codeBlock": "Блок кода"
388+
},
389+
"table": {
390+
"title": "Таблица",
391+
"insertRowAbove": "Вставить строку выше",
392+
"insertRowBelow": "Вставить строку ниже",
393+
"deleteRow": "Удалить строку",
394+
"insertColumnLeft": "Вставить столбец слева",
395+
"insertColumnRight": "Вставить столбец справа",
396+
"deleteColumn": "Удалить столбец",
397+
"alignLeft": "По левому краю",
398+
"alignCenter": "По центру",
399+
"alignRight": "По правому краю"
400+
}
401+
},
402+
"table": {
403+
"addColumn": "Добавить столбец справа",
404+
"addRow": "Добавить строку снизу"
405+
}
406+
},
361407
"tasks": {
362408
"title": "Задачи",
363409
"today": "Сегодня",

src/renderer/components/notes/NotesEditor.vue

Lines changed: 184 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
<script setup lang="ts">
22
import type { NotesEditorMode } from '@/composables/spaces/notes/useNotesApp'
3+
import type { EditorMenuCommand } from './NotesEditorContextMenu.vue'
34
import { createCodeHighlight } from '@/components/cm-extensions/codeHighlight'
45
import { editorScrollbarTheme } from '@/components/cm-extensions/scrollbarTheme'
6+
import * as ContextMenu from '@/components/ui/shadcn/context-menu'
57
import {
68
applyPendingNavigationUIStateForNote,
79
registerNavigationNoteUIState,
@@ -21,6 +23,26 @@ import {
2123
placeholder,
2224
} from '@codemirror/view'
2325
import { GFM, type MarkdownConfig } from '@lezer/markdown'
26+
import {
27+
clearInlineFormatting,
28+
getHeadingLevel,
29+
insertCallout,
30+
insertCodeBlock,
31+
insertHorizontalRule,
32+
insertLink,
33+
insertTable,
34+
setBody,
35+
setHeading,
36+
toggleBold,
37+
toggleBulletList,
38+
toggleHighlight,
39+
toggleInlineCode,
40+
toggleItalic,
41+
toggleOrderedList,
42+
toggleQuote,
43+
toggleStrikethrough,
44+
toggleTaskList,
45+
} from './cm-extensions/editorCommands'
2446
import { editorFocusExtension } from './cm-extensions/editorFocus'
2547
import { createExternalLinksNavigation } from './cm-extensions/externalLinks'
2648
import { createHideMarkup } from './cm-extensions/hideMarkup'
@@ -37,8 +59,17 @@ import { Highlight } from './cm-extensions/markdownHighlight'
3759
import { markdownShortcuts } from './cm-extensions/markdownShortcuts'
3860
import { createMermaidBlocks } from './cm-extensions/mermaidBlocks'
3961
import { moveSelectionToAdjacentMermaidSource } from './cm-extensions/mermaidNavigation'
40-
import { createTableBlocks } from './cm-extensions/tableBlocks'
41-
import { moveSelectionToAdjacentTableSource } from './cm-extensions/tableNavigation'
62+
import { revealSelectionFreeze } from './cm-extensions/revealSelection'
63+
import {
64+
createTableBlocks,
65+
getActiveTableCellContext,
66+
getActiveTableCellEditor,
67+
requestTableCellFocus,
68+
runActiveTableCellCommand,
69+
type TableCellMenuCommand,
70+
type TableCellMenuContext,
71+
} from './cm-extensions/tableBlocks'
72+
import { moveSelectionToAdjacentTableCell } from './cm-extensions/tableNavigation'
4273
import { createNotesEditTheme } from './theme'
4374
4475
interface Props {
@@ -123,14 +154,14 @@ const navigationKeymap: KeyBinding[] = [
123154
key: 'ArrowDown',
124155
run: view =>
125156
moveSelectionToAdjacentMermaidSource(view, 'down')
126-
|| moveSelectionToAdjacentTableSource(view, 'down')
157+
|| moveSelectionToAdjacentTableCell(view, 'down')
127158
|| moveSelectionToAdjacentImageSource(view, 'down'),
128159
},
129160
{
130161
key: 'ArrowUp',
131162
run: view =>
132163
moveSelectionToAdjacentMermaidSource(view, 'up')
133-
|| moveSelectionToAdjacentTableSource(view, 'up')
164+
|| moveSelectionToAdjacentTableCell(view, 'up')
134165
|| moveSelectionToAdjacentImageSource(view, 'up'),
135166
},
136167
]
@@ -148,6 +179,9 @@ const presentationTheme = EditorView.theme({
148179
lineHeight: '1.58',
149180
maxWidth: '980px',
150181
margin: '0 auto',
182+
// Широкий блок-виджет не должен распирать контент и давать редактору
183+
// горизонтальную прокрутку (см. minWidth в createNotesEditThemeStyles).
184+
minWidth: '0',
151185
},
152186
'.cm-gutters': {
153187
display: 'none',
@@ -211,14 +245,16 @@ function createEditorState(doc: string): EditorState {
211245
212246
if (!raw) {
213247
extensions.push(
248+
revealSelectionFreeze,
214249
createMermaidBlocks({
215250
enabled: true,
216251
isDark: isDark.value,
217252
showSourceWhenSelectionInside: editable,
218253
}),
219254
createTableBlocks({
220255
enabled: true,
221-
showSourceWhenSelectionInside: editable,
256+
editable,
257+
isDark: isDark.value,
222258
}),
223259
createImageBlocks({
224260
enabled: true,
@@ -365,6 +401,130 @@ watch(
365401
noteId => syncNavigationNoteUIStateRegistration(noteId),
366402
)
367403
404+
// Контекст контекстного меню форматирования, снимается на правый клик.
405+
const menuHasSelection = ref(false)
406+
const menuHeadingLevel = ref(0)
407+
const menuTable = shallowRef<TableCellMenuContext | null>(null)
408+
let pendingInsertedTableStart: number | null = null
409+
410+
function onEditorContextMenu() {
411+
if (!view)
412+
return
413+
414+
// Правый клик в ячейке таблицы: меню работает с вложенным редактором.
415+
const cellEditor = getActiveTableCellEditor()
416+
const target = cellEditor ?? view
417+
418+
menuHasSelection.value = !target.state.selection.main.empty
419+
menuHeadingLevel.value = cellEditor ? 0 : getHeadingLevel(view)
420+
menuTable.value = getActiveTableCellContext()
421+
}
422+
423+
function onContextMenuCloseAutoFocus(event: Event) {
424+
// Фокус должен вернуться во вложенный редактор ячейки (а не на контейнер):
425+
// после команды он уже там, а при закрытии меню без команды возвращаем сами.
426+
const cellEditor = getActiveTableCellEditor()
427+
if (cellEditor) {
428+
event.preventDefault()
429+
cellEditor.focus()
430+
return
431+
}
432+
433+
if (pendingInsertedTableStart === null || !view)
434+
return
435+
436+
event.preventDefault()
437+
requestTableCellFocus(view, {
438+
tableFrom: pendingInsertedTableStart,
439+
selector: 'thead th:first-child',
440+
mode: 'select',
441+
})
442+
pendingInsertedTableStart = null
443+
}
444+
445+
function onMenuCommand(command: EditorMenuCommand) {
446+
if (!view)
447+
return
448+
449+
if (command.startsWith('table-')) {
450+
runActiveTableCellCommand(command as TableCellMenuCommand)
451+
return
452+
}
453+
454+
// Внутри ячейки таблицы inline-команды идут во вложенный редактор, а
455+
// блочные (заголовки, списки, вставка) не имеют смысла — игнорируем.
456+
const cellEditor = getActiveTableCellEditor()
457+
const inlineTarget = cellEditor ?? view
458+
const isInlineCommand = [
459+
'bold',
460+
'italic',
461+
'strikethrough',
462+
'highlight',
463+
'code',
464+
'link',
465+
'clear-formatting',
466+
].includes(command)
467+
468+
if (cellEditor && !isInlineCommand)
469+
return
470+
471+
if (command.startsWith('heading-')) {
472+
setHeading(view, Number(command.slice('heading-'.length)))
473+
return
474+
}
475+
476+
switch (command) {
477+
case 'bold':
478+
toggleBold(inlineTarget)
479+
break
480+
case 'italic':
481+
toggleItalic(inlineTarget)
482+
break
483+
case 'strikethrough':
484+
toggleStrikethrough(inlineTarget)
485+
break
486+
case 'highlight':
487+
toggleHighlight(inlineTarget)
488+
break
489+
case 'code':
490+
toggleInlineCode(inlineTarget)
491+
break
492+
case 'link':
493+
insertLink(inlineTarget)
494+
break
495+
case 'clear-formatting':
496+
clearInlineFormatting(inlineTarget)
497+
break
498+
case 'bullet-list':
499+
toggleBulletList(view)
500+
break
501+
case 'numbered-list':
502+
toggleOrderedList(view)
503+
break
504+
case 'task-list':
505+
toggleTaskList(view)
506+
break
507+
case 'body':
508+
setBody(view)
509+
break
510+
case 'quote':
511+
toggleQuote(view)
512+
break
513+
case 'table':
514+
pendingInsertedTableStart = insertTable(view)
515+
break
516+
case 'callout':
517+
insertCallout(view)
518+
break
519+
case 'horizontal-rule':
520+
insertHorizontalRule(view)
521+
break
522+
case 'code-block':
523+
insertCodeBlock(view)
524+
break
525+
}
526+
}
527+
368528
onMounted(() => {
369529
if (!editorContainer.value)
370530
return
@@ -391,10 +551,25 @@ onUnmounted(() => {
391551

392552
<template>
393553
<div class="h-full overflow-hidden">
394-
<div
395-
ref="editorContainer"
396-
class="h-full overflow-hidden"
397-
/>
554+
<ContextMenu.ContextMenu>
555+
<ContextMenu.ContextMenuTrigger
556+
as-child
557+
:disabled="isPreviewMode"
558+
>
559+
<div
560+
ref="editorContainer"
561+
class="h-full overflow-hidden"
562+
@contextmenu="onEditorContextMenu"
563+
/>
564+
</ContextMenu.ContextMenuTrigger>
565+
<NotesEditorContextMenu
566+
:has-selection="menuHasSelection"
567+
:heading-level="menuHeadingLevel"
568+
:table="menuTable"
569+
@close-auto-focus="onContextMenuCloseAutoFocus"
570+
@command="onMenuCommand"
571+
/>
572+
</ContextMenu.ContextMenu>
398573
<NotesInternalLinksOverlay />
399574
</div>
400575
</template>

0 commit comments

Comments
 (0)