// TipTap node definition for inline model-element chips. // // M2: chips are inline, atomic, selectable. They carry { kind, refId, label }. // In MVP M5 the label will be derived from a live model lookup keyed by refId; // for M2 we store it directly on the node so renaming + persistence Just Work. import { Node, mergeAttributes } from "@tiptap/core"; import { ReactNodeViewRenderer } from "@tiptap/react"; import type { ChipKind } from "../../lib/fixtures/aristotle"; import { ChipView } from "./ChipView"; export interface ChipAttrs { kind: ChipKind; refId: string | null; label: string; } declare module "@tiptap/core" { interface Commands { chip: { insertChip: (attrs: ChipAttrs) => ReturnType; }; } } export const ChipNode = Node.create({ name: "chip", group: "inline", inline: true, atom: true, selectable: true, draggable: false, addAttributes() { return { kind: { default: "block", parseHTML: el => el.getAttribute("data-kind") ?? "block", renderHTML: attrs => ({ "data-kind": attrs.kind }), }, refId: { default: null, parseHTML: el => el.getAttribute("data-ref-id"), renderHTML: attrs => (attrs.refId ? { "data-ref-id": attrs.refId } : {}), }, label: { default: "untitled", parseHTML: el => el.getAttribute("data-label") ?? el.textContent ?? "untitled", renderHTML: attrs => ({ "data-label": attrs.label }), }, }; }, parseHTML() { return [{ tag: "span[data-chip]" }]; }, renderHTML({ HTMLAttributes, node }) { return [ "span", mergeAttributes({ "data-chip": "" }, HTMLAttributes), `${node.attrs.label}`, ]; }, addNodeView() { return ReactNodeViewRenderer(ChipView); }, addCommands() { return { insertChip: (attrs: ChipAttrs) => ({ chain }) => chain() .insertContent({ type: this.name, attrs }) // Insert a trailing space so the caret leaves the chip cleanly .insertContent(" ") .run(), }; }, });