@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.
419 lines (416 loc) • 13.1 kB
JavaScript
import { inline, flip, hide, shift, limitShift, offset, size, autoUpdate, useFloating } from '@floating-ui/react-dom';
import { makeEventSource, kInternal, HttpError } from '@liveblocks/core';
import { useClient } from '@liveblocks/react';
import { useLayoutEffect } from '@liveblocks/react/_private';
import { useState, useCallback, useMemo, useEffect, useSyncExternalStore } from 'react';
import { FLOATING_ELEMENT_COLLISION_PADDING, FLOATING_ELEMENT_SIDE_OFFSET } from '../../constants.js';
import { isComposerBodyAutoLink } from '../../slate/plugins/auto-links.js';
import { isComposerBodyCustomLink } from '../../slate/plugins/custom-links.js';
import { isComposerBodyMention } from '../../slate/plugins/mentions.js';
import { isText } from '../../slate/utils/is-text.js';
import { getFiles } from '../../utils/data-transfer.js';
import { exists } from '../../utils/exists.js';
import { useInitial } from '../../utils/use-initial.js';
import { useLatest } from '../../utils/use-latest.js';
import { isCommentBodyMention, isCommentBodyLink, isCommentBodyText } from '../Comment/utils.js';
import { useComposer, useComposerAttachmentsContext } from './contexts.js';
function composerBodyMentionToCommentBodyMention(mention) {
return {
type: "mention",
id: mention.id
};
}
function composerBodyAutoLinkToCommentBodyLink(link) {
return {
type: "link",
url: link.url
};
}
function composerBodyCustomLinkToCommentBodyLink(link) {
return {
type: "link",
url: link.url,
text: link.children.map((child) => child.text).join("")
};
}
function commentBodyMentionToComposerBodyMention(mention) {
return {
type: "mention",
id: mention.id,
children: [{ text: "" }]
};
}
function commentBodyLinkToComposerBodyLink(link) {
if (link.text) {
return {
type: "custom-link",
url: link.url,
children: [{ text: link.text }]
};
} else {
return {
type: "auto-link",
url: link.url,
children: [{ text: link.url }]
};
}
}
function composerBodyToCommentBody(body) {
return {
version: 1,
content: body.map((block) => {
if (block.type !== "paragraph") {
return null;
}
const children = block.children.map((inline2) => {
if (isComposerBodyMention(inline2)) {
return composerBodyMentionToCommentBodyMention(inline2);
}
if (isComposerBodyAutoLink(inline2)) {
return composerBodyAutoLinkToCommentBodyLink(inline2);
}
if (isComposerBodyCustomLink(inline2)) {
return composerBodyCustomLinkToCommentBodyLink(inline2);
}
if (isText(inline2)) {
return inline2;
}
return null;
}).filter(exists);
return {
...block,
children
};
}).filter(exists)
};
}
const emptyComposerBody = [];
function commentBodyToComposerBody(body) {
if (!body || !body?.content) {
return emptyComposerBody;
}
return body.content.map((block) => {
if (block.type !== "paragraph") {
return null;
}
const children = block.children.map((inline2) => {
if (isCommentBodyMention(inline2)) {
return commentBodyMentionToComposerBodyMention(inline2);
}
if (isCommentBodyLink(inline2)) {
return commentBodyLinkToComposerBodyLink(inline2);
}
if (isCommentBodyText(inline2)) {
return inline2;
}
return null;
}).filter(exists);
return {
...block,
children
};
}).filter(exists);
}
function getRtlFloatingAlignment(alignment) {
switch (alignment) {
case "start":
return "end";
case "end":
return "start";
default:
return "center";
}
}
function getSideAndAlignFromFloatingPlacement(placement) {
const [side, align = "center"] = placement.split("-");
return [side, align];
}
function useContentZIndex() {
const [content, setContent] = useState(null);
const contentRef = useCallback(setContent, [setContent]);
const [contentZIndex, setContentZIndex] = useState();
useLayoutEffect(() => {
if (content) {
setContentZIndex(window.getComputedStyle(content).zIndex);
}
}, [content]);
return [contentRef, contentZIndex];
}
function useFloatingWithOptions({
type = "bounds",
position,
alignment,
dir,
open
}) {
const floatingOptions = useMemo(() => {
const detectOverflowOptions = {
padding: FLOATING_ELEMENT_COLLISION_PADDING
};
const middleware = [
type === "range" ? inline(detectOverflowOptions) : null,
flip({ ...detectOverflowOptions, crossAxis: false }),
hide(detectOverflowOptions),
shift({
...detectOverflowOptions,
limiter: limitShift()
}),
type === "range" ? offset(FLOATING_ELEMENT_SIDE_OFFSET) : null,
size({
...detectOverflowOptions,
apply({ availableWidth, availableHeight, elements }) {
elements.floating.style.setProperty(
"--lb-composer-floating-available-width",
`${availableWidth}px`
);
elements.floating.style.setProperty(
"--lb-composer-floating-available-height",
`${availableHeight}px`
);
}
})
];
return {
strategy: "fixed",
placement: alignment === "center" ? position : `${position}-${dir === "rtl" ? getRtlFloatingAlignment(alignment) : alignment}`,
middleware,
whileElementsMounted: (...args) => {
return autoUpdate(...args, {
animationFrame: true
});
}
};
}, [alignment, position, dir, type]);
return useFloating({
...floatingOptions,
open
});
}
function useComposerAttachmentsDropArea({
onDragEnter,
onDragLeave,
onDragOver,
onDrop,
disabled
}) {
const { isDisabled: isComposerDisabled } = useComposer();
const isDisabled = isComposerDisabled || disabled;
const { createAttachments } = useComposerAttachmentsContext();
const [isDraggingOver, setDraggingOver] = useState(false);
const latestIsDraggingOver = useLatest(isDraggingOver);
const handleDragEnter = useCallback(
(event) => {
onDragEnter?.(event);
if (latestIsDraggingOver.current || isDisabled || event.isDefaultPrevented()) {
return;
}
const dataTransfer = event.dataTransfer;
if (!dataTransfer.types.includes("Files")) {
return;
}
event.preventDefault();
event.stopPropagation();
setDraggingOver(true);
},
[onDragEnter, isDisabled]
);
const handleDragLeave = useCallback(
(event) => {
onDragLeave?.(event);
if (!latestIsDraggingOver.current || isDisabled || event.isDefaultPrevented()) {
return;
}
if (event.relatedTarget ? event.relatedTarget === event.currentTarget || event.currentTarget.contains(event.relatedTarget) : event.currentTarget !== event.target) {
return;
}
event.preventDefault();
event.stopPropagation();
setDraggingOver(false);
},
[onDragLeave, isDisabled]
);
const handleDragOver = useCallback(
(event) => {
onDragOver?.(event);
if (isDisabled || event.isDefaultPrevented()) {
return;
}
event.preventDefault();
event.stopPropagation();
},
[onDragOver, isDisabled]
);
const handleDrop = useCallback(
(event) => {
onDrop?.(event);
if (!latestIsDraggingOver.current || isDisabled || event.isDefaultPrevented()) {
return;
}
event.preventDefault();
event.stopPropagation();
setDraggingOver(false);
const files = getFiles(event.dataTransfer);
createAttachments(files);
},
[onDrop, isDisabled, createAttachments]
);
return [
isDraggingOver,
{
onDragEnter: handleDragEnter,
onDragLeave: handleDragLeave,
onDragOver: handleDragOver,
onDrop: handleDrop,
"data-drop": isDraggingOver ? "" : void 0,
"data-disabled": isDisabled ? "" : void 0
}
];
}
class AttachmentTooLargeError extends Error {
origin;
name = "AttachmentTooLargeError";
constructor(message, origin = "client") {
super(message);
this.origin = origin;
}
}
function createComposerAttachmentsManager(client, roomId, options) {
const attachments = /* @__PURE__ */ new Map();
const abortControllers = /* @__PURE__ */ new Map();
const eventSource = makeEventSource();
let cachedSnapshot = null;
function notifySubscribers() {
cachedSnapshot = null;
eventSource.notify();
}
function uploadAttachment(attachment) {
const abortController = new AbortController();
abortControllers.set(attachment.id, abortController);
client[kInternal].httpClient.uploadAttachment({
roomId,
attachment,
signal: abortController.signal
}).then(() => {
attachments.set(attachment.id, {
...attachment,
status: "uploaded"
});
notifySubscribers();
}).catch((error) => {
if (error instanceof Error && error.name !== "AbortError" && error.name !== "TimeoutError") {
attachments.set(attachment.id, {
...attachment,
status: "error",
error: error instanceof HttpError && error.status === 413 ? new AttachmentTooLargeError("File is too large.", "server") : error
});
notifySubscribers();
}
});
}
function addAttachments(addedAttachments) {
if (addedAttachments.length === 0) {
return;
}
const newAttachments = addedAttachments.filter(
(attachment) => !attachments.has(attachment.id)
);
const attachmentsToUpload = [];
for (const attachment of newAttachments) {
if (attachment.type === "localAttachment") {
if (attachment.file.size > options.maxFileSize) {
attachments.set(attachment.id, {
...attachment,
status: "error",
error: new AttachmentTooLargeError("File is too large.", "client")
});
continue;
}
attachments.set(attachment.id, {
...attachment,
status: "uploading"
});
attachmentsToUpload.push(attachment);
} else {
attachments.set(attachment.id, attachment);
}
}
if (newAttachments.length > 0) {
notifySubscribers();
}
for (const attachment of attachmentsToUpload) {
uploadAttachment(attachment);
}
}
function removeAttachment(attachmentId) {
const abortController = abortControllers.get(attachmentId);
abortController?.abort();
attachments.delete(attachmentId);
abortControllers.delete(attachmentId);
notifySubscribers();
}
function getSnapshot() {
if (!cachedSnapshot) {
cachedSnapshot = Array.from(attachments.values());
}
return cachedSnapshot;
}
function clear() {
abortControllers.forEach((controller) => controller.abort());
abortControllers.clear();
attachments.clear();
notifySubscribers();
}
return {
addAttachments,
removeAttachment,
getSnapshot,
subscribe: eventSource.subscribe,
clear
};
}
function preventBeforeUnloadDefault(event) {
event.preventDefault();
}
function useComposerAttachmentsManager(defaultAttachments, options) {
const client = useClient();
const frozenDefaultAttachments = useInitial(defaultAttachments);
const frozenAttachmentsManager = useInitial(
() => createComposerAttachmentsManager(client, options.roomId, options)
);
useEffect(() => {
frozenAttachmentsManager.addAttachments(frozenDefaultAttachments);
}, [frozenDefaultAttachments, frozenAttachmentsManager]);
useEffect(() => {
return () => {
frozenAttachmentsManager.clear();
};
}, [frozenAttachmentsManager]);
const attachments = useSyncExternalStore(
frozenAttachmentsManager.subscribe,
frozenAttachmentsManager.getSnapshot,
frozenAttachmentsManager.getSnapshot
);
const isUploadingAttachments = useMemo(() => {
return attachments.some(
(attachment) => attachment.type === "localAttachment" && attachment.status === "uploading"
);
}, [attachments]);
useEffect(() => {
if (!isUploadingAttachments) {
return;
}
window.addEventListener("beforeunload", preventBeforeUnloadDefault);
return () => {
window.removeEventListener("beforeunload", preventBeforeUnloadDefault);
};
}, [isUploadingAttachments]);
return {
attachments,
isUploadingAttachments,
addAttachments: frozenAttachmentsManager.addAttachments,
removeAttachment: frozenAttachmentsManager.removeAttachment,
clearAttachments: frozenAttachmentsManager.clear
};
}
export { AttachmentTooLargeError, commentBodyLinkToComposerBodyLink, commentBodyMentionToComposerBodyMention, commentBodyToComposerBody, composerBodyAutoLinkToCommentBodyLink, composerBodyCustomLinkToCommentBodyLink, composerBodyMentionToCommentBodyMention, composerBodyToCommentBody, getRtlFloatingAlignment, getSideAndAlignFromFloatingPlacement, useComposerAttachmentsDropArea, useComposerAttachmentsManager, useContentZIndex, useFloatingWithOptions };
//# sourceMappingURL=utils.js.map