diff --git a/src/app.css b/src/app.css index c48914febf..756b0d9a08 100644 --- a/src/app.css +++ b/src/app.css @@ -420,6 +420,22 @@ input[type='number'] { content: '\200B'; } +.tiptap .suggestion { + border-radius: 0.4rem; + box-decoration-break: clone; + padding: 0.1rem 0.3rem; + @apply bg-purple-100/20 text-purple-900 dark:bg-purple-500/20 dark:text-purple-100; +} + +.tiptap .suggestion::after { + content: '\200B'; +} + +.tiptap .suggestion.is-empty::after { + content: '\00A0'; + border-bottom: 1px dotted rgba(31, 41, 55, 0.12); +} + .input-prose .tiptap ul[data-type='taskList'] { list-style: none; margin-left: 0; diff --git a/src/lib/components/channel/MessageInput/MentionList.svelte b/src/lib/components/channel/MessageInput/MentionList.svelte index 9f6526852a..4bc2b5c8a1 100644 --- a/src/lib/components/channel/MessageInput/MentionList.svelte +++ b/src/lib/components/channel/MessageInput/MentionList.svelte @@ -15,7 +15,7 @@ const select = (index: number) => { const item = filteredItems[index]; - if (item) command(item); + if (item) command({ id: item.id, label: item.name }); }; const onKeyDown = (event: KeyboardEvent) => { diff --git a/src/lib/components/common/RichTextInput.svelte b/src/lib/components/common/RichTextInput.svelte index caa62bb454..c6e2e2c6ff 100644 --- a/src/lib/components/common/RichTextInput.svelte +++ b/src/lib/components/common/RichTextInput.svelte @@ -91,6 +91,18 @@ } }); + // Convert TipTap mention spans -> <@id> + turndownService.addRule('mentions', { + filter: (node) => node.nodeName === 'SPAN' && node.getAttribute('data-type') === 'mention', + replacement: (_content, node: HTMLElement) => { + const id = node.getAttribute('data-id') || ''; + // TipTap stores the trigger char in data-mention-suggestion-char (usually "@") + const ch = node.getAttribute('data-mention-suggestion-char') || '@'; + // Emit <@id> style, e.g. <@llama3.2:latest> + return `<${ch}${id}>`; + } + }); + import { onMount, onDestroy, tick, getContext } from 'svelte'; import { createEventDispatcher } from 'svelte'; @@ -100,7 +112,7 @@ import { Fragment, DOMParser } from 'prosemirror-model'; import { EditorState, Plugin, PluginKey, TextSelection, Selection } from 'prosemirror-state'; import { Decoration, DecorationSet } from 'prosemirror-view'; - import { Editor, Extension } from '@tiptap/core'; + import { Editor, Extension, mergeAttributes } from '@tiptap/core'; // Yjs imports import * as Y from 'yjs'; @@ -142,9 +154,6 @@ import { PASTED_TEXT_CHARACTER_LIMIT } from '$lib/constants'; import { all, createLowlight } from 'lowlight'; - import MentionList from '../channel/MessageInput/MentionList.svelte'; - import { getSuggestionRenderer } from './RichTextInput/suggestions.js'; - export let oncompositionstart = (e) => {}; export let oncompositionend = (e) => {}; export let onChange = (e) => {}; @@ -1054,7 +1063,6 @@ htmlValue = editor.getHTML(); jsonValue = editor.getJSON(); - mdValue = turndownService .turndown( htmlValue diff --git a/src/lib/components/common/RichTextInput/suggestions.ts b/src/lib/components/common/RichTextInput/suggestions.ts index 1af255004e..8d4244700b 100644 --- a/src/lib/components/common/RichTextInput/suggestions.ts +++ b/src/lib/components/common/RichTextInput/suggestions.ts @@ -19,6 +19,7 @@ export function getSuggestionRenderer(Component: any, ComponentProps = {}) { target: container, props: { char: props?.text, + query: props?.query, command: (item) => { props.command({ id: item.id, label: item.label }); }, @@ -54,7 +55,14 @@ export function getSuggestionRenderer(Component: any, ComponentProps = {}) { onUpdate: (props: any) => { if (!component) return; - component.$set({ query: props.query }); + + component.$set({ + query: props.query, + command: (item) => { + props.command({ id: item.id, label: item.label }); + } + }); + if (props.clientRect && popup) { popup.setProps({ getReferenceClientRect: props.clientRect as any }); }