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.

419 lines (416 loc) 13.1 kB
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