update mention input for

This commit is contained in:
Nate Kelley 2025-09-29 11:04:22 -06:00
parent d4075311a3
commit 4807e092bb
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
8 changed files with 102 additions and 62 deletions

View File

@ -1,11 +1,11 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { fn } from 'storybook/test';
import { BusterInput } from './BusterInput';
import type { BusterInputProps } from './BusterInput.types';
const meta: Meta<typeof BusterInput> = {
title: 'UI/Inputs/BusterInput',
component: BusterInput,
tags: ['autodocs'],
decorators: [
(Story) => (
<div style={{ width: '300px', minHeight: '500px' }}>
@ -78,5 +78,6 @@ export const Default: Story = {
value: 'Sample text value',
suggestionItems: items,
mentions,
onSubmit: fn(),
},
};

View File

@ -15,6 +15,7 @@ export const BusterInput = ({
emptyComponent,
submitting,
onSubmit,
onPressEnter,
disabled: disabledGlobal = false,
onStop,
sendIcon,
@ -105,6 +106,7 @@ export const BusterInput = ({
shouldFilter={shouldFilter}
filter={filter}
onMentionItemClick={onMentionItemClick}
onPressEnter={onPressEnter || onSubmit}
/>
</BusterInputContainer>
<BusterInputList show={showSuggestionList}>

View File

@ -1,7 +1,7 @@
import type { Command } from 'cmdk';
import type React from 'react';
import type { MentionSuggestionExtension } from '../MentionInput';
import type { MentionTriggerItem } from '../MentionInput/MentionInput.types';
import type { MentionInputProps, MentionTriggerItem } from '../MentionInput/MentionInput.types';
/**
* @description Override the addValueToInput and closeOnSelect props for the item based on the group props
@ -59,6 +59,7 @@ export type BusterInputProps<T = string> = {
disabled?: boolean;
readOnly?: boolean;
onSubmit: (value: string) => void;
onPressEnter: MentionInputProps['onPressEnter'];
onStop: () => void;
variant?: 'default';
autoFocus?: boolean;

View File

@ -15,6 +15,7 @@ export type BusterMentionsInputProps = Pick<
| 'onMentionItemClick'
> & {
onChange: MentionInputProps['onChange'];
onPressEnter: MentionInputProps['onPressEnter'];
className?: string;
style?: React.CSSProperties;
autoFocus?: boolean;
@ -35,7 +36,9 @@ export const BusterMentionsInput = ({
<Command.Input
value={value}
autoFocus={false}
// className="sr-only hidden h-0 border-0 p-0"
className="absolute -top-5 left-0 w-full outline-1 outline-amber-200 pointer-events-none"
aria-hidden="true"
/>
</React.Fragment>
);

View File

@ -2,23 +2,13 @@ import { ClientOnly } from '@tanstack/react-router';
import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text';
import {
type Editor,
EditorContent,
EditorContext,
type NodeType,
type TextType,
useEditor,
} from '@tiptap/react';
import { EditorContent, EditorContext, useEditor } from '@tiptap/react';
import { useMemo } from 'react';
import { cn } from '@/lib/utils';
import { MentionExtension } from './MentionExtension';
import type {
MentionArrayItem,
MentionInputProps,
MentionSuggestionExtension,
} from './MentionInput.types';
import type { MentionPillAttributes } from './MentionPill';
import type { MentionInputProps, MentionSuggestionExtension } from './MentionInput.types';
import { SubmitOnEnter } from './SubmitEnterExtension';
import { onUpdateTransformer } from './update-transformers';
export const MentionInput = ({
mentions,
@ -31,6 +21,7 @@ export const MentionInput = ({
className,
readOnly,
disabled,
onPressEnter,
}: MentionInputProps) => {
const mentionsByTrigger = useMemo(() => {
return mentions.reduce(
@ -45,7 +36,16 @@ export const MentionInput = ({
}, [mentions]);
const editor = useEditor({
extensions: [Document, Paragraph, Text, MentionExtension(mentions)],
extensions: [
Document,
Paragraph,
Text,
MentionExtension(mentions),
SubmitOnEnter({
mentionsByTrigger,
onPressEnter,
}),
],
content: defaultValue,
autofocus: autoFocus,
editable: !disabled && !readOnly,
@ -75,48 +75,3 @@ export const MentionInput = ({
</ClientOnly>
);
};
const onUpdateTransformer = ({
editor,
mentionsByTrigger,
}: {
editor: Editor;
mentionsByTrigger: Record<string, MentionSuggestionExtension>;
}) => {
const editorText = editor.getText();
const editorJson = editor.getJSON();
const transformedJson: MentionArrayItem[] = editorJson.content.reduce<MentionArrayItem[]>(
(acc, item) => {
if (item.type === 'paragraph') {
item.content?.forEach((item) => {
if (item.type === 'text') {
const _item = item as TextType;
acc.push({ type: 'text', text: _item.text });
} else if (item.type === 'mention') {
const _item = item as NodeType<'mention', MentionPillAttributes>;
acc.push({ type: 'mention', attrs: _item.attrs });
}
});
}
return acc;
},
[]
);
const transformedValue = transformedJson.reduce((acc, item) => {
if (item.type === 'text') {
return acc + item.text;
}
if (item.type === 'mention') {
const onChangeTransform = mentionsByTrigger[item.attrs.trigger]?.onChangeTransform;
if (onChangeTransform) return acc + onChangeTransform(item.attrs);
return acc + item.attrs.label;
}
return acc;
}, '');
return {
transformedValue,
transformedJson,
editorText,
};
};

View File

@ -90,6 +90,7 @@ export type MentionOnChangeParams = (
export type MentionInputProps = {
mentions: MentionSuggestionExtension[];
onChange: MentionOnChangeParams;
onPressEnter?: MentionOnChangeParams;
onFocus?: (v: EditorEvents['focus']) => void;
onBlur?: (v: EditorEvents['blur']) => void;
defaultValue?: string;

View File

@ -0,0 +1,29 @@
import { Extension } from '@tiptap/core';
import type { MentionInputProps, MentionSuggestionExtension } from './MentionInput.types';
import { onUpdateTransformer } from './update-transformers';
export const SubmitOnEnter = ({
onPressEnter,
mentionsByTrigger,
}: {
onPressEnter?: MentionInputProps['onPressEnter'];
mentionsByTrigger: Record<string, MentionSuggestionExtension>;
}) =>
Extension.create({
addKeyboardShortcuts() {
return {
Enter: ({ editor }) => {
console.log('Enter', onPressEnter);
if (onPressEnter) {
const { transformedValue, transformedJson, editorText } = onUpdateTransformer({
editor,
mentionsByTrigger,
});
onPressEnter?.(transformedValue, transformedJson, editorText);
}
return !!onPressEnter;
},
'Shift-Enter': () => this.editor.commands.newlineInCode(), // or insert a break
};
},
});

View File

@ -0,0 +1,48 @@
import type { Editor, NodeType, TextType } from '@tiptap/react';
import type { MentionArrayItem, MentionSuggestionExtension } from './MentionInput.types';
import type { MentionPillAttributes } from './MentionPill';
export const onUpdateTransformer = ({
editor,
mentionsByTrigger,
}: {
editor: Editor;
mentionsByTrigger: Record<string, MentionSuggestionExtension>;
}) => {
const editorText = editor.getText();
const editorJson = editor.getJSON();
const transformedJson: MentionArrayItem[] = editorJson.content.reduce<MentionArrayItem[]>(
(acc, item) => {
if (item.type === 'paragraph') {
item.content?.forEach((item) => {
if (item.type === 'text') {
const _item = item as TextType;
acc.push({ type: 'text', text: _item.text });
} else if (item.type === 'mention') {
const _item = item as NodeType<'mention', MentionPillAttributes>;
acc.push({ type: 'mention', attrs: _item.attrs });
}
});
}
return acc;
},
[]
);
const transformedValue = transformedJson.reduce((acc, item) => {
if (item.type === 'text') {
return acc + item.text;
}
if (item.type === 'mention') {
const onChangeTransform = mentionsByTrigger[item.attrs.trigger]?.onChangeTransform;
if (onChangeTransform) return acc + onChangeTransform(item.attrs);
return acc + item.attrs.label;
}
return acc;
}, '');
return {
transformedValue,
transformedJson,
editorText,
};
};