@liveblocks/react-ui 2.7.2 → 2.8.0-beta2

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.
Files changed (126) hide show
  1. package/dist/_private/index.d.mts +5 -3
  2. package/dist/_private/index.d.ts +5 -3
  3. package/dist/_private/index.js +4 -2
  4. package/dist/_private/index.js.map +1 -1
  5. package/dist/_private/index.mjs +2 -1
  6. package/dist/_private/index.mjs.map +1 -1
  7. package/dist/components/Comment.js +106 -3
  8. package/dist/components/Comment.js.map +1 -1
  9. package/dist/components/Comment.mjs +107 -5
  10. package/dist/components/Comment.mjs.map +1 -1
  11. package/dist/components/Composer.js +216 -103
  12. package/dist/components/Composer.js.map +1 -1
  13. package/dist/components/Composer.mjs +220 -107
  14. package/dist/components/Composer.mjs.map +1 -1
  15. package/dist/components/HistoryVersionSummary.js +5 -0
  16. package/dist/components/HistoryVersionSummary.js.map +1 -1
  17. package/dist/components/HistoryVersionSummary.mjs +5 -0
  18. package/dist/components/HistoryVersionSummary.mjs.map +1 -1
  19. package/dist/components/InboxNotification.js +20 -4
  20. package/dist/components/InboxNotification.js.map +1 -1
  21. package/dist/components/InboxNotification.mjs +20 -4
  22. package/dist/components/InboxNotification.mjs.map +1 -1
  23. package/dist/components/Thread.js +5 -0
  24. package/dist/components/Thread.js.map +1 -1
  25. package/dist/components/Thread.mjs +5 -0
  26. package/dist/components/Thread.mjs.map +1 -1
  27. package/dist/components/internal/Attachment.js +314 -0
  28. package/dist/components/internal/Attachment.js.map +1 -0
  29. package/dist/components/internal/Attachment.mjs +310 -0
  30. package/dist/components/internal/Attachment.mjs.map +1 -0
  31. package/dist/components/internal/InboxNotificationThread.js +12 -2
  32. package/dist/components/internal/InboxNotificationThread.js.map +1 -1
  33. package/dist/components/internal/InboxNotificationThread.mjs +13 -3
  34. package/dist/components/internal/InboxNotificationThread.mjs.map +1 -1
  35. package/dist/icons/Attachment.js +15 -0
  36. package/dist/icons/Attachment.js.map +1 -0
  37. package/dist/icons/Attachment.mjs +13 -0
  38. package/dist/icons/Attachment.mjs.map +1 -0
  39. package/dist/icons/Spinner.js +3 -9
  40. package/dist/icons/Spinner.js.map +1 -1
  41. package/dist/icons/Spinner.mjs +4 -10
  42. package/dist/icons/Spinner.mjs.map +1 -1
  43. package/dist/icons/{Missing.js → Warning.js} +3 -3
  44. package/dist/icons/{Missing.js.map → Warning.js.map} +1 -1
  45. package/dist/icons/{Missing.mjs → Warning.mjs} +3 -3
  46. package/dist/icons/{Missing.mjs.map → Warning.mjs.map} +1 -1
  47. package/dist/index.d.mts +73 -4
  48. package/dist/index.d.ts +73 -4
  49. package/dist/index.js.map +1 -1
  50. package/dist/index.mjs.map +1 -1
  51. package/dist/overrides.js +5 -0
  52. package/dist/overrides.js.map +1 -1
  53. package/dist/overrides.mjs +5 -0
  54. package/dist/overrides.mjs.map +1 -1
  55. package/dist/primitives/Composer/contexts.js +17 -3
  56. package/dist/primitives/Composer/contexts.js.map +1 -1
  57. package/dist/primitives/Composer/contexts.mjs +15 -4
  58. package/dist/primitives/Composer/contexts.mjs.map +1 -1
  59. package/dist/primitives/Composer/index.js +207 -30
  60. package/dist/primitives/Composer/index.js.map +1 -1
  61. package/dist/primitives/Composer/index.mjs +210 -35
  62. package/dist/primitives/Composer/index.mjs.map +1 -1
  63. package/dist/primitives/Composer/utils.js +231 -0
  64. package/dist/primitives/Composer/utils.js.map +1 -1
  65. package/dist/primitives/Composer/utils.mjs +229 -1
  66. package/dist/primitives/Composer/utils.mjs.map +1 -1
  67. package/dist/primitives/EmojiPicker/utils.js +2 -2
  68. package/dist/primitives/EmojiPicker/utils.js.map +1 -1
  69. package/dist/primitives/EmojiPicker/utils.mjs +1 -1
  70. package/dist/primitives/EmojiPicker/utils.mjs.map +1 -1
  71. package/dist/primitives/FileSize.js +33 -0
  72. package/dist/primitives/FileSize.js.map +1 -0
  73. package/dist/primitives/FileSize.mjs +31 -0
  74. package/dist/primitives/FileSize.mjs.map +1 -0
  75. package/dist/primitives/index.d.mts +87 -3
  76. package/dist/primitives/index.d.ts +87 -3
  77. package/dist/primitives/index.js +4 -0
  78. package/dist/primitives/index.js.map +1 -1
  79. package/dist/primitives/index.mjs +2 -0
  80. package/dist/primitives/index.mjs.map +1 -1
  81. package/dist/slate/plugins/{paste-html.js → paste.js} +16 -5
  82. package/dist/slate/plugins/paste.js.map +1 -0
  83. package/dist/slate/plugins/{paste-html.mjs → paste.mjs} +16 -5
  84. package/dist/slate/plugins/paste.mjs.map +1 -0
  85. package/dist/utils/data-transfer.js +20 -0
  86. package/dist/utils/data-transfer.js.map +1 -0
  87. package/dist/utils/data-transfer.mjs +18 -0
  88. package/dist/utils/data-transfer.mjs.map +1 -0
  89. package/dist/utils/download.js +14 -0
  90. package/dist/utils/download.js.map +1 -0
  91. package/dist/utils/download.mjs +12 -0
  92. package/dist/utils/download.mjs.map +1 -0
  93. package/dist/utils/format-file-size.js +45 -0
  94. package/dist/utils/format-file-size.js.map +1 -0
  95. package/dist/utils/format-file-size.mjs +43 -0
  96. package/dist/utils/format-file-size.mjs.map +1 -0
  97. package/dist/utils/intl.js +6 -0
  98. package/dist/utils/intl.js.map +1 -1
  99. package/dist/utils/intl.mjs +6 -1
  100. package/dist/utils/intl.mjs.map +1 -1
  101. package/dist/utils/use-initial.js +2 -1
  102. package/dist/utils/use-initial.js.map +1 -1
  103. package/dist/utils/use-initial.mjs +3 -2
  104. package/dist/utils/use-initial.mjs.map +1 -1
  105. package/dist/utils/use-latest.js.map +1 -1
  106. package/dist/utils/use-latest.mjs.map +1 -1
  107. package/dist/version.js +1 -1
  108. package/dist/version.js.map +1 -1
  109. package/dist/version.mjs +1 -1
  110. package/dist/version.mjs.map +1 -1
  111. package/package.json +4 -4
  112. package/src/styles/dark/index.css +1 -0
  113. package/src/styles/index.css +343 -62
  114. package/src/styles/utils.css +44 -0
  115. package/styles/dark/attributes.css +1 -1
  116. package/styles/dark/attributes.css.map +1 -1
  117. package/styles/dark/media-query.css +1 -1
  118. package/styles/dark/media-query.css.map +1 -1
  119. package/styles.css +1 -1
  120. package/styles.css.map +1 -1
  121. package/dist/slate/plugins/paste-html.js.map +0 -1
  122. package/dist/slate/plugins/paste-html.mjs.map +0 -1
  123. package/dist/utils/chunk.js +0 -12
  124. package/dist/utils/chunk.js.map +0 -1
  125. package/dist/utils/chunk.mjs +0 -10
  126. package/dist/utils/chunk.mjs.map +0 -1
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { flip, hide, shift, limitShift, size, autoUpdate, useFloating } from '@floating-ui/react-dom';
3
- import { useSelf } from '@liveblocks/react';
4
- import { Slot } from '@radix-ui/react-slot';
3
+ import { useRoom } from '@liveblocks/react';
4
+ import { Slottable, Slot } from '@radix-ui/react-slot';
5
5
  import React__default, { forwardRef, useRef, useMemo, useEffect, useCallback, useState, useImperativeHandle } from 'react';
6
6
  import { Transforms, Editor, insertText, createEditor } from 'slate';
7
7
  import { withHistory } from 'slate-history';
@@ -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 { withPasteHtml } from '../../slate/plugins/paste-html.mjs';
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';
@@ -28,8 +28,8 @@ import { useInitial } from '../../utils/use-initial.mjs';
28
28
  import { useLayoutEffect } from '../../utils/use-layout-effect.mjs';
29
29
  import { useRefs } from '../../utils/use-refs.mjs';
30
30
  import { toAbsoluteUrl } from '../Comment/utils.mjs';
31
- import { useComposerSuggestionsContext, useComposerEditorContext, useComposer, ComposerEditorContext, ComposerContext, ComposerSuggestionsContext } from './contexts.mjs';
32
- import { getSideAndAlignFromPlacement, commentBodyToComposerBody, composerBodyToCommentBody, getPlacementFromPosition } from './utils.mjs';
31
+ import { useComposerSuggestionsContext, useComposerEditorContext, useComposer, ComposerEditorContext, ComposerAttachmentsContext, ComposerContext, useComposerAttachmentsContext, ComposerSuggestionsContext } from './contexts.mjs';
32
+ import { getSideAndAlignFromPlacement, commentBodyToComposerBody, useComposerAttachmentsManager, composerBodyToCommentBody, useComposerAttachmentsDropArea, getPlacementFromPosition } from './utils.mjs';
33
33
 
34
34
  const MENTION_SUGGESTIONS_POSITION = "top";
35
35
  const COMPOSER_MENTION_NAME = "ComposerMention";
@@ -39,18 +39,26 @@ const COMPOSER_SUGGESTIONS_LIST_NAME = "ComposerSuggestionsList";
39
39
  const COMPOSER_SUGGESTIONS_LIST_ITEM_NAME = "ComposerSuggestionsListItem";
40
40
  const COMPOSER_SUBMIT_NAME = "ComposerSubmit";
41
41
  const COMPOSER_EDITOR_NAME = "ComposerEditor";
42
+ const COMPOSER_ATTACH_FILES_NAME = "ComposerAttachFiles";
43
+ const COMPOSER_ATTACHMENTS_DROP_AREA_NAME = "ComposerAttachmentsDropArea";
42
44
  const COMPOSER_FORM_NAME = "ComposerForm";
43
45
  const emptyCommentBody = {
44
46
  version: 1,
45
47
  content: [{ type: "paragraph", children: [{ text: "" }] }]
46
48
  };
47
- function createComposerEditor() {
49
+ function createComposerEditor({
50
+ createAttachments,
51
+ pasteFilesAsAttachments
52
+ }) {
48
53
  return withMentions(
49
54
  withCustomLinks(
50
55
  withAutoLinks(
51
56
  withAutoFormatting(
52
57
  withEmptyClearFormatting(
53
- withPasteHtml(withHistory(withReact(createEditor())))
58
+ withPaste(withHistory(withReact(createEditor())), {
59
+ createAttachments,
60
+ pasteFilesAsAttachments
61
+ })
54
62
  )
55
63
  )
56
64
  )
@@ -392,13 +400,16 @@ const ComposerEditor = forwardRef(
392
400
  dir,
393
401
  ...props
394
402
  }, forwardedRef) => {
395
- const self = useSelf();
396
- const isDisabled = useMemo(
397
- () => disabled || (self ? !self.canComment : false),
398
- [disabled, self?.canComment]
399
- );
400
403
  const { editor, validate, setFocused } = useComposerEditorContext();
401
- const { submit, focus, select, isEmpty, isFocused } = useComposer();
404
+ const {
405
+ submit,
406
+ focus,
407
+ select,
408
+ canSubmit,
409
+ isDisabled: isComposerDisabled,
410
+ isFocused
411
+ } = useComposer();
412
+ const isDisabled = isComposerDisabled || disabled;
402
413
  const initialBody = useInitial(defaultValue ?? emptyCommentBody);
403
414
  const initialEditorValue = useMemo(() => {
404
415
  return commentBodyToComposerBody(initialBody);
@@ -489,9 +500,11 @@ const ComposerEditor = forwardRef(
489
500
  event.preventDefault();
490
501
  ReactEditor.blur(editor);
491
502
  }
492
- if (isKey(event, "Enter", { shift: false }) && !isEmpty) {
503
+ if (isKey(event, "Enter", { shift: false })) {
493
504
  event.preventDefault();
494
- submit();
505
+ if (canSubmit) {
506
+ submit();
507
+ }
495
508
  }
496
509
  if (isKey(event, "Enter", { shift: true })) {
497
510
  event.preventDefault();
@@ -518,7 +531,7 @@ const ComposerEditor = forwardRef(
518
531
  [
519
532
  createMention,
520
533
  editor,
521
- isEmpty,
534
+ canSubmit,
522
535
  mentionDraft,
523
536
  mentionSuggestions,
524
537
  selectedMentionSuggestionIndex,
@@ -624,14 +637,76 @@ const ComposerEditor = forwardRef(
624
637
  }));
625
638
  }
626
639
  );
640
+ const MAX_ATTACHMENTS = 10;
641
+ const MAX_ATTACHMENT_SIZE = 1024 * 1024 * 1024;
627
642
  const ComposerForm = forwardRef(
628
- ({ children, onSubmit, onComposerSubmit, asChild, ...props }, forwardedRef) => {
643
+ ({
644
+ children,
645
+ onSubmit,
646
+ onComposerSubmit,
647
+ defaultAttachments = [],
648
+ pasteFilesAsAttachments,
649
+ disabled,
650
+ asChild,
651
+ ...props
652
+ }, forwardedRef) => {
629
653
  const Component = asChild ? Slot : "form";
630
- const editor = useInitial(createComposerEditor);
654
+ const room = useRoom();
631
655
  const [isEmpty$1, setEmpty] = useState(true);
656
+ const [isSubmitting, setSubmitting] = useState(false);
632
657
  const [isFocused, setFocused] = useState(false);
658
+ const maxAttachments = MAX_ATTACHMENTS;
659
+ const maxAttachmentSize = MAX_ATTACHMENT_SIZE;
660
+ const {
661
+ attachments,
662
+ isUploadingAttachments,
663
+ addAttachments,
664
+ removeAttachment,
665
+ clearAttachments
666
+ } = useComposerAttachmentsManager(defaultAttachments, {
667
+ maxFileSize: maxAttachmentSize
668
+ });
669
+ const numberOfAttachments = attachments.length;
670
+ const hasMaxAttachments = numberOfAttachments >= maxAttachments;
671
+ const isDisabled = useMemo(() => {
672
+ const self = room.getSelf();
673
+ const canComment = self?.canComment ?? true;
674
+ return isSubmitting || disabled || !canComment;
675
+ }, [isSubmitting, disabled, room]);
676
+ const canSubmit = useMemo(() => {
677
+ return !isEmpty$1 && !isUploadingAttachments;
678
+ }, [isEmpty$1, isUploadingAttachments]);
633
679
  const ref = useRef(null);
634
680
  const mergedRefs = useRefs(forwardedRef, ref);
681
+ const fileInputRef = useRef(null);
682
+ const createAttachments = useCallback(
683
+ (files) => {
684
+ if (!files.length) {
685
+ return;
686
+ }
687
+ const numberOfAcceptedFiles = Math.max(
688
+ 0,
689
+ maxAttachments - numberOfAttachments
690
+ );
691
+ files.splice(numberOfAcceptedFiles);
692
+ const attachments2 = files.map((file) => room.prepareAttachment(file));
693
+ addAttachments(attachments2);
694
+ },
695
+ [addAttachments, maxAttachments, numberOfAttachments, room]
696
+ );
697
+ const createAttachmentsRef = useRef(createAttachments);
698
+ useEffect(() => {
699
+ createAttachmentsRef.current = createAttachments;
700
+ }, [createAttachments]);
701
+ const stableCreateAttachments = useCallback((files) => {
702
+ createAttachmentsRef.current(files);
703
+ }, []);
704
+ const editor = useInitial(
705
+ () => createComposerEditor({
706
+ createAttachments: stableCreateAttachments,
707
+ pasteFilesAsAttachments
708
+ })
709
+ );
635
710
  const validate = useCallback(
636
711
  (value) => {
637
712
  setEmpty(isEmpty(editor, value));
@@ -639,12 +714,15 @@ const ComposerForm = forwardRef(
639
714
  [editor]
640
715
  );
641
716
  const submit = useCallback(() => {
717
+ if (!canSubmit) {
718
+ return;
719
+ }
642
720
  requestAnimationFrame(() => {
643
721
  if (ref.current) {
644
722
  requestSubmit(ref.current);
645
723
  }
646
724
  });
647
- }, []);
725
+ }, [canSubmit]);
648
726
  const clear = useCallback(() => {
649
727
  Transforms.delete(editor, {
650
728
  at: {
@@ -674,10 +752,6 @@ const ComposerForm = forwardRef(
674
752
  const blur = useCallback(() => {
675
753
  ReactEditor.blur(editor);
676
754
  }, [editor]);
677
- const onSubmitEnd = useCallback(() => {
678
- clear();
679
- blur();
680
- }, [blur, clear]);
681
755
  const createMention = useCallback(() => {
682
756
  focus();
683
757
  insertMentionCharacter(editor);
@@ -689,6 +763,25 @@ const ComposerForm = forwardRef(
689
763
  },
690
764
  [editor, focus]
691
765
  );
766
+ const attachFiles = useCallback(() => {
767
+ if (fileInputRef.current) {
768
+ fileInputRef.current.click();
769
+ }
770
+ }, []);
771
+ const handleAttachmentsInputChange = useCallback(
772
+ (event) => {
773
+ if (event.target.files) {
774
+ createAttachments(Array.from(event.target.files));
775
+ }
776
+ },
777
+ [createAttachments]
778
+ );
779
+ const onSubmitEnd = useCallback(() => {
780
+ clear();
781
+ blur();
782
+ clearAttachments();
783
+ setSubmitting(false);
784
+ }, [blur, clear, clearAttachments]);
692
785
  const handleSubmit = useCallback(
693
786
  (event) => {
694
787
  const isEmpty2 = isEmpty(editor, editor.children);
@@ -704,16 +797,30 @@ const ComposerForm = forwardRef(
704
797
  const body = composerBodyToCommentBody(
705
798
  editor.children
706
799
  );
707
- const comment = { body };
708
- const promise = onComposerSubmit(comment, event);
800
+ const commentAttachments = attachments.filter(
801
+ (attachment) => attachment.type === "attachment" || attachment.type === "localAttachment" && attachment.status === "uploaded"
802
+ ).map((attachment) => {
803
+ return {
804
+ id: attachment.id,
805
+ type: "attachment",
806
+ mimeType: attachment.mimeType,
807
+ size: attachment.size,
808
+ name: attachment.name
809
+ };
810
+ });
811
+ const promise = onComposerSubmit(
812
+ { body, attachments: commentAttachments },
813
+ event
814
+ );
709
815
  event.preventDefault();
710
816
  if (promise) {
817
+ setSubmitting(true);
711
818
  promise.then(onSubmitEnd);
712
819
  } else {
713
820
  onSubmitEnd();
714
821
  }
715
822
  },
716
- [editor, onComposerSubmit, onSubmit, onSubmitEnd]
823
+ [editor, attachments, onComposerSubmit, onSubmit, onSubmitEnd]
717
824
  );
718
825
  return /* @__PURE__ */ React__default.createElement(ComposerEditorContext.Provider, {
719
826
  value: {
@@ -721,34 +828,50 @@ const ComposerForm = forwardRef(
721
828
  validate,
722
829
  setFocused
723
830
  }
831
+ }, /* @__PURE__ */ React__default.createElement(ComposerAttachmentsContext.Provider, {
832
+ value: {
833
+ createAttachments,
834
+ isUploadingAttachments,
835
+ hasMaxAttachments,
836
+ maxAttachments,
837
+ maxAttachmentSize
838
+ }
724
839
  }, /* @__PURE__ */ React__default.createElement(ComposerContext.Provider, {
725
840
  value: {
841
+ isDisabled,
726
842
  isFocused,
727
843
  isEmpty: isEmpty$1,
844
+ canSubmit,
728
845
  submit,
729
846
  clear,
730
847
  select,
731
848
  focus,
732
849
  blur,
733
850
  createMention,
734
- insertText: insertText$1
851
+ insertText: insertText$1,
852
+ attachments,
853
+ attachFiles,
854
+ removeAttachment
735
855
  }
736
856
  }, /* @__PURE__ */ React__default.createElement(Component, {
737
857
  ...props,
738
858
  onSubmit: handleSubmit,
739
859
  ref: mergedRefs
740
- }, children)));
860
+ }, /* @__PURE__ */ React__default.createElement("input", {
861
+ type: "file",
862
+ multiple: true,
863
+ ref: fileInputRef,
864
+ onChange: handleAttachmentsInputChange,
865
+ tabIndex: -1,
866
+ style: { display: "none" }
867
+ }), /* @__PURE__ */ React__default.createElement(Slottable, null, children)))));
741
868
  }
742
869
  );
743
870
  const ComposerSubmit = forwardRef(
744
871
  ({ children, disabled, asChild, ...props }, forwardedRef) => {
745
872
  const Component = asChild ? Slot : "button";
746
- const { isEmpty } = useComposer();
747
- const self = useSelf();
748
- const isDisabled = useMemo(
749
- () => disabled || isEmpty || (self ? !self.canComment : false),
750
- [disabled, isEmpty, self?.canComment]
751
- );
873
+ const { canSubmit, isDisabled: isComposerDisabled } = useComposer();
874
+ const isDisabled = isComposerDisabled || disabled || !canSubmit;
752
875
  return /* @__PURE__ */ React__default.createElement(Component, {
753
876
  type: "submit",
754
877
  ...props,
@@ -757,7 +880,59 @@ const ComposerSubmit = forwardRef(
757
880
  }, children);
758
881
  }
759
882
  );
883
+ const ComposerAttachFiles = forwardRef(({ children, onClick, disabled, asChild, ...props }, forwardedRef) => {
884
+ const Component = asChild ? Slot : "button";
885
+ const { hasMaxAttachments } = useComposerAttachmentsContext();
886
+ const { isDisabled: isComposerDisabled, attachFiles } = useComposer();
887
+ const isDisabled = isComposerDisabled || hasMaxAttachments || disabled;
888
+ const handleClick = useCallback(
889
+ (event) => {
890
+ onClick?.(event);
891
+ if (!event.isDefaultPrevented()) {
892
+ attachFiles();
893
+ }
894
+ },
895
+ [attachFiles, onClick]
896
+ );
897
+ return /* @__PURE__ */ React__default.createElement(Component, {
898
+ type: "button",
899
+ ...props,
900
+ onClick: handleClick,
901
+ ref: forwardedRef,
902
+ disabled: isDisabled
903
+ }, children);
904
+ });
905
+ const ComposerAttachmentsDropArea = forwardRef(
906
+ ({
907
+ onDragEnter,
908
+ onDragLeave,
909
+ onDragOver,
910
+ onDrop,
911
+ disabled,
912
+ asChild,
913
+ ...props
914
+ }, forwardedRef) => {
915
+ const Component = asChild ? Slot : "div";
916
+ const { isDisabled: isComposerDisabled } = useComposer();
917
+ const isDisabled = isComposerDisabled || disabled;
918
+ const [, dropAreaProps] = useComposerAttachmentsDropArea({
919
+ onDragEnter,
920
+ onDragLeave,
921
+ onDragOver,
922
+ onDrop,
923
+ disabled: isDisabled
924
+ });
925
+ return /* @__PURE__ */ React__default.createElement(Component, {
926
+ ...dropAreaProps,
927
+ "data-disabled": isDisabled ? "" : void 0,
928
+ ...props,
929
+ ref: forwardedRef
930
+ });
931
+ }
932
+ );
760
933
  if (process.env.NODE_ENV !== "production") {
934
+ ComposerAttachFiles.displayName = COMPOSER_ATTACH_FILES_NAME;
935
+ ComposerAttachmentsDropArea.displayName = COMPOSER_ATTACHMENTS_DROP_AREA_NAME;
761
936
  ComposerEditor.displayName = COMPOSER_EDITOR_NAME;
762
937
  ComposerForm.displayName = COMPOSER_FORM_NAME;
763
938
  ComposerMention.displayName = COMPOSER_MENTION_NAME;
@@ -768,5 +943,5 @@ if (process.env.NODE_ENV !== "production") {
768
943
  ComposerSuggestionsListItem.displayName = COMPOSER_SUGGESTIONS_LIST_ITEM_NAME;
769
944
  }
770
945
 
771
- export { ComposerEditor as Editor, ComposerForm as Form, ComposerLink as Link, ComposerMention as Mention, ComposerSubmit as Submit, ComposerSuggestions as Suggestions, ComposerSuggestionsList as SuggestionsList, ComposerSuggestionsListItem as SuggestionsListItem };
946
+ export { ComposerAttachFiles as AttachFiles, ComposerAttachmentsDropArea as AttachmentsDropArea, ComposerEditor as Editor, ComposerForm as Form, ComposerLink as Link, ComposerMention as Mention, ComposerSubmit as Submit, ComposerSuggestions as Suggestions, ComposerSuggestionsList as SuggestionsList, ComposerSuggestionsListItem as SuggestionsListItem };
772
947
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","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 { CommentBody } from \"@liveblocks/core\";\nimport { useSelf } from \"@liveblocks/react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport type {\n AriaAttributes,\n FocusEvent,\n FormEvent,\n KeyboardEvent,\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 ComposerContext,\n ComposerEditorContext,\n ComposerSuggestionsContext,\n useComposer,\n useComposerEditorContext,\n useComposerSuggestionsContext,\n} from \"./contexts\";\nimport type {\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} 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_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: (...args) => {\n return autoUpdate(...args, {\n animationFrame: true,\n });\n },\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 self = useSelf();\n const isDisabled = useMemo(\n () => disabled || (self ? !self.canComment : false),\n [disabled, self?.canComment] // eslint-disable-line react-hooks/exhaustive-deps\n );\n const { editor, validate, setFocused } = useComposerEditorContext();\n const { submit, focus, select, isEmpty, isFocused } = useComposer();\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 }) && !isEmpty) {\n event.preventDefault();\n submit();\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 isEmpty,\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/**\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 { children, onSubmit, onComposerSubmit, asChild, ...props },\n forwardedRef\n ) => {\n const Component = asChild ? Slot : \"form\";\n const editor = useInitial(createComposerEditor);\n const [isEmpty, setEmpty] = useState(true);\n const [isFocused, setFocused] = useState(false);\n const ref = useRef<HTMLFormElement>(null);\n const mergedRefs = useRefs(forwardedRef, ref);\n\n const validate = useCallback(\n (value: SlateElement[]) => {\n setEmpty(isEditorEmpty(editor, value));\n },\n [editor]\n );\n\n const submit = useCallback(() => {\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 }, []);\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 onSubmitEnd = useCallback(() => {\n clear();\n blur();\n }, [blur, clear]);\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 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 const comment = { body };\n\n const promise = onComposerSubmit(comment, event);\n\n event.preventDefault();\n\n if (promise) {\n promise.then(onSubmitEnd);\n } else {\n onSubmitEnd();\n }\n },\n [editor, onComposerSubmit, onSubmit, onSubmitEnd]\n );\n\n return (\n <ComposerEditorContext.Provider\n value={{\n editor,\n validate,\n setFocused,\n }}\n >\n <ComposerContext.Provider\n value={{\n isFocused,\n isEmpty,\n submit,\n clear,\n select,\n focus,\n blur,\n createMention,\n insertText,\n }}\n >\n <Component {...props} onSubmit={handleSubmit} ref={mergedRefs}>\n {children}\n </Component>\n </ComposerContext.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 { isEmpty } = useComposer();\n const self = useSelf();\n const isDisabled = useMemo(\n () => disabled || isEmpty || (self ? !self.canComment : false),\n [disabled, isEmpty, self?.canComment] // eslint-disable-line react-hooks/exhaustive-deps\n );\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\nif (process.env.NODE_ENV !== \"production\") {\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 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8HA;AAEA;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;AAEE;AAA2B;AACT;AACjB;AACH;AACF;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;AAAmB;AAC4B;AAClB;AAE7B;AACA;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;AACE;AACA;AAAO;AAIT;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;AAWA;AAAqB;AAKjB;AACA;AACA;AACA;AACA;AACA;AAEA;AAAiB;AAEb;AAAqC;AACvC;AACO;AAGT;AAGE;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;AAAK;AAGP;AACE;AACA;AAA6B;AAG/B;AAAmB;AAEf;AACA;AAA4B;AAC9B;AACc;AAGhB;AAAqB;AAKjB;AAGA;AACE;AAEA;AAAA;AAGF;AAEA;AACE;AAEA;AAAA;AAGF;AAAa;AACJ;AAET;AAEA;AAEA;AAEA;AACE;AAAwB;AAExB;AAAY;AACd;AACF;AACgD;AAGlD;AACG;AACQ;AACL;AACA;AACA;AACF;AAEC;AACQ;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF;AAEC;AAAc;AAAiB;AAAmB;AAIvD;AAGN;AAQA;AAAuB;AAEnB;AACA;AACA;AACA;AAAmB;AACuC;AACpB;AAGtC;AACG;AACM;AACD;AACC;AACK;AAGZ;AAGN;AAEA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF;;"}
1
+ {"version":3,"file":"index.mjs","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: (...args) => {\n return autoUpdate(...args, {\n animationFrame: true,\n });\n },\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\nconst MAX_ATTACHMENTS = 10;\nconst MAX_ATTACHMENT_SIZE = 1024 * 1024 * 1024; // 1 GB\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 // Later: Offer as Composer.Form props: { maxAttachments: number; maxAttachmentSize: number; supportedAttachmentMimeTypes: string[]; }\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 createAttachmentsRef = useRef(createAttachments);\n\n useEffect(() => {\n createAttachmentsRef.current = createAttachments;\n }, [createAttachments]);\n\n const stableCreateAttachments = useCallback((files: File[]) => {\n createAttachmentsRef.current(files);\n }, []);\n\n const editor = useInitial(() =>\n createComposerEditor({\n createAttachments: stableCreateAttachments,\n pasteFilesAsAttachments,\n })\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 type=\"button\"\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;AAEE;AAA2B;AACT;AACjB;AACH;AACF;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;AAEA;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;AAEA;AACE;AAA+B;AAGjC;AACE;AAAkC;AAGpC;AAAe;AACQ;AACA;AACnB;AACD;AAGH;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;AACM;AACD;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;;"}