UNPKG

@liveblocks/react-ui

Version:

A set of React pre-built components for the Liveblocks products. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.

196 lines (193 loc) 6.83 kB
import { jsx } from 'react/jsx-runtime'; import { useLayoutEffect } from '@liveblocks/react/_private'; import { Slot } from '@radix-ui/react-slot'; import { createContext, forwardRef, useRef, useState, useCallback, useImperativeHandle, useContext, useEffect, useMemo } from 'react'; import { createEditor, Transforms, Editor } from 'slate'; import { withHistory } from 'slate-history'; import { withReact, ReactEditor, Slate, Editable } from 'slate-react'; import { withNormalize } from '../slate/plugins/normalize.js'; import { isEmpty } from '../slate/utils/is-empty.js'; const AI_CHAT_COMPOSER_SUBMIT_NAME = "AiChatComposerSubmit"; const AI_CHAT_COMPOSER_EDITOR_NAME = "AiChatComposerEditor"; const AI_CHAT_COMPOSER_FORM_NAME = "AiChatComposerForm"; const AiChatComposerContext = createContext(null); const AiChatComposerForm = forwardRef( ({ onComposerSubmit, onSubmit, disabled, asChild, ...props }, forwardedRef) => { const Component = asChild ? Slot : "form"; const formRef = useRef(null); const editorRef = useRef(null); if (editorRef.current === null) { editorRef.current = withNormalize(withHistory(withReact(createEditor()))); } const editor = editorRef.current; const [isEditorEmpty, setIsEditorEmpty] = useState(true); const handleSubmit = useCallback( (event) => { if (disabled || isEmpty(editor, editor.children)) return; onSubmit?.(event); if (onComposerSubmit === void 0 || event.isDefaultPrevented()) { event.preventDefault(); return; } const content = editor.children.map((block) => { if ("type" in block && block.type === "paragraph") { return block.children.map((child) => { if ("text" in child) { return child.text; } return ""; }).join(""); } return ""; }).join("\n"); onComposerSubmit({ text: content }, event); if (event.isDefaultPrevented()) { return; } event.preventDefault(); Transforms.delete(editor, { at: { anchor: Editor.start(editor, []), focus: Editor.end(editor, []) } }); }, [disabled, editor, onSubmit, onComposerSubmit] ); useLayoutEffect(() => { setIsEditorEmpty(isEmpty(editor, editor.children)); }, [editor]); const handleEditorValueChange = useCallback(() => { setIsEditorEmpty(isEmpty(editor, editor.children)); }, [editor]); const requestFormSubmit = useCallback(() => { if (isEmpty(editor, editor.children)) return; requestAnimationFrame(() => { if (formRef.current === null) return; if (typeof formRef.current.requestSubmit === "function") { return formRef.current.requestSubmit(); } const submitter = document.createElement("input"); submitter.type = "submit"; submitter.hidden = true; formRef.current.appendChild(submitter); submitter.click(); formRef.current.removeChild(submitter); }); }, [editor]); useImperativeHandle( forwardedRef, () => formRef.current, [] ); return /* @__PURE__ */ jsx(AiChatComposerContext.Provider, { value: { editor, onEditorValueChange: handleEditorValueChange, isEditorEmpty, requestFormSubmit, disabled: disabled || false }, children: /* @__PURE__ */ jsx(Component, { onSubmit: handleSubmit, ...props, ref: formRef }) }); } ); const AiChatComposerEditor = forwardRef( ({ defaultValue = "", onKeyDown, disabled, autoFocus, ...props }, forwardedRef) => { const context = useContext(AiChatComposerContext); if (context === null) { throw new Error("AiChatComposer.Form is missing from the React tree."); } const { editor, onEditorValueChange, requestFormSubmit, disabled: isFormDisabled } = context; const handleKeyDown = useCallback( (event) => { onKeyDown?.(event); if (event.isDefaultPrevented()) return; if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); requestFormSubmit(); } else if (event.key === "Enter" && event.shiftKey) { event.preventDefault(); editor.insertBreak(); } }, [editor, onKeyDown, requestFormSubmit] ); useImperativeHandle( forwardedRef, () => ReactEditor.toDOMNode(editor, editor), [editor] ); useEffect(() => { if (!autoFocus) return; try { if (!ReactEditor.isFocused(editor)) { Transforms.select(editor, Editor.end(editor, [])); ReactEditor.focus(editor); } } catch { } }, [editor, autoFocus]); const initialValue = useMemo(() => { return defaultValue.split("\n").map((text) => ({ type: "paragraph", children: [{ text }] })); }, [defaultValue]); return /* @__PURE__ */ jsx(Slate, { editor, initialValue, onValueChange: onEditorValueChange, children: /* @__PURE__ */ jsx(Editable, { enterKeyHint: "send", autoCapitalize: "sentences", onKeyDown: handleKeyDown, "data-disabled": disabled || isFormDisabled || void 0, ...props, readOnly: disabled || isFormDisabled, disabled: disabled || isFormDisabled, renderPlaceholder: function({ attributes, children }) { const { opacity: _opacity, ...style } = attributes.style; return /* @__PURE__ */ jsx("span", { ...attributes, style, "data-placeholder": "", children }); } }) }); } ); const AiChatComposerSubmit = forwardRef(({ disabled, asChild, ...props }, forwardedRef) => { const Component = asChild ? Slot : "button"; const context = useContext(AiChatComposerContext); if (context === null) { throw new Error("AiChatComposer.Form is missing from the React tree."); } const { disabled: isFormDisabled, isEditorEmpty } = context; return /* @__PURE__ */ jsx(Component, { type: "submit", ...props, ref: forwardedRef, disabled: disabled || isFormDisabled || isEditorEmpty }); }); if (process.env.NODE_ENV !== "production") { AiChatComposerEditor.displayName = AI_CHAT_COMPOSER_EDITOR_NAME; AiChatComposerForm.displayName = AI_CHAT_COMPOSER_FORM_NAME; AiChatComposerSubmit.displayName = AI_CHAT_COMPOSER_SUBMIT_NAME; } export { AiChatComposerEditor, AiChatComposerForm, AiChatComposerSubmit, AiChatComposerEditor as Editor, AiChatComposerForm as Form, AiChatComposerSubmit as Submit }; //# sourceMappingURL=index.js.map