feat(transcription): re-edit @mention via a pencil affordance (#628)
Hosts each mention as a Tiptap NodeView (mentionNodeView.ts) that renders the @displayName token (textContent — never innerHTML) plus a contenteditable=false pencil button in a fixed-width slot, revealed on whole-token hover and keyboard focus (instant opacity swap, no reflow). Activating the pencil (click or Enter/ Space) opens the single mention dropdown via the controller, anchored at the token and pre-filled with the stored displayName. commitRelink swaps ONLY personId in place via setNodeMarkup, sourcing the id solely from the selected Person — the stored displayName is preserved by construction (AC-3), even after the search input is edited (AC-5, the #380 AC-1 invariant). renderHTML/renderText stay for serialization + clipboard. AC-1/AC-2/AC-3/AC-5 + serializer round-trip covered by browser tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import type { PersonMention } from '$lib/shared/types';
|
||||
import { deserialize, serialize } from '$lib/shared/discussion/mentionSerializer';
|
||||
import { debounce } from '$lib/shared/utils/debounce';
|
||||
import MentionDropdown from './MentionDropdown.svelte';
|
||||
import { createMentionNodeView } from './mentionNodeView';
|
||||
import { MAX_QUERY_LENGTH, SEARCH_DEBOUNCE_MS, SEARCH_RESULT_LIMIT } from './mentionConstants';
|
||||
|
||||
type Person = components['schemas']['Person'];
|
||||
@@ -190,6 +191,37 @@ function createMentionController(): MentionController {
|
||||
|
||||
const controller = createMentionController();
|
||||
|
||||
// Re-link an existing mention in place (#628 AC-3/AC-5). Captures the node's
|
||||
// pos when the pencil opens and swaps ONLY personId via setNodeMarkup, so the
|
||||
// stored displayName is preserved by construction — never round-tripped through
|
||||
// insertContentAt (which would rebind displayName to the search query). The new
|
||||
// personId comes solely from the selected Person (anti-spoof — never from the
|
||||
// reflected data-person-id or the search input).
|
||||
function commitRelink(pos: number): CommitFn {
|
||||
return (item: Person) => {
|
||||
if (!editor) return;
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.command(({ tr, state }) => {
|
||||
const node = state.doc.nodeAt(pos);
|
||||
if (!node || node.type.name !== 'mention') return false;
|
||||
tr.setNodeMarkup(pos, undefined, { ...node.attrs, personId: item.id });
|
||||
return true;
|
||||
})
|
||||
.run();
|
||||
controller.close();
|
||||
};
|
||||
}
|
||||
|
||||
// Entry point handed to the mention NodeView's pencil. Opens the one dropdown
|
||||
// (closing any other first — AC-6) anchored at the mention and pre-filled with
|
||||
// the stored displayName.
|
||||
function requestRelink(getRect: () => DOMRect | null, displayName: string, pos: number) {
|
||||
if (!editor || !editor.isEditable) return;
|
||||
controller.open(getRect, displayName, commitRelink(pos));
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Custom Mention node: uses personId / displayName instead of the
|
||||
// default id / label attribute names so the mentionSerializer can
|
||||
@@ -215,6 +247,12 @@ onMount(() => {
|
||||
})
|
||||
}
|
||||
};
|
||||
},
|
||||
// #628: host the mention as a NodeView so each token carries a pencil
|
||||
// re-edit affordance. renderHTML/renderText below stay for serialization
|
||||
// and clipboard; this only governs the live in-editor DOM.
|
||||
addNodeView() {
|
||||
return createMentionNodeView(requestRelink);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user