diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index 6227666..b2c1e9f 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -48,6 +48,7 @@ import { WrapSelectedText } from '$lib/editor/extensions/wrapSelectedText'; import { calloutGroup, calloutIcon, calloutLabel, CALLOUT_MENU, transformCalloutBlockquotes, serializeCallout } from '$lib/editor/callouts'; import { wrapTextareaSelection } from '$lib/editor/source/selectionPairs'; + import { convertListNode, type MixedListName } from '$lib/editor/mixedLists'; import { relativePath } from '$lib/utils/paths'; import GraphView from './GraphView.svelte'; import TagSuggestInput from './TagSuggestInput.svelte'; @@ -70,6 +71,16 @@ const LARGE_DOC_CHARS = 100_000; let isLargeDoc = $state(false); let editor: Editor | null = null; + const MixedListShortcuts = Extension.create({ + name: 'mixedListShortcuts', + priority: 1000, + addKeyboardShortcuts() { + return { + 'Mod-Shift-8': () => toggleBulletList(), + 'Mod-Shift-9': () => toggleTaskList(), + }; + }, + }); let editorReady = $state(false); let sourceContent = $state(''); let sourceHistory: Array<{ content: string; cursor: number }> = []; @@ -381,9 +392,9 @@ { label: 'Heading 1', aliases: ['h1', 'heading1', 'title'], icon: '', action: () => editor?.chain().focus().toggleHeading({ level: 1 }).run() }, { label: 'Heading 2', aliases: ['h2', 'heading2', 'subtitle'], icon: '', action: () => editor?.chain().focus().toggleHeading({ level: 2 }).run() }, { label: 'Heading 3', aliases: ['h3', 'heading3'], icon: '', action: () => editor?.chain().focus().toggleHeading({ level: 3 }).run() }, - { label: 'Bullet List', aliases: ['ul', 'unordered', 'bullets', 'list'], icon: '', action: () => editor?.chain().focus().toggleBulletList().run() }, + { label: 'Bullet List', aliases: ['ul', 'unordered', 'bullets', 'list'], icon: '', action: toggleBulletList }, { label: 'Numbered List', aliases: ['ol', 'ordered', 'number'], icon: '', action: () => editor?.chain().focus().toggleOrderedList().run() }, - { label: 'Task List', aliases: ['checklist', 'checkbox', 'todo', 'check'], icon: '', action: () => editor?.chain().focus().toggleTaskList().run() }, + { label: 'Task List', aliases: ['checklist', 'checkbox', 'todo', 'check'], icon: '', action: toggleTaskList }, { label: 'Code Block', aliases: ['code', 'codeblock', 'pre', 'snippet'], icon: '', action: () => editor?.chain().focus().toggleCodeBlock().run() }, { label: 'Secret', aliases: ['secret', 'encrypt', 'password', 'private'], icon: '', action: () => openSecretInsert() }, { label: 'Blockquote', aliases: ['quote', 'blockquote', 'citation'], icon: '', action: () => editor?.chain().focus().toggleBlockquote().run() }, @@ -4334,6 +4345,7 @@ element: editorElement, editable: !$readOnly, extensions: [ + MixedListShortcuts, StarterKit.configure({ codeBlock: false }), Placeholder.configure({ includeChildren: true, @@ -5141,8 +5153,52 @@ closeTextContextMenu(); } + function handleMixedListToggle(targetListName: MixedListName): boolean { + if (!editor) return false; + const { selection, schema } = editor.state; + const { $from: fromResolved, $to: toResolved } = selection; + let listDepth = -1; + + for (let depth = fromResolved.depth; depth > 0; depth--) { + const nodeName = fromResolved.node(depth).type.name; + // An ordered list cannot contain task items. Keep it ordered instead of lifting its item. + if (nodeName === 'orderedList') return targetListName === 'taskList'; + if (nodeName === 'bulletList' || nodeName === 'taskList') { + listDepth = depth; + break; + } + } + + if (listDepth < 0 || toResolved.depth < listDepth) return false; + const listNode = fromResolved.node(listDepth); + if (listNode.type.name === targetListName || toResolved.node(listDepth) !== listNode) return false; + + const convertedList = convertListNode(schema, listNode, targetListName); + if (!convertedList) return false; + const listPos = fromResolved.before(listDepth); + const transaction = editor.state.tr.replaceWith( + listPos, + listPos + listNode.nodeSize, + convertedList + ); + transaction.setSelection(TextSelection.create(transaction.doc, selection.from, selection.to)); + editor.view.dispatch(transaction.scrollIntoView()); + editor.view.focus(); + return true; + } + + function toggleBulletList(): boolean { + if (!editor) return false; + return handleMixedListToggle('bulletList') || editor.chain().focus().toggleBulletList().run(); + } + + function toggleTaskList(): boolean { + if (!editor) return false; + return handleMixedListToggle('taskList') || editor.chain().focus().toggleTaskList().run(); + } + function ctxBulletList() { - editor?.chain().focus().toggleBulletList().run(); + toggleBulletList(); closeTextContextMenu(); } @@ -5152,7 +5208,7 @@ } function ctxTaskList() { - editor?.chain().focus().toggleTaskList().run(); + toggleTaskList(); closeTextContextMenu(); } @@ -6551,13 +6607,13 @@
-