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,296 lines (1,289 loc) 40.3 kB
"use client"; 'use strict'; var jsxRuntime = require('react/jsx-runtime'); var core = require('@liveblocks/core'); var react$1 = require('@liveblocks/react'); var _private = require('@liveblocks/react/_private'); var reactSlot = require('@radix-ui/react-slot'); var TogglePrimitive = require('@radix-ui/react-toggle'); var react = require('react'); var slate = require('slate'); var slateHistory = require('slate-history'); var slateReact = require('slate-react'); var config = require('../../config.cjs'); var autoFormatting = require('../../slate/plugins/auto-formatting.cjs'); var autoLinks = require('../../slate/plugins/auto-links.cjs'); var customLinks = require('../../slate/plugins/custom-links.cjs'); var emptyClearFormatting = require('../../slate/plugins/empty-clear-formatting.cjs'); var mentions = require('../../slate/plugins/mentions.cjs'); var normalize = require('../../slate/plugins/normalize.cjs'); var paste = require('../../slate/plugins/paste.cjs'); var getDomRange = require('../../slate/utils/get-dom-range.cjs'); var isEmpty = require('../../slate/utils/is-empty.cjs'); var marks = require('../../slate/utils/marks.cjs'); var isKey = require('../../utils/is-key.cjs'); var Persist = require('../../utils/Persist.cjs'); var Portal = require('../../utils/Portal.cjs'); var requestSubmit = require('../../utils/request-submit.cjs'); var useIndex = require('../../utils/use-index.cjs'); var useInitial = require('../../utils/use-initial.cjs'); var useObservable = require('../../utils/use-observable.cjs'); var useRefs = require('../../utils/use-refs.cjs'); var utils = require('../Comment/utils.cjs'); var contexts = require('./contexts.cjs'); var utils$1 = require('./utils.cjs'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var TogglePrimitive__namespace = /*#__PURE__*/_interopNamespaceDefault(TogglePrimitive); 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 normalize.withNormalize( mentions.withMentions( customLinks.withCustomLinks( autoLinks.withAutoLinks( autoFormatting.withAutoFormatting( emptyClearFormatting.withEmptyClearFormatting( paste.withPaste(slateHistory.withHistory(slateReact.withReact(slate.createEditor())), { createAttachments, pasteFilesAsAttachments }) ) ) ) ) ) ); } function ComposerEditorMentionWrapper({ Mention, attributes, children, element }) { const isSelected = slateReact.useSelected(); return /* @__PURE__ */ jsxRuntime.jsxs("span", { ...attributes, children: [ element.id ? /* @__PURE__ */ jsxRuntime.jsx(Mention, { userId: element.id, isSelected }) : null, children ] }); } function ComposerEditorLinkWrapper({ Link, attributes, element, children }) { const href = react.useMemo( () => utils.toAbsoluteUrl(element.url) ?? element.url, [element.url] ); return /* @__PURE__ */ jsxRuntime.jsx("span", { ...attributes, children: /* @__PURE__ */ jsxRuntime.jsx(Link, { href, children }) }); } function ComposerEditorMentionSuggestionsWrapper({ id, itemId, userIds, selectedUserId, setSelectedUserId, mentionDraft, setMentionDraft, onItemSelect, position = MENTION_SUGGESTIONS_POSITION, dir, MentionSuggestions }) { const editor = slateReact.useSlateStatic(); const { onEditorChange } = contexts.useComposerEditorContext(); const { isFocused } = contexts.useComposer(); const { portalContainer } = config.useLiveblocksUIConfig(); const [contentRef, contentZIndex] = utils$1.useContentZIndex(); const isOpen = isFocused && mentionDraft?.range !== void 0 && userIds !== void 0; const { refs: { setReference, setFloating }, strategy, isPositioned, placement, x, y, update, elements } = utils$1.useFloatingWithOptions({ position, dir, alignment: "start", open: isOpen }); useObservable.useObservable(onEditorChange, () => { setMentionDraft(mentions.getMentionDraftAtSelection(editor)); }); _private.useLayoutEffect(() => { if (!mentionDraft) { setReference(null); return; } const domRange = getDomRange.getDOMRange(editor, mentionDraft.range); setReference(domRange ?? null); }, [setReference, editor, mentionDraft]); _private.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__ */ jsxRuntime.jsx(Persist.Persist, { children: isOpen ? /* @__PURE__ */ jsxRuntime.jsx(contexts.ComposerSuggestionsContext.Provider, { value: { id, itemId, selectedValue: selectedUserId, setSelectedValue: setSelectedUserId, onItemSelect, placement, dir, ref: contentRef }, children: /* @__PURE__ */ jsxRuntime.jsx(Portal.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__ */ jsxRuntime.jsx(MentionSuggestions, { userIds, selectedUserId }) }) }) : null }); } function ComposerEditorFloatingToolbarWrapper({ id, position = FLOATING_TOOLBAR_POSITION, dir, FloatingToolbar, hasFloatingToolbarRange, setHasFloatingToolbarRange }) { const editor = slateReact.useSlateStatic(); const { onEditorChange } = contexts.useComposerEditorContext(); const { isFocused } = contexts.useComposer(); const { portalContainer } = config.useLiveblocksUIConfig(); const [contentRef, contentZIndex] = utils$1.useContentZIndex(); const [isPointerDown, setPointerDown] = react.useState(false); const isOpen = isFocused && !isPointerDown && hasFloatingToolbarRange; const { refs: { setReference, setFloating }, strategy, isPositioned, placement, x, y } = utils$1.useFloatingWithOptions({ type: "range", position, dir, alignment: "center", open: isOpen }); _private.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.useObservable(onEditorChange, () => { setReference(null); requestAnimationFrame(() => { const domSelection = window.getSelection(); if (!editor.selection || slate.Range.isCollapsed(editor.selection) || !domSelection || !domSelection.rangeCount) { setHasFloatingToolbarRange(false); setReference(null); } else { setHasFloatingToolbarRange(true); const domRange = domSelection.getRangeAt(0); setReference(domRange); } }); }); return /* @__PURE__ */ jsxRuntime.jsx(Persist.Persist, { children: isOpen ? /* @__PURE__ */ jsxRuntime.jsx(contexts.ComposerFloatingToolbarContext.Provider, { value: { id, placement, dir, ref: contentRef }, children: /* @__PURE__ */ jsxRuntime.jsx(Portal.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__ */ jsxRuntime.jsx(FloatingToolbar, {}) }) }) : null }); } const ComposerFloatingToolbar = react.forwardRef(({ children, onPointerDown, style, asChild, ...props }, forwardedRef) => { const [isPresent] = Persist.usePersist(); const ref = react.useRef(null); const { id, ref: contentRef, placement, dir } = contexts.useComposerFloatingToolbarContext(COMPOSER_FLOATING_TOOLBAR_NAME); const mergedRefs = useRefs.useRefs(forwardedRef, contentRef, ref); const [side, align] = react.useMemo( () => utils$1.getSideAndAlignFromFloatingPlacement(placement), [placement] ); const Component = asChild ? reactSlot.Slot : "div"; Persist.useAnimationPersist(ref); const handlePointerDown = react.useCallback( (event) => { onPointerDown?.(event); event.preventDefault(); event.stopPropagation(); }, [onPointerDown] ); return /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(ComposerEditorMentionWrapper, { Mention, ...props }); case "auto-link": case "custom-link": return /* @__PURE__ */ jsxRuntime.jsx(ComposerEditorLinkWrapper, { Link, ...props }); case "paragraph": return /* @__PURE__ */ jsxRuntime.jsx("p", { ...attributes, style: { position: "relative" }, children }); default: return null; } } function ComposerEditorLeaf({ attributes, children, leaf }) { if (leaf.bold) { children = /* @__PURE__ */ jsxRuntime.jsx("strong", { children }); } if (leaf.italic) { children = /* @__PURE__ */ jsxRuntime.jsx("em", { children }); } if (leaf.strikethrough) { children = /* @__PURE__ */ jsxRuntime.jsx("s", { children }); } if (leaf.code) { children = /* @__PURE__ */ jsxRuntime.jsx("code", { children }); } return /* @__PURE__ */ jsxRuntime.jsx("span", { ...attributes, children }); } function ComposerEditorPlaceholder({ attributes, children }) { const { opacity: _opacity, ...style } = attributes.style; return /* @__PURE__ */ jsxRuntime.jsx("span", { ...attributes, style, "data-placeholder": "", children }); } const ComposerMention = react.forwardRef( ({ children, asChild, ...props }, forwardedRef) => { const Component = asChild ? reactSlot.Slot : "span"; const isSelected = slateReact.useSelected(); return /* @__PURE__ */ jsxRuntime.jsx(Component, { "data-selected": isSelected || void 0, ...props, ref: forwardedRef, children }); } ); const ComposerLink = react.forwardRef( ({ children, asChild, ...props }, forwardedRef) => { const Component = asChild ? reactSlot.Slot : "a"; return /* @__PURE__ */ jsxRuntime.jsx(Component, { target: "_blank", rel: "noopener noreferrer nofollow", ...props, ref: forwardedRef, children }); } ); const ComposerSuggestions = react.forwardRef(({ children, style, asChild, ...props }, forwardedRef) => { const [isPresent] = Persist.usePersist(); const ref = react.useRef(null); const { ref: contentRef, placement, dir } = contexts.useComposerSuggestionsContext(COMPOSER_SUGGESTIONS_NAME); const mergedRefs = useRefs.useRefs(forwardedRef, contentRef, ref); const [side, align] = react.useMemo( () => utils$1.getSideAndAlignFromFloatingPlacement(placement), [placement] ); const Component = asChild ? reactSlot.Slot : "div"; Persist.useAnimationPersist(ref); return /* @__PURE__ */ jsxRuntime.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 = react.forwardRef(({ children, asChild, ...props }, forwardedRef) => { const { id } = contexts.useComposerSuggestionsContext(COMPOSER_SUGGESTIONS_LIST_NAME); const Component = asChild ? reactSlot.Slot : "ul"; return /* @__PURE__ */ jsxRuntime.jsx(Component, { role: "listbox", id, "aria-label": "Suggestions list", ...props, ref: forwardedRef, children }); }); const ComposerSuggestionsListItem = react.forwardRef( ({ value, children, onPointerMove, onPointerDown, onClick, asChild, ...props }, forwardedRef) => { const ref = react.useRef(null); const mergedRefs = useRefs.useRefs(forwardedRef, ref); const { selectedValue, setSelectedValue, itemId, onItemSelect } = contexts.useComposerSuggestionsContext(COMPOSER_SUGGESTIONS_LIST_ITEM_NAME); const Component = asChild ? reactSlot.Slot : "li"; const isSelected = react.useMemo( () => selectedValue === value, [selectedValue, value] ); const id = react.useMemo(() => itemId(value), [itemId, value]); react.useEffect(() => { if (ref?.current && isSelected) { ref.current.scrollIntoView({ block: "nearest" }); } }, [isSelected]); const handlePointerMove = react.useCallback( (event) => { onPointerMove?.(event); if (!event.isDefaultPrevented()) { setSelectedValue(value); } }, [onPointerMove, setSelectedValue, value] ); const handlePointerDown = react.useCallback( (event) => { onPointerDown?.(event); event.preventDefault(); event.stopPropagation(); }, [onPointerDown] ); const handleClick = react.useCallback( (event) => { onClick?.(event); const wasDefaultPrevented = event.isDefaultPrevented(); event.preventDefault(); event.stopPropagation(); if (!wasDefaultPrevented) { onItemSelect(value); } }, [onClick, onItemSelect, value] ); return /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(ComposerLink, { href, children }); }, Mention: ({ userId }) => { return /* @__PURE__ */ jsxRuntime.jsxs(ComposerMention, { children: [ mentions.MENTION_CHARACTER, userId ] }); }, MentionSuggestions: ({ userIds }) => { return userIds.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(ComposerSuggestions, { children: /* @__PURE__ */ jsxRuntime.jsx(ComposerSuggestionsList, { children: userIds.map((userId) => /* @__PURE__ */ jsxRuntime.jsx(ComposerSuggestionsListItem, { value: userId, children: userId }, userId)) }) }) : null; } }; const ComposerEditor = react.forwardRef( ({ defaultValue, onKeyDown, onFocus, onBlur, disabled, autoFocus, components, dir, ...props }, forwardedRef) => { const client = _private.useClientOrNull(); const { editor, validate, setFocused, onEditorChange, roomId } = contexts.useComposerEditorContext(); const { submit, focus, blur, select, canSubmit, isDisabled: isComposerDisabled, isFocused } = contexts.useComposer(); const isDisabled = isComposerDisabled || disabled; const initialBody = useInitial.useInitial(defaultValue ?? emptyCommentBody); const initialEditorValue = react.useMemo(() => { return utils$1.commentBodyToComposerBody(initialBody); }, [initialBody]); const { Link, Mention, MentionSuggestions, FloatingToolbar } = react.useMemo( () => ({ ...defaultEditorComponents, ...components }), [components] ); const [hasFloatingToolbarRange, setHasFloatingToolbarRange] = react.useState(false); const resolveMentionSuggestions = _private.useResolveMentionSuggestions(); const hasResolveMentionSuggestions = client ? resolveMentionSuggestions : true; const [mentionDraft, setMentionDraft] = react.useState(); const mentionSuggestions = _private.useMentionSuggestions( roomId, mentionDraft?.text ); const [ selectedMentionSuggestionIndex, setPreviousSelectedMentionSuggestionIndex, setNextSelectedMentionSuggestionIndex, setSelectedMentionSuggestionIndex ] = useIndex.useIndex(0, mentionSuggestions?.length ?? 0); const id = react.useId(); const floatingToolbarId = `liveblocks-floating-toolbar-${id}`; const suggestionsListId = `liveblocks-suggestions-list-${id}`; const suggestionsListItemId = react.useCallback( (userId) => userId ? `liveblocks-suggestions-list-item-${id}-${userId}` : void 0, [id] ); const renderElement = react.useCallback( (props2) => { return /* @__PURE__ */ jsxRuntime.jsx(ComposerEditorElement, { Mention, Link, ...props2 }); }, [Link, Mention] ); const handleChange = react.useCallback( (value) => { validate(value); onEditorChange.notify(); }, [validate, onEditorChange] ); const createMention = react.useCallback( (userId) => { if (!mentionDraft || !userId) { return; } slate.Transforms.select(editor, mentionDraft.range); mentions.insertMention(editor, userId); setMentionDraft(void 0); setSelectedMentionSuggestionIndex(0); }, [editor, mentionDraft, setSelectedMentionSuggestionIndex] ); const handleKeyDown = react.useCallback( (event) => { onKeyDown?.(event); if (event.isDefaultPrevented()) { return; } if (isKey.isKey(event, "ArrowLeft")) { marks.leaveMarkEdge(editor, "start"); } if (isKey.isKey(event, "ArrowRight")) { marks.leaveMarkEdge(editor, "end"); } if (mentionDraft && mentionSuggestions?.length) { if (isKey.isKey(event, "ArrowDown")) { event.preventDefault(); setNextSelectedMentionSuggestionIndex(); } if (isKey.isKey(event, "ArrowUp")) { event.preventDefault(); setPreviousSelectedMentionSuggestionIndex(); } if (isKey.isKey(event, "Enter") || isKey.isKey(event, "Tab")) { event.preventDefault(); const userId = mentionSuggestions?.[selectedMentionSuggestionIndex]; createMention(userId); } if (isKey.isKey(event, "Escape")) { event.preventDefault(); setMentionDraft(void 0); setSelectedMentionSuggestionIndex(0); } } else { if (hasFloatingToolbarRange) { if (isKey.isKey(event, "Escape")) { event.preventDefault(); setHasFloatingToolbarRange(false); } } if (isKey.isKey(event, "Escape")) { blur(); } if (isKey.isKey(event, "Enter", { shift: false })) { event.preventDefault(); if (canSubmit) { submit(); } } if (isKey.isKey(event, "Enter", { shift: true })) { event.preventDefault(); editor.insertBreak(); } if (isKey.isKey(event, "b", { mod: true })) { event.preventDefault(); marks.toggleMark(editor, "bold"); } if (isKey.isKey(event, "i", { mod: true })) { event.preventDefault(); marks.toggleMark(editor, "italic"); } if (isKey.isKey(event, "s", { mod: true, shift: true })) { event.preventDefault(); marks.toggleMark(editor, "strikethrough"); } if (isKey.isKey(event, "e", { mod: true })) { event.preventDefault(); marks.toggleMark(editor, "code"); } } }, [ onKeyDown, mentionDraft, mentionSuggestions, hasFloatingToolbarRange, editor, setNextSelectedMentionSuggestionIndex, setPreviousSelectedMentionSuggestionIndex, selectedMentionSuggestionIndex, createMention, setSelectedMentionSuggestionIndex, blur, canSubmit, submit ] ); const handleFocus = react.useCallback( (event) => { onFocus?.(event); if (!event.isDefaultPrevented()) { setFocused(true); } }, [onFocus, setFocused] ); const handleBlur = react.useCallback( (event) => { onBlur?.(event); if (!event.isDefaultPrevented()) { setFocused(false); } }, [onBlur, setFocused] ); const selectedMentionSuggestionUserId = react.useMemo( () => mentionSuggestions?.[selectedMentionSuggestionIndex], [selectedMentionSuggestionIndex, mentionSuggestions] ); const setSelectedMentionSuggestionUserId = react.useCallback( (userId) => { const index = mentionSuggestions?.indexOf(userId); if (index !== void 0 && index >= 0) { setSelectedMentionSuggestionIndex(index); } }, [setSelectedMentionSuggestionIndex, mentionSuggestions] ); const additionalProps = react.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 ] ); react.useImperativeHandle(forwardedRef, () => { return slateReact.ReactEditor.toDOMNode(editor, editor); }, [editor]); _private.useLayoutEffect(() => { if (autoFocus) { focus(); } }, [autoFocus, editor, focus]); _private.useLayoutEffect(() => { if (isFocused && editor.selection === null) { select(); } }, [editor, select, isFocused]); return /* @__PURE__ */ jsxRuntime.jsxs(slateReact.Slate, { editor, initialValue: initialEditorValue, onChange: handleChange, children: [ /* @__PURE__ */ jsxRuntime.jsx(slateReact.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__ */ jsxRuntime.jsx(ComposerEditorMentionSuggestionsWrapper, { dir, mentionDraft, setMentionDraft, selectedUserId: selectedMentionSuggestionUserId, setSelectedUserId: setSelectedMentionSuggestionUserId, userIds: mentionSuggestions, id: suggestionsListId, itemId: suggestionsListItemId, onItemSelect: createMention, MentionSuggestions }), FloatingToolbar && /* @__PURE__ */ jsxRuntime.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: core.createCommentAttachmentId(), name: file.name, size: file.size, mimeType: file.type, file }; } const ComposerForm = react.forwardRef( ({ children, onSubmit, onComposerSubmit, defaultAttachments = [], pasteFilesAsAttachments, preventUnsavedChanges = true, disabled, asChild, roomId: _roomId, ...props }, forwardedRef) => { const Component = asChild ? reactSlot.Slot : "form"; const [isEmpty$1, setEmpty] = react.useState(true); const [isSubmitting, setSubmitting] = react.useState(false); const [isFocused, setFocused] = react.useState(false); const room = react$1.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 } = utils$1.useComposerAttachmentsManager(defaultAttachments, { maxFileSize: maxAttachmentSize, roomId }); const numberOfAttachments = attachments.length; const hasMaxAttachments = numberOfAttachments >= maxAttachments; const isDisabled = react.useMemo(() => { return isSubmitting || disabled === true; }, [isSubmitting, disabled]); const canSubmit = react.useMemo(() => { return !isEmpty$1 && !isUploadingAttachments; }, [isEmpty$1, isUploadingAttachments]); const [marks$1, setMarks] = react.useState(marks.getMarks); const ref = react.useRef(null); const mergedRefs = useRefs.useRefs(forwardedRef, ref); const fileInputRef = react.useRef(null); const syncSource = _private.useSyncSource(); const isPending = !preventUnsavedChanges ? false : !isEmpty$1 || isUploadingAttachments || attachments.length > 0; react.useEffect(() => { syncSource?.setSyncStatus( isPending ? "has-local-changes" : "synchronized" ); }, [syncSource, isPending]); const createAttachments = react.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 = react.useRef(createAttachments); react.useEffect(() => { createAttachmentsRef.current = createAttachments; }, [createAttachments]); const stableCreateAttachments = react.useCallback((files) => { createAttachmentsRef.current(files); }, []); const editor = useInitial.useInitial( () => createComposerEditor({ createAttachments: stableCreateAttachments, pasteFilesAsAttachments }) ); const onEditorChange = useInitial.useInitial(core.makeEventSource); const validate = react.useCallback( (value) => { setEmpty(isEmpty.isEmpty(editor, value)); }, [editor] ); const submit = react.useCallback(() => { if (!canSubmit) { return; } requestAnimationFrame(() => { if (ref.current) { requestSubmit.requestSubmit(ref.current); } }); }, [canSubmit]); const clear = react.useCallback(() => { slate.Transforms.delete(editor, { at: { anchor: slate.Editor.start(editor, []), focus: slate.Editor.end(editor, []) } }); }, [editor]); const select = react.useCallback(() => { slate.Transforms.select(editor, slate.Editor.end(editor, [])); }, [editor]); const focus = react.useCallback( (resetSelection = true) => { try { if (!slateReact.ReactEditor.isFocused(editor)) { slate.Transforms.select( editor, resetSelection || !editor.selection ? slate.Editor.end(editor, []) : editor.selection ); slateReact.ReactEditor.focus(editor); } } catch { } }, [editor] ); const blur = react.useCallback(() => { try { slateReact.ReactEditor.blur(editor); } catch { } }, [editor]); const createMention = react.useCallback(() => { if (disabled) { return; } focus(); mentions.insertMentionCharacter(editor); }, [disabled, editor, focus]); const insertText = react.useCallback( (text) => { if (disabled) { return; } focus(false); slate.insertText(editor, text); }, [disabled, editor, focus] ); const attachFiles = react.useCallback(() => { if (disabled) { return; } if (fileInputRef.current) { fileInputRef.current.click(); } }, [disabled]); const handleAttachmentsInputChange = react.useCallback( (event) => { if (disabled) { return; } if (event.target.files) { createAttachments(Array.from(event.target.files)); event.target.value = ""; } }, [createAttachments, disabled] ); const onSubmitEnd = react.useCallback(() => { clear(); blur(); clearAttachments(); setSubmitting(false); }, [blur, clear, clearAttachments]); const handleSubmit = react.useCallback( (event) => { if (disabled) { return; } const isEmpty2 = isEmpty.isEmpty(editor, editor.children); if (isEmpty2) { event.preventDefault(); return; } onSubmit?.(event); if (!onComposerSubmit || event.isDefaultPrevented()) { event.preventDefault(); return; } const body = utils$1.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 = react.useCallback((event) => { event.stopPropagation(); }, []); const toggleMark = react.useCallback( (mark) => { marks.toggleMark(editor, mark); }, [editor] ); useObservable.useObservable(onEditorChange, () => { setMarks(marks.getMarks(editor)); }); return /* @__PURE__ */ jsxRuntime.jsx(contexts.ComposerEditorContext.Provider, { value: { editor, validate, setFocused, onEditorChange, roomId }, children: /* @__PURE__ */ jsxRuntime.jsx(contexts.ComposerAttachmentsContext.Provider, { value: { createAttachments, isUploadingAttachments, hasMaxAttachments, maxAttachments, maxAttachmentSize }, children: /* @__PURE__ */ jsxRuntime.jsx(contexts.ComposerContext.Provider, { value: { isDisabled, isFocused, isEmpty: isEmpty$1, canSubmit, submit, clear, select, focus, blur, createMention, insertText, attachments, attachFiles, removeAttachment, toggleMark, marks: marks$1 }, children: /* @__PURE__ */ jsxRuntime.jsxs(Component, { ...props, onSubmit: handleSubmit, ref: mergedRefs, children: [ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "file", multiple: true, ref: fileInputRef, onChange: handleAttachmentsInputChange, onClick: stopPropagation, tabIndex: -1, style: { display: "none" } }), /* @__PURE__ */ jsxRuntime.jsx(reactSlot.Slottable, { children }) ] }) }) }) }); } ); const ComposerSubmit = react.forwardRef( ({ children, disabled, asChild, ...props }, forwardedRef) => { const Component = asChild ? reactSlot.Slot : "button"; const { canSubmit, isDisabled: isComposerDisabled } = contexts.useComposer(); const isDisabled = isComposerDisabled || disabled || !canSubmit; return /* @__PURE__ */ jsxRuntime.jsx(Component, { type: "submit", ...props, ref: forwardedRef, disabled: isDisabled, children }); } ); const ComposerAttachFiles = react.forwardRef(({ children, onClick, disabled, asChild, ...props }, forwardedRef) => { const Component = asChild ? reactSlot.Slot : "button"; const { hasMaxAttachments } = contexts.useComposerAttachmentsContext(); const { isDisabled: isComposerDisabled, attachFiles } = contexts.useComposer(); const isDisabled = isComposerDisabled || hasMaxAttachments || disabled; const handleClick = react.useCallback( (event) => { onClick?.(event); if (!event.isDefaultPrevented()) { attachFiles(); } }, [attachFiles, onClick] ); return /* @__PURE__ */ jsxRuntime.jsx(Component, { type: "button", ...props, onClick: handleClick, ref: forwardedRef, disabled: isDisabled, children }); }); const ComposerAttachmentsDropArea = react.forwardRef( ({ onDragEnter, onDragLeave, onDragOver, onDrop, disabled, asChild, ...props }, forwardedRef) => { const Component = asChild ? reactSlot.Slot : "div"; const { isDisabled: isComposerDisabled } = contexts.useComposer(); const isDisabled = isComposerDisabled || disabled; const [, dropAreaProps] = utils$1.useComposerAttachmentsDropArea({ onDragEnter, onDragLeave, onDragOver, onDrop, disabled: isDisabled }); return /* @__PURE__ */ jsxRuntime.jsx(Component, { ...dropAreaProps, "data-disabled": isDisabled ? "" : void 0, ...props, ref: forwardedRef }); } ); const ComposerMarkToggle = react.forwardRef( ({ children, mark, onValueChange, onClick, onPointerDown, asChild, ...props }, forwardedRef) => { const Component = asChild ? reactSlot.Slot : "button"; const { marks, toggleMark } = contexts.useComposer(); const handlePointerDown = react.useCallback( (event) => { onPointerDown?.(event); event.preventDefault(); event.stopPropagation(); }, [onPointerDown] ); const handleClick = react.useCallback( (event) => { onClick?.(event); if (!event.isDefaultPrevented()) { event.preventDefault(); event.stopPropagation(); toggleMark(mark); onValueChange?.(mark); } }, [mark, onClick, onValueChange, toggleMark] ); return /* @__PURE__ */ jsxRuntime.jsx(TogglePrimitive__namespace.Root, { asChild: true, pressed: marks[mark], onClick: handleClick, onPointerDown: handlePointerDown, ...props, children: /* @__PURE__ */ jsxRuntime.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; } exports.AttachFiles = ComposerAttachFiles; exports.AttachmentsDropArea = ComposerAttachmentsDropArea; exports.Editor = ComposerEditor; exports.FloatingToolbar = ComposerFloatingToolbar; exports.Form = ComposerForm; exports.Link = ComposerLink; exports.MarkToggle = ComposerMarkToggle; exports.Mention = ComposerMention; exports.Submit = ComposerSubmit; exports.Suggestions = ComposerSuggestions; exports.SuggestionsList = ComposerSuggestionsList; exports.SuggestionsListItem = ComposerSuggestionsListItem; //# sourceMappingURL=index.cjs.map