Compare commits

..

5 Commits

Author SHA1 Message Date
Philipinho b22dfa27c2 limit 2026-02-17 03:01:21 +00:00
Philipinho 0cb565c77a pagination 2026-02-17 02:52:26 +00:00
Philipinho 2f99cd8f7d label type 2026-02-17 02:42:30 +00:00
Philipinho 75f7f9b296 WIP 2026-02-17 02:32:00 +00:00
Philipinho ca173a9c98 feat: labels (WIP) 2026-02-17 02:01:32 +00:00
65 changed files with 1284 additions and 3129 deletions
@@ -274,7 +274,6 @@
"Add row below": "Add row below",
"Delete table": "Delete table",
"Info": "Info",
"Note": "Note",
"Success": "Success",
"Warning": "Warning",
"Danger": "Danger",
@@ -364,15 +363,6 @@
"Heading {{level}}": "Heading {{level}}",
"Toggle title": "Toggle title",
"Write anything. Enter \"/\" for commands": "Write anything. Enter \"/\" for commands",
"Write...": "Write...",
"Column count": "Column count",
"{{count}} Columns": "{{count}} Columns",
"Equal columns": "Equal columns",
"Left sidebar": "Left sidebar",
"Right sidebar": "Right sidebar",
"Wide center": "Wide center",
"Left wide": "Left wide",
"Right wide": "Right wide",
"Names do not match": "Names do not match",
"Today, {{time}}": "Today, {{time}}",
"Yesterday, {{time}}": "Yesterday, {{time}}",
@@ -1,27 +0,0 @@
import { rem } from "@mantine/core";
type Props = {
size?: number | string;
stroke?: number;
};
export function IconColumns4({ size = 24, stroke = 2 }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={rem(size)}
height={rem(size)}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={stroke}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 4a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v16a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1v-16" />
<path d="M7.5 3v18" />
<path d="M12 3v18" />
<path d="M16.5 3v18" />
</svg>
);
}
@@ -1,28 +0,0 @@
import { rem } from "@mantine/core";
type Props = {
size?: number | string;
stroke?: number;
};
export function IconColumns5({ size = 24, stroke = 2 }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={rem(size)}
height={rem(size)}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={stroke}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 4a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v16a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1v-16" />
<path d="M6.6 3v18" />
<path d="M10.2 3v18" />
<path d="M13.8 3v18" />
<path d="M17.4 3v18" />
</svg>
);
}
@@ -7,7 +7,7 @@ import { notifications } from "@mantine/notifications";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { IAuthProvider } from "@/ee/security/types/security.types";
import APP_ROUTE, { getPostLoginRedirect } from "@/lib/app-route";
import APP_ROUTE from "@/lib/app-route";
import { ldapLogin } from "@/ee/security/services/ldap-auth-service";
const formSchema = z.object({
@@ -59,13 +59,13 @@ export function LdapLoginModal({
// Handle MFA like the regular login
if (response?.userHasMfa) {
onClose();
navigate(APP_ROUTE.AUTH.MFA_CHALLENGE + window.location.search);
navigate(APP_ROUTE.AUTH.MFA_CHALLENGE);
} else if (response?.requiresMfaSetup) {
onClose();
navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED + window.location.search);
navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED);
} else {
onClose();
navigate(getPostLoginRedirect());
navigate(APP_ROUTE.HOME);
}
} catch (err: any) {
setIsLoading(false);
@@ -18,7 +18,7 @@ import { useNavigate } from "react-router-dom";
import { notifications } from "@mantine/notifications";
import classes from "./mfa-challenge.module.css";
import { verifyMfa } from "@/ee/mfa";
import APP_ROUTE, { getPostLoginRedirect } from "@/lib/app-route";
import APP_ROUTE from "@/lib/app-route";
import { useTranslation } from "react-i18next";
import * as z from "zod";
import { MfaBackupCodeInput } from "./mfa-backup-code-input";
@@ -53,7 +53,7 @@ export function MfaChallenge() {
setIsLoading(true);
try {
await verifyMfa(values.code);
navigate(getPostLoginRedirect());
navigate(APP_ROUTE.HOME);
} catch (error: any) {
setIsLoading(false);
notifications.show({
@@ -3,7 +3,7 @@ import { Container, Paper, Title, Text, Alert, Stack } from "@mantine/core";
import { IconAlertCircle } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { MfaSetupModal } from "@/ee/mfa";
import APP_ROUTE, { getPostLoginRedirect } from "@/lib/app-route.ts";
import APP_ROUTE from "@/lib/app-route.ts";
import { useNavigate } from "react-router-dom";
export default function MfaSetupRequired() {
@@ -11,7 +11,7 @@ export default function MfaSetupRequired() {
const navigate = useNavigate();
const handleSetupComplete = () => {
navigate(getPostLoginRedirect());
navigate(APP_ROUTE.HOME);
};
return (
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import APP_ROUTE, { getPostLoginRedirect } from "@/lib/app-route";
import APP_ROUTE from "@/lib/app-route";
import { validateMfaAccess } from "@/ee/mfa";
export function useMfaPageProtection() {
@@ -13,10 +13,8 @@ export function useMfaPageProtection() {
const checkAccess = async () => {
const result = await validateMfaAccess();
const search = location.search;
if (!result.valid) {
navigate(APP_ROUTE.AUTH.LOGIN + search);
navigate(APP_ROUTE.AUTH.LOGIN);
return;
}
@@ -28,17 +26,17 @@ export function useMfaPageProtection() {
if (result.requiresMfaSetup && !isOnSetupPage) {
// User needs to set up MFA but is on challenge page
navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED + search);
navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED);
} else if (
!result.requiresMfaSetup &&
result.userHasMfa &&
!isOnChallengePage
) {
// User has MFA and should be on challenge page
navigate(APP_ROUTE.AUTH.MFA_CHALLENGE + search);
navigate(APP_ROUTE.AUTH.MFA_CHALLENGE);
} else if (!result.isTransferToken) {
// User has a regular auth token, shouldn't be on MFA pages
navigate(getPostLoginRedirect());
navigate(APP_ROUTE.HOME);
} else {
setIsValid(true);
}
@@ -23,7 +23,7 @@ import {
acceptInvitation,
createWorkspace,
} from "@/features/workspace/services/workspace-service.ts";
import APP_ROUTE, { getPostLoginRedirect } from "@/lib/app-route.ts";
import APP_ROUTE from "@/lib/app-route.ts";
import { RESET } from "jotai/utils";
import { useTranslation } from "react-i18next";
import { isCloud } from "@/lib/config.ts";
@@ -44,11 +44,11 @@ export default function useAuth() {
// Check if MFA is required
if (response?.userHasMfa) {
navigate(APP_ROUTE.AUTH.MFA_CHALLENGE + window.location.search);
navigate(APP_ROUTE.AUTH.MFA_CHALLENGE);
} else if (response?.requiresMfaSetup) {
navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED + window.location.search);
navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED);
} else {
navigate(getPostLoginRedirect());
navigate(APP_ROUTE.HOME);
}
} catch (err) {
setIsLoading(false);
@@ -1,6 +1,6 @@
import { useEffect } from "react";
import useCurrentUser from "@/features/user/hooks/use-current-user.ts";
import { getPostLoginRedirect } from "@/lib/app-route.ts";
import APP_ROUTE from "@/lib/app-route.ts";
import { useNavigate } from "react-router-dom";
export function useRedirectIfAuthenticated() {
@@ -9,7 +9,7 @@ export function useRedirectIfAuthenticated() {
useEffect(() => {
if (data && data?.user) {
navigate(getPostLoginRedirect());
navigate(APP_ROUTE.HOME);
}
}, [isLoading, data]);
}
@@ -7,19 +7,16 @@ import {
ShouldShowProps,
} from "@/features/editor/components/table/types/types.ts";
import { ActionIcon, Tooltip } from "@mantine/core";
import clsx from "clsx";
import {
IconAlertTriangleFilled,
IconCircleCheckFilled,
IconCircleXFilled,
IconInfoCircleFilled,
IconMoodSmile,
IconNotes,
} from "@tabler/icons-react";
import { CalloutType, isTextSelected } from "@docmost/editor-ext";
import { CalloutType } from "@docmost/editor-ext";
import { useTranslation } from "react-i18next";
import EmojiPicker from "@/components/ui/emoji-picker.tsx";
import classes from "../common/toolbar-menu.module.css";
export function CalloutMenu({ editor }: EditorMenuProps) {
const { t } = useTranslation();
@@ -29,7 +26,6 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
if (!state) {
return false;
}
if (isTextSelected(editor)) return false;
return editor.isActive("callout");
},
@@ -46,7 +42,6 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
return {
isCallout: ctx.editor.isActive("callout"),
isInfo: ctx.editor.isActive("callout", { type: "info" }),
isNote: ctx.editor.isActive("callout", { type: "note" }),
isSuccess: ctx.editor.isActive("callout", { type: "success" }),
isWarning: ctx.editor.isActive("callout", { type: "warning" }),
isDanger: ctx.editor.isActive("callout", { type: "danger" }),
@@ -131,31 +126,15 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
}}
shouldShow={shouldShow}
>
<div className={classes.toolbar}>
<ActionIcon.Group className="actionIconGroup">
<Tooltip position="top" label={t("Info")}>
<ActionIcon
onClick={() => setCalloutType("info")}
size="lg"
aria-label={t("Info")}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isInfo })}
variant={editorState?.isInfo ? "light" : "default"}
>
<IconInfoCircleFilled
size={18}
color="var(--mantine-color-blue-5)"
/>
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Note")}>
<ActionIcon
onClick={() => setCalloutType("note")}
size="lg"
aria-label={t("Note")}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isNote })}
>
<IconNotes size={18} color="var(--mantine-color-grape-5)" />
<IconInfoCircleFilled size={18} />
</ActionIcon>
</Tooltip>
@@ -164,13 +143,9 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
onClick={() => setCalloutType("success")}
size="lg"
aria-label={t("Success")}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isSuccess })}
variant={editorState?.isSuccess ? "light" : "default"}
>
<IconCircleCheckFilled
size={18}
color="var(--mantine-color-green-5)"
/>
<IconCircleCheckFilled size={18} />
</ActionIcon>
</Tooltip>
@@ -179,13 +154,9 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
onClick={() => setCalloutType("warning")}
size="lg"
aria-label={t("Warning")}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isWarning })}
variant={editorState?.isWarning ? "light" : "default"}
>
<IconAlertTriangleFilled
size={18}
color="var(--mantine-color-orange-5)"
/>
<IconAlertTriangleFilled size={18} />
</ActionIcon>
</Tooltip>
@@ -194,10 +165,9 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
onClick={() => setCalloutType("danger")}
size="lg"
aria-label={t("Danger")}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isDanger })}
variant={editorState?.isDanger ? "light" : "default"}
>
<IconCircleXFilled size={18} color="var(--mantine-color-red-5)" />
<IconCircleXFilled size={18} />
</ActionIcon>
</Tooltip>
@@ -208,10 +178,11 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
icon={currentIcon || <IconMoodSmile size={18} />}
actionIconProps={{
size: "lg",
variant: "subtle",
variant: "default",
c: undefined,
}}
/>
</div>
</ActionIcon.Group>
</BaseBubbleMenu>
);
}
@@ -4,7 +4,6 @@ import {
IconCircleCheckFilled,
IconCircleXFilled,
IconInfoCircleFilled,
IconNotes,
} from "@tabler/icons-react";
import { Alert } from "@mantine/core";
import classes from "./callout.module.css";
@@ -23,7 +22,6 @@ export default function CalloutView(props: NodeViewProps) {
icon={getCalloutIcon(type, icon)}
p="xs"
classNames={{
root: classes.root,
message: classes.message,
icon: classes.icon,
}}
@@ -36,14 +34,12 @@ export default function CalloutView(props: NodeViewProps) {
function getCalloutIcon(type: CalloutType, customIcon?: string) {
if (customIcon && customIcon.trim() !== "") {
return <span style={{ fontSize: "18px" }}>{customIcon}</span>;
return <span style={{ fontSize: '18px' }}>{customIcon}</span>;
}
switch (type) {
case "info":
return <IconInfoCircleFilled />;
case "note":
return <IconNotes />;
case "success":
return <IconCircleCheckFilled />;
case "warning":
@@ -59,8 +55,6 @@ function getCalloutColor(type: CalloutType) {
switch (type) {
case "info":
return "blue";
case "note":
return "grape";
case "success":
return "green";
case "warning":
@@ -1,13 +1,9 @@
.root {
overflow: visible;
}
.icon {
font-size: 24px;
line-height: 1;
width: 20px;
height: 20px;
margin-inline-end: var(--mantine-spacing-xs);
margin-inline-end: var(--mantine-spacing-md);
margin-top: 4px;
cursor: pointer;
}
@@ -15,8 +11,18 @@
.message {
font-size: var(--mantine-font-size-md);
color: var(--mantine-color-default-color);
overflow: visible;
text-overflow: unset;
white-space: nowrap;
word-break: break-word;
overflow-wrap: break-word;
}
/*
@mixin where-light {
color: var(--mantine-color-default-color);
}
@mixin where-dark {
color: var(--mantine-color-default-color);
}
*/
@@ -1,267 +0,0 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import React, { useCallback, useState } from "react";
import { Node as PMNode } from "prosemirror-model";
import {
EditorMenuProps,
ShouldShowProps,
} from "@/features/editor/components/table/types/types.ts";
import { ActionIcon, Tooltip, Popover, Button } from "@mantine/core";
import clsx from "clsx";
import {
IconChevronDown,
IconCheck,
IconColumns2,
IconColumns3,
IconLayoutSidebar,
IconLayoutSidebarRight,
IconLayoutAlignCenter,
} from "@tabler/icons-react";
import { isTextSelected } from "@docmost/editor-ext";
import type { WidthMode, ColumnsLayout } from "@docmost/editor-ext";
import { useTranslation } from "react-i18next";
import classes from "../common/toolbar-menu.module.css";
type LayoutPreset = {
layout: ColumnsLayout;
label: string;
icon: React.ElementType;
};
const twoColumnPresets: LayoutPreset[] = [
{ layout: "two_equal", label: "Equal columns", icon: IconColumns2 },
{
layout: "two_left_sidebar",
label: "Left sidebar",
icon: IconLayoutSidebar,
},
{
layout: "two_right_sidebar",
label: "Right sidebar",
icon: IconLayoutSidebarRight,
},
];
const threeColumnPresets: LayoutPreset[] = [
{ layout: "three_equal", label: "Equal columns", icon: IconColumns3 },
{
layout: "three_with_sidebars",
label: "Wide center",
icon: IconLayoutAlignCenter,
},
{
layout: "three_left_wide",
label: "Left wide",
icon: IconLayoutSidebarRight,
},
{ layout: "three_right_wide", label: "Right wide", icon: IconLayoutSidebar
},
];
function getPresetsForCount(count: number): LayoutPreset[] {
if (count === 2) return twoColumnPresets;
if (count === 3) return threeColumnPresets;
return [];
}
export function ColumnsMenu({ editor }: EditorMenuProps) {
const { t } = useTranslation();
const [isCountOpen, setIsCountOpen] = useState(false);
const nodesWithMenus = [
"callout",
"image",
"video",
"drawio",
"excalidraw",
"table",
];
const shouldShow = useCallback(
({ state }: ShouldShowProps) => {
if (!state) return false;
if (!editor.isActive("columns")) return false;
if (isTextSelected(editor)) return false;
if (nodesWithMenus.some((name) => editor.isActive(name))) return false;
const parent = findParentNode(
(node: PMNode) => node.type.name === "columns",
)(state.selection);
if (!parent) return false;
const dom = editor.view.nodeDOM(parent.pos) as HTMLElement;
if (!dom) return false;
const rect = dom.getBoundingClientRect();
return rect.bottom > 0 && rect.top < window.innerHeight;
},
[editor],
);
const editorState = useEditorState({
editor,
selector: (ctx) => {
if (!ctx.editor) return null;
const { selection } = ctx.editor.state;
const parent = findParentNode(
(node: PMNode) => node.type.name === "columns",
)(selection);
return {
columnCount: parent?.node.childCount || 2,
layout: (parent?.node.attrs.layout as ColumnsLayout) || "two_equal",
isNormal: ctx.editor.isActive("columns", { widthMode: "normal" }),
isWide: ctx.editor.isActive("columns", { widthMode: "wide" }),
};
},
});
const getReferencedVirtualElement = useCallback(() => {
if (!editor) return;
const { selection } = editor.state;
const predicate = (node: PMNode) => node.type.name === "columns";
const parent = findParentNode(predicate)(selection);
if (parent) {
const dom = editor.view.nodeDOM(parent?.pos) as HTMLElement;
const domRect = dom.getBoundingClientRect();
// Columns entirely out of viewport — return real rect so menu goes off-screen
if (domRect.bottom <= 0 || domRect.top >= window.innerHeight) {
return {
getBoundingClientRect: () => domRect,
getClientRects: () => [domRect],
};
}
// Clamp bottom so menu stays within viewport when columns extend below it
// 55px = 15px offset + ~40px menu height
const maxBottom = window.innerHeight - 55;
if (domRect.bottom > maxBottom) {
const clamped = new DOMRect(
domRect.x,
domRect.y,
domRect.width,
maxBottom - domRect.y,
);
return {
getBoundingClientRect: () => clamped,
getClientRects: () => [clamped],
};
}
return {
getBoundingClientRect: () => domRect,
getClientRects: () => [domRect],
};
}
const domRect = posToDOMRect(editor.view, selection.from, selection.to);
return {
getBoundingClientRect: () => domRect,
getClientRects: () => [domRect],
};
}, [editor]);
const setColumnCount = useCallback(
(count: number) => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
.setColumnCount(count)
.run();
setIsCountOpen(false);
},
[editor],
);
const setLayout = useCallback(
(layout: ColumnsLayout) => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
.setColumnsLayout(layout)
.run();
},
[editor],
);
const columnCount = editorState?.columnCount || 2;
const currentLayout = editorState?.layout || "two_equal";
const presets = getPresetsForCount(columnCount);
return (
<BaseBubbleMenu
editor={editor}
pluginKey="columns-menu"
updateDelay={0}
getReferencedVirtualElement={getReferencedVirtualElement}
options={{
placement: "bottom",
offset: {
mainAxis: 5,
},
flip: false,
}}
shouldShow={shouldShow}
>
<div className={classes.toolbar}>
<Popover opened={isCountOpen} onChange={setIsCountOpen} withArrow>
<Popover.Target>
<Button
variant="subtle"
color="dark"
size="compact-sm"
rightSection={<IconChevronDown size={12} />}
onClick={() => setIsCountOpen(!isCountOpen)}
aria-label={t("Column count")}
>
{t("{{count}} Columns", { count: columnCount })}
</Button>
</Popover.Target>
<Popover.Dropdown p={4}>
<Button.Group orientation="vertical">
{[2, 3, 4, 5].map((n) => (
<Button
key={n}
variant={n === columnCount ? "light" : "subtle"}
color={n === columnCount ? "blue" : "dark"}
justify="space-between"
fullWidth
rightSection={
n === columnCount ? <IconCheck size={14} /> : null
}
onClick={() => setColumnCount(n)}
size="xs"
>
{t("{{count}} Columns", { count: n })}
</Button>
))}
</Button.Group>
</Popover.Dropdown>
</Popover>
{presets.length > 0 && <div className={classes.divider} />}
{presets.map((preset) => (
<Tooltip key={preset.layout} position="top" label={t(preset.label)}>
<ActionIcon
onClick={() => setLayout(preset.layout)}
size="lg"
aria-label={t(preset.label)}
variant="subtle"
className={clsx({
[classes.active]: currentLayout === preset.layout,
})}
>
<preset.icon size={18} />
</ActionIcon>
</Tooltip>
))}
</div>
</BaseBubbleMenu>
);
}
export default ColumnsMenu;
@@ -1,35 +0,0 @@
import type { ResizableNodeViewDirection } from "@tiptap/core";
import classes from "./node-resize.module.css";
export function createResizeHandle(
direction: ResizableNodeViewDirection,
): HTMLElement {
const handle = document.createElement("div");
handle.dataset.resizeHandle = direction;
handle.style.position = "absolute";
handle.className = classes.handle;
if (direction === "left") {
handle.style.left = "-8px";
handle.style.top = "0";
handle.style.bottom = "0";
} else if (direction === "right") {
handle.style.right = "-8px";
handle.style.top = "0";
handle.style.bottom = "0";
}
const bar = document.createElement("div");
bar.className = classes.handleBar;
handle.appendChild(bar);
return handle;
}
export function buildResizeClasses(nodeClass: string) {
return {
container: `${classes.container} ${nodeClass}`,
wrapper: classes.wrapper,
resizing: classes.resizing,
};
}
@@ -1,65 +0,0 @@
.container {
display: flex;
}
.wrapper {
position: relative;
border-radius: 8px;
overflow: visible;
max-width: 100%;
}
.wrapper img,
.wrapper video {
height: auto !important;
}
.resizing {
user-select: none;
}
.handle {
position: absolute;
top: 0;
bottom: 0;
width: 16px;
display: flex;
align-items: center;
justify-content: center;
cursor: ew-resize;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 2;
}
.handle[data-resize-handle="left"] {
left: -8px;
}
.handle[data-resize-handle="right"] {
right: -8px;
}
.wrapper:hover .handle {
opacity: 1;
}
.resizing .handle {
opacity: 1;
}
.handleBar {
width: 4px;
height: 48px;
border-radius: 4px;
transition: background-color 0.15s ease;
background-color: light-dark(var(--mantine-color-blue-4), var(--mantine-color-blue-5));
}
.handle:hover .handleBar {
background-color: light-dark(var(--mantine-color-blue-6), var(--mantine-color-blue-4));
}
.resizing .handleBar {
background-color: light-dark(var(--mantine-color-blue-6), var(--mantine-color-blue-4));
}
@@ -1,29 +0,0 @@
.toolbar {
display: flex;
align-items: center;
gap: 2px;
padding: 3px;
border-radius: 8px;
border: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6));
box-shadow: 0 2px 12px light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.35));
}
.toolbar :global(.mantine-ActionIcon-root) {
--ai-color: light-dark(var(--mantine-color-dark-7), var(--mantine-color-gray-4)) !important;
--ai-hover: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5)) !important;
}
.toolbar .active {
--ai-color: light-dark(var(--mantine-color-blue-7), var(--mantine-color-blue-3)) !important;
--ai-hover: light-dark(var(--mantine-color-blue-0), var(--mantine-color-dark-5)) !important;
background-color: light-dark(var(--mantine-color-blue-0), var(--mantine-color-dark-5));
}
.divider {
width: 1px;
height: 16px;
align-self: center;
margin: 0 2px;
background-color: light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-3));
}
@@ -1,41 +1,24 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import { useCallback, useRef, useState } from "react";
import { useCallback } from "react";
import { Node as PMNode } from "prosemirror-model";
import {
EditorMenuProps,
ShouldShowProps,
} from "@/features/editor/components/table/types/types.ts";
import { ActionIcon, Modal, Tooltip, useComputedColorScheme } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import clsx from "clsx";
import {
IconLayoutAlignCenter,
IconLayoutAlignLeft,
IconLayoutAlignRight,
IconDownload,
IconEdit,
IconTrash,
} from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { getDrawioUrl, getFileUrl } from "@/lib/config.ts";
import { uploadFile } from "@/features/page/services/page-service.ts";
import {
DrawIoEmbed,
DrawIoEmbedRef,
EventExit,
EventSave,
} from "react-drawio";
import { decodeBase64ToSvgString, svgStringToFile } from "@/lib/utils";
import { IAttachment } from "@/features/attachments/types/attachment.types";
import classes from "../common/toolbar-menu.module.css";
import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx";
export function DrawioMenu({ editor }: EditorMenuProps) {
const { t } = useTranslation();
const [opened, { open, close }] = useDisclosure(false);
const [initialXML, setInitialXML] = useState<string>("");
const drawioRef = useRef<DrawIoEmbedRef>(null);
const computedColorScheme = useComputedColorScheme();
const shouldShow = useCallback(
({ state }: ShouldShowProps) => {
if (!state) {
return false;
}
return editor.isActive("drawio") && editor.getAttributes("drawio")?.src;
},
[editor],
);
const editorState = useEditorState({
editor,
@@ -47,26 +30,11 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
const drawioAttr = ctx.editor.getAttributes("drawio");
return {
isDrawio: ctx.editor.isActive("drawio"),
isAlignLeft: ctx.editor.isActive("drawio", { align: "left" }),
isAlignCenter: ctx.editor.isActive("drawio", { align: "center" }),
isAlignRight: ctx.editor.isActive("drawio", { align: "right" }),
src: drawioAttr?.src || null,
attachmentId: drawioAttr?.attachmentId || null,
width: drawioAttr?.width ? parseInt(drawioAttr.width) : null,
};
},
});
const shouldShow = useCallback(
({ state }: ShouldShowProps) => {
if (!state) {
return false;
}
return editor.isActive("drawio") && editor.getAttributes("drawio")?.src;
},
[editor],
);
const getReferencedVirtualElement = useCallback(() => {
if (!editor) return;
const { selection } = editor.state;
@@ -89,218 +57,38 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
};
}, [editor]);
const alignLeft = useCallback(() => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
.setDrawioAlign("left")
.run();
}, [editor]);
const alignCenter = useCallback(() => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
.setDrawioAlign("center")
.run();
}, [editor]);
const alignRight = useCallback(() => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
.setDrawioAlign("right")
.run();
}, [editor]);
const handleDownload = useCallback(() => {
if (!editorState?.src) return;
const url = getFileUrl(editorState.src);
const a = document.createElement("a");
a.href = url;
a.download = "";
a.click();
}, [editorState?.src]);
const handleDelete = useCallback(() => {
editor.commands.deleteSelection();
}, [editor]);
const handleOpen = useCallback(async () => {
if (!editorState?.src) return;
try {
const url = getFileUrl(editorState.src);
const request = await fetch(url, {
credentials: "include",
cache: "no-store",
});
const blob = await request.blob();
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => {
const base64data = (reader.result || "") as string;
setInitialXML(base64data);
};
} catch (err) {
console.error(err);
} finally {
open();
}
}, [editorState?.src, open]);
const handleSave = useCallback(
async (data: EventSave) => {
const svgString = decodeBase64ToSvgString(data.xml);
const fileName = "diagram.drawio.svg";
const drawioSVGFile = await svgStringToFile(svgString, fileName);
// @ts-ignore
const pageId = editor.storage?.pageId;
const attachmentId = editorState?.attachmentId;
let attachment: IAttachment = null;
if (attachmentId) {
attachment = await uploadFile(drawioSVGFile, pageId, attachmentId);
} else {
attachment = await uploadFile(drawioSVGFile, pageId);
}
editor.commands.updateAttributes("drawio", {
src: `/api/files/${attachment.id}/${attachment.fileName}?t=${new Date(attachment.updatedAt).getTime()}`,
title: attachment.fileName,
size: attachment.fileSize,
attachmentId: attachment.id,
});
close();
const onWidthChange = useCallback(
(value: number) => {
editor.commands.updateAttributes("drawio", { width: `${value}%` });
},
[editor, editorState?.attachmentId, close],
[editor],
);
return (
<>
<BaseBubbleMenu
editor={editor}
pluginKey={`drawio-menu`}
updateDelay={0}
getReferencedVirtualElement={getReferencedVirtualElement}
options={{
placement: "top",
offset: 8,
flip: false,
<BaseBubbleMenu
editor={editor}
pluginKey={`drawio-menu`}
updateDelay={0}
getReferencedVirtualElement={getReferencedVirtualElement}
options={{
placement: "top",
offset: 8,
flip: false,
}}
shouldShow={shouldShow}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
shouldShow={shouldShow}
>
<div className={classes.toolbar}>
<Tooltip position="top" label={t("Align left")}>
<ActionIcon
onClick={alignLeft}
size="lg"
aria-label={t("Align left")}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isAlignLeft })}
>
<IconLayoutAlignLeft size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Align center")}>
<ActionIcon
onClick={alignCenter}
size="lg"
aria-label={t("Align center")}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isAlignCenter })}
>
<IconLayoutAlignCenter size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Align right")}>
<ActionIcon
onClick={alignRight}
size="lg"
aria-label={t("Align right")}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isAlignRight })}
>
<IconLayoutAlignRight size={18} />
</ActionIcon>
</Tooltip>
<div className={classes.divider} />
<Tooltip position="top" label={t("Edit")}>
<ActionIcon
onClick={handleOpen}
size="lg"
aria-label={t("Edit")}
variant="subtle"
>
<IconEdit size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Download")}>
<ActionIcon
onClick={handleDownload}
size="lg"
aria-label={t("Download")}
variant="subtle"
>
<IconDownload size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Delete")}>
<ActionIcon
onClick={handleDelete}
size="lg"
aria-label={t("Delete")}
variant="subtle"
>
<IconTrash size={18} />
</ActionIcon>
</Tooltip>
</div>
</BaseBubbleMenu>
<Modal.Root opened={opened} onClose={close} fullScreen>
<Modal.Overlay />
<Modal.Content style={{ overflow: "hidden" }}>
<Modal.Body>
<div style={{ height: "100vh" }}>
<DrawIoEmbed
ref={drawioRef}
xml={initialXML}
baseUrl={getDrawioUrl()}
urlParameters={{
ui: computedColorScheme === "light" ? "kennedy" : "dark",
spin: true,
libraries: true,
saveAndExit: true,
noSaveBtn: true,
}}
onSave={(data: EventSave) => {
if (data.parentEvent !== "save") {
return;
}
handleSave(data);
}}
onClose={(data: EventExit) => {
if (data.parentEvent) {
return;
}
close();
}}
/>
</div>
</Modal.Body>
</Modal.Content>
</Modal.Root>
</>
{editorState?.width && (
<NodeWidthResize onChange={onWidthChange} value={editorState.width} />
)}
</div>
</BaseBubbleMenu>
);
}
@@ -2,6 +2,7 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
import {
ActionIcon,
Card,
Image,
Modal,
Text,
useComputedColorScheme,
@@ -9,7 +10,7 @@ import {
import { useRef, useState } from "react";
import { uploadFile } from "@/features/page/services/page-service.ts";
import { useDisclosure } from "@mantine/hooks";
import { getDrawioUrl } from "@/lib/config.ts";
import { getDrawioUrl, getFileUrl } from "@/lib/config.ts";
import {
DrawIoEmbed,
DrawIoEmbedRef,
@@ -25,7 +26,7 @@ import { useTranslation } from "react-i18next";
export default function DrawioView(props: NodeViewProps) {
const { t } = useTranslation();
const { node, updateAttributes, editor, selected } = props;
const { attachmentId } = node.attrs;
const { src, title, width, attachmentId } = node.attrs;
const drawioRef = useRef<DrawIoEmbedRef>(null);
const [initialXML, setInitialXML] = useState<string>("");
const [opened, { open, close }] = useDisclosure(false);
@@ -35,11 +36,33 @@ export default function DrawioView(props: NodeViewProps) {
if (!editor.isEditable) {
return;
}
open();
try {
if (src) {
const url = getFileUrl(src);
const request = await fetch(url, {
credentials: "include",
cache: "no-store",
});
const blob = await request.blob();
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => {
const base64data = (reader.result || "") as string;
setInitialXML(base64data);
};
}
} catch (err) {
console.error(err);
} finally {
open();
}
};
const handleSave = async (data: EventSave) => {
const svgString = decodeBase64ToSvgString(data.xml);
const fileName = "diagram.drawio.svg";
const drawioSVGFile = await svgStringToFile(svgString, fileName);
@@ -47,6 +70,7 @@ export default function DrawioView(props: NodeViewProps) {
const pageId = editor.storage?.pageId;
let attachment: IAttachment = null;
if (attachmentId) {
attachment = await uploadFile(drawioSVGFile, pageId, attachmentId);
} else {
@@ -82,12 +106,14 @@ export default function DrawioView(props: NodeViewProps) {
noSaveBtn: true,
}}
onSave={(data: EventSave) => {
// If the save is triggered by another event, then do nothing
if (data.parentEvent !== "save") {
return;
}
handleSave(data);
}}
onClose={(data: EventExit) => {
// If the exit is triggered by another event, then do nothing
if (data.parentEvent) {
return;
}
@@ -99,28 +125,62 @@ export default function DrawioView(props: NodeViewProps) {
</Modal.Content>
</Modal.Root>
<Card
radius="md"
onClick={(e) => e.detail === 2 && handleOpen()}
p="xs"
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
withBorder
className={clsx(selected ? "ProseMirror-selectednode" : "")}
>
<div style={{ display: "flex", alignItems: "center" }}>
<ActionIcon variant="transparent" color="gray">
<IconEdit size={18} />
</ActionIcon>
{src ? (
<div style={{ position: "relative" }}>
<Image
onClick={(e) => e.detail === 2 && handleOpen()}
radius="md"
fit="contain"
w={width}
src={getFileUrl(src)}
alt={title}
className={clsx(
selected ? "ProseMirror-selectednode" : "",
"alignCenter",
)}
/>
<Text component="span" size="lg" c="dimmed">
{t("Double-click to edit Draw.io diagram")}
</Text>
{selected && editor.isEditable && (
<ActionIcon
onClick={handleOpen}
variant="default"
color="gray"
mx="xs"
className="print-hide"
style={{
position: "absolute",
top: 8,
right: 8,
}}
>
<IconEdit size={18} />
</ActionIcon>
)}
</div>
</Card>
) : (
<Card
radius="md"
onClick={(e) => e.detail === 2 && handleOpen()}
p="xs"
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
withBorder
className={clsx(selected ? "ProseMirror-selectednode" : "")}
>
<div style={{ display: "flex", alignItems: "center" }}>
<ActionIcon variant="transparent" color="gray">
<IconEdit size={18} />
</ActionIcon>
<Text component="span" size="lg" c="dimmed">
{t("Double-click to edit Draw.io diagram")}
</Text>
</div>
</Card>
)}
</NodeViewWrapper>
);
}
@@ -1,57 +1,26 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import { lazy, Suspense, useCallback, useState } from "react";
import { useCallback } from "react";
import { Node as PMNode } from "prosemirror-model";
import {
EditorMenuProps,
ShouldShowProps,
} from "@/features/editor/components/table/types/types.ts";
import {
ActionIcon,
Button,
Group,
Tooltip,
useComputedColorScheme,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import clsx from "clsx";
import {
IconLayoutAlignCenter,
IconLayoutAlignLeft,
IconLayoutAlignRight,
IconDownload,
IconEdit,
IconTrash,
} from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import { getFileUrl } from "@/lib/config.ts";
import { uploadFile } from "@/features/page/services/page-service.ts";
import { svgStringToFile } from "@/lib";
import "@excalidraw/excalidraw/index.css";
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
import { IAttachment } from "@/features/attachments/types/attachment.types";
import ReactClearModal from "react-clear-modal";
import { useHandleLibrary } from "@excalidraw/excalidraw";
import { localStorageLibraryAdapter } from "@/features/editor/components/excalidraw/excalidraw-utils.ts";
import classes from "../common/toolbar-menu.module.css";
const ExcalidrawComponent = lazy(() =>
import("@excalidraw/excalidraw").then((module) => ({
default: module.Excalidraw,
})),
);
import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx";
export function ExcalidrawMenu({ editor }: EditorMenuProps) {
const { t } = useTranslation();
const [opened, { open, close }] = useDisclosure(false);
const [excalidrawAPI, setExcalidrawAPI] =
useState<ExcalidrawImperativeAPI>(null);
useHandleLibrary({
excalidrawAPI,
adapter: localStorageLibraryAdapter,
});
const [excalidrawData, setExcalidrawData] = useState<any>(null);
const computedColorScheme = useComputedColorScheme();
const shouldShow = useCallback(
({ state }: ShouldShowProps) => {
if (!state) {
return false;
}
return (
editor.isActive("excalidraw") && editor.getAttributes("excalidraw")?.src
);
},
[editor],
);
const editorState = useEditorState({
editor,
@@ -63,29 +32,11 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
const excalidrawAttr = ctx.editor.getAttributes("excalidraw");
return {
isExcalidraw: ctx.editor.isActive("excalidraw"),
isAlignLeft: ctx.editor.isActive("excalidraw", { align: "left" }),
isAlignCenter: ctx.editor.isActive("excalidraw", { align: "center" }),
isAlignRight: ctx.editor.isActive("excalidraw", { align: "right" }),
src: excalidrawAttr?.src || null,
attachmentId: excalidrawAttr?.attachmentId || null,
width: excalidrawAttr?.width ? parseInt(excalidrawAttr.width) : null,
};
},
});
const shouldShow = useCallback(
({ state }: ShouldShowProps) => {
if (!state) {
return false;
}
return (
editor.isActive("excalidraw") &&
editor.getAttributes("excalidraw")?.src
);
},
[editor],
);
const getReferencedVirtualElement = useCallback(() => {
if (!editor) return;
const { selection } = editor.state;
@@ -108,248 +59,38 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
};
}, [editor]);
const alignLeft = useCallback(() => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
.setExcalidrawAlign("left")
.run();
}, [editor]);
const alignCenter = useCallback(() => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
.setExcalidrawAlign("center")
.run();
}, [editor]);
const alignRight = useCallback(() => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
.setExcalidrawAlign("right")
.run();
}, [editor]);
const handleDownload = useCallback(() => {
if (!editorState?.src) return;
const url = getFileUrl(editorState.src);
const a = document.createElement("a");
a.href = url;
a.download = "";
a.click();
}, [editorState?.src]);
const handleDelete = useCallback(() => {
editor.commands.deleteSelection();
}, [editor]);
const handleOpen = useCallback(async () => {
if (!editorState?.src) return;
try {
const url = getFileUrl(editorState.src);
const request = await fetch(url, {
credentials: "include",
cache: "no-store",
});
const { loadFromBlob } = await import("@excalidraw/excalidraw");
const data = await loadFromBlob(await request.blob(), null, null);
setExcalidrawData(data);
} catch (err) {
console.error(err);
} finally {
open();
}
}, [editorState?.src, open]);
const handleSave = useCallback(async () => {
if (!excalidrawAPI) {
return;
}
const { exportToSvg } = await import("@excalidraw/excalidraw");
const svg = await exportToSvg({
elements: excalidrawAPI?.getSceneElements(),
appState: {
exportEmbedScene: true,
exportWithDarkMode: false,
},
files: excalidrawAPI?.getFiles(),
});
const serializer = new XMLSerializer();
let svgString = serializer.serializeToString(svg);
svgString = svgString.replace(
/https:\/\/unpkg\.com\/@excalidraw\/excalidraw@undefined/g,
"https://unpkg.com/@excalidraw/excalidraw@latest",
);
const fileName = "diagram.excalidraw.svg";
const excalidrawSvgFile = await svgStringToFile(svgString, fileName);
// @ts-ignore
const pageId = editor.storage?.pageId;
const attachmentId = editorState?.attachmentId;
let attachment: IAttachment = null;
if (attachmentId) {
attachment = await uploadFile(excalidrawSvgFile, pageId, attachmentId);
} else {
attachment = await uploadFile(excalidrawSvgFile, pageId);
}
editor.commands.updateAttributes("excalidraw", {
src: `/api/files/${attachment.id}/${attachment.fileName}?t=${new Date(attachment.updatedAt).getTime()}`,
title: attachment.fileName,
size: attachment.fileSize,
attachmentId: attachment.id,
});
close();
}, [editor, excalidrawAPI, editorState?.attachmentId, close]);
const onWidthChange = useCallback(
(value: number) => {
editor.commands.updateAttributes("excalidraw", { width: `${value}%` });
},
[editor],
);
return (
<>
<BaseBubbleMenu
editor={editor}
pluginKey={`excalidraw-menu`}
updateDelay={0}
getReferencedVirtualElement={getReferencedVirtualElement}
options={{
placement: "top",
offset: 8,
flip: false,
}}
shouldShow={shouldShow}
>
<div className={classes.toolbar}>
<Tooltip position="top" label={t("Align left")}>
<ActionIcon
onClick={alignLeft}
size="lg"
aria-label={t("Align left")}
variant="subtle"
className={clsx({
[classes.active]: editorState?.isAlignLeft,
})}
>
<IconLayoutAlignLeft size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Align center")}>
<ActionIcon
onClick={alignCenter}
size="lg"
aria-label={t("Align center")}
variant="subtle"
className={clsx({
[classes.active]: editorState?.isAlignCenter,
})}
>
<IconLayoutAlignCenter size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Align right")}>
<ActionIcon
onClick={alignRight}
size="lg"
aria-label={t("Align right")}
variant="subtle"
className={clsx({
[classes.active]: editorState?.isAlignRight,
})}
>
<IconLayoutAlignRight size={18} />
</ActionIcon>
</Tooltip>
<div className={classes.divider} />
<Tooltip position="top" label={t("Edit")}>
<ActionIcon
onClick={handleOpen}
size="lg"
aria-label={t("Edit")}
variant="subtle"
>
<IconEdit size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Download")}>
<ActionIcon
onClick={handleDownload}
size="lg"
aria-label={t("Download")}
variant="subtle"
>
<IconDownload size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Delete")}>
<ActionIcon
onClick={handleDelete}
size="lg"
aria-label={t("Delete")}
variant="subtle"
>
<IconTrash size={18} />
</ActionIcon>
</Tooltip>
</div>
</BaseBubbleMenu>
<ReactClearModal
<BaseBubbleMenu
editor={editor}
pluginKey={`excalidraw-menu`}
updateDelay={0}
getReferencedVirtualElement={getReferencedVirtualElement}
options={{
placement: "top",
offset: 8,
flip: false,
}}
shouldShow={shouldShow}
>
<div
style={{
backgroundColor: "rgba(0, 0, 0, 0.5)",
padding: 0,
zIndex: 200,
}}
isOpen={opened}
onRequestClose={close}
disableCloseOnBgClick={true}
contentProps={{
style: {
padding: 0,
width: "90vw",
},
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Group
justify="flex-end"
wrap="nowrap"
bg="var(--mantine-color-body)"
p="xs"
>
<Button onClick={handleSave} size={"compact-sm"}>
{t("Save & Exit")}
</Button>
<Button onClick={close} color="red" size={"compact-sm"}>
{t("Exit")}
</Button>
</Group>
<div style={{ height: "90vh" }}>
<Suspense fallback={null}>
<ExcalidrawComponent
excalidrawAPI={(api) => setExcalidrawAPI(api)}
initialData={{
...excalidrawData,
scrollToContent: true,
}}
theme={computedColorScheme}
/>
</Suspense>
</div>
</ReactClearModal>
</>
{editorState?.width && (
<NodeWidthResize onChange={onWidthChange} value={editorState.width} />
)}
</div>
</BaseBubbleMenu>
);
}
@@ -4,24 +4,28 @@ import {
Button,
Card,
Group,
Image,
Text,
useComputedColorScheme,
} from "@mantine/core";
import { lazy, Suspense, useState } from "react";
import { useState } from "react";
import { uploadFile } from "@/features/page/services/page-service.ts";
import { svgStringToFile } from "@/lib";
import { useDisclosure } from "@mantine/hooks";
import { getFileUrl } from "@/lib/config.ts";
import "@excalidraw/excalidraw/index.css";
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
import { IAttachment } from "@/features/attachments/types/attachment.types";
import ReactClearModal from "react-clear-modal";
import clsx from "clsx";
import { IconEdit } from "@tabler/icons-react";
import { lazy } from "react";
import { Suspense } from "react";
import { useTranslation } from "react-i18next";
import { useHandleLibrary } from "@excalidraw/excalidraw";
import { localStorageLibraryAdapter } from "@/features/editor/components/excalidraw/excalidraw-utils.ts";
const ExcalidrawComponent = lazy(() =>
const Excalidraw = lazy(() =>
import("@excalidraw/excalidraw").then((module) => ({
default: module.Excalidraw,
})),
@@ -30,7 +34,7 @@ const ExcalidrawComponent = lazy(() =>
export default function ExcalidrawView(props: NodeViewProps) {
const { t } = useTranslation();
const { node, updateAttributes, editor, selected } = props;
const { attachmentId } = node.attrs;
const { src, title, width, attachmentId } = node.attrs;
const [excalidrawAPI, setExcalidrawAPI] =
useState<ExcalidrawImperativeAPI>(null);
@@ -46,7 +50,25 @@ export default function ExcalidrawView(props: NodeViewProps) {
if (!editor.isEditable) {
return;
}
open();
try {
if (src) {
const url = getFileUrl(src);
const request = await fetch(url, {
credentials: "include",
cache: "no-store",
});
const { loadFromBlob } = await import("@excalidraw/excalidraw");
const data = await loadFromBlob(await request.blob(), null, null);
setExcalidrawData(data);
}
} catch (err) {
console.error(err);
} finally {
open();
}
};
const handleSave = async () => {
@@ -129,7 +151,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
</Group>
<div style={{ height: "90vh" }}>
<Suspense fallback={null}>
<ExcalidrawComponent
<Excalidraw
excalidrawAPI={(api) => setExcalidrawAPI(api)}
initialData={{
...excalidrawData,
@@ -141,28 +163,62 @@ export default function ExcalidrawView(props: NodeViewProps) {
</div>
</ReactClearModal>
<Card
radius="md"
onClick={(e) => e.detail === 2 && handleOpen()}
p="xs"
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
withBorder
className={clsx(selected ? "ProseMirror-selectednode" : "")}
>
<div style={{ display: "flex", alignItems: "center" }}>
<ActionIcon variant="transparent" color="gray">
<IconEdit size={18} />
</ActionIcon>
{src ? (
<div style={{ position: "relative" }}>
<Image
onClick={(e) => e.detail === 2 && handleOpen()}
radius="md"
fit="contain"
w={width}
src={getFileUrl(src)}
alt={title}
className={clsx(
selected ? "ProseMirror-selectednode" : "",
"alignCenter",
)}
/>
<Text component="span" size="lg" c="dimmed">
{t("Double-click to edit Excalidraw diagram")}
</Text>
{selected && editor.isEditable && (
<ActionIcon
onClick={handleOpen}
variant="default"
color="gray"
mx="xs"
className="print-hide"
style={{
position: "absolute",
top: 8,
right: 8,
}}
>
<IconEdit size={18} />
</ActionIcon>
)}
</div>
</Card>
) : (
<Card
radius="md"
onClick={(e) => e.detail === 2 && handleOpen()}
p="xs"
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
withBorder
className={clsx(selected ? "ProseMirror-selectednode" : "")}
>
<div style={{ display: "flex", alignItems: "center" }}>
<ActionIcon variant="transparent" color="gray">
<IconEdit size={18} />
</ActionIcon>
<Text component="span" size="lg" c="dimmed">
{t("Double-click to edit Excalidraw diagram")}
</Text>
</div>
</Card>
)}
</NodeViewWrapper>
);
}
@@ -1,29 +1,22 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import React, { useCallback, useRef } from "react";
import React, { useCallback } from "react";
import { Node as PMNode } from "prosemirror-model";
import {
EditorMenuProps,
ShouldShowProps,
} from "@/features/editor/components/table/types/types.ts";
import { ActionIcon, Tooltip } from "@mantine/core";
import clsx from "clsx";
import {
IconLayoutAlignCenter,
IconLayoutAlignLeft,
IconLayoutAlignRight,
IconDownload,
IconRefresh,
IconTrash,
} from "@tabler/icons-react";
import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx";
import { useTranslation } from "react-i18next";
import { getFileUrl } from "@/lib/config.ts";
import { uploadImageAction } from "@/features/editor/components/image/upload-image-action.tsx";
import classes from "../common/toolbar-menu.module.css";
export function ImageMenu({ editor }: EditorMenuProps) {
const { t } = useTranslation();
const fileInputRef = useRef<HTMLInputElement>(null);
const editorState = useEditorState({
editor,
@@ -39,7 +32,7 @@ export function ImageMenu({ editor }: EditorMenuProps) {
isAlignLeft: ctx.editor.isActive("image", { align: "left" }),
isAlignCenter: ctx.editor.isActive("image", { align: "center" }),
isAlignRight: ctx.editor.isActive("image", { align: "right" }),
src: imageAttrs?.src || null,
width: imageAttrs?.width ? parseInt(imageAttrs.width) : null,
};
},
});
@@ -101,40 +94,17 @@ export function ImageMenu({ editor }: EditorMenuProps) {
.run();
}, [editor]);
const handleDownload = useCallback(() => {
if (!editorState?.src) return;
const url = getFileUrl(editorState.src);
const a = document.createElement("a");
a.href = url;
a.download = "";
a.click();
}, [editorState?.src]);
const handleReplace = useCallback(() => {
fileInputRef.current?.click();
}, []);
const handleFileChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// @ts-ignore
const pageId = editor.storage?.pageId;
if (pageId) {
const pos = editor.state.selection.from;
uploadImageAction(file, editor, pos, pageId);
}
// Reset so the same file can be selected again
e.target.value = "";
const onWidthChange = useCallback(
(value: number) => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
.setImageWidth(value)
.run();
},
[editor],
);
const handleDelete = useCallback(() => {
editor.commands.deleteSelection();
}, [editor]);
return (
<BaseBubbleMenu
editor={editor}
@@ -148,14 +118,13 @@ export function ImageMenu({ editor }: EditorMenuProps) {
}}
shouldShow={shouldShow}
>
<div className={classes.toolbar}>
<ActionIcon.Group className="actionIconGroup">
<Tooltip position="top" label={t("Align left")}>
<ActionIcon
onClick={alignImageLeft}
size="lg"
aria-label={t("Align left")}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isAlignLeft })}
variant={editorState?.isAlignLeft ? "light" : "default"}
>
<IconLayoutAlignLeft size={18} />
</ActionIcon>
@@ -166,8 +135,7 @@ export function ImageMenu({ editor }: EditorMenuProps) {
onClick={alignImageCenter}
size="lg"
aria-label={t("Align center")}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isAlignCenter })}
variant={editorState?.isAlignCenter ? "light" : "default"}
>
<IconLayoutAlignCenter size={18} />
</ActionIcon>
@@ -178,56 +146,16 @@ export function ImageMenu({ editor }: EditorMenuProps) {
onClick={alignImageRight}
size="lg"
aria-label={t("Align right")}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isAlignRight })}
variant={editorState?.isAlignRight ? "light" : "default"}
>
<IconLayoutAlignRight size={18} />
</ActionIcon>
</Tooltip>
</ActionIcon.Group>
<div className={classes.divider} />
<Tooltip position="top" label={t("Download")}>
<ActionIcon
onClick={handleDownload}
size="lg"
aria-label={t("Download")}
variant="subtle"
>
<IconDownload size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Replace image")}>
<ActionIcon
onClick={handleReplace}
size="lg"
aria-label={t("Replace image")}
variant="subtle"
>
<IconRefresh size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Delete")}>
<ActionIcon
onClick={handleDelete}
size="lg"
aria-label={t("Delete")}
variant="subtle"
>
<IconTrash size={18} />
</ActionIcon>
</Tooltip>
</div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
style={{ display: "none" }}
onChange={handleFileChange}
/>
{editorState?.width && (
<NodeWidthResize onChange={onWidthChange} value={editorState.width} />
)}
</BaseBubbleMenu>
);
}
@@ -1,7 +0,0 @@
import {
createResizeHandle,
buildResizeClasses,
} from "../common/node-resize-handles";
export const createImageHandle = createResizeHandle;
export const imageResizeClasses = buildResizeClasses("node-image");
@@ -1,64 +0,0 @@
.container {
display: flex;
}
.wrapper {
position: relative;
border-radius: 8px;
overflow: visible;
max-width: 100%;
}
.wrapper img {
height: auto !important;
}
.resizing {
user-select: none;
}
.handle {
position: absolute;
top: 0;
bottom: 0;
width: 16px;
display: flex;
align-items: center;
justify-content: center;
cursor: ew-resize;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 2;
}
.handle[data-resize-handle="left"] {
left: -8px;
}
.handle[data-resize-handle="right"] {
right: -8px;
}
.wrapper:hover .handle {
opacity: 1;
}
.resizing .handle {
opacity: 1;
}
.handleBar {
width: 4px;
height: 48px;
border-radius: 4px;
transition: background-color 0.15s ease;
background-color: light-dark(var(--mantine-color-blue-4), var(--mantine-color-blue-5));
}
.handle:hover .handleBar {
background-color: light-dark(var(--mantine-color-blue-6), var(--mantine-color-blue-4));
}
.resizing .handleBar {
background-color: light-dark(var(--mantine-color-blue-6), var(--mantine-color-blue-4));
}
@@ -20,8 +20,6 @@ import {
IconCalendar,
IconAppWindow,
IconSitemap,
IconColumns3,
IconColumns2,
} from "@tabler/icons-react";
import {
CommandProps,
@@ -33,8 +31,6 @@ import { uploadAttachmentAction } from "@/features/editor/components/attachment/
import IconExcalidraw from "@/components/icons/icon-excalidraw";
import IconMermaid from "@/components/icons/icon-mermaid";
import IconDrawio from "@/components/icons/icon-drawio";
import { IconColumns4 } from "@/components/icons/icon-columns-4";
import { IconColumns5 } from "@/components/icons/icon-columns-5";
import {
AirtableIcon,
FigmaIcon,
@@ -394,58 +390,6 @@ const CommandGroups: SlashMenuGroupedItemsType = {
editor.chain().focus().deleteRange(range).insertSubpages().run();
},
},
{
title: "2 Columns",
description: "Split content into two columns.",
searchTerms: ["columns", "layout", "split", "side"],
icon: IconColumns2,
command: ({ editor, range }: CommandProps) =>
editor
.chain()
.focus()
.deleteRange(range)
.insertColumns({ layout: "two_equal" })
.run(),
},
{
title: "3 Columns",
description: "Split content into three columns.",
searchTerms: ["columns", "layout", "split", "triple"],
icon: IconColumns3,
command: ({ editor, range }: CommandProps) =>
editor
.chain()
.focus()
.deleteRange(range)
.insertColumns({ layout: "three_equal" })
.run(),
},
{
title: "4 Columns",
description: "Split content into four columns.",
searchTerms: ["columns", "layout", "split"],
icon: IconColumns4,
command: ({ editor, range }: CommandProps) =>
editor
.chain()
.focus()
.deleteRange(range)
.insertColumns({ layout: "four_equal" })
.run(),
},
{
title: "5 Columns",
description: "Split content into five columns.",
searchTerms: ["columns", "layout", "split"],
icon: IconColumns5,
command: ({ editor, range }: CommandProps) =>
editor
.chain()
.focus()
.deleteRange(range)
.insertColumns({ layout: "five_equal" })
.run(),
},
{
title: "Iframe embed",
description: "Embed any Iframe",
@@ -95,7 +95,7 @@ export const TableBackgroundColor: FC<TableBackgroundColorProps> = ({
<Popover.Target>
<Tooltip label={t("Background color")} withArrow>
<ActionIcon
variant="subtle"
variant="default"
size="lg"
aria-label={t("Background color")}
onClick={() => setOpened(!opened)}
@@ -16,7 +16,6 @@ import { useTranslation } from "react-i18next";
import { TableBackgroundColor } from "./table-background-color";
import { TableTextAlignment } from "./table-text-alignment";
import { BubbleMenu } from "@tiptap/react/menus";
import classes from "../common/toolbar-menu.module.css";
export const TableCellMenu = React.memo(
({ editor, appendTo }: EditorMenuProps): JSX.Element => {
@@ -70,16 +69,14 @@ export const TableCellMenu = React.memo(
}}
shouldShow={shouldShow}
>
<div className={classes.toolbar}>
<ActionIcon.Group>
<TableBackgroundColor editor={editor} />
<TableTextAlignment editor={editor} />
<div className={classes.divider} />
<Tooltip position="top" label={t("Merge cells")}>
<ActionIcon
onClick={mergeCells}
variant="subtle"
variant="default"
size="lg"
aria-label={t("Merge cells")}
>
@@ -90,7 +87,7 @@ export const TableCellMenu = React.memo(
<Tooltip position="top" label={t("Split cell")}>
<ActionIcon
onClick={splitCell}
variant="subtle"
variant="default"
size="lg"
aria-label={t("Split cell")}
>
@@ -98,12 +95,10 @@ export const TableCellMenu = React.memo(
</ActionIcon>
</Tooltip>
<div className={classes.divider} />
<Tooltip position="top" label={t("Delete column")}>
<ActionIcon
onClick={deleteColumn}
variant="subtle"
variant="default"
size="lg"
aria-label={t("Delete column")}
>
@@ -114,7 +109,7 @@ export const TableCellMenu = React.memo(
<Tooltip position="top" label={t("Delete row")}>
<ActionIcon
onClick={deleteRow}
variant="subtle"
variant="default"
size="lg"
aria-label={t("Delete row")}
>
@@ -122,19 +117,17 @@ export const TableCellMenu = React.memo(
</ActionIcon>
</Tooltip>
<div className={classes.divider} />
<Tooltip position="top" label={t("Toggle header cell")}>
<ActionIcon
onClick={toggleHeaderCell}
variant="subtle"
variant="default"
size="lg"
aria-label={t("Toggle header cell")}
>
<IconTableRow size={18} />
</ActionIcon>
</Tooltip>
</div>
</ActionIcon.Group>
</BubbleMenu>
);
}
@@ -18,9 +18,8 @@ import {
IconTrashX,
} from "@tabler/icons-react";
import { BubbleMenu } from "@tiptap/react/menus";
import { isCellSelection, isTextSelected } from "@docmost/editor-ext";
import { isCellSelection } from "@docmost/editor-ext";
import { useTranslation } from "react-i18next";
import classes from "../common/toolbar-menu.module.css";
export const TableMenu = React.memo(
({ editor }: EditorMenuProps): JSX.Element => {
@@ -31,7 +30,6 @@ export const TableMenu = React.memo(
return false;
}
if (isTextSelected(editor)) return false;
return editor.isActive("table") && !isCellSelection(state.selection);
},
[editor]
@@ -120,11 +118,11 @@ export const TableMenu = React.memo(
}}
shouldShow={shouldShow}
>
<div className={classes.toolbar}>
<ActionIcon.Group>
<Tooltip position="top" label={t("Add left column")}>
<ActionIcon
onClick={addColumnLeft}
variant="subtle"
variant="default"
size="lg"
aria-label={t("Add left column")}
>
@@ -135,7 +133,7 @@ export const TableMenu = React.memo(
<Tooltip position="top" label={t("Add right column")}>
<ActionIcon
onClick={addColumnRight}
variant="subtle"
variant="default"
size="lg"
aria-label={t("Add right column")}
>
@@ -146,7 +144,7 @@ export const TableMenu = React.memo(
<Tooltip position="top" label={t("Delete column")}>
<ActionIcon
onClick={deleteColumn}
variant="subtle"
variant="default"
size="lg"
aria-label={t("Delete column")}
>
@@ -154,12 +152,10 @@ export const TableMenu = React.memo(
</ActionIcon>
</Tooltip>
<div className={classes.divider} />
<Tooltip position="top" label={t("Add row above")}>
<ActionIcon
onClick={addRowAbove}
variant="subtle"
variant="default"
size="lg"
aria-label={t("Add row above")}
>
@@ -170,7 +166,7 @@ export const TableMenu = React.memo(
<Tooltip position="top" label={t("Add row below")}>
<ActionIcon
onClick={addRowBelow}
variant="subtle"
variant="default"
size="lg"
aria-label={t("Add row below")}
>
@@ -181,7 +177,7 @@ export const TableMenu = React.memo(
<Tooltip position="top" label={t("Delete row")}>
<ActionIcon
onClick={deleteRow}
variant="subtle"
variant="default"
size="lg"
aria-label={t("Delete row")}
>
@@ -189,12 +185,10 @@ export const TableMenu = React.memo(
</ActionIcon>
</Tooltip>
<div className={classes.divider} />
<Tooltip position="top" label={t("Toggle header row")}>
<ActionIcon
onClick={toggleHeaderRow}
variant="subtle"
variant="default"
size="lg"
aria-label={t("Toggle header row")}
>
@@ -205,7 +199,7 @@ export const TableMenu = React.memo(
<Tooltip position="top" label={t("Toggle header column")}>
<ActionIcon
onClick={toggleHeaderColumn}
variant="subtle"
variant="default"
size="lg"
aria-label={t("Toggle header column")}
>
@@ -213,19 +207,18 @@ export const TableMenu = React.memo(
</ActionIcon>
</Tooltip>
<div className={classes.divider} />
<Tooltip position="top" label={t("Delete table")}>
<ActionIcon
onClick={deleteTable}
variant="subtle"
variant="default"
size="lg"
color="red"
aria-label={t("Delete table")}
>
<IconTrashX size={18} />
</ActionIcon>
</Tooltip>
</div>
</ActionIcon.Group>
</BubbleMenu>
);
}
@@ -88,7 +88,7 @@ export const TableTextAlignment: FC<TableTextAlignmentProps> = ({ editor }) => {
<Popover.Target>
<Tooltip label={t("Text alignment")} withArrow>
<ActionIcon
variant="subtle"
variant="default"
size="lg"
aria-label={t("Text alignment")}
onClick={() => setOpened(!opened)}
@@ -1,23 +1,19 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import { useCallback } from "react";
import React, { useCallback } from "react";
import { Node as PMNode } from "prosemirror-model";
import {
EditorMenuProps,
ShouldShowProps,
} from "@/features/editor/components/table/types/types.ts";
import { ActionIcon, Tooltip } from "@mantine/core";
import clsx from "clsx";
import {
IconLayoutAlignCenter,
IconLayoutAlignLeft,
IconLayoutAlignRight,
IconDownload,
IconTrash,
} from "@tabler/icons-react";
import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx";
import { useTranslation } from "react-i18next";
import { getFileUrl } from "@/lib/config.ts";
import classes from "../common/toolbar-menu.module.css";
export function VideoMenu({ editor }: EditorMenuProps) {
const { t } = useTranslation();
@@ -36,7 +32,7 @@ export function VideoMenu({ editor }: EditorMenuProps) {
isAlignLeft: ctx.editor.isActive("video", { align: "left" }),
isAlignCenter: ctx.editor.isActive("video", { align: "center" }),
isAlignRight: ctx.editor.isActive("video", { align: "right" }),
src: videoAttrs?.src || null,
width: videoAttrs?.width ? parseInt(videoAttrs.width) : null,
};
},
});
@@ -74,7 +70,7 @@ export function VideoMenu({ editor }: EditorMenuProps) {
};
}, [editor]);
const alignLeft = useCallback(() => {
const alignVideoLeft = useCallback(() => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
@@ -82,7 +78,7 @@ export function VideoMenu({ editor }: EditorMenuProps) {
.run();
}, [editor]);
const alignCenter = useCallback(() => {
const alignVideoCenter = useCallback(() => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
@@ -90,7 +86,7 @@ export function VideoMenu({ editor }: EditorMenuProps) {
.run();
}, [editor]);
const alignRight = useCallback(() => {
const alignVideoRight = useCallback(() => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
@@ -98,18 +94,16 @@ export function VideoMenu({ editor }: EditorMenuProps) {
.run();
}, [editor]);
const handleDownload = useCallback(() => {
if (!editorState?.src) return;
const url = getFileUrl(editorState.src);
const a = document.createElement("a");
a.href = url;
a.download = "";
a.click();
}, [editorState?.src]);
const handleDelete = useCallback(() => {
editor.commands.deleteSelection();
}, [editor]);
const onWidthChange = useCallback(
(value: number) => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
.setVideoWidth(value)
.run();
},
[editor],
);
return (
<BaseBubbleMenu
@@ -124,14 +118,13 @@ export function VideoMenu({ editor }: EditorMenuProps) {
}}
shouldShow={shouldShow}
>
<div className={classes.toolbar}>
<ActionIcon.Group className="actionIconGroup">
<Tooltip position="top" label={t("Align left")}>
<ActionIcon
onClick={alignLeft}
onClick={alignVideoLeft}
size="lg"
aria-label={t("Align left")}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isAlignLeft })}
variant={editorState?.isAlignLeft ? "light" : "default"}
>
<IconLayoutAlignLeft size={18} />
</ActionIcon>
@@ -139,11 +132,10 @@ export function VideoMenu({ editor }: EditorMenuProps) {
<Tooltip position="top" label={t("Align center")}>
<ActionIcon
onClick={alignCenter}
onClick={alignVideoCenter}
size="lg"
aria-label={t("Align center")}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isAlignCenter })}
variant={editorState?.isAlignCenter ? "light" : "default"}
>
<IconLayoutAlignCenter size={18} />
</ActionIcon>
@@ -151,40 +143,19 @@ export function VideoMenu({ editor }: EditorMenuProps) {
<Tooltip position="top" label={t("Align right")}>
<ActionIcon
onClick={alignRight}
onClick={alignVideoRight}
size="lg"
aria-label={t("Align right")}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isAlignRight })}
variant={editorState?.isAlignRight ? "light" : "default"}
>
<IconLayoutAlignRight size={18} />
</ActionIcon>
</Tooltip>
</ActionIcon.Group>
<div className={classes.divider} />
<Tooltip position="top" label={t("Download")}>
<ActionIcon
onClick={handleDownload}
size="lg"
aria-label={t("Download")}
variant="subtle"
>
<IconDownload size={18} />
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Delete")}>
<ActionIcon
onClick={handleDelete}
size="lg"
aria-label={t("Delete")}
variant="subtle"
>
<IconTrash size={18} />
</ActionIcon>
</Tooltip>
</div>
{editorState?.width && (
<NodeWidthResize onChange={onWidthChange} value={editorState.width} />
)}
</BaseBubbleMenu>
);
}
@@ -43,8 +43,6 @@ import {
Highlight,
UniqueID,
SharedStorage,
Columns,
Column,
} from "@docmost/editor-ext";
import {
randomElement,
@@ -54,14 +52,6 @@ import { IUser } from "@/features/user/types/user.types.ts";
import MathInlineView from "@/features/editor/components/math/math-inline.tsx";
import MathBlockView from "@/features/editor/components/math/math-block.tsx";
import ImageView from "@/features/editor/components/image/image-view.tsx";
import {
createImageHandle,
imageResizeClasses,
} from "@/features/editor/components/image/image-resize-handles.ts";
import {
createResizeHandle,
buildResizeClasses,
} from "@/features/editor/components/common/node-resize-handles.ts";
import CalloutView from "@/features/editor/components/callout/callout-view.tsx";
import VideoView from "@/features/editor/components/video/video-view.tsx";
import AttachmentView from "@/features/editor/components/attachment/attachment-view.tsx";
@@ -101,7 +91,6 @@ lowlight.register("fortran", fortran);
lowlight.register("haskell", haskell);
lowlight.register("scala", scala);
// @ts-ignore
export const mainExtensions = [
StarterKit.configure({
heading: false,
@@ -126,7 +115,7 @@ export const mainExtensions = [
filterTransaction: (transaction) => !isChangeOrigin(transaction),
}),
Placeholder.configure({
placeholder: ({ editor, node, pos }) => {
placeholder: ({ node }) => {
if (node.type.name === "heading") {
return i18n.t("Heading {{level}}", { level: node.attrs.level });
}
@@ -134,17 +123,6 @@ export const mainExtensions = [
return i18n.t("Toggle title");
}
if (node.type.name === "paragraph") {
const $pos = editor.state.doc.resolve(pos);
const parentName = $pos.parent.type.name;
if (
parentName === "column" ||
parentName === "tableCell" ||
parentName === "tableHeader" ||
parentName === "callout" ||
parentName === "blockquote"
) {
return i18n.t("Write...");
}
return i18n.t('Write anything. Enter "/" for commands');
}
},
@@ -222,29 +200,9 @@ export const mainExtensions = [
TiptapImage.configure({
view: ImageView,
allowBase64: false,
resize: {
enabled: true,
directions: ["left", "right"],
minWidth: 80,
minHeight: 40,
alwaysPreserveAspectRatio: true,
//@ts-ignore
createCustomHandle: createImageHandle,
className: imageResizeClasses,
},
}),
TiptapVideo.configure({
view: VideoView,
resize: {
enabled: true,
directions: ["left", "right"],
minWidth: 80,
minHeight: 40,
alwaysPreserveAspectRatio: true,
//@ts-ignore
createCustomHandle: createResizeHandle,
className: buildResizeClasses("node-video"),
},
}),
Callout.configure({
view: CalloutView,
@@ -263,29 +221,9 @@ export const mainExtensions = [
}),
Drawio.configure({
view: DrawioView,
resize: {
enabled: true,
directions: ["left", "right"],
minWidth: 80,
minHeight: 40,
alwaysPreserveAspectRatio: true,
//@ts-ignore
createCustomHandle: createResizeHandle,
className: buildResizeClasses("node-drawio"),
},
}),
Excalidraw.configure({
view: ExcalidrawView,
resize: {
enabled: true,
directions: ["left", "right"],
minWidth: 80,
minHeight: 40,
alwaysPreserveAspectRatio: true,
//@ts-ignore
createCustomHandle: createResizeHandle,
className: buildResizeClasses("node-excalidraw"),
},
}),
Embed.configure({
view: EmbedView,
@@ -315,8 +253,6 @@ export const mainExtensions = [
};
},
}).configure(),
Columns,
Column,
] as any;
type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[];
@@ -67,7 +67,6 @@ import { jwtDecode } from "jwt-decode";
import { searchSpotlight } from "@/features/search/constants.ts";
import { useEditorScroll } from "./hooks/use-editor-scroll";
import { EditorAiMenu } from "@/ee/ai/components/editor/ai-menu/ai-menu";
import ColumnsMenu from "@/features/editor/components/columns/columns-menu.tsx";
interface PageEditorProps {
pageId: string;
@@ -417,7 +416,6 @@ export default function PageEditor({
<SubpagesMenu editor={editor} />
<ExcalidrawMenu editor={editor} />
<DrawioMenu editor={editor} />
<ColumnsMenu editor={editor} />
<LinkMenu editor={editor} appendTo={menuContainerRef} />
</div>
)}
@@ -1,116 +0,0 @@
div[data-type="columns"] {
display: flex;
margin: 0.75rem 0;
padding: 0.5em;
}
div[data-type="columns"] > div[data-type="column"] {
flex: 1;
min-width: 0;
}
div[data-type="columns"] > div[data-type="column"] + div[data-type="column"] {
border-left: 1px solid transparent;
padding-left: 1rem;
transition: border 0.3s;
}
div[data-type="columns"]:hover
> div[data-type="column"]
+ div[data-type="column"] {
border-left: 1px solid
light-dark(var(--mantine-color-gray-3), var(--mantine-color-gray-7));
}
/* Confluence layout types */
div[data-type="columns"][data-layout="two_left_sidebar"]
> div[data-type="column"]:first-child {
flex: 1;
}
div[data-type="columns"][data-layout="two_left_sidebar"]
> div[data-type="column"]:last-child {
flex: 2;
}
div[data-type="columns"][data-layout="two_right_sidebar"]
> div[data-type="column"]:first-child {
flex: 2;
}
div[data-type="columns"][data-layout="two_right_sidebar"]
> div[data-type="column"]:last-child {
flex: 1;
}
div[data-type="columns"][data-layout="three_left_wide"]
> div[data-type="column"]:first-child {
flex: 2;
}
div[data-type="columns"][data-layout="three_right_wide"]
> div[data-type="column"]:last-child {
flex: 2;
}
div[data-type="columns"][data-layout="three_with_sidebars"]
> div[data-type="column"]:first-child,
div[data-type="columns"][data-layout="three_with_sidebars"]
> div[data-type="column"]:last-child {
flex: 1;
}
div[data-type="columns"][data-layout="three_with_sidebars"]
> div[data-type="column"]:nth-child(2) {
flex: 2;
}
/* Stack columns vertically on small viewports */
@media (max-width: 820px) {
div[data-type="columns"] {
flex-direction: column;
}
div[data-type="columns"] > div[data-type="column"] + div[data-type="column"] {
border-left: none;
padding-left: 0;
}
div[data-type="columns"]:hover
> div[data-type="column"]
+ div[data-type="column"] {
border-left: none;
}
}
/* Wide width mode — extends columns to full container width */
div[data-type="columns"][data-width-mode="wide"] {
margin-left: -3rem;
margin-right: -3rem;
width: calc(100% + 6rem);
}
@media (max-width: $mantine-breakpoint-sm) {
div[data-type="columns"][data-width-mode="wide"] {
margin-left: -1rem;
margin-right: -1rem;
width: calc(100% + 2rem);
}
}
@media print {
div[data-type="columns"] {
flex-direction: row !important;
}
div[data-type="columns"] > div[data-type="column"] + div[data-type="column"] {
border-left: none;
padding-left: 1rem;
}
div[data-type="columns"][data-width-mode="wide"] {
margin-left: 0;
margin-right: 0;
width: 100%;
}
}
+14 -11
View File
@@ -82,9 +82,13 @@
}
blockquote {
padding-left: 1rem;
border-left: 3px solid
light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4));
padding-left: 25px;
padding-right: 25px;
border-left: 2px solid var(--mantine-color-gray-6);
background-color: light-dark(
var(--mantine-color-gray-0),
var(--mantine-color-dark-8)
);
margin: 0;
}
@@ -122,14 +126,13 @@
margin-bottom: 0;
}
}
.react-renderer.node-callout div[style*="white-space: inherit;"] > :first-child {
margin-top: 0;
}
.react-renderer.node-callout + .react-renderer.node-callout {
margin-top: 0.75em;
&.node-callout {
div[style*="white-space: inherit;"] {
> :first-child {
margin: 0;
}
}
}
}
.selection {
@@ -13,4 +13,3 @@
@import "./mention.css";
@import "./ordered-list.css";
@import "./highlight.css";
@import "./columns.css";
+1 -5
View File
@@ -68,14 +68,10 @@ function redirectToLogin() {
APP_ROUTE.AUTH.SIGNUP,
APP_ROUTE.AUTH.FORGOT_PASSWORD,
APP_ROUTE.AUTH.PASSWORD_RESET,
APP_ROUTE.AUTH.MFA_CHALLENGE,
APP_ROUTE.AUTH.MFA_SETUP_REQUIRED,
"/invites",
];
if (!exemptPaths.some((path) => window.location.pathname.startsWith(path))) {
const redirectTo = window.location.pathname;
const params = new URLSearchParams({ redirect: redirectTo });
window.location.href = `${APP_ROUTE.AUTH.LOGIN}?${params.toString()}`;
window.location.href = APP_ROUTE.AUTH.LOGIN;
}
}
-16
View File
@@ -29,20 +29,4 @@ const APP_ROUTE = {
},
};
export function getPostLoginRedirect(): string {
const params = new URLSearchParams(window.location.search);
const redirect = params.get("redirect");
if (redirect) {
try {
const resolved = new URL(redirect, window.location.origin);
if (resolved.origin === window.location.origin) {
return resolved.pathname + resolved.search + resolved.hash;
}
} catch {
// malformed URL, fall through to default
}
}
return APP_ROUTE.HOME;
}
export default APP_ROUTE;
+3 -3
View File
@@ -42,9 +42,9 @@ if (isCloud() && isPostHogEnabled) {
});
}
const container = document.getElementById("root") as HTMLElement;
const root = (container as any).__reactRoot ??= ReactDOM.createRoot(container);
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement,
);
root.render(
<BrowserRouter>
@@ -35,8 +35,6 @@ import {
UniqueID,
addUniqueIdsToDoc,
htmlToMarkdown,
Columns,
Column,
} from '@docmost/editor-ext';
import { generateText, getSchema, JSONContent } from '@tiptap/core';
import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html';
@@ -93,8 +91,6 @@ export const tiptapExtensions = [
Embed,
Mention,
Subpages,
Columns,
Column,
] as any;
export function jsonToHtml(tiptapJson: any) {
@@ -2,7 +2,6 @@ import { MultipartFile } from '@fastify/multipart';
import * as path from 'path';
import { AttachmentType } from './attachment.constants';
import { sanitizeFileName } from '../../common/helpers';
import { getMimeType } from '../../common/helpers';
export interface PreparedFile {
buffer?: Buffer;
@@ -41,7 +40,7 @@ export async function prepareFile(
fileName,
fileSize,
fileExtension,
mimeType: getMimeType(file.filename),
mimeType: file.mimetype,
multiPartFile: file,
};
} catch (error) {
+2
View File
@@ -16,6 +16,7 @@ import { GroupModule } from './group/group.module';
import { CaslModule } from './casl/casl.module';
import { DomainMiddleware } from '../common/middlewares/domain.middleware';
import { ShareModule } from './share/share.module';
import { LabelModule } from './label/label.module';
import { NotificationModule } from './notification/notification.module';
import { WatcherModule } from './watcher/watcher.module';
@@ -32,6 +33,7 @@ import { WatcherModule } from './watcher/watcher.module';
GroupModule,
CaslModule,
ShareModule,
LabelModule,
NotificationModule,
WatcherModule,
],
@@ -0,0 +1,61 @@
import {
ArrayMaxSize,
ArrayMinSize,
IsArray,
IsNotEmpty,
IsOptional,
IsString,
IsUUID,
Matches,
MaxLength,
} from 'class-validator';
import { Transform } from 'class-transformer';
function normalizeLabel(name: string): string {
return name.trim().replace(/\s+/g, '-').toLowerCase();
}
export class AddLabelsDto {
@IsString()
@IsNotEmpty()
pageId: string;
@IsArray()
@ArrayMinSize(1)
@ArrayMaxSize(25)
@IsString({ each: true })
@IsNotEmpty({ each: true })
@Transform(({ value }) =>
Array.isArray(value) ? value.map(normalizeLabel) : value,
)
@MaxLength(100, { each: true })
@Matches(/^[a-z0-9_~-]+$/, {
each: true,
message: 'Label names can only contain letters, numbers, hyphens, underscores, and tildes',
})
names: string[];
}
export class RemoveLabelDto {
@IsString()
@IsNotEmpty()
pageId: string;
@IsUUID()
labelId: string;
}
export class PageLabelsDto {
@IsString()
@IsNotEmpty()
pageId: string;
}
export class SearchPagesByLabelDto {
@IsUUID()
labelId: string;
@IsOptional()
@IsUUID()
spaceId?: string;
}
@@ -0,0 +1,135 @@
import {
Body,
Controller,
ForbiddenException,
HttpCode,
HttpStatus,
NotFoundException,
Post,
UseGuards,
} from '@nestjs/common';
import { LabelService } from './label.service';
import {
AddLabelsDto,
PageLabelsDto,
RemoveLabelDto,
SearchPagesByLabelDto,
} from './dto/label.dto';
import { AuthUser } from '../../common/decorators/auth-user.decorator';
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { User, Workspace } from '@docmost/db/types/entity.types';
import SpaceAbilityFactory from '../casl/abilities/space-ability.factory';
import {
SpaceCaslAction,
SpaceCaslSubject,
} from '../casl/interfaces/space-ability.type';
import { PageRepo } from '@docmost/db/repos/page/page.repo';
import { LabelRepo } from '@docmost/db/repos/label/label.repo';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
@UseGuards(JwtAuthGuard)
@Controller('labels')
export class LabelController {
constructor(
private readonly labelService: LabelService,
private readonly labelRepo: LabelRepo,
private readonly pageRepo: PageRepo,
private readonly spaceAbility: SpaceAbilityFactory,
) {}
@HttpCode(HttpStatus.OK)
@Post('/')
async getLabels(
@Body() pagination: PaginationOptions,
@AuthWorkspace() workspace: Workspace,
) {
return this.labelService.getLabels(workspace.id, pagination);
}
@HttpCode(HttpStatus.OK)
@Post('add')
async addLabels(
@Body() dto: AddLabelsDto,
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
) {
const page = await this.pageRepo.findById(dto.pageId);
if (!page || page.deletedAt) {
throw new NotFoundException('Page not found');
}
const ability = await this.spaceAbility.createForUser(user, page.spaceId);
if (ability.cannot(SpaceCaslAction.Edit, SpaceCaslSubject.Page)) {
throw new ForbiddenException();
}
return this.labelService.addLabelsToPage(
page.id,
dto.names,
workspace.id,
);
}
@HttpCode(HttpStatus.OK)
@Post('remove')
async removeLabel(
@Body() dto: RemoveLabelDto,
@AuthUser() user: User,
) {
const page = await this.pageRepo.findById(dto.pageId);
if (!page || page.deletedAt) {
throw new NotFoundException('Page not found');
}
const ability = await this.spaceAbility.createForUser(user, page.spaceId);
if (ability.cannot(SpaceCaslAction.Edit, SpaceCaslSubject.Page)) {
throw new ForbiddenException();
}
await this.labelService.removeLabelFromPage(page.id, dto.labelId);
}
@HttpCode(HttpStatus.OK)
@Post('page')
async getPageLabels(
@Body() dto: PageLabelsDto,
@Body() pagination: PaginationOptions,
@AuthUser() user: User,
) {
const page = await this.pageRepo.findById(dto.pageId);
if (!page) {
throw new NotFoundException('Page not found');
}
const ability = await this.spaceAbility.createForUser(user, page.spaceId);
if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
throw new ForbiddenException();
}
return this.labelService.getPageLabels(page.id, pagination);
}
@HttpCode(HttpStatus.OK)
@Post('search-pages')
async searchPagesByLabel(
@Body() dto: SearchPagesByLabelDto,
@AuthUser() user: User,
) {
const label = await this.labelRepo.findById(dto.labelId);
if (!label) {
throw new NotFoundException('Label not found');
}
if (dto.spaceId) {
const ability = await this.spaceAbility.createForUser(user, dto.spaceId);
if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
throw new ForbiddenException();
}
}
return this.labelService.searchPagesByLabel(label.id, user.id, {
spaceId: dto.spaceId,
});
}
}
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { LabelController } from './label.controller';
import { LabelService } from './label.service';
@Module({
controllers: [LabelController],
providers: [LabelService],
exports: [LabelService],
})
export class LabelModule {}
@@ -0,0 +1,82 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { LabelRepo, LabelType } from '@docmost/db/repos/label/label.repo';
import { InjectKysely } from 'nestjs-kysely';
import { KyselyDB } from '@docmost/db/types/kysely.types';
import { executeTx } from '@docmost/db/utils';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
const MAX_LABELS_PER_PAGE = 25;
@Injectable()
export class LabelService {
constructor(
private readonly labelRepo: LabelRepo,
@InjectKysely() private readonly db: KyselyDB,
) {}
async addLabelsToPage(
pageId: string,
names: string[],
workspaceId: string,
) {
await executeTx(this.db, async (trx) => {
const currentCount = await this.labelRepo.getPageLabelCount(pageId, trx);
if (currentCount + names.length > MAX_LABELS_PER_PAGE) {
throw new BadRequestException(
`A page can have a maximum of ${MAX_LABELS_PER_PAGE} labels`,
);
}
for (const name of names) {
const label = await this.labelRepo.findOrCreate(
name.trim(),
workspaceId,
LabelType.PAGE,
trx,
);
await this.labelRepo.addLabelToPage(pageId, label.id, trx);
}
});
return this.labelRepo.findLabelsByPageId(pageId, { limit: 100 } as PaginationOptions);
}
async removeLabelFromPage(
pageId: string,
labelId: string,
): Promise<void> {
await executeTx(this.db, async (trx) => {
await this.labelRepo.removeLabelFromPage(pageId, labelId, trx);
const count = await this.labelRepo.getLabelPageCount(labelId, trx);
if (count === 0) {
await this.labelRepo.deleteLabel(labelId, trx);
}
});
}
async getPageLabels(pageId: string, pagination: PaginationOptions) {
return this.labelRepo.findLabelsByPageId(pageId, pagination);
}
async getLabels(
workspaceId: string,
pagination: PaginationOptions,
) {
return this.labelRepo.findLabels(workspaceId, LabelType.PAGE, pagination);
}
async searchPagesByLabel(
labelId: string,
userId: string,
opts?: { spaceId?: string },
) {
return this.labelRepo.findPagesByLabelId(labelId, userId, opts);
}
async cleanupOrphanedLabels(pageIds: string[]): Promise<void> {
const labelIds = await this.labelRepo.findLabelIdsByPageIds(pageIds);
if (labelIds.length === 0) return;
await this.labelRepo.deleteOrphanedLabels(labelIds);
}
}
@@ -48,6 +48,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
import { CollaborationGateway } from '../../../collaboration/collaboration.gateway';
import { markdownToHtml } from '@docmost/editor-ext';
import { WatcherService } from '../../watcher/watcher.service';
import { LabelRepo } from '@docmost/db/repos/label/label.repo';
@Injectable()
export class PageService {
@@ -64,6 +65,7 @@ export class PageService {
private eventEmitter: EventEmitter2,
private collaborationGateway: CollaborationGateway,
private readonly watcherService: WatcherService,
private readonly labelRepo: LabelRepo,
) {}
async findById(
@@ -729,11 +731,18 @@ export class PageService {
}
if (pageIds.length > 0) {
const affectedLabelIds =
await this.labelRepo.findLabelIdsByPageIds(pageIds);
await this.db.deleteFrom('pages').where('id', 'in', pageIds).execute();
this.eventEmitter.emit(EventName.PAGE_DELETED, {
pageIds: pageIds,
workspaceId,
});
if (affectedLabelIds.length > 0) {
await this.labelRepo.deleteOrphanedLabels(affectedLabelIds);
}
}
}
@@ -5,6 +5,7 @@ import { KyselyDB } from '@docmost/db/types/kysely.types';
import { InjectQueue } from '@nestjs/bullmq';
import { Queue } from 'bullmq';
import { QueueJob, QueueName } from '../../../integrations/queue/constants';
import { LabelRepo } from '@docmost/db/repos/label/label.repo';
@Injectable()
export class TrashCleanupService {
@@ -14,6 +15,7 @@ export class TrashCleanupService {
constructor(
@InjectKysely() private readonly db: KyselyDB,
@InjectQueue(QueueName.ATTACHMENT_QUEUE) private attachmentQueue: Queue,
private readonly labelRepo: LabelRepo,
) {}
@Interval('trash-cleanup', 24 * 60 * 60 * 1000) // every 24 hours
@@ -104,7 +106,14 @@ export class TrashCleanupService {
try {
if (pageIds.length > 0) {
const affectedLabelIds =
await this.labelRepo.findLabelIdsByPageIds(pageIds);
await this.db.deleteFrom('pages').where('id', 'in', pageIds).execute();
if (affectedLabelIds.length > 0) {
await this.labelRepo.deleteOrphanedLabels(affectedLabelIds);
}
}
} catch (error) {
// Log but don't throw - pages might have been deleted by another node
@@ -26,6 +26,7 @@ import { BacklinkRepo } from '@docmost/db/repos/backlink/backlink.repo';
import { ShareRepo } from '@docmost/db/repos/share/share.repo';
import { NotificationRepo } from '@docmost/db/repos/notification/notification.repo';
import { WatcherRepo } from '@docmost/db/repos/watcher/watcher.repo';
import { LabelRepo } from '@docmost/db/repos/label/label.repo';
import { PageListener } from '@docmost/db/listeners/page.listener';
import { PostgresJSDialect } from 'kysely-postgres-js';
import * as postgres from 'postgres';
@@ -84,6 +85,7 @@ import { normalizePostgresUrl } from '../common/helpers';
ShareRepo,
NotificationRepo,
WatcherRepo,
LabelRepo,
PageListener,
],
exports: [
@@ -102,6 +104,7 @@ import { normalizePostgresUrl } from '../common/helpers';
ShareRepo,
NotificationRepo,
WatcherRepo,
LabelRepo,
],
})
export class DatabaseModule
@@ -0,0 +1,59 @@
import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createTable('labels')
.addColumn('id', 'uuid', (col) =>
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
)
.addColumn('name', 'varchar', (col) => col.notNull())
.addColumn('type', 'varchar', (col) => col.notNull().defaultTo('page'))
.addColumn('workspace_id', 'uuid', (col) =>
col.references('workspaces.id').onDelete('cascade').notNull(),
)
.addColumn('created_at', 'timestamptz', (col) =>
col.notNull().defaultTo(sql`now()`),
)
.addColumn('updated_at', 'timestamptz', (col) =>
col.notNull().defaultTo(sql`now()`),
)
.execute();
await db.schema
.createIndex('labels_workspace_id_name_unique')
.on('labels')
.columns(['workspace_id', 'name', 'type'])
.unique()
.execute();
await db.schema
.createTable('page_labels')
.addColumn('id', 'uuid', (col) =>
col.primaryKey().defaultTo(sql`gen_uuid_v7()`),
)
.addColumn('page_id', 'uuid', (col) =>
col.references('pages.id').onDelete('cascade').notNull(),
)
.addColumn('label_id', 'uuid', (col) =>
col.references('labels.id').onDelete('cascade').notNull(),
)
.addColumn('created_at', 'timestamptz', (col) =>
col.notNull().defaultTo(sql`now()`),
)
.addUniqueConstraint('page_labels_page_id_label_id_unique', [
'page_id',
'label_id',
])
.execute();
await db.schema
.createIndex('page_labels_label_id_idx')
.on('page_labels')
.column('label_id')
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropTable('page_labels').execute();
await db.schema.dropTable('labels').execute();
}
@@ -0,0 +1,279 @@
import { Injectable } from '@nestjs/common';
import { InjectKysely } from 'nestjs-kysely';
import { KyselyDB, KyselyTransaction } from '../../types/kysely.types';
import { Label } from '@docmost/db/types/entity.types';
import { dbOrTx } from '@docmost/db/utils';
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
export const LabelType = {
PAGE: 'page',
SPACE: 'space',
} as const;
export type LabelType = (typeof LabelType)[keyof typeof LabelType];
@Injectable()
export class LabelRepo {
constructor(
@InjectKysely() private readonly db: KyselyDB,
private readonly spaceMemberRepo: SpaceMemberRepo,
) {}
async findById(
labelId: string,
trx?: KyselyTransaction,
): Promise<Label | undefined> {
const db = dbOrTx(this.db, trx);
return db
.selectFrom('labels')
.selectAll()
.where('id', '=', labelId)
.executeTakeFirst();
}
async findByNameAndWorkspace(
name: string,
workspaceId: string,
type: LabelType,
trx?: KyselyTransaction,
): Promise<Label | undefined> {
const db = dbOrTx(this.db, trx);
return db
.selectFrom('labels')
.selectAll()
.where('name', '=', name.toLowerCase())
.where('type', '=', type)
.where('workspaceId', '=', workspaceId)
.executeTakeFirst();
}
async findOrCreate(
name: string,
workspaceId: string,
type: LabelType,
trx?: KyselyTransaction,
): Promise<Label> {
const db = dbOrTx(this.db, trx);
const normalizedName = name.trim().toLowerCase();
const result = await db
.insertInto('labels')
.values({ name: normalizedName, type, workspaceId })
.onConflict((oc) =>
oc.columns(['name', 'type', 'workspaceId']).doNothing(),
)
.returningAll()
.executeTakeFirst();
if (result) {
return result;
}
return this.findByNameAndWorkspace(normalizedName, workspaceId, type, trx);
}
async findLabelsByPageId(pageId: string, pagination: PaginationOptions) {
const query = this.db
.selectFrom('labels')
.innerJoin('pageLabels', 'pageLabels.labelId', 'labels.id')
.select([
'labels.id',
'labels.name',
'labels.type',
'labels.createdAt',
'labels.updatedAt',
'labels.workspaceId',
])
.where('pageLabels.pageId', '=', pageId)
.where('labels.type', '=', LabelType.PAGE);
return executeWithCursorPagination(query, {
perPage: pagination.limit,
cursor: pagination.cursor,
beforeCursor: pagination.beforeCursor,
fields: [
{ expression: 'labels.name', direction: 'asc', key: 'name' },
{ expression: 'labels.id', direction: 'asc', key: 'id' },
],
parseCursor: (cursor) => ({
name: cursor.name,
id: cursor.id,
}),
});
}
async findLabels(
workspaceId: string,
type: LabelType,
pagination: PaginationOptions,
) {
let query = this.db
.selectFrom('labels')
.select(['id', 'name', 'type', 'createdAt', 'updatedAt', 'workspaceId'])
.where('workspaceId', '=', workspaceId)
.where('type', '=', type);
if (pagination.query) {
query = query.where(
'name',
'like',
`%${pagination.query.toLowerCase()}%`,
);
}
return executeWithCursorPagination(query, {
perPage: pagination.limit,
cursor: pagination.cursor,
beforeCursor: pagination.beforeCursor,
fields: [
{ expression: 'name', direction: 'asc' },
{ expression: 'id', direction: 'asc' },
],
parseCursor: (cursor) => ({
name: cursor.name,
id: cursor.id,
}),
});
}
async addLabelToPage(
pageId: string,
labelId: string,
trx?: KyselyTransaction,
): Promise<void> {
const db = dbOrTx(this.db, trx);
await db
.insertInto('pageLabels')
.values({ pageId, labelId })
.onConflict((oc) => oc.doNothing())
.execute();
}
async removeLabelFromPage(
pageId: string,
labelId: string,
trx?: KyselyTransaction,
): Promise<void> {
const db = dbOrTx(this.db, trx);
await db
.deleteFrom('pageLabels')
.where('pageId', '=', pageId)
.where('labelId', '=', labelId)
.execute();
}
async getPageLabelCount(
pageId: string,
trx?: KyselyTransaction,
): Promise<number> {
const db = dbOrTx(this.db, trx);
const result = await db
.selectFrom('pageLabels')
.select((eb) => eb.fn.count('id').as('count'))
.where('pageId', '=', pageId)
.executeTakeFirst();
return Number(result?.count ?? 0);
}
async getLabelPageCount(
labelId: string,
trx?: KyselyTransaction,
): Promise<number> {
const db = dbOrTx(this.db, trx);
const result = await db
.selectFrom('pageLabels')
.select((eb) => eb.fn.count('id').as('count'))
.where('labelId', '=', labelId)
.executeTakeFirst();
return Number(result?.count ?? 0);
}
async deleteLabel(
labelId: string,
trx?: KyselyTransaction,
): Promise<void> {
const db = dbOrTx(this.db, trx);
await db
.deleteFrom('labels')
.where('id', '=', labelId)
.execute();
}
async deleteOrphanedLabels(
labelIds: string[],
trx?: KyselyTransaction,
): Promise<void> {
if (labelIds.length === 0) return;
const db = dbOrTx(this.db, trx);
const labelsWithPages = await db
.selectFrom('pageLabels')
.select('labelId')
.where('labelId', 'in', labelIds)
.groupBy('labelId')
.execute();
const labelsStillInUse = new Set(labelsWithPages.map((r) => r.labelId));
const orphanedIds = labelIds.filter((id) => !labelsStillInUse.has(id));
if (orphanedIds.length > 0) {
await db
.deleteFrom('labels')
.where('id', 'in', orphanedIds)
.execute();
}
}
async findPagesByLabelId(
labelId: string,
userId: string,
opts?: { spaceId?: string },
) {
let query = this.db
.selectFrom('pages')
.innerJoin('pageLabels', 'pageLabels.pageId', 'pages.id')
.select([
'pages.id',
'pages.slugId',
'pages.title',
'pages.icon',
'pages.spaceId',
'pages.createdAt',
'pages.updatedAt',
])
.where('pageLabels.labelId', '=', labelId)
.where('pages.deletedAt', 'is', null);
if (opts?.spaceId) {
query = query.where('pages.spaceId', '=', opts.spaceId);
} else {
query = query.where(
'pages.spaceId',
'in',
this.spaceMemberRepo.getUserSpaceIdsQuery(userId),
);
}
return query.orderBy('pages.updatedAt', 'desc').execute();
}
async findLabelIdsByPageIds(
pageIds: string[],
trx?: KyselyTransaction,
): Promise<string[]> {
if (pageIds.length === 0) return [];
const db = dbOrTx(this.db, trx);
const results = await db
.selectFrom('pageLabels')
.select('labelId')
.where('pageId', 'in', pageIds)
.groupBy('labelId')
.execute();
return results.map((r) => r.labelId);
}
}
+18
View File
@@ -390,6 +390,22 @@ export interface Watchers {
createdAt: Generated<Timestamp>;
}
export interface Labels {
id: Generated<string>;
name: string;
type: Generated<string>;
workspaceId: string;
createdAt: Generated<Timestamp>;
updatedAt: Generated<Timestamp>;
}
export interface PageLabels {
id: Generated<string>;
pageId: string;
labelId: string;
createdAt: Generated<Timestamp>;
}
export interface DB {
apiKeys: ApiKeys;
attachments: Attachments;
@@ -401,8 +417,10 @@ export interface DB {
fileTasks: FileTasks;
groups: Groups;
groupUsers: GroupUsers;
labels: Labels;
notifications: Notifications;
pageHistory: PageHistory;
pageLabels: PageLabels;
pages: Pages;
shares: Shares;
spaceMembers: SpaceMembers;
+51 -2
View File
@@ -1,6 +1,55 @@
import { DB } from '@docmost/db/types/db';
import {
ApiKeys,
Attachments,
AuthAccounts,
AuthProviders,
Backlinks,
Billing,
Comments,
FileTasks,
Groups,
GroupUsers,
Labels,
Notifications,
PageHistory,
PageLabels,
Pages,
Shares,
SpaceMembers,
Spaces,
UserMfa,
Users,
UserTokens,
Watchers,
WorkspaceInvitations,
Workspaces,
} from '@docmost/db/types/db';
import { PageEmbeddings } from '@docmost/db/types/embeddings.types';
export interface DbInterface extends DB {
export interface DbInterface {
attachments: Attachments;
authAccounts: AuthAccounts;
authProviders: AuthProviders;
backlinks: Backlinks;
billing: Billing;
comments: Comments;
fileTasks: FileTasks;
groups: Groups;
groupUsers: GroupUsers;
labels: Labels;
notifications: Notifications;
pageEmbeddings: PageEmbeddings;
pageHistory: PageHistory;
pageLabels: PageLabels;
pages: Pages;
shares: Shares;
spaceMembers: SpaceMembers;
spaces: Spaces;
userMfa: UserMfa;
users: Users;
userTokens: UserTokens;
watchers: Watchers;
workspaceInvitations: WorkspaceInvitations;
workspaces: Workspaces;
apiKeys: ApiKeys;
}
@@ -3,7 +3,9 @@ import {
Attachments,
Comments,
Groups,
Labels,
Notifications,
PageLabels,
Pages,
Spaces,
Users,
@@ -143,3 +145,12 @@ export type UpdatableNotification = Updateable<Omit<Notifications, 'id'>>;
export type Watcher = Selectable<Watchers>;
export type InsertableWatcher = Insertable<Watchers>;
export type UpdatableWatcher = Updateable<Omit<Watchers, 'id'>>;
// Label
export type Label = Selectable<Labels>;
export type InsertableLabel = Insertable<Labels>;
export type UpdatableLabel = Updateable<Omit<Labels, 'id'>>;
// PageLabel
export type PageLabel = Selectable<PageLabels>;
export type InsertablePageLabel = Insertable<PageLabels>;
+1 -2
View File
@@ -96,8 +96,7 @@
"packageManager": "pnpm@10.4.0",
"pnpm": {
"patchedDependencies": {
"react-arborist@3.4.0": "patches/react-arborist@3.4.0.patch",
"@tiptap/core": "patches/@tiptap__core.patch"
"react-arborist@3.4.0": "patches/react-arborist@3.4.0.patch"
},
"overrides": {
"jsdom": "25.0.1",
-1
View File
@@ -25,4 +25,3 @@ export * from "./lib/heading/heading";
export * from "./lib/unique-id";
export * from "./lib/shared-storage";
export * from "./lib/recreate-transform";
export * from "./lib/columns";
+3 -16
View File
@@ -1,21 +1,8 @@
export type CalloutType =
| 'default'
| 'info'
| 'note'
| 'success'
| 'warning'
| 'danger';
const validCalloutTypes = [
'default',
'info',
'note',
'success',
'warning',
'danger',
];
export type CalloutType = "default" | "info" | "success" | "warning" | "danger";
const validCalloutTypes = ["default", "info", "success", "warning", "danger"];
export function getValidCalloutType(value: string): string {
if (value) {
return validCalloutTypes.includes(value) ? value : 'info';
return validCalloutTypes.includes(value) ? value : "info";
}
}
@@ -1,127 +0,0 @@
import { Node, mergeAttributes, findParentNode } from "@tiptap/core";
import { TextSelection } from "prosemirror-state";
export interface ColumnOptions {
HTMLAttributes: Record<string, any>;
}
export interface ColumnAttributes {
width?: number | null;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
column: {
setColumnWidth: (width: number | null) => ReturnType;
};
}
}
export const Column = Node.create<ColumnOptions>({
name: "column",
group: "block",
content: "block+",
defining: true,
isolating: true,
selectable: false,
addOptions() {
return {
HTMLAttributes: {},
};
},
addAttributes() {
return {
width: {
default: null,
parseHTML: (element) => {
const value = element.getAttribute("data-width");
return value ? parseFloat(value) : null;
},
renderHTML: (attributes: ColumnAttributes) => {
if (!attributes.width) return {};
return {
"data-width": attributes.width,
style: `flex: ${attributes.width}`,
};
},
},
};
},
parseHTML() {
return [
{
tag: `div[data-type="${this.name}"]`,
},
];
},
renderHTML({ HTMLAttributes }) {
return [
"div",
mergeAttributes(
{ "data-type": this.name },
this.options.HTMLAttributes,
HTMLAttributes,
),
0,
];
},
addKeyboardShortcuts() {
const jumpToColumn = (direction: 1 | -1) => () => {
const { state, dispatch } = this.editor.view;
const columns = findParentNode(
(node) => node.type.name === "columns",
)(state.selection);
if (!columns) return false;
const column = findParentNode(
(node) => node.type.name === "column",
)(state.selection);
if (!column) return false;
let currentIndex = -1;
columns.node.forEach((_child, offset, index) => {
if (columns.pos + 1 + offset === column.pos) {
currentIndex = index;
}
});
const targetIndex = currentIndex + direction;
if (targetIndex < 0 || targetIndex >= columns.node.childCount) {
return true;
}
let offset = 0;
for (let j = 0; j < targetIndex; j++) {
offset += columns.node.child(j).nodeSize;
}
const targetPos = columns.pos + 1 + offset + 1 + 1;
if (dispatch) {
dispatch(
state.tr.setSelection(TextSelection.create(state.doc, targetPos)),
);
}
return true;
};
return {
Tab: jumpToColumn(1),
"Shift-Tab": jumpToColumn(-1),
};
},
addCommands() {
return {
setColumnWidth:
(width) =>
({ commands }) =>
commands.updateAttributes("column", { width }),
};
},
});
@@ -1,196 +0,0 @@
import { Node, mergeAttributes, findParentNode } from "@tiptap/core";
import { Fragment, Node as PMNode } from "prosemirror-model";
import { TextSelection } from "prosemirror-state";
export type ColumnsLayout =
| "two_equal"
| "two_left_sidebar"
| "two_right_sidebar"
| "three_equal"
| "three_left_wide"
| "three_right_wide"
| "three_with_sidebars"
| "four_equal"
| "five_equal";
export interface ColumnsOptions {
HTMLAttributes: Record<string, any>;
}
export type WidthMode = "normal" | "wide";
export interface ColumnsAttributes {
layout?: ColumnsLayout;
widthMode?: WidthMode;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
columns: {
insertColumns: (attributes?: ColumnsAttributes) => ReturnType;
setColumnsWidthMode: (widthMode: WidthMode) => ReturnType;
setColumnCount: (count: number) => ReturnType;
setColumnsLayout: (layout: ColumnsLayout) => ReturnType;
};
}
}
function columnCountFromLayout(layout: string): number {
if (layout.startsWith("five")) return 5;
if (layout.startsWith("four")) return 4;
if (layout.startsWith("three")) return 3;
return 2;
}
function defaultLayoutForCount(count: number): ColumnsLayout {
if (count === 3) return "three_equal";
if (count === 4) return "four_equal";
if (count === 5) return "five_equal";
return "two_equal";
}
export const Columns = Node.create<ColumnsOptions>({
name: "columns",
group: "block",
content: "column+",
defining: true,
isolating: true,
addOptions() {
return {
HTMLAttributes: {},
};
},
addAttributes() {
return {
layout: {
default: "two_equal",
parseHTML: (element) => element.getAttribute("data-layout"),
renderHTML: (attributes: ColumnsAttributes) => ({
"data-layout": attributes.layout,
}),
},
widthMode: {
default: "normal",
parseHTML: (element) =>
element.getAttribute("data-width-mode") || "normal",
renderHTML: (attributes: ColumnsAttributes) => {
if (!attributes.widthMode || attributes.widthMode === "normal")
return {};
return { "data-width-mode": attributes.widthMode };
},
},
};
},
parseHTML() {
return [
{
tag: `div[data-type="${this.name}"]`,
},
];
},
renderHTML({ HTMLAttributes }) {
return [
"div",
mergeAttributes(
{ "data-type": this.name },
this.options.HTMLAttributes,
HTMLAttributes,
),
0,
];
},
addCommands() {
return {
insertColumns:
(attributes) =>
({ tr, state, dispatch }) => {
const layout = attributes?.layout || "two_equal";
const count = columnCountFromLayout(layout);
const columnType = state.schema.nodes.column;
const paraType = state.schema.nodes.paragraph;
const children = Array.from({ length: count }, () =>
columnType.create(null, paraType.create()),
);
const columnsNode = this.type.create(
attributes,
Fragment.from(children),
);
const stepsBefore = tr.steps.length;
tr.replaceSelectionWith(columnsNode);
if (tr.steps.length > stepsBefore) {
const stepMap = tr.steps[tr.steps.length - 1].getMap();
let insertStart = 0;
stepMap.forEach((_from, _to, newFrom) => {
insertStart = newFrom;
});
tr.setSelection(
TextSelection.near(tr.doc.resolve(insertStart + 1), 1),
);
}
if (dispatch) dispatch(tr);
return true;
},
setColumnsWidthMode:
(widthMode) =>
({ commands }) =>
commands.updateAttributes("columns", { widthMode }),
setColumnCount:
(count: number) =>
({ tr, state }) => {
const predicate = (node: PMNode) => node.type.name === "columns";
const parent = findParentNode(predicate)(state.selection);
if (!parent) return false;
const { node: columnsNode, pos: parentPos } = parent;
const currentCount = columnsNode.childCount;
if (count === currentCount || count < 2 || count > 5) return false;
const columnType = state.schema.nodes.column;
const paraType = state.schema.nodes.paragraph;
const newChildren: PMNode[] = [];
if (count > currentCount) {
for (let i = 0; i < currentCount; i++) {
newChildren.push(columnsNode.child(i));
}
for (let i = currentCount; i < count; i++) {
newChildren.push(columnType.create(null, paraType.create()));
}
} else {
for (let i = 0; i < count - 1; i++) {
newChildren.push(columnsNode.child(i));
}
let mergedContent = columnsNode.child(count - 1).content;
for (let j = count; j < currentCount; j++) {
mergedContent = mergedContent.append(columnsNode.child(j).content);
}
newChildren.push(columnType.create(null, mergedContent));
}
const newLayout = defaultLayoutForCount(count);
const newNode = columnsNode.type.create(
{ ...columnsNode.attrs, layout: newLayout },
Fragment.from(newChildren),
);
tr.replaceWith(parentPos, parentPos + columnsNode.nodeSize, newNode);
return true;
},
setColumnsLayout:
(layout) =>
({ commands }) =>
commands.updateAttributes("columns", { layout }),
};
},
});
@@ -1,4 +0,0 @@
export { Columns } from "./columns";
export type { ColumnsOptions, ColumnsAttributes, ColumnsLayout, WidthMode } from "./columns";
export { Column } from "./column";
export type { ColumnOptions, ColumnAttributes } from "./column";
+8 -215
View File
@@ -1,35 +1,15 @@
import { Node, mergeAttributes, ResizableNodeView } from "@tiptap/core";
import type { ResizableNodeViewDirection } from "@tiptap/core";
import { Node, mergeAttributes } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
export type DrawioResizeOptions = {
enabled: boolean;
directions?: ResizableNodeViewDirection[];
minWidth?: number;
minHeight?: number;
alwaysPreserveAspectRatio?: boolean;
createCustomHandle?: (direction: ResizableNodeViewDirection) => HTMLElement;
className?: {
container?: string;
wrapper?: string;
handle?: string;
resizing?: string;
};
};
export interface DrawioOptions {
HTMLAttributes: Record<string, any>;
view: any;
resize: DrawioResizeOptions | false;
}
export interface DrawioAttributes {
src?: string;
title?: string;
size?: number;
width?: number | string;
height?: number;
aspectRatio?: number;
width?: string;
align?: string;
attachmentId?: string;
}
@@ -38,8 +18,6 @@ declare module "@tiptap/core" {
interface Commands<ReturnType> {
drawio: {
setDrawio: (attributes?: DrawioAttributes) => ReturnType;
setDrawioAlign: (align: "left" | "center" | "right") => ReturnType;
setDrawioSize: (width: number, height: number) => ReturnType;
};
}
}
@@ -57,7 +35,6 @@ export const Drawio = Node.create<DrawioOptions>({
return {
HTMLAttributes: {},
view: null,
resize: false,
};
},
@@ -78,30 +55,12 @@ export const Drawio = Node.create<DrawioOptions>({
}),
},
width: {
default: null,
parseHTML: (element) => {
const raw = element.getAttribute("data-width");
if (!raw) return null;
if (raw.endsWith("%")) return raw;
const num = parseFloat(raw);
return isNaN(num) ? null : num;
},
default: "100%",
parseHTML: (element) => element.getAttribute("data-width"),
renderHTML: (attributes: DrawioAttributes) => ({
"data-width": attributes.width,
}),
},
height: {
default: null,
parseHTML: (element) => {
const raw = element.getAttribute("data-height");
if (!raw) return null;
const num = parseFloat(raw);
return isNaN(num) ? null : num;
},
renderHTML: (attributes: DrawioAttributes) => ({
"data-height": attributes.height,
}),
},
size: {
default: null,
parseHTML: (element) => element.getAttribute("data-size"),
@@ -109,13 +68,6 @@ export const Drawio = Node.create<DrawioOptions>({
"data-size": attributes.size,
}),
},
aspectRatio: {
default: null,
parseHTML: (element) => element.getAttribute("data-aspect-ratio"),
renderHTML: (attributes: DrawioAttributes) => ({
"data-aspect-ratio": attributes.aspectRatio,
}),
},
align: {
default: "center",
parseHTML: (element) => element.getAttribute("data-align"),
@@ -147,7 +99,7 @@ export const Drawio = Node.create<DrawioOptions>({
mergeAttributes(
{ "data-type": this.name },
this.options.HTMLAttributes,
HTMLAttributes,
HTMLAttributes
),
[
"img",
@@ -170,172 +122,13 @@ export const Drawio = Node.create<DrawioOptions>({
attrs: attrs,
});
},
setDrawioAlign:
(align) =>
({ commands }) =>
commands.updateAttributes("drawio", { align }),
setDrawioSize:
(width, height) =>
({ commands }) =>
commands.updateAttributes("drawio", { width, height }),
};
},
addNodeView() {
const resize = this.options.resize;
// 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;
if (!resize || !resize.enabled) {
this.editor.isInitialized = true;
return ReactNodeViewRenderer(this.options.view);
}
const {
directions,
minWidth,
minHeight,
alwaysPreserveAspectRatio,
createCustomHandle,
className,
} = resize;
return (props) => {
const { node, getPos, HTMLAttributes, editor } = props;
if (!node.attrs.src) {
editor.isInitialized = true;
const reactView = ReactNodeViewRenderer(this.options.view);
const view = reactView(props);
const originalUpdate = view.update?.bind(view);
view.update = (updatedNode, decorations, innerDecorations) => {
if (updatedNode.attrs.src && !node.attrs.src) {
return false;
}
if (originalUpdate) {
return originalUpdate(updatedNode, decorations, innerDecorations);
}
return true;
};
return view;
}
const el = document.createElement("img");
el.src = node.attrs.src;
el.alt = node.attrs.title || "";
el.style.display = "block";
el.style.maxWidth = "100%";
el.style.borderRadius = "8px";
let currentNode = node;
const nodeView = new ResizableNodeView({
element: el,
editor,
node,
getPos,
onResize: (w, h) => {
el.style.width = `${w}px`;
el.style.height = `${h}px`;
},
onCommit: () => {
const pos = getPos();
if (pos === undefined) return;
this.editor
.chain()
.setNodeSelection(pos)
.updateAttributes(this.name, {
width: Math.round(el.offsetWidth),
height: Math.round(el.offsetHeight),
})
.run();
},
onUpdate: (updatedNode, _decorations, _innerDecorations) => {
if (updatedNode.type !== currentNode.type) {
return false;
}
if (updatedNode.attrs.src !== currentNode.attrs.src) {
el.src = updatedNode.attrs.src || "";
}
const w = updatedNode.attrs.width;
const h = updatedNode.attrs.height;
if (w != null) {
el.style.width = `${w}px`;
}
if (h != null) {
el.style.height = `${h}px`;
}
const align = updatedNode.attrs.align || "center";
const container = nodeView.dom as HTMLElement;
applyAlignment(container, align);
currentNode = updatedNode;
return true;
},
options: {
directions,
min: {
width: minWidth,
height: minHeight,
},
preserveAspectRatio: alwaysPreserveAspectRatio === true,
createCustomHandle,
className,
},
});
const dom = nodeView.dom as HTMLElement;
applyAlignment(dom, node.attrs.align || "center");
// Handle percentage width backward compat
const widthAttr = node.attrs.width;
if (typeof widthAttr === "string" && widthAttr.endsWith("%")) {
requestAnimationFrame(() => {
const parentEl = dom.parentElement;
if (parentEl) {
const containerWidth = parentEl.clientWidth;
const pctValue = parseInt(widthAttr, 10);
if (!isNaN(pctValue) && containerWidth > 0) {
const pxWidth = Math.round(
containerWidth * (pctValue / 100),
);
el.style.width = `${pxWidth}px`;
if (node.attrs.aspectRatio) {
el.style.height = `${Math.round(pxWidth / node.attrs.aspectRatio)}px`;
}
}
}
dom.style.visibility = "";
dom.style.pointerEvents = "";
});
}
// Hide until image loads
dom.style.visibility = "hidden";
dom.style.pointerEvents = "none";
el.onload = () => {
dom.style.visibility = "";
dom.style.pointerEvents = "";
};
return nodeView;
};
return ReactNodeViewRenderer(this.options.view);
},
});
function applyAlignment(container: HTMLElement, align: string) {
if (align === "left") {
container.style.justifyContent = "flex-start";
} else if (align === "right") {
container.style.justifyContent = "flex-end";
} else {
container.style.justifyContent = "center";
}
}
+8 -216
View File
@@ -1,35 +1,15 @@
import { Node, mergeAttributes, ResizableNodeView } from "@tiptap/core";
import type { ResizableNodeViewDirection } from "@tiptap/core";
import { Node, mergeAttributes } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
export type ExcalidrawResizeOptions = {
enabled: boolean;
directions?: ResizableNodeViewDirection[];
minWidth?: number;
minHeight?: number;
alwaysPreserveAspectRatio?: boolean;
createCustomHandle?: (direction: ResizableNodeViewDirection) => HTMLElement;
className?: {
container?: string;
wrapper?: string;
handle?: string;
resizing?: string;
};
};
export interface ExcalidrawOptions {
HTMLAttributes: Record<string, any>;
view: any;
resize: ExcalidrawResizeOptions | false;
}
export interface ExcalidrawAttributes {
src?: string;
title?: string;
size?: number;
width?: number | string;
height?: number;
aspectRatio?: number;
width?: string;
align?: string;
attachmentId?: string;
}
@@ -38,8 +18,6 @@ declare module "@tiptap/core" {
interface Commands<ReturnType> {
excalidraw: {
setExcalidraw: (attributes?: ExcalidrawAttributes) => ReturnType;
setExcalidrawAlign: (align: "left" | "center" | "right") => ReturnType;
setExcalidrawSize: (width: number, height: number) => ReturnType;
};
}
}
@@ -57,10 +35,8 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
return {
HTMLAttributes: {},
view: null,
resize: false,
};
},
addAttributes() {
return {
src: {
@@ -78,30 +54,12 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
}),
},
width: {
default: null,
parseHTML: (element) => {
const raw = element.getAttribute("data-width");
if (!raw) return null;
if (raw.endsWith("%")) return raw;
const num = parseFloat(raw);
return isNaN(num) ? null : num;
},
default: "100%",
parseHTML: (element) => element.getAttribute("data-width"),
renderHTML: (attributes: ExcalidrawAttributes) => ({
"data-width": attributes.width,
}),
},
height: {
default: null,
parseHTML: (element) => {
const raw = element.getAttribute("data-height");
if (!raw) return null;
const num = parseFloat(raw);
return isNaN(num) ? null : num;
},
renderHTML: (attributes: ExcalidrawAttributes) => ({
"data-height": attributes.height,
}),
},
size: {
default: null,
parseHTML: (element) => element.getAttribute("data-size"),
@@ -109,13 +67,6 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
"data-size": attributes.size,
}),
},
aspectRatio: {
default: null,
parseHTML: (element) => element.getAttribute("data-aspect-ratio"),
renderHTML: (attributes: ExcalidrawAttributes) => ({
"data-aspect-ratio": attributes.aspectRatio,
}),
},
align: {
default: "center",
parseHTML: (element) => element.getAttribute("data-align"),
@@ -147,7 +98,7 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
mergeAttributes(
{ "data-type": this.name },
this.options.HTMLAttributes,
HTMLAttributes,
HTMLAttributes
),
[
"img",
@@ -170,172 +121,13 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
attrs: attrs,
});
},
setExcalidrawAlign:
(align) =>
({ commands }) =>
commands.updateAttributes("excalidraw", { align }),
setExcalidrawSize:
(width, height) =>
({ commands }) =>
commands.updateAttributes("excalidraw", { width, height }),
};
},
addNodeView() {
const resize = this.options.resize;
// 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;
if (!resize || !resize.enabled) {
this.editor.isInitialized = true;
return ReactNodeViewRenderer(this.options.view);
}
const {
directions,
minWidth,
minHeight,
alwaysPreserveAspectRatio,
createCustomHandle,
className,
} = resize;
return (props) => {
const { node, getPos, HTMLAttributes, editor } = props;
if (!node.attrs.src) {
editor.isInitialized = true;
const reactView = ReactNodeViewRenderer(this.options.view);
const view = reactView(props);
const originalUpdate = view.update?.bind(view);
view.update = (updatedNode, decorations, innerDecorations) => {
if (updatedNode.attrs.src && !node.attrs.src) {
return false;
}
if (originalUpdate) {
return originalUpdate(updatedNode, decorations, innerDecorations);
}
return true;
};
return view;
}
const el = document.createElement("img");
el.src = node.attrs.src;
el.alt = node.attrs.title || "";
el.style.display = "block";
el.style.maxWidth = "100%";
el.style.borderRadius = "8px";
let currentNode = node;
const nodeView = new ResizableNodeView({
element: el,
editor,
node,
getPos,
onResize: (w, h) => {
el.style.width = `${w}px`;
el.style.height = `${h}px`;
},
onCommit: () => {
const pos = getPos();
if (pos === undefined) return;
this.editor
.chain()
.setNodeSelection(pos)
.updateAttributes(this.name, {
width: Math.round(el.offsetWidth),
height: Math.round(el.offsetHeight),
})
.run();
},
onUpdate: (updatedNode, _decorations, _innerDecorations) => {
if (updatedNode.type !== currentNode.type) {
return false;
}
if (updatedNode.attrs.src !== currentNode.attrs.src) {
el.src = updatedNode.attrs.src || "";
}
const w = updatedNode.attrs.width;
const h = updatedNode.attrs.height;
if (w != null) {
el.style.width = `${w}px`;
}
if (h != null) {
el.style.height = `${h}px`;
}
const align = updatedNode.attrs.align || "center";
const container = nodeView.dom as HTMLElement;
applyAlignment(container, align);
currentNode = updatedNode;
return true;
},
options: {
directions,
min: {
width: minWidth,
height: minHeight,
},
preserveAspectRatio: alwaysPreserveAspectRatio === true,
createCustomHandle,
className,
},
});
const dom = nodeView.dom as HTMLElement;
applyAlignment(dom, node.attrs.align || "center");
// Handle percentage width backward compat
const widthAttr = node.attrs.width;
if (typeof widthAttr === "string" && widthAttr.endsWith("%")) {
requestAnimationFrame(() => {
const parentEl = dom.parentElement;
if (parentEl) {
const containerWidth = parentEl.clientWidth;
const pctValue = parseInt(widthAttr, 10);
if (!isNaN(pctValue) && containerWidth > 0) {
const pxWidth = Math.round(
containerWidth * (pctValue / 100),
);
el.style.width = `${pxWidth}px`;
if (node.attrs.aspectRatio) {
el.style.height = `${Math.round(pxWidth / node.attrs.aspectRatio)}px`;
}
}
}
dom.style.visibility = "";
dom.style.pointerEvents = "";
});
}
// Hide until image loads
dom.style.visibility = "hidden";
dom.style.pointerEvents = "none";
el.onload = () => {
dom.style.visibility = "";
dom.style.pointerEvents = "";
};
return nodeView;
};
return ReactNodeViewRenderer(this.options.view);
},
});
function applyAlignment(container: HTMLElement, align: string) {
if (align === "left") {
container.style.justifyContent = "flex-start";
} else if (align === "right") {
container.style.justifyContent = "flex-end";
} else {
container.style.justifyContent = "center";
}
}
@@ -20,7 +20,7 @@ export const Heading = TiptapHeading.extend<TiptapHeadingOptions>({
const { doc } = state;
doc.descendants((node, pos) => {
if (node.type.name === "heading" && node.content.size > 1) {
if (node.type.name === "heading" && node.content.size > 0) {
const deco = Decoration.widget(
pos + node.nodeSize - 1,
() => {
+11 -230
View File
@@ -1,41 +1,18 @@
import Image from "@tiptap/extension-image";
import { ImageOptions as DefaultImageOptions } from "@tiptap/extension-image";
import { ReactNodeViewRenderer } from "@tiptap/react";
import {
mergeAttributes,
Range,
ResizableNodeView,
} from "@tiptap/core";
import type { ResizableNodeViewDirection } from "@tiptap/core";
export type ImageResizeOptions = {
enabled: boolean;
directions?: ResizableNodeViewDirection[];
minWidth?: number;
minHeight?: number;
alwaysPreserveAspectRatio?: boolean;
createCustomHandle?: (direction: ResizableNodeViewDirection) => HTMLElement;
className?: {
container?: string;
wrapper?: string;
handle?: string;
resizing?: string;
};
};
import { mergeAttributes, Range } from "@tiptap/core";
export interface ImageOptions extends DefaultImageOptions {
view: any;
resize: ImageResizeOptions | false;
}
export interface ImageAttributes {
src?: string;
alt?: string;
align?: string;
attachmentId?: string;
size?: number;
width?: number | string;
height?: number;
width?: number;
aspectRatio?: number;
placeholder?: {
id: string;
@@ -48,11 +25,10 @@ declare module "@tiptap/core" {
imageBlock: {
setImage: (attributes: ImageAttributes) => ReturnType;
setImageAt: (
attributes: ImageAttributes & { pos: number | Range },
attributes: ImageAttributes & { pos: number | Range }
) => ReturnType;
setImageAlign: (align: "left" | "center" | "right") => ReturnType;
setImageWidth: (width: number) => ReturnType;
setImageSize: (width: number, height: number) => ReturnType;
};
}
}
@@ -70,7 +46,6 @@ export const TiptapImage = Image.extend<ImageOptions>({
return {
...this.parent?.(),
view: null,
resize: false,
};
},
@@ -84,30 +59,12 @@ export const TiptapImage = Image.extend<ImageOptions>({
}),
},
width: {
default: null,
parseHTML: (element) => {
const raw = element.getAttribute("width");
if (!raw) return null;
if (raw.endsWith("%")) return raw;
const num = parseFloat(raw);
return isNaN(num) ? null : num;
},
default: "100%",
parseHTML: (element) => element.getAttribute("width"),
renderHTML: (attributes: ImageAttributes) => ({
width: attributes.width,
}),
},
height: {
default: null,
parseHTML: (element) => {
const raw = element.getAttribute("height");
if (!raw) return null;
const num = parseFloat(raw);
return isNaN(num) ? null : num;
},
renderHTML: (attributes: ImageAttributes) => ({
height: attributes.height,
}),
},
align: {
default: "center",
parseHTML: (element) => element.getAttribute("data-align"),
@@ -185,192 +142,16 @@ export const TiptapImage = Image.extend<ImageOptions>({
setImageWidth:
(width) =>
({ commands }) =>
commands.updateAttributes("image", { width }),
setImageSize:
(width, height) =>
({ commands }) =>
commands.updateAttributes("image", { width, height }),
commands.updateAttributes("image", {
width: `${Math.max(0, Math.min(100, width))}%`,
}),
};
},
addNodeView() {
const resize = this.options.resize;
// 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;
if (!resize || !resize.enabled) {
// Fallback to React node view (existing behavior)
this.editor.isInitialized = true;
return ReactNodeViewRenderer(this.options.view);
}
const {
directions,
minWidth,
minHeight,
alwaysPreserveAspectRatio,
createCustomHandle,
className,
} = resize;
return (props) => {
const { node, getPos, HTMLAttributes, editor } = props;
// If no src yet (placeholder/uploading), use React view for loading UI
if (!HTMLAttributes.src) {
editor.isInitialized = true;
const reactView = ReactNodeViewRenderer(this.options.view);
const view = reactView(props);
// When the node gets a src, return false from update to force rebuild
const originalUpdate = view.update?.bind(view);
view.update = (updatedNode, decorations, innerDecorations) => {
if (updatedNode.attrs.src && !node.attrs.src) {
return false;
}
if (originalUpdate) {
return originalUpdate(updatedNode, decorations, innerDecorations);
}
return true;
};
return view;
}
// Has src — use ResizableNodeView
const el = document.createElement("img");
Object.entries(HTMLAttributes).forEach(([key, value]) => {
if (value != null) {
switch (key) {
case "width":
case "height":
break;
default:
el.setAttribute(key, String(value));
break;
}
}
});
el.src = HTMLAttributes.src;
el.style.display = "block";
el.style.maxWidth = "100%";
el.style.borderRadius = "8px";
let currentNode = node;
const nodeView = new ResizableNodeView({
element: el,
editor,
node,
getPos,
onResize: (w, h) => {
el.style.width = `${w}px`;
el.style.height = `${h}px`;
},
onCommit: () => {
const pos = getPos();
if (pos === undefined) return;
this.editor
.chain()
.setNodeSelection(pos)
.updateAttributes(this.name, {
width: Math.round(el.offsetWidth),
height: Math.round(el.offsetHeight),
})
.run();
},
onUpdate: (updatedNode, _decorations, _innerDecorations) => {
if (updatedNode.type !== currentNode.type) {
return false;
}
if (updatedNode.attrs.src !== currentNode.attrs.src) {
el.src = updatedNode.attrs.src || "";
}
if (updatedNode.attrs.alt !== currentNode.attrs.alt) {
el.alt = updatedNode.attrs.alt || "";
}
const w = updatedNode.attrs.width;
const h = updatedNode.attrs.height;
if (w != null) {
el.style.width = `${w}px`;
}
if (h != null) {
el.style.height = `${h}px`;
}
// Update alignment on container
const align = updatedNode.attrs.align || "center";
const container = nodeView.dom as HTMLElement;
applyAlignment(container, align);
currentNode = updatedNode;
return true;
},
options: {
directions,
min: {
width: minWidth,
height: minHeight,
},
preserveAspectRatio: alwaysPreserveAspectRatio === true,
createCustomHandle,
className,
},
});
const dom = nodeView.dom as HTMLElement;
// Apply initial alignment
applyAlignment(dom, node.attrs.align || "center");
// Handle percentage width backward compat
const widthAttr = node.attrs.width;
if (typeof widthAttr === "string" && widthAttr.endsWith("%")) {
// Defer conversion until we can measure the container
requestAnimationFrame(() => {
const parentEl = dom.parentElement;
if (parentEl) {
const containerWidth = parentEl.clientWidth;
const pctValue = parseInt(widthAttr, 10);
if (!isNaN(pctValue) && containerWidth > 0) {
const pxWidth = Math.round(
containerWidth * (pctValue / 100),
);
el.style.width = `${pxWidth}px`;
if (node.attrs.aspectRatio) {
el.style.height = `${Math.round(pxWidth / node.attrs.aspectRatio)}px`;
}
}
}
dom.style.visibility = "";
dom.style.pointerEvents = "";
});
}
// Hide until image loads (official TipTap pattern)
dom.style.visibility = "hidden";
dom.style.pointerEvents = "none";
el.onload = () => {
dom.style.visibility = "";
dom.style.pointerEvents = "";
};
return nodeView;
};
return ReactNodeViewRenderer(this.options.view);
},
});
function applyAlignment(container: HTMLElement, align: string) {
if (align === "left") {
container.style.justifyContent = "flex-start";
} else if (align === "right") {
container.style.justifyContent = "flex-end";
} else {
container.style.justifyContent = "center";
}
}
+7 -201
View File
@@ -1,35 +1,16 @@
import { ReactNodeViewRenderer } from "@tiptap/react";
import { Range, Node, mergeAttributes, ResizableNodeView } from "@tiptap/core";
import type { ResizableNodeViewDirection } from "@tiptap/core";
export type VideoResizeOptions = {
enabled: boolean;
directions?: ResizableNodeViewDirection[];
minWidth?: number;
minHeight?: number;
alwaysPreserveAspectRatio?: boolean;
createCustomHandle?: (direction: ResizableNodeViewDirection) => HTMLElement;
className?: {
container?: string;
wrapper?: string;
handle?: string;
resizing?: string;
};
};
import { Range, Node } from "@tiptap/core";
export interface VideoOptions {
view: any;
HTMLAttributes: Record<string, any>;
resize: VideoResizeOptions | false;
}
export interface VideoAttributes {
src?: string;
align?: string;
attachmentId?: string;
size?: number;
width?: number | string;
height?: number;
width?: number;
aspectRatio?: number;
placeholder?: {
id: string;
@@ -46,7 +27,6 @@ declare module "@tiptap/core" {
) => ReturnType;
setVideoAlign: (align: "left" | "center" | "right") => ReturnType;
setVideoWidth: (width: number) => ReturnType;
setVideoSize: (width: number, height: number) => ReturnType;
};
}
}
@@ -64,7 +44,6 @@ export const TiptapVideo = Node.create<VideoOptions>({
return {
view: null,
HTMLAttributes: {},
resize: false,
};
},
@@ -85,30 +64,12 @@ export const TiptapVideo = Node.create<VideoOptions>({
}),
},
width: {
default: null,
parseHTML: (element) => {
const raw = element.getAttribute("width");
if (!raw) return null;
if (raw.endsWith("%")) return raw;
const num = parseFloat(raw);
return isNaN(num) ? null : num;
},
default: "100%",
parseHTML: (element) => element.getAttribute("width"),
renderHTML: (attributes: VideoAttributes) => ({
width: attributes.width,
}),
},
height: {
default: null,
parseHTML: (element) => {
const raw = element.getAttribute("height");
if (!raw) return null;
const num = parseFloat(raw);
return isNaN(num) ? null : num;
},
renderHTML: (attributes: VideoAttributes) => ({
height: attributes.height,
}),
},
size: {
default: null,
parseHTML: (element) => element.getAttribute("data-size"),
@@ -175,168 +136,13 @@ export const TiptapVideo = Node.create<VideoOptions>({
commands.updateAttributes("video", {
width: `${Math.max(0, Math.min(100, width))}%`,
}),
setVideoSize:
(width, height) =>
({ commands }) =>
commands.updateAttributes("video", { width, height }),
};
},
addNodeView() {
const resize = this.options.resize;
// 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;
if (!resize || !resize.enabled) {
this.editor.isInitialized = true;
return ReactNodeViewRenderer(this.options.view);
}
const {
directions,
minWidth,
minHeight,
alwaysPreserveAspectRatio,
createCustomHandle,
className,
} = resize;
return (props) => {
const { node, getPos, HTMLAttributes, editor } = props;
if (!node.attrs.src) {
editor.isInitialized = true;
const reactView = ReactNodeViewRenderer(this.options.view);
const view = reactView(props);
const originalUpdate = view.update?.bind(view);
view.update = (updatedNode, decorations, innerDecorations) => {
if (updatedNode.attrs.src && !node.attrs.src) {
return false;
}
if (originalUpdate) {
return originalUpdate(updatedNode, decorations, innerDecorations);
}
return true;
};
return view;
}
const el = document.createElement("video");
el.src = node.attrs.src;
el.controls = true;
el.preload = "metadata";
el.style.display = "block";
el.style.maxWidth = "100%";
el.style.borderRadius = "8px";
let currentNode = node;
const nodeView = new ResizableNodeView({
element: el,
editor,
node,
getPos,
onResize: (w, h) => {
el.style.width = `${w}px`;
el.style.height = `${h}px`;
},
onCommit: () => {
const pos = getPos();
if (pos === undefined) return;
this.editor
.chain()
.setNodeSelection(pos)
.updateAttributes(this.name, {
width: Math.round(el.offsetWidth),
height: Math.round(el.offsetHeight),
})
.run();
},
onUpdate: (updatedNode, _decorations, _innerDecorations) => {
if (updatedNode.type !== currentNode.type) {
return false;
}
if (updatedNode.attrs.src !== currentNode.attrs.src) {
el.src = updatedNode.attrs.src || "";
}
const w = updatedNode.attrs.width;
const h = updatedNode.attrs.height;
if (w != null) {
el.style.width = `${w}px`;
}
if (h != null) {
el.style.height = `${h}px`;
}
const align = updatedNode.attrs.align || "center";
const container = nodeView.dom as HTMLElement;
applyAlignment(container, align);
currentNode = updatedNode;
return true;
},
options: {
directions,
min: {
width: minWidth,
height: minHeight,
},
preserveAspectRatio: alwaysPreserveAspectRatio === true,
createCustomHandle,
className,
},
});
const dom = nodeView.dom as HTMLElement;
applyAlignment(dom, node.attrs.align || "center");
// Handle percentage width backward compat
const widthAttr = node.attrs.width;
if (typeof widthAttr === "string" && widthAttr.endsWith("%")) {
requestAnimationFrame(() => {
const parentEl = dom.parentElement;
if (parentEl) {
const containerWidth = parentEl.clientWidth;
const pctValue = parseInt(widthAttr, 10);
if (!isNaN(pctValue) && containerWidth > 0) {
const pxWidth = Math.round(
containerWidth * (pctValue / 100),
);
el.style.width = `${pxWidth}px`;
if (node.attrs.aspectRatio) {
el.style.height = `${Math.round(pxWidth / node.attrs.aspectRatio)}px`;
}
}
}
dom.style.visibility = "";
dom.style.pointerEvents = "";
});
}
// Hide until video metadata loads
dom.style.visibility = "hidden";
dom.style.pointerEvents = "none";
el.onloadedmetadata = () => {
dom.style.visibility = "";
dom.style.pointerEvents = "";
};
return nodeView;
};
return ReactNodeViewRenderer(this.options.view);
},
});
function applyAlignment(container: HTMLElement, align: string) {
if (align === "left") {
container.style.justifyContent = "flex-start";
} else if (align === "right") {
container.style.justifyContent = "flex-end";
} else {
container.style.justifyContent = "center";
}
}
-105
View File
@@ -1,105 +0,0 @@
diff --git a/dist/index.cjs b/dist/index.cjs
index 01d6999642c5ae990083798a1bf0ef87068e4192..891b13c6901f28a6ab413c6dbae0ea726a76a196 100644
--- a/dist/index.cjs
+++ b/dist/index.cjs
@@ -5463,7 +5463,10 @@ var ResizableNodeView = class {
this.container.classList.remove(this.classNames.resizing);
}
document.removeEventListener("mousemove", this.handleMouseMove);
+ document.removeEventListener("touchmove", this.handleTouchMove);
document.removeEventListener("mouseup", this.handleMouseUp);
+ document.removeEventListener("touchend", this.handleMouseUp);
+ window.removeEventListener("blur", this.handleMouseUp);
document.removeEventListener("keydown", this.handleKeyDown);
document.removeEventListener("keyup", this.handleKeyUp);
};
@@ -5593,7 +5596,10 @@ var ResizableNodeView = class {
this.container.classList.remove(this.classNames.resizing);
}
document.removeEventListener("mousemove", this.handleMouseMove);
+ document.removeEventListener("touchmove", this.handleTouchMove);
document.removeEventListener("mouseup", this.handleMouseUp);
+ document.removeEventListener("touchend", this.handleMouseUp);
+ window.removeEventListener("blur", this.handleMouseUp);
document.removeEventListener("keydown", this.handleKeyDown);
document.removeEventListener("keyup", this.handleKeyUp);
this.isResizing = false;
@@ -5796,6 +5802,8 @@ var ResizableNodeView = class {
document.addEventListener("mousemove", this.handleMouseMove);
document.addEventListener("touchmove", this.handleTouchMove);
document.addEventListener("mouseup", this.handleMouseUp);
+ document.addEventListener("touchend", this.handleMouseUp);
+ window.addEventListener("blur", this.handleMouseUp);
document.addEventListener("keydown", this.handleKeyDown);
document.addEventListener("keyup", this.handleKeyUp);
}
diff --git a/dist/index.js b/dist/index.js
index 6f357a03b038abeb5ed86967b7fc7c3e5eb1d2d6..2d2742532860821984e1ba82625821504538ebbe 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -5330,7 +5330,10 @@ var ResizableNodeView = class {
this.container.classList.remove(this.classNames.resizing);
}
document.removeEventListener("mousemove", this.handleMouseMove);
+ document.removeEventListener("touchmove", this.handleTouchMove);
document.removeEventListener("mouseup", this.handleMouseUp);
+ document.removeEventListener("touchend", this.handleMouseUp);
+ window.removeEventListener("blur", this.handleMouseUp);
document.removeEventListener("keydown", this.handleKeyDown);
document.removeEventListener("keyup", this.handleKeyUp);
};
@@ -5460,7 +5463,10 @@ var ResizableNodeView = class {
this.container.classList.remove(this.classNames.resizing);
}
document.removeEventListener("mousemove", this.handleMouseMove);
+ document.removeEventListener("touchmove", this.handleTouchMove);
document.removeEventListener("mouseup", this.handleMouseUp);
+ document.removeEventListener("touchend", this.handleMouseUp);
+ window.removeEventListener("blur", this.handleMouseUp);
document.removeEventListener("keydown", this.handleKeyDown);
document.removeEventListener("keyup", this.handleKeyUp);
this.isResizing = false;
@@ -5663,6 +5669,8 @@ var ResizableNodeView = class {
document.addEventListener("mousemove", this.handleMouseMove);
document.addEventListener("touchmove", this.handleTouchMove);
document.addEventListener("mouseup", this.handleMouseUp);
+ document.addEventListener("touchend", this.handleMouseUp);
+ window.addEventListener("blur", this.handleMouseUp);
document.addEventListener("keydown", this.handleKeyDown);
document.addEventListener("keyup", this.handleKeyUp);
}
diff --git a/src/lib/ResizableNodeView.ts b/src/lib/ResizableNodeView.ts
index f13e210b0aa46aefe7c31105deee3d2aa8a26cd5..9bac138dbf17c6ae6c3c129cbedb3a81bd39b60c 100644
--- a/src/lib/ResizableNodeView.ts
+++ b/src/lib/ResizableNodeView.ts
@@ -523,7 +523,10 @@ export class ResizableNodeView {
}
document.removeEventListener('mousemove', this.handleMouseMove)
+ document.removeEventListener('touchmove', this.handleTouchMove)
document.removeEventListener('mouseup', this.handleMouseUp)
+ document.removeEventListener('touchend', this.handleMouseUp)
+ window.removeEventListener('blur', this.handleMouseUp)
document.removeEventListener('keydown', this.handleKeyDown)
document.removeEventListener('keyup', this.handleKeyUp)
this.isResizing = false
@@ -774,6 +777,8 @@ export class ResizableNodeView {
document.addEventListener('mousemove', this.handleMouseMove)
document.addEventListener('touchmove', this.handleTouchMove)
document.addEventListener('mouseup', this.handleMouseUp)
+ document.addEventListener('touchend', this.handleMouseUp)
+ window.addEventListener('blur', this.handleMouseUp)
document.addEventListener('keydown', this.handleKeyDown)
document.addEventListener('keyup', this.handleKeyUp)
}
@@ -859,7 +864,10 @@ export class ResizableNodeView {
// Clean up document-level listeners
document.removeEventListener('mousemove', this.handleMouseMove)
+ document.removeEventListener('touchmove', this.handleTouchMove)
document.removeEventListener('mouseup', this.handleMouseUp)
+ document.removeEventListener('touchend', this.handleMouseUp)
+ window.removeEventListener('blur', this.handleMouseUp)
document.removeEventListener('keydown', this.handleKeyDown)
document.removeEventListener('keyup', this.handleKeyUp)
}
+138 -141
View File
@@ -29,9 +29,6 @@ overrides:
'@tiptap/extension-code': 3.17.1
patchedDependencies:
'@tiptap/core':
hash: efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00
path: patches/@tiptap__core.patch
react-arborist@3.4.0:
hash: 419b3b02e24afe928cc006a006f6e906666aff19aa6fd7daaa788ccc2202678a
path: patches/react-arborist@3.4.0.patch
@@ -60,7 +57,7 @@ importers:
version: 3.4.4(y-protocols@1.0.6(yjs@13.6.29))(yjs@13.6.29)
'@hocuspocus/transformer':
specifier: 3.4.4
version: 3.4.4(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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)
version: 3.4.4(@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
@@ -72,85 +69,85 @@ importers:
version: 1.1.0
'@tiptap/core':
specifier: 3.17.1
version: 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
version: 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/extension-code-block':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@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: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@tiptap/y-tiptap@3.0.2(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)
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@tiptap/y-tiptap@3.0.2(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(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@tiptap/y-tiptap@3.0.2(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))
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@tiptap/y-tiptap@3.0.2(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: 3.17.1
version: 3.17.1(@tiptap/extension-text-style@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@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: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))
'@tiptap/extension-heading':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))
'@tiptap/extension-highlight':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))
'@tiptap/extension-history':
specifier: 3.17.1
version: 3.17.1(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))
version: 3.17.1(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))
'@tiptap/extension-image':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))
'@tiptap/extension-link':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@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(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@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: 3.17.1
version: 3.17.1(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))
version: 3.17.1(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))
'@tiptap/extension-subscript':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@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: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@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: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@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: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))
'@tiptap/extension-text-align':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))
'@tiptap/extension-text-style':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))
'@tiptap/extension-typography':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@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.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)
version: 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)
'@tiptap/extension-youtube':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))
'@tiptap/html':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(happy-dom@20.1.0)
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: 3.17.1
version: 3.17.1
'@tiptap/react':
specifier: 3.17.1
version: 3.17.1(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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)
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: 3.17.1
version: 3.17.1
'@tiptap/suggestion':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@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':
specifier: ^3.0.2
version: 3.0.2(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)
@@ -12335,9 +12332,9 @@ snapshots:
- bufferutil
- utf-8-validate
'@hocuspocus/transformer@3.4.4(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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)':
'@hocuspocus/transformer@3.4.4(@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': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@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)
@@ -14898,191 +14895,191 @@ snapshots:
'@tanstack/query-core': 5.90.17
react: 18.3.1
'@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)':
'@tiptap/core@3.17.1(@tiptap/pm@3.17.1)':
dependencies:
'@tiptap/pm': 3.17.1
'@tiptap/extension-blockquote@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
'@tiptap/extension-blockquote@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/extension-bold@3.17.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
'@tiptap/extension-bold@3.17.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/extension-bubble-menu@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)':
'@tiptap/extension-bubble-menu@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)':
dependencies:
'@floating-ui/dom': 1.7.4
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/pm': 3.17.1
optional: true
'@tiptap/extension-bullet-list@3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@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))':
dependencies:
'@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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-code-block@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@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)':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@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(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
'@tiptap/extension-code@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/extension-collaboration-caret@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@tiptap/y-tiptap@3.0.2(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-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.2(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': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/pm': 3.17.1
'@tiptap/y-tiptap': 3.0.2(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-collaboration@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@tiptap/y-tiptap@3.0.2(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@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@tiptap/y-tiptap@3.0.2(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': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/pm': 3.17.1
'@tiptap/y-tiptap': 3.0.2(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-color@3.17.1(@tiptap/extension-text-style@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)))':
'@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/extension-text-style': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
'@tiptap/extension-text-style': 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))
'@tiptap/extension-document@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
'@tiptap/extension-document@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/extension-dropcursor@3.19.0(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))':
'@tiptap/extension-dropcursor@3.19.0(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)
'@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)
'@tiptap/extension-floating-menu@3.19.0(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)':
'@tiptap/extension-floating-menu@3.19.0(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)':
dependencies:
'@floating-ui/dom': 1.7.3
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/pm': 3.17.1
optional: true
'@tiptap/extension-gapcursor@3.19.0(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))':
'@tiptap/extension-gapcursor@3.19.0(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)
'@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)
'@tiptap/extension-hard-break@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
'@tiptap/extension-hard-break@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/extension-heading@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
'@tiptap/extension-heading@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/extension-highlight@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
'@tiptap/extension-highlight@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/extension-history@3.17.1(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))':
'@tiptap/extension-history@3.17.1(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)
'@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)
'@tiptap/extension-horizontal-rule@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)':
'@tiptap/extension-horizontal-rule@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/pm': 3.17.1
'@tiptap/extension-image@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
'@tiptap/extension-image@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/extension-italic@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
'@tiptap/extension-italic@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/extension-link@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@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)':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/pm': 3.17.1
linkifyjs: 4.3.2
'@tiptap/extension-list-item@3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))':
'@tiptap/extension-list-item@3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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-keymap@3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))':
'@tiptap/extension-list-keymap@3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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)':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/pm': 3.17.1
'@tiptap/extension-ordered-list@3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))':
'@tiptap/extension-ordered-list@3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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-paragraph@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
'@tiptap/extension-paragraph@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/extension-placeholder@3.17.1(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))':
'@tiptap/extension-placeholder@3.17.1(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)
'@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)
'@tiptap/extension-strike@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
'@tiptap/extension-strike@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/extension-subscript@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.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': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/pm': 3.17.1
'@tiptap/extension-superscript@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.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': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/pm': 3.17.1
'@tiptap/extension-table@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.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': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/pm': 3.17.1
'@tiptap/extension-text-align@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
'@tiptap/extension-text-align@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/extension-text-style@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
'@tiptap/extension-text-style@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/extension-text@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
'@tiptap/extension-text@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/extension-typography@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
'@tiptap/extension-typography@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/extension-underline@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
'@tiptap/extension-underline@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/extension-unique-id@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)':
'@tiptap/extension-unique-id@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/pm': 3.17.1
uuid: 10.0.0
'@tiptap/extension-youtube@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
'@tiptap/extension-youtube@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)':
'@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/pm': 3.17.1
'@tiptap/html@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(happy-dom@20.1.0)':
'@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': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/pm': 3.17.1
happy-dom: 20.1.0
@@ -15107,9 +15104,9 @@ snapshots:
prosemirror-transform: 1.10.4
prosemirror-view: 1.40.0
'@tiptap/react@3.17.1(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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/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': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.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
@@ -15119,41 +15116,41 @@ snapshots:
react-dom: 18.3.1(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(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)
'@tiptap/extension-floating-menu': 3.19.0(@floating-ui/dom@1.7.3)(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)
'@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.19.0(@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@3.17.1':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/extension-blockquote': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
'@tiptap/extension-bold': 3.17.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
'@tiptap/extension-bullet-list': 3.17.1(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))
'@tiptap/extension-code': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
'@tiptap/extension-code-block': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)
'@tiptap/extension-document': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
'@tiptap/extension-dropcursor': 3.19.0(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))
'@tiptap/extension-gapcursor': 3.19.0(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))
'@tiptap/extension-hard-break': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
'@tiptap/extension-heading': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
'@tiptap/extension-horizontal-rule': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)
'@tiptap/extension-italic': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
'@tiptap/extension-link': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)
'@tiptap/extension-list': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)
'@tiptap/extension-list-item': 3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))
'@tiptap/extension-list-keymap': 3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))
'@tiptap/extension-ordered-list': 3.19.0(@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))
'@tiptap/extension-paragraph': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
'@tiptap/extension-strike': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
'@tiptap/extension-text': 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
'@tiptap/extension-underline': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
'@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.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.0(@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.19.0(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))
'@tiptap/extension-gapcursor': 3.19.0(@tiptap/extensions@3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))
'@tiptap/extension-hard-break': 3.19.0(@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.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)
'@tiptap/extension-italic': 3.19.0(@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.19.0(@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.19.0(@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.19.0(@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.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))
'@tiptap/extension-strike': 3.19.0(@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.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))
'@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)
'@tiptap/pm': 3.17.1
'@tiptap/suggestion@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)':
'@tiptap/suggestion@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)':
dependencies:
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/pm': 3.17.1
'@tiptap/y-tiptap@3.0.2(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)':