@liveblocks/react-ui 2.7.0-beta1 → 2.7.0-beta3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Comment.js +53 -8
- package/dist/components/Comment.js.map +1 -1
- package/dist/components/Comment.mjs +54 -9
- package/dist/components/Comment.mjs.map +1 -1
- package/dist/components/Composer.js +13 -16
- package/dist/components/Composer.js.map +1 -1
- package/dist/components/Composer.mjs +14 -17
- package/dist/components/Composer.mjs.map +1 -1
- package/dist/components/InboxNotification.js +4 -1
- package/dist/components/InboxNotification.js.map +1 -1
- package/dist/components/InboxNotification.mjs +4 -1
- package/dist/components/InboxNotification.mjs.map +1 -1
- package/dist/components/internal/Attachment.js +161 -74
- package/dist/components/internal/Attachment.js.map +1 -1
- package/dist/components/internal/Attachment.mjs +161 -76
- package/dist/components/internal/Attachment.mjs.map +1 -1
- package/dist/components/internal/InboxNotificationThread.js +5 -3
- package/dist/components/internal/InboxNotificationThread.js.map +1 -1
- package/dist/components/internal/InboxNotificationThread.mjs +5 -3
- package/dist/components/internal/InboxNotificationThread.mjs.map +1 -1
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/primitives/Composer/contexts.js.map +1 -1
- package/dist/primitives/Composer/contexts.mjs.map +1 -1
- package/dist/primitives/Composer/index.js +33 -26
- package/dist/primitives/Composer/index.js.map +1 -1
- package/dist/primitives/Composer/index.mjs +33 -26
- package/dist/primitives/Composer/index.mjs.map +1 -1
- package/dist/primitives/Composer/utils.js +73 -66
- package/dist/primitives/Composer/utils.js.map +1 -1
- package/dist/primitives/Composer/utils.mjs +73 -66
- package/dist/primitives/Composer/utils.mjs.map +1 -1
- package/dist/primitives/index.d.mts +4 -0
- package/dist/primitives/index.d.ts +4 -0
- package/dist/slate/plugins/{paste-html.js → paste.js} +11 -3
- package/dist/slate/plugins/paste.js.map +1 -0
- package/dist/slate/plugins/{paste-html.mjs → paste.mjs} +11 -3
- package/dist/slate/plugins/paste.mjs.map +1 -0
- package/dist/utils/data-transfer.js +14 -0
- package/dist/utils/data-transfer.js.map +1 -0
- package/dist/utils/data-transfer.mjs +12 -0
- package/dist/utils/data-transfer.mjs.map +1 -0
- package/dist/utils/use-latest.js.map +1 -1
- package/dist/utils/use-latest.mjs.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.mjs +1 -1
- package/package.json +4 -4
- package/src/styles/index.css +91 -44
- package/styles.css +1 -1
- package/styles.css.map +1 -1
- package/dist/slate/plugins/paste-html.js.map +0 -1
- package/dist/slate/plugins/paste-html.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contexts.mjs","sources":["../../../src/primitives/Composer/contexts.ts"],"sourcesContent":["import type { Placement } from \"@floating-ui/react-dom\";\nimport type { CommentMixedAttachment } from \"@liveblocks/core\";\nimport { nn } from \"@liveblocks/core\";\nimport type { Direction } from \"@radix-ui/react-dropdown-menu\";\nimport type { Dispatch, Ref, SetStateAction } from \"react\";\nimport { createContext, useContext } from \"react\";\nimport type { Editor as SlateEditor, Element as SlateElement } from \"slate\";\n\nexport type ComposerContext = {\n /**\n * Whether the composer is currently disabled.\n */\n isDisabled: boolean;\n\n /**\n * Whether the composer can currently be submitted.\n */\n canSubmit: boolean;\n\n /**\n * Whether the editor is currently focused.\n */\n isFocused: boolean;\n\n /**\n * Whether the editor is currently empty.\n */\n isEmpty: boolean;\n\n /**\n * Submit the composer programmatically.\n */\n submit: () => void;\n\n /**\n * Clear the composer programmatically.\n */\n clear: () => void;\n\n /**\n * Select the editor programmatically.\n */\n select: () => void;\n\n /**\n * Focus the editor programmatically.\n */\n focus: () => void;\n\n /**\n * Blur the editor programmatically.\n */\n blur: () => void;\n\n /**\n * Start creating a mention at the current selection.\n */\n createMention: () => void;\n\n /**\n * Insert text at the current selection.\n */\n insertText: (text: string) => void;\n\n /**\n * Open a file picker programmatically to create attachments.\n */\n attachFiles: () => void;\n\n /**\n * The composer's current attachments.\n */\n attachments: CommentMixedAttachment[];\n\n /**\n * Remove an attachment by its ID.\n */\n removeAttachment: (attachmentId: string) => void;\n};\n\nexport type ComposerEditorContext = {\n validate: (value: SlateElement[]) => void;\n editor: SlateEditor;\n setFocused: Dispatch<SetStateAction<boolean>>;\n};\n\nexport type ComposerAttachmentsContext = {\n
|
|
1
|
+
{"version":3,"file":"contexts.mjs","sources":["../../../src/primitives/Composer/contexts.ts"],"sourcesContent":["import type { Placement } from \"@floating-ui/react-dom\";\nimport type { CommentMixedAttachment } from \"@liveblocks/core\";\nimport { nn } from \"@liveblocks/core\";\nimport type { Direction } from \"@radix-ui/react-dropdown-menu\";\nimport type { Dispatch, Ref, SetStateAction } from \"react\";\nimport { createContext, useContext } from \"react\";\nimport type { Editor as SlateEditor, Element as SlateElement } from \"slate\";\n\nexport type ComposerContext = {\n /**\n * Whether the composer is currently disabled.\n */\n isDisabled: boolean;\n\n /**\n * Whether the composer can currently be submitted.\n */\n canSubmit: boolean;\n\n /**\n * Whether the editor is currently focused.\n */\n isFocused: boolean;\n\n /**\n * Whether the editor is currently empty.\n */\n isEmpty: boolean;\n\n /**\n * Submit the composer programmatically.\n */\n submit: () => void;\n\n /**\n * Clear the composer programmatically.\n */\n clear: () => void;\n\n /**\n * Select the editor programmatically.\n */\n select: () => void;\n\n /**\n * Focus the editor programmatically.\n */\n focus: () => void;\n\n /**\n * Blur the editor programmatically.\n */\n blur: () => void;\n\n /**\n * Start creating a mention at the current selection.\n */\n createMention: () => void;\n\n /**\n * Insert text at the current selection.\n */\n insertText: (text: string) => void;\n\n /**\n * Open a file picker programmatically to create attachments.\n */\n attachFiles: () => void;\n\n /**\n * The composer's current attachments.\n */\n attachments: CommentMixedAttachment[];\n\n /**\n * Remove an attachment by its ID.\n */\n removeAttachment: (attachmentId: string) => void;\n};\n\nexport type ComposerEditorContext = {\n validate: (value: SlateElement[]) => void;\n editor: SlateEditor;\n setFocused: Dispatch<SetStateAction<boolean>>;\n};\n\nexport type ComposerAttachmentsContext = {\n hasMaxAttachments: boolean;\n createAttachments: (files: File[]) => void;\n isUploadingAttachments: boolean;\n maxAttachments: number;\n maxAttachmentSize: number;\n};\n\nexport type ComposerSuggestionsContext = {\n dir?: Direction;\n id: string;\n itemId: (value?: string) => string | undefined;\n placement: Placement;\n selectedValue?: string;\n setSelectedValue: (value: string) => void;\n onItemSelect: (value: string) => void;\n ref: Ref<HTMLDivElement>;\n};\n\nexport const ComposerContext = createContext<ComposerContext | null>(null);\nexport const ComposerEditorContext =\n createContext<ComposerEditorContext | null>(null);\nexport const ComposerAttachmentsContext =\n createContext<ComposerAttachmentsContext | null>(null);\nexport const ComposerSuggestionsContext =\n createContext<ComposerSuggestionsContext | null>(null);\n\nexport function useComposerEditorContext() {\n const composerEditorContext = useContext(ComposerEditorContext);\n\n return nn(\n composerEditorContext,\n \"Composer.Form is missing from the React tree.\"\n );\n}\n\nexport function useComposerAttachmentsContextOrNull() {\n return useContext(ComposerAttachmentsContext);\n}\n\nexport function useComposerAttachmentsContext() {\n const composerAttachmentsContext = useComposerAttachmentsContextOrNull();\n\n return nn(\n composerAttachmentsContext,\n \"Composer.Form is missing from the React tree.\"\n );\n}\n\nexport function useComposerSuggestionsContext(\n source = \"useComposerSuggestionsContext\"\n) {\n const composerSuggestionsContext = useContext(ComposerSuggestionsContext);\n\n return nn(\n composerSuggestionsContext,\n `${source} can’t be used outside of Composer.Editor.`\n );\n}\n\nexport function useComposer(): ComposerContext {\n const composerContext = useContext(ComposerContext);\n\n return nn(composerContext, \"Composer.Form is missing from the React tree.\");\n}\n"],"names":[],"mappings":";;;AAyGa,MAAA,eAAA,GAAkB,cAAsC,IAAI,EAAA;AAC5D,MAAA,qBAAA,GACX,cAA4C,IAAI,EAAA;AACrC,MAAA,0BAAA,GACX,cAAiD,IAAI,EAAA;AAC1C,MAAA,0BAAA,GACX,cAAiD,IAAI,EAAA;AAEhD,SAAS,wBAA2B,GAAA;AACzC,EAAM,MAAA,qBAAA,GAAwB,WAAW,qBAAqB,CAAA,CAAA;AAE9D,EAAO,OAAA,EAAA;AAAA,IACL,qBAAA;AAAA,IACA,+CAAA;AAAA,GACF,CAAA;AACF,CAAA;AAEO,SAAS,mCAAsC,GAAA;AACpD,EAAA,OAAO,WAAW,0BAA0B,CAAA,CAAA;AAC9C,CAAA;AAEO,SAAS,6BAAgC,GAAA;AAC9C,EAAA,MAAM,6BAA6B,mCAAoC,EAAA,CAAA;AAEvE,EAAO,OAAA,EAAA;AAAA,IACL,0BAAA;AAAA,IACA,+CAAA;AAAA,GACF,CAAA;AACF,CAAA;AAEgB,SAAA,6BAAA,CACd,SAAS,+BACT,EAAA;AACA,EAAM,MAAA,0BAAA,GAA6B,WAAW,0BAA0B,CAAA,CAAA;AAExE,EAAO,OAAA,EAAA;AAAA,IACL,0BAAA;AAAA,IACA,CAAG,EAAA,MAAA,CAAA,+CAAA,CAAA;AAAA,GACL,CAAA;AACF,CAAA;AAEO,SAAS,WAA+B,GAAA;AAC7C,EAAM,MAAA,eAAA,GAAkB,WAAW,eAAe,CAAA,CAAA;AAElD,EAAO,OAAA,EAAA,CAAG,iBAAiB,+CAA+C,CAAA,CAAA;AAC5E;;;;"}
|
|
@@ -16,7 +16,7 @@ var autoLinks = require('../../slate/plugins/auto-links.js');
|
|
|
16
16
|
var customLinks = require('../../slate/plugins/custom-links.js');
|
|
17
17
|
var emptyClearFormatting = require('../../slate/plugins/empty-clear-formatting.js');
|
|
18
18
|
var mentions = require('../../slate/plugins/mentions.js');
|
|
19
|
-
var
|
|
19
|
+
var paste = require('../../slate/plugins/paste.js');
|
|
20
20
|
var getDomRange = require('../../slate/utils/get-dom-range.js');
|
|
21
21
|
var isEmpty = require('../../slate/utils/is-empty.js');
|
|
22
22
|
var marks = require('../../slate/utils/marks.js');
|
|
@@ -48,13 +48,19 @@ const emptyCommentBody = {
|
|
|
48
48
|
version: 1,
|
|
49
49
|
content: [{ type: "paragraph", children: [{ text: "" }] }]
|
|
50
50
|
};
|
|
51
|
-
function createComposerEditor(
|
|
51
|
+
function createComposerEditor({
|
|
52
|
+
createAttachments,
|
|
53
|
+
pasteFilesAsAttachments
|
|
54
|
+
}) {
|
|
52
55
|
return mentions.withMentions(
|
|
53
56
|
customLinks.withCustomLinks(
|
|
54
57
|
autoLinks.withAutoLinks(
|
|
55
58
|
autoFormatting.withAutoFormatting(
|
|
56
59
|
emptyClearFormatting.withEmptyClearFormatting(
|
|
57
|
-
|
|
60
|
+
paste.withPaste(slateHistory.withHistory(slateReact.withReact(slate.createEditor())), {
|
|
61
|
+
createAttachments,
|
|
62
|
+
pasteFilesAsAttachments
|
|
63
|
+
})
|
|
58
64
|
)
|
|
59
65
|
)
|
|
60
66
|
)
|
|
@@ -637,12 +643,12 @@ const ComposerForm = React.forwardRef(
|
|
|
637
643
|
onSubmit,
|
|
638
644
|
onComposerSubmit,
|
|
639
645
|
defaultAttachments = [],
|
|
646
|
+
pasteFilesAsAttachments,
|
|
640
647
|
disabled,
|
|
641
648
|
asChild,
|
|
642
649
|
...props
|
|
643
650
|
}, forwardedRef) => {
|
|
644
651
|
const Component = asChild ? reactSlot.Slot : "form";
|
|
645
|
-
const editor = useInitial.useInitial(createComposerEditor);
|
|
646
652
|
const room = react.useRoom();
|
|
647
653
|
const [isEmpty$1, setEmpty] = React.useState(true);
|
|
648
654
|
const [isSubmitting, setSubmitting] = React.useState(false);
|
|
@@ -652,14 +658,14 @@ const ComposerForm = React.forwardRef(
|
|
|
652
658
|
const {
|
|
653
659
|
attachments,
|
|
654
660
|
isUploadingAttachments,
|
|
655
|
-
|
|
661
|
+
addAttachments,
|
|
656
662
|
removeAttachment,
|
|
657
663
|
clearAttachments
|
|
658
664
|
} = utils.useComposerAttachmentsManager(defaultAttachments, {
|
|
659
665
|
maxFileSize: maxAttachmentSize
|
|
660
666
|
});
|
|
661
667
|
const numberOfAttachments = attachments.length;
|
|
662
|
-
const
|
|
668
|
+
const hasMaxAttachments = numberOfAttachments >= maxAttachments;
|
|
663
669
|
const isDisabled = React.useMemo(() => {
|
|
664
670
|
const self = room.getSelf();
|
|
665
671
|
const canComment = self?.canComment ?? true;
|
|
@@ -671,6 +677,24 @@ const ComposerForm = React.forwardRef(
|
|
|
671
677
|
const ref = React.useRef(null);
|
|
672
678
|
const mergedRefs = useRefs.useRefs(forwardedRef, ref);
|
|
673
679
|
const fileInputRef = React.useRef(null);
|
|
680
|
+
const createAttachments = React.useCallback(
|
|
681
|
+
(files) => {
|
|
682
|
+
if (!files.length) {
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const numberOfAcceptedFiles = Math.max(
|
|
686
|
+
0,
|
|
687
|
+
maxAttachments - numberOfAttachments
|
|
688
|
+
);
|
|
689
|
+
files.splice(numberOfAcceptedFiles);
|
|
690
|
+
const attachments2 = files.map((file) => room.prepareAttachment(file));
|
|
691
|
+
addAttachments(attachments2);
|
|
692
|
+
},
|
|
693
|
+
[addAttachments, maxAttachments, numberOfAttachments, room]
|
|
694
|
+
);
|
|
695
|
+
const editor = useInitial.useInitial(
|
|
696
|
+
() => createComposerEditor({ createAttachments, pasteFilesAsAttachments })
|
|
697
|
+
);
|
|
674
698
|
const validate = React.useCallback(
|
|
675
699
|
(value) => {
|
|
676
700
|
setEmpty(isEmpty.isEmpty(editor, value));
|
|
@@ -727,23 +751,6 @@ const ComposerForm = React.forwardRef(
|
|
|
727
751
|
},
|
|
728
752
|
[editor, focus]
|
|
729
753
|
);
|
|
730
|
-
const createAttachments = React.useCallback(
|
|
731
|
-
(files) => {
|
|
732
|
-
if (!files.length) {
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
735
|
-
const numberOfAcceptedFiles = Math.max(
|
|
736
|
-
0,
|
|
737
|
-
maxAttachments - numberOfAttachments
|
|
738
|
-
);
|
|
739
|
-
files.splice(numberOfAcceptedFiles);
|
|
740
|
-
for (const file of files) {
|
|
741
|
-
const attachment = room.prepareAttachment(file);
|
|
742
|
-
addAttachment(attachment);
|
|
743
|
-
}
|
|
744
|
-
},
|
|
745
|
-
[addAttachment, maxAttachments, numberOfAttachments, room]
|
|
746
|
-
);
|
|
747
754
|
const attachFiles = React.useCallback(() => {
|
|
748
755
|
if (fileInputRef.current) {
|
|
749
756
|
fileInputRef.current.click();
|
|
@@ -813,7 +820,7 @@ const ComposerForm = React.forwardRef(
|
|
|
813
820
|
value: {
|
|
814
821
|
createAttachments,
|
|
815
822
|
isUploadingAttachments,
|
|
816
|
-
|
|
823
|
+
hasMaxAttachments,
|
|
817
824
|
maxAttachments,
|
|
818
825
|
maxAttachmentSize
|
|
819
826
|
}
|
|
@@ -863,9 +870,9 @@ const ComposerSubmit = React.forwardRef(
|
|
|
863
870
|
);
|
|
864
871
|
const ComposerAttachFiles = React.forwardRef(({ children, onClick, disabled, asChild, ...props }, forwardedRef) => {
|
|
865
872
|
const Component = asChild ? reactSlot.Slot : "button";
|
|
866
|
-
const {
|
|
873
|
+
const { hasMaxAttachments } = contexts.useComposerAttachmentsContext();
|
|
867
874
|
const { isDisabled: isComposerDisabled, attachFiles } = contexts.useComposer();
|
|
868
|
-
const isDisabled = isComposerDisabled ||
|
|
875
|
+
const isDisabled = isComposerDisabled || hasMaxAttachments || disabled;
|
|
869
876
|
const handleClick = React.useCallback(
|
|
870
877
|
(event) => {
|
|
871
878
|
onClick?.(event);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../../src/primitives/Composer/index.tsx"],"sourcesContent":["\"use client\";\n\nimport type {\n DetectOverflowOptions,\n UseFloatingOptions,\n} from \"@floating-ui/react-dom\";\nimport {\n autoUpdate,\n flip,\n hide,\n limitShift,\n shift,\n size,\n useFloating,\n} from \"@floating-ui/react-dom\";\nimport type { CommentAttachment, CommentBody } from \"@liveblocks/core\";\nimport { useRoom } from \"@liveblocks/react\";\nimport { Slot, Slottable } from \"@radix-ui/react-slot\";\nimport type {\n AriaAttributes,\n ChangeEvent,\n FocusEvent,\n FormEvent,\n KeyboardEvent,\n MouseEvent,\n PointerEvent,\n} from \"react\";\nimport React, {\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport type {\n Descendant as SlateDescendant,\n Element as SlateElement,\n} from \"slate\";\nimport {\n createEditor,\n Editor as SlateEditor,\n insertText as insertSlateText,\n Transforms as SlateTransforms,\n} from \"slate\";\nimport { withHistory } from \"slate-history\";\nimport type {\n RenderElementProps,\n RenderElementSpecificProps,\n RenderLeafProps,\n RenderPlaceholderProps,\n} from \"slate-react\";\nimport {\n Editable,\n ReactEditor,\n Slate,\n useSelected,\n useSlateStatic,\n withReact,\n} from \"slate-react\";\n\nimport { useLiveblocksUIConfig } from \"../../config\";\nimport { FLOATING_ELEMENT_COLLISION_PADDING } from \"../../constants\";\nimport { useMentionSuggestions } from \"../../shared\";\nimport { withAutoFormatting } from \"../../slate/plugins/auto-formatting\";\nimport { withAutoLinks } from \"../../slate/plugins/auto-links\";\nimport { withCustomLinks } from \"../../slate/plugins/custom-links\";\nimport { withEmptyClearFormatting } from \"../../slate/plugins/empty-clear-formatting\";\nimport type { MentionDraft } from \"../../slate/plugins/mentions\";\nimport {\n getMentionDraftAtSelection,\n insertMention,\n insertMentionCharacter,\n MENTION_CHARACTER,\n withMentions,\n} from \"../../slate/plugins/mentions\";\nimport { withPasteHtml } from \"../../slate/plugins/paste-html\";\nimport { getDOMRange } from \"../../slate/utils/get-dom-range\";\nimport { isEmpty as isEditorEmpty } from \"../../slate/utils/is-empty\";\nimport { leaveMarkEdge, toggleMark } from \"../../slate/utils/marks\";\nimport type {\n ComposerBody as ComposerBodyData,\n ComposerBodyAutoLink,\n ComposerBodyCustomLink,\n ComposerBodyMention,\n} from \"../../types\";\nimport { isKey } from \"../../utils/is-key\";\nimport { Persist, useAnimationPersist, usePersist } from \"../../utils/Persist\";\nimport { Portal } from \"../../utils/Portal\";\nimport { requestSubmit } from \"../../utils/request-submit\";\nimport { useId } from \"../../utils/use-id\";\nimport { useIndex } from \"../../utils/use-index\";\nimport { useInitial } from \"../../utils/use-initial\";\nimport { useLayoutEffect } from \"../../utils/use-layout-effect\";\nimport { useRefs } from \"../../utils/use-refs\";\nimport { toAbsoluteUrl } from \"../Comment/utils\";\nimport {\n ComposerAttachmentsContext,\n ComposerContext,\n ComposerEditorContext,\n ComposerSuggestionsContext,\n useComposer,\n useComposerAttachmentsContext,\n useComposerEditorContext,\n useComposerSuggestionsContext,\n} from \"./contexts\";\nimport type {\n ComposerAttachFilesProps,\n ComposerAttachmentsDropAreaProps,\n ComposerEditorComponents,\n ComposerEditorElementProps,\n ComposerEditorLinkWrapperProps,\n ComposerEditorMentionSuggestionsWrapperProps,\n ComposerEditorMentionWrapperProps,\n ComposerEditorProps,\n ComposerFormProps,\n ComposerLinkProps,\n ComposerMentionProps,\n ComposerSubmitProps,\n ComposerSuggestionsListItemProps,\n ComposerSuggestionsListProps,\n ComposerSuggestionsProps,\n SuggestionsPosition,\n} from \"./types\";\nimport {\n commentBodyToComposerBody,\n composerBodyToCommentBody,\n getPlacementFromPosition,\n getSideAndAlignFromPlacement,\n useComposerAttachmentsDropArea,\n useComposerAttachmentsManager,\n} from \"./utils\";\n\nconst MENTION_SUGGESTIONS_POSITION: SuggestionsPosition = \"top\";\n\nconst COMPOSER_MENTION_NAME = \"ComposerMention\";\nconst COMPOSER_LINK_NAME = \"ComposerLink\";\nconst COMPOSER_SUGGESTIONS_NAME = \"ComposerSuggestions\";\nconst COMPOSER_SUGGESTIONS_LIST_NAME = \"ComposerSuggestionsList\";\nconst COMPOSER_SUGGESTIONS_LIST_ITEM_NAME = \"ComposerSuggestionsListItem\";\nconst COMPOSER_SUBMIT_NAME = \"ComposerSubmit\";\nconst COMPOSER_EDITOR_NAME = \"ComposerEditor\";\nconst COMPOSER_ATTACH_FILES_NAME = \"ComposerAttachFiles\";\nconst COMPOSER_ATTACHMENTS_DROP_AREA_NAME = \"ComposerAttachmentsDropArea\";\nconst COMPOSER_FORM_NAME = \"ComposerForm\";\n\nconst emptyCommentBody: CommentBody = {\n version: 1,\n content: [{ type: \"paragraph\", children: [{ text: \"\" }] }],\n};\n\nfunction createComposerEditor() {\n return withMentions(\n withCustomLinks(\n withAutoLinks(\n withAutoFormatting(\n withEmptyClearFormatting(\n withPasteHtml(withHistory(withReact(createEditor())))\n )\n )\n )\n )\n );\n}\n\nfunction ComposerEditorMentionWrapper({\n Mention,\n attributes,\n children,\n element,\n}: ComposerEditorMentionWrapperProps) {\n const isSelected = useSelected();\n\n return (\n <span {...attributes}>\n {element.id ? (\n <Mention userId={element.id} isSelected={isSelected} />\n ) : null}\n {children}\n </span>\n );\n}\n\nfunction ComposerEditorLinkWrapper({\n Link,\n attributes,\n element,\n children,\n}: ComposerEditorLinkWrapperProps) {\n const href = useMemo(\n () => toAbsoluteUrl(element.url) ?? element.url,\n [element.url]\n );\n\n return (\n <span {...attributes}>\n <Link href={href}>{children}</Link>\n </span>\n );\n}\n\nfunction ComposerEditorMentionSuggestionsWrapper({\n id,\n itemId,\n userIds,\n selectedUserId,\n setSelectedUserId,\n mentionDraft,\n onItemSelect,\n position = MENTION_SUGGESTIONS_POSITION,\n dir,\n MentionSuggestions,\n}: ComposerEditorMentionSuggestionsWrapperProps) {\n const editor = useSlateStatic();\n const { isFocused } = useComposer();\n const [content, setContent] = useState<HTMLDivElement | null>(null);\n const [contentZIndex, setContentZIndex] = useState<string>();\n const contentRef = useCallback(setContent, [setContent]);\n const { portalContainer } = useLiveblocksUIConfig();\n const floatingOptions: UseFloatingOptions = useMemo(() => {\n const detectOverflowOptions: DetectOverflowOptions = {\n padding: FLOATING_ELEMENT_COLLISION_PADDING,\n };\n\n return {\n strategy: \"fixed\",\n placement: getPlacementFromPosition(position, dir),\n middleware: [\n flip({ ...detectOverflowOptions, crossAxis: false }),\n hide(detectOverflowOptions),\n shift({\n ...detectOverflowOptions,\n limiter: limitShift(),\n }),\n size({\n ...detectOverflowOptions,\n apply({ availableWidth, availableHeight, elements }) {\n elements.floating.style.setProperty(\n \"--lb-composer-suggestions-available-width\",\n `${availableWidth}px`\n );\n elements.floating.style.setProperty(\n \"--lb-composer-suggestions-available-height\",\n `${availableHeight}px`\n );\n },\n }),\n ],\n whileElementsMounted: autoUpdate,\n };\n }, [position, dir]);\n const {\n refs: { setReference, setFloating },\n strategy,\n isPositioned,\n placement,\n x,\n y,\n } = useFloating(floatingOptions);\n\n // Copy `z-index` from content to wrapper.\n // Inspired by https://github.com/radix-ui/primitives/blob/main/packages/react/popper/src/Popper.tsx\n useLayoutEffect(() => {\n if (content) {\n setContentZIndex(window.getComputedStyle(content).zIndex);\n }\n }, [content]);\n\n useLayoutEffect(() => {\n if (!mentionDraft) {\n return;\n }\n\n const domRange = getDOMRange(editor, mentionDraft.range);\n\n if (domRange) {\n setReference({\n getBoundingClientRect: () => domRange.getBoundingClientRect(),\n getClientRects: () => domRange.getClientRects(),\n });\n }\n }, [setReference, editor, mentionDraft]);\n\n return (\n <Persist>\n {mentionDraft?.range && isFocused && userIds ? (\n <ComposerSuggestionsContext.Provider\n value={{\n id,\n itemId,\n selectedValue: selectedUserId,\n setSelectedValue: setSelectedUserId,\n onItemSelect,\n placement,\n dir,\n ref: contentRef,\n }}\n >\n <Portal\n ref={setFloating}\n container={portalContainer}\n style={{\n position: strategy,\n top: 0,\n left: 0,\n transform: isPositioned\n ? `translate3d(${Math.round(x)}px, ${Math.round(y)}px, 0)`\n : \"translate3d(0, -200%, 0)\",\n minWidth: \"max-content\",\n zIndex: contentZIndex,\n }}\n >\n <MentionSuggestions\n userIds={userIds}\n selectedUserId={selectedUserId}\n />\n </Portal>\n </ComposerSuggestionsContext.Provider>\n ) : null}\n </Persist>\n );\n}\n\nfunction ComposerEditorElement({\n Mention,\n Link,\n ...props\n}: ComposerEditorElementProps) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n const { attributes, children, element } = props;\n\n switch (element.type) {\n case \"mention\":\n return (\n <ComposerEditorMentionWrapper\n Mention={Mention}\n {...(props as RenderElementSpecificProps<ComposerBodyMention>)}\n />\n );\n case \"auto-link\":\n case \"custom-link\":\n return (\n <ComposerEditorLinkWrapper\n Link={Link}\n {...(props as RenderElementSpecificProps<\n ComposerBodyAutoLink | ComposerBodyCustomLink\n >)}\n />\n );\n case \"paragraph\":\n return (\n <p {...attributes} style={{ position: \"relative\" }}>\n {children}\n </p>\n );\n default:\n return null;\n }\n}\n\n// <code><s><em><strong>text</strong></s></em></code>\nfunction ComposerEditorLeaf({ attributes, children, leaf }: RenderLeafProps) {\n if (leaf.bold) {\n children = <strong>{children}</strong>;\n }\n\n if (leaf.italic) {\n children = <em>{children}</em>;\n }\n\n if (leaf.strikethrough) {\n children = <s>{children}</s>;\n }\n\n if (leaf.code) {\n children = <code>{children}</code>;\n }\n\n return <span {...attributes}>{children}</span>;\n}\n\nfunction ComposerEditorPlaceholder({\n attributes,\n children,\n}: RenderPlaceholderProps) {\n const { opacity: _opacity, ...style } = attributes.style;\n\n return (\n <span {...attributes} style={style} data-placeholder=\"\">\n {children}\n </span>\n );\n}\n\n/**\n * Displays mentions within `Composer.Editor`.\n *\n * @example\n * <Composer.Mention>@{userId}</Composer.Mention>\n */\nconst ComposerMention = forwardRef<HTMLSpanElement, ComposerMentionProps>(\n ({ children, asChild, ...props }, forwardedRef) => {\n const Component = asChild ? Slot : \"span\";\n const isSelected = useSelected();\n\n return (\n <Component\n data-selected={isSelected || undefined}\n {...props}\n ref={forwardedRef}\n >\n {children}\n </Component>\n );\n }\n);\n\n/**\n * Displays links within `Composer.Editor`.\n *\n * @example\n * <Composer.Link href={href}>{children}</Composer.Link>\n */\nconst ComposerLink = forwardRef<HTMLAnchorElement, ComposerLinkProps>(\n ({ children, asChild, ...props }, forwardedRef) => {\n const Component = asChild ? Slot : \"a\";\n\n return (\n <Component\n target=\"_blank\"\n rel=\"noopener noreferrer nofollow\"\n {...props}\n ref={forwardedRef}\n >\n {children}\n </Component>\n );\n }\n);\n\n/**\n * Contains suggestions within `Composer.Editor`.\n */\nconst ComposerSuggestions = forwardRef<\n HTMLDivElement,\n ComposerSuggestionsProps\n>(({ children, style, asChild, ...props }, forwardedRef) => {\n const [isPresent] = usePersist();\n const ref = useRef<HTMLDivElement>(null);\n const {\n ref: contentRef,\n placement,\n dir,\n } = useComposerSuggestionsContext(COMPOSER_SUGGESTIONS_NAME);\n const mergedRefs = useRefs(forwardedRef, contentRef, ref);\n const [side, align] = useMemo(\n () => getSideAndAlignFromPlacement(placement),\n [placement]\n );\n const Component = asChild ? Slot : \"div\";\n useAnimationPersist(ref);\n\n return (\n <Component\n dir={dir}\n {...props}\n data-state={isPresent ? \"open\" : \"closed\"}\n data-side={side}\n data-align={align}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n maxHeight: \"var(--lb-composer-suggestions-available-height)\",\n overflowY: \"auto\",\n ...style,\n }}\n ref={mergedRefs}\n >\n {children}\n </Component>\n );\n});\n\n/**\n * Displays a list of suggestions within `Composer.Editor`.\n *\n * @example\n * <Composer.SuggestionsList>\n * {userIds.map((userId) => (\n * <Composer.SuggestionsListItem key={userId} value={userId}>\n * @{userId}\n * </Composer.SuggestionsListItem>\n * ))}\n * </Composer.SuggestionsList>\n */\nconst ComposerSuggestionsList = forwardRef<\n HTMLUListElement,\n ComposerSuggestionsListProps\n>(({ children, asChild, ...props }, forwardedRef) => {\n const { id } = useComposerSuggestionsContext(COMPOSER_SUGGESTIONS_LIST_NAME);\n const Component = asChild ? Slot : \"ul\";\n\n return (\n <Component\n role=\"listbox\"\n id={id}\n aria-label=\"Suggestions list\"\n {...props}\n ref={forwardedRef}\n >\n {children}\n </Component>\n );\n});\n\n/**\n * Displays a suggestion within `Composer.SuggestionsList`.\n *\n * @example\n * <Composer.SuggestionsListItem key={userId} value={userId}>\n * @{userId}\n * </Composer.SuggestionsListItem>\n */\nconst ComposerSuggestionsListItem = forwardRef<\n HTMLLIElement,\n ComposerSuggestionsListItemProps\n>(\n (\n { value, children, onPointerMove, onPointerDown, asChild, ...props },\n forwardedRef\n ) => {\n const ref = useRef<HTMLLIElement>(null);\n const mergedRefs = useRefs(forwardedRef, ref);\n const { selectedValue, setSelectedValue, itemId, onItemSelect } =\n useComposerSuggestionsContext(COMPOSER_SUGGESTIONS_LIST_ITEM_NAME);\n const Component = asChild ? Slot : \"li\";\n const isSelected = useMemo(\n () => selectedValue === value,\n [selectedValue, value]\n );\n // TODO: Support props.id if provided, it will need to be sent up to Composer.Editor to use it in aria-activedescendant\n const id = useMemo(() => itemId(value), [itemId, value]);\n\n useEffect(() => {\n if (ref?.current && isSelected) {\n ref.current.scrollIntoView({ block: \"nearest\" });\n }\n }, [isSelected]);\n\n const handlePointerMove = useCallback(\n (event: PointerEvent<HTMLLIElement>) => {\n onPointerMove?.(event);\n\n if (!event.isDefaultPrevented()) {\n setSelectedValue(value);\n }\n },\n [onPointerMove, setSelectedValue, value]\n );\n\n const handlePointerDown = useCallback(\n (event: PointerEvent<HTMLLIElement>) => {\n onPointerDown?.(event);\n\n if (!event.isDefaultPrevented()) {\n const target = event.target as HTMLElement;\n\n if (target.hasPointerCapture(event.pointerId)) {\n target.releasePointerCapture(event.pointerId);\n }\n\n if (event.button === 0 && event.ctrlKey === false) {\n onItemSelect(value);\n\n event.preventDefault();\n }\n }\n },\n [onItemSelect, onPointerDown, value]\n );\n\n return (\n <Component\n role=\"option\"\n id={id}\n data-selected={isSelected || undefined}\n aria-selected={isSelected || undefined}\n onPointerMove={handlePointerMove}\n onPointerDown={handlePointerDown}\n {...props}\n ref={mergedRefs}\n >\n {children}\n </Component>\n );\n }\n);\n\nconst defaultEditorComponents: ComposerEditorComponents = {\n Link: ({ href, children }) => {\n return <ComposerLink href={href}>{children}</ComposerLink>;\n },\n Mention: ({ userId }) => {\n return (\n <ComposerMention>\n {MENTION_CHARACTER}\n {userId}\n </ComposerMention>\n );\n },\n MentionSuggestions: ({ userIds }) => {\n return userIds.length > 0 ? (\n <ComposerSuggestions>\n <ComposerSuggestionsList>\n {userIds.map((userId) => (\n <ComposerSuggestionsListItem key={userId} value={userId}>\n {userId}\n </ComposerSuggestionsListItem>\n ))}\n </ComposerSuggestionsList>\n </ComposerSuggestions>\n ) : null;\n },\n};\n\n/**\n * Displays the composer's editor.\n *\n * @example\n * <Composer.Editor placeholder=\"Write a comment…\" />\n */\nconst ComposerEditor = forwardRef<HTMLDivElement, ComposerEditorProps>(\n (\n {\n defaultValue,\n onKeyDown,\n onFocus,\n onBlur,\n disabled,\n autoFocus,\n components,\n dir,\n ...props\n },\n forwardedRef\n ) => {\n const { editor, validate, setFocused } = useComposerEditorContext();\n const {\n submit,\n focus,\n select,\n canSubmit,\n isDisabled: isComposerDisabled,\n isFocused,\n } = useComposer();\n const isDisabled = isComposerDisabled || disabled;\n const initialBody = useInitial(defaultValue ?? emptyCommentBody);\n const initialEditorValue = useMemo(() => {\n return commentBodyToComposerBody(initialBody);\n }, [initialBody]);\n const { Link, Mention, MentionSuggestions } = useMemo(\n () => ({ ...defaultEditorComponents, ...components }),\n [components]\n );\n\n const [mentionDraft, setMentionDraft] = useState<MentionDraft>();\n const mentionSuggestions = useMentionSuggestions(mentionDraft?.text);\n const [\n selectedMentionSuggestionIndex,\n setPreviousSelectedMentionSuggestionIndex,\n setNextSelectedMentionSuggestionIndex,\n setSelectedMentionSuggestionIndex,\n ] = useIndex(0, mentionSuggestions?.length ?? 0);\n const id = useId();\n const suggestionsListId = useMemo(\n () => `liveblocks-suggestions-list-${id}`,\n [id]\n );\n const suggestionsListItemId = useCallback(\n (userId?: string) =>\n userId ? `liveblocks-suggestions-list-item-${id}-${userId}` : undefined,\n [id]\n );\n const renderElement = useCallback(\n (props: RenderElementProps) => {\n return (\n <ComposerEditorElement Mention={Mention} Link={Link} {...props} />\n );\n },\n [Link, Mention]\n );\n\n const handleChange = useCallback(\n (value: SlateDescendant[]) => {\n validate(value as SlateElement[]);\n\n setMentionDraft(getMentionDraftAtSelection(editor));\n },\n [editor, validate]\n );\n\n const createMention = useCallback(\n (userId?: string) => {\n if (!mentionDraft || !userId) {\n return;\n }\n\n SlateTransforms.select(editor, mentionDraft.range);\n insertMention(editor, userId);\n setMentionDraft(undefined);\n setSelectedMentionSuggestionIndex(0);\n },\n [editor, mentionDraft, setSelectedMentionSuggestionIndex]\n );\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(event);\n\n if (event.isDefaultPrevented()) {\n return;\n }\n\n // Allow leaving marks with ArrowLeft\n if (isKey(event, \"ArrowLeft\")) {\n leaveMarkEdge(editor, \"start\");\n }\n\n // Allow leaving marks with ArrowRight\n if (isKey(event, \"ArrowRight\")) {\n leaveMarkEdge(editor, \"end\");\n }\n\n if (mentionDraft && mentionSuggestions?.length) {\n // Select the next mention suggestion on ArrowDown\n if (isKey(event, \"ArrowDown\")) {\n event.preventDefault();\n setNextSelectedMentionSuggestionIndex();\n }\n\n // Select the previous mention suggestion on ArrowUp\n if (isKey(event, \"ArrowUp\")) {\n event.preventDefault();\n setPreviousSelectedMentionSuggestionIndex();\n }\n\n // Create a mention on Enter/Tab\n if (isKey(event, \"Enter\") || isKey(event, \"Tab\")) {\n event.preventDefault();\n\n const userId = mentionSuggestions?.[selectedMentionSuggestionIndex];\n createMention(userId);\n }\n\n // Close the suggestions on Escape\n if (isKey(event, \"Escape\")) {\n event.preventDefault();\n setMentionDraft(undefined);\n setSelectedMentionSuggestionIndex(0);\n }\n } else {\n // Blur the editor on Escape\n if (isKey(event, \"Escape\")) {\n event.preventDefault();\n ReactEditor.blur(editor);\n }\n\n // Submit the editor on Enter\n if (isKey(event, \"Enter\", { shift: false })) {\n // Even if submitting is not possible, don't do anything else on Enter. (e.g. creating a new line)\n event.preventDefault();\n\n if (canSubmit) {\n submit();\n }\n }\n\n // Create a new line on Shift + Enter\n if (isKey(event, \"Enter\", { shift: true })) {\n event.preventDefault();\n editor.insertBreak();\n }\n\n // Toggle bold on Command/Control + B\n if (isKey(event, \"b\", { mod: true })) {\n event.preventDefault();\n toggleMark(editor, \"bold\");\n }\n\n // Toggle italic on Command/Control + I\n if (isKey(event, \"i\", { mod: true })) {\n event.preventDefault();\n toggleMark(editor, \"italic\");\n }\n\n // Toggle strikethrough on Command/Control + Shift + S\n if (isKey(event, \"s\", { mod: true, shift: true })) {\n event.preventDefault();\n toggleMark(editor, \"strikethrough\");\n }\n\n // Toggle code on Command/Control + E\n if (isKey(event, \"e\", { mod: true })) {\n event.preventDefault();\n toggleMark(editor, \"code\");\n }\n }\n },\n [\n createMention,\n editor,\n canSubmit,\n mentionDraft,\n mentionSuggestions,\n selectedMentionSuggestionIndex,\n onKeyDown,\n setNextSelectedMentionSuggestionIndex,\n setPreviousSelectedMentionSuggestionIndex,\n setSelectedMentionSuggestionIndex,\n submit,\n ]\n );\n\n const handleFocus = useCallback(\n (event: FocusEvent<HTMLDivElement>) => {\n onFocus?.(event);\n\n if (!event.isDefaultPrevented()) {\n setFocused(true);\n }\n },\n [onFocus, setFocused]\n );\n\n const handleBlur = useCallback(\n (event: FocusEvent<HTMLDivElement>) => {\n onBlur?.(event);\n\n if (!event.isDefaultPrevented()) {\n setFocused(false);\n }\n },\n [onBlur, setFocused]\n );\n\n const selectedMentionSuggestionUserId = useMemo(\n () => mentionSuggestions?.[selectedMentionSuggestionIndex],\n [selectedMentionSuggestionIndex, mentionSuggestions]\n );\n const setSelectedMentionSuggestionUserId = useCallback(\n (userId: string) => {\n const index = mentionSuggestions?.indexOf(userId);\n\n if (index !== undefined && index >= 0) {\n setSelectedMentionSuggestionIndex(index);\n }\n },\n [setSelectedMentionSuggestionIndex, mentionSuggestions]\n );\n\n const propsWhileSuggesting: AriaAttributes = useMemo(\n () =>\n mentionDraft\n ? {\n role: \"combobox\",\n \"aria-autocomplete\": \"list\",\n \"aria-expanded\": true,\n \"aria-controls\": suggestionsListId,\n \"aria-activedescendant\": suggestionsListItemId(\n selectedMentionSuggestionUserId\n ),\n }\n : {},\n [\n mentionDraft,\n suggestionsListId,\n suggestionsListItemId,\n selectedMentionSuggestionUserId,\n ]\n );\n\n useImperativeHandle(forwardedRef, () => {\n return ReactEditor.toDOMNode(editor, editor) as HTMLDivElement;\n }, [editor]);\n\n // Manually focus the editor when `autoFocus` is true\n useEffect(() => {\n if (autoFocus) {\n focus();\n }\n }, [autoFocus, editor, focus]);\n\n // Manually add a selection in the editor if the selection\n // is still empty after being focused\n useEffect(() => {\n if (isFocused && editor.selection === null) {\n select();\n }\n }, [editor, select, isFocused]);\n\n return (\n <Slate\n editor={editor}\n initialValue={initialEditorValue}\n onChange={handleChange}\n >\n <Editable\n dir={dir}\n enterKeyHint={mentionDraft ? \"enter\" : \"send\"}\n autoCapitalize=\"sentences\"\n aria-label=\"Composer editor\"\n data-focused={isFocused || undefined}\n data-disabled={isDisabled || undefined}\n {...propsWhileSuggesting}\n {...props}\n readOnly={isDisabled}\n disabled={isDisabled}\n onKeyDown={handleKeyDown}\n onFocus={handleFocus}\n onBlur={handleBlur}\n renderElement={renderElement}\n renderLeaf={ComposerEditorLeaf}\n renderPlaceholder={ComposerEditorPlaceholder}\n />\n <ComposerEditorMentionSuggestionsWrapper\n dir={dir}\n mentionDraft={mentionDraft}\n selectedUserId={selectedMentionSuggestionUserId}\n setSelectedUserId={setSelectedMentionSuggestionUserId}\n userIds={mentionSuggestions}\n id={suggestionsListId}\n itemId={suggestionsListItemId}\n onItemSelect={createMention}\n MentionSuggestions={MentionSuggestions}\n />\n </Slate>\n );\n }\n);\n\n// TODO: Convert to Composer.Form props?\nconst MAX_ATTACHMENTS = 5;\nconst MAX_ATTACHMENT_SIZE = 20 * 1024 * 1024; // 20 MB\n\n/**\n * Surrounds the composer's content and handles submissions.\n *\n * @example\n * <Composer.Form onComposerSubmit={({ body }) => {}}>\n *\t <Composer.Editor />\n * <Composer.Submit />\n * </Composer.Form>\n */\nconst ComposerForm = forwardRef<HTMLFormElement, ComposerFormProps>(\n (\n {\n children,\n onSubmit,\n onComposerSubmit,\n defaultAttachments = [],\n disabled,\n asChild,\n ...props\n },\n forwardedRef\n ) => {\n const Component = asChild ? Slot : \"form\";\n const editor = useInitial(createComposerEditor);\n const room = useRoom();\n const [isEmpty, setEmpty] = useState(true);\n const [isSubmitting, setSubmitting] = useState(false);\n const [isFocused, setFocused] = useState(false);\n // TODO: Convert to Composer.Form props?\n const maxAttachments = MAX_ATTACHMENTS;\n const maxAttachmentSize = MAX_ATTACHMENT_SIZE;\n const {\n attachments,\n isUploadingAttachments,\n addAttachment,\n removeAttachment,\n clearAttachments,\n } = useComposerAttachmentsManager(defaultAttachments, {\n maxFileSize: maxAttachmentSize,\n });\n const numberOfAttachments = attachments.length;\n const canAddAttachments = numberOfAttachments < maxAttachments;\n const isDisabled = useMemo(() => {\n const self = room.getSelf();\n const canComment = self?.canComment ?? true;\n\n return isSubmitting || disabled || !canComment;\n }, [isSubmitting, disabled, room]);\n const canSubmit = useMemo(() => {\n return !isEmpty && !isUploadingAttachments;\n }, [isEmpty, isUploadingAttachments]);\n const ref = useRef<HTMLFormElement>(null);\n const mergedRefs = useRefs(forwardedRef, ref);\n const fileInputRef = useRef<HTMLInputElement>(null);\n\n const validate = useCallback(\n (value: SlateElement[]) => {\n setEmpty(isEditorEmpty(editor, value));\n },\n [editor]\n );\n\n const submit = useCallback(() => {\n if (!canSubmit) {\n return;\n }\n\n // We need to wait for the next frame in some cases like when composing diacritics,\n // we want any native handling to be done first while still being handled on `keydown`.\n requestAnimationFrame(() => {\n if (ref.current) {\n requestSubmit(ref.current);\n }\n });\n }, [canSubmit]);\n\n const clear = useCallback(() => {\n SlateTransforms.delete(editor, {\n at: {\n anchor: SlateEditor.start(editor, []),\n focus: SlateEditor.end(editor, []),\n },\n });\n }, [editor]);\n\n const select = useCallback(() => {\n SlateTransforms.select(editor, {\n anchor: SlateEditor.end(editor, []),\n focus: SlateEditor.end(editor, []),\n });\n }, [editor]);\n\n const focus = useCallback(\n (resetSelection = true) => {\n if (!ReactEditor.isFocused(editor)) {\n SlateTransforms.select(\n editor,\n resetSelection || !editor.selection\n ? SlateEditor.end(editor, [])\n : editor.selection\n );\n ReactEditor.focus(editor);\n }\n },\n [editor]\n );\n\n const blur = useCallback(() => {\n ReactEditor.blur(editor);\n }, [editor]);\n\n const createMention = useCallback(() => {\n focus();\n insertMentionCharacter(editor);\n }, [editor, focus]);\n\n const insertText = useCallback(\n (text: string) => {\n focus(false);\n insertSlateText(editor, text);\n },\n [editor, focus]\n );\n\n const createAttachments = useCallback(\n (files: File[]) => {\n if (!files.length) {\n return;\n }\n\n const numberOfAcceptedFiles = Math.max(\n 0,\n maxAttachments - numberOfAttachments\n );\n\n files.splice(numberOfAcceptedFiles);\n\n for (const file of files) {\n const attachment = room.prepareAttachment(file);\n\n addAttachment(attachment);\n }\n },\n [addAttachment, maxAttachments, numberOfAttachments, room]\n );\n\n const attachFiles = useCallback(() => {\n if (fileInputRef.current) {\n fileInputRef.current.click();\n }\n }, []);\n\n const handleAttachmentsInputChange = useCallback(\n (event: ChangeEvent<HTMLInputElement>) => {\n if (event.target.files) {\n createAttachments(Array.from(event.target.files));\n }\n },\n [createAttachments]\n );\n\n const onSubmitEnd = useCallback(() => {\n clear();\n blur();\n clearAttachments();\n setSubmitting(false);\n }, [blur, clear, clearAttachments]);\n\n const handleSubmit = useCallback(\n (event: FormEvent<HTMLFormElement>) => {\n // In some situations (e.g. pressing Enter while composing diacritics), it's possible\n // for the form to be submitted as empty even though we already checked whether the\n // editor was empty when handling the key press.\n const isEmpty = isEditorEmpty(editor, editor.children);\n\n // We even prevent the user's `onSubmit` handler from being called if the editor is empty.\n if (isEmpty) {\n event.preventDefault();\n\n return;\n }\n\n onSubmit?.(event);\n\n if (!onComposerSubmit || event.isDefaultPrevented()) {\n event.preventDefault();\n\n return;\n }\n\n const body = composerBodyToCommentBody(\n editor.children as ComposerBodyData\n );\n // Only non-local attachments are included to be submitted.\n const commentAttachments: CommentAttachment[] = attachments\n .filter(\n (attachment) =>\n attachment.type === \"attachment\" ||\n (attachment.type === \"localAttachment\" &&\n attachment.status === \"uploaded\")\n )\n .map((attachment) => {\n return {\n id: attachment.id,\n type: \"attachment\",\n mimeType: attachment.mimeType,\n size: attachment.size,\n name: attachment.name,\n };\n });\n\n const promise = onComposerSubmit(\n { body, attachments: commentAttachments },\n event\n );\n\n event.preventDefault();\n\n if (promise) {\n setSubmitting(true);\n promise.then(onSubmitEnd);\n } else {\n onSubmitEnd();\n }\n },\n [editor, attachments, onComposerSubmit, onSubmit, onSubmitEnd]\n );\n\n return (\n <ComposerEditorContext.Provider\n value={{\n editor,\n validate,\n setFocused,\n }}\n >\n <ComposerAttachmentsContext.Provider\n value={{\n createAttachments,\n isUploadingAttachments,\n canAddAttachments,\n maxAttachments,\n maxAttachmentSize,\n }}\n >\n <ComposerContext.Provider\n value={{\n isDisabled,\n isFocused,\n isEmpty,\n canSubmit,\n submit,\n clear,\n select,\n focus,\n blur,\n createMention,\n insertText,\n attachments,\n attachFiles,\n removeAttachment,\n }}\n >\n <Component {...props} onSubmit={handleSubmit} ref={mergedRefs}>\n <input\n type=\"file\"\n multiple\n ref={fileInputRef}\n onChange={handleAttachmentsInputChange}\n tabIndex={-1}\n style={{ display: \"none\" }}\n />\n <Slottable>{children}</Slottable>\n </Component>\n </ComposerContext.Provider>\n </ComposerAttachmentsContext.Provider>\n </ComposerEditorContext.Provider>\n );\n }\n);\n\n/**\n * A button to submit the composer.\n *\n * @example\n * <Composer.Submit>Send</Composer.Submit>\n */\nconst ComposerSubmit = forwardRef<HTMLButtonElement, ComposerSubmitProps>(\n ({ children, disabled, asChild, ...props }, forwardedRef) => {\n const Component = asChild ? Slot : \"button\";\n const { canSubmit, isDisabled: isComposerDisabled } = useComposer();\n const isDisabled = isComposerDisabled || disabled || !canSubmit;\n\n return (\n <Component\n type=\"submit\"\n {...props}\n ref={forwardedRef}\n disabled={isDisabled}\n >\n {children}\n </Component>\n );\n }\n);\n\n/**\n * A button which opens a file picker to create attachments.\n *\n * @example\n * <Composer.AttachFiles>Attach files</Composer.AttachFiles>\n */\nconst ComposerAttachFiles = forwardRef<\n HTMLButtonElement,\n ComposerAttachFilesProps\n>(({ children, onClick, disabled, asChild, ...props }, forwardedRef) => {\n const Component = asChild ? Slot : \"button\";\n const { canAddAttachments } = useComposerAttachmentsContext();\n const { isDisabled: isComposerDisabled, attachFiles } = useComposer();\n const isDisabled = isComposerDisabled || !canAddAttachments || disabled;\n\n const handleClick = useCallback(\n (event: MouseEvent<HTMLButtonElement>) => {\n onClick?.(event);\n\n if (!event.isDefaultPrevented()) {\n attachFiles();\n }\n },\n [attachFiles, onClick]\n );\n\n return (\n <Component\n {...props}\n onClick={handleClick}\n ref={forwardedRef}\n disabled={isDisabled}\n >\n {children}\n </Component>\n );\n});\n\n/**\n * A drop area which accepts files to create attachments.\n *\n * @example\n * <Composer.AttachmentsDropArea>\n * Drop files here\n * </Composer.AttachmentsDropArea>\n */\nconst ComposerAttachmentsDropArea = forwardRef<\n HTMLDivElement,\n ComposerAttachmentsDropAreaProps\n>(\n (\n {\n onDragEnter,\n onDragLeave,\n onDragOver,\n onDrop,\n disabled,\n asChild,\n ...props\n },\n forwardedRef\n ) => {\n const Component = asChild ? Slot : \"div\";\n const { isDisabled: isComposerDisabled } = useComposer();\n const isDisabled = isComposerDisabled || disabled;\n const [, dropAreaProps] = useComposerAttachmentsDropArea({\n onDragEnter,\n onDragLeave,\n onDragOver,\n onDrop,\n disabled: isDisabled,\n });\n\n return (\n <Component\n {...dropAreaProps}\n data-disabled={isDisabled ? \"\" : undefined}\n {...props}\n ref={forwardedRef}\n />\n );\n }\n);\n\nif (process.env.NODE_ENV !== \"production\") {\n ComposerAttachFiles.displayName = COMPOSER_ATTACH_FILES_NAME;\n ComposerAttachmentsDropArea.displayName = COMPOSER_ATTACHMENTS_DROP_AREA_NAME;\n ComposerEditor.displayName = COMPOSER_EDITOR_NAME;\n ComposerForm.displayName = COMPOSER_FORM_NAME;\n ComposerMention.displayName = COMPOSER_MENTION_NAME;\n ComposerLink.displayName = COMPOSER_LINK_NAME;\n ComposerSubmit.displayName = COMPOSER_SUBMIT_NAME;\n ComposerSuggestions.displayName = COMPOSER_SUGGESTIONS_NAME;\n ComposerSuggestionsList.displayName = COMPOSER_SUGGESTIONS_LIST_NAME;\n ComposerSuggestionsListItem.displayName = COMPOSER_SUGGESTIONS_LIST_ITEM_NAME;\n}\n\n// NOTE: Every export from this file will be available publicly as Composer.*\nexport {\n ComposerAttachFiles as AttachFiles,\n ComposerAttachmentsDropArea as AttachmentsDropArea,\n ComposerEditor as Editor,\n ComposerForm as Form,\n ComposerLink as Link,\n ComposerMention as Mention,\n ComposerSubmit as Submit,\n ComposerSuggestions as Suggestions,\n ComposerSuggestionsList as SuggestionsList,\n ComposerSuggestionsListItem as SuggestionsListItem,\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsIA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AAAsC;AAC3B;AAEX;AAEA;AACE;AAAO;AACL;AACE;AACE;AACE;AACsD;AACtD;AACF;AACF;AACF;AAEJ;AAEA;AAAsC;AACpC;AACA;AACA;AAEF;AACE;AAEA;AACG;AAAS;AAEL;AAAwB;AAAI;AAKrC;AAEA;AAAmC;AACjC;AACA;AACA;AAEF;AACE;AAAa;AACiC;AAChC;AAGd;AACG;AAAS;AACP;AAAK;AAGZ;AAEA;AAAiD;AAC/C;AACA;AACA;AACA;AACA;AACA;AACA;AACW;AACX;AAEF;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACE;AAAqD;AAC1C;AAGX;AAAO;AACK;AACuC;AACrC;AACyC;AACzB;AACpB;AACD;AACiB;AACrB;AACI;AACA;AAED;AAAwB;AACtB;AACG;AAEL;AAAwB;AACtB;AACG;AACL;AACF;AACD;AACH;AACsB;AACxB;AAEF;AAAM;AAC8B;AAClC;AACA;AACA;AACA;AACA;AAKF;AACE;AACE;AAAwD;AAC1D;AAGF;AACE;AACE;AAAA;AAGF;AAEA;AACE;AAAa;AACiD;AACd;AAC/C;AACH;AAGF;AAGO;AACQ;AACL;AACA;AACe;AACG;AAClB;AACA;AACA;AACK;AACP;AAEC;AACM;AACM;AACJ;AACK;AACL;AACC;AAGF;AACM;AACF;AACV;AAEC;AACC;AACA;AAOd;AAEA;AAA+B;AAC7B;AACA;AAEF;AAEE;AAEA;AAAsB;AAElB;AACG;AACC;AACK;AACP;AAEC;AAEH;AACG;AACC;AACK;AAGP;AAGF;AACG;AAAM;AAA0C;AAEjD;AAGF;AAAO;AAEb;AAGA;AACE;AACE;AAA6B;AAG/B;AACE;AAAyB;AAG3B;AACE;AAAwB;AAG1B;AACE;AAA2B;AAG7B;AAAQ;AAAS;AACnB;AAEA;AAAmC;AACjC;AAEF;AACE;AAEA;AACG;AAAS;AAAY;AAA+B;AAIzD;AAQA;AAAwB;AAEpB;AACA;AAEA;AACG;AAC8B;AACzB;AACC;AAGP;AAGN;AAQA;AAAqB;AAEjB;AAEA;AACG;AACQ;AACH;AACA;AACC;AAGP;AAGN;AAKM;AAIJ;AACA;AACA;AAAM;AACC;AACL;AACA;AAEF;AACA;AAAsB;AACwB;AAClC;AAEZ;AACA;AAEA;AACG;AACC;AACI;AAC6B;AACtB;AACC;AACL;AACI;AACM;AACJ;AACA;AACR;AACL;AACK;AAKX;AAcM;AAIJ;AACA;AAEA;AACG;AACM;AACL;AACW;AACP;AACC;AAKX;AAUA;AAAoC;AAQhC;AACA;AACA;AAEA;AACA;AAAmB;AACO;AACH;AAGvB;AAEA;AACE;AACE;AAA+C;AACjD;AAGF;AAA0B;AAEtB;AAEA;AACE;AAAsB;AACxB;AACF;AACuC;AAGzC;AAA0B;AAEtB;AAEA;AACE;AAEA;AACE;AAA4C;AAG9C;AACE;AAEA;AAAqB;AACvB;AACF;AACF;AACmC;AAGrC;AACG;AACM;AACL;AAC6B;AACA;AACd;AACA;AACX;AACC;AAGP;AAGN;AAEA;AAA0D;AAEtD;AAAQ;AAAa;AAAsB;AAC7C;AAEE;AAIE;AAEJ;AAEE;AAIS;AAAiC;AAAe;AAMrD;AAER;AAQA;AAAuB;AAEnB;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACG;AAIL;AACA;AAAM;AACJ;AACA;AACA;AACA;AACY;AACZ;AAEF;AACA;AACA;AACE;AAA4C;AAE9C;AAA8C;AACO;AACxC;AAGb;AACA;AACA;AAAM;AACJ;AACA;AACA;AACA;AAEF;AACA;AAA0B;AACa;AAClC;AAEL;AAA8B;AAEoC;AAC7D;AAEL;AAAsB;AAElB;AACG;AAAsB;AAAkB;AAAgB;AAAO;AAEpE;AACc;AAGhB;AAAqB;AAEjB;AAEA;AAAkD;AACpD;AACiB;AAGnB;AAAsB;AAElB;AACE;AAAA;AAGF;AACA;AACA;AACA;AAAmC;AACrC;AACwD;AAG1D;AAAsB;AAElB;AAEA;AACE;AAAA;AAIF;AACE;AAA6B;AAI/B;AACE;AAA2B;AAG7B;AAEE;AACE;AACA;AAAsC;AAIxC;AACE;AACA;AAA0C;AAI5C;AACE;AAEA;AACA;AAAoB;AAItB;AACE;AACA;AACA;AAAmC;AACrC;AAGA;AACE;AACA;AAAuB;AAIzB;AAEE;AAEA;AACE;AAAO;AACT;AAIF;AACE;AACA;AAAmB;AAIrB;AACE;AACA;AAAyB;AAI3B;AACE;AACA;AAA2B;AAI7B;AACE;AACA;AAAkC;AAIpC;AACE;AACA;AAAyB;AAC3B;AACF;AACF;AACA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF;AAGF;AAAoB;AAEhB;AAEA;AACE;AAAe;AACjB;AACF;AACoB;AAGtB;AAAmB;AAEf;AAEA;AACE;AAAgB;AAClB;AACF;AACmB;AAGrB;AAAwC;AACX;AACwB;AAErD;AAA2C;AAEvC;AAEA;AACE;AAAuC;AACzC;AACF;AACsD;AAGxD;AAA6C;AAGrC;AACQ;AACe;AACJ;AACA;AACQ;AACvB;AACF;AAED;AACP;AACE;AACA;AACA;AACA;AACF;AAGF;AACE;AAA2C;AAI7C;AACE;AACE;AAAM;AACR;AAKF;AACE;AACE;AAAO;AACT;AAGF;AACG;AACC;AACc;AACJ;AAET;AACC;AACuC;AACxB;AACJ;AACgB;AACE;AACzB;AACA;AACM;AACA;AACC;AACF;AACD;AACR;AACY;AACO;AAEpB;AACC;AACA;AACgB;AACG;AACV;AACL;AACI;AACM;AACd;AAEJ;AAGN;AAGA;AACA;AAWA;AAAqB;AAEjB;AACE;AACA;AACA;AACsB;AACtB;AACA;AACG;AAIL;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAAM;AACJ;AACA;AACA;AACA;AACA;AACoD;AACvC;AAEf;AACA;AACA;AACE;AACA;AAEA;AAAoC;AAEtC;AACE;AAAoB;AAEtB;AACA;AACA;AAEA;AAAiB;AAEb;AAAqC;AACvC;AACO;AAGT;AACE;AACE;AAAA;AAKF;AACE;AACE;AAAyB;AAC3B;AACD;AAGH;AACE;AAA+B;AACzB;AACkC;AACH;AACnC;AACD;AAGH;AACE;AAA+B;AACK;AACD;AAClC;AAGH;AAAc;AAEV;AACE;AAAgB;AACd;AAGW;AAEb;AAAwB;AAC1B;AACF;AACO;AAGT;AACE;AAAuB;AAGzB;AACE;AACA;AAA6B;AAG/B;AAAmB;AAEf;AACA;AAA4B;AAC9B;AACc;AAGhB;AAA0B;AAEtB;AACE;AAAA;AAGF;AAAmC;AACjC;AACiB;AAGnB;AAEA;AACE;AAEA;AAAwB;AAC1B;AACF;AACyD;AAG3D;AACE;AACE;AAA2B;AAC7B;AAGF;AAAqC;AAEjC;AACE;AAAgD;AAClD;AACF;AACkB;AAGpB;AACE;AACA;AACA;AACA;AAAmB;AAGrB;AAAqB;AAKjB;AAGA;AACE;AAEA;AAAA;AAGF;AAEA;AACE;AAEA;AAAA;AAGF;AAAa;AACJ;AAGT;AACG;AAI2B;AAG1B;AAAO;AACU;AACT;AACe;AACJ;AACA;AACnB;AAGJ;AAAgB;AAC0B;AACxC;AAGF;AAEA;AACE;AACA;AAAwB;AAExB;AAAY;AACd;AACF;AAC6D;AAG/D;AACG;AACQ;AACL;AACA;AACA;AACF;AAEC;AACQ;AACL;AACA;AACA;AACA;AACA;AACF;AAEC;AACQ;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF;AAEC;AAAc;AAAiB;AAAmB;AAChD;AACM;AACG;AACH;AACK;AACA;AACe;AAMnC;AAGN;AAQA;AAAuB;AAEnB;AACA;AACA;AAEA;AACG;AACM;AACD;AACC;AACK;AAGZ;AAGN;AAQM;AAIJ;AACA;AACA;AACA;AAEA;AAAoB;AAEhB;AAEA;AACE;AAAY;AACd;AACF;AACqB;AAGvB;AACG;AACK;AACK;AACJ;AACK;AAKhB;AAUA;AAAoC;AAKhC;AACE;AACA;AACA;AACA;AACA;AACA;AACG;AAIL;AACA;AACA;AACA;AAAyD;AACvD;AACA;AACA;AACA;AACU;AAGZ;AACG;AACK;AAC6B;AAC7B;AACC;AACP;AAGN;AAEA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF;;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../src/primitives/Composer/index.tsx"],"sourcesContent":["\"use client\";\n\nimport type {\n DetectOverflowOptions,\n UseFloatingOptions,\n} from \"@floating-ui/react-dom\";\nimport {\n autoUpdate,\n flip,\n hide,\n limitShift,\n shift,\n size,\n useFloating,\n} from \"@floating-ui/react-dom\";\nimport type { CommentAttachment, CommentBody } from \"@liveblocks/core\";\nimport { useRoom } from \"@liveblocks/react\";\nimport { Slot, Slottable } from \"@radix-ui/react-slot\";\nimport type {\n AriaAttributes,\n ChangeEvent,\n FocusEvent,\n FormEvent,\n KeyboardEvent,\n MouseEvent,\n PointerEvent,\n} from \"react\";\nimport React, {\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport type {\n Descendant as SlateDescendant,\n Element as SlateElement,\n} from \"slate\";\nimport {\n createEditor,\n Editor as SlateEditor,\n insertText as insertSlateText,\n Transforms as SlateTransforms,\n} from \"slate\";\nimport { withHistory } from \"slate-history\";\nimport type {\n RenderElementProps,\n RenderElementSpecificProps,\n RenderLeafProps,\n RenderPlaceholderProps,\n} from \"slate-react\";\nimport {\n Editable,\n ReactEditor,\n Slate,\n useSelected,\n useSlateStatic,\n withReact,\n} from \"slate-react\";\n\nimport { useLiveblocksUIConfig } from \"../../config\";\nimport { FLOATING_ELEMENT_COLLISION_PADDING } from \"../../constants\";\nimport { useMentionSuggestions } from \"../../shared\";\nimport { withAutoFormatting } from \"../../slate/plugins/auto-formatting\";\nimport { withAutoLinks } from \"../../slate/plugins/auto-links\";\nimport { withCustomLinks } from \"../../slate/plugins/custom-links\";\nimport { withEmptyClearFormatting } from \"../../slate/plugins/empty-clear-formatting\";\nimport type { MentionDraft } from \"../../slate/plugins/mentions\";\nimport {\n getMentionDraftAtSelection,\n insertMention,\n insertMentionCharacter,\n MENTION_CHARACTER,\n withMentions,\n} from \"../../slate/plugins/mentions\";\nimport { withPaste } from \"../../slate/plugins/paste\";\nimport { getDOMRange } from \"../../slate/utils/get-dom-range\";\nimport { isEmpty as isEditorEmpty } from \"../../slate/utils/is-empty\";\nimport { leaveMarkEdge, toggleMark } from \"../../slate/utils/marks\";\nimport type {\n ComposerBody as ComposerBodyData,\n ComposerBodyAutoLink,\n ComposerBodyCustomLink,\n ComposerBodyMention,\n} from \"../../types\";\nimport { isKey } from \"../../utils/is-key\";\nimport { Persist, useAnimationPersist, usePersist } from \"../../utils/Persist\";\nimport { Portal } from \"../../utils/Portal\";\nimport { requestSubmit } from \"../../utils/request-submit\";\nimport { useId } from \"../../utils/use-id\";\nimport { useIndex } from \"../../utils/use-index\";\nimport { useInitial } from \"../../utils/use-initial\";\nimport { useLayoutEffect } from \"../../utils/use-layout-effect\";\nimport { useRefs } from \"../../utils/use-refs\";\nimport { toAbsoluteUrl } from \"../Comment/utils\";\nimport {\n ComposerAttachmentsContext,\n ComposerContext,\n ComposerEditorContext,\n ComposerSuggestionsContext,\n useComposer,\n useComposerAttachmentsContext,\n useComposerEditorContext,\n useComposerSuggestionsContext,\n} from \"./contexts\";\nimport type {\n ComposerAttachFilesProps,\n ComposerAttachmentsDropAreaProps,\n ComposerEditorComponents,\n ComposerEditorElementProps,\n ComposerEditorLinkWrapperProps,\n ComposerEditorMentionSuggestionsWrapperProps,\n ComposerEditorMentionWrapperProps,\n ComposerEditorProps,\n ComposerFormProps,\n ComposerLinkProps,\n ComposerMentionProps,\n ComposerSubmitProps,\n ComposerSuggestionsListItemProps,\n ComposerSuggestionsListProps,\n ComposerSuggestionsProps,\n SuggestionsPosition,\n} from \"./types\";\nimport {\n commentBodyToComposerBody,\n composerBodyToCommentBody,\n getPlacementFromPosition,\n getSideAndAlignFromPlacement,\n useComposerAttachmentsDropArea,\n useComposerAttachmentsManager,\n} from \"./utils\";\n\nconst MENTION_SUGGESTIONS_POSITION: SuggestionsPosition = \"top\";\n\nconst COMPOSER_MENTION_NAME = \"ComposerMention\";\nconst COMPOSER_LINK_NAME = \"ComposerLink\";\nconst COMPOSER_SUGGESTIONS_NAME = \"ComposerSuggestions\";\nconst COMPOSER_SUGGESTIONS_LIST_NAME = \"ComposerSuggestionsList\";\nconst COMPOSER_SUGGESTIONS_LIST_ITEM_NAME = \"ComposerSuggestionsListItem\";\nconst COMPOSER_SUBMIT_NAME = \"ComposerSubmit\";\nconst COMPOSER_EDITOR_NAME = \"ComposerEditor\";\nconst COMPOSER_ATTACH_FILES_NAME = \"ComposerAttachFiles\";\nconst COMPOSER_ATTACHMENTS_DROP_AREA_NAME = \"ComposerAttachmentsDropArea\";\nconst COMPOSER_FORM_NAME = \"ComposerForm\";\n\nconst emptyCommentBody: CommentBody = {\n version: 1,\n content: [{ type: \"paragraph\", children: [{ text: \"\" }] }],\n};\n\nfunction createComposerEditor({\n createAttachments,\n pasteFilesAsAttachments,\n}: {\n createAttachments: (files: File[]) => void;\n pasteFilesAsAttachments?: boolean;\n}) {\n return withMentions(\n withCustomLinks(\n withAutoLinks(\n withAutoFormatting(\n withEmptyClearFormatting(\n withPaste(withHistory(withReact(createEditor())), {\n createAttachments,\n pasteFilesAsAttachments,\n })\n )\n )\n )\n )\n );\n}\n\nfunction ComposerEditorMentionWrapper({\n Mention,\n attributes,\n children,\n element,\n}: ComposerEditorMentionWrapperProps) {\n const isSelected = useSelected();\n\n return (\n <span {...attributes}>\n {element.id ? (\n <Mention userId={element.id} isSelected={isSelected} />\n ) : null}\n {children}\n </span>\n );\n}\n\nfunction ComposerEditorLinkWrapper({\n Link,\n attributes,\n element,\n children,\n}: ComposerEditorLinkWrapperProps) {\n const href = useMemo(\n () => toAbsoluteUrl(element.url) ?? element.url,\n [element.url]\n );\n\n return (\n <span {...attributes}>\n <Link href={href}>{children}</Link>\n </span>\n );\n}\n\nfunction ComposerEditorMentionSuggestionsWrapper({\n id,\n itemId,\n userIds,\n selectedUserId,\n setSelectedUserId,\n mentionDraft,\n onItemSelect,\n position = MENTION_SUGGESTIONS_POSITION,\n dir,\n MentionSuggestions,\n}: ComposerEditorMentionSuggestionsWrapperProps) {\n const editor = useSlateStatic();\n const { isFocused } = useComposer();\n const [content, setContent] = useState<HTMLDivElement | null>(null);\n const [contentZIndex, setContentZIndex] = useState<string>();\n const contentRef = useCallback(setContent, [setContent]);\n const { portalContainer } = useLiveblocksUIConfig();\n const floatingOptions: UseFloatingOptions = useMemo(() => {\n const detectOverflowOptions: DetectOverflowOptions = {\n padding: FLOATING_ELEMENT_COLLISION_PADDING,\n };\n\n return {\n strategy: \"fixed\",\n placement: getPlacementFromPosition(position, dir),\n middleware: [\n flip({ ...detectOverflowOptions, crossAxis: false }),\n hide(detectOverflowOptions),\n shift({\n ...detectOverflowOptions,\n limiter: limitShift(),\n }),\n size({\n ...detectOverflowOptions,\n apply({ availableWidth, availableHeight, elements }) {\n elements.floating.style.setProperty(\n \"--lb-composer-suggestions-available-width\",\n `${availableWidth}px`\n );\n elements.floating.style.setProperty(\n \"--lb-composer-suggestions-available-height\",\n `${availableHeight}px`\n );\n },\n }),\n ],\n whileElementsMounted: autoUpdate,\n };\n }, [position, dir]);\n const {\n refs: { setReference, setFloating },\n strategy,\n isPositioned,\n placement,\n x,\n y,\n } = useFloating(floatingOptions);\n\n // Copy `z-index` from content to wrapper.\n // Inspired by https://github.com/radix-ui/primitives/blob/main/packages/react/popper/src/Popper.tsx\n useLayoutEffect(() => {\n if (content) {\n setContentZIndex(window.getComputedStyle(content).zIndex);\n }\n }, [content]);\n\n useLayoutEffect(() => {\n if (!mentionDraft) {\n return;\n }\n\n const domRange = getDOMRange(editor, mentionDraft.range);\n\n if (domRange) {\n setReference({\n getBoundingClientRect: () => domRange.getBoundingClientRect(),\n getClientRects: () => domRange.getClientRects(),\n });\n }\n }, [setReference, editor, mentionDraft]);\n\n return (\n <Persist>\n {mentionDraft?.range && isFocused && userIds ? (\n <ComposerSuggestionsContext.Provider\n value={{\n id,\n itemId,\n selectedValue: selectedUserId,\n setSelectedValue: setSelectedUserId,\n onItemSelect,\n placement,\n dir,\n ref: contentRef,\n }}\n >\n <Portal\n ref={setFloating}\n container={portalContainer}\n style={{\n position: strategy,\n top: 0,\n left: 0,\n transform: isPositioned\n ? `translate3d(${Math.round(x)}px, ${Math.round(y)}px, 0)`\n : \"translate3d(0, -200%, 0)\",\n minWidth: \"max-content\",\n zIndex: contentZIndex,\n }}\n >\n <MentionSuggestions\n userIds={userIds}\n selectedUserId={selectedUserId}\n />\n </Portal>\n </ComposerSuggestionsContext.Provider>\n ) : null}\n </Persist>\n );\n}\n\nfunction ComposerEditorElement({\n Mention,\n Link,\n ...props\n}: ComposerEditorElementProps) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n const { attributes, children, element } = props;\n\n switch (element.type) {\n case \"mention\":\n return (\n <ComposerEditorMentionWrapper\n Mention={Mention}\n {...(props as RenderElementSpecificProps<ComposerBodyMention>)}\n />\n );\n case \"auto-link\":\n case \"custom-link\":\n return (\n <ComposerEditorLinkWrapper\n Link={Link}\n {...(props as RenderElementSpecificProps<\n ComposerBodyAutoLink | ComposerBodyCustomLink\n >)}\n />\n );\n case \"paragraph\":\n return (\n <p {...attributes} style={{ position: \"relative\" }}>\n {children}\n </p>\n );\n default:\n return null;\n }\n}\n\n// <code><s><em><strong>text</strong></s></em></code>\nfunction ComposerEditorLeaf({ attributes, children, leaf }: RenderLeafProps) {\n if (leaf.bold) {\n children = <strong>{children}</strong>;\n }\n\n if (leaf.italic) {\n children = <em>{children}</em>;\n }\n\n if (leaf.strikethrough) {\n children = <s>{children}</s>;\n }\n\n if (leaf.code) {\n children = <code>{children}</code>;\n }\n\n return <span {...attributes}>{children}</span>;\n}\n\nfunction ComposerEditorPlaceholder({\n attributes,\n children,\n}: RenderPlaceholderProps) {\n const { opacity: _opacity, ...style } = attributes.style;\n\n return (\n <span {...attributes} style={style} data-placeholder=\"\">\n {children}\n </span>\n );\n}\n\n/**\n * Displays mentions within `Composer.Editor`.\n *\n * @example\n * <Composer.Mention>@{userId}</Composer.Mention>\n */\nconst ComposerMention = forwardRef<HTMLSpanElement, ComposerMentionProps>(\n ({ children, asChild, ...props }, forwardedRef) => {\n const Component = asChild ? Slot : \"span\";\n const isSelected = useSelected();\n\n return (\n <Component\n data-selected={isSelected || undefined}\n {...props}\n ref={forwardedRef}\n >\n {children}\n </Component>\n );\n }\n);\n\n/**\n * Displays links within `Composer.Editor`.\n *\n * @example\n * <Composer.Link href={href}>{children}</Composer.Link>\n */\nconst ComposerLink = forwardRef<HTMLAnchorElement, ComposerLinkProps>(\n ({ children, asChild, ...props }, forwardedRef) => {\n const Component = asChild ? Slot : \"a\";\n\n return (\n <Component\n target=\"_blank\"\n rel=\"noopener noreferrer nofollow\"\n {...props}\n ref={forwardedRef}\n >\n {children}\n </Component>\n );\n }\n);\n\n/**\n * Contains suggestions within `Composer.Editor`.\n */\nconst ComposerSuggestions = forwardRef<\n HTMLDivElement,\n ComposerSuggestionsProps\n>(({ children, style, asChild, ...props }, forwardedRef) => {\n const [isPresent] = usePersist();\n const ref = useRef<HTMLDivElement>(null);\n const {\n ref: contentRef,\n placement,\n dir,\n } = useComposerSuggestionsContext(COMPOSER_SUGGESTIONS_NAME);\n const mergedRefs = useRefs(forwardedRef, contentRef, ref);\n const [side, align] = useMemo(\n () => getSideAndAlignFromPlacement(placement),\n [placement]\n );\n const Component = asChild ? Slot : \"div\";\n useAnimationPersist(ref);\n\n return (\n <Component\n dir={dir}\n {...props}\n data-state={isPresent ? \"open\" : \"closed\"}\n data-side={side}\n data-align={align}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n maxHeight: \"var(--lb-composer-suggestions-available-height)\",\n overflowY: \"auto\",\n ...style,\n }}\n ref={mergedRefs}\n >\n {children}\n </Component>\n );\n});\n\n/**\n * Displays a list of suggestions within `Composer.Editor`.\n *\n * @example\n * <Composer.SuggestionsList>\n * {userIds.map((userId) => (\n * <Composer.SuggestionsListItem key={userId} value={userId}>\n * @{userId}\n * </Composer.SuggestionsListItem>\n * ))}\n * </Composer.SuggestionsList>\n */\nconst ComposerSuggestionsList = forwardRef<\n HTMLUListElement,\n ComposerSuggestionsListProps\n>(({ children, asChild, ...props }, forwardedRef) => {\n const { id } = useComposerSuggestionsContext(COMPOSER_SUGGESTIONS_LIST_NAME);\n const Component = asChild ? Slot : \"ul\";\n\n return (\n <Component\n role=\"listbox\"\n id={id}\n aria-label=\"Suggestions list\"\n {...props}\n ref={forwardedRef}\n >\n {children}\n </Component>\n );\n});\n\n/**\n * Displays a suggestion within `Composer.SuggestionsList`.\n *\n * @example\n * <Composer.SuggestionsListItem key={userId} value={userId}>\n * @{userId}\n * </Composer.SuggestionsListItem>\n */\nconst ComposerSuggestionsListItem = forwardRef<\n HTMLLIElement,\n ComposerSuggestionsListItemProps\n>(\n (\n { value, children, onPointerMove, onPointerDown, asChild, ...props },\n forwardedRef\n ) => {\n const ref = useRef<HTMLLIElement>(null);\n const mergedRefs = useRefs(forwardedRef, ref);\n const { selectedValue, setSelectedValue, itemId, onItemSelect } =\n useComposerSuggestionsContext(COMPOSER_SUGGESTIONS_LIST_ITEM_NAME);\n const Component = asChild ? Slot : \"li\";\n const isSelected = useMemo(\n () => selectedValue === value,\n [selectedValue, value]\n );\n // TODO: Support props.id if provided, it will need to be sent up to Composer.Editor to use it in aria-activedescendant\n const id = useMemo(() => itemId(value), [itemId, value]);\n\n useEffect(() => {\n if (ref?.current && isSelected) {\n ref.current.scrollIntoView({ block: \"nearest\" });\n }\n }, [isSelected]);\n\n const handlePointerMove = useCallback(\n (event: PointerEvent<HTMLLIElement>) => {\n onPointerMove?.(event);\n\n if (!event.isDefaultPrevented()) {\n setSelectedValue(value);\n }\n },\n [onPointerMove, setSelectedValue, value]\n );\n\n const handlePointerDown = useCallback(\n (event: PointerEvent<HTMLLIElement>) => {\n onPointerDown?.(event);\n\n if (!event.isDefaultPrevented()) {\n const target = event.target as HTMLElement;\n\n if (target.hasPointerCapture(event.pointerId)) {\n target.releasePointerCapture(event.pointerId);\n }\n\n if (event.button === 0 && event.ctrlKey === false) {\n onItemSelect(value);\n\n event.preventDefault();\n }\n }\n },\n [onItemSelect, onPointerDown, value]\n );\n\n return (\n <Component\n role=\"option\"\n id={id}\n data-selected={isSelected || undefined}\n aria-selected={isSelected || undefined}\n onPointerMove={handlePointerMove}\n onPointerDown={handlePointerDown}\n {...props}\n ref={mergedRefs}\n >\n {children}\n </Component>\n );\n }\n);\n\nconst defaultEditorComponents: ComposerEditorComponents = {\n Link: ({ href, children }) => {\n return <ComposerLink href={href}>{children}</ComposerLink>;\n },\n Mention: ({ userId }) => {\n return (\n <ComposerMention>\n {MENTION_CHARACTER}\n {userId}\n </ComposerMention>\n );\n },\n MentionSuggestions: ({ userIds }) => {\n return userIds.length > 0 ? (\n <ComposerSuggestions>\n <ComposerSuggestionsList>\n {userIds.map((userId) => (\n <ComposerSuggestionsListItem key={userId} value={userId}>\n {userId}\n </ComposerSuggestionsListItem>\n ))}\n </ComposerSuggestionsList>\n </ComposerSuggestions>\n ) : null;\n },\n};\n\n/**\n * Displays the composer's editor.\n *\n * @example\n * <Composer.Editor placeholder=\"Write a comment…\" />\n */\nconst ComposerEditor = forwardRef<HTMLDivElement, ComposerEditorProps>(\n (\n {\n defaultValue,\n onKeyDown,\n onFocus,\n onBlur,\n disabled,\n autoFocus,\n components,\n dir,\n ...props\n },\n forwardedRef\n ) => {\n const { editor, validate, setFocused } = useComposerEditorContext();\n const {\n submit,\n focus,\n select,\n canSubmit,\n isDisabled: isComposerDisabled,\n isFocused,\n } = useComposer();\n const isDisabled = isComposerDisabled || disabled;\n const initialBody = useInitial(defaultValue ?? emptyCommentBody);\n const initialEditorValue = useMemo(() => {\n return commentBodyToComposerBody(initialBody);\n }, [initialBody]);\n const { Link, Mention, MentionSuggestions } = useMemo(\n () => ({ ...defaultEditorComponents, ...components }),\n [components]\n );\n\n const [mentionDraft, setMentionDraft] = useState<MentionDraft>();\n const mentionSuggestions = useMentionSuggestions(mentionDraft?.text);\n const [\n selectedMentionSuggestionIndex,\n setPreviousSelectedMentionSuggestionIndex,\n setNextSelectedMentionSuggestionIndex,\n setSelectedMentionSuggestionIndex,\n ] = useIndex(0, mentionSuggestions?.length ?? 0);\n const id = useId();\n const suggestionsListId = useMemo(\n () => `liveblocks-suggestions-list-${id}`,\n [id]\n );\n const suggestionsListItemId = useCallback(\n (userId?: string) =>\n userId ? `liveblocks-suggestions-list-item-${id}-${userId}` : undefined,\n [id]\n );\n const renderElement = useCallback(\n (props: RenderElementProps) => {\n return (\n <ComposerEditorElement Mention={Mention} Link={Link} {...props} />\n );\n },\n [Link, Mention]\n );\n\n const handleChange = useCallback(\n (value: SlateDescendant[]) => {\n validate(value as SlateElement[]);\n\n setMentionDraft(getMentionDraftAtSelection(editor));\n },\n [editor, validate]\n );\n\n const createMention = useCallback(\n (userId?: string) => {\n if (!mentionDraft || !userId) {\n return;\n }\n\n SlateTransforms.select(editor, mentionDraft.range);\n insertMention(editor, userId);\n setMentionDraft(undefined);\n setSelectedMentionSuggestionIndex(0);\n },\n [editor, mentionDraft, setSelectedMentionSuggestionIndex]\n );\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(event);\n\n if (event.isDefaultPrevented()) {\n return;\n }\n\n // Allow leaving marks with ArrowLeft\n if (isKey(event, \"ArrowLeft\")) {\n leaveMarkEdge(editor, \"start\");\n }\n\n // Allow leaving marks with ArrowRight\n if (isKey(event, \"ArrowRight\")) {\n leaveMarkEdge(editor, \"end\");\n }\n\n if (mentionDraft && mentionSuggestions?.length) {\n // Select the next mention suggestion on ArrowDown\n if (isKey(event, \"ArrowDown\")) {\n event.preventDefault();\n setNextSelectedMentionSuggestionIndex();\n }\n\n // Select the previous mention suggestion on ArrowUp\n if (isKey(event, \"ArrowUp\")) {\n event.preventDefault();\n setPreviousSelectedMentionSuggestionIndex();\n }\n\n // Create a mention on Enter/Tab\n if (isKey(event, \"Enter\") || isKey(event, \"Tab\")) {\n event.preventDefault();\n\n const userId = mentionSuggestions?.[selectedMentionSuggestionIndex];\n createMention(userId);\n }\n\n // Close the suggestions on Escape\n if (isKey(event, \"Escape\")) {\n event.preventDefault();\n setMentionDraft(undefined);\n setSelectedMentionSuggestionIndex(0);\n }\n } else {\n // Blur the editor on Escape\n if (isKey(event, \"Escape\")) {\n event.preventDefault();\n ReactEditor.blur(editor);\n }\n\n // Submit the editor on Enter\n if (isKey(event, \"Enter\", { shift: false })) {\n // Even if submitting is not possible, don't do anything else on Enter. (e.g. creating a new line)\n event.preventDefault();\n\n if (canSubmit) {\n submit();\n }\n }\n\n // Create a new line on Shift + Enter\n if (isKey(event, \"Enter\", { shift: true })) {\n event.preventDefault();\n editor.insertBreak();\n }\n\n // Toggle bold on Command/Control + B\n if (isKey(event, \"b\", { mod: true })) {\n event.preventDefault();\n toggleMark(editor, \"bold\");\n }\n\n // Toggle italic on Command/Control + I\n if (isKey(event, \"i\", { mod: true })) {\n event.preventDefault();\n toggleMark(editor, \"italic\");\n }\n\n // Toggle strikethrough on Command/Control + Shift + S\n if (isKey(event, \"s\", { mod: true, shift: true })) {\n event.preventDefault();\n toggleMark(editor, \"strikethrough\");\n }\n\n // Toggle code on Command/Control + E\n if (isKey(event, \"e\", { mod: true })) {\n event.preventDefault();\n toggleMark(editor, \"code\");\n }\n }\n },\n [\n createMention,\n editor,\n canSubmit,\n mentionDraft,\n mentionSuggestions,\n selectedMentionSuggestionIndex,\n onKeyDown,\n setNextSelectedMentionSuggestionIndex,\n setPreviousSelectedMentionSuggestionIndex,\n setSelectedMentionSuggestionIndex,\n submit,\n ]\n );\n\n const handleFocus = useCallback(\n (event: FocusEvent<HTMLDivElement>) => {\n onFocus?.(event);\n\n if (!event.isDefaultPrevented()) {\n setFocused(true);\n }\n },\n [onFocus, setFocused]\n );\n\n const handleBlur = useCallback(\n (event: FocusEvent<HTMLDivElement>) => {\n onBlur?.(event);\n\n if (!event.isDefaultPrevented()) {\n setFocused(false);\n }\n },\n [onBlur, setFocused]\n );\n\n const selectedMentionSuggestionUserId = useMemo(\n () => mentionSuggestions?.[selectedMentionSuggestionIndex],\n [selectedMentionSuggestionIndex, mentionSuggestions]\n );\n const setSelectedMentionSuggestionUserId = useCallback(\n (userId: string) => {\n const index = mentionSuggestions?.indexOf(userId);\n\n if (index !== undefined && index >= 0) {\n setSelectedMentionSuggestionIndex(index);\n }\n },\n [setSelectedMentionSuggestionIndex, mentionSuggestions]\n );\n\n const propsWhileSuggesting: AriaAttributes = useMemo(\n () =>\n mentionDraft\n ? {\n role: \"combobox\",\n \"aria-autocomplete\": \"list\",\n \"aria-expanded\": true,\n \"aria-controls\": suggestionsListId,\n \"aria-activedescendant\": suggestionsListItemId(\n selectedMentionSuggestionUserId\n ),\n }\n : {},\n [\n mentionDraft,\n suggestionsListId,\n suggestionsListItemId,\n selectedMentionSuggestionUserId,\n ]\n );\n\n useImperativeHandle(forwardedRef, () => {\n return ReactEditor.toDOMNode(editor, editor) as HTMLDivElement;\n }, [editor]);\n\n // Manually focus the editor when `autoFocus` is true\n useEffect(() => {\n if (autoFocus) {\n focus();\n }\n }, [autoFocus, editor, focus]);\n\n // Manually add a selection in the editor if the selection\n // is still empty after being focused\n useEffect(() => {\n if (isFocused && editor.selection === null) {\n select();\n }\n }, [editor, select, isFocused]);\n\n return (\n <Slate\n editor={editor}\n initialValue={initialEditorValue}\n onChange={handleChange}\n >\n <Editable\n dir={dir}\n enterKeyHint={mentionDraft ? \"enter\" : \"send\"}\n autoCapitalize=\"sentences\"\n aria-label=\"Composer editor\"\n data-focused={isFocused || undefined}\n data-disabled={isDisabled || undefined}\n {...propsWhileSuggesting}\n {...props}\n readOnly={isDisabled}\n disabled={isDisabled}\n onKeyDown={handleKeyDown}\n onFocus={handleFocus}\n onBlur={handleBlur}\n renderElement={renderElement}\n renderLeaf={ComposerEditorLeaf}\n renderPlaceholder={ComposerEditorPlaceholder}\n />\n <ComposerEditorMentionSuggestionsWrapper\n dir={dir}\n mentionDraft={mentionDraft}\n selectedUserId={selectedMentionSuggestionUserId}\n setSelectedUserId={setSelectedMentionSuggestionUserId}\n userIds={mentionSuggestions}\n id={suggestionsListId}\n itemId={suggestionsListItemId}\n onItemSelect={createMention}\n MentionSuggestions={MentionSuggestions}\n />\n </Slate>\n );\n }\n);\n\n// TODO: Convert to Composer.Form props?\nconst MAX_ATTACHMENTS = 5;\nconst MAX_ATTACHMENT_SIZE = 20 * 1024 * 1024; // 20 MB\n\n/**\n * Surrounds the composer's content and handles submissions.\n *\n * @example\n * <Composer.Form onComposerSubmit={({ body }) => {}}>\n *\t <Composer.Editor />\n * <Composer.Submit />\n * </Composer.Form>\n */\nconst ComposerForm = forwardRef<HTMLFormElement, ComposerFormProps>(\n (\n {\n children,\n onSubmit,\n onComposerSubmit,\n defaultAttachments = [],\n pasteFilesAsAttachments,\n disabled,\n asChild,\n ...props\n },\n forwardedRef\n ) => {\n const Component = asChild ? Slot : \"form\";\n const room = useRoom();\n const [isEmpty, setEmpty] = useState(true);\n const [isSubmitting, setSubmitting] = useState(false);\n const [isFocused, setFocused] = useState(false);\n // TODO: Convert to Composer.Form props?\n const maxAttachments = MAX_ATTACHMENTS;\n const maxAttachmentSize = MAX_ATTACHMENT_SIZE;\n const {\n attachments,\n isUploadingAttachments,\n addAttachments,\n removeAttachment,\n clearAttachments,\n } = useComposerAttachmentsManager(defaultAttachments, {\n maxFileSize: maxAttachmentSize,\n });\n const numberOfAttachments = attachments.length;\n const hasMaxAttachments = numberOfAttachments >= maxAttachments;\n const isDisabled = useMemo(() => {\n const self = room.getSelf();\n const canComment = self?.canComment ?? true;\n\n return isSubmitting || disabled || !canComment;\n }, [isSubmitting, disabled, room]);\n const canSubmit = useMemo(() => {\n return !isEmpty && !isUploadingAttachments;\n }, [isEmpty, isUploadingAttachments]);\n const ref = useRef<HTMLFormElement>(null);\n const mergedRefs = useRefs(forwardedRef, ref);\n const fileInputRef = useRef<HTMLInputElement>(null);\n\n const createAttachments = useCallback(\n (files: File[]) => {\n if (!files.length) {\n return;\n }\n\n const numberOfAcceptedFiles = Math.max(\n 0,\n maxAttachments - numberOfAttachments\n );\n\n files.splice(numberOfAcceptedFiles);\n\n const attachments = files.map((file) => room.prepareAttachment(file));\n\n addAttachments(attachments);\n },\n [addAttachments, maxAttachments, numberOfAttachments, room]\n );\n\n const editor = useInitial(() =>\n createComposerEditor({ createAttachments, pasteFilesAsAttachments })\n );\n\n const validate = useCallback(\n (value: SlateElement[]) => {\n setEmpty(isEditorEmpty(editor, value));\n },\n [editor]\n );\n\n const submit = useCallback(() => {\n if (!canSubmit) {\n return;\n }\n\n // We need to wait for the next frame in some cases like when composing diacritics,\n // we want any native handling to be done first while still being handled on `keydown`.\n requestAnimationFrame(() => {\n if (ref.current) {\n requestSubmit(ref.current);\n }\n });\n }, [canSubmit]);\n\n const clear = useCallback(() => {\n SlateTransforms.delete(editor, {\n at: {\n anchor: SlateEditor.start(editor, []),\n focus: SlateEditor.end(editor, []),\n },\n });\n }, [editor]);\n\n const select = useCallback(() => {\n SlateTransforms.select(editor, {\n anchor: SlateEditor.end(editor, []),\n focus: SlateEditor.end(editor, []),\n });\n }, [editor]);\n\n const focus = useCallback(\n (resetSelection = true) => {\n if (!ReactEditor.isFocused(editor)) {\n SlateTransforms.select(\n editor,\n resetSelection || !editor.selection\n ? SlateEditor.end(editor, [])\n : editor.selection\n );\n ReactEditor.focus(editor);\n }\n },\n [editor]\n );\n\n const blur = useCallback(() => {\n ReactEditor.blur(editor);\n }, [editor]);\n\n const createMention = useCallback(() => {\n focus();\n insertMentionCharacter(editor);\n }, [editor, focus]);\n\n const insertText = useCallback(\n (text: string) => {\n focus(false);\n insertSlateText(editor, text);\n },\n [editor, focus]\n );\n\n const attachFiles = useCallback(() => {\n if (fileInputRef.current) {\n fileInputRef.current.click();\n }\n }, []);\n\n const handleAttachmentsInputChange = useCallback(\n (event: ChangeEvent<HTMLInputElement>) => {\n if (event.target.files) {\n createAttachments(Array.from(event.target.files));\n }\n },\n [createAttachments]\n );\n\n const onSubmitEnd = useCallback(() => {\n clear();\n blur();\n clearAttachments();\n setSubmitting(false);\n }, [blur, clear, clearAttachments]);\n\n const handleSubmit = useCallback(\n (event: FormEvent<HTMLFormElement>) => {\n // In some situations (e.g. pressing Enter while composing diacritics), it's possible\n // for the form to be submitted as empty even though we already checked whether the\n // editor was empty when handling the key press.\n const isEmpty = isEditorEmpty(editor, editor.children);\n\n // We even prevent the user's `onSubmit` handler from being called if the editor is empty.\n if (isEmpty) {\n event.preventDefault();\n\n return;\n }\n\n onSubmit?.(event);\n\n if (!onComposerSubmit || event.isDefaultPrevented()) {\n event.preventDefault();\n\n return;\n }\n\n const body = composerBodyToCommentBody(\n editor.children as ComposerBodyData\n );\n // Only non-local attachments are included to be submitted.\n const commentAttachments: CommentAttachment[] = attachments\n .filter(\n (attachment) =>\n attachment.type === \"attachment\" ||\n (attachment.type === \"localAttachment\" &&\n attachment.status === \"uploaded\")\n )\n .map((attachment) => {\n return {\n id: attachment.id,\n type: \"attachment\",\n mimeType: attachment.mimeType,\n size: attachment.size,\n name: attachment.name,\n };\n });\n\n const promise = onComposerSubmit(\n { body, attachments: commentAttachments },\n event\n );\n\n event.preventDefault();\n\n if (promise) {\n setSubmitting(true);\n promise.then(onSubmitEnd);\n } else {\n onSubmitEnd();\n }\n },\n [editor, attachments, onComposerSubmit, onSubmit, onSubmitEnd]\n );\n\n return (\n <ComposerEditorContext.Provider\n value={{\n editor,\n validate,\n setFocused,\n }}\n >\n <ComposerAttachmentsContext.Provider\n value={{\n createAttachments,\n isUploadingAttachments,\n hasMaxAttachments,\n maxAttachments,\n maxAttachmentSize,\n }}\n >\n <ComposerContext.Provider\n value={{\n isDisabled,\n isFocused,\n isEmpty,\n canSubmit,\n submit,\n clear,\n select,\n focus,\n blur,\n createMention,\n insertText,\n attachments,\n attachFiles,\n removeAttachment,\n }}\n >\n <Component {...props} onSubmit={handleSubmit} ref={mergedRefs}>\n <input\n type=\"file\"\n multiple\n ref={fileInputRef}\n onChange={handleAttachmentsInputChange}\n tabIndex={-1}\n style={{ display: \"none\" }}\n />\n <Slottable>{children}</Slottable>\n </Component>\n </ComposerContext.Provider>\n </ComposerAttachmentsContext.Provider>\n </ComposerEditorContext.Provider>\n );\n }\n);\n\n/**\n * A button to submit the composer.\n *\n * @example\n * <Composer.Submit>Send</Composer.Submit>\n */\nconst ComposerSubmit = forwardRef<HTMLButtonElement, ComposerSubmitProps>(\n ({ children, disabled, asChild, ...props }, forwardedRef) => {\n const Component = asChild ? Slot : \"button\";\n const { canSubmit, isDisabled: isComposerDisabled } = useComposer();\n const isDisabled = isComposerDisabled || disabled || !canSubmit;\n\n return (\n <Component\n type=\"submit\"\n {...props}\n ref={forwardedRef}\n disabled={isDisabled}\n >\n {children}\n </Component>\n );\n }\n);\n\n/**\n * A button which opens a file picker to create attachments.\n *\n * @example\n * <Composer.AttachFiles>Attach files</Composer.AttachFiles>\n */\nconst ComposerAttachFiles = forwardRef<\n HTMLButtonElement,\n ComposerAttachFilesProps\n>(({ children, onClick, disabled, asChild, ...props }, forwardedRef) => {\n const Component = asChild ? Slot : \"button\";\n const { hasMaxAttachments } = useComposerAttachmentsContext();\n const { isDisabled: isComposerDisabled, attachFiles } = useComposer();\n const isDisabled = isComposerDisabled || hasMaxAttachments || disabled;\n\n const handleClick = useCallback(\n (event: MouseEvent<HTMLButtonElement>) => {\n onClick?.(event);\n\n if (!event.isDefaultPrevented()) {\n attachFiles();\n }\n },\n [attachFiles, onClick]\n );\n\n return (\n <Component\n {...props}\n onClick={handleClick}\n ref={forwardedRef}\n disabled={isDisabled}\n >\n {children}\n </Component>\n );\n});\n\n/**\n * A drop area which accepts files to create attachments.\n *\n * @example\n * <Composer.AttachmentsDropArea>\n * Drop files here\n * </Composer.AttachmentsDropArea>\n */\nconst ComposerAttachmentsDropArea = forwardRef<\n HTMLDivElement,\n ComposerAttachmentsDropAreaProps\n>(\n (\n {\n onDragEnter,\n onDragLeave,\n onDragOver,\n onDrop,\n disabled,\n asChild,\n ...props\n },\n forwardedRef\n ) => {\n const Component = asChild ? Slot : \"div\";\n const { isDisabled: isComposerDisabled } = useComposer();\n const isDisabled = isComposerDisabled || disabled;\n const [, dropAreaProps] = useComposerAttachmentsDropArea({\n onDragEnter,\n onDragLeave,\n onDragOver,\n onDrop,\n disabled: isDisabled,\n });\n\n return (\n <Component\n {...dropAreaProps}\n data-disabled={isDisabled ? \"\" : undefined}\n {...props}\n ref={forwardedRef}\n />\n );\n }\n);\n\nif (process.env.NODE_ENV !== \"production\") {\n ComposerAttachFiles.displayName = COMPOSER_ATTACH_FILES_NAME;\n ComposerAttachmentsDropArea.displayName = COMPOSER_ATTACHMENTS_DROP_AREA_NAME;\n ComposerEditor.displayName = COMPOSER_EDITOR_NAME;\n ComposerForm.displayName = COMPOSER_FORM_NAME;\n ComposerMention.displayName = COMPOSER_MENTION_NAME;\n ComposerLink.displayName = COMPOSER_LINK_NAME;\n ComposerSubmit.displayName = COMPOSER_SUBMIT_NAME;\n ComposerSuggestions.displayName = COMPOSER_SUGGESTIONS_NAME;\n ComposerSuggestionsList.displayName = COMPOSER_SUGGESTIONS_LIST_NAME;\n ComposerSuggestionsListItem.displayName = COMPOSER_SUGGESTIONS_LIST_ITEM_NAME;\n}\n\n// NOTE: Every export from this file will be available publicly as Composer.*\nexport {\n ComposerAttachFiles as AttachFiles,\n ComposerAttachmentsDropArea as AttachmentsDropArea,\n ComposerEditor as Editor,\n ComposerForm as Form,\n ComposerLink as Link,\n ComposerMention as Mention,\n ComposerSubmit as Submit,\n ComposerSuggestions as Suggestions,\n ComposerSuggestionsList as SuggestionsList,\n ComposerSuggestionsListItem as SuggestionsListItem,\n};\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsIA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AAAsC;AAC3B;AAEX;AAEA;AAA8B;AAC5B;AAEF;AAIE;AAAO;AACL;AACE;AACE;AACE;AACoD;AAChD;AACA;AACD;AACH;AACF;AACF;AACF;AAEJ;AAEA;AAAsC;AACpC;AACA;AACA;AAEF;AACE;AAEA;AACG;AAAS;AAEL;AAAwB;AAAI;AAKrC;AAEA;AAAmC;AACjC;AACA;AACA;AAEF;AACE;AAAa;AACiC;AAChC;AAGd;AACG;AAAS;AACP;AAAK;AAGZ;AAEA;AAAiD;AAC/C;AACA;AACA;AACA;AACA;AACA;AACA;AACW;AACX;AAEF;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACE;AAAqD;AAC1C;AAGX;AAAO;AACK;AACuC;AACrC;AACyC;AACzB;AACpB;AACD;AACiB;AACrB;AACI;AACA;AAED;AAAwB;AACtB;AACG;AAEL;AAAwB;AACtB;AACG;AACL;AACF;AACD;AACH;AACsB;AACxB;AAEF;AAAM;AAC8B;AAClC;AACA;AACA;AACA;AACA;AAKF;AACE;AACE;AAAwD;AAC1D;AAGF;AACE;AACE;AAAA;AAGF;AAEA;AACE;AAAa;AACiD;AACd;AAC/C;AACH;AAGF;AAGO;AACQ;AACL;AACA;AACe;AACG;AAClB;AACA;AACA;AACK;AACP;AAEC;AACM;AACM;AACJ;AACK;AACL;AACC;AAGF;AACM;AACF;AACV;AAEC;AACC;AACA;AAOd;AAEA;AAA+B;AAC7B;AACA;AAEF;AAEE;AAEA;AAAsB;AAElB;AACG;AACC;AACK;AACP;AAEC;AAEH;AACG;AACC;AACK;AAGP;AAGF;AACG;AAAM;AAA0C;AAEjD;AAGF;AAAO;AAEb;AAGA;AACE;AACE;AAA6B;AAG/B;AACE;AAAyB;AAG3B;AACE;AAAwB;AAG1B;AACE;AAA2B;AAG7B;AAAQ;AAAS;AACnB;AAEA;AAAmC;AACjC;AAEF;AACE;AAEA;AACG;AAAS;AAAY;AAA+B;AAIzD;AAQA;AAAwB;AAEpB;AACA;AAEA;AACG;AAC8B;AACzB;AACC;AAGP;AAGN;AAQA;AAAqB;AAEjB;AAEA;AACG;AACQ;AACH;AACA;AACC;AAGP;AAGN;AAKM;AAIJ;AACA;AACA;AAAM;AACC;AACL;AACA;AAEF;AACA;AAAsB;AACwB;AAClC;AAEZ;AACA;AAEA;AACG;AACC;AACI;AAC6B;AACtB;AACC;AACL;AACI;AACM;AACJ;AACA;AACR;AACL;AACK;AAKX;AAcM;AAIJ;AACA;AAEA;AACG;AACM;AACL;AACW;AACP;AACC;AAKX;AAUA;AAAoC;AAQhC;AACA;AACA;AAEA;AACA;AAAmB;AACO;AACH;AAGvB;AAEA;AACE;AACE;AAA+C;AACjD;AAGF;AAA0B;AAEtB;AAEA;AACE;AAAsB;AACxB;AACF;AACuC;AAGzC;AAA0B;AAEtB;AAEA;AACE;AAEA;AACE;AAA4C;AAG9C;AACE;AAEA;AAAqB;AACvB;AACF;AACF;AACmC;AAGrC;AACG;AACM;AACL;AAC6B;AACA;AACd;AACA;AACX;AACC;AAGP;AAGN;AAEA;AAA0D;AAEtD;AAAQ;AAAa;AAAsB;AAC7C;AAEE;AAIE;AAEJ;AAEE;AAIS;AAAiC;AAAe;AAMrD;AAER;AAQA;AAAuB;AAEnB;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACG;AAIL;AACA;AAAM;AACJ;AACA;AACA;AACA;AACY;AACZ;AAEF;AACA;AACA;AACE;AAA4C;AAE9C;AAA8C;AACO;AACxC;AAGb;AACA;AACA;AAAM;AACJ;AACA;AACA;AACA;AAEF;AACA;AAA0B;AACa;AAClC;AAEL;AAA8B;AAEoC;AAC7D;AAEL;AAAsB;AAElB;AACG;AAAsB;AAAkB;AAAgB;AAAO;AAEpE;AACc;AAGhB;AAAqB;AAEjB;AAEA;AAAkD;AACpD;AACiB;AAGnB;AAAsB;AAElB;AACE;AAAA;AAGF;AACA;AACA;AACA;AAAmC;AACrC;AACwD;AAG1D;AAAsB;AAElB;AAEA;AACE;AAAA;AAIF;AACE;AAA6B;AAI/B;AACE;AAA2B;AAG7B;AAEE;AACE;AACA;AAAsC;AAIxC;AACE;AACA;AAA0C;AAI5C;AACE;AAEA;AACA;AAAoB;AAItB;AACE;AACA;AACA;AAAmC;AACrC;AAGA;AACE;AACA;AAAuB;AAIzB;AAEE;AAEA;AACE;AAAO;AACT;AAIF;AACE;AACA;AAAmB;AAIrB;AACE;AACA;AAAyB;AAI3B;AACE;AACA;AAA2B;AAI7B;AACE;AACA;AAAkC;AAIpC;AACE;AACA;AAAyB;AAC3B;AACF;AACF;AACA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF;AAGF;AAAoB;AAEhB;AAEA;AACE;AAAe;AACjB;AACF;AACoB;AAGtB;AAAmB;AAEf;AAEA;AACE;AAAgB;AAClB;AACF;AACmB;AAGrB;AAAwC;AACX;AACwB;AAErD;AAA2C;AAEvC;AAEA;AACE;AAAuC;AACzC;AACF;AACsD;AAGxD;AAA6C;AAGrC;AACQ;AACe;AACJ;AACA;AACQ;AACvB;AACF;AAED;AACP;AACE;AACA;AACA;AACA;AACF;AAGF;AACE;AAA2C;AAI7C;AACE;AACE;AAAM;AACR;AAKF;AACE;AACE;AAAO;AACT;AAGF;AACG;AACC;AACc;AACJ;AAET;AACC;AACuC;AACxB;AACJ;AACgB;AACE;AACzB;AACA;AACM;AACA;AACC;AACF;AACD;AACR;AACY;AACO;AAEpB;AACC;AACA;AACgB;AACG;AACV;AACL;AACI;AACM;AACd;AAEJ;AAGN;AAGA;AACA;AAWA;AAAqB;AAEjB;AACE;AACA;AACA;AACsB;AACtB;AACA;AACA;AACG;AAIL;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAAM;AACJ;AACA;AACA;AACA;AACA;AACoD;AACvC;AAEf;AACA;AACA;AACE;AACA;AAEA;AAAoC;AAEtC;AACE;AAAoB;AAEtB;AACA;AACA;AAEA;AAA0B;AAEtB;AACE;AAAA;AAGF;AAAmC;AACjC;AACiB;AAGnB;AAEA;AAEA;AAA0B;AAC5B;AAC0D;AAG5D;AAAe;AACsD;AAGrE;AAAiB;AAEb;AAAqC;AACvC;AACO;AAGT;AACE;AACE;AAAA;AAKF;AACE;AACE;AAAyB;AAC3B;AACD;AAGH;AACE;AAA+B;AACzB;AACkC;AACH;AACnC;AACD;AAGH;AACE;AAA+B;AACK;AACD;AAClC;AAGH;AAAc;AAEV;AACE;AAAgB;AACd;AAGW;AAEb;AAAwB;AAC1B;AACF;AACO;AAGT;AACE;AAAuB;AAGzB;AACE;AACA;AAA6B;AAG/B;AAAmB;AAEf;AACA;AAA4B;AAC9B;AACc;AAGhB;AACE;AACE;AAA2B;AAC7B;AAGF;AAAqC;AAEjC;AACE;AAAgD;AAClD;AACF;AACkB;AAGpB;AACE;AACA;AACA;AACA;AAAmB;AAGrB;AAAqB;AAKjB;AAGA;AACE;AAEA;AAAA;AAGF;AAEA;AACE;AAEA;AAAA;AAGF;AAAa;AACJ;AAGT;AACG;AAI2B;AAG1B;AAAO;AACU;AACT;AACe;AACJ;AACA;AACnB;AAGJ;AAAgB;AAC0B;AACxC;AAGF;AAEA;AACE;AACA;AAAwB;AAExB;AAAY;AACd;AACF;AAC6D;AAG/D;AACG;AACQ;AACL;AACA;AACA;AACF;AAEC;AACQ;AACL;AACA;AACA;AACA;AACA;AACF;AAEC;AACQ;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF;AAEC;AAAc;AAAiB;AAAmB;AAChD;AACM;AACG;AACH;AACK;AACA;AACe;AAMnC;AAGN;AAQA;AAAuB;AAEnB;AACA;AACA;AAEA;AACG;AACM;AACD;AACC;AACK;AAGZ;AAGN;AAQM;AAIJ;AACA;AACA;AACA;AAEA;AAAoB;AAEhB;AAEA;AACE;AAAY;AACd;AACF;AACqB;AAGvB;AACG;AACK;AACK;AACJ;AACK;AAKhB;AAUA;AAAoC;AAKhC;AACE;AACA;AACA;AACA;AACA;AACA;AACG;AAIL;AACA;AACA;AACA;AAAyD;AACvD;AACA;AACA;AACA;AACU;AAGZ;AACG;AACK;AAC6B;AAC7B;AACC;AACP;AAGN;AAEA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF;;;;;;;;;;;"}
|
|
@@ -14,7 +14,7 @@ import { withAutoLinks } from '../../slate/plugins/auto-links.mjs';
|
|
|
14
14
|
import { withCustomLinks } from '../../slate/plugins/custom-links.mjs';
|
|
15
15
|
import { withEmptyClearFormatting } from '../../slate/plugins/empty-clear-formatting.mjs';
|
|
16
16
|
import { getMentionDraftAtSelection, insertMention, insertMentionCharacter, MENTION_CHARACTER, withMentions } from '../../slate/plugins/mentions.mjs';
|
|
17
|
-
import {
|
|
17
|
+
import { withPaste } from '../../slate/plugins/paste.mjs';
|
|
18
18
|
import { getDOMRange } from '../../slate/utils/get-dom-range.mjs';
|
|
19
19
|
import { isEmpty } from '../../slate/utils/is-empty.mjs';
|
|
20
20
|
import { leaveMarkEdge, toggleMark } from '../../slate/utils/marks.mjs';
|
|
@@ -46,13 +46,19 @@ const emptyCommentBody = {
|
|
|
46
46
|
version: 1,
|
|
47
47
|
content: [{ type: "paragraph", children: [{ text: "" }] }]
|
|
48
48
|
};
|
|
49
|
-
function createComposerEditor(
|
|
49
|
+
function createComposerEditor({
|
|
50
|
+
createAttachments,
|
|
51
|
+
pasteFilesAsAttachments
|
|
52
|
+
}) {
|
|
50
53
|
return withMentions(
|
|
51
54
|
withCustomLinks(
|
|
52
55
|
withAutoLinks(
|
|
53
56
|
withAutoFormatting(
|
|
54
57
|
withEmptyClearFormatting(
|
|
55
|
-
|
|
58
|
+
withPaste(withHistory(withReact(createEditor())), {
|
|
59
|
+
createAttachments,
|
|
60
|
+
pasteFilesAsAttachments
|
|
61
|
+
})
|
|
56
62
|
)
|
|
57
63
|
)
|
|
58
64
|
)
|
|
@@ -635,12 +641,12 @@ const ComposerForm = forwardRef(
|
|
|
635
641
|
onSubmit,
|
|
636
642
|
onComposerSubmit,
|
|
637
643
|
defaultAttachments = [],
|
|
644
|
+
pasteFilesAsAttachments,
|
|
638
645
|
disabled,
|
|
639
646
|
asChild,
|
|
640
647
|
...props
|
|
641
648
|
}, forwardedRef) => {
|
|
642
649
|
const Component = asChild ? Slot : "form";
|
|
643
|
-
const editor = useInitial(createComposerEditor);
|
|
644
650
|
const room = useRoom();
|
|
645
651
|
const [isEmpty$1, setEmpty] = useState(true);
|
|
646
652
|
const [isSubmitting, setSubmitting] = useState(false);
|
|
@@ -650,14 +656,14 @@ const ComposerForm = forwardRef(
|
|
|
650
656
|
const {
|
|
651
657
|
attachments,
|
|
652
658
|
isUploadingAttachments,
|
|
653
|
-
|
|
659
|
+
addAttachments,
|
|
654
660
|
removeAttachment,
|
|
655
661
|
clearAttachments
|
|
656
662
|
} = useComposerAttachmentsManager(defaultAttachments, {
|
|
657
663
|
maxFileSize: maxAttachmentSize
|
|
658
664
|
});
|
|
659
665
|
const numberOfAttachments = attachments.length;
|
|
660
|
-
const
|
|
666
|
+
const hasMaxAttachments = numberOfAttachments >= maxAttachments;
|
|
661
667
|
const isDisabled = useMemo(() => {
|
|
662
668
|
const self = room.getSelf();
|
|
663
669
|
const canComment = self?.canComment ?? true;
|
|
@@ -669,6 +675,24 @@ const ComposerForm = forwardRef(
|
|
|
669
675
|
const ref = useRef(null);
|
|
670
676
|
const mergedRefs = useRefs(forwardedRef, ref);
|
|
671
677
|
const fileInputRef = useRef(null);
|
|
678
|
+
const createAttachments = useCallback(
|
|
679
|
+
(files) => {
|
|
680
|
+
if (!files.length) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const numberOfAcceptedFiles = Math.max(
|
|
684
|
+
0,
|
|
685
|
+
maxAttachments - numberOfAttachments
|
|
686
|
+
);
|
|
687
|
+
files.splice(numberOfAcceptedFiles);
|
|
688
|
+
const attachments2 = files.map((file) => room.prepareAttachment(file));
|
|
689
|
+
addAttachments(attachments2);
|
|
690
|
+
},
|
|
691
|
+
[addAttachments, maxAttachments, numberOfAttachments, room]
|
|
692
|
+
);
|
|
693
|
+
const editor = useInitial(
|
|
694
|
+
() => createComposerEditor({ createAttachments, pasteFilesAsAttachments })
|
|
695
|
+
);
|
|
672
696
|
const validate = useCallback(
|
|
673
697
|
(value) => {
|
|
674
698
|
setEmpty(isEmpty(editor, value));
|
|
@@ -725,23 +749,6 @@ const ComposerForm = forwardRef(
|
|
|
725
749
|
},
|
|
726
750
|
[editor, focus]
|
|
727
751
|
);
|
|
728
|
-
const createAttachments = useCallback(
|
|
729
|
-
(files) => {
|
|
730
|
-
if (!files.length) {
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
const numberOfAcceptedFiles = Math.max(
|
|
734
|
-
0,
|
|
735
|
-
maxAttachments - numberOfAttachments
|
|
736
|
-
);
|
|
737
|
-
files.splice(numberOfAcceptedFiles);
|
|
738
|
-
for (const file of files) {
|
|
739
|
-
const attachment = room.prepareAttachment(file);
|
|
740
|
-
addAttachment(attachment);
|
|
741
|
-
}
|
|
742
|
-
},
|
|
743
|
-
[addAttachment, maxAttachments, numberOfAttachments, room]
|
|
744
|
-
);
|
|
745
752
|
const attachFiles = useCallback(() => {
|
|
746
753
|
if (fileInputRef.current) {
|
|
747
754
|
fileInputRef.current.click();
|
|
@@ -811,7 +818,7 @@ const ComposerForm = forwardRef(
|
|
|
811
818
|
value: {
|
|
812
819
|
createAttachments,
|
|
813
820
|
isUploadingAttachments,
|
|
814
|
-
|
|
821
|
+
hasMaxAttachments,
|
|
815
822
|
maxAttachments,
|
|
816
823
|
maxAttachmentSize
|
|
817
824
|
}
|
|
@@ -861,9 +868,9 @@ const ComposerSubmit = forwardRef(
|
|
|
861
868
|
);
|
|
862
869
|
const ComposerAttachFiles = forwardRef(({ children, onClick, disabled, asChild, ...props }, forwardedRef) => {
|
|
863
870
|
const Component = asChild ? Slot : "button";
|
|
864
|
-
const {
|
|
871
|
+
const { hasMaxAttachments } = useComposerAttachmentsContext();
|
|
865
872
|
const { isDisabled: isComposerDisabled, attachFiles } = useComposer();
|
|
866
|
-
const isDisabled = isComposerDisabled ||
|
|
873
|
+
const isDisabled = isComposerDisabled || hasMaxAttachments || disabled;
|
|
867
874
|
const handleClick = useCallback(
|
|
868
875
|
(event) => {
|
|
869
876
|
onClick?.(event);
|