@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
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 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