@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
JavaScript
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