diff --git a/packages/ui/package.json b/packages/ui/package.json index b20b31ea..bbf6d061 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -37,33 +37,33 @@ "vaul-svelte": "1.0.0-next.7" }, "dependencies": { - "@aarkue/tiptap-math-extension": "^1.4.0", "@floating-ui/dom": "^1.7.4", - "@tiptap/core": "^3.7.1", - "@tiptap/extension-bubble-menu": "^3.7.1", - "@tiptap/extension-code-block-lowlight": "^3.7.1", - "@tiptap/extension-highlight": "^3.7.1", - "@tiptap/extension-image": "^3.7.1", - "@tiptap/extension-list": "^3.7.1", - "@tiptap/extension-subscript": "^3.7.1", - "@tiptap/extension-superscript": "^3.7.1", - "@tiptap/extension-table": "^3.7.1", - "@tiptap/extension-text-align": "^3.7.1", - "@tiptap/extension-text-style": "^3.7.1", - "@tiptap/extension-typography": "^3.7.1", - "@tiptap/extensions": "^3.7.1", - "@tiptap/pm": "^3.7.1", - "@tiptap/starter-kit": "^3.7.1", - "@tiptap/suggestion": "^3.7.1", + "@tiptap/core": "^3.10.7", + "@tiptap/extension-bubble-menu": "^3.10.7", + "@tiptap/extension-code-block-lowlight": "^3.10.7", + "@tiptap/extension-highlight": "^3.10.7", + "@tiptap/extension-image": "^3.10.7", + "@tiptap/extension-list": "^3.10.7", + "@tiptap/extension-mathematics": "^3.10.7", + "@tiptap/extension-subscript": "^3.10.7", + "@tiptap/extension-superscript": "^3.10.7", + "@tiptap/extension-table": "^3.10.7", + "@tiptap/extension-text-align": "^3.10.7", + "@tiptap/extension-text-style": "^3.10.7", + "@tiptap/extension-typography": "^3.10.7", + "@tiptap/extensions": "^3.10.7", + "@tiptap/markdown": "^3.10.7", + "@tiptap/pm": "^3.10.7", + "@tiptap/starter-kit": "^3.10.7", + "@tiptap/suggestion": "^3.10.7", "d3-array": "^3.2.4", "d3-scale": "^4.0.2", "dompurify": "^3.3.0", - "katex": "^0.16.22", + "katex": "^0.16.25", "lowlight": "^3.3.0", - "svelte-tiptap": "^3.0.0", + "svelte-tiptap": "^2.2.0", "tippy.js": "^6.3.7", - "tiptap-extension-auto-joiner": "^0.1.3", - "tiptap-markdown": "^0.9.0" + "tiptap-extension-auto-joiner": "^0.1.3" }, "peerDependencies": { "bits-ui": "^2.14.4" diff --git a/packages/ui/src/custom/editor/commands/toolbar-commands.ts b/packages/ui/src/custom/editor/commands/toolbar-commands.ts index 3f3d9192..8d3191f8 100644 --- a/packages/ui/src/custom/editor/commands/toolbar-commands.ts +++ b/packages/ui/src/custom/editor/commands/toolbar-commands.ts @@ -26,6 +26,10 @@ import Table from '@lucide/svelte/icons/table'; import Underline from '@lucide/svelte/icons/underline'; import Undo from '@lucide/svelte/icons/undo-2'; import Video from '@lucide/svelte/icons/video'; +import Radical from '@lucide/svelte/icons/radical'; +import SquareRadical from '@lucide/svelte/icons/square-radical'; +import { isTextSelection } from '@tiptap/core'; +import Pilcrow from '@lucide/svelte/icons/pilcrow'; import { isMac } from '../utils'; const commands: Record = { @@ -64,6 +68,9 @@ const commands: Record = { onClick: (editor) => { editor.chain().focus().toggleHeading({ level: 1 }).run(); }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).setHeading({ level: 1 }).run(); + }, clickable: (editor) => { return editor.can().toggleHeading({ level: 1 }); }, @@ -79,6 +86,9 @@ const commands: Record = { onClick: (editor) => { editor.chain().focus().toggleHeading({ level: 2 }).run(); }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).setHeading({ level: 2 }).run(); + }, clickable: (editor) => { return editor.can().toggleHeading({ level: 2 }); }, @@ -94,6 +104,9 @@ const commands: Record = { onClick: (editor) => { editor.chain().focus().toggleHeading({ level: 3 }).run(); }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).setHeading({ level: 3 }).run(); + }, clickable: (editor) => { return editor.can().toggleHeading({ level: 3 }); }, @@ -109,6 +122,9 @@ const commands: Record = { onClick: (editor) => { editor.chain().focus().toggleHeading({ level: 4 }).run(); }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).setHeading({ level: 4 }).run(); + }, clickable: (editor) => { return editor.can().toggleHeading({ level: 4 }); }, @@ -136,6 +152,24 @@ const commands: Record = { return editor.isActive('link'); } }, + { + icon: Pilcrow, + name: 'paragraph', + tooltip: 'Paragraph', + shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}0`, + onClick: (editor) => { + editor.chain().focus().setParagraph().run(); + }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).setParagraph().run(); + }, + clickable: (editor) => { + return editor.can().setParagraph(); + }, + isActive: (editor) => { + return editor.isActive('paragraph'); + } + }, { icon: Bold, name: 'bold', @@ -144,6 +178,9 @@ const commands: Record = { onClick: (editor) => { editor.chain().focus().toggleBold().run(); }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).setMark('bold').run(); + }, clickable: (editor) => { return editor.can().toggleBold(); }, @@ -159,6 +196,9 @@ const commands: Record = { onClick: (editor) => { editor.chain().focus().toggleItalic().run(); }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).setMark('italic').run(); + }, clickable: (editor) => { return editor.can().toggleItalic(); }, @@ -174,6 +214,9 @@ const commands: Record = { onClick: (editor) => { editor.chain().focus().toggleUnderline().run(); }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).setMark('underline').run(); + }, clickable: (editor) => { return editor.can().toggleUnderline(); }, @@ -189,6 +232,9 @@ const commands: Record = { onClick: (editor) => { editor.chain().focus().toggleStrike().run(); }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).setMark('strike').run(); + }, clickable: (editor) => { return editor.can().toggleStrike(); }, @@ -204,6 +250,9 @@ const commands: Record = { onClick: (editor) => { editor.chain().focus().toggleBlockquote().run(); }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).toggleBlockquote().run(); + }, clickable: (editor) => { return editor.can().toggleBlockquote(); }, @@ -219,6 +268,9 @@ const commands: Record = { onClick: (editor) => { editor.chain().focus().toggleCode().run(); }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).toggleCodeBlock().run(); + }, clickable: (editor) => { return editor.can().toggleCode(); }, @@ -320,6 +372,9 @@ const commands: Record = { onClick: (editor) => { editor.chain().focus().toggleBulletList().run(); }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).toggleBulletList().run(); + }, isActive: (editor) => editor.isActive('bulletList') }, { @@ -330,7 +385,15 @@ const commands: Record = { onClick: (editor) => { editor.chain().focus().toggleOrderedList().run(); }, - isActive: (editor) => editor.isActive('orderedList') + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).toggleOrderedList().run(); + }, + clickable: (editor) => { + return editor.can().toggleOrderedList(); + }, + isActive: (editor) => { + return editor.isActive('orderedList'); + } }, { icon: ListChecks, @@ -340,7 +403,15 @@ const commands: Record = { onClick: (editor) => { editor.chain().focus().toggleTaskList().run(); }, - isActive: (editor) => editor.isActive('taskList') + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).toggleTaskList().run(); + }, + clickable: (editor) => { + return editor.can().toggleTaskList(); + }, + isActive: (editor) => { + return editor.isActive('taskList'); + } } ], media: [ @@ -398,6 +469,34 @@ const commands: Record = { }, isActive: (editor) => editor.isActive('table') } + ], + math: [ + { + icon: Radical, + name: 'mathematics', + tooltip: 'Inline Expression', + onClick: (editor) => { + let latex = 'a^2 + b^2 = c^2'; + const chain = editor.chain().focus(); + if (isTextSelection(editor.view.state.selection)) { + const { from, to } = editor.view.state.selection; + latex = editor.view.state.doc.textBetween(from, to); + chain.deleteRange({ from, to }); + } + chain.insertInlineMath({ latex }).run(); + }, + isActive: (editor) => editor.isActive('inlineMath') + }, + { + icon: SquareRadical, + name: 'mathematics', + tooltip: 'Block Expression', + onClick: (editor) => { + const latex = 'a^2 + b^2 = c^2'; + editor.chain().focus().insertBlockMath({ latex }).run(); + }, + isActive: (editor) => editor.isActive('blockMath') + } ] }; diff --git a/packages/ui/src/custom/editor/commands/types.ts b/packages/ui/src/custom/editor/commands/types.ts index b677b7da..d22decd2 100644 --- a/packages/ui/src/custom/editor/commands/types.ts +++ b/packages/ui/src/custom/editor/commands/types.ts @@ -7,6 +7,7 @@ export interface EdraToolBarCommands { tooltip?: string; shortCut?: string; onClick?: (editor: Editor) => void; + turnInto?: (editor: Editor, pos: number) => void; isActive?: (editor: Editor) => boolean; clickable?: (editor: Editor) => boolean; } diff --git a/packages/ui/src/custom/editor/editor.css b/packages/ui/src/custom/editor/editor.css index 8904efae..e0ede8f6 100644 --- a/packages/ui/src/custom/editor/editor.css +++ b/packages/ui/src/custom/editor/editor.css @@ -5,11 +5,12 @@ } /* For Placeholder */ -.tiptap .is-empty::before { + +.tiptap .is-empty:not(blockquote.is-empty)::before { pointer-events: none; float: left; height: 0; - color: var(--border-color-hover); + color: var(--color-muted-foreground); content: attr(data-placeholder); } @@ -66,12 +67,12 @@ } .tiptap blockquote::before { - content: ''; + content: ' '; position: absolute; left: 0; top: 0; bottom: 0; - width: 0.25rem; + width: 0.4rem; background-color: var(--blockquote-border); border-radius: 0.5rem; } @@ -208,8 +209,8 @@ input[type='checkbox'] { align-items: center; cursor: grab; opacity: 100; - transition-property: opacity; - transition-duration: 200ms; + transition-property: all; + transition-duration: 300ms; transition-timing-function: cubic-bezier(0.4, 0, 1, 1); color: var(--border-color-hover); } @@ -452,3 +453,41 @@ input[type='checkbox'] { .tiptap iframe { aspect-ratio: 16 / 9; } + +/* Mathematics extension styles */ +.tiptap .tiptap-mathematics-render { + padding: 0 0.25rem; + border-radius: 0.25rem; +} + +/* Editable math block */ +.tiptap .tiptap-mathematics-render--editable { + cursor: pointer; + transition: background 0.2s; +} + +.tiptap .tiptap-mathematics-render--editable:hover { + background: var(--code-bg); +} + +/* Inline math */ +.tiptap .tiptap-mathematics-render[data-type='inline-math'] { + display: inline-block; +} + +/* Block math */ +.tiptap .tiptap-mathematics-render[data-type='block-math'] { + display: block; + margin: 1rem 0; + padding: 0.5rem; + text-align: center; + font-size: 1.25rem; +} + +/* Error styles */ +.tiptap .tiptap-mathematics-render.inline-math-error, +.tiptap .tiptap-mathematics-render.block-math-error { + background: var(--code-bg); + color: var(--color-destructive); + border: 1px solid darkred; +} diff --git a/packages/ui/src/custom/editor/editor.ts b/packages/ui/src/custom/editor/editor.ts index 92ab6827..616c32c5 100644 --- a/packages/ui/src/custom/editor/editor.ts +++ b/packages/ui/src/custom/editor/editor.ts @@ -12,10 +12,12 @@ import SearchAndReplace from './extensions/FindAndReplace'; import { TaskItem, TaskList } from '@tiptap/extension-list'; import { Table, TableCell, TableRow, TableHeader } from './extensions/table'; import { Placeholder } from '@tiptap/extensions'; -import { Markdown } from 'tiptap-markdown'; -import MathExtension from '@aarkue/tiptap-math-extension'; +import { Markdown } from '@tiptap/markdown'; +import MathMatics from '@tiptap/extension-mathematics'; + import AutoJoiner from 'tiptap-extension-auto-joiner'; import 'katex/dist/katex.min.css'; +import { InlineMathReplacer } from './extensions/InlineMathReplacer'; export default ( element?: HTMLElement, @@ -82,22 +84,41 @@ export default ( nested: true }), SearchAndReplace, - MathExtension.configure({ evaluation: true }), + InlineMathReplacer, + MathMatics.configure({ + blockOptions: { + onClick: (node, pos) => { + const newCalculation = prompt('Enter new calculation:', node.attrs.latex); + if (newCalculation) { + editor + .chain() + .setNodeSelection(pos) + .updateBlockMath({ latex: newCalculation }) + .focus() + .run(); + } + } + }, + inlineOptions: { + onClick: (node, pos) => { + const newCalculation = prompt('Enter new calculation:', node.attrs.latex); + if (newCalculation) { + editor + .chain() + .setNodeSelection(pos) + .updateInlineMath({ latex: newCalculation }) + .focus() + .run(); + } + } + } + }), AutoJoiner, Table, TableHeader, TableRow, TableCell, - Markdown.configure({ - html: true, - tightLists: true, - tightListClass: 'tight', - bulletListMarker: '-', - linkify: true, - breaks: true, - transformPastedText: true, - transformCopiedText: false - }), + Markdown, ...(extensions ?? []) ], diff --git a/packages/ui/src/custom/editor/extensions/InlineMathReplacer.ts b/packages/ui/src/custom/editor/extensions/InlineMathReplacer.ts new file mode 100644 index 00000000..f4a08d06 --- /dev/null +++ b/packages/ui/src/custom/editor/extensions/InlineMathReplacer.ts @@ -0,0 +1,21 @@ +import { textInputRule } from '@tiptap/core'; +import { InlineMath } from '@tiptap/extension-mathematics'; + +export const InlineMathReplacer = InlineMath.extend({ + name: 'inlineMathReplacer', + addInputRules() { + return [ + textInputRule({ + find: /\$\$([^$]+)\$\$/, + replace: ({ match, commands }) => { + const latex = match[1]; + // Insert the inline math node with the LaTeX content + commands.insertInlineMath({ + latex + }); + return ''; + } + }) + ]; + } +}); diff --git a/packages/ui/src/custom/editor/extensions/slash-command/groups.ts b/packages/ui/src/custom/editor/extensions/slash-command/groups.ts index da4e7b93..882e5b6e 100644 --- a/packages/ui/src/custom/editor/extensions/slash-command/groups.ts +++ b/packages/ui/src/custom/editor/extensions/slash-command/groups.ts @@ -42,6 +42,7 @@ export const GROUPS: Group[] = [ actions: [ ...commands.media, ...commands.table, + ...commands.math, { icon: Minus, name: 'horizontalRule', diff --git a/packages/ui/src/custom/editor/ui/drag-handle-extended.svelte b/packages/ui/src/custom/editor/ui/drag-handle-extended.svelte index 3afe23e6..92df5e88 100644 --- a/packages/ui/src/custom/editor/ui/drag-handle-extended.svelte +++ b/packages/ui/src/custom/editor/ui/drag-handle-extended.svelte @@ -10,7 +10,10 @@ import Duplicate from '@lucide/svelte/icons/copy'; import Clipboard from '@lucide/svelte/icons/clipboard'; import Delete from '@lucide/svelte/icons/trash-2'; + import Repeat2 from '@lucide/svelte/icons/repeat-2'; + import Plus from '@lucide/svelte/icons/plus'; import { NodeSelection } from '@tiptap/pm/state'; + import commands from '../commands/toolbar-commands'; interface Props { editor: Editor; @@ -21,6 +24,9 @@ let currentNode: Node | null = $state(null); let currentNodePos: number = $state(-1); let open = $state(false); + const turnIntoCommand = Object.values(commands) + .flat() + .filter((c) => c.turnInto !== undefined); const pluginKey = 'globalDragHandle'; @@ -71,10 +77,39 @@ const handleDelete = () => { editor.chain().setMeta('hideDragHandle', true).setNodeSelection(currentNodePos).deleteSelection().run(); }; + + const handleAddNodeNext = () => { + if (currentNodePos === -1) return; + const currentNodeSize = currentNode?.nodeSize || 0; + const insertPos = currentNodePos + currentNodeSize; + const currentNodeIsEmptyParagraph = + currentNode?.type.name === 'paragraph' && currentNode?.content?.size === 0; + const focusPos = currentNodeIsEmptyParagraph ? currentNodePos + 2 : insertPos + 2; + editor + .chain() + .command(({ dispatch, tr, state }) => { + if (dispatch) { + if (currentNodeIsEmptyParagraph) { + tr.insertText('/', currentNodePos, currentNodePos + 1); + } else { + tr.insert( + insertPos, + state.schema.nodes.paragraph.create(null, [state.schema.text('/')]) + ); + } + + return dispatch(tr); + } + + return true; + }) + .focus(focusPos) + .run(); + };
- @@ -82,10 +117,43 @@ Drag Handle + + + {currentNode?.type.name} + + + + + Turn Into + + + {#each turnIntoCommand as command (command)} + {@const Icon = command.icon} + { + if (currentNode && currentNodePos) command.turnInto?.(editor, currentNodePos); + }} + > + + {command.tooltip} + {command.shortCut} + + {/each} + + + + + + + Add Node + Remove Formatting + Duplicate Node @@ -94,6 +162,7 @@ Copy to clipboard + Delete Node diff --git a/packages/ui/src/custom/editor/ui/menus/Menu.svelte b/packages/ui/src/custom/editor/ui/menus/Menu.svelte index bdf36a67..dc69ea16 100644 --- a/packages/ui/src/custom/editor/ui/menus/Menu.svelte +++ b/packages/ui/src/custom/editor/ui/menus/Menu.svelte @@ -106,7 +106,11 @@ {:else} {@const commandGroup = commands[cmd]} {#each commandGroup as command (command)} - + {#if command.name === 'paragraph'} + + {:else} + + {/if} {/each} {/if} {/each}