diff --git a/apps/client/package.json b/apps/client/package.json index e9197ef9..504f0f5f 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -24,7 +24,6 @@ "@mantine/spotlight": "^8.3.12", "@tabler/icons-react": "^3.36.1", "@tanstack/react-query": "^5.90.17", - "@tiptap/extension-character-count": "^2.27.1", "alfaaz": "^1.1.0", "axios": "^1.13.2", "clsx": "^2.1.1", @@ -54,7 +53,6 @@ "react-router-dom": "^7.12.0", "semver": "^7.7.3", "socket.io-client": "^4.8.3", - "tippy.js": "^6.3.7", "tiptap-extension-global-drag-handle": "^0.1.18", "zod": "^3.25.76" }, diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 8cb33378..0cdfbee0 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -328,6 +328,8 @@ "Upload any image from your device.": "Upload any image from your device.", "Upload any video from your device.": "Upload any video from your device.", "Upload any file from your device.": "Upload any file from your device.", + "Uploading {{name}}": "Uploading {{name}}", + "Uploading file": "Uploading file", "Table": "Table", "Insert a table.": "Insert a table.", "Insert collapsible block.": "Insert collapsible block.", diff --git a/apps/client/src/features/editor/components/attachment/attachment-view.tsx b/apps/client/src/features/editor/components/attachment/attachment-view.tsx index d3858520..e3281e64 100644 --- a/apps/client/src/features/editor/components/attachment/attachment-view.tsx +++ b/apps/client/src/features/editor/components/attachment/attachment-view.tsx @@ -1,11 +1,13 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react"; -import { Group, Text, Paper, ActionIcon } from "@mantine/core"; +import { Group, Text, Paper, ActionIcon, Loader } from "@mantine/core"; import { getFileUrl } from "@/lib/config.ts"; import { IconDownload, IconPaperclip } from "@tabler/icons-react"; import { useHover } from "@mantine/hooks"; import { formatBytes } from "@/lib"; +import { useTranslation } from "react-i18next"; export default function AttachmentView(props: NodeViewProps) { + const { t } = useTranslation(); const { node, selected } = props; const { url, name, size } = node.attrs; const { hovered, ref } = useHover(); @@ -20,26 +22,28 @@ export default function AttachmentView(props: NodeViewProps) { wrap="nowrap" h={25} > - - + + {url ? ( + + ) : ( + + )} - - {name} + + {url ? name : t("Uploading {{name}}", { name })} - + {formatBytes(size)} - {selected || hovered ? ( + {url && (selected || hovered) && ( - ) : ( - "" )} diff --git a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx index e8085ca6..a6d143ff 100644 --- a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx @@ -1,10 +1,6 @@ -import { - BubbleMenu, - BubbleMenuProps, - isNodeSelection, - useEditor, - useEditorState, -} from "@tiptap/react"; +import { BubbleMenu, BubbleMenuProps } from "@tiptap/react/menus"; +import { isNodeSelection, useEditorState } from "@tiptap/react"; +import type { Editor } from "@tiptap/react"; import { FC, useEffect, useRef, useState } from "react"; import { IconBold, @@ -38,7 +34,7 @@ export interface BubbleMenuItem { } type EditorBubbleMenuProps = Omit & { - editor: ReturnType; + editor: Editor | null; }; export const EditorBubbleMenu: FC = (props) => { @@ -133,14 +129,9 @@ export const EditorBubbleMenu: FC = (props) => { } return isTextSelected(editor); }, - tippyOptions: { - moveTransition: "transform 0.15s ease-out", - onCreate: (instance) => { - instance.popper.firstChild?.addEventListener("blur", (event) => { - event.preventDefault(); - event.stopImmediatePropagation(); - }); - }, + options: { + placement: "top", + offset: 8, onHide: () => { setIsNodeSelectorOpen(false); setIsTextAlignmentOpen(false); @@ -156,7 +147,7 @@ export const EditorBubbleMenu: FC = (props) => { const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false); return ( - +
{ + const getReferencedVirtualElement = useCallback(() => { + if (!editor) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "callout"; const parent = findParentNode(predicate)(selection); if (parent) { const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement; - return dom.getBoundingClientRect(); + const domRect = dom.getBoundingClientRect(); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; } - return posToDOMRect(editor.view, selection.from, selection.to); + const domRect = posToDOMRect(editor.view, selection.from, selection.to); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; }, [editor]); const setCalloutType = useCallback( @@ -112,14 +117,12 @@ export function CalloutMenu({ editor }: EditorMenuProps) { editor={editor} pluginKey={`callout-menu`} updateDelay={0} - tippyOptions={{ - getReferenceClientRect, - offset: [0, 10], + getReferencedVirtualElement={getReferencedVirtualElement} + options={{ placement: "bottom", - zIndex: 99, - popperOptions: { - modifiers: [{ name: "flip", enabled: false }], - }, + // offset: 233, // // offset: [0, 10], + // zIndex: 99, + flip: false, }} shouldShow={shouldShow} > diff --git a/apps/client/src/features/editor/components/code-block/code-block-view.tsx b/apps/client/src/features/editor/components/code-block/code-block-view.tsx index 07ad2ad0..130016a3 100644 --- a/apps/client/src/features/editor/components/code-block/code-block-view.tsx +++ b/apps/client/src/features/editor/components/code-block/code-block-view.tsx @@ -90,6 +90,7 @@ export default function CodeBlockView(props: NodeViewProps) { node.textContent.length > 0 } > + {/* @ts-ignore */} diff --git a/apps/client/src/features/editor/components/common/editor-paste-handler.tsx b/apps/client/src/features/editor/components/common/editor-paste-handler.tsx index 8eee02fc..61d7534e 100644 --- a/apps/client/src/features/editor/components/common/editor-paste-handler.tsx +++ b/apps/client/src/features/editor/components/common/editor-paste-handler.tsx @@ -1,13 +1,12 @@ -import type { EditorView } from "@tiptap/pm/view"; import { uploadImageAction } from "@/features/editor/components/image/upload-image-action.tsx"; import { uploadVideoAction } from "@/features/editor/components/video/upload-video-action.tsx"; import { uploadAttachmentAction } from "../attachment/upload-attachment-action"; import { createMentionAction } from "@/features/editor/components/link/internal-link-paste.ts"; -import { Slice } from "@tiptap/pm/model"; import { INTERNAL_LINK_REGEX } from "@/lib/constants.ts"; +import { Editor } from "@tiptap/core"; export const handlePaste = ( - view: EditorView, + editor: Editor, event: ClipboardEvent, pageId: string, creatorId?: string, @@ -18,7 +17,7 @@ export const handlePaste = ( // we have to do this validation here to allow the default link extension to takeover if needs be event.preventDefault(); const url = clipboardData.trim(); - const { from: pos, empty } = view.state.selection; + const { from: pos, empty } = editor.state.selection; const match = INTERNAL_LINK_REGEX.exec(url); const currentPageMatch = INTERNAL_LINK_REGEX.exec(window.location.href); @@ -34,19 +33,27 @@ export const handlePaste = ( return false; } - const anchorId = match[6] ? match[6].split('#')[0] : undefined; - const urlWithoutAnchor = anchorId ? url.substring(0, url.indexOf("#")) : url; - createMentionAction(urlWithoutAnchor, view, pos, creatorId, anchorId); + const anchorId = match[6] ? match[6].split("#")[0] : undefined; + const urlWithoutAnchor = anchorId + ? url.substring(0, url.indexOf("#")) + : url; + createMentionAction( + urlWithoutAnchor, + editor.view, + pos, + creatorId, + anchorId, + ); return true; } if (event.clipboardData?.files.length) { event.preventDefault(); for (const file of event.clipboardData.files) { - const pos = view.state.selection.from; - uploadImageAction(file, view, pos, pageId); - uploadVideoAction(file, view, pos, pageId); - uploadAttachmentAction(file, view, pos, pageId); + const pos = editor.state.selection.from; + uploadImageAction(file, editor, pos, pageId); + uploadVideoAction(file, editor, pos, pageId); + uploadAttachmentAction(file, editor, pos, pageId); } return true; } @@ -54,7 +61,7 @@ export const handlePaste = ( }; export const handleFileDrop = ( - view: EditorView, + editor: Editor, event: DragEvent, moved: boolean, pageId: string, @@ -63,14 +70,14 @@ export const handleFileDrop = ( event.preventDefault(); for (const file of event.dataTransfer.files) { - const coordinates = view.posAtCoords({ + const coordinates = editor.view.posAtCoords({ left: event.clientX, top: event.clientY, }); - uploadImageAction(file, view, coordinates?.pos ?? 0 - 1, pageId); - uploadVideoAction(file, view, coordinates?.pos ?? 0 - 1, pageId); - uploadAttachmentAction(file, view, coordinates?.pos ?? 0 - 1, pageId); + uploadImageAction(file, editor, coordinates?.pos ?? 0 - 1, pageId); + uploadVideoAction(file, editor, coordinates?.pos ?? 0 - 1, pageId); + uploadAttachmentAction(file, editor, coordinates?.pos ?? 0 - 1, pageId); } return true; } diff --git a/apps/client/src/features/editor/components/drawio/drawio-menu.tsx b/apps/client/src/features/editor/components/drawio/drawio-menu.tsx index 0efc2ec0..937b8e7d 100644 --- a/apps/client/src/features/editor/components/drawio/drawio-menu.tsx +++ b/apps/client/src/features/editor/components/drawio/drawio-menu.tsx @@ -1,11 +1,6 @@ -import { - BubbleMenu as BaseBubbleMenu, - findParentNode, - posToDOMRect, - useEditorState, -} from "@tiptap/react"; +import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; +import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { useCallback } from "react"; -import { sticky } from "tippy.js"; import { Node as PMNode } from "prosemirror-model"; import { EditorMenuProps, @@ -40,17 +35,26 @@ export function DrawioMenu({ editor }: EditorMenuProps) { }, }); - const getReferenceClientRect = useCallback(() => { + const getReferencedVirtualElement = useCallback(() => { + if (!editor) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "drawio"; const parent = findParentNode(predicate)(selection); if (parent) { const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement; - return dom.getBoundingClientRect(); + const domRect = dom.getBoundingClientRect(); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; } - return posToDOMRect(editor.view, selection.from, selection.to); + const domRect = posToDOMRect(editor.view, selection.from, selection.to); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; }, [editor]); const onWidthChange = useCallback( @@ -65,15 +69,11 @@ export function DrawioMenu({ editor }: EditorMenuProps) { editor={editor} pluginKey={`drawio-menu`} updateDelay={0} - tippyOptions={{ - getReferenceClientRect, - offset: [0, 8], - zIndex: 99, - popperOptions: { - modifiers: [{ name: "flip", enabled: false }], - }, - plugins: [sticky], - sticky: "popper", + getReferencedVirtualElement={getReferencedVirtualElement} + options={{ + placement: "top", + offset: 8, + flip: false, }} shouldShow={shouldShow} > diff --git a/apps/client/src/features/editor/components/drawio/drawio-view.tsx b/apps/client/src/features/editor/components/drawio/drawio-view.tsx index 5a0fbd86..b51e8936 100644 --- a/apps/client/src/features/editor/components/drawio/drawio-view.tsx +++ b/apps/client/src/features/editor/components/drawio/drawio-view.tsx @@ -66,6 +66,7 @@ export default function DrawioView(props: NodeViewProps) { const fileName = "diagram.drawio.svg"; const drawioSVGFile = await svgStringToFile(svgString, fileName); + //@ts-ignore const pageId = editor.storage?.pageId; let attachment: IAttachment = null; diff --git a/apps/client/src/features/editor/components/emoji-menu/render-emoji-items.ts b/apps/client/src/features/editor/components/emoji-menu/render-emoji-items.ts index 82fb24a9..0ae5e24a 100644 --- a/apps/client/src/features/editor/components/emoji-menu/render-emoji-items.ts +++ b/apps/client/src/features/editor/components/emoji-menu/render-emoji-items.ts @@ -1,16 +1,41 @@ import { ReactRenderer, useEditor } from "@tiptap/react"; import EmojiList from "./emoji-list"; -import tippy from "tippy.js"; import { init } from "emoji-mart"; +import { + autoUpdate, + computePosition, + flip, + offset, + shift, +} from "@floating-ui/dom"; const renderEmojiItems = () => { let component: ReactRenderer | null = null; - let popup: any | null = null; + let popup: HTMLDivElement | null = null; + let cleanup: (() => void) | null = null; + let getReferenceClientRect: (() => DOMRect) | null = null; + + const destroy = () => { + if (cleanup) { + cleanup(); + cleanup = null; + } + + if (popup) { + popup.remove(); + popup = null; + } + + if (component) { + component.destroy(); + component = null; + } + }; return { onBeforeStart: (props: { editor: ReturnType; - clientRect: DOMRect; + clientRect: () => DOMRect; }) => { init({ data: async () => (await import("@emoji-mart/data")).default, @@ -25,51 +50,61 @@ const renderEmojiItems = () => { return; } - // @ts-ignore - popup = tippy("body", { - getReferenceClientRect: props.clientRect, - appendTo: () => document.body, - content: component.element, - showOnCreate: true, - interactive: true, - trigger: "manual", - placement: "bottom", + getReferenceClientRect = props.clientRect; + popup = document.createElement("div"); + popup.style.zIndex = "9999"; + popup.style.position = "absolute"; + popup.style.top = "0"; + popup.style.left = "0"; + popup.appendChild(component.element); + document.body.appendChild(popup); + + const virtualElement = { + getBoundingClientRect: () => { + return getReferenceClientRect + ? getReferenceClientRect() + : new DOMRect(0, 0, 0, 0); + }, + }; + + cleanup = autoUpdate(virtualElement, popup, () => { + if (!popup) return; + + computePosition(virtualElement, popup, { + placement: "bottom-start", + middleware: [offset(10), flip(), shift()], + }).then(({ x, y }) => { + if (!popup) return; + + Object.assign(popup.style, { + transform: `translate(${x}px, ${y}px)`, + }); + }); }); }, onStart: (props: { editor: ReturnType; - clientRect: DOMRect; + clientRect: () => DOMRect; }) => { - component?.updateProps({...props, isLoading: false}); + component?.updateProps({ ...props, isLoading: false }); - if (!props.clientRect) { - return; + if (props.clientRect) { + getReferenceClientRect = props.clientRect; } - - popup && - popup[0].setProps({ - getReferenceClientRect: props.clientRect, - }); }, onUpdate: (props: { editor: ReturnType; - clientRect: DOMRect; + clientRect: () => DOMRect; }) => { component?.updateProps(props); - if (!props.clientRect) { - return; + if (props.clientRect) { + getReferenceClientRect = props.clientRect; } - - popup && - popup[0].setProps({ - getReferenceClientRect: props.clientRect, - }); }, onKeyDown: (props: { event: KeyboardEvent }) => { if (props.event.key === "Escape") { - popup?.[0].hide(); - component?.destroy() + destroy(); return true; } @@ -78,13 +113,7 @@ const renderEmojiItems = () => { return component?.ref?.onKeyDown(props); }, onExit: () => { - if (popup && !popup[0]?.state.isDestroyed) { - popup[0]?.destroy(); - } - - if (component) { - component?.destroy(); - } + destroy(); }, }; }; diff --git a/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx b/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx index 42329e5c..06e79515 100644 --- a/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx +++ b/apps/client/src/features/editor/components/excalidraw/excalidraw-menu.tsx @@ -1,11 +1,6 @@ -import { - BubbleMenu as BaseBubbleMenu, - findParentNode, - posToDOMRect, - useEditorState, -} from "@tiptap/react"; +import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; +import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { useCallback } from "react"; -import { sticky } from "tippy.js"; import { Node as PMNode } from "prosemirror-model"; import { EditorMenuProps, @@ -42,17 +37,26 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) { }, }); - const getReferenceClientRect = useCallback(() => { + const getReferencedVirtualElement = useCallback(() => { + if (!editor) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "excalidraw"; const parent = findParentNode(predicate)(selection); if (parent) { const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement; - return dom.getBoundingClientRect(); + const domRect = dom.getBoundingClientRect(); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; } - return posToDOMRect(editor.view, selection.from, selection.to); + const domRect = posToDOMRect(editor.view, selection.from, selection.to); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; }, [editor]); const onWidthChange = useCallback( @@ -65,17 +69,13 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) { return ( diff --git a/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx b/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx index 779a826d..86c9665e 100644 --- a/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx +++ b/apps/client/src/features/editor/components/excalidraw/excalidraw-view.tsx @@ -98,6 +98,7 @@ export default function ExcalidrawView(props: NodeViewProps) { const fileName = "diagram.excalidraw.svg"; const excalidrawSvgFile = await svgStringToFile(svgString, fileName); + // @ts-ignore const pageId = editor.storage?.pageId; let attachment: IAttachment = null; diff --git a/apps/client/src/features/editor/components/image/image-menu.tsx b/apps/client/src/features/editor/components/image/image-menu.tsx index 723ec299..a1699f93 100644 --- a/apps/client/src/features/editor/components/image/image-menu.tsx +++ b/apps/client/src/features/editor/components/image/image-menu.tsx @@ -1,11 +1,6 @@ -import { - BubbleMenu as BaseBubbleMenu, - findParentNode, - posToDOMRect, - useEditorState, -} from "@tiptap/react"; +import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; +import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import React, { useCallback } from "react"; -import { sticky } from "tippy.js"; import { Node as PMNode } from "prosemirror-model"; import { EditorMenuProps, @@ -22,16 +17,6 @@ import { useTranslation } from "react-i18next"; export function ImageMenu({ editor }: EditorMenuProps) { const { t } = useTranslation(); - const shouldShow = useCallback( - ({ state }: ShouldShowProps) => { - if (!state) { - return false; - } - - return editor.isActive("image"); - }, - [editor], - ); const editorState = useEditorState({ editor, @@ -52,17 +37,37 @@ export function ImageMenu({ editor }: EditorMenuProps) { }, }); - const getReferenceClientRect = useCallback(() => { + const shouldShow = useCallback( + ({ state }: ShouldShowProps) => { + if (!state) { + return false; + } + + return editor.isActive("image") && editor.getAttributes("image").src; + }, + [editor], + ); + + const getReferencedVirtualElement = useCallback(() => { + if (!editor) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "image"; const parent = findParentNode(predicate)(selection); if (parent) { const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement; - return dom.getBoundingClientRect(); + const domRect = dom.getBoundingClientRect(); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; } - return posToDOMRect(editor.view, selection.from, selection.to); + const domRect = posToDOMRect(editor.view, selection.from, selection.to); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; }, [editor]); const alignImageLeft = useCallback(() => { @@ -105,15 +110,11 @@ export function ImageMenu({ editor }: EditorMenuProps) { editor={editor} pluginKey={`image-menu`} updateDelay={0} - tippyOptions={{ - getReferenceClientRect, - offset: [0, 8], - zIndex: 99, - popperOptions: { - modifiers: [{ name: "flip", enabled: false }], - }, - plugins: [sticky], - sticky: "popper", + getReferencedVirtualElement={getReferencedVirtualElement} + options={{ + placement: "top", + offset: 8, + flip: false, }} shouldShow={shouldShow} > diff --git a/apps/client/src/features/editor/components/image/image-view.module.css b/apps/client/src/features/editor/components/image/image-view.module.css new file mode 100644 index 00000000..5d02184b --- /dev/null +++ b/apps/client/src/features/editor/components/image/image-view.module.css @@ -0,0 +1,27 @@ +.imageWrapper { + display: flex; + justify-content: center; + align-items: center; + border-radius: 8px; + overflow: hidden; + animation: pulse 1.2s ease-in-out infinite; + + @mixin light { + background: linear-gradient(-90deg, var(--mantine-color-gray-3) 0%, var(--mantine-color-gray-1) 50%, var(--mantine-color-gray-3) 100%); + background-size: 400% 400%; + } + + @mixin dark { + background: linear-gradient(-90deg, var(--mantine-color-dark-6) 0%, var(--mantine-color-dark-5) 50%, var(--mantine-color-dark-6) 100%); + background-size: 400% 400%; + } + + @keyframes pulse { + 0% { + background-position: 0% 0%; + } + 100% { + background-position: -135% 0%; + } + } +} diff --git a/apps/client/src/features/editor/components/image/image-view.tsx b/apps/client/src/features/editor/components/image/image-view.tsx index dbdb8396..defb64c4 100644 --- a/apps/client/src/features/editor/components/image/image-view.tsx +++ b/apps/client/src/features/editor/components/image/image-view.tsx @@ -1,30 +1,70 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react"; +import { Group, Image, Loader, Text } from "@mantine/core"; import { useMemo } from "react"; -import { Image } from "@mantine/core"; import { getFileUrl } from "@/lib/config.ts"; import clsx from "clsx"; +import classes from "./image-view.module.css"; +import { useTranslation } from "react-i18next"; export default function ImageView(props: NodeViewProps) { - const { node, selected } = props; - const { src, width, align, title } = node.attrs; - + const { t } = useTranslation(); + const { editor, node, selected } = props; + const { src, width, align, title, aspectRatio, placeholder } = node.attrs; const alignClass = useMemo(() => { if (align === "left") return "alignLeft"; if (align === "right") return "alignRight"; if (align === "center") return "alignCenter"; return "alignCenter"; }, [align]); + const previewSrc = useMemo(() => { + editor.storage.shared.imagePreviews = + editor.storage.shared.imagePreviews || {}; + + if (placeholder?.id) { + return editor.storage.shared.imagePreviews[placeholder.id]; + } + + return null; + }, [placeholder, editor]); return ( - {title} +
+ {src && ( + {title} + )} + {!src && previewSrc && ( + + {placeholder?.name} + + + )} + {!src && !previewSrc && ( + + + + {placeholder?.name + ? t("Uploading {{name}}", { name: placeholder.name }) + : t("Uploading file")} + + + )} +
); } diff --git a/apps/client/src/features/editor/components/link/link-menu.tsx b/apps/client/src/features/editor/components/link/link-menu.tsx index 69f7c449..63fd10bf 100644 --- a/apps/client/src/features/editor/components/link/link-menu.tsx +++ b/apps/client/src/features/editor/components/link/link-menu.tsx @@ -1,9 +1,10 @@ -import { BubbleMenu as BaseBubbleMenu, useEditorState } from "@tiptap/react"; +import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import React, { useCallback, useState } from "react"; import { EditorMenuProps } from "@/features/editor/components/table/types/types.ts"; import { LinkEditorPanel } from "@/features/editor/components/link/link-editor-panel.tsx"; import { LinkPreviewPanel } from "@/features/editor/components/link/link-preview.tsx"; import { Card } from "@mantine/core"; +import { useEditorState } from "@tiptap/react"; export function LinkMenu({ editor, appendTo }: EditorMenuProps) { const [showEdit, setShowEdit] = useState(false); @@ -59,18 +60,15 @@ export function LinkMenu({ editor, appendTo }: EditorMenuProps) { return ( { - return appendTo?.current; - }, - onHidden: () => { + options={{ + onHide: () => { setShowEdit(false); }, placement: "bottom", - offset: [0, 5], - zIndex: 101, + offset: 5, + // zIndex: 101, }} shouldShow={shouldShow} > diff --git a/apps/client/src/features/editor/components/mention/mention-list.tsx b/apps/client/src/features/editor/components/mention/mention-list.tsx index 389c2ce5..32959146 100644 --- a/apps/client/src/features/editor/components/mention/mention-list.tsx +++ b/apps/client/src/features/editor/components/mention/mention-list.tsx @@ -106,6 +106,7 @@ const MentionList = forwardRef((props, ref) => { setRenderItems(items); // update editor storage + //@ts-ignore props.editor.storage.mentionItems = items; } }, [suggestion, isLoading]); @@ -163,7 +164,7 @@ const MentionList = forwardRef((props, ref) => { const enterHandler = () => { if (!renderItems.length) return; - if (renderItems[selectedIndex].entityType !== "header") { + if (renderItems[selectedIndex]?.entityType !== "header") { selectItem(selectedIndex); } }; @@ -203,7 +204,7 @@ const MentionList = forwardRef((props, ref) => { parentPageId: page.id || null, title: title }; - + let createdPage: IPage; try { createdPage = await createPageMutation.mutateAsync(payload); diff --git a/apps/client/src/features/editor/components/mention/mention-suggestion.ts b/apps/client/src/features/editor/components/mention/mention-suggestion.ts index 11710639..d53c422c 100644 --- a/apps/client/src/features/editor/components/mention/mention-suggestion.ts +++ b/apps/client/src/features/editor/components/mention/mention-suggestion.ts @@ -1,5 +1,11 @@ import { ReactRenderer, useEditor } from "@tiptap/react"; -import tippy from "tippy.js"; +import { + autoUpdate, + computePosition, + flip, + offset, + shift, +} from "@floating-ui/dom"; import MentionList from "@/features/editor/components/mention/mention-list.tsx"; function getWhitespaceCount(query: string) { @@ -9,16 +15,27 @@ function getWhitespaceCount(query: string) { const mentionRenderItems = () => { let component: ReactRenderer | null = null; - let popup: any | null = null; + let activeClientRect: (() => DOMRect) | null = null; + let updatePositionCleanup: (() => void) | null = null; + + const destroy = () => { + updatePositionCleanup?.(); + updatePositionCleanup = null; + component?.destroy(); + if (component?.element?.parentNode) { + component.element.parentNode.removeChild(component.element); + } + component = null; + }; return { onStart: (props: { editor: ReturnType; - clientRect: DOMRect; + clientRect: () => DOMRect; query: string; }) => { // query must not start with a whitespace - if (props.query.charAt(0) === ' '){ + if (props.query.charAt(0) === " ") { return; } @@ -37,75 +54,95 @@ const mentionRenderItems = () => { return; } - // @ts-ignore - popup = tippy("body", { - getReferenceClientRect: props.clientRect, - appendTo: () => document.body, - content: component.element, - showOnCreate: true, - interactive: true, - trigger: "manual", - placement: "bottom-start", - }); + activeClientRect = props.clientRect; + + const { element } = component; + document.body.appendChild(element); + + updatePositionCleanup = autoUpdate( + { + getBoundingClientRect: () => + activeClientRect ? activeClientRect() : new DOMRect(), + }, + element, + () => { + if (!component?.element) return; + computePosition( + { + getBoundingClientRect: () => { + return activeClientRect ? activeClientRect() : new DOMRect(); + }, + }, + element, + { + placement: "bottom-start", + middleware: [offset(0), flip(), shift()], + }, + ).then(({ x, y }) => { + Object.assign(element.style, { + left: `${x}px`, + top: `${y}px`, + position: "absolute", + zIndex: "9999", + }); + }); + }, + ); }, onUpdate: (props: { editor: ReturnType; - clientRect: DOMRect; + clientRect: () => DOMRect; query: string; }) => { // query must not start with a whitespace - if (props.query.charAt(0) === ' '){ - component?.destroy(); + if (props.query.charAt(0) === " ") { + destroy(); return; } // only update component if popup is not destroyed - if (!popup?.[0].state.isDestroyed) { - component?.updateProps(props); + if (component) { + component.updateProps(props); } if (!props || !props.clientRect) { return; } + activeClientRect = props.clientRect; + const whitespaceCount = getWhitespaceCount(props.query); // destroy component if space is greater 3 without a match if ( - whitespaceCount > 3 && - props.editor.storage.mentionItems.length === 0 + whitespaceCount > 4 && + //@ts-ignore + props.editor.storage.mentionItems.length === 1 ) { - popup?.[0]?.destroy(); - component?.destroy(); + destroy(); + return; + } + // fallback exit + if (whitespaceCount > 7) { + destroy(); return; } - - popup && - !popup?.[0].state.isDestroyed && - popup?.[0].setProps({ - getReferenceClientRect: props.clientRect, - }); }, onKeyDown: (props: { event: KeyboardEvent }) => { - if (props.event.key) - if ( - props.event.key === "Escape" || - (props.event.key === "Enter" && !popup?.[0].state.isShown) - ) { - popup?.[0].destroy(); - component?.destroy(); - return false; - } + if (props.event.key === "Escape") { + destroy(); + return true; + } + + if (props.event.key === "Enter" && !component) { + destroy(); + return false; + } + return (component?.ref as any)?.onKeyDown(props); }, onExit: () => { - if (popup && !popup?.[0].state.isDestroyed) { - popup[0].destroy(); - } - - if (component) { - component.destroy(); - } + destroy(); }, }; }; diff --git a/apps/client/src/features/editor/components/search-and-replace/search-and-replace-dialog.tsx b/apps/client/src/features/editor/components/search-and-replace/search-and-replace-dialog.tsx index df6f0031..f5c17661 100644 --- a/apps/client/src/features/editor/components/search-and-replace/search-and-replace-dialog.tsx +++ b/apps/client/src/features/editor/components/search-and-replace/search-and-replace-dialog.tsx @@ -73,6 +73,8 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo if (!editor) return; const { results, resultIndex } = editor.storage.searchAndReplace; + //TODO: check type error + //@ts-ignore const position: Range = results[resultIndex]; if (!position) return; diff --git a/apps/client/src/features/editor/components/slash-menu/menu-items.ts b/apps/client/src/features/editor/components/slash-menu/menu-items.ts index f56d7f04..bebefed4 100644 --- a/apps/client/src/features/editor/components/slash-menu/menu-items.ts +++ b/apps/client/src/features/editor/components/slash-menu/menu-items.ts @@ -161,6 +161,7 @@ const CommandGroups: SlashMenuGroupedItemsType = { command: ({ editor, range }) => { editor.chain().focus().deleteRange(range).run(); + // @ts-ignore const pageId = editor.storage?.pageId; if (!pageId) return; @@ -173,9 +174,13 @@ const CommandGroups: SlashMenuGroupedItemsType = { if (input.files?.length) { for (const file of input.files) { const pos = editor.view.state.selection.from; - uploadImageAction(file, editor.view, pos, pageId); + + uploadImageAction(file, editor, pos, pageId); } } + + // Reset the input value to allow uploading the same file again if needed + input.value = ""; }; input.click(); }, @@ -188,6 +193,7 @@ const CommandGroups: SlashMenuGroupedItemsType = { command: ({ editor, range }) => { editor.chain().focus().deleteRange(range).run(); + // @ts-ignore const pageId = editor.storage?.pageId; if (!pageId) return; @@ -195,12 +201,18 @@ const CommandGroups: SlashMenuGroupedItemsType = { const input = document.createElement("input"); input.type = "file"; input.accept = "video/*"; + input.multiple = true; input.onchange = async () => { if (input.files?.length) { - const file = input.files[0]; - const pos = editor.view.state.selection.from; - uploadVideoAction(file, editor.view, pos, pageId); + for (const file of input.files) { + const pos = editor.view.state.selection.from; + + uploadVideoAction(file, editor, pos, pageId); + } } + + // Reset the input value to allow uploading the same file again if needed + input.value = ""; }; input.click(); }, @@ -213,6 +225,7 @@ const CommandGroups: SlashMenuGroupedItemsType = { command: ({ editor, range }) => { editor.chain().focus().deleteRange(range).run(); + // @ts-ignore const pageId = editor.storage?.pageId; if (!pageId) return; @@ -220,12 +233,18 @@ const CommandGroups: SlashMenuGroupedItemsType = { const input = document.createElement("input"); input.type = "file"; input.accept = ""; + input.multiple = true; input.onchange = async () => { if (input.files?.length) { - const file = input.files[0]; - const pos = editor.view.state.selection.from; - uploadAttachmentAction(file, editor.view, pos, pageId, true); + for (const file of input.files) { + const pos = editor.view.state.selection.from; + + uploadAttachmentAction(file, editor, pos, pageId, true); + } } + + // Reset the input value to allow uploading the same file again if needed + input.value = ""; }; input.click(); }, diff --git a/apps/client/src/features/editor/components/slash-menu/render-items.ts b/apps/client/src/features/editor/components/slash-menu/render-items.ts index db6424e8..057e8214 100644 --- a/apps/client/src/features/editor/components/slash-menu/render-items.ts +++ b/apps/client/src/features/editor/components/slash-menu/render-items.ts @@ -1,10 +1,35 @@ import { ReactRenderer, useEditor } from "@tiptap/react"; import CommandList from "@/features/editor/components/slash-menu/command-list"; -import tippy from "tippy.js"; +import { + autoUpdate, + computePosition, + flip, + offset, + shift, +} from "@floating-ui/dom"; const renderItems = () => { let component: ReactRenderer | null = null; - let popup: any | null = null; + let popup: HTMLElement | null = null; + let cleanup: (() => void) | null = null; + let getReferenceClientRect: (() => DOMRect) | null = null; + + const updatePosition = () => { + if (!popup || !getReferenceClientRect) return; + + // @ts-ignore + const rect = getReferenceClientRect(); + + computePosition({ getBoundingClientRect: () => rect }, popup, { + placement: "bottom-start", + middleware: [offset(0), flip(), shift()], + }).then(({ x, y }) => { + if (popup) { + popup.style.left = `${x}px`; + popup.style.top = `${y}px`; + } + }); + }; return { onStart: (props: { @@ -21,15 +46,29 @@ const renderItems = () => { } // @ts-ignore - popup = tippy("body", { - getReferenceClientRect: props.clientRect, - appendTo: () => document.body, - content: component.element, - showOnCreate: true, - interactive: true, - trigger: "manual", - placement: "bottom-start", - }); + getReferenceClientRect = props.clientRect; + + popup = document.createElement("div"); + popup.style.zIndex = "9999"; + popup.style.position = "absolute"; + popup.style.top = "0"; + popup.style.left = "0"; + + document.body.appendChild(popup); + popup.appendChild(component.element); + + cleanup = autoUpdate( + // @ts-ignore + { + getBoundingClientRect: () => { + return getReferenceClientRect + ? getReferenceClientRect() + : new DOMRect(); + }, + }, + popup, + updatePosition + ); }, onUpdate: (props: { editor: ReturnType; @@ -41,14 +80,15 @@ const renderItems = () => { return; } - popup && - popup[0].setProps({ - getReferenceClientRect: props.clientRect, - }); + // @ts-ignore + getReferenceClientRect = props.clientRect; + updatePosition(); }, onKeyDown: (props: { event: KeyboardEvent }) => { if (props.event.key === "Escape") { - popup?.[0].hide(); + if (popup) { + popup.style.display = "none"; + } return true; } @@ -57,12 +97,19 @@ const renderItems = () => { return component?.ref?.onKeyDown(props); }, onExit: () => { - if (popup && !popup[0].state.isDestroyed) { - popup[0].destroy(); + if (cleanup) { + cleanup(); + cleanup = null; + } + + if (popup) { + popup.remove(); + popup = null; } if (component) { component.destroy(); + component = null; } }, }; diff --git a/apps/client/src/features/editor/components/subpages/subpages-menu.tsx b/apps/client/src/features/editor/components/subpages/subpages-menu.tsx index 6cc017e2..9f0544e6 100644 --- a/apps/client/src/features/editor/components/subpages/subpages-menu.tsx +++ b/apps/client/src/features/editor/components/subpages/subpages-menu.tsx @@ -1,15 +1,11 @@ -import { - BubbleMenu as BaseBubbleMenu, - posToDOMRect, - findParentNode, -} from "@tiptap/react"; +import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; +import { posToDOMRect, findParentNode } from "@tiptap/react"; import { Node as PMNode } from "@tiptap/pm/model"; import React, { useCallback } from "react"; import { ActionIcon, Tooltip } from "@mantine/core"; import { IconTrash } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; import { Editor } from "@tiptap/core"; -import { sticky } from "tippy.js"; interface SubpagesMenuProps { editor: Editor; @@ -33,7 +29,7 @@ export const SubpagesMenu = React.memo( return editor.isActive("subpages"); }, - [editor], + [editor] ); const getReferenceClientRect = useCallback(() => { @@ -62,18 +58,8 @@ export const SubpagesMenu = React.memo( return ( @@ -89,7 +75,7 @@ export const SubpagesMenu = React.memo( ); - }, + } ); export default SubpagesMenu; diff --git a/apps/client/src/features/editor/components/subpages/subpages-view.tsx b/apps/client/src/features/editor/components/subpages/subpages-view.tsx index 0da33028..697c1213 100644 --- a/apps/client/src/features/editor/components/subpages/subpages-view.tsx +++ b/apps/client/src/features/editor/components/subpages/subpages-view.tsx @@ -19,6 +19,7 @@ export default function SubpagesView(props: NodeViewProps) { const { spaceSlug, shareId } = useParams(); const { t } = useTranslation(); + //@ts-ignore const currentPageId = editor.storage.pageId; // Get subpages from shared tree if we're in a shared context diff --git a/apps/client/src/features/editor/components/table/table-cell-menu.tsx b/apps/client/src/features/editor/components/table/table-cell-menu.tsx index 2ea2e8dd..8af896b3 100644 --- a/apps/client/src/features/editor/components/table/table-cell-menu.tsx +++ b/apps/client/src/features/editor/components/table/table-cell-menu.tsx @@ -1,6 +1,4 @@ -import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react"; import React, { useCallback } from "react"; - import { EditorMenuProps, ShouldShowProps, @@ -17,6 +15,7 @@ import { import { useTranslation } from "react-i18next"; import { TableBackgroundColor } from "./table-background-color"; import { TableTextAlignment } from "./table-text-alignment"; +import { BubbleMenu } from "@tiptap/react/menus"; export const TableCellMenu = React.memo( ({ editor, appendTo }: EditorMenuProps): JSX.Element => { @@ -29,7 +28,7 @@ export const TableCellMenu = React.memo( return isCellSelection(state.selection); }, - [editor], + [editor] ); const mergeCells = useCallback(() => { @@ -53,23 +52,27 @@ export const TableCellMenu = React.memo( }, [editor]); return ( - { - return appendTo?.current; + appendTo={() => { + return appendTo?.current; + }} + ref={(element) => { + element.style.zIndex = "99"; + }} + options={{ + offset: { + mainAxis: 15, }, - offset: [0, 15], - zIndex: 99, }} shouldShow={shouldShow} > - + - + ); - }, + } ); export default TableCellMenu; diff --git a/apps/client/src/features/editor/components/table/table-menu.tsx b/apps/client/src/features/editor/components/table/table-menu.tsx index 1d2985e8..e54a06af 100644 --- a/apps/client/src/features/editor/components/table/table-menu.tsx +++ b/apps/client/src/features/editor/components/table/table-menu.tsx @@ -1,11 +1,6 @@ -import { - BubbleMenu as BaseBubbleMenu, - posToDOMRect, - findParentNode, -} from "@tiptap/react"; +import { posToDOMRect, findParentNode } from "@tiptap/react"; import { Node as PMNode } from "@tiptap/pm/model"; import React, { useCallback } from "react"; - import { EditorMenuProps, ShouldShowProps, @@ -17,9 +12,12 @@ import { IconColumnRemove, IconRowInsertBottom, IconRowInsertTop, - IconRowRemove, IconTableColumn, IconTableRow, + IconRowRemove, + IconTableColumn, + IconTableRow, IconTrashX, -} from '@tabler/icons-react'; +} from "@tabler/icons-react"; +import { BubbleMenu } from "@tiptap/react/menus"; import { isCellSelection } from "@docmost/editor-ext"; import { useTranslation } from "react-i18next"; @@ -34,20 +32,28 @@ export const TableMenu = React.memo( return editor.isActive("table") && !isCellSelection(state.selection); }, - [editor], + [editor] ); - const getReferenceClientRect = useCallback(() => { + const getReferencedVirtualElement = useCallback(() => { const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "table"; const parent = findParentNode(predicate)(selection); if (parent) { const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement; - return dom.getBoundingClientRect(); + const rect = dom.getBoundingClientRect(); + return { + getBoundingClientRect: () => rect, + getClientRects: () => [rect], + }; } - return posToDOMRect(editor.view, selection.from, selection.to); + const rect = posToDOMRect(editor.view, selection.from, selection.to); + return { + getBoundingClientRect: () => rect, + getClientRects: () => [rect], + }; }, [editor]); const toggleHeaderColumn = useCallback(() => { @@ -87,42 +93,33 @@ export const TableMenu = React.memo( }, [editor]); return ( - { + element.style.zIndex = "99"; + }} + options={{ + placement: "top", + offset: { + mainAxis: 15, + }, + flip: { + fallbackPlacements: ["top", "bottom"], + padding: { top: 35 + 15, left: 8, right: 8, bottom: -Infinity }, + boundary: editor.options.element as HTMLElement, + }, + shift: { + padding: 8 + 15, + crossAxis: true, }, }} shouldShow={shouldShow} > - + - + - + - + ); - }, + } ); export default TableMenu; diff --git a/apps/client/src/features/editor/components/video/video-menu.tsx b/apps/client/src/features/editor/components/video/video-menu.tsx index 3252e621..dfece398 100644 --- a/apps/client/src/features/editor/components/video/video-menu.tsx +++ b/apps/client/src/features/editor/components/video/video-menu.tsx @@ -1,11 +1,6 @@ -import { - BubbleMenu as BaseBubbleMenu, - findParentNode, - posToDOMRect, - useEditorState, -} from "@tiptap/react"; +import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; +import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import React, { useCallback } from "react"; -import { sticky } from "tippy.js"; import { Node as PMNode } from "prosemirror-model"; import { EditorMenuProps, @@ -22,16 +17,6 @@ import { useTranslation } from "react-i18next"; export function VideoMenu({ editor }: EditorMenuProps) { const { t } = useTranslation(); - const shouldShow = useCallback( - ({ state }: ShouldShowProps) => { - if (!state) { - return false; - } - - return editor.isActive("video"); - }, - [editor], - ); const editorState = useEditorState({ editor, @@ -52,17 +37,37 @@ export function VideoMenu({ editor }: EditorMenuProps) { }, }); - const getReferenceClientRect = useCallback(() => { + const shouldShow = useCallback( + ({ state }: ShouldShowProps) => { + if (!state) { + return false; + } + + return editor.isActive("video") && editor.getAttributes("video").src; + }, + [editor], + ); + + const getReferencedVirtualElement = useCallback(() => { + if (!editor) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "video"; const parent = findParentNode(predicate)(selection); if (parent) { const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement; - return dom.getBoundingClientRect(); + const domRect = dom.getBoundingClientRect(); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; } - return posToDOMRect(editor.view, selection.from, selection.to); + const domRect = posToDOMRect(editor.view, selection.from, selection.to); + return { + getBoundingClientRect: () => domRect, + getClientRects: () => [domRect], + }; }, [editor]); const alignVideoLeft = useCallback(() => { @@ -105,15 +110,11 @@ export function VideoMenu({ editor }: EditorMenuProps) { editor={editor} pluginKey={`video-menu`} updateDelay={0} - tippyOptions={{ - getReferenceClientRect, - offset: [0, 8], - zIndex: 99, - popperOptions: { - modifiers: [{ name: "flip", enabled: false }], - }, - plugins: [sticky], - sticky: "popper", + getReferencedVirtualElement={getReferencedVirtualElement} + options={{ + placement: "top", + offset: 8, + flip: false, }} shouldShow={shouldShow} > diff --git a/apps/client/src/features/editor/components/video/video-view.module.css b/apps/client/src/features/editor/components/video/video-view.module.css new file mode 100644 index 00000000..c0e7f99d --- /dev/null +++ b/apps/client/src/features/editor/components/video/video-view.module.css @@ -0,0 +1,33 @@ +.videoWrapper { + display: flex; + justify-content: center; + align-items: center; + border-radius: 8px; + overflow: hidden; + animation: pulse 1.2s ease-in-out infinite; + + @mixin light { + background: linear-gradient(-90deg, var(--mantine-color-gray-3) 0%, var(--mantine-color-gray-1) 50%, var(--mantine-color-gray-3) 100%); + background-size: 400% 400%; + } + + @mixin dark { + background: linear-gradient(-90deg, var(--mantine-color-dark-6) 0%, var(--mantine-color-dark-5) 50%, var(--mantine-color-dark-6) 100%); + background-size: 400% 400%; + } + + @keyframes pulse { + 0% { + background-position: 0% 0%; + } + 100% { + background-position: -135% 0%; + } + } +} +.video { + display: block; + width: 100%; + height: 100%; + border-radius: 8px; +} diff --git a/apps/client/src/features/editor/components/video/video-view.tsx b/apps/client/src/features/editor/components/video/video-view.tsx index d47d9a4a..e2473afc 100644 --- a/apps/client/src/features/editor/components/video/video-view.tsx +++ b/apps/client/src/features/editor/components/video/video-view.tsx @@ -1,29 +1,75 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react"; +import { Group, Loader, Text } from "@mantine/core"; import { useMemo } from "react"; import { getFileUrl } from "@/lib/config.ts"; import clsx from "clsx"; +import classes from "./video-view.module.css"; +import { useTranslation } from "react-i18next"; export default function VideoView(props: NodeViewProps) { - const { node, selected } = props; - const { src, width, align } = node.attrs; - + const { t } = useTranslation(); + const { editor, node, selected } = props; + const { src, width, align, aspectRatio, placeholder } = node.attrs; const alignClass = useMemo(() => { if (align === "left") return "alignLeft"; if (align === "right") return "alignRight"; if (align === "center") return "alignCenter"; return "alignCenter"; }, [align]); + const previewSrc = useMemo(() => { + editor.storage.shared.videoPreviews = + editor.storage.shared.videoPreviews || {}; + + if (placeholder?.id) { + return editor.storage.shared.videoPreviews[placeholder.id]; + } + + return null; + }, [placeholder, editor]); return ( - ); } diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts index ecdea2e7..ef03108b 100644 --- a/apps/client/src/features/editor/extensions/extensions.ts +++ b/apps/client/src/features/editor/extensions/extensions.ts @@ -1,11 +1,7 @@ import { StarterKit } from "@tiptap/starter-kit"; -import { Placeholder } from "@tiptap/extension-placeholder"; import { TextAlign } from "@tiptap/extension-text-align"; -import { CharacterCount } from "@tiptap/extension-character-count"; -import { TaskList } from "@tiptap/extension-task-list"; -import { ListKeymap } from "@tiptap/extension-list-keymap"; -import { TaskItem } from "@tiptap/extension-task-item"; -import { Underline } from "@tiptap/extension-underline"; +import { TaskList, TaskItem } from "@tiptap/extension-list"; +import { Placeholder, CharacterCount } from "@tiptap/extensions"; import { Superscript } from "@tiptap/extension-superscript"; import SubScript from "@tiptap/extension-subscript"; import { Typography } from "@tiptap/extension-typography"; @@ -15,7 +11,7 @@ import GlobalDragHandle from "tiptap-extension-global-drag-handle"; import { Youtube } from "@tiptap/extension-youtube"; import SlashCommand from "@/features/editor/extensions/slash-command"; import { Collaboration, isChangeOrigin } from "@tiptap/extension-collaboration"; -import { CollaborationCursor } from "@tiptap/extension-collaboration-cursor"; +import { CollaborationCaret } from "@tiptap/extension-collaboration-caret"; import { HocuspocusProvider } from "@hocuspocus/provider"; import { Comment, @@ -41,11 +37,12 @@ import { Embed, SearchAndReplace, Mention, - Subpages, TableDndExtension, + Subpages, Heading, Highlight, UniqueID, + SharedStorage, } from "@docmost/editor-ext"; import { randomElement, @@ -97,7 +94,9 @@ lowlight.register("scala", scala); export const mainExtensions = [ StarterKit.configure({ heading: false, - history: false, + undoRedo: false, + link: false, + trailingNode: false, dropcursor: { width: 3, color: "#70CFF8", @@ -109,6 +108,7 @@ export const mainExtensions = [ }, }, }), + SharedStorage, Heading, UniqueID.configure({ types: ["heading", "paragraph"], @@ -134,8 +134,6 @@ export const mainExtensions = [ TaskItem.configure({ nested: true, }), - ListKeymap, - Underline, LinkExtension.configure({ openOnClick: false, }), @@ -170,6 +168,9 @@ export const mainExtensions = [ }, }).extend({ addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(MentionView); }, }), @@ -208,6 +209,7 @@ export const mainExtensions = [ }), CustomCodeBlock.configure({ view: CodeBlockView, + //@ts-ignore lowlight, HTMLAttributes: { spellcheck: false, @@ -246,7 +248,7 @@ export const mainExtensions = [ Escape: () => { const event = new CustomEvent("closeFindDialogFromEditor", {}); document.dispatchEvent(event); - return true; + return false; }, }; }, @@ -258,8 +260,9 @@ type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[]; export const collabExtensions: CollabExtensions = (provider, user) => [ Collaboration.configure({ document: provider.document, + provider, }), - CollaborationCursor.configure({ + CollaborationCaret.configure({ provider, user: { name: user.name, diff --git a/apps/client/src/features/editor/page-editor.tsx b/apps/client/src/features/editor/page-editor.tsx index b4478920..da8bd84a 100644 --- a/apps/client/src/features/editor/page-editor.tsx +++ b/apps/client/src/features/editor/page-editor.tsx @@ -1,13 +1,22 @@ import "@/features/editor/styles/index.css"; -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { IndexeddbPersistence } from "y-indexeddb"; import * as Y from "yjs"; import { HocuspocusProvider, - onAuthenticationFailedParameters, + onStatusParameters, WebSocketStatus, + HocuspocusProviderWebsocket, + onSyncedParameters, } from "@hocuspocus/provider"; import { + Editor, EditorContent, EditorProvider, useEditor, @@ -69,161 +78,140 @@ export default function PageEditor({ editable, content, }: PageEditorProps) { - - const collaborationURL = useCollaborationUrl(); const isComponentMounted = useRef(false); - const editorCreated = useRef(false); + const editorRef = useRef(null); useEffect(() => { isComponentMounted.current = true; }, []); - + const [currentUser] = useAtom(currentUserAtom); const [, setEditor] = useAtom(pageEditorAtom); const [, setAsideState] = useAtom(asideStateAtom); const [, setActiveCommentId] = useAtom(activeCommentIdAtom); const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom); - const ydocRef = useRef(null); - if (!ydocRef.current) { - ydocRef.current = new Y.Doc(); - } - const ydoc = ydocRef.current; - const [isLocalSynced, setLocalSynced] = useState(false); - const [isRemoteSynced, setRemoteSynced] = useState(false); + const [isLocalSynced, setIsLocalSynced] = useState(false); + const [isRemoteSynced, setIsRemoteSynced] = useState(false); const [yjsConnectionStatus, setYjsConnectionStatus] = useAtom( yjsConnectionStatusAtom, ); const menuContainerRef = useRef(null); - const documentName = `page.${pageId}`; const { data: collabQuery, refetch: refetchCollabToken } = useCollabToken(); const { isIdle, resetIdle } = useIdle(FIVE_MINUTES, { initialState: false }); const documentState = useDocumentVisibility(); - const [isCollabReady, setIsCollabReady] = useState(false); const { pageSlug } = useParams(); const slugId = extractPageSlugId(pageSlug); const userPageEditMode = currentUser?.user?.settings?.preferences?.pageEditMode ?? PageEditMode.Edit; - - const canScroll = useCallback(() => isComponentMounted.current && editorCreated.current, [isComponentMounted, editorCreated]); + const canScroll = useCallback( + () => Boolean(isComponentMounted.current && editorRef.current), + [isComponentMounted], + ); const { handleScrollTo } = useEditorScroll({ canScroll }); // Providers only created once per pageId const providersRef = useRef<{ local: IndexeddbPersistence; remote: HocuspocusProvider; + socket: HocuspocusProviderWebsocket; } | null>(null); const [providersReady, setProvidersReady] = useState(false); - const localProvider = providersRef.current?.local; - const remoteProvider = providersRef.current?.remote; - - // Track when collaborative provider is ready and synced - const [collabReady, setCollabReady] = useState(false); - useEffect(() => { - if ( - remoteProvider?.status === WebSocketStatus.Connected && - isLocalSynced && - isRemoteSynced - ) { - setCollabReady(true); - } - }, [remoteProvider?.status, isLocalSynced, isRemoteSynced]); - useEffect(() => { if (!providersRef.current) { + const documentName = `page.${pageId}`; + const ydoc = new Y.Doc(); const local = new IndexeddbPersistence(documentName, ydoc); - local.on("synced", () => setLocalSynced(true)); - const remote = new HocuspocusProvider({ - name: documentName, + const socket = new HocuspocusProviderWebsocket({ url: collaborationURL, + }); + const onLocalSyncedHandler = () => { + setIsLocalSynced(true); + }; + const onStatusHandler = (event: onStatusParameters) => { + setYjsConnectionStatus(event.status); + }; + const onSyncedHandler = (event: onSyncedParameters) => { + setIsRemoteSynced(event.state); + }; + const onAuthenticationFailedHandler = () => { + const payload = jwtDecode(collabQuery?.token); + const now = Date.now().valueOf() / 1000; + const isTokenExpired = now >= payload.exp; + if (isTokenExpired) { + refetchCollabToken().then((result) => { + if (result.data?.token) { + socket.disconnect(); + setTimeout(() => { + remote.configuration.token = result.data.token; + socket.connect(); + }, 100); + } + }); + } + }; + const remote = new HocuspocusProvider({ + websocketProvider: socket, + name: documentName, document: ydoc, token: collabQuery?.token, - connect: true, - preserveConnection: false, - onAuthenticationFailed: (auth: onAuthenticationFailedParameters) => { - const payload = jwtDecode(collabQuery?.token); - const now = Date.now().valueOf() / 1000; - const isTokenExpired = now >= payload.exp; - if (isTokenExpired) { - refetchCollabToken().then((result) => { - if (result.data?.token) { - remote.disconnect(); - setTimeout(() => { - remote.configuration.token = result.data.token; - remote.connect(); - }, 100); - } - }); - } - }, - onStatus: (status) => { - if (status.status === "connected") { - setYjsConnectionStatus(status.status); - } - }, + onAuthenticationFailed: onAuthenticationFailedHandler, + onStatus: onStatusHandler, + onSynced: onSyncedHandler, }); - remote.on("synced", () => setRemoteSynced(true)); - remote.on("disconnect", () => { - setYjsConnectionStatus(WebSocketStatus.Disconnected); - }); - providersRef.current = { local, remote }; + + local.on("synced", onLocalSyncedHandler); + providersRef.current = { socket, local, remote }; setProvidersReady(true); } else { setProvidersReady(true); } // Only destroy on final unmount return () => { + providersRef.current?.socket.destroy(); providersRef.current?.remote.destroy(); providersRef.current?.local.destroy(); providersRef.current = null; }; }, [pageId]); - /* - useEffect(() => { - // Handle token updates by reconnecting with new token - if (providersRef.current?.remote && collabQuery?.token) { - const currentToken = providersRef.current.remote.configuration.token; - if (currentToken !== collabQuery.token) { - // Token has changed, need to reconnect with new token - providersRef.current.remote.disconnect(); - providersRef.current.remote.configuration.token = collabQuery.token; - providersRef.current.remote.connect(); - } - } - }, [collabQuery?.token]); - */ - // Only connect/disconnect on tab/idle, not destroy useEffect(() => { if (!providersReady || !providersRef.current) return; - const remoteProvider = providersRef.current.remote; + const socket = providersRef.current.socket; + if ( isIdle && documentState === "hidden" && - remoteProvider.status === WebSocketStatus.Connected + yjsConnectionStatus === WebSocketStatus.Connected ) { - remoteProvider.disconnect(); - setIsCollabReady(false); + socket.disconnect(); return; } if ( documentState === "visible" && - remoteProvider.status === WebSocketStatus.Disconnected + yjsConnectionStatus === WebSocketStatus.Disconnected ) { resetIdle(); - remoteProvider.connect(); - setTimeout(() => setIsCollabReady(true), 500); + socket.connect(); } }, [isIdle, documentState, providersReady, resetIdle]); + // Attach here, to make sure the connection gets properly established + providersRef.current?.remote.attach(); + const extensions = useMemo(() => { - if (!remoteProvider || !currentUser?.user) return mainExtensions; + if (!providersReady || !providersRef.current || !currentUser?.user) { + return mainExtensions; + } + + const remoteProvider = providersRef.current.remote; + return [ ...mainExtensions, ...collabExtensions(remoteProvider, currentUser?.user), ]; - }, [remoteProvider, currentUser?.user]); + }, [providersReady, currentUser?.user]); const editor = useEditor( { @@ -266,18 +254,30 @@ export default function PageEditor({ } }, }, - handlePaste: (view, event, slice) => - handlePaste(view, event, pageId, currentUser?.user.id), - handleDrop: (view, event, _slice, moved) => - handleFileDrop(view, event, moved, pageId), + handlePaste: (_view, event) => { + if (!editorRef.current) return false; + + return handlePaste( + editorRef.current, + event, + pageId, + currentUser?.user.id, + ); + }, + handleDrop: (_view, event, _slice, moved) => { + if (!editorRef.current) return false; + + return handleFileDrop(editorRef.current, event, moved, pageId); + }, }, onCreate({ editor }) { if (editor) { // @ts-ignore setEditor(editor); + // @ts-ignore editor.storage.pageId = pageId; handleScrollTo(editor); - editorCreated.current = true; + editorRef.current = editor; } }, onUpdate({ editor }) { @@ -287,7 +287,7 @@ export default function PageEditor({ debouncedUpdateContent(editorJson); }, }, - [pageId, editable, remoteProvider], + [pageId, editable, extensions], ); const editorIsEditable = useEditorState({ @@ -343,30 +343,17 @@ export default function PageEditor({ setAsideState({ tab: "", isAsideOpen: false }); }, [pageId]); - useEffect(() => { - if (remoteProvider?.status === WebSocketStatus.Connecting) { - const timeout = setTimeout(() => { - setYjsConnectionStatus(WebSocketStatus.Disconnected); - }, 5000); - return () => clearTimeout(timeout); - } - }, [remoteProvider?.status]); - const isSynced = isLocalSynced && isRemoteSynced; useEffect(() => { - const collabReadyTimeout = setTimeout(() => { - if ( - !isCollabReady && - isSynced && - remoteProvider?.status === WebSocketStatus.Connected - ) { - setIsCollabReady(true); + const timeout = setTimeout(() => { + if (yjsConnectionStatus === WebSocketStatus.Connecting || !isSynced) { + setYjsConnectionStatus(WebSocketStatus.Disconnected); } - }, 500); - return () => clearTimeout(collabReadyTimeout); - }, [isRemoteSynced, isLocalSynced, remoteProvider?.status]); + }, 7500); + return () => clearTimeout(timeout); + }, [yjsConnectionStatus, isSynced]); useEffect(() => { // Only honor user default page edit mode preference and permissions if (editor) { @@ -388,12 +375,13 @@ export default function PageEditor({ useEffect(() => { if ( !hasConnectedOnceRef.current && - remoteProvider?.status === WebSocketStatus.Connected + yjsConnectionStatus === WebSocketStatus.Connected && + isSynced ) { hasConnectedOnceRef.current = true; setShowStatic(false); } - }, [remoteProvider?.status]); + }, [yjsConnectionStatus, isSynced]); if (showStatic) { return ( diff --git a/apps/client/src/features/editor/readonly-page-editor.tsx b/apps/client/src/features/editor/readonly-page-editor.tsx index c81e4d19..77496fcd 100644 --- a/apps/client/src/features/editor/readonly-page-editor.tsx +++ b/apps/client/src/features/editor/readonly-page-editor.tsx @@ -81,6 +81,7 @@ export default function ReadonlyPageEditor({ onCreate={({ editor }) => { if (editor) { if (pageId) { + // @ts-ignore editor.storage.pageId = pageId; } // @ts-ignore diff --git a/apps/client/src/features/editor/styles/collaboration.css b/apps/client/src/features/editor/styles/collaboration.css index 4a43ac25..a13d2180 100644 --- a/apps/client/src/features/editor/styles/collaboration.css +++ b/apps/client/src/features/editor/styles/collaboration.css @@ -1,5 +1,5 @@ /* Give a remote user a caret */ -.collaboration-cursor__caret { +.collaboration-carets__caret { border-left: 1px solid #0d0d0d; border-right: 1px solid #0d0d0d; margin-left: -1px; @@ -10,7 +10,7 @@ } /* Render the username above the caret */ -.collaboration-cursor__label { +.collaboration-carets__label { border-radius: 3px 3px 3px 0; color: #0d0d0d; font-size: 0.75rem; diff --git a/apps/client/src/features/page-history/components/history-editor.tsx b/apps/client/src/features/page-history/components/history-editor.tsx index 4b5839af..5fa8cf42 100644 --- a/apps/client/src/features/page-history/components/history-editor.tsx +++ b/apps/client/src/features/page-history/components/history-editor.tsx @@ -1,8 +1,9 @@ -import '@/features/editor/styles/index.css'; -import React, { useEffect } from 'react'; -import { EditorContent, useEditor } from '@tiptap/react'; -import { mainExtensions } from '@/features/editor/extensions/extensions'; -import { Title } from '@mantine/core'; +import "@/features/editor/styles/index.css"; +import React, { useEffect } from "react"; +import { EditorContent, useEditor } from "@tiptap/react"; +import { mainExtensions } from "@/features/editor/extensions/extensions"; +import { Title } from "@mantine/core"; +import classes from "./history.module.css"; export interface HistoryEditorProps { title: string; @@ -26,7 +27,9 @@ export function HistoryEditor({ title, content }: HistoryEditorProps) {
{title} - {editor && } + {editor && ( + + )}
); diff --git a/apps/client/src/features/page-history/components/history-list.tsx b/apps/client/src/features/page-history/components/history-list.tsx index af178eac..7b0d9ea2 100644 --- a/apps/client/src/features/page-history/components/history-list.tsx +++ b/apps/client/src/features/page-history/components/history-list.tsx @@ -67,7 +67,7 @@ function HistoryList({ pageId }: Props) { mainEditorTitle .chain() .clearContent() - .setContent(activeHistoryData.title, true) + .setContent(activeHistoryData.title, { emitUpdate: true }) .run(); mainEditor .chain() diff --git a/apps/client/src/features/page-history/components/history.module.css b/apps/client/src/features/page-history/components/history.module.css index 5d23cb0c..66415146 100644 --- a/apps/client/src/features/page-history/components/history.module.css +++ b/apps/client/src/features/page-history/components/history.module.css @@ -1,37 +1,49 @@ .history { - display: block; - width: 100%; - padding: var(--mantine-spacing-md); - color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0)); + display: block; + width: 100%; + padding: var(--mantine-spacing-md); + color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0)); - @mixin hover { - background-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-8)); - } + @mixin hover { + background-color: light-dark( + var(--mantine-color-gray-2), + var(--mantine-color-dark-8) + ); + } +} + +.historyEditor { + :global(.ProseMirror) { + padding: 0 !important; + } } .active { - background-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-8)); + background-color: light-dark( + var(--mantine-color-gray-2), + var(--mantine-color-dark-8) + ); } .sidebar { - max-height: rem(700px); - width: rem(250px); - padding: var(--mantine-spacing-sm); - display: flex; - flex-direction: column; - border-right: rem(1px) solid + max-height: rem(700px); + width: rem(250px); + padding: var(--mantine-spacing-sm); + display: flex; + flex-direction: column; + border-right: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); } .sidebarFlex { - display: flex; + display: flex; } .sidebarMain { - flex: 1; + flex: 1; } .sidebarRightSection { - flex: 1; - padding: rem(16px) rem(40px); + flex: 1; + padding: rem(16px) rem(40px); } diff --git a/apps/client/src/features/page/components/header/page-header-menu.tsx b/apps/client/src/features/page/components/header/page-header-menu.tsx index cf61ac39..6e9625b1 100644 --- a/apps/client/src/features/page/components/header/page-header-menu.tsx +++ b/apps/client/src/features/page/components/header/page-header-menu.tsx @@ -9,20 +9,14 @@ import { IconList, IconMessage, IconPrinter, - IconSearch, IconTrash, IconWifiOff, } from "@tabler/icons-react"; -import React from "react"; +import React, { useEffect, useRef, useState } from "react"; import useToggleAside from "@/hooks/use-toggle-aside.tsx"; -import { useAtom } from "jotai"; +import { useAtom, useAtomValue } from "jotai"; import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts"; -import { - getHotkeyHandler, - useClipboard, - useDisclosure, - useHotkeys, -} from "@mantine/hooks"; +import { useClipboard, useDisclosure, useHotkeys } from "@mantine/hooks"; import { useParams } from "react-router-dom"; import { usePageQuery } from "@/features/page/queries/page-query.ts"; import { buildPageUrl } from "@/features/page/page.utils.ts"; @@ -38,8 +32,7 @@ import { pageEditorAtom, yjsConnectionStatusAtom, } from "@/features/editor/atoms/editor-atoms.ts"; -import { searchAndReplaceStateAtom } from "@/features/editor/components/search-and-replace/atoms/search-and-replace-state-atom.ts"; -import { formattedDate, timeAgo } from "@/lib/time.ts"; +import { formattedDate } from "@/lib/time.ts"; import { PageStateSegmentedControl } from "@/features/user/components/page-state-pref.tsx"; import MovePageModal from "@/features/page/components/move-page-modal.tsx"; import { useTimeAgo } from "@/hooks/use-time-ago.tsx"; @@ -51,7 +44,6 @@ interface PageHeaderMenuProps { export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) { const { t } = useTranslation(); const toggleAside = useToggleAside(); - const [yjsConnectionStatus] = useAtom(yjsConnectionStatusAtom); useHotkeys( [ @@ -68,6 +60,7 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) { const event = new CustomEvent("closeFindDialogFromEditor", {}); document.dispatchEvent(event); }, + { preventDefault: false }, ], ], [], @@ -75,17 +68,7 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) { return ( <> - {yjsConnectionStatus === "disconnected" && ( - - - - - - )} + {!readOnly && } @@ -290,3 +273,51 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) { ); } + +function ConnectionWarning() { + const { t } = useTranslation(); + const yjsConnectionStatus = useAtomValue(yjsConnectionStatusAtom); + const [showWarning, setShowWarning] = useState(false); + const timeoutRef = useRef | null>(null); + + useEffect(() => { + const isDisconnected = ["disconnected", "connecting"].includes( + yjsConnectionStatus, + ); + + if (isDisconnected) { + if (!timeoutRef.current) { + timeoutRef.current = setTimeout(() => setShowWarning(true), 5000); + } + } else { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + setShowWarning(false); + } + }, [yjsConnectionStatus]); + + // Cleanup only on unmount + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + + if (!showWarning) return null; + + return ( + + + + + + ); +} diff --git a/apps/server/src/collaboration/collaboration.gateway.ts b/apps/server/src/collaboration/collaboration.gateway.ts index 3f894572..f1d50671 100644 --- a/apps/server/src/collaboration/collaboration.gateway.ts +++ b/apps/server/src/collaboration/collaboration.gateway.ts @@ -26,7 +26,7 @@ export class CollaborationGateway { ) { this.redisConfig = parseRedisUrl(this.environmentService.getRedisUrl()); - this.hocuspocus = HocuspocusServer.configure({ + this.hocuspocus = new Hocuspocus({ debounce: 10000, maxDebounce: 45000, unloadImmediately: false, @@ -65,6 +65,6 @@ export class CollaborationGateway { } async destroy(): Promise { - await this.hocuspocus.destroy(); + //await this.hocuspocus.destroy(); } } diff --git a/apps/server/src/collaboration/collaboration.util.ts b/apps/server/src/collaboration/collaboration.util.ts index 06133c3f..16ca5bd5 100644 --- a/apps/server/src/collaboration/collaboration.util.ts +++ b/apps/server/src/collaboration/collaboration.util.ts @@ -1,14 +1,12 @@ import { StarterKit } from '@tiptap/starter-kit'; import { TextAlign } from '@tiptap/extension-text-align'; -import { TaskList } from '@tiptap/extension-task-list'; -import { TaskItem } from '@tiptap/extension-task-item'; -import { Underline } from '@tiptap/extension-underline'; import { Superscript } from '@tiptap/extension-superscript'; import SubScript from '@tiptap/extension-subscript'; import { Typography } from '@tiptap/extension-typography'; import { TextStyle } from '@tiptap/extension-text-style'; import { Color } from '@tiptap/extension-color'; import { Youtube } from '@tiptap/extension-youtube'; +import { TaskList, TaskItem } from '@tiptap/extension-list'; import { Heading, Callout, @@ -42,11 +40,14 @@ import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html'; // @tiptap/html library works best for generating prosemirror json state but not HTML // see: https://github.com/ueberdosis/tiptap/issues/5352 // see:https://github.com/ueberdosis/tiptap/issues/4089 +//import { generateJSON } from '@tiptap/html'; import { Node } from '@tiptap/pm/model'; export const tiptapExtensions = [ StarterKit.configure({ codeBlock: false, + link: false, + trailingNode: false, heading: false, }), Heading, @@ -59,7 +60,6 @@ export const tiptapExtensions = [ TaskItem.configure({ nested: true, }), - Underline, LinkExtension, Superscript, SubScript, diff --git a/apps/server/src/collaboration/extensions/authentication.extension.ts b/apps/server/src/collaboration/extensions/authentication.extension.ts index 1a42bd97..04a360f7 100644 --- a/apps/server/src/collaboration/extensions/authentication.extension.ts +++ b/apps/server/src/collaboration/extensions/authentication.extension.ts @@ -69,7 +69,7 @@ export class AuthenticationExtension implements Extension { } if (userSpaceRole === SpaceRole.READER) { - data.connection.readOnly = true; + data.connectionConfig.readOnly = true; this.logger.debug(`User granted readonly access to page: ${pageId}`); } diff --git a/package.json b/package.json index e7550c6a..6a5103e8 100644 --- a/package.json +++ b/package.json @@ -20,56 +20,51 @@ }, "dependencies": { "@braintree/sanitize-url": "^7.1.0", - "@casl/ability": "^6.7.5", + "@casl/ability": "6.8.0", "@docmost/editor-ext": "workspace:*", "@floating-ui/dom": "^1.7.3", - "@hocuspocus/extension-redis": "^2.15.3", - "@hocuspocus/provider": "^2.15.3", - "@hocuspocus/server": "^2.15.3", - "@hocuspocus/transformer": "^2.15.3", + "@hocuspocus/extension-redis": "3.4.3", + "@hocuspocus/provider": "3.4.3", + "@hocuspocus/server": "3.4.3", + "@hocuspocus/transformer": "3.4.3", "@joplin/turndown": "^4.0.74", "@joplin/turndown-plugin-gfm": "^1.0.56", "@sindresorhus/slugify": "1.1.0", - "@tiptap/core": "2.27.1", - "@tiptap/extension-code-block": "2.27.1", - "@tiptap/extension-code-block-lowlight": "2.27.1", - "@tiptap/extension-collaboration": "2.27.1", - "@tiptap/extension-collaboration-cursor": "2.27.1", - "@tiptap/extension-color": "2.27.1", - "@tiptap/extension-document": "2.27.1", - "@tiptap/extension-heading": "2.27.1", - "@tiptap/extension-highlight": "2.27.1", - "@tiptap/extension-history": "2.27.1", - "@tiptap/extension-image": "2.27.1", - "@tiptap/extension-link": "2.27.1", - "@tiptap/extension-list-item": "2.27.1", - "@tiptap/extension-list-keymap": "2.27.1", - "@tiptap/extension-placeholder": "2.27.1", - "@tiptap/extension-subscript": "2.27.1", - "@tiptap/extension-superscript": "2.27.1", - "@tiptap/extension-table": "2.27.1", - "@tiptap/extension-table-cell": "2.27.1", - "@tiptap/extension-table-header": "2.27.1", - "@tiptap/extension-table-row": "2.27.1", - "@tiptap/extension-task-item": "2.27.1", - "@tiptap/extension-task-list": "2.27.1", - "@tiptap/extension-text": "2.27.1", - "@tiptap/extension-text-align": "2.27.1", - "@tiptap/extension-text-style": "2.27.1", - "@tiptap/extension-typography": "2.27.1", - "@tiptap/extension-underline": "2.27.1", - "@tiptap/extension-youtube": "2.27.1", - "@tiptap/html": "2.27.1", - "@tiptap/pm": "2.27.1", - "@tiptap/react": "2.27.1", - "@tiptap/starter-kit": "2.27.1", - "@tiptap/suggestion": "2.27.1", + "@tiptap/core": "3.17.1", + "@tiptap/extension-code-block": "3.17.1", + "@tiptap/extension-collaboration": "3.17.1", + "@tiptap/extension-collaboration-caret": "3.17.1", + "@tiptap/extension-color": "3.17.1", + "@tiptap/extension-document": "3.17.1", + "@tiptap/extension-heading": "3.17.1", + "@tiptap/extension-highlight": "3.17.1", + "@tiptap/extension-history": "3.17.1", + "@tiptap/extension-image": "3.17.1", + "@tiptap/extension-link": "3.17.1", + "@tiptap/extension-list": "3.17.1", + "@tiptap/extension-placeholder": "3.17.1", + "@tiptap/extension-subscript": "3.17.1", + "@tiptap/extension-superscript": "3.17.1", + "@tiptap/extension-table": "3.17.1", + "@tiptap/extension-text": "3.17.1", + "@tiptap/extension-text-align": "3.17.1", + "@tiptap/extension-text-style": "3.17.1", + "@tiptap/extension-typography": "3.17.1", + "@tiptap/extension-unique-id": "^3.17.1", + "@tiptap/extension-youtube": "3.17.1", + "@tiptap/html": "3.17.1", + "@tiptap/pm": "3.17.1", + "@tiptap/react": "3.17.1", + "@tiptap/starter-kit": "3.17.1", + "@tiptap/suggestion": "3.17.1", "@types/qrcode": "^1.5.5", "bytes": "^3.1.2", "cross-env": "^7.0.3", "date-fns": "^4.1.0", "dompurify": "^3.2.6", "fractional-indexing-jittered": "^1.0.0", + "highlight.js": "^11.11.1", + "image-dimensions": "^2.5.0", "ioredis": "^5.4.1", "jszip": "^3.10.1", "linkifyjs": "^4.3.2", @@ -79,7 +74,7 @@ "uuid": "^11.1.0", "y-indexeddb": "^9.0.12", "y-prosemirror": "1.3.7", - "yjs": "^13.6.27" + "yjs": "^13.6.29" }, "devDependencies": { "@nx/js": "20.4.5", diff --git a/packages/editor-ext/src/index.ts b/packages/editor-ext/src/index.ts index 3ff99083..24d0ac5f 100644 --- a/packages/editor-ext/src/index.ts +++ b/packages/editor-ext/src/index.ts @@ -23,3 +23,4 @@ export * from "./lib/subpages"; export * from "./lib/highlight"; export * from "./lib/heading/heading"; export * from "./lib/unique-id"; +export * from "./lib/shared-storage"; diff --git a/packages/editor-ext/src/lib/attachment/attachment-upload.ts b/packages/editor-ext/src/lib/attachment/attachment-upload.ts index 0d2ac6c7..a3446db9 100644 --- a/packages/editor-ext/src/lib/attachment/attachment-upload.ts +++ b/packages/editor-ext/src/lib/attachment/attachment-upload.ts @@ -1,126 +1,125 @@ -import { type EditorState, Plugin, PluginKey } from "@tiptap/pm/state"; -import { Decoration, DecorationSet } from "@tiptap/pm/view"; -import { - insertTrailingNode, - MediaUploadOptions, - UploadFn, -} from "../media-utils"; +import { Node } from "@tiptap/pm/model"; +import { MediaUploadOptions, UploadFn } from "../media-utils"; import { IAttachment } from "../types"; +import { generateNodeId } from "../utils"; +import { Command } from "@tiptap/core"; -const uploadKey = new PluginKey("attachment-upload"); +const findAttachmentNodeByPlaceholderId = ( + doc: Node, + placeholderId: string, +): { node: Node; pos: number } | null => { + let result: { node: Node; pos: number } | null = null; -export const AttachmentUploadPlugin = ({ - placeholderClass, -}: { - placeholderClass: string; -}) => - new Plugin({ - key: uploadKey, - state: { - init() { - return DecorationSet.empty; - }, - apply(tr, set) { - set = set.map(tr.mapping, tr.doc); - // See if the transaction adds or removes any placeholders - //@-ts-expect-error - not yet sure what the type I need here - const action = tr.getMeta(this); - if (action?.add) { - const { id, pos, fileName } = action.add; - - const placeholder = document.createElement("div"); - placeholder.setAttribute("class", placeholderClass); - - const uploadingText = document.createElement("span"); - uploadingText.setAttribute("class", "uploading-text"); - uploadingText.textContent = `Uploading ${fileName}`; - - placeholder.appendChild(uploadingText); - - const realPos = pos + 1; - const deco = Decoration.widget(realPos, placeholder, { - id, - }); - set = set.add(tr.doc, [deco]); - } else if (action?.remove) { - set = set.remove( - set.find( - undefined, - undefined, - (spec) => spec.id == action.remove.id, - ), - ); - } - return set; - }, - }, - props: { - decorations(state) { - return this.getState(state); - }, - }, + doc.descendants((node, pos) => { + if (result) return false; + if ( + node.type.name === "attachment" && + node.attrs.placeholder?.id === placeholderId + ) { + result = { node, pos }; + return false; + } + return true; }); -function findPlaceholder(state: EditorState, id: {}) { - const decos = uploadKey.getState(state) as DecorationSet; - const found = decos.find(undefined, undefined, (spec) => spec.id == id); - return found.length ? found[0]?.from : null; -} - -export const handleAttachmentUpload = + return result; +}; +const handleAttachmentUpload = ({ validateFn, onUpload }: MediaUploadOptions): UploadFn => - async (file, view, pos, pageId, allowMedia) => { + async (file, editor, pos, pageId, allowMedia) => { const validated = validateFn?.(file, allowMedia); // @ts-ignore if (!validated) return; - // A fresh object to act as the ID for this upload - const id = {}; - // Replace the selection with a placeholder - const tr = view.state.tr; - if (!tr.selection.empty) tr.deleteSelection(); + const placeholderId = generateNodeId(); - tr.setMeta(uploadKey, { - add: { - id, - pos, - fileName: file.name, - }, - }); + let placeholderInserted = false; - insertTrailingNode(tr, pos, view); - view.dispatch(tr); + const insertPlaceholder = (): Command => { + return ({ tr, state }) => { + const initialPlaceholderNode = state.schema.nodes.attachment?.create({ + placeholder: { + id: placeholderId, + }, + name: file.name, + size: file.size, + }); - await onUpload(file, pageId).then( - (attachment: IAttachment) => { - const { schema } = view.state; + if (!initialPlaceholderNode) return false; - const pos = findPlaceholder(view.state, id); + const { parent } = tr.doc.resolve(pos); + const isEmptyTextBlock = parent.isTextblock && !parent.childCount; - if (pos == null) return; + if (isEmptyTextBlock) { + tr.replaceRangeWith(pos - 1, pos + 1, initialPlaceholderNode); + } else { + tr.insert(pos, initialPlaceholderNode); + } - if (!attachment) return; + return true; + }; + }; + const replacePlaceholderWithAttachment = ( + attachment: IAttachment, + ): Command => { + return ({ tr }) => { + const { pos: currentPos = null } = + findAttachmentNodeByPlaceholderId(tr.doc, placeholderId) || {}; - const node = schema.nodes.attachment?.create({ + // If the placeholder is not found or attachment is missing, abort the process + if (currentPos === null || !attachment) return false; + + // Update the placeholder node with the actual attachment data + tr.setNodeMarkup(currentPos, undefined, { url: `/api/files/${attachment.id}/${attachment.fileName}`, name: attachment.fileName, mime: attachment.mimeType, size: attachment.fileSize, attachmentId: attachment.id, }); - if (!node) return; - const transaction = view.state.tr - .replaceWith(pos, pos, node) - .setMeta(uploadKey, { remove: { id } }); - view.dispatch(transaction); - }, - () => { - // Deletes the placeholder on error - const transaction = view.state.tr - .delete(pos, pos) - .setMeta(uploadKey, { remove: { id } }); - view.dispatch(transaction); - }, - ); + return true; + }; + }; + const removePlaceholder = (): Command => { + return ({ tr }) => { + const { pos: currentPos = null } = + findAttachmentNodeByPlaceholderId(tr.doc, placeholderId) || {}; + + if (currentPos === null) return false; + + tr.delete(currentPos, currentPos + 2); + + return true; + }; + }; + // Only show the placeholder if the upload takes more than 250ms + const insertPlaceholderTimeout = setTimeout(() => { + editor.commands.command(insertPlaceholder()); + placeholderInserted = true; + }, 250); + + try { + const attachment: IAttachment = await onUpload(file, pageId); + + clearTimeout(insertPlaceholderTimeout); + + if (placeholderInserted) { + setTimeout(() => { + editor.commands.command(replacePlaceholderWithAttachment(attachment)); + }, 100); + } else { + editor + .chain() + .command(insertPlaceholder()) + .command(replacePlaceholderWithAttachment(attachment)) + .run(); + } + } catch (error) { + clearTimeout(insertPlaceholderTimeout); + + editor.commands.command(removePlaceholder()); + } }; + +export { handleAttachmentUpload }; diff --git a/packages/editor-ext/src/lib/attachment/attachment.ts b/packages/editor-ext/src/lib/attachment/attachment.ts index 5231c897..0e37e014 100644 --- a/packages/editor-ext/src/lib/attachment/attachment.ts +++ b/packages/editor-ext/src/lib/attachment/attachment.ts @@ -1,6 +1,5 @@ import { Node, mergeAttributes } from "@tiptap/core"; import { ReactNodeViewRenderer } from "@tiptap/react"; -import { AttachmentUploadPlugin } from "./attachment-upload"; export interface AttachmentOptions { HTMLAttributes: Record; @@ -13,6 +12,7 @@ export interface AttachmentAttributes { mime?: string; // e.g. application/zip size?: number; attachmentId?: string; + placeholder?: string; } declare module "@tiptap/core" { @@ -75,6 +75,10 @@ export const Attachment = Node.create({ "data-attachment-id": attributes.attachmentId, }), }, + placeholder: { + default: null, + rendered: false, + }, }; }, @@ -120,14 +124,9 @@ export const Attachment = Node.create({ }, addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); }, - - addProseMirrorPlugins() { - return [ - AttachmentUploadPlugin({ - placeholderClass: "attachment-placeholder", - }), - ]; - }, }); diff --git a/packages/editor-ext/src/lib/callout/callout.ts b/packages/editor-ext/src/lib/callout/callout.ts index 97c5dfcc..1dc4d800 100644 --- a/packages/editor-ext/src/lib/callout/callout.ts +++ b/packages/editor-ext/src/lib/callout/callout.ts @@ -87,7 +87,7 @@ export const Callout = Node.create({ mergeAttributes( { "data-type": this.name }, this.options.HTMLAttributes, - HTMLAttributes, + HTMLAttributes ), 0, ]; @@ -130,6 +130,9 @@ export const Callout = Node.create({ }, addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); }, @@ -193,7 +196,7 @@ export const Callout = Node.create({ tr.delete(pos, pos + nodeSize); tr.setSelection( - TextSelection.near(tr.doc.resolve(previousPosition - 1)), + TextSelection.near(tr.doc.resolve(previousPosition - 1)) ); tr.insert(previousPosition - 1, content); diff --git a/packages/editor-ext/src/lib/custom-code-block.ts b/packages/editor-ext/src/lib/custom-code-block.ts deleted file mode 100644 index 702e98a9..00000000 --- a/packages/editor-ext/src/lib/custom-code-block.ts +++ /dev/null @@ -1,81 +0,0 @@ -import CodeBlockLowlight, { - CodeBlockLowlightOptions, -} from "@tiptap/extension-code-block-lowlight"; -import { ReactNodeViewRenderer } from "@tiptap/react"; - -export interface CustomCodeBlockOptions extends CodeBlockLowlightOptions { - view: any; -} - -const TAB_CHAR = "\u00A0\u00A0"; - -export const CustomCodeBlock = CodeBlockLowlight.extend( - { - selectable: true, - - addOptions() { - return { - ...this.parent?.(), - view: null, - }; - }, - - addKeyboardShortcuts() { - return { - ...this.parent?.(), - Tab: () => { - if (this.editor.isActive("codeBlock")) { - this.editor - .chain() - .command(({ tr }) => { - tr.insertText(TAB_CHAR); - return true; - }) - .run(); - return true; - } - }, - "Mod-a": () => { - if (this.editor.isActive("codeBlock")) { - const { state } = this.editor; - const { $from } = state.selection; - - let codeBlockNode = null; - let codeBlockPos = null; - let depth = 0; - - for (depth = $from.depth; depth > 0; depth--) { - const node = $from.node(depth); - if (node.type.name === "codeBlock") { - codeBlockNode = node; - codeBlockPos = $from.start(depth) - 1; - break; - } - } - - if (codeBlockNode && codeBlockPos !== null) { - const codeBlockStart = codeBlockPos; - const codeBlockEnd = codeBlockPos + codeBlockNode.nodeSize; - - const contentStart = codeBlockStart + 1; - const contentEnd = codeBlockEnd - 1; - - this.editor.commands.setTextSelection({ - from: contentStart, - to: contentEnd, - }); - - return true; - } - } - - return false; - }, - }; - }, - - addNodeView() { - return ReactNodeViewRenderer(this.options.view); - }, - } -); diff --git a/packages/editor-ext/src/lib/custom-code-block/custom-code-block.ts b/packages/editor-ext/src/lib/custom-code-block/custom-code-block.ts new file mode 100644 index 00000000..ba9fe9c1 --- /dev/null +++ b/packages/editor-ext/src/lib/custom-code-block/custom-code-block.ts @@ -0,0 +1,108 @@ +import type { CodeBlockOptions } from "@tiptap/extension-code-block"; +import CodeBlock from "@tiptap/extension-code-block"; + +import { LowlightPlugin } from "./lowlight-plugin.js"; +import { ReactNodeViewRenderer } from "@tiptap/react"; + +export interface CodeBlockLowlightOptions extends CodeBlockOptions { + /** + * The lowlight instance. + */ + lowlight: any; + view: any; +} + +const TAB_CHAR = "\u00A0\u00A0"; + +/** + * This extension allows you to highlight code blocks with lowlight. + * @see https://tiptap.dev/api/nodes/code-block-lowlight + */ +export const CustomCodeBlock = CodeBlock.extend({ + selectable: true, + + addOptions() { + return { + ...this.parent?.(), + lowlight: {}, + languageClassPrefix: "language-", + exitOnTripleEnter: true, + exitOnArrowDown: true, + defaultLanguage: null, + HTMLAttributes: {}, + view: null, + }; + }, + + addKeyboardShortcuts() { + return { + ...this.parent?.(), + Tab: () => { + if (this.editor.isActive("codeBlock")) { + this.editor + .chain() + .command(({ tr }) => { + tr.insertText(TAB_CHAR); + return true; + }) + .run(); + return true; + } + }, + "Mod-a": () => { + if (this.editor.isActive("codeBlock")) { + const { state } = this.editor; + const { $from } = state.selection; + + let codeBlockNode = null; + let codeBlockPos = null; + let depth = 0; + + for (depth = $from.depth; depth > 0; depth--) { + const node = $from.node(depth); + if (node.type.name === "codeBlock") { + codeBlockNode = node; + codeBlockPos = $from.start(depth) - 1; + break; + } + } + + if (codeBlockNode && codeBlockPos !== null) { + const codeBlockStart = codeBlockPos; + const codeBlockEnd = codeBlockPos + codeBlockNode.nodeSize; + + const contentStart = codeBlockStart + 1; + const contentEnd = codeBlockEnd - 1; + + this.editor.commands.setTextSelection({ + from: contentStart, + to: contentEnd, + }); + + return true; + } + } + + return false; + }, + }; + }, + + addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + + return ReactNodeViewRenderer(this.options.view); + }, + + addProseMirrorPlugins() { + return [ + ...(this.parent?.() || []), + LowlightPlugin({ + name: this.name, + lowlight: this.options.lowlight, + defaultLanguage: this.options.defaultLanguage, + }), + ]; + }, +}); diff --git a/packages/editor-ext/src/lib/custom-code-block/index.ts b/packages/editor-ext/src/lib/custom-code-block/index.ts new file mode 100644 index 00000000..f6e3470f --- /dev/null +++ b/packages/editor-ext/src/lib/custom-code-block/index.ts @@ -0,0 +1 @@ +export { CustomCodeBlock } from "./custom-code-block"; diff --git a/packages/editor-ext/src/lib/custom-code-block/lowlight-plugin.ts b/packages/editor-ext/src/lib/custom-code-block/lowlight-plugin.ts new file mode 100644 index 00000000..505b8f20 --- /dev/null +++ b/packages/editor-ext/src/lib/custom-code-block/lowlight-plugin.ts @@ -0,0 +1,159 @@ +import { findChildren } from '@tiptap/core' +import type { Node as ProsemirrorNode } from '@tiptap/pm/model' +import { Plugin, PluginKey } from '@tiptap/pm/state' +import { Decoration, DecorationSet } from '@tiptap/pm/view' +// @ts-ignore +import highlight from 'highlight.js/lib/core' + +function parseNodes(nodes: any[], className: string[] = []): { text: string; classes: string[] }[] { + return nodes + .map(node => { + const classes = [...className, ...(node.properties ? node.properties.className : [])] + + if (node.children) { + return parseNodes(node.children, classes) + } + + return { + text: node.value, + classes, + } + }) + .flat() +} + +function getHighlightNodes(result: any) { + // `.value` for lowlight v1, `.children` for lowlight v2 + return result.value || result.children || [] +} + +function registered(aliasOrLanguage: string) { + return Boolean(highlight.getLanguage(aliasOrLanguage)) +} + +function getDecorations({ + doc, + name, + lowlight, + defaultLanguage, +}: { + doc: ProsemirrorNode + name: string + lowlight: any + defaultLanguage: string | null | undefined +}) { + const decorations: Decoration[] = [] + + findChildren(doc, node => node.type.name === name).forEach(block => { + let from = block.pos + 1 + const language = block.node.attrs.language || defaultLanguage + const languages = lowlight.listLanguages() + + const nodes = + language && (languages.includes(language) || registered(language) || lowlight.registered?.(language)) + ? getHighlightNodes(lowlight.highlight(language, block.node.textContent)) + : getHighlightNodes(lowlight.highlightAuto(block.node.textContent)) + + parseNodes(nodes).forEach(node => { + const to = from + node.text.length + + if (node.classes.length) { + const decoration = Decoration.inline(from, to, { + class: node.classes.join(' '), + }) + + decorations.push(decoration) + } + + from = to + }) + }) + + return DecorationSet.create(doc, decorations) +} + +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type +function isFunction(param: any): param is Function { + return typeof param === 'function' +} + +export function LowlightPlugin({ + name, + lowlight, + defaultLanguage, +}: { + name: string + lowlight: any + defaultLanguage: string | null | undefined +}) { + if (!['highlight', 'highlightAuto', 'listLanguages'].every(api => isFunction(lowlight[api]))) { + throw Error('You should provide an instance of lowlight to use the code-block-lowlight extension') + } + + const lowlightPlugin: Plugin = new Plugin({ + key: new PluginKey('lowlight'), + + state: { + init: (_, { doc }) => + getDecorations({ + doc, + name, + lowlight, + defaultLanguage, + }), + apply: (transaction, decorationSet, oldState, newState) => { + const oldNodeName = oldState.selection.$head.parent.type.name + const newNodeName = newState.selection.$head.parent.type.name + const oldNodes = findChildren(oldState.doc, node => node.type.name === name) + const newNodes = findChildren(newState.doc, node => node.type.name === name) + + if ( + transaction.docChanged && + // Apply decorations if: + // selection includes named node, + ([oldNodeName, newNodeName].includes(name) || + // OR transaction adds/removes named node, + newNodes.length !== oldNodes.length || + // OR transaction has changes that completely encapsulte a node + // (for example, a transaction that affects the entire document). + // Such transactions can happen during collab syncing via y-prosemirror, for example. + transaction.steps.some(step => { + // @ts-ignore + return ( + // @ts-ignore + step.from !== undefined && + // @ts-ignore + step.to !== undefined && + oldNodes.some(node => { + // @ts-ignore + return ( + // @ts-ignore + node.pos >= step.from && + // @ts-ignore + node.pos + node.node.nodeSize <= step.to + ) + }) + ) + })) + ) { + return getDecorations({ + doc: transaction.doc, + name, + lowlight, + defaultLanguage, + }) + } + + return decorationSet.map(transaction.mapping, transaction.doc) + }, + }, + + props: { + decorations(state) { + return lowlightPlugin.getState(state) + }, + }, + }) + + return lowlightPlugin +} \ No newline at end of file diff --git a/packages/editor-ext/src/lib/details/details.ts b/packages/editor-ext/src/lib/details/details.ts index b28c4de7..41c66dca 100644 --- a/packages/editor-ext/src/lib/details/details.ts +++ b/packages/editor-ext/src/lib/details/details.ts @@ -27,6 +27,7 @@ export const Details = Node.create({ content: "detailsSummary detailsContent", defining: true, isolating: true, + // @ts-ignore allowGapCursor: false, addOptions() { return { diff --git a/packages/editor-ext/src/lib/drawio.ts b/packages/editor-ext/src/lib/drawio.ts index 319853b2..3cc041a2 100644 --- a/packages/editor-ext/src/lib/drawio.ts +++ b/packages/editor-ext/src/lib/drawio.ts @@ -41,45 +41,45 @@ export const Drawio = Node.create({ addAttributes() { return { src: { - default: '', - parseHTML: (element) => element.getAttribute('data-src'), + default: "", + parseHTML: (element) => element.getAttribute("data-src"), renderHTML: (attributes) => ({ - 'data-src': attributes.src, + "data-src": attributes.src, }), }, title: { default: undefined, - parseHTML: (element) => element.getAttribute('data-title'), + parseHTML: (element) => element.getAttribute("data-title"), renderHTML: (attributes: DrawioAttributes) => ({ - 'data-title': attributes.title, + "data-title": attributes.title, }), }, width: { - default: '100%', - parseHTML: (element) => element.getAttribute('data-width'), + default: "100%", + parseHTML: (element) => element.getAttribute("data-width"), renderHTML: (attributes: DrawioAttributes) => ({ - 'data-width': attributes.width, + "data-width": attributes.width, }), }, size: { default: null, - parseHTML: (element) => element.getAttribute('data-size'), + parseHTML: (element) => element.getAttribute("data-size"), renderHTML: (attributes: DrawioAttributes) => ({ - 'data-size': attributes.size, + "data-size": attributes.size, }), }, align: { - default: 'center', - parseHTML: (element) => element.getAttribute('data-align'), + default: "center", + parseHTML: (element) => element.getAttribute("data-align"), renderHTML: (attributes: DrawioAttributes) => ({ - 'data-align': attributes.align, + "data-align": attributes.align, }), }, attachmentId: { default: undefined, - parseHTML: (element) => element.getAttribute('data-attachment-id'), + parseHTML: (element) => element.getAttribute("data-attachment-id"), renderHTML: (attributes: DrawioAttributes) => ({ - 'data-attachment-id': attributes.attachmentId, + "data-attachment-id": attributes.attachmentId, }), }, }; @@ -95,13 +95,20 @@ export const Drawio = Node.create({ renderHTML({ HTMLAttributes }) { return [ - 'div', + "div", mergeAttributes( - { 'data-type': this.name }, + { "data-type": this.name }, this.options.HTMLAttributes, HTMLAttributes ), - ['img', { src: HTMLAttributes['data-src'], alt: HTMLAttributes['data-title'], width: HTMLAttributes['data-width'] }], + [ + "img", + { + src: HTMLAttributes["data-src"], + alt: HTMLAttributes["data-title"], + width: HTMLAttributes["data-width"], + }, + ], ]; }, @@ -119,6 +126,9 @@ export const Drawio = Node.create({ }, addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); }, }); diff --git a/packages/editor-ext/src/lib/embed.ts b/packages/editor-ext/src/lib/embed.ts index 47fc251e..a93648b1 100644 --- a/packages/editor-ext/src/lib/embed.ts +++ b/packages/editor-ext/src/lib/embed.ts @@ -1,6 +1,6 @@ -import { Node, mergeAttributes } from '@tiptap/core'; -import { ReactNodeViewRenderer } from '@tiptap/react'; -import { sanitizeUrl } from './utils'; +import { Node, mergeAttributes } from "@tiptap/core"; +import { ReactNodeViewRenderer } from "@tiptap/react"; +import { sanitizeUrl } from "./utils"; export interface EmbedOptions { HTMLAttributes: Record; @@ -14,7 +14,7 @@ export interface EmbedAttributes { height?: number; } -declare module '@tiptap/core' { +declare module "@tiptap/core" { interface Commands { embeds: { setEmbed: (attributes?: EmbedAttributes) => ReturnType; @@ -23,9 +23,9 @@ declare module '@tiptap/core' { } export const Embed = Node.create({ - name: 'embed', + name: "embed", inline: false, - group: 'block', + group: "block", isolating: true, atom: true, defining: true, @@ -40,41 +40,41 @@ export const Embed = Node.create({ addAttributes() { return { src: { - default: '', + default: "", parseHTML: (element) => { - const src = element.getAttribute('data-src'); + const src = element.getAttribute("data-src"); return sanitizeUrl(src); }, renderHTML: (attributes: EmbedAttributes) => ({ - 'data-src': sanitizeUrl(attributes.src), + "data-src": sanitizeUrl(attributes.src), }), }, provider: { - default: '', - parseHTML: (element) => element.getAttribute('data-provider'), + default: "", + parseHTML: (element) => element.getAttribute("data-provider"), renderHTML: (attributes: EmbedAttributes) => ({ - 'data-provider': attributes.provider, + "data-provider": attributes.provider, }), }, align: { - default: 'center', - parseHTML: (element) => element.getAttribute('data-align'), + default: "center", + parseHTML: (element) => element.getAttribute("data-align"), renderHTML: (attributes: EmbedAttributes) => ({ - 'data-align': attributes.align, + "data-align": attributes.align, }), }, width: { default: 640, - parseHTML: (element) => element.getAttribute('data-width'), + parseHTML: (element) => element.getAttribute("data-width"), renderHTML: (attributes: EmbedAttributes) => ({ - 'data-width': attributes.width, + "data-width": attributes.width, }), }, height: { default: 480, - parseHTML: (element) => element.getAttribute('data-height'), + parseHTML: (element) => element.getAttribute("data-height"), renderHTML: (attributes: EmbedAttributes) => ({ - 'data-height': attributes.height, + "data-height": attributes.height, }), }, }; @@ -91,13 +91,13 @@ export const Embed = Node.create({ renderHTML({ HTMLAttributes }) { const src = HTMLAttributes["data-src"]; const safeHref = sanitizeUrl(src); - + return [ "div", mergeAttributes( { "data-type": this.name }, this.options.HTMLAttributes, - HTMLAttributes, + HTMLAttributes ), [ "a", @@ -120,9 +120,9 @@ export const Embed = Node.create({ ...attrs, src: sanitizeUrl(attrs.src), }; - + return commands.insertContent({ - type: 'embed', + type: "embed", attrs: validatedAttrs, }); }, @@ -130,6 +130,9 @@ export const Embed = Node.create({ }, addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); }, }); diff --git a/packages/editor-ext/src/lib/excalidraw.ts b/packages/editor-ext/src/lib/excalidraw.ts index a7e3a468..28b064e4 100644 --- a/packages/editor-ext/src/lib/excalidraw.ts +++ b/packages/editor-ext/src/lib/excalidraw.ts @@ -1,5 +1,5 @@ -import { Node, mergeAttributes } from '@tiptap/core'; -import { ReactNodeViewRenderer } from '@tiptap/react'; +import { Node, mergeAttributes } from "@tiptap/core"; +import { ReactNodeViewRenderer } from "@tiptap/react"; export interface ExcalidrawOptions { HTMLAttributes: Record; @@ -14,7 +14,7 @@ export interface ExcalidrawAttributes { attachmentId?: string; } -declare module '@tiptap/core' { +declare module "@tiptap/core" { interface Commands { excalidraw: { setExcalidraw: (attributes?: ExcalidrawAttributes) => ReturnType; @@ -23,9 +23,9 @@ declare module '@tiptap/core' { } export const Excalidraw = Node.create({ - name: 'excalidraw', + name: "excalidraw", inline: false, - group: 'block', + group: "block", isolating: true, atom: true, defining: true, @@ -40,45 +40,45 @@ export const Excalidraw = Node.create({ addAttributes() { return { src: { - default: '', - parseHTML: (element) => element.getAttribute('data-src'), + default: "", + parseHTML: (element) => element.getAttribute("data-src"), renderHTML: (attributes) => ({ - 'data-src': attributes.src, + "data-src": attributes.src, }), }, title: { default: undefined, - parseHTML: (element) => element.getAttribute('data-title'), + parseHTML: (element) => element.getAttribute("data-title"), renderHTML: (attributes: ExcalidrawAttributes) => ({ - 'data-title': attributes.title, + "data-title": attributes.title, }), }, width: { - default: '100%', - parseHTML: (element) => element.getAttribute('data-width'), + default: "100%", + parseHTML: (element) => element.getAttribute("data-width"), renderHTML: (attributes: ExcalidrawAttributes) => ({ - 'data-width': attributes.width, + "data-width": attributes.width, }), }, size: { default: null, - parseHTML: (element) => element.getAttribute('data-size'), + parseHTML: (element) => element.getAttribute("data-size"), renderHTML: (attributes: ExcalidrawAttributes) => ({ - 'data-size': attributes.size, + "data-size": attributes.size, }), }, align: { - default: 'center', - parseHTML: (element) => element.getAttribute('data-align'), + default: "center", + parseHTML: (element) => element.getAttribute("data-align"), renderHTML: (attributes: ExcalidrawAttributes) => ({ - 'data-align': attributes.align, + "data-align": attributes.align, }), }, attachmentId: { default: undefined, - parseHTML: (element) => element.getAttribute('data-attachment-id'), + parseHTML: (element) => element.getAttribute("data-attachment-id"), renderHTML: (attributes: ExcalidrawAttributes) => ({ - 'data-attachment-id': attributes.attachmentId, + "data-attachment-id": attributes.attachmentId, }), }, }; @@ -94,13 +94,20 @@ export const Excalidraw = Node.create({ renderHTML({ HTMLAttributes }) { return [ - 'div', + "div", mergeAttributes( - { 'data-type': this.name }, + { "data-type": this.name }, this.options.HTMLAttributes, HTMLAttributes ), - ['img', { src: HTMLAttributes['data-src'], alt: HTMLAttributes['data-title'], width: HTMLAttributes['data-width'] }], + [ + "img", + { + src: HTMLAttributes["data-src"], + alt: HTMLAttributes["data-title"], + width: HTMLAttributes["data-width"], + }, + ], ]; }, @@ -110,7 +117,7 @@ export const Excalidraw = Node.create({ (attrs: ExcalidrawAttributes) => ({ commands }) => { return commands.insertContent({ - type: 'excalidraw', + type: "excalidraw", attrs: attrs, }); }, @@ -118,6 +125,9 @@ export const Excalidraw = Node.create({ }, addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); }, }); diff --git a/packages/editor-ext/src/lib/image/image-upload.ts b/packages/editor-ext/src/lib/image/image-upload.ts index 9a759903..d5acdcff 100644 --- a/packages/editor-ext/src/lib/image/image-upload.ts +++ b/packages/editor-ext/src/lib/image/image-upload.ts @@ -1,127 +1,145 @@ -import { type EditorState, Plugin, PluginKey } from "@tiptap/pm/state"; -import { Decoration, DecorationSet } from "@tiptap/pm/view"; -import { insertTrailingNode, MediaUploadOptions, UploadFn } from "../media-utils"; +import { imageDimensionsFromStream } from "image-dimensions"; +import { MediaUploadOptions, UploadFn } from "../media-utils"; import { IAttachment } from "../types"; +import { generateNodeId } from "../utils"; +import { Node } from "@tiptap/pm/model"; +import { Command } from "@tiptap/core"; -const uploadKey = new PluginKey("image-upload"); +const findImageNodeByPlaceholderId = ( + doc: Node, + placeholderId: string, +): { node: Node; pos: number } | null => { + let result: { node: Node; pos: number } | null = null; -export const ImageUploadPlugin = ({ - placeholderClass, -}: { - placeholderClass: string; -}) => - new Plugin({ - key: uploadKey, - state: { - init() { - return DecorationSet.empty; - }, - apply(tr, set) { - set = set.map(tr.mapping, tr.doc); - // See if the transaction adds or removes any placeholders - //@-ts-expect-error - not yet sure what the type I need here - const action = tr.getMeta(this); - if (action?.add) { - const { id, pos, src } = action.add; - - const placeholder = document.createElement("div"); - placeholder.setAttribute("class", "img-placeholder"); - const image = document.createElement("img"); - image.setAttribute("class", placeholderClass); - image.src = src; - placeholder.appendChild(image); - const deco = Decoration.widget(pos + 1, placeholder, { - id, - }); - set = set.add(tr.doc, [deco]); - } else if (action?.remove) { - set = set.remove( - set.find( - undefined, - undefined, - (spec) => spec.id == action.remove.id, - ), - ); - } - return set; - }, - }, - props: { - decorations(state) { - return this.getState(state); - }, - }, + doc.descendants((node, pos) => { + if (result) return false; + if ( + node.type.name === "image" && + node.attrs.placeholder?.id === placeholderId + ) { + result = { node, pos }; + return false; + } + return true; }); -function findPlaceholder(state: EditorState, id: {}) { - const decos = uploadKey.getState(state) as DecorationSet; - const found = decos.find(undefined, undefined, (spec) => spec.id == id); - return found.length ? found[0]?.from : null; -} - -export const handleImageUpload = + return result; +}; +const handleImageUpload = ({ validateFn, onUpload }: MediaUploadOptions): UploadFn => - async (file, view, pos, pageId) => { + async (file, editor, pos, pageId) => { // check if the file is an image const validated = validateFn?.(file); // @ts-ignore if (!validated) return; - // A fresh object to act as the ID for this upload - const id = {}; - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => { - const tr = view.state.tr; - // Replace the selection with a placeholder - if (!tr.selection.empty) tr.deleteSelection(); + const objectUrl = URL.createObjectURL(file); + const imageDimensions = await imageDimensionsFromStream(file.stream()); + const placeholderId = generateNodeId(); + const aspectRatio = imageDimensions + ? imageDimensions.width / imageDimensions.height + : undefined; - tr.setMeta(uploadKey, { - add: { - id, - pos, - src: reader.result, - }, - }); + let placeholderInserted = false; - insertTrailingNode(tr, pos, view); - view.dispatch(tr); + editor.storage.shared.imagePreviews = + editor.storage.shared.imagePreviews || {}; + editor.storage.shared.imagePreviews[placeholderId] = objectUrl; + + const insertPlaceholder = (): Command => { + return ({ tr, state }) => { + const initialPlaceholderNode = state.schema.nodes.image?.create({ + placeholder: { + id: placeholderId, + name: file.name, + }, + aspectRatio, + }); + + if (!initialPlaceholderNode) return false; + + const { parent } = tr.doc.resolve(pos); + const isEmptyTextBlock = parent.isTextblock && !parent.childCount; + + if (isEmptyTextBlock) { + // Replace e.g. empty paragraph with the image + tr.replaceRangeWith(pos - 1, pos + 1, initialPlaceholderNode); + } else { + tr.insert(pos, initialPlaceholderNode); + } + + return true; + }; }; + const replacePlaceholderWithImage = (attachment: IAttachment): Command => { + return ({ tr }) => { + const { pos: currentPos = null } = + findImageNodeByPlaceholderId(tr.doc, placeholderId) || {}; - await onUpload(file, pageId).then( - (attachment: IAttachment) => { - const { schema } = view.state; + // If the placeholder is not found or attachment is missing, abort the process + if (currentPos === null || !attachment) return false; - const pos = findPlaceholder(view.state, id); - - // If the content around the placeholder has been deleted, drop - // the image - if (pos == null) return; - - // Otherwise, insert it at the placeholder's position, and remove - // the placeholder - - if (!attachment) return; - - const node = schema.nodes.image?.create({ + // Update the placeholder node with the actual image data + tr.setNodeMarkup(currentPos, undefined, { src: `/api/files/${attachment.id}/${attachment.fileName}`, attachmentId: attachment.id, - title: attachment.fileName, size: attachment.fileSize, + aspectRatio, }); - if (!node) return; - const transaction = view.state.tr - .replaceWith(pos, pos, node) - .setMeta(uploadKey, { remove: { id } }); - view.dispatch(transaction); - }, - () => { - // Deletes the image placeholder on error - const transaction = view.state.tr - .delete(pos, pos) - .setMeta(uploadKey, { remove: { id } }); - view.dispatch(transaction); - }, - ); + return true; + }; + }; + const removePlaceholder = (): Command => { + return ({ tr }) => { + const { pos: currentPos = null } = + findImageNodeByPlaceholderId(tr.doc, placeholderId) || {}; + + if (currentPos === null) return false; + + // Remove the placeholder node + tr.delete(currentPos, currentPos + 2); + + return true; + }; + }; + // Only show the placeholder if the upload takes more than 250ms + const insertPlaceholderTimeout = setTimeout(() => { + editor.commands.command(insertPlaceholder()); + placeholderInserted = true; + }, 250); + const disposePreviewFile = () => { + URL.revokeObjectURL(objectUrl); + + if (editor.storage.shared.imagePreviews) { + delete editor.storage.shared.imagePreviews[placeholderId]; + } + }; + + try { + const attachment: IAttachment = await onUpload(file, pageId); + + clearTimeout(insertPlaceholderTimeout); + + if (placeholderInserted) { + setTimeout(() => { + editor.commands.command(replacePlaceholderWithImage(attachment)); + disposePreviewFile(); + }, 100); + } else { + editor + .chain() + .command(insertPlaceholder()) + .command(replacePlaceholderWithImage(attachment)) + .run(); + disposePreviewFile(); + } + } catch (error) { + clearTimeout(insertPlaceholderTimeout); + + editor.commands.command(removePlaceholder()); + disposePreviewFile(); + } }; + +export { handleImageUpload }; diff --git a/packages/editor-ext/src/lib/image/image.ts b/packages/editor-ext/src/lib/image/image.ts index 3f7683e4..e6426f23 100644 --- a/packages/editor-ext/src/lib/image/image.ts +++ b/packages/editor-ext/src/lib/image/image.ts @@ -1,7 +1,6 @@ import Image from "@tiptap/extension-image"; import { ImageOptions as DefaultImageOptions } from "@tiptap/extension-image"; import { ReactNodeViewRenderer } from "@tiptap/react"; -import { ImageUploadPlugin } from "./image-upload"; import { mergeAttributes, Range } from "@tiptap/core"; export interface ImageOptions extends DefaultImageOptions { @@ -10,11 +9,15 @@ export interface ImageOptions extends DefaultImageOptions { export interface ImageAttributes { src?: string; alt?: string; - title?: string; align?: string; attachmentId?: string; size?: number; width?: number; + aspectRatio?: number; + placeholder?: { + id: string; + name: string; + }; } declare module "@tiptap/core" { @@ -90,6 +93,17 @@ export const TiptapImage = Image.extend({ "data-size": attributes.size, }), }, + aspectRatio: { + default: null, + parseHTML: (element) => element.getAttribute("data-aspect-ratio"), + renderHTML: (attributes: ImageAttributes) => ({ + "data-aspect-ratio": attributes.aspectRatio, + }), + }, + placeholder: { + default: null, + rendered: false, + }, }; }, @@ -135,14 +149,9 @@ export const TiptapImage = Image.extend({ }, addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); }, - - addProseMirrorPlugins() { - return [ - ImageUploadPlugin({ - placeholderClass: "image-upload", - }), - ]; - }, }); diff --git a/packages/editor-ext/src/lib/math/math-block.ts b/packages/editor-ext/src/lib/math/math-block.ts index a580596b..cf11e8f8 100644 --- a/packages/editor-ext/src/lib/math/math-block.ts +++ b/packages/editor-ext/src/lib/math/math-block.ts @@ -63,6 +63,9 @@ export const MathBlock = Node.create({ }, addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); }, diff --git a/packages/editor-ext/src/lib/math/math-inline.ts b/packages/editor-ext/src/lib/math/math-inline.ts index 39c1cd49..3de9d291 100644 --- a/packages/editor-ext/src/lib/math/math-inline.ts +++ b/packages/editor-ext/src/lib/math/math-inline.ts @@ -64,6 +64,9 @@ export const MathInline = Node.create({ }, addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); }, diff --git a/packages/editor-ext/src/lib/media-utils.ts b/packages/editor-ext/src/lib/media-utils.ts index f05c4264..02a4a1d1 100644 --- a/packages/editor-ext/src/lib/media-utils.ts +++ b/packages/editor-ext/src/lib/media-utils.ts @@ -1,9 +1,8 @@ -import type { EditorView } from "@tiptap/pm/view"; -import { Transaction } from "@tiptap/pm/state"; +import { Editor } from "@tiptap/core"; export type UploadFn = ( file: File, - view: EditorView, + editor: Editor, pos: number, pageId: string, // only applicable to file attachments @@ -14,16 +13,3 @@ export interface MediaUploadOptions { validateFn?: (file: File, allowMedia?: boolean) => void; onUpload: (file: File, pageId: string) => Promise; } - -export function insertTrailingNode( - tr: Transaction, - pos: number, - view: EditorView, -) { - // create trailing node after decoration - // if decoration is at the last node - const currentDocSize = view.state.doc.content.size; - if (pos + 1 === currentDocSize) { - tr.insert(currentDocSize, view.state.schema.nodes.paragraph.create()); - } -} diff --git a/packages/editor-ext/src/lib/search-and-replace/search-and-replace.ts b/packages/editor-ext/src/lib/search-and-replace/search-and-replace.ts index ca66958f..1ed7632d 100644 --- a/packages/editor-ext/src/lib/search-and-replace/search-and-replace.ts +++ b/packages/editor-ext/src/lib/search-and-replace/search-and-replace.ts @@ -31,6 +31,9 @@ import { import { Node as PMNode, Mark } from "@tiptap/pm/model"; declare module "@tiptap/core" { + interface Storage { + searchAndReplace: SearchAndReplaceStorage; + } interface Commands { search: { /** @@ -184,21 +187,21 @@ const replace = ( if (dispatch) { const tr = state.tr; - + // Get all marks that span the text being replaced const marksSet = new Set(); state.doc.nodesBetween(from, to, (node) => { if (node.isText && node.marks) { - node.marks.forEach(mark => marksSet.add(mark)); + node.marks.forEach((mark) => marksSet.add(mark)); } }); - + const marks = Array.from(marksSet); - + // Delete the old text and insert new text with preserved marks tr.delete(from, to); tr.insert(from, state.schema.text(replaceTerm, marks)); - + dispatch(tr); } }; @@ -215,17 +218,17 @@ const replaceAll = ( // Process replacements in reverse order to avoid position shifting issues for (let i = resultsCopy.length - 1; i >= 0; i -= 1) { const { from, to } = resultsCopy[i]; - + // Get all marks that span the text being replaced const marksSet = new Set(); tr.doc.nodesBetween(from, to, (node) => { if (node.isText && node.marks) { - node.marks.forEach(mark => marksSet.add(mark)); + node.marks.forEach((mark) => marksSet.add(mark)); } }); - + const marks = Array.from(marksSet); - + // Delete and insert with preserved marks tr.delete(from, to); tr.insert(from, tr.doc.type.schema.text(replaceTerm, marks)); @@ -352,10 +355,17 @@ export const SearchAndReplace = Extension.create< // The results will be recalculated by the plugin, but we need to ensure // the index doesn't exceed the new bounds setTimeout(() => { - const newResultsLength = editor.storage.searchAndReplace.results.length; - if (newResultsLength > 0 && editor.storage.searchAndReplace.resultIndex >= newResultsLength) { + const newResultsLength = + editor.storage.searchAndReplace.results.length; + if ( + newResultsLength > 0 && + editor.storage.searchAndReplace.resultIndex >= newResultsLength + ) { // Keep the same position if possible, otherwise go to the last result - editor.storage.searchAndReplace.resultIndex = Math.min(resultIndex, newResultsLength - 1); + editor.storage.searchAndReplace.resultIndex = Math.min( + resultIndex, + newResultsLength - 1, + ); } }, 0); diff --git a/packages/editor-ext/src/lib/shared-storage/index.ts b/packages/editor-ext/src/lib/shared-storage/index.ts new file mode 100644 index 00000000..5b486420 --- /dev/null +++ b/packages/editor-ext/src/lib/shared-storage/index.ts @@ -0,0 +1 @@ +export { SharedStorage } from "./shared-storage"; diff --git a/packages/editor-ext/src/lib/shared-storage/shared-storage.ts b/packages/editor-ext/src/lib/shared-storage/shared-storage.ts new file mode 100644 index 00000000..aa008d45 --- /dev/null +++ b/packages/editor-ext/src/lib/shared-storage/shared-storage.ts @@ -0,0 +1,17 @@ +import { Extension } from "@tiptap/core"; + +declare module "@tiptap/core" { + interface Storage { + shared: Record; + } +} + +const SharedStorage = Extension.create({ + name: "shared", + + addStorage() { + return {}; + }, +}); + +export { SharedStorage }; diff --git a/packages/editor-ext/src/lib/subpages/subpages.ts b/packages/editor-ext/src/lib/subpages/subpages.ts index 59eb9896..617f43ce 100644 --- a/packages/editor-ext/src/lib/subpages/subpages.ts +++ b/packages/editor-ext/src/lib/subpages/subpages.ts @@ -44,7 +44,7 @@ export const Subpages = Node.create({ mergeAttributes( { "data-type": this.name }, this.options.HTMLAttributes, - HTMLAttributes, + HTMLAttributes ), ]; }, @@ -63,6 +63,9 @@ export const Subpages = Node.create({ }, addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); }, }); diff --git a/packages/editor-ext/src/lib/table/cell.ts b/packages/editor-ext/src/lib/table/cell.ts index 63df7dcf..2f693573 100644 --- a/packages/editor-ext/src/lib/table/cell.ts +++ b/packages/editor-ext/src/lib/table/cell.ts @@ -1,4 +1,4 @@ -import { TableCell as TiptapTableCell } from "@tiptap/extension-table-cell"; +import { TableCell as TiptapTableCell } from "@tiptap/extension-table"; export const TableCell = TiptapTableCell.extend({ name: "tableCell", diff --git a/packages/editor-ext/src/lib/table/dnd/dnd-extension.ts b/packages/editor-ext/src/lib/table/dnd/dnd-extension.ts index b4ac2950..1ad57ec1 100644 --- a/packages/editor-ext/src/lib/table/dnd/dnd-extension.ts +++ b/packages/editor-ext/src/lib/table/dnd/dnd-extension.ts @@ -1,7 +1,12 @@ import { Editor, Extension } from "@tiptap/core"; import { PluginKey, Plugin, PluginSpec } from "@tiptap/pm/state"; import { EditorProps, EditorView } from "@tiptap/pm/view"; -import { DraggingDOMs, getDndRelatedDOMs, getHoveringCell, HoveringCellInfo } from "./utils"; +import { + DraggingDOMs, + getDndRelatedDOMs, + getHoveringCell, + HoveringCellInfo, +} from "./utils"; import { getDragOverColumn, getDragOverRow } from "./calc-drag-over"; import { moveColumn, moveRow } from "../utils"; import { PreviewController } from "./preview/preview-controller"; @@ -10,268 +15,302 @@ import { DragHandleController } from "./handle/drag-handle-controller"; import { EmptyImageController } from "./handle/empty-image-controller"; import { AutoScrollController } from "./auto-scroll-controller"; -export const TableDndKey = new PluginKey('table-drag-and-drop') +export const TableDndKey = new PluginKey("table-drag-and-drop"); class TableDragHandlePluginSpec implements PluginSpec { - key = TableDndKey - props: EditorProps> + key = TableDndKey; + props: EditorProps>; - private _colDragHandle: HTMLElement; - private _rowDragHandle: HTMLElement; - private _hoveringCell?: HoveringCellInfo; - private _disposables: (() => void)[] = []; - private _draggingCoords: { x: number; y: number } = { x: 0, y: 0 }; - private _dragging = false; - private _draggingDirection: 'col' | 'row' = 'col'; - private _draggingIndex = -1; - private _droppingIndex = -1; - private _draggingDOMs?: DraggingDOMs | undefined - private _startCoords: { x: number; y: number } = { x: 0, y: 0 }; - private _previewController: PreviewController; - private _dropIndicatorController: DropIndicatorController; - private _dragHandleController: DragHandleController; - private _emptyImageController: EmptyImageController; - private _autoScrollController: AutoScrollController; + private _colDragHandle: HTMLElement; + private _rowDragHandle: HTMLElement; + private _hoveringCell?: HoveringCellInfo; + private _disposables: (() => void)[] = []; + private _draggingCoords: { x: number; y: number } = { x: 0, y: 0 }; + private _dragging = false; + private _draggingDirection: "col" | "row" = "col"; + private _draggingIndex = -1; + private _droppingIndex = -1; + private _draggingDOMs?: DraggingDOMs | undefined; + private _startCoords: { x: number; y: number } = { x: 0, y: 0 }; + private _previewController: PreviewController; + private _dropIndicatorController: DropIndicatorController; + private _dragHandleController: DragHandleController; + private _emptyImageController: EmptyImageController; + private _autoScrollController: AutoScrollController; - constructor(public editor: Editor) { - this.props = { - handleDOMEvents: { - pointerover: this._pointerOver, - } - } + constructor(public editor: Editor) { + this.props = { + handleDOMEvents: { + pointerover: this._pointerOver, + }, + }; - this._dragHandleController = new DragHandleController(); - this._colDragHandle = this._dragHandleController.colDragHandle; - this._rowDragHandle = this._dragHandleController.rowDragHandle; + this._dragHandleController = new DragHandleController(); + this._colDragHandle = this._dragHandleController.colDragHandle; + this._rowDragHandle = this._dragHandleController.rowDragHandle; - this._previewController = new PreviewController(); - this._dropIndicatorController = new DropIndicatorController(); - this._emptyImageController = new EmptyImageController(); + this._previewController = new PreviewController(); + this._dropIndicatorController = new DropIndicatorController(); + this._emptyImageController = new EmptyImageController(); - this._autoScrollController = new AutoScrollController(); + this._autoScrollController = new AutoScrollController(); - this._bindDragEvents(); + this._bindDragEvents(); + } + + view = () => { + const wrapper = this.editor.options.element; + //@ts-ignore + wrapper.appendChild(this._colDragHandle); + //@ts-ignore + wrapper.appendChild(this._rowDragHandle); + //@ts-ignore + wrapper.appendChild(this._previewController.previewRoot); + //@ts-ignore + wrapper.appendChild(this._dropIndicatorController.dropIndicatorRoot); + + return { + update: this.update, + destroy: this.destroy, + }; + }; + + update = () => {}; + + destroy = () => { + if (!this.editor.isDestroyed) return; + this._dragHandleController.destroy(); + this._emptyImageController.destroy(); + this._previewController.destroy(); + this._dropIndicatorController.destroy(); + this._autoScrollController.stop(); + + this._disposables.forEach((disposable) => disposable()); + }; + + private _pointerOver = (view: EditorView, event: PointerEvent) => { + if (this._dragging) return; + + // Don't show drag handles in readonly mode + if (!this.editor.isEditable) { + this._dragHandleController.hide(); + return; } - view = () => { - const wrapper = this.editor.options.element; - wrapper.appendChild(this._colDragHandle) - wrapper.appendChild(this._rowDragHandle) - wrapper.appendChild(this._previewController.previewRoot) - wrapper.appendChild(this._dropIndicatorController.dropIndicatorRoot) + const hoveringCell = getHoveringCell(view, event); + this._hoveringCell = hoveringCell; + if (!hoveringCell) { + this._dragHandleController.hide(); + } else { + this._dragHandleController.show(this.editor, hoveringCell); + } + }; - return { - update: this.update, - destroy: this.destroy, - } + private _onDragColStart = (event: DragEvent) => { + this._onDragStart(event, "col"); + }; + + private _onDraggingCol = (event: DragEvent) => { + const draggingDOMs = this._draggingDOMs; + if (!draggingDOMs) return; + + this._draggingCoords = { x: event.clientX, y: event.clientY }; + this._previewController.onDragging( + draggingDOMs, + this._draggingCoords.x, + this._draggingCoords.y, + "col", + ); + + this._autoScrollController.checkXAutoScroll(event.clientX, draggingDOMs); + + const direction = + this._startCoords.x > this._draggingCoords.x ? "left" : "right"; + const dragOverColumn = getDragOverColumn( + draggingDOMs.table, + this._draggingCoords.x, + ); + if (!dragOverColumn) return; + + const [col, index] = dragOverColumn; + this._droppingIndex = index; + this._dropIndicatorController.onDragging(col, direction, "col"); + }; + + private _onDragRowStart = (event: DragEvent) => { + this._onDragStart(event, "row"); + }; + + private _onDraggingRow = (event: DragEvent) => { + const draggingDOMs = this._draggingDOMs; + if (!draggingDOMs) return; + + this._draggingCoords = { x: event.clientX, y: event.clientY }; + this._previewController.onDragging( + draggingDOMs, + this._draggingCoords.x, + this._draggingCoords.y, + "row", + ); + + this._autoScrollController.checkYAutoScroll(event.clientY); + + const direction = + this._startCoords.y > this._draggingCoords.y ? "up" : "down"; + const dragOverRow = getDragOverRow( + draggingDOMs.table, + this._draggingCoords.y, + ); + if (!dragOverRow) return; + + const [row, index] = dragOverRow; + this._droppingIndex = index; + this._dropIndicatorController.onDragging(row, direction, "row"); + }; + + private _onDragEnd = () => { + this._dragging = false; + this._draggingIndex = -1; + this._droppingIndex = -1; + this._startCoords = { x: 0, y: 0 }; + this._autoScrollController.stop(); + this._dropIndicatorController.onDragEnd(); + this._previewController.onDragEnd(); + }; + + private _bindDragEvents = () => { + this._colDragHandle.addEventListener("dragstart", this._onDragColStart); + this._disposables.push(() => { + this._colDragHandle.removeEventListener( + "dragstart", + this._onDragColStart, + ); + }); + + this._colDragHandle.addEventListener("dragend", this._onDragEnd); + this._disposables.push(() => { + this._colDragHandle.removeEventListener("dragend", this._onDragEnd); + }); + + this._rowDragHandle.addEventListener("dragstart", this._onDragRowStart); + this._disposables.push(() => { + this._rowDragHandle.removeEventListener( + "dragstart", + this._onDragRowStart, + ); + }); + + this._rowDragHandle.addEventListener("dragend", this._onDragEnd); + this._disposables.push(() => { + this._rowDragHandle.removeEventListener("dragend", this._onDragEnd); + }); + + const ownerDocument = this.editor.view.dom?.ownerDocument; + if (ownerDocument) { + // To make `drop` event work, we need to prevent the default behavior of the + // `dragover` event for drop zone. Here we set the whole document as the + // drop zone so that even the mouse moves outside the editor, the `drop` + // event will still be triggered. + ownerDocument.addEventListener("drop", this._onDrop); + ownerDocument.addEventListener("dragover", this._onDrag); + this._disposables.push(() => { + ownerDocument.removeEventListener("drop", this._onDrop); + ownerDocument.removeEventListener("dragover", this._onDrag); + }); + } + }; + + private _onDragStart = (event: DragEvent, type: "col" | "row") => { + const dataTransfer = event.dataTransfer; + if (dataTransfer) { + dataTransfer.effectAllowed = "move"; + this._emptyImageController.hideDragImage(dataTransfer); + } + this._dragging = true; + this._draggingDirection = type; + this._startCoords = { x: event.clientX, y: event.clientY }; + const draggingIndex = + (type === "col" + ? this._hoveringCell?.colIndex + : this._hoveringCell?.rowIndex) ?? 0; + + this._draggingIndex = draggingIndex; + + const relatedDoms = getDndRelatedDOMs( + this.editor.view, + this._hoveringCell?.cellPos, + draggingIndex, + type, + ); + this._draggingDOMs = relatedDoms; + + const index = + type === "col" + ? this._hoveringCell?.colIndex + : this._hoveringCell?.rowIndex; + + this._previewController.onDragStart(relatedDoms, index, type); + this._dropIndicatorController.onDragStart(relatedDoms, type); + }; + + private _onDrag = (event: DragEvent) => { + event.preventDefault(); + if (!this._dragging) return; + if (this._draggingDirection === "col") { + this._onDraggingCol(event); + } else { + this._onDraggingRow(event); + } + }; + + private _onDrop = () => { + if (!this._dragging) return; + const direction = this._draggingDirection; + const from = this._draggingIndex; + const to = this._droppingIndex; + const tr = this.editor.state.tr; + const pos = this.editor.state.selection.from; + + if (direction === "col") { + const canMove = moveColumn({ + tr, + originIndex: from, + targetIndex: to, + select: true, + pos, + }); + if (canMove) { + this.editor.view.dispatch(tr); + } + + return; } - update = () => {} + if (direction === "row") { + const canMove = moveRow({ + tr, + originIndex: from, + targetIndex: to, + select: true, + pos, + }); + if (canMove) { + this.editor.view.dispatch(tr); + } - destroy = () => { - if (!this.editor.isDestroyed) return; - this._dragHandleController.destroy(); - this._emptyImageController.destroy(); - this._previewController.destroy(); - this._dropIndicatorController.destroy(); - this._autoScrollController.stop(); - - this._disposables.forEach(disposable => disposable()); - } - - private _pointerOver = (view: EditorView, event: PointerEvent) => { - if (this._dragging) return; - - // Don't show drag handles in readonly mode - if (!this.editor.isEditable) { - this._dragHandleController.hide(); - return; - } - - const hoveringCell = getHoveringCell(view, event) - this._hoveringCell = hoveringCell; - if (!hoveringCell) { - this._dragHandleController.hide(); - } else { - this._dragHandleController.show(this.editor, hoveringCell); - } - } - - private _onDragColStart = (event: DragEvent) => { - this._onDragStart(event, 'col'); - } - - private _onDraggingCol = (event: DragEvent) => { - const draggingDOMs = this._draggingDOMs; - if (!draggingDOMs) return; - - this._draggingCoords = { x: event.clientX, y: event.clientY }; - this._previewController.onDragging(draggingDOMs, this._draggingCoords.x, this._draggingCoords.y, 'col'); - - this._autoScrollController.checkXAutoScroll(event.clientX, draggingDOMs); - - const direction = this._startCoords.x > this._draggingCoords.x ? 'left' : 'right'; - const dragOverColumn = getDragOverColumn(draggingDOMs.table, this._draggingCoords.x); - if (!dragOverColumn) return; - - const [col, index] = dragOverColumn; - this._droppingIndex = index; - this._dropIndicatorController.onDragging(col, direction, 'col'); - } - - private _onDragRowStart = (event: DragEvent) => { - this._onDragStart(event, 'row'); - } - - private _onDraggingRow = (event: DragEvent) => { - const draggingDOMs = this._draggingDOMs; - if (!draggingDOMs) return; - - this._draggingCoords = { x: event.clientX, y: event.clientY }; - this._previewController.onDragging(draggingDOMs, this._draggingCoords.x, this._draggingCoords.y, 'row'); - - this._autoScrollController.checkYAutoScroll(event.clientY); - - const direction = this._startCoords.y > this._draggingCoords.y ? 'up' : 'down'; - const dragOverRow = getDragOverRow(draggingDOMs.table, this._draggingCoords.y); - if (!dragOverRow) return; - - const [row, index] = dragOverRow; - this._droppingIndex = index; - this._dropIndicatorController.onDragging(row, direction, 'row'); - } - - private _onDragEnd = () => { - this._dragging = false; - this._draggingIndex = -1; - this._droppingIndex = -1; - this._startCoords = { x: 0, y: 0 }; - this._autoScrollController.stop(); - this._dropIndicatorController.onDragEnd(); - this._previewController.onDragEnd(); - } - - private _bindDragEvents = () => { - this._colDragHandle.addEventListener('dragstart', this._onDragColStart); - this._disposables.push(() => { - this._colDragHandle.removeEventListener('dragstart', this._onDragColStart); - }) - - this._colDragHandle.addEventListener('dragend', this._onDragEnd); - this._disposables.push(() => { - this._colDragHandle.removeEventListener('dragend', this._onDragEnd); - }) - - this._rowDragHandle.addEventListener('dragstart', this._onDragRowStart); - this._disposables.push(() => { - this._rowDragHandle.removeEventListener('dragstart', this._onDragRowStart); - }) - - this._rowDragHandle.addEventListener('dragend', this._onDragEnd); - this._disposables.push(() => { - this._rowDragHandle.removeEventListener('dragend', this._onDragEnd); - }) - - const ownerDocument = this.editor.view.dom?.ownerDocument - if (ownerDocument) { - // To make `drop` event work, we need to prevent the default behavior of the - // `dragover` event for drop zone. Here we set the whole document as the - // drop zone so that even the mouse moves outside the editor, the `drop` - // event will still be triggered. - ownerDocument.addEventListener('drop', this._onDrop); - ownerDocument.addEventListener('dragover', this._onDrag); - this._disposables.push(() => { - ownerDocument.removeEventListener('drop', this._onDrop); - ownerDocument.removeEventListener('dragover', this._onDrag); - }); - } - } - - private _onDragStart = (event: DragEvent, type: 'col' | 'row') => { - const dataTransfer = event.dataTransfer; - if (dataTransfer) { - dataTransfer.effectAllowed = 'move'; - this._emptyImageController.hideDragImage(dataTransfer); - } - this._dragging = true; - this._draggingDirection = type; - this._startCoords = { x: event.clientX, y: event.clientY }; - const draggingIndex = (type === 'col' ? this._hoveringCell?.colIndex : this._hoveringCell?.rowIndex) ?? 0; - - this._draggingIndex = draggingIndex; - - const relatedDoms = getDndRelatedDOMs( - this.editor.view, - this._hoveringCell?.cellPos, - draggingIndex, - type - ) - this._draggingDOMs = relatedDoms; - - const index = type === 'col' ? this._hoveringCell?.colIndex : this._hoveringCell?.rowIndex; - - this._previewController.onDragStart(relatedDoms, index, type); - this._dropIndicatorController.onDragStart(relatedDoms, type); - } - - private _onDrag = (event: DragEvent) => { - event.preventDefault() - if (!this._dragging) return; - if (this._draggingDirection === 'col') { - this._onDraggingCol(event); - } else { - this._onDraggingRow(event); - } - } - - private _onDrop = () => { - if (!this._dragging) return; - const direction = this._draggingDirection; - const from = this._draggingIndex; - const to = this._droppingIndex; - const tr = this.editor.state.tr; - const pos = this.editor.state.selection.from; - - if (direction === 'col') { - const canMove = moveColumn({ - tr, - originIndex: from, - targetIndex: to, - select: true, - pos, - }) - if (canMove) { - this.editor.view.dispatch(tr); - } - - return; - } - - if (direction === 'row') { - const canMove = moveRow({ - tr, - originIndex: from, - targetIndex: to, - select: true, - pos, - }) - if (canMove) { - this.editor.view.dispatch(tr); - } - - return; - } + return; } + }; } export const TableDndExtension = Extension.create({ - name: 'table-drag-and-drop', - addProseMirrorPlugins() { - const editor = this.editor + name: "table-drag-and-drop", + addProseMirrorPlugins() { + const editor = this.editor; - const dragHandlePluginSpec = new TableDragHandlePluginSpec(editor) - const dragHandlePlugin = new Plugin(dragHandlePluginSpec) + const dragHandlePluginSpec = new TableDragHandlePluginSpec(editor); + const dragHandlePlugin = new Plugin(dragHandlePluginSpec); - return [dragHandlePlugin] - } -}) + return [dragHandlePlugin]; + }, +}); diff --git a/packages/editor-ext/src/lib/table/header.ts b/packages/editor-ext/src/lib/table/header.ts index 501f089d..77ab02f1 100644 --- a/packages/editor-ext/src/lib/table/header.ts +++ b/packages/editor-ext/src/lib/table/header.ts @@ -1,4 +1,4 @@ -import { TableHeader as TiptapTableHeader } from "@tiptap/extension-table-header"; +import { TableHeader as TiptapTableHeader } from "@tiptap/extension-table"; export const TableHeader = TiptapTableHeader.extend({ name: "tableHeader", diff --git a/packages/editor-ext/src/lib/table/row.ts b/packages/editor-ext/src/lib/table/row.ts index 3aa67dcd..7839afdf 100644 --- a/packages/editor-ext/src/lib/table/row.ts +++ b/packages/editor-ext/src/lib/table/row.ts @@ -1,6 +1,5 @@ -import TiptapTableRow from "@tiptap/extension-table-row"; +import { TableRow as TiptapTableRow } from "@tiptap/extension-table"; export const TableRow = TiptapTableRow.extend({ - allowGapCursor: false, content: "(tableCell | tableHeader)*", }); diff --git a/packages/editor-ext/src/lib/table/table.ts b/packages/editor-ext/src/lib/table/table.ts index 87053832..f1436c28 100644 --- a/packages/editor-ext/src/lib/table/table.ts +++ b/packages/editor-ext/src/lib/table/table.ts @@ -1,4 +1,4 @@ -import Table from "@tiptap/extension-table"; +import { Table } from "@tiptap/extension-table"; import { Editor } from "@tiptap/core"; import { DOMOutputSpec } from "@tiptap/pm/model"; diff --git a/packages/editor-ext/src/lib/unique-id/helpers/findDuplicates.ts b/packages/editor-ext/src/lib/unique-id/helpers/findDuplicates.ts deleted file mode 100644 index d193e8b3..00000000 --- a/packages/editor-ext/src/lib/unique-id/helpers/findDuplicates.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { removeDuplicates } from './removeDuplicates.js' - -/** - * Returns a list of duplicated items within an array. - */ -export function findDuplicates(items: any[]): any[] { - const filtered = items.filter((el, index) => items.indexOf(el) !== index) - const duplicates = removeDuplicates(filtered) - - return duplicates -} diff --git a/packages/editor-ext/src/lib/unique-id/helpers/removeDuplicates.ts b/packages/editor-ext/src/lib/unique-id/helpers/removeDuplicates.ts deleted file mode 100644 index 2bae38fd..00000000 --- a/packages/editor-ext/src/lib/unique-id/helpers/removeDuplicates.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Removes duplicated values within an array. - * Supports numbers, strings and objects. - */ -export function removeDuplicates(array: T[], by = JSON.stringify): T[] { - const seen: Record = {} - - return array.filter(item => { - const key = by(item) - - return Object.prototype.hasOwnProperty.call(seen, key) - ? false - : (seen[key] = true) - }) -} diff --git a/packages/editor-ext/src/lib/unique-id/unique-id.ts b/packages/editor-ext/src/lib/unique-id/unique-id.ts index 6ecf15f0..8436cbd2 100644 --- a/packages/editor-ext/src/lib/unique-id/unique-id.ts +++ b/packages/editor-ext/src/lib/unique-id/unique-id.ts @@ -1,386 +1,11 @@ -import { - combineTransactionSteps, - Extension, - findChildren, - findChildrenInRange, - getChangedRanges, -} from "@tiptap/core"; -import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; -import { Fragment, Slice } from "@tiptap/pm/model"; -import type { Transaction } from "@tiptap/pm/state"; -import { Plugin, PluginKey } from "@tiptap/pm/state"; - -import { findDuplicates } from "./helpers/findDuplicates.js"; import { generateNodeId } from "../utils"; +import { UniqueID as TiptapUniqueID } from "@tiptap/extension-unique-id"; -export type UniqueIDGenerationContext = { - node: ProseMirrorNode; - pos: number; -}; - -export interface UniqueIDOptions { - /** - * The name of the attribute to add the unique ID to. - * @default "id" - */ - attributeName: string; - /** - * The types of nodes to add unique IDs to. - * @default [] - */ - types: string[]; - /** - * The function that generates the unique ID. By default, a UUID v4 is - * generated. However, you can provide your own function to generate the - * unique ID based on the node type and the position. - */ - generateID: (ctx: UniqueIDGenerationContext) => any; - /** - * Ignore some mutations, for example applied from other users through the collaboration plugin. - * - * @default null - */ - filterTransaction: ((transaction: Transaction) => boolean) | null; - /** - * Whether to update the document by adding unique IDs to the nodes. Set this - * property to `false` if the document is in `readonly` mode, is immutable, or - * you don't want it to be modified. - * - * @default true - */ - updateDocument: boolean; -} - -export const UniqueID = Extension.create({ - name: "uniqueID", - - // we’ll set a very high priority to make sure this runs first - // and is compatible with `appendTransaction` hooks of other extensions - priority: 10000, - +export const UniqueID = TiptapUniqueID.extend({ addOptions() { return { - attributeName: "id", - types: [], + ...this.parent?.(), generateID: () => generateNodeId(), - filterTransaction: null, - updateDocument: true, }; }, - - addGlobalAttributes() { - return [ - { - types: this.options.types, - attributes: { - [this.options.attributeName]: { - default: null, - parseHTML: (element) => - element.getAttribute(`data-${this.options.attributeName}`), - renderHTML: (attributes) => { - if (!attributes[this.options.attributeName]) { - return {}; - } - - return { - [`data-${this.options.attributeName}`]: - attributes[this.options.attributeName], - }; - }, - }, - }, - }, - ]; - }, - - // check initial content for missing ids - onCreate() { - if (!this.options.updateDocument) { - return; - } - - const collaboration = this.editor.extensionManager.extensions.find( - (ext) => ext.name === "collaboration", - ); - const collaborationCursor = this.editor.extensionManager.extensions.find( - (ext) => ext.name === "collaborationCursor", - ); - - const collabExtensions = [collaboration, collaborationCursor].filter( - Boolean, - ); - const collab = collabExtensions.find((ext) => ext?.options?.provider); - const provider = collab?.options?.provider; - - const createIds = () => { - const { view, state } = this.editor; - const { tr, doc } = state; - const { types, attributeName, generateID } = this.options; - const nodesWithoutId = findChildren(doc, (node) => { - return ( - types.includes(node.type.name) && node.attrs[attributeName] === null - ); - }); - - nodesWithoutId.forEach(({ node, pos }) => { - tr.setNodeMarkup(pos, undefined, { - ...node.attrs, - [attributeName]: generateID({ node, pos }), - }); - }); - - tr.setMeta("addToHistory", false); - - view.dispatch(tr); - - if (provider) { - provider.off("synced", createIds); - } - }; - - /** - * We need to handle collaboration a bit different here - * because we can't automatically add IDs when the provider is not yet synced - * otherwise we end up with empty paragraphs - */ - if (collab) { - if (!provider) { - return createIds(); - } - - provider.on("synced", createIds); - } else { - return createIds(); - } - }, - - addProseMirrorPlugins() { - if (!this.options.updateDocument) { - return []; - } - - let dragSourceElement: Element | null = null; - let transformPasted = false; - - return [ - new Plugin({ - key: new PluginKey("uniqueID"), - - appendTransaction: (transactions, oldState, newState) => { - const hasDocChanges = - transactions.some((transaction) => transaction.docChanged) && - !oldState.doc.eq(newState.doc); - const filterTransactions = - this.options.filterTransaction && - transactions.some((tr) => !this.options.filterTransaction?.(tr)); - - const isCollabTransaction = transactions.find((tr) => - tr.getMeta("y-sync$"), - ); - - if (isCollabTransaction) { - return; - } - - if (!hasDocChanges || filterTransactions) { - return; - } - - const { tr } = newState; - - const { types, attributeName, generateID } = this.options; - const transform = combineTransactionSteps( - oldState.doc, - transactions as Transaction[], - ); - const { mapping } = transform; - - // get changed ranges based on the old state - const changes = getChangedRanges(transform); - - changes.forEach(({ newRange }) => { - const newNodes = findChildrenInRange( - newState.doc, - newRange, - (node) => { - return types.includes(node.type.name); - }, - ); - - const newIds = newNodes - .map(({ node }) => node.attrs[attributeName]) - .filter((id) => id !== null); - - newNodes.forEach(({ node, pos }, i) => { - // instead of checking `node.attrs[attributeName]` directly - // we look at the current state of the node within `tr.doc`. - // this helps to prevent adding new ids to the same node - // if the node changed multiple times within one transaction - const id = tr.doc.nodeAt(pos)?.attrs[attributeName]; - - if (id === null) { - tr.setNodeMarkup(pos, undefined, { - ...node.attrs, - [attributeName]: generateID({ node, pos }), - }); - - return; - } - - const nextNode = newNodes[i + 1]; - - if (nextNode && node.content.size === 0) { - tr.setNodeMarkup(nextNode.pos, undefined, { - ...nextNode.node.attrs, - [attributeName]: id, - }); - newIds[i + 1] = id; - - if (nextNode.node.attrs[attributeName]) { - return; - } - - const generatedId = generateID({ node, pos }); - - tr.setNodeMarkup(pos, undefined, { - ...node.attrs, - [attributeName]: generatedId, - }); - newIds[i] = generatedId; - - return tr; - } - - const duplicatedNewIds = findDuplicates(newIds); - - // check if the node doesn’t exist in the old state - const { deleted } = mapping.invert().mapResult(pos); - - const newNode = deleted && duplicatedNewIds.includes(id); - - if (newNode) { - tr.setNodeMarkup(pos, undefined, { - ...node.attrs, - [attributeName]: generateID({ node, pos }), - }); - } - }); - }); - - if (!tr.steps.length) { - return; - } - - // `tr.setNodeMarkup` resets the stored marks - // so we'll restore them if they exist - tr.setStoredMarks(newState.tr.storedMarks); - - // Mark this transaction as coming from UniqueID - // to prevent infinite loops with other extensions (e.g., TrailingNode) - tr.setMeta("__uniqueIDTransaction", true); - - return tr; - }, - - // we register a global drag handler to track the current drag source element - view(view) { - const handleDragstart = (event: DragEvent) => { - dragSourceElement = view.dom.parentElement?.contains( - event.target as Element, - ) - ? view.dom.parentElement - : null; - }; - - window.addEventListener("dragstart", handleDragstart); - - return { - destroy() { - window.removeEventListener("dragstart", handleDragstart); - }, - }; - }, - - props: { - // `handleDOMEvents` is called before `transformPasted` - // so we can do some checks before - handleDOMEvents: { - // only create new ids for dropped content - // or dropped content while holding `alt` - // or content is dragged from another editor - drop: (view, event) => { - if ( - dragSourceElement !== view.dom.parentElement || - event.dataTransfer?.effectAllowed === "copyMove" || - event.dataTransfer?.effectAllowed === "copy" - ) { - dragSourceElement = null; - transformPasted = true; - } - - return false; - }, - // always create new ids on pasted content - paste: () => { - transformPasted = true; - - return false; - }, - }, - - // we’ll remove ids for every pasted node - // so we can create a new one within `appendTransaction` - transformPasted: (slice) => { - if (!transformPasted) { - return slice; - } - - const { types, attributeName } = this.options; - const removeId = (fragment: Fragment): Fragment => { - const list: ProseMirrorNode[] = []; - - fragment.forEach((node) => { - // don’t touch text nodes - if (node.isText) { - list.push(node); - - return; - } - - // check for any other child nodes - if (!types.includes(node.type.name)) { - list.push(node.copy(removeId(node.content))); - - return; - } - - // remove id - const nodeWithoutId = node.type.create( - { - ...node.attrs, - [attributeName]: null, - }, - removeId(node.content), - node.marks, - ); - - list.push(nodeWithoutId); - }); - - return Fragment.from(list); - }; - - // reset check - transformPasted = false; - - return new Slice( - removeId(slice.content), - slice.openStart, - slice.openEnd, - ); - }, - }, - }), - ]; - }, }); diff --git a/packages/editor-ext/src/lib/utils.ts b/packages/editor-ext/src/lib/utils.ts index e4e7fda4..350ab3bb 100644 --- a/packages/editor-ext/src/lib/utils.ts +++ b/packages/editor-ext/src/lib/utils.ts @@ -1,6 +1,6 @@ -// @ts-nocheck import { Editor, findParentNode, isTextSelection } from "@tiptap/core"; -import { Selection, Transaction } from "@tiptap/pm/state"; +import { EditorState, Selection, Transaction } from "@tiptap/pm/state"; +import { EditorView } from "@tiptap/pm/view"; import { CellSelection, TableMap } from "@tiptap/pm/tables"; import { Node, ResolvedPos } from "@tiptap/pm/model"; import { sanitizeUrl as braintreeSanitizeUrl } from "@braintree/sanitize-url"; @@ -287,11 +287,7 @@ export const isColumnGripSelected = ({ const nodeDOM = view.nodeDOM(from) as HTMLElement; const node = nodeDOM || domAtPos; - if ( - !editor.isActive("table") || - !node || - isTableSelected(state.selection) - ) { + if (!editor.isActive("table") || !node || isTableSelected(state.selection)) { return false; } @@ -324,11 +320,7 @@ export const isRowGripSelected = ({ const nodeDOM = view.nodeDOM(from) as HTMLElement; const node = nodeDOM || domAtPos; - if ( - !editor.isActive(Table.name) || - !node || - isTableSelected(state.selection) - ) { + if (!editor.isActive("table") || !node || isTableSelected(state.selection)) { return false; } diff --git a/packages/editor-ext/src/lib/video/video-upload.ts b/packages/editor-ext/src/lib/video/video-upload.ts index 1e976ecc..404cf99e 100644 --- a/packages/editor-ext/src/lib/video/video-upload.ts +++ b/packages/editor-ext/src/lib/video/video-upload.ts @@ -1,132 +1,169 @@ -import { type EditorState, Plugin, PluginKey } from "@tiptap/pm/state"; -import { Decoration, DecorationSet } from "@tiptap/pm/view"; -import { - insertTrailingNode, - MediaUploadOptions, - UploadFn, -} from "../media-utils"; +import { MediaUploadOptions, UploadFn } from "../media-utils"; import { IAttachment } from "../types"; +import { generateNodeId } from "../utils"; +import { Node } from "@tiptap/pm/model"; +import { Command } from "@tiptap/core"; -const uploadKey = new PluginKey("video-upload"); +const findVideoNodeByPlaceholderId = ( + doc: Node, + placeholderId: string, +): { node: Node; pos: number } | null => { + let result: { node: Node; pos: number } | null = null; -export const VideoUploadPlugin = ({ - placeholderClass, -}: { - placeholderClass: string; -}) => - new Plugin({ - key: uploadKey, - state: { - init() { - return DecorationSet.empty; - }, - apply(tr, set) { - set = set.map(tr.mapping, tr.doc); - // See if the transaction adds or removes any placeholders - //@-ts-expect-error - not yet sure what the type I need here - const action = tr.getMeta(this); - if (action?.add) { - const { id, pos, src } = action.add; + doc.descendants((node, pos) => { + if (result) return false; - const placeholder = document.createElement("div"); - placeholder.setAttribute("class", "video-placeholder"); - const video = document.createElement("video"); - video.setAttribute("class", placeholderClass); - video.src = src; - placeholder.appendChild(video); - const deco = Decoration.widget(pos + 1, placeholder, { - id, - }); - set = set.add(tr.doc, [deco]); - } else if (action?.remove) { - set = set.remove( - set.find( - undefined, - undefined, - (spec) => spec.id == action.remove.id, - ), - ); - } - return set; - }, - }, - props: { - decorations(state) { - return this.getState(state); - }, - }, + if ( + node.type.name === "video" && + node.attrs.placeholder?.id === placeholderId + ) { + result = { node, pos }; + return false; + } + + return true; }); -function findPlaceholder(state: EditorState, id: {}) { - const decos = uploadKey.getState(state) as DecorationSet; - const found = decos.find(undefined, undefined, (spec) => spec.id == id); - return found.length ? found[0]?.from : null; -} + return result; +}; +const getVideoDimensions = ( + url: string, +): Promise< + { width: number; height: number; aspectRatio: number } | undefined +> => { + return new Promise< + { width: number; height: number; aspectRatio: number } | undefined + >((resolve) => { + const video = document.createElement("video"); -export const handleVideoUpload = + video.preload = "metadata"; + video.onloadedmetadata = () => { + const width = video.videoWidth; + const height = video.videoHeight; + const aspectRatio = height > 0 ? width / height : 1; + + resolve({ width, height, aspectRatio }); + }; + video.onerror = () => { + resolve(undefined); + }; + video.src = url; + }); +}; +const handleVideoUpload = ({ validateFn, onUpload }: MediaUploadOptions): UploadFn => - async (file, view, pos, pageId) => { - // check if the file is an image + async (file, editor, pos, pageId) => { + // check if the file is valid const validated = validateFn?.(file); // @ts-ignore if (!validated) return; - // A fresh object to act as the ID for this upload - const id = {}; - // Replace the selection with a placeholder + const objectUrl = URL.createObjectURL(file); + const videoDimensions = await getVideoDimensions(objectUrl); + const placeholderId = generateNodeId(); + const aspectRatio = videoDimensions.aspectRatio; - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => { - const tr = view.state.tr; - if (!tr.selection.empty) tr.deleteSelection(); + let placeholderInserted = false; - tr.setMeta(uploadKey, { - add: { - id, - pos, - src: reader.result, - }, - }); + editor.storage.shared.videoPreviews = + editor.storage.shared.videoPreviews || {}; + editor.storage.shared.videoPreviews[placeholderId] = objectUrl; - insertTrailingNode(tr, pos, view); - view.dispatch(tr); + const insertPlaceholder = (): Command => { + return ({ tr, state }) => { + const initialPlaceholderNode = state.schema.nodes.video?.create({ + placeholder: { + id: placeholderId, + name: file.name, + }, + aspectRatio, + }); + + if (!initialPlaceholderNode) return false; + + const { parent } = tr.doc.resolve(pos); + const isEmptyTextBlock = parent.isTextblock && !parent.childCount; + + if (isEmptyTextBlock) { + // Replace e.g. empty paragraph with the video + tr.replaceRangeWith(pos - 1, pos + 1, initialPlaceholderNode); + } else { + tr.insert(pos, initialPlaceholderNode); + } + + return true; + }; }; + const replacePlaceholderWithVideo = (attachment: IAttachment): Command => { + return ({ tr }) => { + const { pos: currentPos = null } = + findVideoNodeByPlaceholderId(tr.doc, placeholderId) || {}; - await onUpload(file, pageId).then( - (attachment: IAttachment) => { - const { schema } = view.state; + // If the placeholder is not found or attachment is missing, abort the process + if (currentPos === null || !attachment) return; - const pos = findPlaceholder(view.state, id); - - // If the content around the placeholder has been deleted, drop - // the image - if (pos == null) return; - - // Otherwise, insert it at the placeholder's position, and remove - // the placeholder - - if (!attachment) return; - - const node = schema.nodes.video?.create({ + // Update the placeholder node with the actual video data + tr.setNodeMarkup(currentPos, undefined, { src: `/api/files/${attachment.id}/${attachment.fileName}`, attachmentId: attachment.id, title: attachment.fileName, size: attachment.fileSize, + aspectRatio, }); - if (!node) return; - const transaction = view.state.tr - .replaceWith(pos, pos, node) - .setMeta(uploadKey, { remove: { id } }); - view.dispatch(transaction); - }, - () => { - // Deletes the image placeholder on error - const transaction = view.state.tr - .delete(pos, pos) - .setMeta(uploadKey, { remove: { id } }); - view.dispatch(transaction); - }, - ); + return true; + }; + }; + const removePlaceholder = (): Command => { + return ({ tr }) => { + const { pos: currentPos = null } = + findVideoNodeByPlaceholderId(tr.doc, placeholderId) || {}; + + if (currentPos === null) return false; + + tr.delete(currentPos, currentPos + 2); + + return true; + }; + }; + + // Only show the placeholder if the upload takes more than 250ms + const insertPlaceholderTimeout = setTimeout(() => { + editor.commands.command(insertPlaceholder()); + placeholderInserted = true; + }, 250); + const disposePreviewFile = () => { + URL.revokeObjectURL(objectUrl); + + if (editor.storage.shared.videoPreviews) { + delete editor.storage.shared.videoPreviews[placeholderId]; + } + }; + + try { + const attachment: IAttachment = await onUpload(file, pageId); + + clearTimeout(insertPlaceholderTimeout); + + if (placeholderInserted) { + setTimeout(() => { + editor.commands.command(replacePlaceholderWithVideo(attachment)); + disposePreviewFile(); + }, 100); + } else { + editor + .chain() + .command(insertPlaceholder()) + .command(replacePlaceholderWithVideo(attachment)) + .run(); + disposePreviewFile(); + } + } catch (error) { + clearTimeout(insertPlaceholderTimeout); + + editor.commands.command(removePlaceholder()); + disposePreviewFile(); + } }; + +export { handleVideoUpload }; diff --git a/packages/editor-ext/src/lib/video/video.ts b/packages/editor-ext/src/lib/video/video.ts index 6f28e7c0..31c68f89 100644 --- a/packages/editor-ext/src/lib/video/video.ts +++ b/packages/editor-ext/src/lib/video/video.ts @@ -1,6 +1,5 @@ import { ReactNodeViewRenderer } from "@tiptap/react"; -import { VideoUploadPlugin } from "./video-upload"; -import { mergeAttributes, Range, Node, nodeInputRule } from "@tiptap/core"; +import { Range, Node } from "@tiptap/core"; export interface VideoOptions { view: any; @@ -8,11 +7,15 @@ export interface VideoOptions { } export interface VideoAttributes { src?: string; - title?: string; align?: string; attachmentId?: string; size?: number; width?: number; + aspectRatio?: number; + placeholder?: { + id: string; + name: string; + }; } declare module "@tiptap/core" { @@ -81,15 +84,26 @@ export const TiptapVideo = Node.create({ "data-align": attributes.align, }), }, + aspectRatio: { + default: null, + parseHTML: (element) => element.getAttribute("data-aspect-ratio"), + renderHTML: (attributes: VideoAttributes) => ({ + "data-aspect-ratio": attributes.aspectRatio, + }), + }, + placeholder: { + default: null, + rendered: false, + }, }; }, parseHTML() { return [ { - tag: 'video', + tag: "video", }, - ] + ]; }, renderHTML({ HTMLAttributes }) { @@ -126,14 +140,9 @@ export const TiptapVideo = Node.create({ }, addNodeView() { + // Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191) + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); }, - - addProseMirrorPlugins() { - return [ - VideoUploadPlugin({ - placeholderClass: "video-upload", - }), - ]; - }, }); diff --git a/packages/editor-ext/tsconfig.json b/packages/editor-ext/tsconfig.json index efbfcd61..974fea06 100644 --- a/packages/editor-ext/tsconfig.json +++ b/packages/editor-ext/tsconfig.json @@ -8,6 +8,7 @@ "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "target": "ES2022", + "jsx": "react-jsx", "sourceMap": true, "outDir": "./dist", "baseUrl": "./", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 759fedc6..5b5e98be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,8 +22,8 @@ importers: specifier: ^7.1.0 version: 7.1.0 '@casl/ability': - specifier: ^6.7.5 - version: 6.7.5 + specifier: 6.8.0 + version: 6.8.0 '@docmost/editor-ext': specifier: workspace:* version: link:packages/editor-ext @@ -31,17 +31,17 @@ importers: specifier: ^1.7.3 version: 1.7.3 '@hocuspocus/extension-redis': - specifier: ^2.15.3 - version: 2.15.3(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) + specifier: 3.4.3 + version: 3.4.3(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29) '@hocuspocus/provider': - specifier: ^2.15.3 - version: 2.15.3(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) + specifier: 3.4.3 + version: 3.4.3(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29) '@hocuspocus/server': - specifier: ^2.15.3 - version: 2.15.3(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) + specifier: 3.4.3 + version: 3.4.3(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29) '@hocuspocus/transformer': - specifier: ^2.15.3 - version: 2.15.3(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27) + specifier: 3.4.3 + version: 3.4.3(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29))(yjs@13.6.29) '@joplin/turndown': specifier: ^4.0.74 version: 4.0.74 @@ -52,107 +52,86 @@ importers: specifier: 1.1.0 version: 1.1.0 '@tiptap/core': - specifier: 2.27.1 - version: 2.27.1(@tiptap/pm@2.27.1) + specifier: 3.17.1 + version: 3.17.1(@tiptap/pm@3.17.1) '@tiptap/extension-code-block': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-code-block-lowlight': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/extension-code-block@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(highlight.js@11.11.1)(lowlight@3.3.0) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@tiptap/extension-collaboration': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)) - '@tiptap/extension-collaboration-cursor': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@tiptap/y-tiptap@3.0.1(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29))(yjs@13.6.29) + '@tiptap/extension-collaboration-caret': + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@tiptap/y-tiptap@3.0.1(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29)) '@tiptap/extension-color': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/extension-text-style@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))) + specifier: 3.17.1 + version: 3.17.1(@tiptap/extension-text-style@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))) '@tiptap/extension-document': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) '@tiptap/extension-heading': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) '@tiptap/extension-highlight': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) '@tiptap/extension-history': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) + specifier: 3.17.1 + version: 3.17.1(@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) '@tiptap/extension-image': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) '@tiptap/extension-link': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-list-item': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-list-keymap': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-list': + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@tiptap/extension-placeholder': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) + specifier: 3.17.1 + version: 3.17.1(@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) '@tiptap/extension-subscript': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@tiptap/extension-superscript': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@tiptap/extension-table': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-table-cell': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-table-header': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-table-row': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-task-item': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-task-list': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@tiptap/extension-text': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) '@tiptap/extension-text-align': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) '@tiptap/extension-text-style': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) '@tiptap/extension-typography': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-underline': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-unique-id': + specifier: ^3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@tiptap/extension-youtube': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) '@tiptap/html': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(happy-dom@20.1.0) '@tiptap/pm': - specifier: 2.27.1 - version: 2.27.1 + specifier: 3.17.1 + version: 3.17.1 '@tiptap/react': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: 3.17.1 + version: 3.17.1(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tiptap/starter-kit': - specifier: 2.27.1 - version: 2.27.1 + specifier: 3.17.1 + version: 3.17.1 '@tiptap/suggestion': - specifier: 2.27.1 - version: 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) + specifier: 3.17.1 + version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) '@types/qrcode': specifier: ^1.5.5 version: 1.5.5 @@ -171,6 +150,12 @@ importers: fractional-indexing-jittered: specifier: ^1.0.0 version: 1.0.0 + highlight.js: + specifier: ^11.11.1 + version: 11.11.1 + image-dimensions: + specifier: ^2.5.0 + version: 2.5.0 ioredis: specifier: ^5.4.1 version: 5.4.1 @@ -194,13 +179,13 @@ importers: version: 11.1.0 y-indexeddb: specifier: ^9.0.12 - version: 9.0.12(yjs@13.6.27) + version: 9.0.12(yjs@13.6.29) y-prosemirror: specifier: 1.3.7 - version: 1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) + version: 1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29) yjs: - specifier: ^13.6.27 - version: 13.6.27 + specifier: ^13.6.29 + version: 13.6.29 devDependencies: '@nx/js': specifier: 20.4.5 @@ -225,7 +210,7 @@ importers: dependencies: '@casl/react': specifier: ^4.0.0 - version: 4.0.0(@casl/ability@6.7.5)(react@18.3.1) + version: 4.0.0(@casl/ability@6.8.0)(react@18.3.1) '@docmost/editor-ext': specifier: workspace:* version: link:../../packages/editor-ext @@ -265,9 +250,6 @@ importers: '@tanstack/react-query': specifier: ^5.90.17 version: 5.90.17(react@18.3.1) - '@tiptap/extension-character-count': - specifier: ^2.27.1 - version: 2.27.2(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) alfaaz: specifier: ^1.1.0 version: 1.1.0 @@ -355,9 +337,6 @@ importers: socket.io-client: specifier: ^4.8.3 version: 4.8.3 - tippy.js: - specifier: ^6.3.7 - version: 6.3.7 tiptap-extension-global-drag-handle: specifier: ^0.1.18 version: 0.1.18 @@ -614,15 +593,15 @@ importers: pgvector: specifier: ^0.2.1 version: 0.2.1 - postgres: - specifier: ^3.4.8 - version: 3.4.8 pino-http: specifier: ^11.0.0 version: 11.0.0 pino-pretty: specifier: ^13.1.3 version: 13.1.3 + postgres: + specifier: ^3.4.8 + version: 3.4.8 postmark: specifier: ^4.0.5 version: 4.0.5 @@ -1868,8 +1847,8 @@ packages: '@braintree/sanitize-url@7.1.1': resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==} - '@casl/ability@6.7.5': - resolution: {integrity: sha512-NaOHPi9JMn8Kesh+GRkjNKAYkl4q8qMFAlqw7w2yrE+cBQZSbV9GkBGKvgzs3CdzEc5Yl1cn3JwDxxbBN5gjog==} + '@casl/ability@6.8.0': + resolution: {integrity: sha512-Ipt4mzI4gSgnomFdaPjaLgY2MWuXqAEZLrU6qqWBB7khGiBBuuEp6ytYDnq09bRXqcjaeeHiaCvCGFbBA2SpvA==} '@casl/react@4.0.0': resolution: {integrity: sha512-ovmI4JfNw7TfVVV+XhAJ//gXgMEkkPJU6YBWFVFZGa8Oikdh8Qxr/sdXcqj71QWEHAGN7aSKMtBE0MZylPUVsg==} @@ -2398,32 +2377,32 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} - '@hocuspocus/common@2.15.3': - resolution: {integrity: sha512-Rzh1HF0a2o/tf90A3w2XNdXd9Ym3aQzMDfD3lAUONCX9B9QOdqdyiORrj6M25QEaJrEIbXFy8LtAFcL0wRdWzA==} + '@hocuspocus/common@3.4.3': + resolution: {integrity: sha512-wnBBO9sWcVAoUPEXN1qO+zk3HaEF9VTemxB6kjuuH6e1dHnD0v12m4P4X1wiZVhmMIX/PMl/fu3MGtYWQJz8gA==} - '@hocuspocus/extension-redis@2.15.3': - resolution: {integrity: sha512-gKeiiuQcAoRYb+QK9vyIczRrjNy8NW6ky+oyVv7raMcaizfFxeWP3TaAHPyC2pjGKfXsqN2m3YM0GbBGZfMiCg==} + '@hocuspocus/extension-redis@3.4.3': + resolution: {integrity: sha512-r64Vpgk6tt0VZaQPEo1dQuyur2ozr243ncDcDM+4gFPuV8ZRUjL1rvaJTidb2HCcAW2zjfwshNxw4+OixeksBA==} peerDependencies: y-protocols: ^1.0.6 yjs: ^13.6.8 - '@hocuspocus/provider@2.15.3': - resolution: {integrity: sha512-oadN05m+KL4ylNKVo5YspNG4MXkT2Y+FUFzrgigpQeTjQibkPUwCNmUnkUxMgrGRgxb+O0lJCfirFIJMxedctA==} + '@hocuspocus/provider@3.4.3': + resolution: {integrity: sha512-zt+UgVXGsEQrqnDZgavc2PT9yKJjmVjV+5YxvhlmFVFLVORqawT4l601aKmLPhvyK97un4ZApZ5rso8iO6crWg==} peerDependencies: y-protocols: ^1.0.6 yjs: ^13.6.8 - '@hocuspocus/server@2.15.3': - resolution: {integrity: sha512-Ju4ty4/7JtmvivcP7gKReOLf8KrFwN7Yx/5VhXYh4TRULy4kSo2fsDVUaluPp0neZa6PbVhizJuzlOim73IEbQ==} + '@hocuspocus/server@3.4.3': + resolution: {integrity: sha512-a9bqAXUMBo9YBeuzqNf9C3eVbu1RIWUrtmFMGq+ZssQr3Jugt/5PCkZskgqhJNvPkyTARHcUtN80j/SDLylZmg==} peerDependencies: y-protocols: ^1.0.6 yjs: ^13.6.8 - '@hocuspocus/transformer@2.15.3': - resolution: {integrity: sha512-01UU3iZA9MF+MmB2SweKyC70nBM/FkBt3veWiAMoXPiegUG47wY8QO2MksBD/ucnz7C5M/0oAsTjqrx+j0ynIw==} + '@hocuspocus/transformer@3.4.3': + resolution: {integrity: sha512-jQZiqFGCvGQJLgE0nHZ4TdpEJlI7WkM8CKA1wLcs0beVs0kNXg32lykGckjveJwwJuJ/hieMqIEqj9POxTWPEw==} peerDependencies: - '@tiptap/core': ^2.6.4 - '@tiptap/pm': ^2.6.4 + '@tiptap/core': ^3.0.1 + '@tiptap/pm': ^3.0.1 y-prosemirror: 1.3.7 yjs: ^13.6.8 @@ -3392,9 +3371,6 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@popperjs/core@2.11.8': - resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@radix-ui/primitive@1.1.1': resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} @@ -3905,6 +3881,12 @@ packages: '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + '@sesamecare-oss/redlock@1.4.0': + resolution: {integrity: sha512-2z589R+yxKLN4CgKxP1oN4dsg6Y548SE4bVYam/R0kHk7Q9VrQ9l66q+k1ehhSLLY4or9hcchuF9/MhuuZdjJg==} + engines: {node: '>=16'} + peerDependencies: + ioredis: '>=5' + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -4240,271 +4222,261 @@ packages: peerDependencies: react: ^18 || ^19 - '@tiptap/core@2.27.1': - resolution: {integrity: sha512-nkerkl8syHj44ZzAB7oA2GPmmZINKBKCa79FuNvmGJrJ4qyZwlkDzszud23YteFZEytbc87kVd/fP76ROS6sLg==} + '@tiptap/core@3.17.1': + resolution: {integrity: sha512-f8hB9MzXqsuXoF9qXEDEH5Fb3VgwhEFMBMfk9EKN88l5adri6oM8mt2XOWVxVVssjpEW0177zXSLPKWzoS/vrw==} peerDependencies: - '@tiptap/pm': ^2.7.0 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-blockquote@2.27.1': - resolution: {integrity: sha512-QrUX3muElDrNjKM3nqCSAtm3H3pT33c6ON8kwRiQboOAjT/9D57Cs7XEVY7r6rMaJPeKztrRUrNVF9w/w/6B0A==} + '@tiptap/extension-blockquote@3.17.1': + resolution: {integrity: sha512-X4jU/fllJQ8QbjCHUafU4QIHBobyXP3yGBoOcXxUaKlWbLvUs0SQTREM3n6/86m2YyAxwTPG1cn3Xypf42DMAQ==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-bold@2.27.1': - resolution: {integrity: sha512-g4l4p892x/r7mhea8syp3fNYODxsDrimgouQ+q4DKXIgQmm5+uNhyuEPexP3I8TFNXqQ4DlMNFoM9yCqk97etQ==} + '@tiptap/extension-bold@3.17.1': + resolution: {integrity: sha512-PZmrljcVBziJkQDXT/QJv4ESxVVQ0iRH+ruTzPda56Kk4h2310cSXGjI33W7rlCikGPoBAAjY/inujm46YB4bw==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-bubble-menu@2.27.1': - resolution: {integrity: sha512-ki1R27VsSvY2tT9Q2DIlcATwLOoEjf5DsN+5sExarQ8S/ZxT/tvIjRxB8Dx7lb2a818W5f/NER26YchGtmHfpg==} + '@tiptap/extension-bubble-menu@3.17.1': + resolution: {integrity: sha512-z3E8biLiWlzZJwNHnB6j/ZyBdFrJmpl1lqKHc72JqahUHZvidZHdCOYssvR3fc6IaI7MXV13XY1DXUdFbatnaw==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-bullet-list@2.27.1': - resolution: {integrity: sha512-5FmnfXkJ76wN4EbJNzBhAlmQxho8yEMIJLchTGmXdsD/n/tsyVVtewnQYaIOj/Z7naaGySTGDmjVtLgTuQ+Sxw==} + '@tiptap/extension-bullet-list@3.17.1': + resolution: {integrity: sha512-2zw17XHruOJQK7ntLVq0PmOLajFhvQ+U4/qTfJnV3VOsHkm+2GPAksFe7I7+X0XmSmDru0pcT339Yywx/6Aykw==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/extension-list': ^3.17.1 - '@tiptap/extension-character-count@2.27.2': - resolution: {integrity: sha512-EcQRIvbLbMDDzo7uFqXYgh1CfgedS9sYX4BllktY2OlXLPdNpwo9t8WMK/a7soESNv0Le3WZ5pNvnNhv7Z2YdA==} + '@tiptap/extension-code-block@3.17.1': + resolution: {integrity: sha512-h4i+Y/cN7nMi0Tmlp6V1w4dI7NTqrUFSr1W/vMqnq4vn+c6jvm35KubKU5ry/1qQp8KfndDA02BtVQiMx6DmpA==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-code-block-lowlight@2.27.1': - resolution: {integrity: sha512-Ijg9724uX/l4LXLELEeztZIgg+bDE/jJCkgS1+mavkRA/qtidpQkHo7L/Ry22fmj/ktCtZLjPXE5JAPAoRU6zA==} + '@tiptap/extension-code@3.17.1': + resolution: {integrity: sha512-4W0x1ZZqSnIVzQV0/b5VR0bktef2HykH5I/Czzir9yqoZ5zV2cLrMVuLvdFNgRIckU60tQLmHrfKWLF50OY0ew==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/extension-code-block': ^2.7.0 - '@tiptap/pm': ^2.7.0 - highlight.js: ^11 - lowlight: ^2 || ^3 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-code-block@2.27.1': - resolution: {integrity: sha512-wCI5VIOfSAdkenCWFvh4m8FFCJ51EOK+CUmOC/PWUjyo2Dgn8QC8HMi015q8XF7886T0KvYVVoqxmxJSUDAYNg==} + '@tiptap/extension-collaboration-caret@3.17.1': + resolution: {integrity: sha512-tYzujG4ABacSbjd8QOqMt1IP3QdCmAEBHP2faF4SeFauaP6Nto88JvTiZVCHad0BBwiNrj4UPGZSujcNQiLjTA==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 + '@tiptap/y-tiptap': ^3.0.2 - '@tiptap/extension-code@2.27.1': - resolution: {integrity: sha512-i65wUGJevzBTIIUBHBc1ggVa27bgemvGl/tY1/89fEuS/0Xmre+OQjw8rCtSLevoHSiYYLgLRlvjtUSUhE4kgg==} + '@tiptap/extension-collaboration@3.17.1': + resolution: {integrity: sha512-4ehZ5LL7M3nFfcogCG7bWRHIR/8366i1vz5i0PaaoArJga2N5sXnWcuBGXG7ykC8owbgrfL3agFxjHlhTl4sNw==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 + '@tiptap/y-tiptap': ^3.0.2 + yjs: ^13 - '@tiptap/extension-collaboration-cursor@2.27.1': - resolution: {integrity: sha512-k4vLA1QeGM4FfO9BMKw8O0Nxv2zDrsUpnP7wKAJp/zmr2lHbQX86cO+SGEy+kcRtPeIp6Y4Phytp6F+1HMjbLA==} + '@tiptap/extension-color@3.17.1': + resolution: {integrity: sha512-QVlzpzGB+QcZgHgvFMRPckZutpkOLzNmZzhupNA7G2CMeeoCwZOJeZkyd3zvtAnRZkf7FrQBO123On30pJt7TA==} peerDependencies: - '@tiptap/core': ^2.7.0 - y-prosemirror: 1.3.7 + '@tiptap/extension-text-style': ^3.17.1 - '@tiptap/extension-collaboration@2.27.1': - resolution: {integrity: sha512-fR35dIYDHM9870zl2sHaA2ytSVcjASv8Nfnb1Mgslt/F3Lqsu9TOv/oJWi9nYBvjjrfK0RNaoGFVH7p2z7FR3w==} + '@tiptap/extension-document@3.17.1': + resolution: {integrity: sha512-F7Q5HoAU383HWFa6AXZQ5N6t6lTJzVjYM8z93XrtH/2GzDFwy1UmDSrsXqvgznedBLAOgCNVTNh9PjXpLoOUbg==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - y-prosemirror: 1.3.7 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-color@2.27.1': - resolution: {integrity: sha512-raYRsdG2tZvVvY1LV/VTZnDG44Y0xRBwo5CZEat0OUqdx34dfvCtYm8HIOTyWBwr7OOW+yR4O1Vc2zFkmfthZw==} + '@tiptap/extension-dropcursor@3.17.1': + resolution: {integrity: sha512-EKJYPb7OSk3p9mX1SmHt4ccw89w1P1d55hC8aPtZJ6jxAUd5MSuVwvEEVz7LGldUZD9HZz9WFQ0Sv9U73Bpkmw==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/extension-text-style': ^2.7.0 + '@tiptap/extensions': ^3.17.1 - '@tiptap/extension-document@2.27.1': - resolution: {integrity: sha512-NtJzJY7Q/6XWjpOm5OXKrnEaofrcc1XOTYlo/SaTwl8k2bZo918Vl0IDBWhPVDsUN7kx767uHwbtuQZ+9I82hA==} + '@tiptap/extension-floating-menu@3.17.1': + resolution: {integrity: sha512-zYkoYsxp+cZ8tBDODm4E8hnSaMTdDWKJuCQWY2Ep14oMPkAkSJr8sCLL1tOnNSAnhGwLJQtRLkZ41nvUEP6xKA==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-dropcursor@2.27.1': - resolution: {integrity: sha512-3MBQRGHHZ0by3OT0CWbLKS7J3PH9PpobrXjmIR7kr0nde7+bHqxXiVNuuIf501oKU9rnEUSedipSHkLYGkmfsA==} + '@tiptap/extension-gapcursor@3.17.1': + resolution: {integrity: sha512-xItmJZTi+Z6UbLBhpBBL9RZDNbDXf+ntWVgblAmxtpyEyNh5k5tkM6IP9SJRhk92uVfnFpH9qkGo66a537I8QA==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/extensions': ^3.17.1 - '@tiptap/extension-floating-menu@2.27.1': - resolution: {integrity: sha512-nUk/8DbiXO69l6FDwkWso94BTf52IBoWALo+YGWT6o+FO6cI9LbUGghEX2CdmQYXCvSvwvISF2jXeLQWNZvPZQ==} + '@tiptap/extension-hard-break@3.17.1': + resolution: {integrity: sha512-28FZPUho1Q2AB3ka5SVEVib5f9dMKbE1kewLZeRIOQ5FuFNholGIPL5X1tKcwGW7G3A7Y0fGxeNmIZJ3hrqhzA==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-gapcursor@2.27.1': - resolution: {integrity: sha512-A9e1jr+jGhDWzNSXtIO6PYVYhf5j/udjbZwMja+wCE/3KvZU9V3IrnGKz1xNW+2Q2BDOe1QO7j5uVL9ElR6nTA==} + '@tiptap/extension-heading@3.17.1': + resolution: {integrity: sha512-rT+Su/YnHdlikg8f78t6RXlc1sVSfp7B0fdJdtFgS2e6BBYJQoDMp5L9nt54RR9Yy953aDW2sko7NArUCb8log==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-hard-break@2.27.1': - resolution: {integrity: sha512-W4hHa4Io6QCTwpyTlN6UAvqMIQ7t56kIUByZhyY9EWrg/+JpbfpxE1kXFLPB4ZGgwBknFOw+e4bJ1j3oAbTJFw==} + '@tiptap/extension-highlight@3.17.1': + resolution: {integrity: sha512-I4EdBhPVzJd4ECMI9kP0NE4aG4Numd46jy/AqeZyf3dqVgCxRyAbSyU7oy4aXUnsojYODrKKG6+djm07KgOGoQ==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-heading@2.27.1': - resolution: {integrity: sha512-6xoC7igZlW1EmnQ5WVH9IL7P1nCQb3bBUaIDLvk7LbweEogcTUECI4Xg1vxMOVmj9tlDe1I4BsgfcKpB5KEsZw==} + '@tiptap/extension-history@3.17.1': + resolution: {integrity: sha512-YHW4HP9ovZ/zqc1u3+cDdAY/LITaMQNRnX5foLsDFLV5FU+zqonYo2CqDkVwaQs9UfCp9PM0ehZzxMI8hc58oA==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/extensions': ^3.17.1 - '@tiptap/extension-highlight@2.27.1': - resolution: {integrity: sha512-ntuYX09tvHQE/R/8WbTOxbFuQhRr2jhTkKz/gLwDD2o8IhccSy3f0nm+mVmVamKQnbsBBbLohojd5IGOnX9f1A==} + '@tiptap/extension-horizontal-rule@3.17.1': + resolution: {integrity: sha512-CHG6LBtxV+3qj5EcCRVlpvSW5udKD6KbnXIGhP+Tvy+OabLGzO4HNxz3+duDE0pMR4eKX1libsnqffj0vq7mnQ==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-history@2.27.1': - resolution: {integrity: sha512-K8PHC9gegSAt0wzSlsd4aUpoEyIJYOmVVeyniHr1P1mIblW1KYEDbRGbDlrLALTyUEfMcBhdIm8zrB9X2Nihvg==} + '@tiptap/extension-image@3.17.1': + resolution: {integrity: sha512-VbSSZ//5qijm8F0lQQ6K+DGnZgjLKYQY2c+O56QNEoN8BaCFrJlsVgF1ttrSRUmoG4XBNIMlAS07kZXvMZQr0g==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-horizontal-rule@2.27.1': - resolution: {integrity: sha512-WxXWGEEsqDmGIF2o9av+3r9Qje4CKrqrpeQY6aRO5bxvWX9AabQCfasepayBok6uwtvNzh3Xpsn9zbbSk09dNA==} + '@tiptap/extension-italic@3.17.1': + resolution: {integrity: sha512-unfRLmvf680Y0UkBToUcrDkSEKO/wAjd3nQ7CNPMfAc8m+ZMReXkcgLpeVvnDEiHNsJ0PlYSW7a45tnQD9HQdg==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-image@2.27.1': - resolution: {integrity: sha512-wu3vMKDYWJwKS6Hrw5PPCKBO2RxyHNeFLiA/uDErEV7axzNpievK/U9DyaDXmtK3K/h1XzJAJz19X+2d/pY68w==} + '@tiptap/extension-link@3.17.1': + resolution: {integrity: sha512-5kdN7vms5hMXtjiophUkgvzy8dNGvGSmol1Sawh30TEPrgXc93Ayj7YyGZlbimInKZcD8q+Od/FFc+wkrof3nA==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-italic@2.27.1': - resolution: {integrity: sha512-rcm0GyniWW0UhcNI9+1eIK64GqWQLyIIrWGINslvqSUoBc+WkfocLvv4CMpRkzKlfsAxwVIBuH2eLxHKDtAREA==} + '@tiptap/extension-list-item@3.17.1': + resolution: {integrity: sha512-Qjj4oIa44cTX0E6aw/4+wleqX21t5jMDxeSqP5uQ8Q3IdD1GoR5+yo+41XAHELaeZOXLHLkAIbzIxik3pOqO8w==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/extension-list': ^3.17.1 - '@tiptap/extension-link@2.27.1': - resolution: {integrity: sha512-cCwWPZsnVh9MXnGOqSIRXPPuUixRDK8eMN2TvqwbxUBb1TU7b/HtNvfMU4tAOqAuMRJ0aJkFuf3eB0Gi8LVb1g==} + '@tiptap/extension-list-keymap@3.17.1': + resolution: {integrity: sha512-zRidxbkJNe/j3nZpOGLnPeVdyciUM8MM+NHhxcjVKoNDA+/zEBfjXJ1dKC4UBsnSr4AS/3SCWBYHGXOoSqdUaA==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/extension-list': ^3.17.1 - '@tiptap/extension-list-item@2.27.1': - resolution: {integrity: sha512-dtsxvtzxfwOJP6dKGf0vb2MJAoDF2NxoiWzpq0XTvo7NGGYUHfuHjX07Zp0dYqb4seaDXjwsi5BIQUOp3+WMFQ==} + '@tiptap/extension-list@3.17.1': + resolution: {integrity: sha512-LHKIxmXe5Me+vJZKhiwMBGHlApaBIAduNMRUpm5mkY7ER/m96zKR0VqrJd4LjVVH2iDvck5h1Ka4396MHWlKNg==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-list-keymap@2.27.1': - resolution: {integrity: sha512-k7+Ulz9B1NjqwU6NEFYkJh4rGGT/iRVaCBa8OL9YYrVS3H44LgEqUCEbRu6TeEq4XXrLwueQpkkyl4Evi15lAQ==} + '@tiptap/extension-ordered-list@3.17.1': + resolution: {integrity: sha512-pahAXbVajqX0Y51Zge9jKZlCtPV1oiq5Fbzs7gHF80KICIKf44i/AsUvfdJyT2N5/8kZrAMQHEiU/UgTMrhM3w==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/extension-list': ^3.17.1 - '@tiptap/extension-ordered-list@2.27.1': - resolution: {integrity: sha512-U1/sWxc2TciozQsZjH35temyidYUjvroHj3PUPzPyh19w2fwKh1NSbFybWuoYs6jS3XnMSwnM2vF52tOwvfEmA==} + '@tiptap/extension-paragraph@3.17.1': + resolution: {integrity: sha512-Vl+xAlINaPtX8XTPvPmeveYMEIMLs8gA7ItcKpyyo4cCzAfVCY3DKuWzOkQGUf7DKrhyJQZhpgLNMaq+h5sTSw==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-paragraph@2.27.1': - resolution: {integrity: sha512-R3QdrHcUdFAsdsn2UAIvhY0yWyHjqGyP/Rv8RRdN0OyFiTKtwTPqreKMHKJOflgX4sMJl/OpHTpNG1Kaf7Lo2A==} + '@tiptap/extension-placeholder@3.17.1': + resolution: {integrity: sha512-cE8Rij5/1t4KnWE7GaDewhBek9DKNB+97yrxyggMegILg6v195hOmOkRZkyfnFMYZoBDlrfSAtX9wBvbZBqIsg==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/extensions': ^3.17.1 - '@tiptap/extension-placeholder@2.27.1': - resolution: {integrity: sha512-UbXaibHHFE+lOTlw/vs3jPzBoj1sAfbXuTAhXChjgYIcTTY5Cr6yxwcymLcimbQ79gf04Xkua2FCN3YsJxIFmw==} + '@tiptap/extension-strike@3.17.1': + resolution: {integrity: sha512-c6fS6YIhxoU55etlJgM0Xqker+jn7I1KC7GVu6ljmda8I00K3/lOLZgvFUNPmgp8EJWtyTctj+3D3D+PaZaFAA==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-strike@2.27.1': - resolution: {integrity: sha512-S9I//K8KPgfFTC5I5lorClzXk0g4lrAv9y5qHzHO5EOWt7AFl0YTg2oN8NKSIBK4bHRnPIrjJJKv+dDFnUp5jQ==} + '@tiptap/extension-subscript@3.17.1': + resolution: {integrity: sha512-+y/sl1d+TcecX2n1r6ZTjBmY3D6cfqAW86iKsvudCFSpp9SQk85RaumPzELOXWOjz9g0mtfUnXifrLYF3dS+vA==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-subscript@2.27.1': - resolution: {integrity: sha512-n2jTaYriewwz3ES1o6Wt/OwREvPwi97n+yEsJ7i31wiuxGTdCP31eAuppC6DvixEvDt3/rZMZcNp8Ah9crlbnw==} + '@tiptap/extension-superscript@3.17.1': + resolution: {integrity: sha512-FKt+lI1ocFRW0EFla9EuO71aLQINpkC/wt9zxWnJJnfPIWfxYlsTSFJLjLkVungTmwfeCnoCVcXnZ0dSKDnoGg==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-superscript@2.27.1': - resolution: {integrity: sha512-zTYOD7k3txm21rjeYHsf/VIpBe9IvVfNHSNayyY/JOgyQ/fW40cgX0gADNoT2ayAtRes4TvpcUYdgF9vC5bkJw==} + '@tiptap/extension-table@3.17.1': + resolution: {integrity: sha512-FuAMdmM330tHJUYT5IV2ooFRqtXf+0D8llcE9nIQQCXKL4J0pfGSOIm40LVpunYgx2pV8SSCL51qTBuEmR84tQ==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-table-cell@2.27.1': - resolution: {integrity: sha512-VowNmz1kub2qfntWkU8jGA6DoCl9xjJBWSypuQIeiN/IRId3BMrJodT26pTNJ3ChDMtYaanWaUvYqckRxgTC2A==} + '@tiptap/extension-text-align@3.17.1': + resolution: {integrity: sha512-CyJbZf823dqPZ/1zwRsza5pk/NQwFZwILdFYLVkV88I4+Ua9YVztI9kmwTB6dJyuKT4kTc7nhQHdaa957alGZQ==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-table-header@2.27.1': - resolution: {integrity: sha512-lSbGB6kBp/sTVzAWl4v7v7ztL5XU3aTdlS7FhfGjpdsxd4zPKYG8kx+Uxgq25W9/BlCbnqHnO0poAMfOlspDQw==} + '@tiptap/extension-text-style@3.17.1': + resolution: {integrity: sha512-TCMsEU92r/TfZkN8AKo/WIcJ1uNq/5NiZxloq5drF1HXxDDjwliurgwBw3OTGUlKQmer0N9hV0AAePY/G+5Akw==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-table-row@2.27.1': - resolution: {integrity: sha512-3xtlmZ6NWDi5a42gK0qQQTeBUpJ2j1o7qyXTFkhQaJAeIFEqsemgSRhgXZxbwSmQQZsPJ/86KWBNVkT0FaRFDw==} + '@tiptap/extension-text@3.17.1': + resolution: {integrity: sha512-rGml96vokQbvPB+w6L3+WKyYJWwqELaLdFUr1WMgg+py5uNYGJYAExYNAbDb5biWJBrX9GgMlCaNeiJj849L1w==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-table@2.27.1': - resolution: {integrity: sha512-iOoOo0vYFzAogAZlw36DgmFfNM5vOkLqnApm81soO/YWpqtKAvBn+TMY4ss4OMDsOefUzBa6xqOJ0gJR5ZygjA==} + '@tiptap/extension-typography@3.17.1': + resolution: {integrity: sha512-bEocTrK/gryk3VtthC9Ca03p2kutVIIFnDkVW6iOG8PgQWEspuQRgqE8yPnHxY8pBBDWxiaBzcGTSrp+3U9d5A==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-task-item@2.27.1': - resolution: {integrity: sha512-vaEtdos+9jApD6yRfD6F/xShikiZFHi7I0nswAmGKT/kE1wmHCUxme8OFMe7642e2OK0lqgHsUaOLxP/0nZJ5A==} + '@tiptap/extension-underline@3.17.1': + resolution: {integrity: sha512-6RdBzmkg6DYs0EqPyoqLGkISXzCnPqM/q3A6nh3EmFmORcIDfuNmcidvA6EImebK8KQGmtZKsRhQSnK4CNQ39g==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-task-list@2.27.1': - resolution: {integrity: sha512-KRlYOZ6kdURvAspUrLVsC7mLkVW2DYhpj+7QxH7gVDZuAuoPUEmpJVcBVPq7GhPF9PccaRLru+n1Ege5VqvZ+Q==} + '@tiptap/extension-unique-id@3.17.1': + resolution: {integrity: sha512-R+lXBIaEmJ23rJMMLs6dPIVMhpv+TU8vEFVtpbMoOl/yfoc9Pvr6Q0EgLnRDX6l4yAekenem4KmGeG9CmuoskA==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-text-align@2.27.1': - resolution: {integrity: sha512-D7dLPk7y5mDn9ZNANQ4K2gCq4vy+Emm5AdeWOGzNeqJsYrBotiQYXd9rb1QYjdup2kzAoKduMTUXV92ujo5cEg==} + '@tiptap/extension-youtube@3.17.1': + resolution: {integrity: sha512-AarpN4vI/S6jPMuLuFGEFLgdoasGiUW+rGLj+jH/0Of6l27nKRN00MTm/fD/62qjR6At3Rd7Xsue/GuXdmDUWw==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 - '@tiptap/extension-text-style@2.27.1': - resolution: {integrity: sha512-NagQ9qLk0Ril83gfrk+C65SvTqPjL3WVnLF2arsEVnCrxcx3uDOvdJW67f/K5HEwEHsoqJ4Zq9Irco/koXrOXA==} + '@tiptap/extensions@3.17.1': + resolution: {integrity: sha512-aQ4WA5bdRpv9yPQ6rRdiqwlMZ1eJw1HyEaNPQhOr2HVhQ0EqSDIOEXF4ymCveGAHxXbxNvtQ+4t1ymQEikGfXA==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 - '@tiptap/extension-text@2.27.1': - resolution: {integrity: sha512-a4GCT+GZ9tUwl82F4CEum9/+WsuW0/De9Be/NqrMmi7eNfAwbUTbLCTFU0gEvv25WMHCoUzaeNk/qGmzeVPJ1Q==} + '@tiptap/html@3.17.1': + resolution: {integrity: sha512-fLb2fo8+3oQ+5FTx5IGZvLI5+VLgN9BM6pHaO1+IrwqQ5w2RBFIGp8M946asBPkxJ74EtzHqFKJpVFtaY2CcpA==} peerDependencies: - '@tiptap/core': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 + happy-dom: ^20.0.2 - '@tiptap/extension-typography@2.27.1': - resolution: {integrity: sha512-jAZU5IuWH9CtZlolQ1gRhV+bT75s19SXjadQwkk18gMMiapcaIVVTxUDWY6ycv9ge4cjRoaP3lqBviW3cGqhOA==} + '@tiptap/pm@3.17.1': + resolution: {integrity: sha512-UyVLkN8axV/zop6Se2DCBJRu5DM21X0XEQvwEC5P/vk8eC9OcQZ3FLtxeYy2ZjpAZUzBGLw0/BGsmEip/n7olw==} + + '@tiptap/react@3.17.1': + resolution: {integrity: sha512-Hn/pIP3HG9xYnhI3iGrfVhgQhfIdOaEBSxOFzJ37patqSOlIoP5aZH/b2HZ4vgo5DdRlV56q7WtRC+vLIw4Neg==} peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-underline@2.27.1': - resolution: {integrity: sha512-fPTmfJFAQWg1O/os1pYSPVdtvly6eW/w5sDofG7pre+bdQUN+8s1cZYelSuj/ltNVioRaB2Ws7tvNgnHL0aAJQ==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/extension-youtube@2.27.1': - resolution: {integrity: sha512-HjBBgE0Zbch/S2UP0YYQXervfoBd4Trw0dYmlZbX9cXJcZv+QFx0vsPGmjAGlqzXf9Y8ZioWm8fso4u6AsUfTw==} - peerDependencies: - '@tiptap/core': ^2.7.0 - - '@tiptap/html@2.27.1': - resolution: {integrity: sha512-5iPo36g4nbBVoEVBQb6my4KNpNzu38gtCFXIIlAJdAZQvPs+XC8TkrnGK/G4UGpwBXCuQjSQm0iyn4znmQPDsw==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 - - '@tiptap/pm@2.27.1': - resolution: {integrity: sha512-ijKo3+kIjALthYsnBmkRXAuw2Tswd9gd7BUR5OMfIcjGp8v576vKxOxrRfuYiUM78GPt//P0sVc1WV82H5N0PQ==} - - '@tiptap/react@2.27.1': - resolution: {integrity: sha512-leJximSjYJuhLJQv9azOP9R7w6zuxVgKOHYT4w83Gte7GhWMpNL6xRWzld280vyq/YW/cSYjPb/8ESEOgKNBdQ==} - peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + '@types/react-dom': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tiptap/starter-kit@2.27.1': - resolution: {integrity: sha512-uQQlP0Nmn9eq19qm8YoOeloEfmcGbPpB1cujq54Q6nPgxaBozR7rE7tXbFTinxRW2+Hr7XyNWhpjB7DMNkdU2Q==} + '@tiptap/starter-kit@3.17.1': + resolution: {integrity: sha512-3vBGqag9mwuQoWTrfQlULtHeoFs7k/2Q8CREf3Y79hv2fqAXTvTOKlWYPSgZhiGVMp6Dti7BDiE9Y1QpvAat2g==} - '@tiptap/suggestion@2.27.1': - resolution: {integrity: sha512-yTy75ZMYgVWM18cl7YxLqMJ7TorQTGysSd1aKmBA9qd8uzYlvLMmHKE9qBDxM9HXODBz1DA/BLLm9esv2enmFw==} + '@tiptap/suggestion@3.17.1': + resolution: {integrity: sha512-a188uVYjlLsUiwK3Ki7KsaWVWC0u28KsqGEAqCk9ECYmtVY99Hrb+rcAwGpMjA7tn8WAwThOxiLISoMdpuqXwg==} peerDependencies: - '@tiptap/core': ^2.7.0 - '@tiptap/pm': ^2.7.0 + '@tiptap/core': ^3.17.1 + '@tiptap/pm': ^3.17.1 + + '@tiptap/y-tiptap@3.0.1': + resolution: {integrity: sha512-F3hj5X77ckmyIywbCQpKgyX3xKra2/acJPWaV5R9wqp0cUPBmm62FYbkQ6HaqxH1VhCkUhhAZcDSQjbjj7tnWw==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + prosemirror-model: ^1.7.1 + prosemirror-state: ^1.2.3 + prosemirror-view: ^1.9.10 + y-protocols: ^1.0.1 + yjs: ^13.5.38 '@tokenizer/inflate@0.4.1': resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} @@ -5269,6 +5241,9 @@ packages: async-lock@1.4.1: resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} + async-mutex@0.5.0: + resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} + async@3.2.5: resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} @@ -5375,9 +5350,6 @@ packages: bluebird@3.4.7: resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} - bluebird@3.7.2: - resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -6089,10 +6061,6 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - denque@1.5.1: - resolution: {integrity: sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==} - engines: {node: '>=0.10'} - denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} @@ -6272,10 +6240,6 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - entities@5.0.0: - resolution: {integrity: sha512-BeJFvFRJddxobhvEdm5GqHzRV/X+ACeuw0/BuuxsCh1EUZcAIz8+kYmBp/LrQuloy6K1f3a0M7+IhmZ7QnkISA==} - engines: {node: '>=0.12'} - entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} @@ -6507,6 +6471,10 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-equals@5.3.4: + resolution: {integrity: sha512-d+yU9iNQbbC098NOuMlAIth/g+owbpX/uuOkH/DQcC2fMMyjOlX292Op29DrUKq388m4UUyOdWakUH/msGypOg==} + engines: {node: '>=6.0.0'} + fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} @@ -6931,6 +6899,11 @@ packages: image-blob-reduce@3.0.1: resolution: {integrity: sha512-/VmmWgIryG/wcn4TVrV7cC4mlfUC/oyiKIfSg5eVM3Ten/c1c34RJhMYKCWTnoSMHSqXLt3tsrBR4Q2HInvN+Q==} + image-dimensions@2.5.0: + resolution: {integrity: sha512-CKZPHjAEtSg9lBV9eER0bhNn/yrY7cFEQEhkwjLhqLY+Na8lcP1pEyWsaGMGc8t2qbKWA/tuqbhFQpOKGN72Yw==} + engines: {node: '>=18'} + hasBin: true + image-size@0.5.5: resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} engines: {node: '>=0.10.0'} @@ -6983,10 +6956,6 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} - ioredis@4.28.5: - resolution: {integrity: sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==} - engines: {node: '>=6'} - ioredis@5.4.1: resolution: {integrity: sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==} engines: {node: '>=12.22.0'} @@ -7618,11 +7587,6 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lib0@0.2.108: - resolution: {integrity: sha512-+3eK/B0SqYoZiQu9fNk4VEc6EX8cb0Li96tPGKgugzoGj/OdRdREtuTLvUW+mtinoB2mFiJjSqOJBIaMkAGhxQ==} - engines: {node: '>=16'} - hasBin: true - lib0@0.2.114: resolution: {integrity: sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==} engines: {node: '>=16'} @@ -7687,9 +7651,6 @@ packages: lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} - lodash.flatten@4.4.0: - resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} - lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -8305,10 +8266,6 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - p-map@2.1.0: - resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} - engines: {node: '>=6'} - p-queue@6.6.2: resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} engines: {node: '>=8'} @@ -8725,8 +8682,8 @@ packages: prosemirror-schema-basic@1.2.3: resolution: {integrity: sha512-h+H0OQwZVqMon1PNn0AG9cTfx513zgIG2DY00eJ00Yvgb3UD+GQ/VlWW5rcaxacpCGT1Yx8nuhwXk4+QbXUfJA==} - prosemirror-schema-list@1.4.1: - resolution: {integrity: sha512-jbDyaP/6AFfDfu70VzySsD75Om2t3sXTOdl5+31Wlxlg62td1haUpty/ybajSfJ1pkGadlOfwQq9kgW5IMo1Rg==} + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} prosemirror-state@1.4.3: resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==} @@ -8996,9 +8953,6 @@ packages: resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} engines: {node: '>= 0.10'} - redis-commands@1.7.0: - resolution: {integrity: sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==} - redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -9007,10 +8961,6 @@ packages: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} - redlock@4.2.0: - resolution: {integrity: sha512-j+oQlG+dOwcetUt2WJWttu4CZVeRzUrcVcISFmEmfyuwCVSJ93rDT7YSgg7H7rnxwoRyk/jU46kycVka5tW7jA==} - engines: {node: '>=8.0.0'} - redux@4.2.1: resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} @@ -9566,9 +9516,6 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tippy.js@6.3.7: - resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} - tiptap-extension-global-drag-handle@0.1.18: resolution: {integrity: sha512-jwFuy1K8DP3a4bFy76Hpc63w1Sil0B7uZ3mvhQomVvUFCU787Lg2FowNhn7NFzeyok761qY2VG+PZ/FDthWUdg==} @@ -9918,6 +9865,11 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + utf8-byte-length@1.0.4: resolution: {integrity: sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==} @@ -10303,8 +10255,8 @@ packages: resolution: {integrity: sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==} engines: {node: '>=12'} - yjs@13.6.27: - resolution: {integrity: sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==} + yjs@13.6.29: + resolution: {integrity: sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} yn@3.1.1: @@ -10323,10 +10275,6 @@ packages: resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} engines: {node: '>=18'} - zeed-dom@0.15.1: - resolution: {integrity: sha512-dtZ0aQSFyZmoJS0m06/xBN1SazUBPL5HpzlAcs/KcRW0rzadYw12deQBjeMhGKMMeGEp7bA9vmikMLaO4exBcg==} - engines: {node: '>=14.13.1'} - zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -12240,13 +12188,13 @@ snapshots: '@braintree/sanitize-url@7.1.1': {} - '@casl/ability@6.7.5': + '@casl/ability@6.8.0': dependencies: '@ucast/mongo2js': 1.3.4 - '@casl/react@4.0.0(@casl/ability@6.7.5)(react@18.3.1)': + '@casl/react@4.0.0(@casl/ability@6.8.0)(react@18.3.1)': dependencies: - '@casl/ability': 6.7.5 + '@casl/ability': 6.8.0 react: 18.3.1 '@cfworker/json-schema@4.1.1': {} @@ -12703,58 +12651,57 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@hocuspocus/common@2.15.3': + '@hocuspocus/common@3.4.3': dependencies: lib0: 0.2.114 - '@hocuspocus/extension-redis@2.15.3(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)': + '@hocuspocus/extension-redis@3.4.3(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29)': dependencies: - '@hocuspocus/server': 2.15.3(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) - ioredis: 4.28.5 + '@hocuspocus/server': 3.4.3(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29) + '@sesamecare-oss/redlock': 1.4.0(ioredis@5.8.2) + ioredis: 5.8.2 kleur: 4.1.5 lodash.debounce: 4.0.8 - redlock: 4.2.0 - uuid: 11.1.0 - y-protocols: 1.0.6(yjs@13.6.27) - yjs: 13.6.27 + y-protocols: 1.0.6(yjs@13.6.29) + yjs: 13.6.29 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - '@hocuspocus/provider@2.15.3(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)': + '@hocuspocus/provider@3.4.3(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29)': dependencies: - '@hocuspocus/common': 2.15.3 + '@hocuspocus/common': 3.4.3 '@lifeomic/attempt': 3.0.3 lib0: 0.2.114 ws: 8.19.0 - y-protocols: 1.0.6(yjs@13.6.27) - yjs: 13.6.27 + y-protocols: 1.0.6(yjs@13.6.29) + yjs: 13.6.29 transitivePeerDependencies: - bufferutil - utf-8-validate - '@hocuspocus/server@2.15.3(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27)': + '@hocuspocus/server@3.4.3(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29)': dependencies: - '@hocuspocus/common': 2.15.3 + '@hocuspocus/common': 3.4.3 async-lock: 1.4.1 + async-mutex: 0.5.0 kleur: 4.1.5 lib0: 0.2.114 - uuid: 11.1.0 ws: 8.19.0 - y-protocols: 1.0.6(yjs@13.6.27) - yjs: 13.6.27 + y-protocols: 1.0.6(yjs@13.6.29) + yjs: 13.6.29 transitivePeerDependencies: - bufferutil - utf-8-validate - '@hocuspocus/transformer@2.15.3(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))(yjs@13.6.27)': + '@hocuspocus/transformer@3.4.3(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29))(yjs@13.6.29)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 - '@tiptap/starter-kit': 2.27.1 - y-prosemirror: 1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) - yjs: 13.6.27 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 + '@tiptap/starter-kit': 3.17.1 + y-prosemirror: 1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29) + yjs: 13.6.29 '@humanfs/core@0.19.1': {} @@ -13777,8 +13724,6 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@popperjs/core@2.11.8': {} - '@radix-ui/primitive@1.1.1': {} '@radix-ui/react-arrow@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -14202,6 +14147,10 @@ snapshots: domhandler: 5.0.3 selderee: 0.11.0 + '@sesamecare-oss/redlock@1.4.0(ioredis@5.8.2)': + dependencies: + ioredis: 5.8.2 + '@sinclair/typebox@0.27.8': {} '@sindresorhus/slugify@1.1.0': @@ -14652,212 +14601,195 @@ snapshots: '@tanstack/query-core': 5.90.17 react: 18.3.1 - '@tiptap/core@2.27.1(@tiptap/pm@2.27.1)': + '@tiptap/core@3.17.1(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/pm': 2.27.1 + '@tiptap/pm': 3.17.1 - '@tiptap/extension-blockquote@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-blockquote@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-bold@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-bold@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-bubble-menu@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-bubble-menu@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 - tippy.js: 6.3.7 + '@floating-ui/dom': 1.7.4 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 + optional: true - '@tiptap/extension-bullet-list@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-bullet-list@3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-character-count@2.27.2(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-code-block@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 - '@tiptap/extension-code-block-lowlight@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/extension-code-block@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(highlight.js@11.11.1)(lowlight@3.3.0)': + '@tiptap/extension-code@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/extension-code-block': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 - highlight.js: 11.11.1 - lowlight: 3.3.0 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-code-block@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-collaboration-caret@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@tiptap/y-tiptap@3.0.1(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 + '@tiptap/y-tiptap': 3.0.1(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29) - '@tiptap/extension-code@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-collaboration@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@tiptap/y-tiptap@3.0.1(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29))(yjs@13.6.29)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 + '@tiptap/y-tiptap': 3.0.1(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29) + yjs: 13.6.29 - '@tiptap/extension-collaboration-cursor@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))': + '@tiptap/extension-color@3.17.1(@tiptap/extension-text-style@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - y-prosemirror: 1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) + '@tiptap/extension-text-style': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) - '@tiptap/extension-collaboration@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27))': + '@tiptap/extension-document@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 - y-prosemirror: 1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-color@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/extension-text-style@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)))': + '@tiptap/extension-dropcursor@3.17.1(@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/extension-text-style': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) + '@tiptap/extensions': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-document@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-floating-menu@3.17.1(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@floating-ui/dom': 1.7.3 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 + optional: true - '@tiptap/extension-dropcursor@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-gapcursor@3.17.1(@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/extensions': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-floating-menu@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-hard-break@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 - tippy.js: 6.3.7 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-gapcursor@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-heading@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-hard-break@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-highlight@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-heading@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-history@3.17.1(@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/extensions': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-highlight@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-horizontal-rule@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 - '@tiptap/extension-history@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-image@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-horizontal-rule@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-italic@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-image@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-link@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - - '@tiptap/extension-italic@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': - dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - - '@tiptap/extension-link@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': - dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 linkifyjs: 4.3.2 - '@tiptap/extension-list-item@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-list-item@3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-list-keymap@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-list-keymap@3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-ordered-list@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 - '@tiptap/extension-paragraph@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-ordered-list@3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-placeholder@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-paragraph@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-strike@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-placeholder@3.17.1(@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/extensions': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) - '@tiptap/extension-subscript@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-strike@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-superscript@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-subscript@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 - '@tiptap/extension-table-cell@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-superscript@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 - '@tiptap/extension-table-header@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-table@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 - '@tiptap/extension-table-row@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-text-align@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-table@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-text-style@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-task-item@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/extension-text@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-task-list@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-typography@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-text-align@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-underline@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-text-style@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-unique-id@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 + uuid: 10.0.0 - '@tiptap/extension-text@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extension-youtube@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) - '@tiptap/extension-typography@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 - '@tiptap/extension-underline@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': + '@tiptap/html@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(happy-dom@20.1.0)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 + happy-dom: 20.1.0 - '@tiptap/extension-youtube@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))': - dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - - '@tiptap/html@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': - dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 - zeed-dom: 0.15.1 - - '@tiptap/pm@2.27.1': + '@tiptap/pm@3.17.1': dependencies: prosemirror-changeset: 2.3.1 prosemirror-collab: 1.3.1 @@ -14871,53 +14803,70 @@ snapshots: prosemirror-menu: 1.2.4 prosemirror-model: 1.25.1 prosemirror-schema-basic: 1.2.3 - prosemirror-schema-list: 1.4.1 + prosemirror-schema-list: 1.5.1 prosemirror-state: 1.4.3 prosemirror-tables: 1.7.1 prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0) prosemirror-transform: 1.10.4 prosemirror-view: 1.40.0 - '@tiptap/react@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tiptap/react@3.17.1(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/extension-bubble-menu': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-floating-menu': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 '@types/use-sync-external-store': 0.0.6 - fast-deep-equal: 3.1.3 + fast-equals: 5.3.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - use-sync-external-store: 1.2.2(react@18.3.1) + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@tiptap/extension-bubble-menu': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-floating-menu': 3.17.1(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + transitivePeerDependencies: + - '@floating-ui/dom' - '@tiptap/starter-kit@2.27.1': + '@tiptap/starter-kit@3.17.1': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/extension-blockquote': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-bold': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-bullet-list': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-code': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-code-block': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-document': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-dropcursor': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-gapcursor': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-hard-break': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-heading': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-history': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-horizontal-rule': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1) - '@tiptap/extension-italic': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-list-item': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-ordered-list': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-paragraph': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-strike': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-text': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/extension-text-style': 2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1)) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/extension-blockquote': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-bold': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-bullet-list': 3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) + '@tiptap/extension-code': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-code-block': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-document': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-dropcursor': 3.17.1(@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) + '@tiptap/extension-gapcursor': 3.17.1(@tiptap/extensions@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) + '@tiptap/extension-hard-break': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-heading': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-horizontal-rule': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-italic': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-link': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/extension-list-item': 3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) + '@tiptap/extension-list-keymap': 3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) + '@tiptap/extension-ordered-list': 3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)) + '@tiptap/extension-paragraph': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-strike': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-text': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extension-underline': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)) + '@tiptap/extensions': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 - '@tiptap/suggestion@2.27.1(@tiptap/core@2.27.1(@tiptap/pm@2.27.1))(@tiptap/pm@2.27.1)': + '@tiptap/suggestion@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)': dependencies: - '@tiptap/core': 2.27.1(@tiptap/pm@2.27.1) - '@tiptap/pm': 2.27.1 + '@tiptap/core': 3.17.1(@tiptap/pm@3.17.1) + '@tiptap/pm': 3.17.1 + + '@tiptap/y-tiptap@3.0.1(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29)': + dependencies: + lib0: 0.2.114 + prosemirror-model: 1.25.1 + prosemirror-state: 1.4.3 + prosemirror-view: 1.40.0 + y-protocols: 1.0.6(yjs@13.6.29) + yjs: 13.6.29 '@tokenizer/inflate@0.4.1': dependencies: @@ -15844,6 +15793,10 @@ snapshots: async-lock@1.4.1: {} + async-mutex@0.5.0: + dependencies: + tslib: 2.8.1 + async@3.2.5: {} asynckit@0.4.0: {} @@ -16026,8 +15979,6 @@ snapshots: bluebird@3.4.7: {} - bluebird@3.7.2: {} - boolbase@1.0.0: {} bowser@2.11.0: {} @@ -16778,8 +16729,6 @@ snapshots: delayed-stream@1.0.0: {} - denque@1.5.1: {} - denque@2.1.0: {} depd@2.0.0: {} @@ -16967,8 +16916,6 @@ snapshots: entities@4.5.0: {} - entities@5.0.0: {} - entities@6.0.1: {} env-paths@2.2.1: {} @@ -17356,6 +17303,8 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-equals@5.3.4: {} + fast-glob@3.3.2: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -17834,6 +17783,8 @@ snapshots: dependencies: pica: 7.1.1 + image-dimensions@2.5.0: {} + image-size@0.5.5: optional: true @@ -17878,22 +17829,6 @@ snapshots: dependencies: loose-envify: 1.4.0 - ioredis@4.28.5: - dependencies: - cluster-key-slot: 1.1.2 - debug: 4.4.1 - denque: 1.5.1 - lodash.defaults: 4.2.0 - lodash.flatten: 4.4.0 - lodash.isarguments: 3.1.0 - p-map: 2.1.0 - redis-commands: 1.7.0 - redis-errors: 1.2.0 - redis-parser: 3.0.0 - standard-as-callback: 2.1.0 - transitivePeerDependencies: - - supports-color - ioredis@5.4.1: dependencies: '@ioredis/commands': 1.2.0 @@ -18744,10 +18679,6 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lib0@0.2.108: - dependencies: - isomorphic.js: 0.2.5 - lib0@0.2.114: dependencies: isomorphic.js: 0.2.5 @@ -18804,8 +18735,6 @@ snapshots: lodash.defaults@4.2.0: {} - lodash.flatten@4.4.0: {} - lodash.includes@4.3.0: {} lodash.isarguments@3.1.0: {} @@ -19547,8 +19476,6 @@ snapshots: dependencies: p-limit: 3.1.0 - p-map@2.1.0: {} - p-queue@6.6.2: dependencies: eventemitter3: 4.0.7 @@ -20007,7 +19934,7 @@ snapshots: dependencies: prosemirror-model: 1.25.1 - prosemirror-schema-list@1.4.1: + prosemirror-schema-list@1.5.1: dependencies: prosemirror-model: 1.25.1 prosemirror-state: 1.4.3 @@ -20312,18 +20239,12 @@ snapshots: dependencies: resolve: 1.22.8 - redis-commands@1.7.0: {} - redis-errors@1.2.0: {} redis-parser@3.0.0: dependencies: redis-errors: 1.2.0 - redlock@4.2.0: - dependencies: - bluebird: 3.7.2 - redux@4.2.1: dependencies: '@babel/runtime': 7.25.6 @@ -20967,10 +20888,6 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - tippy.js@6.3.7: - dependencies: - '@popperjs/core': 2.11.8 - tiptap-extension-global-drag-handle@0.1.18: {} tldts-core@6.1.72: {} @@ -21311,6 +21228,10 @@ snapshots: dependencies: react: 18.3.1 + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + utf8-byte-length@1.0.4: {} util-deprecate@1.0.2: {} @@ -21588,24 +21509,24 @@ snapshots: xtend@4.0.2: optional: true - y-indexeddb@9.0.12(yjs@13.6.27): + y-indexeddb@9.0.12(yjs@13.6.29): dependencies: lib0: 0.2.88 - yjs: 13.6.27 + yjs: 13.6.29 - y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27): + y-prosemirror@1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29): dependencies: lib0: 0.2.114 prosemirror-model: 1.25.1 prosemirror-state: 1.4.3 prosemirror-view: 1.40.0 - y-protocols: 1.0.6(yjs@13.6.27) - yjs: 13.6.27 + y-protocols: 1.0.6(yjs@13.6.29) + yjs: 13.6.29 - y-protocols@1.0.6(yjs@13.6.27): + y-protocols@1.0.6(yjs@13.6.29): dependencies: lib0: 0.2.114 - yjs: 13.6.27 + yjs: 13.6.29 y18n@4.0.3: {} @@ -21655,9 +21576,9 @@ snapshots: buffer-crc32: 0.2.13 pend: 1.2.0 - yjs@13.6.27: + yjs@13.6.29: dependencies: - lib0: 0.2.108 + lib0: 0.2.114 yn@3.1.1: {} @@ -21667,11 +21588,6 @@ snapshots: yoctocolors-cjs@2.1.2: {} - zeed-dom@0.15.1: - dependencies: - css-what: 6.1.0 - entities: 5.0.0 - zod@3.25.76: {} zod@4.3.5: {}