UNPKG

@liveblocks/react-ui

Version:

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

1,264 lines (1,260 loc) 38.6 kB
"use client"; import { jsxs, jsx } from 'react/jsx-runtime'; import { createCommentAttachmentId, makeEventSource } from '@liveblocks/core'; import { useRoom } from '@liveblocks/react'; import { useLayoutEffect, useClientOrNull, useResolveMentionSuggestions, useMentionSuggestions, useSyncSource } from '@liveblocks/react/_private'; import { Slot, Slottable } from '@radix-ui/react-slot'; import * as TogglePrimitive from '@radix-ui/react-toggle'; import { useMemo, useState, forwardRef, useRef, useCallback, useEffect, useId, useImperativeHandle } from 'react'; import { createEditor, Range, Transforms, Editor, insertText } from 'slate'; import { withHistory } from 'slate-history'; import { withReact, useSelected, useSlateStatic, ReactEditor, Slate, Editable } from 'slate-react'; import { useLiveblocksUIConfig } from '../../config.js'; import { withAutoFormatting } from '../../slate/plugins/auto-formatting.js'; import { withAutoLinks } from '../../slate/plugins/auto-links.js'; import { withCustomLinks } from '../../slate/plugins/custom-links.js'; import { withEmptyClearFormatting } from '../../slate/plugins/empty-clear-formatting.js'; import { withMentions, getMentionDraftAtSelection, MENTION_CHARACTER, insertMention, insertMentionCharacter } from '../../slate/plugins/mentions.js'; import { withNormalize } from '../../slate/plugins/normalize.js'; import { withPaste } from '../../slate/plugins/paste.js'; import { getDOMRange } from '../../slate/utils/get-dom-range.js'; import { isEmpty } from '../../slate/utils/is-empty.js'; import { leaveMarkEdge, toggleMark, getMarks } from '../../slate/utils/marks.js'; import { isKey } from '../../utils/is-key.js'; import { Persist, usePersist, useAnimationPersist } from '../../utils/Persist.js'; import { Portal } from '../../utils/Portal.js'; import { requestSubmit } from '../../utils/request-submit.js'; import { useIndex } from '../../utils/use-index.js'; import { useInitial } from '../../utils/use-initial.js'; import { useObservable } from '../../utils/use-observable.js'; import { useRefs } from '../../utils/use-refs.js'; import { toAbsoluteUrl } from '../Comment/utils.js'; import { useComposerEditorContext, useComposer, ComposerSuggestionsContext, ComposerFloatingToolbarContext, useComposerFloatingToolbarContext, useComposerSuggestionsContext, ComposerEditorContext, ComposerAttachmentsContext, ComposerContext, useComposerAttachmentsContext } from './contexts.js'; import { useContentZIndex, useFloatingWithOptions, getSideAndAlignFromFloatingPlacement, commentBodyToComposerBody, useComposerAttachmentsManager, composerBodyToCommentBody, useComposerAttachmentsDropArea } from './utils.js'; const MENTION_SUGGESTIONS_POSITION = "top"; const FLOATING_TOOLBAR_POSITION = "top"; const COMPOSER_MENTION_NAME = "ComposerMention"; const COMPOSER_LINK_NAME = "ComposerLink"; const COMPOSER_FLOATING_TOOLBAR_NAME = "ComposerFloatingToolbar"; const COMPOSER_SUGGESTIONS_NAME = "ComposerSuggestions"; const COMPOSER_SUGGESTIONS_LIST_NAME = "ComposerSuggestionsList"; const COMPOSER_SUGGESTIONS_LIST_ITEM_NAME = "ComposerSuggestionsListItem"; const COMPOSER_SUBMIT_NAME = "ComposerSubmit"; const COMPOSER_EDITOR_NAME = "ComposerEditor"; const COMPOSER_ATTACH_FILES_NAME = "ComposerAttachFiles"; const COMPOSER_ATTACHMENTS_DROP_AREA_NAME = "ComposerAttachmentsDropArea"; const COMPOSER_MARK_TOGGLE_NAME = "ComposerMarkToggle"; const COMPOSER_FORM_NAME = "ComposerForm"; const emptyCommentBody = { version: 1, content: [{ type: "paragraph", children: [{ text: "" }] }] }; function createComposerEditor({ createAttachments, pasteFilesAsAttachments }) { return withNormalize( withMentions( withCustomLinks( withAutoLinks( withAutoFormatting( withEmptyClearFormatting( withPaste(withHistory(withReact(createEditor())), { createAttachments, pasteFilesAsAttachments }) ) ) ) ) ) ); } function ComposerEditorMentionWrapper({ Mention, attributes, children, element }) { const isSelected = useSelected(); return /* @__PURE__ */ jsxs("span", { ...attributes, children: [ element.id ? /* @__PURE__ */ jsx(Mention, { userId: element.id, isSelected }) : null, children ] }); } function ComposerEditorLinkWrapper({ Link, attributes, element, children }) { const href = useMemo( () => toAbsoluteUrl(element.url) ?? element.url, [element.url] ); return /* @__PURE__ */ jsx("span", { ...attributes, children: /* @__PURE__ */ jsx(Link, { href, children }) }); } function ComposerEditorMentionSuggestionsWrapper({ id, itemId, userIds, selectedUserId, setSelectedUserId, mentionDraft, setMentionDraft, onItemSelect, position = MENTION_SUGGESTIONS_POSITION, dir, MentionSuggestions }) { const editor = useSlateStatic(); const { onEditorChange } = useComposerEditorContext(); const { isFocused } = useComposer(); const { portalContainer } = useLiveblocksUIConfig(); const [contentRef, contentZIndex] = useContentZIndex(); const isOpen = isFocused && mentionDraft?.range !== void 0 && userIds !== void 0; const { refs: { setReference, setFloating }, strategy, isPositioned, placement, x, y, update, elements } = useFloatingWithOptions({ position, dir, alignment: "start", open: isOpen }); useObservable(onEditorChange, () => { setMentionDraft(getMentionDraftAtSelection(editor)); }); useLayoutEffect(() => { if (!mentionDraft) { setReference(null); return; } const domRange = getDOMRange(editor, mentionDraft.range); setReference(domRange ?? null); }, [setReference, editor, mentionDraft]); useLayoutEffect(() => { if (!isOpen) return; const mentionSuggestions = elements.floating?.firstChild; if (!mentionSuggestions) { return; } mentionSuggestions.style.overflowY = "visible"; mentionSuggestions.style.maxHeight = "none"; update(); const animationFrame = requestAnimationFrame(() => { mentionSuggestions.style.overflowY = "auto"; mentionSuggestions.style.maxHeight = "var(--lb-composer-floating-available-height)"; }); return () => { cancelAnimationFrame(animationFrame); }; }, [userIds?.length, isOpen, elements.floating, update]); return /* @__PURE__ */ jsx(Persist, { children: isOpen ? /* @__PURE__ */ jsx(ComposerSuggestionsContext.Provider, { value: { id, itemId, selectedValue: selectedUserId, setSelectedValue: setSelectedUserId, onItemSelect, placement, dir, ref: contentRef }, children: /* @__PURE__ */ jsx(Portal, { ref: setFloating, container: portalContainer, style: { position: strategy, top: 0, left: 0, transform: isPositioned ? `translate3d(${Math.round(x)}px, ${Math.round(y)}px, 0)` : "translate3d(0, -200%, 0)", minWidth: "max-content", zIndex: contentZIndex }, children: /* @__PURE__ */ jsx(MentionSuggestions, { userIds, selectedUserId }) }) }) : null }); } function ComposerEditorFloatingToolbarWrapper({ id, position = FLOATING_TOOLBAR_POSITION, dir, FloatingToolbar, hasFloatingToolbarRange, setHasFloatingToolbarRange }) { const editor = useSlateStatic(); const { onEditorChange } = useComposerEditorContext(); const { isFocused } = useComposer(); const { portalContainer } = useLiveblocksUIConfig(); const [contentRef, contentZIndex] = useContentZIndex(); const [isPointerDown, setPointerDown] = useState(false); const isOpen = isFocused && !isPointerDown && hasFloatingToolbarRange; const { refs: { setReference, setFloating }, strategy, isPositioned, placement, x, y } = useFloatingWithOptions({ type: "range", position, dir, alignment: "center", open: isOpen }); useLayoutEffect(() => { if (!isFocused) { return; } const handlePointerDown = () => setPointerDown(true); const handlePointerUp = () => setPointerDown(false); document.addEventListener("pointerdown", handlePointerDown); document.addEventListener("pointerup", handlePointerUp); return () => { document.removeEventListener("pointerdown", handlePointerDown); document.removeEventListener("pointerup", handlePointerUp); }; }, [isFocused]); useObservable(onEditorChange, () => { setReference(null); requestAnimationFrame(() => { const domSelection = window.getSelection(); if (!editor.selection || Range.isCollapsed(editor.selection) || !domSelection || !domSelection.rangeCount) { setHasFloatingToolbarRange(false); setReference(null); } else { setHasFloatingToolbarRange(true); const domRange = domSelection.getRangeAt(0); setReference(domRange); } }); }); return /* @__PURE__ */ jsx(Persist, { children: isOpen ? /* @__PURE__ */ jsx(ComposerFloatingToolbarContext.Provider, { value: { id, placement, dir, ref: contentRef }, children: /* @__PURE__ */ jsx(Portal, { ref: setFloating, container: portalContainer, style: { position: strategy, top: 0, left: 0, transform: isPositioned ? `translate3d(${Math.round(x)}px, ${Math.round(y)}px, 0)` : "translate3d(0, -200%, 0)", minWidth: "max-content", zIndex: contentZIndex }, children: /* @__PURE__ */ jsx(FloatingToolbar, {}) }) }) : null }); } const ComposerFloatingToolbar = forwardRef(({ children, onPointerDown, style, asChild, ...props }, forwardedRef) => { const [isPresent] = usePersist(); const ref = useRef(null); const { id, ref: contentRef, placement, dir } = useComposerFloatingToolbarContext(COMPOSER_FLOATING_TOOLBAR_NAME); const mergedRefs = useRefs(forwardedRef, contentRef, ref); const [side, align] = useMemo( () => getSideAndAlignFromFloatingPlacement(placement), [placement] ); const Component = asChild ? Slot : "div"; useAnimationPersist(ref); const handlePointerDown = useCallback( (event) => { onPointerDown?.(event); event.preventDefault(); event.stopPropagation(); }, [onPointerDown] ); return /* @__PURE__ */ jsx(Component, { dir, role: "toolbar", id, "aria-label": "Floating toolbar", ...props, onPointerDown: handlePointerDown, "data-state": isPresent ? "open" : "closed", "data-side": side, "data-align": align, style: { display: "flex", flexDirection: "row", maxWidth: "var(--lb-composer-floating-available-width)", overflowX: "auto", ...style }, ref: mergedRefs, children }); }); function ComposerEditorElement({ Mention, Link, ...props }) { const { attributes, children, element } = props; switch (element.type) { case "mention": return /* @__PURE__ */ jsx(ComposerEditorMentionWrapper, { Mention, ...props }); case "auto-link": case "custom-link": return /* @__PURE__ */ jsx(ComposerEditorLinkWrapper, { Link, ...props }); case "paragraph": return /* @__PURE__ */ jsx("p", { ...attributes, style: { position: "relative" }, children }); default: return null; } } function ComposerEditorLeaf({ attributes, children, leaf }) { if (leaf.bold) { children = /* @__PURE__ */ jsx("strong", { children }); } if (leaf.italic) { children = /* @__PURE__ */ jsx("em", { children }); } if (leaf.strikethrough) { children = /* @__PURE__ */ jsx("s", { children }); } if (leaf.code) { children = /* @__PURE__ */ jsx("code", { children }); } return /* @__PURE__ */ jsx("span", { ...attributes, children }); } function ComposerEditorPlaceholder({ attributes, children }) { const { opacity: _opacity, ...style } = attributes.style; return /* @__PURE__ */ jsx("span", { ...attributes, style, "data-placeholder": "", children }); } const ComposerMention = forwardRef( ({ children, asChild, ...props }, forwardedRef) => { const Component = asChild ? Slot : "span"; const isSelected = useSelected(); return /* @__PURE__ */ jsx(Component, { "data-selected": isSelected || void 0, ...props, ref: forwardedRef, children }); } ); const ComposerLink = forwardRef( ({ children, asChild, ...props }, forwardedRef) => { const Component = asChild ? Slot : "a"; return /* @__PURE__ */ jsx(Component, { target: "_blank", rel: "noopener noreferrer nofollow", ...props, ref: forwardedRef, children }); } ); const ComposerSuggestions = forwardRef(({ children, style, asChild, ...props }, forwardedRef) => { const [isPresent] = usePersist(); const ref = useRef(null); const { ref: contentRef, placement, dir } = useComposerSuggestionsContext(COMPOSER_SUGGESTIONS_NAME); const mergedRefs = useRefs(forwardedRef, contentRef, ref); const [side, align] = useMemo( () => getSideAndAlignFromFloatingPlacement(placement), [placement] ); const Component = asChild ? Slot : "div"; useAnimationPersist(ref); return /* @__PURE__ */ jsx(Component, { dir, ...props, "data-state": isPresent ? "open" : "closed", "data-side": side, "data-align": align, style: { display: "flex", flexDirection: "column", maxHeight: "var(--lb-composer-floating-available-height)", overflowY: "auto", ...style }, ref: mergedRefs, children }); }); const ComposerSuggestionsList = forwardRef(({ children, asChild, ...props }, forwardedRef) => { const { id } = useComposerSuggestionsContext(COMPOSER_SUGGESTIONS_LIST_NAME); const Component = asChild ? Slot : "ul"; return /* @__PURE__ */ jsx(Component, { role: "listbox", id, "aria-label": "Suggestions list", ...props, ref: forwardedRef, children }); }); const ComposerSuggestionsListItem = forwardRef( ({ value, children, onPointerMove, onPointerDown, onClick, asChild, ...props }, forwardedRef) => { const ref = useRef(null); const mergedRefs = useRefs(forwardedRef, ref); const { selectedValue, setSelectedValue, itemId, onItemSelect } = useComposerSuggestionsContext(COMPOSER_SUGGESTIONS_LIST_ITEM_NAME); const Component = asChild ? Slot : "li"; const isSelected = useMemo( () => selectedValue === value, [selectedValue, value] ); const id = useMemo(() => itemId(value), [itemId, value]); useEffect(() => { if (ref?.current && isSelected) { ref.current.scrollIntoView({ block: "nearest" }); } }, [isSelected]); const handlePointerMove = useCallback( (event) => { onPointerMove?.(event); if (!event.isDefaultPrevented()) { setSelectedValue(value); } }, [onPointerMove, setSelectedValue, value] ); const handlePointerDown = useCallback( (event) => { onPointerDown?.(event); event.preventDefault(); event.stopPropagation(); }, [onPointerDown] ); const handleClick = useCallback( (event) => { onClick?.(event); const wasDefaultPrevented = event.isDefaultPrevented(); event.preventDefault(); event.stopPropagation(); if (!wasDefaultPrevented) { onItemSelect(value); } }, [onClick, onItemSelect, value] ); return /* @__PURE__ */ jsx(Component, { role: "option", id, "data-selected": isSelected || void 0, "aria-selected": isSelected || void 0, onPointerMove: handlePointerMove, onPointerDown: handlePointerDown, onClick: handleClick, ...props, ref: mergedRefs, children }); } ); const defaultEditorComponents = { Link: ({ href, children }) => { return /* @__PURE__ */ jsx(ComposerLink, { href, children }); }, Mention: ({ userId }) => { return /* @__PURE__ */ jsxs(ComposerMention, { children: [ MENTION_CHARACTER, userId ] }); }, MentionSuggestions: ({ userIds }) => { return userIds.length > 0 ? /* @__PURE__ */ jsx(ComposerSuggestions, { children: /* @__PURE__ */ jsx(ComposerSuggestionsList, { children: userIds.map((userId) => /* @__PURE__ */ jsx(ComposerSuggestionsListItem, { value: userId, children: userId }, userId)) }) }) : null; } }; const ComposerEditor = forwardRef( ({ defaultValue, onKeyDown, onFocus, onBlur, disabled, autoFocus, components, dir, ...props }, forwardedRef) => { const client = useClientOrNull(); const { editor, validate, setFocused, onEditorChange, roomId } = useComposerEditorContext(); const { submit, focus, blur, select, canSubmit, isDisabled: isComposerDisabled, isFocused } = useComposer(); const isDisabled = isComposerDisabled || disabled; const initialBody = useInitial(defaultValue ?? emptyCommentBody); const initialEditorValue = useMemo(() => { return commentBodyToComposerBody(initialBody); }, [initialBody]); const { Link, Mention, MentionSuggestions, FloatingToolbar } = useMemo( () => ({ ...defaultEditorComponents, ...components }), [components] ); const [hasFloatingToolbarRange, setHasFloatingToolbarRange] = useState(false); const resolveMentionSuggestions = useResolveMentionSuggestions(); const hasResolveMentionSuggestions = client ? resolveMentionSuggestions : true; const [mentionDraft, setMentionDraft] = useState(); const mentionSuggestions = useMentionSuggestions( roomId, mentionDraft?.text ); const [ selectedMentionSuggestionIndex, setPreviousSelectedMentionSuggestionIndex, setNextSelectedMentionSuggestionIndex, setSelectedMentionSuggestionIndex ] = useIndex(0, mentionSuggestions?.length ?? 0); const id = useId(); const floatingToolbarId = `liveblocks-floating-toolbar-${id}`; const suggestionsListId = `liveblocks-suggestions-list-${id}`; const suggestionsListItemId = useCallback( (userId) => userId ? `liveblocks-suggestions-list-item-${id}-${userId}` : void 0, [id] ); const renderElement = useCallback( (props2) => { return /* @__PURE__ */ jsx(ComposerEditorElement, { Mention, Link, ...props2 }); }, [Link, Mention] ); const handleChange = useCallback( (value) => { validate(value); onEditorChange.notify(); }, [validate, onEditorChange] ); const createMention = useCallback( (userId) => { if (!mentionDraft || !userId) { return; } Transforms.select(editor, mentionDraft.range); insertMention(editor, userId); setMentionDraft(void 0); setSelectedMentionSuggestionIndex(0); }, [editor, mentionDraft, setSelectedMentionSuggestionIndex] ); const handleKeyDown = useCallback( (event) => { onKeyDown?.(event); if (event.isDefaultPrevented()) { return; } if (isKey(event, "ArrowLeft")) { leaveMarkEdge(editor, "start"); } if (isKey(event, "ArrowRight")) { leaveMarkEdge(editor, "end"); } if (mentionDraft && mentionSuggestions?.length) { if (isKey(event, "ArrowDown")) { event.preventDefault(); setNextSelectedMentionSuggestionIndex(); } if (isKey(event, "ArrowUp")) { event.preventDefault(); setPreviousSelectedMentionSuggestionIndex(); } if (isKey(event, "Enter") || isKey(event, "Tab")) { event.preventDefault(); const userId = mentionSuggestions?.[selectedMentionSuggestionIndex]; createMention(userId); } if (isKey(event, "Escape")) { event.preventDefault(); setMentionDraft(void 0); setSelectedMentionSuggestionIndex(0); } } else { if (hasFloatingToolbarRange) { if (isKey(event, "Escape")) { event.preventDefault(); setHasFloatingToolbarRange(false); } } if (isKey(event, "Escape")) { blur(); } if (isKey(event, "Enter", { shift: false })) { event.preventDefault(); if (canSubmit) { submit(); } } if (isKey(event, "Enter", { shift: true })) { event.preventDefault(); editor.insertBreak(); } if (isKey(event, "b", { mod: true })) { event.preventDefault(); toggleMark(editor, "bold"); } if (isKey(event, "i", { mod: true })) { event.preventDefault(); toggleMark(editor, "italic"); } if (isKey(event, "s", { mod: true, shift: true })) { event.preventDefault(); toggleMark(editor, "strikethrough"); } if (isKey(event, "e", { mod: true })) { event.preventDefault(); toggleMark(editor, "code"); } } }, [ onKeyDown, mentionDraft, mentionSuggestions, hasFloatingToolbarRange, editor, setNextSelectedMentionSuggestionIndex, setPreviousSelectedMentionSuggestionIndex, selectedMentionSuggestionIndex, createMention, setSelectedMentionSuggestionIndex, blur, canSubmit, submit ] ); const handleFocus = useCallback( (event) => { onFocus?.(event); if (!event.isDefaultPrevented()) { setFocused(true); } }, [onFocus, setFocused] ); const handleBlur = useCallback( (event) => { onBlur?.(event); if (!event.isDefaultPrevented()) { setFocused(false); } }, [onBlur, setFocused] ); const selectedMentionSuggestionUserId = useMemo( () => mentionSuggestions?.[selectedMentionSuggestionIndex], [selectedMentionSuggestionIndex, mentionSuggestions] ); const setSelectedMentionSuggestionUserId = useCallback( (userId) => { const index = mentionSuggestions?.indexOf(userId); if (index !== void 0 && index >= 0) { setSelectedMentionSuggestionIndex(index); } }, [setSelectedMentionSuggestionIndex, mentionSuggestions] ); const additionalProps = useMemo( () => mentionDraft ? { role: "combobox", "aria-autocomplete": "list", "aria-expanded": true, "aria-controls": suggestionsListId, "aria-activedescendant": suggestionsListItemId( selectedMentionSuggestionUserId ) } : hasFloatingToolbarRange ? { "aria-haspopup": true, "aria-controls": floatingToolbarId } : {}, [ mentionDraft, suggestionsListId, suggestionsListItemId, selectedMentionSuggestionUserId, hasFloatingToolbarRange, floatingToolbarId ] ); useImperativeHandle(forwardedRef, () => { return ReactEditor.toDOMNode(editor, editor); }, [editor]); useLayoutEffect(() => { if (autoFocus) { focus(); } }, [autoFocus, editor, focus]); useLayoutEffect(() => { if (isFocused && editor.selection === null) { select(); } }, [editor, select, isFocused]); return /* @__PURE__ */ jsxs(Slate, { editor, initialValue: initialEditorValue, onChange: handleChange, children: [ /* @__PURE__ */ jsx(Editable, { dir, enterKeyHint: mentionDraft ? "enter" : "send", autoCapitalize: "sentences", "aria-label": "Composer editor", "data-focused": isFocused || void 0, "data-disabled": isDisabled || void 0, ...additionalProps, ...props, readOnly: isDisabled, disabled: isDisabled, onKeyDown: handleKeyDown, onFocus: handleFocus, onBlur: handleBlur, renderElement, renderLeaf: ComposerEditorLeaf, renderPlaceholder: ComposerEditorPlaceholder }), hasResolveMentionSuggestions && /* @__PURE__ */ jsx(ComposerEditorMentionSuggestionsWrapper, { dir, mentionDraft, setMentionDraft, selectedUserId: selectedMentionSuggestionUserId, setSelectedUserId: setSelectedMentionSuggestionUserId, userIds: mentionSuggestions, id: suggestionsListId, itemId: suggestionsListItemId, onItemSelect: createMention, MentionSuggestions }), FloatingToolbar && /* @__PURE__ */ jsx(ComposerEditorFloatingToolbarWrapper, { dir, id: floatingToolbarId, hasFloatingToolbarRange, setHasFloatingToolbarRange, FloatingToolbar }) ] }); } ); const MAX_ATTACHMENTS = 10; const MAX_ATTACHMENT_SIZE = 1024 * 1024 * 1024; function prepareAttachment(file) { return { type: "localAttachment", status: "idle", id: createCommentAttachmentId(), name: file.name, size: file.size, mimeType: file.type, file }; } const ComposerForm = forwardRef( ({ children, onSubmit, onComposerSubmit, defaultAttachments = [], pasteFilesAsAttachments, preventUnsavedChanges = true, disabled, asChild, roomId: _roomId, ...props }, forwardedRef) => { const Component = asChild ? Slot : "form"; const [isEmpty$1, setEmpty] = useState(true); const [isSubmitting, setSubmitting] = useState(false); const [isFocused, setFocused] = useState(false); const room = useRoom({ allowOutsideRoom: true }); const roomId = _roomId !== void 0 ? _roomId : room?.id; if (roomId === void 0) { throw new Error("Composer.Form must be a descendant of RoomProvider."); } const maxAttachments = MAX_ATTACHMENTS; const maxAttachmentSize = MAX_ATTACHMENT_SIZE; const { attachments, isUploadingAttachments, addAttachments, removeAttachment, clearAttachments } = useComposerAttachmentsManager(defaultAttachments, { maxFileSize: maxAttachmentSize, roomId }); const numberOfAttachments = attachments.length; const hasMaxAttachments = numberOfAttachments >= maxAttachments; const isDisabled = useMemo(() => { return isSubmitting || disabled === true; }, [isSubmitting, disabled]); const canSubmit = useMemo(() => { return !isEmpty$1 && !isUploadingAttachments; }, [isEmpty$1, isUploadingAttachments]); const [marks, setMarks] = useState(getMarks); const ref = useRef(null); const mergedRefs = useRefs(forwardedRef, ref); const fileInputRef = useRef(null); const syncSource = useSyncSource(); const isPending = !preventUnsavedChanges ? false : !isEmpty$1 || isUploadingAttachments || attachments.length > 0; useEffect(() => { syncSource?.setSyncStatus( isPending ? "has-local-changes" : "synchronized" ); }, [syncSource, isPending]); const createAttachments = useCallback( (files) => { if (!files.length) { return; } const numberOfAcceptedFiles = Math.max( 0, maxAttachments - numberOfAttachments ); files.splice(numberOfAcceptedFiles); const attachments2 = files.map((file) => prepareAttachment(file)); addAttachments(attachments2); }, [addAttachments, maxAttachments, numberOfAttachments] ); const createAttachmentsRef = useRef(createAttachments); useEffect(() => { createAttachmentsRef.current = createAttachments; }, [createAttachments]); const stableCreateAttachments = useCallback((files) => { createAttachmentsRef.current(files); }, []); const editor = useInitial( () => createComposerEditor({ createAttachments: stableCreateAttachments, pasteFilesAsAttachments }) ); const onEditorChange = useInitial(makeEventSource); const validate = useCallback( (value) => { setEmpty(isEmpty(editor, value)); }, [editor] ); const submit = useCallback(() => { if (!canSubmit) { return; } requestAnimationFrame(() => { if (ref.current) { requestSubmit(ref.current); } }); }, [canSubmit]); const clear = useCallback(() => { Transforms.delete(editor, { at: { anchor: Editor.start(editor, []), focus: Editor.end(editor, []) } }); }, [editor]); const select = useCallback(() => { Transforms.select(editor, Editor.end(editor, [])); }, [editor]); const focus = useCallback( (resetSelection = true) => { try { if (!ReactEditor.isFocused(editor)) { Transforms.select( editor, resetSelection || !editor.selection ? Editor.end(editor, []) : editor.selection ); ReactEditor.focus(editor); } } catch { } }, [editor] ); const blur = useCallback(() => { try { ReactEditor.blur(editor); } catch { } }, [editor]); const createMention = useCallback(() => { if (disabled) { return; } focus(); insertMentionCharacter(editor); }, [disabled, editor, focus]); const insertText$1 = useCallback( (text) => { if (disabled) { return; } focus(false); insertText(editor, text); }, [disabled, editor, focus] ); const attachFiles = useCallback(() => { if (disabled) { return; } if (fileInputRef.current) { fileInputRef.current.click(); } }, [disabled]); const handleAttachmentsInputChange = useCallback( (event) => { if (disabled) { return; } if (event.target.files) { createAttachments(Array.from(event.target.files)); event.target.value = ""; } }, [createAttachments, disabled] ); const onSubmitEnd = useCallback(() => { clear(); blur(); clearAttachments(); setSubmitting(false); }, [blur, clear, clearAttachments]); const handleSubmit = useCallback( (event) => { if (disabled) { return; } const isEmpty2 = isEmpty(editor, editor.children); if (isEmpty2) { event.preventDefault(); return; } onSubmit?.(event); if (!onComposerSubmit || event.isDefaultPrevented()) { event.preventDefault(); return; } const body = composerBodyToCommentBody( editor.children ); const commentAttachments = attachments.filter( (attachment) => attachment.type === "attachment" || attachment.type === "localAttachment" && attachment.status === "uploaded" ).map((attachment) => { return { id: attachment.id, type: "attachment", mimeType: attachment.mimeType, size: attachment.size, name: attachment.name }; }); const promise = onComposerSubmit( { body, attachments: commentAttachments }, event ); event.preventDefault(); if (promise) { setSubmitting(true); promise.then(onSubmitEnd); } else { onSubmitEnd(); } }, [disabled, editor, attachments, onComposerSubmit, onSubmit, onSubmitEnd] ); const stopPropagation = useCallback((event) => { event.stopPropagation(); }, []); const toggleMark$1 = useCallback( (mark) => { toggleMark(editor, mark); }, [editor] ); useObservable(onEditorChange, () => { setMarks(getMarks(editor)); }); return /* @__PURE__ */ jsx(ComposerEditorContext.Provider, { value: { editor, validate, setFocused, onEditorChange, roomId }, children: /* @__PURE__ */ jsx(ComposerAttachmentsContext.Provider, { value: { createAttachments, isUploadingAttachments, hasMaxAttachments, maxAttachments, maxAttachmentSize }, children: /* @__PURE__ */ jsx(ComposerContext.Provider, { value: { isDisabled, isFocused, isEmpty: isEmpty$1, canSubmit, submit, clear, select, focus, blur, createMention, insertText: insertText$1, attachments, attachFiles, removeAttachment, toggleMark: toggleMark$1, marks }, children: /* @__PURE__ */ jsxs(Component, { ...props, onSubmit: handleSubmit, ref: mergedRefs, children: [ /* @__PURE__ */ jsx("input", { type: "file", multiple: true, ref: fileInputRef, onChange: handleAttachmentsInputChange, onClick: stopPropagation, tabIndex: -1, style: { display: "none" } }), /* @__PURE__ */ jsx(Slottable, { children }) ] }) }) }) }); } ); const ComposerSubmit = forwardRef( ({ children, disabled, asChild, ...props }, forwardedRef) => { const Component = asChild ? Slot : "button"; const { canSubmit, isDisabled: isComposerDisabled } = useComposer(); const isDisabled = isComposerDisabled || disabled || !canSubmit; return /* @__PURE__ */ jsx(Component, { type: "submit", ...props, ref: forwardedRef, disabled: isDisabled, children }); } ); const ComposerAttachFiles = forwardRef(({ children, onClick, disabled, asChild, ...props }, forwardedRef) => { const Component = asChild ? Slot : "button"; const { hasMaxAttachments } = useComposerAttachmentsContext(); const { isDisabled: isComposerDisabled, attachFiles } = useComposer(); const isDisabled = isComposerDisabled || hasMaxAttachments || disabled; const handleClick = useCallback( (event) => { onClick?.(event); if (!event.isDefaultPrevented()) { attachFiles(); } }, [attachFiles, onClick] ); return /* @__PURE__ */ jsx(Component, { type: "button", ...props, onClick: handleClick, ref: forwardedRef, disabled: isDisabled, children }); }); const ComposerAttachmentsDropArea = forwardRef( ({ onDragEnter, onDragLeave, onDragOver, onDrop, disabled, asChild, ...props }, forwardedRef) => { const Component = asChild ? Slot : "div"; const { isDisabled: isComposerDisabled } = useComposer(); const isDisabled = isComposerDisabled || disabled; const [, dropAreaProps] = useComposerAttachmentsDropArea({ onDragEnter, onDragLeave, onDragOver, onDrop, disabled: isDisabled }); return /* @__PURE__ */ jsx(Component, { ...dropAreaProps, "data-disabled": isDisabled ? "" : void 0, ...props, ref: forwardedRef }); } ); const ComposerMarkToggle = forwardRef( ({ children, mark, onValueChange, onClick, onPointerDown, asChild, ...props }, forwardedRef) => { const Component = asChild ? Slot : "button"; const { marks, toggleMark } = useComposer(); const handlePointerDown = useCallback( (event) => { onPointerDown?.(event); event.preventDefault(); event.stopPropagation(); }, [onPointerDown] ); const handleClick = useCallback( (event) => { onClick?.(event); if (!event.isDefaultPrevented()) { event.preventDefault(); event.stopPropagation(); toggleMark(mark); onValueChange?.(mark); } }, [mark, onClick, onValueChange, toggleMark] ); return /* @__PURE__ */ jsx(TogglePrimitive.Root, { asChild: true, pressed: marks[mark], onClick: handleClick, onPointerDown: handlePointerDown, ...props, children: /* @__PURE__ */ jsx(Component, { ...props, ref: forwardedRef, children }) }); } ); if (process.env.NODE_ENV !== "production") { ComposerAttachFiles.displayName = COMPOSER_ATTACH_FILES_NAME; ComposerAttachmentsDropArea.displayName = COMPOSER_ATTACHMENTS_DROP_AREA_NAME; ComposerEditor.displayName = COMPOSER_EDITOR_NAME; ComposerFloatingToolbar.displayName = COMPOSER_FLOATING_TOOLBAR_NAME; ComposerForm.displayName = COMPOSER_FORM_NAME; ComposerMention.displayName = COMPOSER_MENTION_NAME; ComposerLink.displayName = COMPOSER_LINK_NAME; ComposerSubmit.displayName = COMPOSER_SUBMIT_NAME; ComposerSuggestions.displayName = COMPOSER_SUGGESTIONS_NAME; ComposerSuggestionsList.displayName = COMPOSER_SUGGESTIONS_LIST_NAME; ComposerSuggestionsListItem.displayName = COMPOSER_SUGGESTIONS_LIST_ITEM_NAME; ComposerMarkToggle.displayName = COMPOSER_MARK_TOGGLE_NAME; } export { ComposerAttachFiles as AttachFiles, ComposerAttachmentsDropArea as AttachmentsDropArea, ComposerEditor as Editor, ComposerFloatingToolbar as FloatingToolbar, ComposerForm as Form, ComposerLink as Link, ComposerMarkToggle as MarkToggle, ComposerMention as Mention, ComposerSubmit as Submit, ComposerSuggestions as Suggestions, ComposerSuggestionsList as SuggestionsList, ComposerSuggestionsListItem as SuggestionsListItem }; //# sourceMappingURL=index.js.map