@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
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');
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