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.

570 lines (565 loc) 18.7 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 react = require('react'); var config = require('../config.cjs'); var constants = require('../constants.cjs'); var Attachment = require('../icons/Attachment.cjs'); var Bold = require('../icons/Bold.cjs'); var Code = require('../icons/Code.cjs'); var Emoji = require('../icons/Emoji.cjs'); var Italic = require('../icons/Italic.cjs'); var Mention = require('../icons/Mention.cjs'); var Send = require('../icons/Send.cjs'); var Strikethrough = require('../icons/Strikethrough.cjs'); var overrides = require('../overrides.cjs'); var index = require('../primitives/Composer/index.cjs'); var contexts = require('../primitives/Composer/contexts.cjs'); var utils = require('../primitives/Composer/utils.cjs'); var mentions = require('../slate/plugins/mentions.cjs'); var classNames = require('../utils/class-names.cjs'); var useControllableState = require('../utils/use-controllable-state.cjs'); var Attachment$1 = require('./internal/Attachment.cjs'); var Attribution = require('./internal/Attribution.cjs'); var Avatar = require('./internal/Avatar.cjs'); var Button = require('./internal/Button.cjs'); var EmojiPicker = require('./internal/EmojiPicker.cjs'); var Tooltip = require('./internal/Tooltip.cjs'); var User = require('./internal/User.cjs'); var PopoverPrimitive = require('@radix-ui/react-popover'); var TooltipPrimitive = require('@radix-ui/react-tooltip'); function ComposerInsertMentionEditorAction({ label, tooltipLabel, className, onClick, ...props }) { const { createMention } = contexts.useComposer(); const preventDefault = react.useCallback((event) => { event.preventDefault(); }, []); const handleClick = react.useCallback( (event) => { onClick?.(event); if (!event.isDefaultPrevented()) { event.stopPropagation(); createMention(); } }, [createMention, onClick] ); return /* @__PURE__ */ jsxRuntime.jsx(Tooltip.Tooltip, { content: tooltipLabel ?? label, children: /* @__PURE__ */ jsxRuntime.jsx(Button.Button, { className: classNames.classNames("lb-composer-editor-action", className), onPointerDown: preventDefault, onClick: handleClick, "aria-label": label, icon: /* @__PURE__ */ jsxRuntime.jsx(Mention.MentionIcon, {}), ...props }) }); } function ComposerInsertEmojiEditorAction({ label, tooltipLabel, onPickerOpenChange, className, ...props }) { const { insertText } = contexts.useComposer(); const preventDefault = react.useCallback((event) => { event.preventDefault(); }, []); const stopPropagation = react.useCallback((event) => { event.stopPropagation(); }, []); return /* @__PURE__ */ jsxRuntime.jsx(EmojiPicker.EmojiPicker, { onEmojiSelect: insertText, onOpenChange: onPickerOpenChange, children: /* @__PURE__ */ jsxRuntime.jsx(Tooltip.Tooltip, { content: tooltipLabel ?? label, children: /* @__PURE__ */ jsxRuntime.jsx(PopoverPrimitive.PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(Button.Button, { className: classNames.classNames("lb-composer-editor-action", className), onPointerDown: preventDefault, onClick: stopPropagation, "aria-label": label, icon: /* @__PURE__ */ jsxRuntime.jsx(Emoji.EmojiIcon, {}), ...props }) }) }) }); } function ComposerAttachFilesEditorAction({ label, tooltipLabel, className, ...props }) { const preventDefault = react.useCallback((event) => { event.preventDefault(); }, []); const stopPropagation = react.useCallback((event) => { event.stopPropagation(); }, []); return /* @__PURE__ */ jsxRuntime.jsx(Tooltip.Tooltip, { content: tooltipLabel ?? label, children: /* @__PURE__ */ jsxRuntime.jsx(index.AttachFiles, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(Button.Button, { className: classNames.classNames("lb-composer-editor-action", className), onPointerDown: preventDefault, onClick: stopPropagation, "aria-label": label, icon: /* @__PURE__ */ jsxRuntime.jsx(Attachment.AttachmentIcon, {}), ...props }) }) }); } function ComposerMention({ userId }) { return /* @__PURE__ */ jsxRuntime.jsxs(index.Mention, { className: "lb-composer-mention", children: [ mentions.MENTION_CHARACTER, /* @__PURE__ */ jsxRuntime.jsx(User.User, { userId }) ] }); } function ComposerMentionSuggestions({ userIds }) { return userIds.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(index.Suggestions, { className: "lb-root lb-portal lb-elevation lb-composer-suggestions lb-composer-mention-suggestions", children: /* @__PURE__ */ jsxRuntime.jsx(index.SuggestionsList, { className: "lb-composer-suggestions-list lb-composer-mention-suggestions-list", children: userIds.map((userId) => /* @__PURE__ */ jsxRuntime.jsxs(index.SuggestionsListItem, { className: "lb-composer-suggestions-list-item lb-composer-mention-suggestion", value: userId, children: [ /* @__PURE__ */ jsxRuntime.jsx(Avatar.Avatar, { userId, className: "lb-composer-mention-suggestion-avatar" }), /* @__PURE__ */ jsxRuntime.jsx(User.User, { userId, className: "lb-composer-mention-suggestion-user" }) ] }, userId)) }) }) : null; } function MarkToggle({ mark, icon, shortcut, children, ...props }) { const $ = overrides.useOverrides(); const label = react.useMemo(() => { return $.COMPOSER_TOGGLE_MARK(mark); }, [$, mark]); return /* @__PURE__ */ jsxRuntime.jsx(Tooltip.ShortcutTooltip, { content: label, shortcut, sideOffset: constants.FLOATING_ELEMENT_SIDE_OFFSET + 2, children: /* @__PURE__ */ jsxRuntime.jsx(index.MarkToggle, { mark, asChild: true, ...props, children: /* @__PURE__ */ jsxRuntime.jsx(Button.Button, { "aria-label": label, variant: "toolbar", icon, children }) }) }); } const markToggles = { bold: () => /* @__PURE__ */ jsxRuntime.jsx(MarkToggle, { mark: "bold", shortcut: "Mod-B", icon: /* @__PURE__ */ jsxRuntime.jsx(Bold.BoldIcon, {}) }), italic: () => /* @__PURE__ */ jsxRuntime.jsx(MarkToggle, { mark: "italic", shortcut: "Mod-I", icon: /* @__PURE__ */ jsxRuntime.jsx(Italic.ItalicIcon, {}) }), strikethrough: () => /* @__PURE__ */ jsxRuntime.jsx(MarkToggle, { mark: "strikethrough", shortcut: "Mod-Shift-S", icon: /* @__PURE__ */ jsxRuntime.jsx(Strikethrough.StrikethroughIcon, {}) }), code: () => /* @__PURE__ */ jsxRuntime.jsx(MarkToggle, { mark: "code", shortcut: "Mod-E", icon: /* @__PURE__ */ jsxRuntime.jsx(Code.CodeIcon, {}) }) }; const markTogglesList = Object.entries(markToggles).map(([mark, Toggle]) => /* @__PURE__ */ jsxRuntime.jsx(Toggle, {}, mark)); function ComposerFloatingToolbar() { return /* @__PURE__ */ jsxRuntime.jsx(index.FloatingToolbar, { className: "lb-root lb-portal lb-elevation lb-composer-floating-toolbar", children: markTogglesList }); } function ComposerLink({ href, children }) { return /* @__PURE__ */ jsxRuntime.jsx(index.Link, { href, className: "lb-composer-link", children }); } function ComposerFileAttachment({ attachment, className, overrides, ...props }) { const { removeAttachment } = contexts.useComposer(); const { roomId } = contexts.useComposerEditorContext(); const handleDeleteClick = react.useCallback(() => { removeAttachment(attachment.id); }, [attachment.id, removeAttachment]); return /* @__PURE__ */ jsxRuntime.jsx(Attachment$1.FileAttachment, { className: classNames.classNames("lb-composer-attachment", className), ...props, attachment, onDeleteClick: handleDeleteClick, preventFocusOnDelete: true, overrides, roomId }); } function ComposerAttachments({ overrides, className, ...props }) { const { attachments } = contexts.useComposer(); if (attachments.length === 0) { return null; } return /* @__PURE__ */ jsxRuntime.jsx("div", { className: classNames.classNames("lb-composer-attachments", className), ...props, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lb-attachments", children: attachments.map((attachment) => { return /* @__PURE__ */ jsxRuntime.jsx(ComposerFileAttachment, { attachment, overrides }, attachment.id); }) }) }); } const editorRequiredComponents = { Mention: ComposerMention, MentionSuggestions: ComposerMentionSuggestions, Link: ComposerLink }; function ComposerEditorContainer({ showAttachments = true, showFormattingControls = true, showAttribution, defaultValue, isCollapsed, overrides: overrides$1, actions, autoFocus, disabled, hasResolveMentionSuggestions, onEmojiPickerOpenChange, onEmptyChange, onEditorClick }) { const { isEmpty } = contexts.useComposer(); const { hasMaxAttachments } = contexts.useComposerAttachmentsContext(); const $ = overrides.useOverrides(overrides$1); const components = react.useMemo(() => { return { ...editorRequiredComponents, FloatingToolbar: showFormattingControls ? ComposerFloatingToolbar : void 0 }; }, [showFormattingControls]); const [isDraggingOver, dropAreaProps] = utils.useComposerAttachmentsDropArea({ disabled: disabled || hasMaxAttachments }); _private.useLayoutEffect(() => { onEmptyChange(isEmpty); }, [isEmpty, onEmptyChange]); const preventDefault = react.useCallback((event) => { event.preventDefault(); }, []); const stopPropagation = react.useCallback((event) => { event.stopPropagation(); }, []); return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lb-composer-editor-container", ...dropAreaProps, children: [ /* @__PURE__ */ jsxRuntime.jsx(index.Editor, { className: "lb-composer-editor", onClick: onEditorClick, placeholder: $.COMPOSER_PLACEHOLDER, defaultValue, autoFocus, components, disabled, dir: $.dir }), showAttachments && /* @__PURE__ */ jsxRuntime.jsx(ComposerAttachments, { overrides: overrides$1 }), (!isCollapsed || isDraggingOver) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lb-composer-footer", children: [ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lb-composer-editor-actions", children: [ hasResolveMentionSuggestions && /* @__PURE__ */ jsxRuntime.jsx(ComposerInsertMentionEditorAction, { label: $.COMPOSER_INSERT_MENTION, disabled }), /* @__PURE__ */ jsxRuntime.jsx(ComposerInsertEmojiEditorAction, { label: $.COMPOSER_INSERT_EMOJI, onPickerOpenChange: onEmojiPickerOpenChange, disabled }), showAttachments && /* @__PURE__ */ jsxRuntime.jsx(ComposerAttachFilesEditorAction, { label: $.COMPOSER_ATTACH_FILES, disabled }) ] }), showAttribution && /* @__PURE__ */ jsxRuntime.jsx(Attribution.Attribution, {}), /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lb-composer-actions", children: actions ?? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsx(Tooltip.ShortcutTooltip, { content: $.COMPOSER_SEND, shortcut: "Enter", children: /* @__PURE__ */ jsxRuntime.jsx(index.Submit, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(Button.Button, { onPointerDown: preventDefault, onClick: stopPropagation, className: "lb-composer-action", variant: "primary", "aria-label": $.COMPOSER_SEND, icon: /* @__PURE__ */ jsxRuntime.jsx(Send.SendIcon, {}) }) }) }) }) }) ] }), showAttachments && isDraggingOver && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lb-composer-attachments-drop-area", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lb-composer-attachments-drop-area-label", children: [ /* @__PURE__ */ jsxRuntime.jsx(Attachment.AttachmentIcon, {}), $.COMPOSER_ATTACH_FILES ] }) }) ] }); } const ComposerRoomIdContext = react.createContext(null); const Composer = react.forwardRef( ({ threadId, commentId, metadata, defaultValue, defaultAttachments, onComposerSubmit, collapsed: controlledCollapsed, defaultCollapsed, onCollapsedChange: controlledOnCollapsedChange, overrides: overrides$1, actions, onBlur, className, onFocus, autoFocus, disabled, showAttachments = true, showFormattingControls = true, showAttribution, roomId: _roomId, ...props }, forwardedRef) => { const room = react$1.useRoom({ allowOutsideRoom: true }); const roomId = _roomId !== void 0 ? _roomId : room?.id; if (roomId === void 0) { throw new Error( "Composer must be a descendant of RoomProvider component" ); } const createThread = _private.useCreateRoomThread(roomId); const createComment = _private.useCreateRoomComment(roomId); const editComment = _private.useEditRoomComment(roomId); const { preventUnsavedComposerChanges } = config.useLiveblocksUIConfig(); const hasResolveMentionSuggestions = _private.useResolveMentionSuggestions() !== void 0; const isEmptyRef = react.useRef(true); const isEmojiPickerOpenRef = react.useRef(false); const $ = overrides.useOverrides(overrides$1); const [isCollapsed, onCollapsedChange] = useControllableState.useControllableState( controlledCollapsed === void 0 && defaultCollapsed === void 0 ? false : controlledCollapsed, controlledOnCollapsedChange, defaultCollapsed ); const canCommentFallback = react.useSyncExternalStore( react.useCallback( (callback) => { if (room === null) return () => { }; return room.events.self.subscribeOnce(callback); }, [room] ), react.useCallback(() => { return room?.getSelf()?.canComment ?? true; }, [room]), react.useCallback(() => true, []) ); const permissions = _private.useRoomPermissions(roomId); const canComment = permissions.size > 0 ? permissions.has(core.Permission.CommentsWrite) || permissions.has(core.Permission.Write) : canCommentFallback; const setEmptyRef = react.useCallback((isEmpty) => { isEmptyRef.current = isEmpty; }, []); const setEmojiPickerOpenRef = react.useCallback((isEmojiPickerOpen) => { isEmojiPickerOpenRef.current = isEmojiPickerOpen; }, []); const handleFocus = react.useCallback( (event) => { onFocus?.(event); if (event.isDefaultPrevented()) { return; } if (isEmptyRef.current && canComment) { onCollapsedChange?.(false); } }, [onCollapsedChange, onFocus, canComment] ); const handleBlur = react.useCallback( (event) => { onBlur?.(event); if (event.isDefaultPrevented()) { return; } const isOutside = !event.currentTarget.contains( event.relatedTarget ?? document.activeElement ); if (isOutside && isEmptyRef.current && !isEmojiPickerOpenRef.current) { onCollapsedChange?.(true); } }, [onBlur, onCollapsedChange] ); const handleEditorClick = react.useCallback( (event) => { event.stopPropagation(); if (isEmptyRef.current && canComment) { onCollapsedChange?.(false); } }, [onCollapsedChange, canComment] ); const handleCommentSubmit = react.useCallback( (comment, event) => { onComposerSubmit?.(comment, event); if (event.isDefaultPrevented()) { return; } if (commentId && threadId) { editComment({ commentId, threadId, body: comment.body, attachments: comment.attachments }); } else if (threadId) { createComment({ threadId, body: comment.body, attachments: comment.attachments }); } else { createThread({ body: comment.body, metadata: metadata ?? {}, attachments: comment.attachments }); } }, [ commentId, createComment, createThread, editComment, metadata, onComposerSubmit, threadId ] ); return /* @__PURE__ */ jsxRuntime.jsx(TooltipPrimitive.TooltipProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(index.Form, { onComposerSubmit: handleCommentSubmit, className: classNames.classNames( "lb-root lb-composer lb-composer-form", className ), dir: $.dir, ...props, ref: forwardedRef, "data-collapsed": isCollapsed ? "" : void 0, onFocus: handleFocus, onBlur: handleBlur, disabled: disabled || !canComment, defaultAttachments, pasteFilesAsAttachments: showAttachments, preventUnsavedChanges: preventUnsavedComposerChanges, roomId, children: /* @__PURE__ */ jsxRuntime.jsx(ComposerEditorContainer, { defaultValue, actions, overrides: overrides$1, isCollapsed, showAttachments, showAttribution, showFormattingControls, hasResolveMentionSuggestions, onEmptyChange: setEmptyRef, onEmojiPickerOpenChange: setEmojiPickerOpenRef, onEditorClick: handleEditorClick, autoFocus, disabled }) }) }); } ); exports.Composer = Composer; exports.ComposerRoomIdContext = ComposerRoomIdContext; //# sourceMappingURL=Composer.cjs.map