mirror of https://github.com/buster-so/buster.git
pass in read props
This commit is contained in:
parent
0c576b62e6
commit
d4075311a3
|
@ -22,6 +22,8 @@ export const BusterInput = ({
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
onChange,
|
onChange,
|
||||||
ariaLabel = 'Buster Input',
|
ariaLabel = 'Buster Input',
|
||||||
|
readOnly,
|
||||||
|
autoFocus,
|
||||||
//suggestions
|
//suggestions
|
||||||
suggestionItems,
|
suggestionItems,
|
||||||
closeSuggestionOnSelect = true,
|
closeSuggestionOnSelect = true,
|
||||||
|
@ -58,6 +60,7 @@ export const BusterInput = ({
|
||||||
console.warn('Item is loading', params);
|
console.warn('Item is loading', params);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.log('onSelectItem', addValueToInput, params);
|
||||||
if (addValueToInput) setValue(inputValue ?? String(label));
|
if (addValueToInput) setValue(inputValue ?? String(label));
|
||||||
onClick?.();
|
onClick?.();
|
||||||
if (closeSuggestionOnSelect) setHasClickedSelect(true);
|
if (closeSuggestionOnSelect) setHasClickedSelect(true);
|
||||||
|
@ -92,13 +95,13 @@ export const BusterInput = ({
|
||||||
variant={variant}
|
variant={variant}
|
||||||
>
|
>
|
||||||
<BusterMentionsInput
|
<BusterMentionsInput
|
||||||
autoFocus
|
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
readOnly
|
readOnly={readOnly}
|
||||||
|
autoFocus={autoFocus}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
mentions={mentions}
|
mentions={mentions}
|
||||||
value={value}
|
value={value}
|
||||||
onChangeInputValue={onChangeInputValue}
|
onChange={onChangeInputValue}
|
||||||
shouldFilter={shouldFilter}
|
shouldFilter={shouldFilter}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
onMentionItemClick={onMentionItemClick}
|
onMentionItemClick={onMentionItemClick}
|
||||||
|
|
|
@ -57,9 +57,11 @@ export type BusterInputProps<T = string> = {
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
submitting?: boolean;
|
submitting?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
readOnly?: boolean;
|
||||||
onSubmit: (value: string) => void;
|
onSubmit: (value: string) => void;
|
||||||
onStop: () => void;
|
onStop: () => void;
|
||||||
variant?: 'default';
|
variant?: 'default';
|
||||||
|
autoFocus?: boolean;
|
||||||
sendIcon?: React.ReactNode;
|
sendIcon?: React.ReactNode;
|
||||||
secondaryActions?: React.ReactNode;
|
secondaryActions?: React.ReactNode;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
@ -67,7 +69,7 @@ export type BusterInputProps<T = string> = {
|
||||||
emptyComponent?: React.ReactNode | string | false; //if false, no empty component will be shown
|
emptyComponent?: React.ReactNode | string | false; //if false, no empty component will be shown
|
||||||
//mentions
|
//mentions
|
||||||
onMentionItemClick?: (params: MentionTriggerItem<T>) => void;
|
onMentionItemClick?: (params: MentionTriggerItem<T>) => void;
|
||||||
mentions?: MentionSuggestionExtension[];
|
mentions: MentionSuggestionExtension[];
|
||||||
//suggestions
|
//suggestions
|
||||||
suggestionItems: (
|
suggestionItems: (
|
||||||
| BusterInputDropdownItem<T>
|
| BusterInputDropdownItem<T>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/** biome-ignore-all lint/complexity/noUselessFragments: Intersting bug when NOT using fragments */
|
/** biome-ignore-all lint/complexity/noUselessFragments: Intersting bug when NOT using fragments */
|
||||||
import { Command } from 'cmdk';
|
import { Command } from 'cmdk';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { cn } from '@/lib/classMerge';
|
import { MentionInput, type MentionInputProps } from '../MentionInput';
|
||||||
import type { BusterInputProps } from './BusterInput.types';
|
import type { BusterInputProps } from './BusterInput.types';
|
||||||
|
|
||||||
export type BusterMentionsInputProps = Pick<
|
export type BusterMentionsInputProps = Pick<
|
||||||
|
@ -14,33 +14,29 @@ export type BusterMentionsInputProps = Pick<
|
||||||
| 'filter'
|
| 'filter'
|
||||||
| 'onMentionItemClick'
|
| 'onMentionItemClick'
|
||||||
> & {
|
> & {
|
||||||
onChangeInputValue: (value: string) => void;
|
onChange: MentionInputProps['onChange'];
|
||||||
} & React.ComponentPropsWithoutRef<typeof Command.Input>;
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
readOnly?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export const BusterMentionsInput = ({
|
export const BusterMentionsInput = ({
|
||||||
children,
|
|
||||||
value: valueProp,
|
value: valueProp,
|
||||||
placeholder,
|
placeholder,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
mentions,
|
mentions,
|
||||||
value,
|
value,
|
||||||
onChangeInputValue,
|
|
||||||
className,
|
|
||||||
style,
|
|
||||||
...props
|
...props
|
||||||
}: BusterMentionsInputProps) => {
|
}: BusterMentionsInputProps) => {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<textarea />
|
<MentionInput mentions={mentions} defaultValue={value} {...props} />
|
||||||
|
|
||||||
<Command.Input
|
<Command.Input
|
||||||
value={value}
|
value={value}
|
||||||
{...props}
|
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
className="absolute -top-5 left-0 w-full outline-1 outline-amber-200 pointer-events-none"
|
className="absolute -top-5 left-0 w-full outline-1 outline-amber-200 pointer-events-none"
|
||||||
>
|
/>
|
||||||
{children}
|
|
||||||
</Command.Input>
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
import { fn } from 'storybook/test';
|
||||||
import { createMentionSuggestionExtension } from './createMentionSuggestionOption';
|
import { createMentionSuggestionExtension } from './createMentionSuggestionOption';
|
||||||
import { MentionInput } from './MentionInput';
|
import { MentionInput } from './MentionInput';
|
||||||
import type { MentionInputTriggerItem } from './MentionInput.types';
|
import type { MentionInputTriggerItem } from './MentionInput.types';
|
||||||
|
@ -55,48 +56,6 @@ const looneyTunesCharacters: MentionInputTriggerItem[] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const theSimpsonsCharacters: MentionInputTriggerItem[] = [
|
|
||||||
{
|
|
||||||
value: 'Homer Simpson',
|
|
||||||
label: 'Homer Simpson',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'Marge Simpson',
|
|
||||||
label: 'Marge Simpson',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'Bart Simpson',
|
|
||||||
label: 'Bart Simpson',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'Lisa Simpson',
|
|
||||||
label: 'Lisa Simpson',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'Maggie Simpson',
|
|
||||||
label: 'Maggie Simpson',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'Ned Flanders',
|
|
||||||
label: 'Ned Flanders',
|
|
||||||
},
|
|
||||||
].map((item) => ({
|
|
||||||
...item,
|
|
||||||
label: (
|
|
||||||
<span className="gap-x-1 space-x-1">
|
|
||||||
<img
|
|
||||||
src={faker.image.url({
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
})}
|
|
||||||
alt=""
|
|
||||||
className="w-3 h-3 rounded-full bg-item-active inline-block align-middle"
|
|
||||||
/>
|
|
||||||
<span className="inline-block">{item.label}</span>
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const arthurCharacters: MentionInputTriggerItem[] = [
|
const arthurCharacters: MentionInputTriggerItem[] = [
|
||||||
{
|
{
|
||||||
value: 'Arthur Read',
|
value: 'Arthur Read',
|
||||||
|
@ -138,7 +97,7 @@ const arthurCharacters: MentionInputTriggerItem[] = [
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const looneyTunesSuggestions = createMentionSuggestionExtension({
|
const looneyTunesSuggestions = createMentionSuggestionExtension({
|
||||||
trigger: '@',
|
trigger: '@',
|
||||||
items: looneyTunesCharacters,
|
items: looneyTunesCharacters,
|
||||||
popoverContent: (props) => {
|
popoverContent: (props) => {
|
||||||
|
@ -150,10 +109,52 @@ export const looneyTunesSuggestions = createMentionSuggestionExtension({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
onChangeTransform: (v) => {
|
onChangeTransform: (v) => {
|
||||||
return `[@${String(v.label)}](${String(v.value)})`;
|
return `We can totally transform this into anything we want. The original value was [@${String(v.label)}](${String(v.value)})`;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const theSimpsonsCharacters: MentionInputTriggerItem[] = [
|
||||||
|
{
|
||||||
|
value: 'Homer Simpson',
|
||||||
|
label: 'Homer Simpson',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Marge Simpson',
|
||||||
|
label: 'Marge Simpson',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Bart Simpson',
|
||||||
|
label: 'Bart Simpson',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Lisa Simpson',
|
||||||
|
label: 'Lisa Simpson',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Maggie Simpson',
|
||||||
|
label: 'Maggie Simpson',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'Ned Flanders',
|
||||||
|
label: 'Ned Flanders',
|
||||||
|
},
|
||||||
|
].map((item) => ({
|
||||||
|
...item,
|
||||||
|
label: (
|
||||||
|
<span className="gap-x-1 space-x-1">
|
||||||
|
<img
|
||||||
|
src={faker.image.url({
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
})}
|
||||||
|
alt=""
|
||||||
|
className="w-3 h-3 rounded-full bg-item-active inline-block align-middle"
|
||||||
|
/>
|
||||||
|
<span className="inline-block">{item.label}</span>
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
const theSimpsonsSuggestions = createMentionSuggestionExtension({
|
const theSimpsonsSuggestions = createMentionSuggestionExtension({
|
||||||
trigger: '#',
|
trigger: '#',
|
||||||
items: theSimpsonsCharacters,
|
items: theSimpsonsCharacters,
|
||||||
|
@ -301,6 +302,7 @@ export const Default: Story = {
|
||||||
arthurSuggestions,
|
arthurSuggestions,
|
||||||
spongebobSuggestions,
|
spongebobSuggestions,
|
||||||
],
|
],
|
||||||
|
onChange: fn(),
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
docs: {
|
docs: {
|
||||||
|
|
|
@ -3,18 +3,19 @@ import Document from '@tiptap/extension-document';
|
||||||
import Paragraph from '@tiptap/extension-paragraph';
|
import Paragraph from '@tiptap/extension-paragraph';
|
||||||
import Text from '@tiptap/extension-text';
|
import Text from '@tiptap/extension-text';
|
||||||
import {
|
import {
|
||||||
|
type Editor,
|
||||||
EditorContent,
|
EditorContent,
|
||||||
EditorContext,
|
EditorContext,
|
||||||
type EditorEvents,
|
|
||||||
type NodeType,
|
type NodeType,
|
||||||
type TextType,
|
type TextType,
|
||||||
useEditor,
|
useEditor,
|
||||||
} from '@tiptap/react';
|
} from '@tiptap/react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import { MentionExtension } from './MentionExtension';
|
import { MentionExtension } from './MentionExtension';
|
||||||
import type {
|
import type {
|
||||||
MentionArrayItem,
|
MentionArrayItem,
|
||||||
MentionOnChangeParams,
|
MentionInputProps,
|
||||||
MentionSuggestionExtension,
|
MentionSuggestionExtension,
|
||||||
} from './MentionInput.types';
|
} from './MentionInput.types';
|
||||||
import type { MentionPillAttributes } from './MentionPill';
|
import type { MentionPillAttributes } from './MentionPill';
|
||||||
|
@ -25,13 +26,12 @@ export const MentionInput = ({
|
||||||
defaultValue = '',
|
defaultValue = '',
|
||||||
onFocus,
|
onFocus,
|
||||||
onBlur,
|
onBlur,
|
||||||
}: {
|
autoFocus,
|
||||||
mentions: MentionSuggestionExtension[];
|
style,
|
||||||
onChange?: MentionOnChangeParams;
|
className,
|
||||||
onFocus?: (v: EditorEvents['focus']) => void;
|
readOnly,
|
||||||
onBlur?: (v: EditorEvents['blur']) => void;
|
disabled,
|
||||||
defaultValue?: string;
|
}: MentionInputProps) => {
|
||||||
}) => {
|
|
||||||
const mentionsByTrigger = useMemo(() => {
|
const mentionsByTrigger = useMemo(() => {
|
||||||
return mentions.reduce(
|
return mentions.reduce(
|
||||||
(acc, mention) => {
|
(acc, mention) => {
|
||||||
|
@ -47,43 +47,18 @@ export const MentionInput = ({
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [Document, Paragraph, Text, MentionExtension(mentions)],
|
extensions: [Document, Paragraph, Text, MentionExtension(mentions)],
|
||||||
content: defaultValue,
|
content: defaultValue,
|
||||||
autofocus: true,
|
autofocus: autoFocus,
|
||||||
|
editable: !disabled && !readOnly,
|
||||||
editorProps: {
|
editorProps: {
|
||||||
attributes: {
|
attributes: {
|
||||||
class: 'p-1',
|
class: cn('p-1 border rounded outline-0', className),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
onUpdate: ({ editor }) => {
|
onUpdate: ({ editor }) => {
|
||||||
const editorText = editor.getText();
|
const { transformedValue, transformedJson, editorText } = onUpdateTransformer({
|
||||||
const editorJson = editor.getJSON();
|
editor,
|
||||||
const transformedJson: MentionArrayItem[] = editorJson.content.reduce<MentionArrayItem[]>(
|
mentionsByTrigger,
|
||||||
(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;
|
|
||||||
}, '');
|
|
||||||
onChange?.(transformedValue, transformedJson, editorText);
|
onChange?.(transformedValue, transformedJson, editorText);
|
||||||
},
|
},
|
||||||
onFocus: onFocus,
|
onFocus: onFocus,
|
||||||
|
@ -95,8 +70,53 @@ export const MentionInput = ({
|
||||||
return (
|
return (
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<EditorContext.Provider value={providerValue}>
|
<EditorContext.Provider value={providerValue}>
|
||||||
<EditorContent className="rounded p-1 border outline-1 min-w-120" editor={editor} />
|
<EditorContent className="outline-0" editor={editor} style={style} />
|
||||||
</EditorContext.Provider>
|
</EditorContext.Provider>
|
||||||
</ClientOnly>
|
</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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type { MentionNodeAttrs, MentionOptions } from '@tiptap/extension-mention';
|
import type { MentionNodeAttrs, MentionOptions } from '@tiptap/extension-mention';
|
||||||
|
import type { EditorEvents } from '@tiptap/react';
|
||||||
import type { MentionPillAttributes } from './MentionPill';
|
import type { MentionPillAttributes } from './MentionPill';
|
||||||
|
|
||||||
export type MentionOnSelectParams<T = unknown> = {
|
export type MentionOnSelectParams<T = unknown> = {
|
||||||
|
@ -86,6 +87,19 @@ export type MentionOnChangeParams = (
|
||||||
rawValue: string
|
rawValue: string
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
|
export type MentionInputProps = {
|
||||||
|
mentions: MentionSuggestionExtension[];
|
||||||
|
onChange: MentionOnChangeParams;
|
||||||
|
onFocus?: (v: EditorEvents['focus']) => void;
|
||||||
|
onBlur?: (v: EditorEvents['blur']) => void;
|
||||||
|
defaultValue?: string;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
className?: string;
|
||||||
|
readOnly?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
||||||
interface Storage {
|
interface Storage {
|
||||||
mention: {
|
mention: {
|
||||||
|
|
Loading…
Reference in New Issue