Skip to content
This repository has been archived by the owner on Nov 4, 2024. It is now read-only.

Commit

Permalink
feat(text-editor): improve command handling
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidRouyer committed Sep 13, 2023
1 parent 0000aa6 commit c3c0534
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 45 deletions.
100 changes: 55 additions & 45 deletions apps/customer-service/src/components/messages/message-form.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { FC } from 'react';
import { createContext, FC, RefObject, useRef } from 'react';
import { PaperclipIcon, SmilePlusIcon } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { Controller, FormProvider, useForm } from 'react-hook-form';
Expand All @@ -21,7 +21,11 @@ type MessageFormSchema = {
content: string;
};

export const FormElementContext =
createContext<RefObject<HTMLFormElement> | null>(null);

export const MessageForm: FC<{ ticketId: number }> = ({ ticketId }) => {
const formRef = useRef<HTMLFormElement>(null);
const session = useSession();
const utils = api.useContext();
const { mutateAsync: sendMessage } = api.message.create.useMutation({
Expand Down Expand Up @@ -90,55 +94,61 @@ export const MessageForm: FC<{ ticketId: number }> = ({ ticketId }) => {
};

return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="relative">
<div className="overflow-hidden rounded-lg shadow-sm ring-1 ring-inset ring-border focus-within:ring-2 focus-within:ring-foreground">
<Controller
name="content"
control={form.control}
render={({ field }) => <TextEditor {...field} />}
></Controller>
{/* Spacer element to match the height of the toolbar */}
<div className="py-2" aria-hidden="true">
{/* Matches height of button in toolbar (1px border + 36px content height) */}
<div className="py-px">
<div className="h-9" />
<FormElementContext.Provider value={formRef}>
<FormProvider {...form}>
<form
ref={formRef}
onSubmit={form.handleSubmit(onSubmit)}
className="relative"
>
<div className="overflow-hidden rounded-lg shadow-sm ring-1 ring-inset ring-border focus-within:ring-2 focus-within:ring-foreground">
<Controller
name="content"
control={form.control}
render={({ field }) => <TextEditor {...field} />}
></Controller>
{/* Spacer element to match the height of the toolbar */}
<div className="py-2" aria-hidden="true">
{/* Matches height of button in toolbar (1px border + 36px content height) */}
<div className="py-px">
<div className="h-9" />
</div>
</div>
</div>
</div>
<div className="absolute inset-x-0 bottom-0 flex justify-between py-2 pl-3 pr-2">
<div className="flex items-center space-x-5">
<div className="flex items-center">
<button
type="button"
className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500"
>
<PaperclipIcon className="h-5 w-5" aria-hidden="true" />
<span className="sr-only">
<FormattedMessage id="text_editor.attach_files" />
</span>
</button>
<div className="absolute inset-x-0 bottom-0 flex justify-between py-2 pl-3 pr-2">
<div className="flex items-center space-x-5">
<div className="flex items-center">
<button
type="button"
className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500"
>
<PaperclipIcon className="h-5 w-5" aria-hidden="true" />
<span className="sr-only">
<FormattedMessage id="text_editor.attach_files" />
</span>
</button>
</div>
<div className="flex items-center">
<button
type="button"
className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500"
>
<SmilePlusIcon className="h-5 w-5" aria-hidden="true" />
<span className="sr-only">
<FormattedMessage id="text_editor.add_emoticons" />
</span>
</button>
</div>
</div>
<div className="flex items-center">
<button
type="button"
className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500"
>
<SmilePlusIcon className="h-5 w-5" aria-hidden="true" />
<span className="sr-only">
<FormattedMessage id="text_editor.add_emoticons" />
</span>
</button>
<div className="shrink-0">
<Button type="submit" className="h-auto px-3">
<FormattedMessage id="text_editor.send" />
</Button>
</div>
</div>
<div className="shrink-0">
<Button type="submit" className="h-auto px-3">
<FormattedMessage id="text_editor.send" />
</Button>
</div>
</div>
</form>
</FormProvider>
</form>
</FormProvider>
</FormElementContext.Provider>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useContext, useEffect } from 'react';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { mergeRegister } from '@lexical/utils';
import { COMMAND_PRIORITY_LOW, KEY_ENTER_COMMAND } from 'lexical';

import { FormElementContext } from '~/components/messages/message-form';

// Lexical React plugins are React components, which makes them
// highly composable. Furthermore, you can lazy load plugins if
// desired, so you don't pay the cost for plugins until you
// actually use them.
export default function MyCustomCommandHandlerPlugin() {
const [editor] = useLexicalComposerContext();
const form = useContext(FormElementContext);

useEffect(() => {
return mergeRegister(
editor.registerCommand(
KEY_ENTER_COMMAND,
(event: KeyboardEvent) => {
// skipping if shift is pressed (defaulting to line-break)
if (event !== null && !event.ctrlKey) {
event.preventDefault();
form?.current?.requestSubmit();
return true;
}
return false;
},
COMMAND_PRIORITY_LOW
)
);
}, [editor]);

return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { FormattedMessage } from 'react-intl';
import editorConfig from '~/components/text-editor/editorConfig';
import EmoticonPlugin from '~/components/text-editor/plugins/emoticon-plugin';
import MyCustomAutoFocusPlugin from '~/components/text-editor/plugins/my-custom-auto-focus-plugin';
import MyCustomCommandHandlerPlugin from '~/components/text-editor/plugins/my-custom-command-handler-plugin';
import MyCustomOnChangePlugin from '~/components/text-editor/plugins/my-custom-on-change-plugin';
import MyCustomValuePlugin from '~/components/text-editor/plugins/my-custom-value-plugin';

Expand Down Expand Up @@ -40,6 +41,7 @@ export const TextEditor: FC<TextEditorProps> = ({ value, onChange }) => {
<EmoticonPlugin />
<ClearEditorPlugin />
<MyCustomAutoFocusPlugin />
<MyCustomCommandHandlerPlugin />
</div>
</LexicalComposer>
);
Expand Down

0 comments on commit c3c0534

Please sign in to comment.