import * as React from 'react';
import {
I18nProvider,
required,
useGetManyReference,
useRecordContext,
TestMemoryRouter,
ResourceContextProvider,
} from 'ra-core';
import {
AdminContext,
Edit,
PrevNextButtons,
SimpleForm,
SimpleFormProps,
TopToolbar,
Toolbar as RAToolbar,
SaveButton,
} from 'ra-ui-materialui';
import { useWatch } from 'react-hook-form';
import fakeRestDataProvider from 'ra-data-fakerest';
import { Routes, Route } from 'react-router-dom';
import Mention from '@tiptap/extension-mention';
import { Editor, ReactRenderer } from '@tiptap/react';
import tippy, { Instance as TippyInstance } from 'tippy.js';
import {
DefaultEditorOptions,
RichTextInput,
RichTextInputProps,
} from './RichTextInput';
import { RichTextInputToolbar } from './RichTextInputToolbar';
import {
Box,
Button,
Card,
List,
ListItem,
ListItemButton,
ListItemText,
Paper,
} from '@mui/material';
import { FormatButtons } from './buttons';
export default { title: 'ra-input-rich-text/RichTextInput' };
const FormInspector = ({ name = 'body' }) => {
const value = useWatch({ name });
return (
({ backgroundColor: theme.palette.divider })}>
{name} value in form:
{JSON.stringify(value)} ({typeof value})
);
};
const i18nProvider: I18nProvider = {
translate: (key: string, options: any) => options?._ ?? key,
changeLocale: () => Promise.resolve(),
getLocale: () => 'en',
};
export const Basic = (props: Partial) => (
{}}
{...props}
>
);
export const Disabled = (props: Partial) => (
{}}
{...props}
>
);
export const ReadOnly = (props: Partial) => (
{}}
{...props}
>
);
export const Small = (props: Partial) => (
{}}
{...props}
>
}
label="Body"
source="body"
/>
);
export const Medium = (props: Partial) => (
{}}
{...props}
>
}
label="Body"
source="body"
/>
);
export const Large = (props: Partial) => (
{}}
{...props}
>
}
label="Body"
source="body"
/>
);
export const NotFullWidth = (props: Partial) => (
{}}
{...props}
>
}
label="Body"
source="body"
fullWidth={false}
/>
);
export const Sx = (props: Partial) => (
{}}
{...props}
>
);
export const Validation = (props: Partial) => (
{}} {...props}>
);
const MyRichTextInputToolbar = ({ ...props }) => {
return (
);
};
export const Toolbar = (props: Partial) => (
{}}
{...props}
>
}
/>
);
export const EditorReference = (props: Partial) => {
const editorRef = React.useRef(null);
const EditorToolbar = () => (
);
return (
}
onSubmit={() => {}}
{...props}
>
{
editorRef.current = editor;
},
}}
/>
);
};
const dataProvider = fakeRestDataProvider({
posts: [
{ id: 1, body: 'Post 1' },
{ id: 2, body: 'Post 2' },
{ id: 3, body: 'Post 3' },
],
tags: [
{ id: 1, name: 'tag1', post_id: 1 },
{ id: 2, name: 'tag2', post_id: 1 },
{ id: 3, name: 'tag3', post_id: 2 },
{ id: 4, name: 'tag4', post_id: 2 },
{ id: 5, name: 'tag5', post_id: 3 },
{ id: 6, name: 'tag6', post_id: 3 },
],
});
const MyRichTextInput = (props: RichTextInputProps) => {
const record = useRecordContext();
const tags = useGetManyReference('tags', {
target: 'post_id',
id: record.id,
});
const editorOptions = React.useMemo(() => {
return {
...DefaultEditorOptions,
extensions: [
...DefaultEditorOptions.extensions,
Mention.configure({
HTMLAttributes: {
class: 'mention',
},
suggestion: suggestions(tags.data?.map(t => t.name) ?? []),
}),
],
};
}, [tags.data]);
return ;
};
export const CustomOptions = () => (
}
>
}
/>
);
const MentionList = React.forwardRef<
MentionListRef,
{
items: string[];
command: (props: { id: string }) => void;
}
>((props, ref) => {
const [selectedIndex, setSelectedIndex] = React.useState(0);
const selectItem = index => {
const item = props.items[index];
if (item) {
props.command({ id: item });
}
};
const upHandler = () => {
setSelectedIndex(
(selectedIndex + props.items.length - 1) % props.items.length
);
};
const downHandler = () => {
setSelectedIndex((selectedIndex + 1) % props.items.length);
};
const enterHandler = () => {
selectItem(selectedIndex);
};
React.useEffect(() => setSelectedIndex(0), [props.items]);
React.useImperativeHandle(ref, () => ({
onKeyDown: ({ event }) => {
if (event.key === 'ArrowUp') {
upHandler();
return true;
}
if (event.key === 'ArrowDown') {
downHandler();
return true;
}
if (event.key === 'Enter') {
enterHandler();
return true;
}
return false;
},
}));
return (
{props.items.length ? (
props.items.map((item, index) => (
selectItem(index)}
>
{item}
))
) : (
No result
)}
);
});
type MentionListRef = {
onKeyDown: (props: { event: React.KeyboardEvent }) => boolean;
};
const suggestions = tags => {
return {
items: ({ query }) => {
return tags
.filter(item =>
item.toLowerCase().startsWith(query.toLowerCase())
)
.slice(0, 5);
},
render: () => {
let component: ReactRenderer;
let popup: TippyInstance[];
return {
onStart: props => {
component = new ReactRenderer(MentionList, {
props,
editor: props.editor,
});
if (!props.clientRect) {
return;
}
popup = tippy('body', {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'bottom-start',
});
},
onUpdate(props) {
if (component) {
component.updateProps(props);
}
if (!props.clientRect) {
return;
}
if (popup && popup[0]) {
popup[0].setProps({
getReferenceClientRect: props.clientRect,
});
}
},
onKeyDown(props) {
if (popup && popup[0] && props.event.key === 'Escape') {
popup[0].hide();
return true;
}
if (!component.ref) {
return false;
}
return component.ref.onKeyDown(props);
},
onExit() {
queueMicrotask(() => {
if (popup && popup[0] && !popup[0].state.isDestroyed) {
popup[0].destroy();
}
if (component) {
component.destroy();
}
// Remove references to the old popup and component upon destruction/exit.
// (This should prevent redundant calls to `popup.destroy()`, which Tippy
// warns in the console is a sign of a memory leak, as the `suggestion`
// plugin seems to call `onExit` both when a suggestion menu is closed after
// a user chooses an option, *and* when the editor itself is destroyed.)
popup = undefined;
component = undefined;
});
},
};
},
};
};