@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.
579 lines (574 loc) • 20.5 kB
JavaScript
"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');
require('../icons/index.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 cn = require('../utils/cn.cjs');
var useControllableState = require('../utils/use-controllable-state.cjs');
var useGroupMention = require('../utils/use-group-mention.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 Group = require('./internal/Group.cjs');
var GroupDescription = require('./internal/GroupDescription.cjs');
var Tooltip = require('./internal/Tooltip.cjs');
var User = require('./internal/User.cjs');
var PopoverPrimitive = require('@radix-ui/react-popover');
var Users = require('../icons/Users.cjs');
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: cn.cn("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: cn.cn("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: cn.cn("lb-composer-editor-action", className),
onPointerDown: preventDefault,
onClick: stopPropagation,
"aria-label": label,
icon: /* @__PURE__ */ jsxRuntime.jsx(Attachment.AttachmentIcon, {}),
...props
}
) }) });
}
function ComposerUserMention({ mention }) {
return /* @__PURE__ */ jsxRuntime.jsxs(index.Mention, { className: "lb-mention lb-composer-mention", children: [
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "lb-mention-symbol", children: core.MENTION_CHARACTER }),
/* @__PURE__ */ jsxRuntime.jsx(User.User, { userId: mention.id })
] });
}
function ComposerGroupMention({ mention }) {
const isMember = useGroupMention.useIsGroupMentionMember(mention);
return /* @__PURE__ */ jsxRuntime.jsxs(
index.Mention,
{
className: "lb-mention lb-composer-mention",
"data-self": isMember ? "" : void 0,
children: [
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "lb-mention-symbol", children: core.MENTION_CHARACTER }),
/* @__PURE__ */ jsxRuntime.jsx(Group.Group, { groupId: mention.id })
]
}
);
}
function ComposerMention({ mention, ...props }) {
switch (mention.kind) {
case "user":
return /* @__PURE__ */ jsxRuntime.jsx(ComposerUserMention, { mention, ...props });
case "group":
return /* @__PURE__ */ jsxRuntime.jsx(ComposerGroupMention, { mention, ...props });
default:
return core.assertNever(mention, "Unhandled mention kind");
}
}
function ComposerMentionSuggestions({
mentions
}) {
return mentions.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: mentions.map((mention) => {
return /* @__PURE__ */ jsxRuntime.jsx(
index.SuggestionsListItem,
{
className: "lb-composer-suggestions-list-item lb-composer-mention-suggestion",
value: mention.id,
children: mention.kind === "user" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
/* @__PURE__ */ jsxRuntime.jsx(
Avatar.Avatar,
{
userId: mention.id,
className: "lb-composer-mention-suggestion-avatar"
}
),
/* @__PURE__ */ jsxRuntime.jsx(
User.User,
{
userId: mention.id,
className: "lb-composer-mention-suggestion-user"
}
)
] }) : mention.kind === "group" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
/* @__PURE__ */ jsxRuntime.jsx(
Avatar.Avatar,
{
groupId: mention.id,
className: "lb-composer-mention-suggestion-avatar",
icon: /* @__PURE__ */ jsxRuntime.jsx(Users.UsersIcon, {})
}
),
/* @__PURE__ */ jsxRuntime.jsx(
Group.Group,
{
groupId: mention.id,
className: "lb-composer-mention-suggestion-group",
children: /* @__PURE__ */ jsxRuntime.jsx(
GroupDescription.GroupDescription,
{
groupId: mention.id,
className: "lb-composer-mention-suggestion-group-description"
}
)
}
)
] }) : core.assertNever(mention, "Unhandled mention kind")
},
mention.id
);
}) }) }) : 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: cn.cn("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: cn.cn("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 || !showAttachments || 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,
blurOnSubmit = true,
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(
defaultCollapsed ?? false,
controlledCollapsed,
controlledOnCollapsedChange
);
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 handleComposerSubmit = react.useCallback(
(comment, event) => {
onComposerSubmit?.(comment, event);
if (event.isDefaultPrevented()) {
return;
}
event.stopPropagation();
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: handleComposerSubmit,
className: cn.cn("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,
blurOnSubmit,
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.ComposerMention = ComposerMention;
exports.ComposerRoomIdContext = ComposerRoomIdContext;
//# sourceMappingURL=Composer.cjs.map