From c3c05349e6d34381188daa59eff0020e34568694 Mon Sep 17 00:00:00 2001
From: David Rouyer <rouyer.david@gmail.com>
Date: Wed, 13 Sep 2023 15:03:12 +0200
Subject: [PATCH] feat(text-editor): improve command handling

---
 .../src/components/messages/message-form.tsx  | 100 ++++++++++--------
 .../my-custom-command-handler-plugin.ts       |  35 ++++++
 .../components/text-editor/text-editor.tsx    |   2 +
 3 files changed, 92 insertions(+), 45 deletions(-)
 create mode 100644 apps/customer-service/src/components/text-editor/plugins/my-custom-command-handler-plugin.ts

diff --git a/apps/customer-service/src/components/messages/message-form.tsx b/apps/customer-service/src/components/messages/message-form.tsx
index 9caa05996..da93ae2f5 100644
--- a/apps/customer-service/src/components/messages/message-form.tsx
+++ b/apps/customer-service/src/components/messages/message-form.tsx
@@ -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';
@@ -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({
@@ -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>
   );
 };
 
diff --git a/apps/customer-service/src/components/text-editor/plugins/my-custom-command-handler-plugin.ts b/apps/customer-service/src/components/text-editor/plugins/my-custom-command-handler-plugin.ts
new file mode 100644
index 000000000..e57003b19
--- /dev/null
+++ b/apps/customer-service/src/components/text-editor/plugins/my-custom-command-handler-plugin.ts
@@ -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;
+}
diff --git a/apps/customer-service/src/components/text-editor/text-editor.tsx b/apps/customer-service/src/components/text-editor/text-editor.tsx
index 31dde36e6..11cba050a 100644
--- a/apps/customer-service/src/components/text-editor/text-editor.tsx
+++ b/apps/customer-service/src/components/text-editor/text-editor.tsx
@@ -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';
 
@@ -40,6 +41,7 @@ export const TextEditor: FC<TextEditorProps> = ({ value, onChange }) => {
         <EmoticonPlugin />
         <ClearEditorPlugin />
         <MyCustomAutoFocusPlugin />
+        <MyCustomCommandHandlerPlugin />
       </div>
     </LexicalComposer>
   );