Browse Source

feat(admin-ui): Add React RichTextEditor component & hook (#2675)

Aleksander Bondar 1 year ago
parent
commit
68e0fa529f

+ 46 - 0
docs/docs/reference/admin-ui-api/react-components/rich-text-editor.md

@@ -0,0 +1,46 @@
+---
+title: "RichTextEditor"
+isDefaultIndex: false
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+import MemberInfo from '@site/src/components/MemberInfo';
+import GenerationInfo from '@site/src/components/GenerationInfo';
+import MemberDescription from '@site/src/components/MemberDescription';
+
+
+## RichTextEditor
+
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/react/src/react-components/RichTextEditor.tsx" sourceLine="59" packageName="@vendure/admin-ui" />
+
+A rich text editor component which uses ProseMirror (rich text editor) under the hood.
+
+*Example*
+
+```ts
+import { RichTextEditor } from '@vendure/admin-ui/react';
+import React from 'react';
+
+export function MyComponent() {
+  const onSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    const form = new FormData(e.target as HTMLFormElement);
+    const content = form.get("content");
+    console.log(content);
+  };
+
+  return (
+    <form className="w-full" onSubmit={onSubmit}>
+      <RichTextEditor
+        name="content"
+        readOnly={false}
+        onMount={(e) => console.log("Mounted", e)}
+      />
+      <button type="submit" className="btn btn-primary">
+        Submit
+      </button>
+    </form>
+  );
+}
+```
+

+ 34 - 0
docs/docs/reference/admin-ui-api/react-hooks/use-rich-text-editor.md

@@ -0,0 +1,34 @@
+---
+title: "UseRichTextEditor"
+isDefaultIndex: false
+generated: true
+---
+<!-- This file was generated from the Vendure source. Do not modify. Instead, re-run the "docs:build" script -->
+import MemberInfo from '@site/src/components/MemberInfo';
+import GenerationInfo from '@site/src/components/GenerationInfo';
+import MemberDescription from '@site/src/components/MemberDescription';
+
+
+## useRichTextEditor
+
+<GenerationInfo sourceFile="packages/admin-ui/src/lib/react/src/react-hooks/use-rich-text-editor.ts" sourceLine="40" packageName="@vendure/admin-ui" />
+
+Provides access to the ProseMirror (rich text editor) instance.
+
+*Example*
+
+```ts
+import { useRichTextEditor } from '@vendure/admin-ui/react';
+import React from 'react';
+
+export function Component() {
+    const { ref, editor } = useRichTextEditor({
+       attributes: { class: '' },
+       onTextInput: (text) => console.log(text),
+       isReadOnly: () => false,
+    });
+
+    return <div className="w-full" ref={ref} />
+}
+```
+

+ 93 - 0
packages/admin-ui/src/lib/react/src/react-components/RichTextEditor.tsx

@@ -0,0 +1,93 @@
+import React, {
+    ChangeEvent,
+    ForwardedRef,
+    InputHTMLAttributes,
+    forwardRef,
+    useEffect,
+    useState,
+} from 'react';
+import { ProsemirrorService } from '@vendure/admin-ui/core';
+import { useRichTextEditor } from '../react-hooks/use-rich-text-editor';
+
+export type RichTextEditorType = InputHTMLAttributes<HTMLInputElement> & {
+    /**
+     * @description
+     * Control the DOM attributes of the editable element. May be either an object or a function going from an editor state to an object.
+     * By default, the element will get a class "ProseMirror", and will have its contentEditable attribute determined by the editable prop.
+     * Additional classes provided here will be added to the class. For other attributes, the value provided first (as in someProp) will be used.
+     * Copied from real property description.
+     */
+    attributes?: Record<string, string>;
+    readOnly?: boolean;
+    onMount?: (editor: ProsemirrorService) => void;
+};
+
+/**
+ * @description
+ * A rich text editor component which uses ProseMirror (rich text editor) under the hood.
+ *
+ * @example
+ * ```ts
+ * import { RichTextEditor } from '@vendure/admin-ui/react';
+ * import React from 'react';
+ *
+ * export function MyComponent() {
+ *   const onSubmit = async (e: React.FormEvent) => {
+ *     e.preventDefault();
+ *     const form = new FormData(e.target as HTMLFormElement);
+ *     const content = form.get("content");
+ *     console.log(content);
+ *   };
+ *
+ *   return (
+ *     <form className="w-full" onSubmit={onSubmit}>
+ *       <RichTextEditor
+ *         name="content"
+ *         readOnly={false}
+ *         onMount={(e) => console.log("Mounted", e)}
+ *       />
+ *       <button type="submit" className="btn btn-primary">
+ *         Submit
+ *       </button>
+ *     </form>
+ *   );
+ * }
+ * ```
+ *
+ * @docsCategory react-components
+ */
+export const RichTextEditor = forwardRef((props: RichTextEditorType, ref: ForwardedRef<HTMLInputElement>) => {
+    const [data, setData] = useState<string>('');
+    const { readOnly, ...rest } = props;
+    const { ref: _ref, editor } = useRichTextEditor({
+        attributes: props.attributes,
+        isReadOnly: () => readOnly || false,
+        onTextInput: text => {
+            setData(text);
+            if (props.onChange) {
+                props.onChange({
+                    target: { value: text },
+                } as ChangeEvent<HTMLInputElement>);
+            }
+            if (ref && 'current' in ref && ref.current) {
+                ref.current.value = text;
+                const event = new Event('input', {
+                    bubbles: true,
+                    cancelable: true,
+                });
+                ref.current.dispatchEvent(event);
+            }
+        },
+    });
+    useEffect(() => {
+        if (props.onMount && editor) props.onMount(editor);
+    }, []);
+    return (
+        <>
+            <div ref={_ref} {...rest} />
+            <input type="hidden" value={data} ref={ref} />
+        </>
+    );
+});
+
+RichTextEditor.displayName = 'RichTextEditor';

+ 63 - 0
packages/admin-ui/src/lib/react/src/react-hooks/use-rich-text-editor.ts

@@ -0,0 +1,63 @@
+import { useEffect, useRef } from 'react';
+import { Injector } from '@angular/core';
+
+import { CreateEditorViewOptions, ProsemirrorService, ContextMenuService } from '@vendure/admin-ui/core';
+import { useInjector } from './use-injector';
+
+export interface useRichTextEditorOptions extends Omit<CreateEditorViewOptions, 'element'> {
+    /**
+     * @description
+     * Control the DOM attributes of the editable element. May be either an object or a function going from an editor state to an object.
+     * By default, the element will get a class "ProseMirror", and will have its contentEditable attribute determined by the editable prop.
+     * Additional classes provided here will be added to the class. For other attributes, the value provided first (as in someProp) will be used.
+     * Copied from real property description.
+     */
+    attributes?: Record<string, string>;
+}
+
+/**
+ * @description
+ * Provides access to the ProseMirror (rich text editor) instance.
+ *
+ * @example
+ * ```ts
+ * import { useRichTextEditor } from '\@vendure/admin-ui/react';
+ * import React from 'react';
+ *
+ * export function Component() {
+ *     const { ref, editor } = useRichTextEditor({
+ *        attributes: { class: '' },
+ *        onTextInput: (text) => console.log(text),
+ *        isReadOnly: () => false,
+ *     });
+ *
+ *     return <div className="w-full" ref={ref} />
+ * }
+ * ```
+ *
+ * @docsCategory react-hooks
+ */
+export const useRichTextEditor = ({ attributes, onTextInput, isReadOnly }: useRichTextEditorOptions) => {
+    const injector = useInjector(Injector);
+    const ref = useRef<HTMLDivElement>(null);
+    const prosemirror = new ProsemirrorService(injector, useInjector(ContextMenuService));
+
+    useEffect(() => {
+        if (!ref.current) return;
+        prosemirror.createEditorView({
+            element: ref.current,
+            isReadOnly,
+            onTextInput,
+        });
+        const readOnly = isReadOnly();
+        prosemirror.editorView.setProps({
+            attributes,
+            editable: readOnly ? () => false : () => true,
+        });
+        return () => {
+            prosemirror.destroy();
+        };
+    }, [ref.current]);
+
+    return { ref, editor: prosemirror };
+};