Compare commits

...

25 Commits

Author SHA1 Message Date
Philipinho 6433cddb98 fix callout in columns 2026-02-24 15:22:11 +00:00
Philipinho 038d87c08a quote 2026-02-24 14:33:20 +00:00
Philipinho 22ade69f97 fix blockquote 2026-02-24 14:30:14 +00:00
Philipinho aef806a262 selective placeholder 2026-02-24 14:25:51 +00:00
Philipinho 6205741d26 fix columns 2026-02-24 14:14:38 +00:00
Philipinho 6f66577d61 fix print 2026-02-24 13:50:34 +00:00
Philipinho 6f0fb9beff hide columns menu when some nodes are focused 2026-02-24 13:49:22 +00:00
Philipinho c5639feff5 fix print 2026-02-24 13:45:32 +00:00
Philipinho b164ff2e2f capture tab key in column 2026-02-24 13:36:31 +00:00
Philipinho 59f111e730 focus on first column 2026-02-24 13:34:45 +00:00
Philipinho 9f2be72173 notes callout 2026-02-24 13:21:49 +00:00
Philipinho 993d771282 feat: columns 2026-02-24 13:01:40 +00:00
Philipinho a925dc0782 fix: patch @tiptap/core ResizableNodeView to prevent resize sticking after mouseup 2026-02-24 10:35:09 +00:00
Philipinho 0cc3c6c68a fix color scheme 2026-02-24 10:14:19 +00:00
Philipinho 052b2042ff refresh table menus 2026-02-24 09:58:32 +00:00
Philipinho 22182418ed callout menu refresh 2026-02-24 09:52:54 +00:00
Philipinho e71584dfd4 video resize 2026-02-24 09:46:00 +00:00
Philipinho a1b6e7dbbd support image resize undo 2026-02-24 09:38:05 +00:00
Philipinho 8c380db8c3 refactor excalidraw and drawio menu 2026-02-23 23:48:14 +00:00
Philipinho 4c5b684ed4 feat: new image menu
* switch to resizable side handles
* use pixels
2026-02-23 22:46:58 +00:00
Philipinho c172d3bd5e fix 2026-02-21 00:43:49 +00:00
Philip Okugbe 53132acb0a fix: redirect to original page after re-authentication (#1959)
* fix: redirect to original page after re-authentication

When a session expires, the current URL is now preserved as a query
parameter on the login page. After successful login (including MFA
flows), the user is redirected back to their original page instead of
always landing on /home.

* secure

---------

Co-authored-by: Julien Fontanet <julien.fontanet@isonoe.net>
2026-02-21 00:02:23 +00:00
b4sh2 d6472f0876 Merge commit from fork
Co-authored-by: b4sh2 <b4sh2@users.noreply.github.com>
2026-02-20 16:59:44 +00:00
Philipinho 873c963043 fix db types duplication 2026-02-19 22:34:07 +00:00
Julien Fontanet 03a70d768a fix: allow deleting last character in headings (#1954)
The copy-link decoration widget (contentEditable="false") injected
inside headings prevented browsers from deleting the last remaining
character via Backspace or Delete keys. Only show the widget when the
heading has more than one character of content.
2026-02-18 13:48:15 +00:00
53 changed files with 3129 additions and 602 deletions
@@ -274,6 +274,7 @@
"Add row below": "Add row below",
"Delete table": "Delete table",
"Info": "Info",
"Note": "Note",
"Success": "Success",
"Warning": "Warning",
"Danger": "Danger",
@@ -363,6 +364,15 @@
"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}}",
@@ -0,0 +1,27 @@
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>
);
}
@@ -0,0 +1,28 @@
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 from "@/lib/app-route";
import APP_ROUTE, { getPostLoginRedirect } 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);
navigate(APP_ROUTE.AUTH.MFA_CHALLENGE + window.location.search);
} else if (response?.requiresMfaSetup) {
onClose();
navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED);
navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED + window.location.search);
} else {
onClose();
navigate(APP_ROUTE.HOME);
navigate(getPostLoginRedirect());
}
} 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 from "@/lib/app-route";
import APP_ROUTE, { getPostLoginRedirect } 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(APP_ROUTE.HOME);
navigate(getPostLoginRedirect());
} 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 from "@/lib/app-route.ts";
import APP_ROUTE, { getPostLoginRedirect } 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(APP_ROUTE.HOME);
navigate(getPostLoginRedirect());
};
return (
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import APP_ROUTE from "@/lib/app-route";
import APP_ROUTE, { getPostLoginRedirect } from "@/lib/app-route";
import { validateMfaAccess } from "@/ee/mfa";
export function useMfaPageProtection() {
@@ -13,8 +13,10 @@ export function useMfaPageProtection() {
const checkAccess = async () => {
const result = await validateMfaAccess();
const search = location.search;
if (!result.valid) {
navigate(APP_ROUTE.AUTH.LOGIN);
navigate(APP_ROUTE.AUTH.LOGIN + search);
return;
}
@@ -26,17 +28,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);
navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED + search);
} else if (
!result.requiresMfaSetup &&
result.userHasMfa &&
!isOnChallengePage
) {
// User has MFA and should be on challenge page
navigate(APP_ROUTE.AUTH.MFA_CHALLENGE);
navigate(APP_ROUTE.AUTH.MFA_CHALLENGE + search);
} else if (!result.isTransferToken) {
// User has a regular auth token, shouldn't be on MFA pages
navigate(APP_ROUTE.HOME);
navigate(getPostLoginRedirect());
} else {
setIsValid(true);
}
@@ -23,7 +23,7 @@ import {
acceptInvitation,
createWorkspace,
} from "@/features/workspace/services/workspace-service.ts";
import APP_ROUTE from "@/lib/app-route.ts";
import APP_ROUTE, { getPostLoginRedirect } 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);
navigate(APP_ROUTE.AUTH.MFA_CHALLENGE + window.location.search);
} else if (response?.requiresMfaSetup) {
navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED);
navigate(APP_ROUTE.AUTH.MFA_SETUP_REQUIRED + window.location.search);
} else {
navigate(APP_ROUTE.HOME);
navigate(getPostLoginRedirect());
}
} catch (err) {
setIsLoading(false);
@@ -1,6 +1,6 @@
import { useEffect } from "react";
import useCurrentUser from "@/features/user/hooks/use-current-user.ts";
import APP_ROUTE from "@/lib/app-route.ts";
import { getPostLoginRedirect } 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(APP_ROUTE.HOME);
navigate(getPostLoginRedirect());
}
}, [isLoading, data]);
}
@@ -7,16 +7,19 @@ 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 } from "@docmost/editor-ext";
import { CalloutType, isTextSelected } 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();
@@ -26,6 +29,7 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
if (!state) {
return false;
}
if (isTextSelected(editor)) return false;
return editor.isActive("callout");
},
@@ -42,6 +46,7 @@ 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" }),
@@ -126,15 +131,31 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
}}
shouldShow={shouldShow}
>
<ActionIcon.Group className="actionIconGroup">
<div className={classes.toolbar}>
<Tooltip position="top" label={t("Info")}>
<ActionIcon
onClick={() => setCalloutType("info")}
size="lg"
aria-label={t("Info")}
variant={editorState?.isInfo ? "light" : "default"}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isInfo })}
>
<IconInfoCircleFilled size={18} />
<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)" />
</ActionIcon>
</Tooltip>
@@ -143,9 +164,13 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
onClick={() => setCalloutType("success")}
size="lg"
aria-label={t("Success")}
variant={editorState?.isSuccess ? "light" : "default"}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isSuccess })}
>
<IconCircleCheckFilled size={18} />
<IconCircleCheckFilled
size={18}
color="var(--mantine-color-green-5)"
/>
</ActionIcon>
</Tooltip>
@@ -154,9 +179,13 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
onClick={() => setCalloutType("warning")}
size="lg"
aria-label={t("Warning")}
variant={editorState?.isWarning ? "light" : "default"}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isWarning })}
>
<IconAlertTriangleFilled size={18} />
<IconAlertTriangleFilled
size={18}
color="var(--mantine-color-orange-5)"
/>
</ActionIcon>
</Tooltip>
@@ -165,9 +194,10 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
onClick={() => setCalloutType("danger")}
size="lg"
aria-label={t("Danger")}
variant={editorState?.isDanger ? "light" : "default"}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isDanger })}
>
<IconCircleXFilled size={18} />
<IconCircleXFilled size={18} color="var(--mantine-color-red-5)" />
</ActionIcon>
</Tooltip>
@@ -178,11 +208,10 @@ export function CalloutMenu({ editor }: EditorMenuProps) {
icon={currentIcon || <IconMoodSmile size={18} />}
actionIconProps={{
size: "lg",
variant: "default",
c: undefined,
variant: "subtle",
}}
/>
</ActionIcon.Group>
</div>
</BaseBubbleMenu>
);
}
@@ -4,6 +4,7 @@ import {
IconCircleCheckFilled,
IconCircleXFilled,
IconInfoCircleFilled,
IconNotes,
} from "@tabler/icons-react";
import { Alert } from "@mantine/core";
import classes from "./callout.module.css";
@@ -22,6 +23,7 @@ export default function CalloutView(props: NodeViewProps) {
icon={getCalloutIcon(type, icon)}
p="xs"
classNames={{
root: classes.root,
message: classes.message,
icon: classes.icon,
}}
@@ -34,12 +36,14 @@ 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":
@@ -55,6 +59,8 @@ function getCalloutColor(type: CalloutType) {
switch (type) {
case "info":
return "blue";
case "note":
return "grape";
case "success":
return "green";
case "warning":
@@ -1,9 +1,13 @@
.root {
overflow: visible;
}
.icon {
font-size: 24px;
line-height: 1;
width: 20px;
height: 20px;
margin-inline-end: var(--mantine-spacing-md);
margin-inline-end: var(--mantine-spacing-xs);
margin-top: 4px;
cursor: pointer;
}
@@ -11,18 +15,8 @@
.message {
font-size: var(--mantine-font-size-md);
color: var(--mantine-color-default-color);
white-space: nowrap;
overflow: visible;
text-overflow: unset;
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);
}
*/
@@ -0,0 +1,267 @@
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;
@@ -0,0 +1,35 @@
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,
};
}
@@ -0,0 +1,65 @@
.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));
}
@@ -0,0 +1,29 @@
.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,24 +1,41 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import { useCallback } from "react";
import { useCallback, useRef, useState } from "react";
import { Node as PMNode } from "prosemirror-model";
import {
EditorMenuProps,
ShouldShowProps,
} from "@/features/editor/components/table/types/types.ts";
import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx";
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";
export function DrawioMenu({ editor }: EditorMenuProps) {
const shouldShow = useCallback(
({ state }: ShouldShowProps) => {
if (!state) {
return false;
}
return editor.isActive("drawio") && editor.getAttributes("drawio")?.src;
},
[editor],
);
const { t } = useTranslation();
const [opened, { open, close }] = useDisclosure(false);
const [initialXML, setInitialXML] = useState<string>("");
const drawioRef = useRef<DrawIoEmbedRef>(null);
const computedColorScheme = useComputedColorScheme();
const editorState = useEditorState({
editor,
@@ -30,11 +47,26 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
const drawioAttr = ctx.editor.getAttributes("drawio");
return {
isDrawio: ctx.editor.isActive("drawio"),
width: drawioAttr?.width ? parseInt(drawioAttr.width) : null,
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,
};
},
});
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;
@@ -57,38 +89,218 @@ export function DrawioMenu({ editor }: EditorMenuProps) {
};
}, [editor]);
const onWidthChange = useCallback(
(value: number) => {
editor.commands.updateAttributes("drawio", { width: `${value}%` });
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();
},
[editor],
[editor, editorState?.attachmentId, close],
);
return (
<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",
<>
<BaseBubbleMenu
editor={editor}
pluginKey={`drawio-menu`}
updateDelay={0}
getReferencedVirtualElement={getReferencedVirtualElement}
options={{
placement: "top",
offset: 8,
flip: false,
}}
shouldShow={shouldShow}
>
{editorState?.width && (
<NodeWidthResize onChange={onWidthChange} value={editorState.width} />
)}
</div>
</BaseBubbleMenu>
<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>
</>
);
}
@@ -2,7 +2,6 @@ import { NodeViewProps, NodeViewWrapper } from "@tiptap/react";
import {
ActionIcon,
Card,
Image,
Modal,
Text,
useComputedColorScheme,
@@ -10,7 +9,7 @@ import {
import { useRef, useState } from "react";
import { uploadFile } from "@/features/page/services/page-service.ts";
import { useDisclosure } from "@mantine/hooks";
import { getDrawioUrl, getFileUrl } from "@/lib/config.ts";
import { getDrawioUrl } from "@/lib/config.ts";
import {
DrawIoEmbed,
DrawIoEmbedRef,
@@ -26,7 +25,7 @@ import { useTranslation } from "react-i18next";
export default function DrawioView(props: NodeViewProps) {
const { t } = useTranslation();
const { node, updateAttributes, editor, selected } = props;
const { src, title, width, attachmentId } = node.attrs;
const { attachmentId } = node.attrs;
const drawioRef = useRef<DrawIoEmbedRef>(null);
const [initialXML, setInitialXML] = useState<string>("");
const [opened, { open, close }] = useDisclosure(false);
@@ -36,33 +35,11 @@ export default function DrawioView(props: NodeViewProps) {
if (!editor.isEditable) {
return;
}
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();
}
open();
};
const handleSave = async (data: EventSave) => {
const svgString = decodeBase64ToSvgString(data.xml);
const fileName = "diagram.drawio.svg";
const drawioSVGFile = await svgStringToFile(svgString, fileName);
@@ -70,7 +47,6 @@ export default function DrawioView(props: NodeViewProps) {
const pageId = editor.storage?.pageId;
let attachment: IAttachment = null;
if (attachmentId) {
attachment = await uploadFile(drawioSVGFile, pageId, attachmentId);
} else {
@@ -106,14 +82,12 @@ 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;
}
@@ -125,62 +99,28 @@ export default function DrawioView(props: NodeViewProps) {
</Modal.Content>
</Modal.Root>
{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",
)}
/>
<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>
{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>
)}
<Text component="span" size="lg" c="dimmed">
{t("Double-click to edit Draw.io diagram")}
</Text>
</div>
) : (
<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>
)}
</Card>
</NodeViewWrapper>
);
}
@@ -1,26 +1,57 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import { useCallback } from "react";
import { lazy, Suspense, useCallback, useState } from "react";
import { Node as PMNode } from "prosemirror-model";
import {
EditorMenuProps,
ShouldShowProps,
} from "@/features/editor/components/table/types/types.ts";
import { NodeWidthResize } from "@/features/editor/components/common/node-width-resize.tsx";
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,
})),
);
export function ExcalidrawMenu({ editor }: EditorMenuProps) {
const shouldShow = useCallback(
({ state }: ShouldShowProps) => {
if (!state) {
return false;
}
return (
editor.isActive("excalidraw") && editor.getAttributes("excalidraw")?.src
);
},
[editor],
);
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 editorState = useEditorState({
editor,
@@ -32,11 +63,29 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
const excalidrawAttr = ctx.editor.getAttributes("excalidraw");
return {
isExcalidraw: ctx.editor.isActive("excalidraw"),
width: excalidrawAttr?.width ? parseInt(excalidrawAttr.width) : null,
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,
};
},
});
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;
@@ -59,38 +108,248 @@ export function ExcalidrawMenu({ editor }: EditorMenuProps) {
};
}, [editor]);
const onWidthChange = useCallback(
(value: number) => {
editor.commands.updateAttributes("excalidraw", { width: `${value}%` });
},
[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]);
return (
<BaseBubbleMenu
editor={editor}
pluginKey={`excalidraw-menu`}
updateDelay={0}
getReferencedVirtualElement={getReferencedVirtualElement}
options={{
placement: "top",
offset: 8,
flip: false,
}}
shouldShow={shouldShow}
>
<div
<>
<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
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
backgroundColor: "rgba(0, 0, 0, 0.5)",
padding: 0,
zIndex: 200,
}}
isOpen={opened}
onRequestClose={close}
disableCloseOnBgClick={true}
contentProps={{
style: {
padding: 0,
width: "90vw",
},
}}
>
{editorState?.width && (
<NodeWidthResize onChange={onWidthChange} value={editorState.width} />
)}
</div>
</BaseBubbleMenu>
<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>
</>
);
}
@@ -4,28 +4,24 @@ import {
Button,
Card,
Group,
Image,
Text,
useComputedColorScheme,
} from "@mantine/core";
import { useState } from "react";
import { lazy, Suspense, 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 Excalidraw = lazy(() =>
const ExcalidrawComponent = lazy(() =>
import("@excalidraw/excalidraw").then((module) => ({
default: module.Excalidraw,
})),
@@ -34,7 +30,7 @@ const Excalidraw = lazy(() =>
export default function ExcalidrawView(props: NodeViewProps) {
const { t } = useTranslation();
const { node, updateAttributes, editor, selected } = props;
const { src, title, width, attachmentId } = node.attrs;
const { attachmentId } = node.attrs;
const [excalidrawAPI, setExcalidrawAPI] =
useState<ExcalidrawImperativeAPI>(null);
@@ -50,25 +46,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
if (!editor.isEditable) {
return;
}
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();
}
open();
};
const handleSave = async () => {
@@ -151,7 +129,7 @@ export default function ExcalidrawView(props: NodeViewProps) {
</Group>
<div style={{ height: "90vh" }}>
<Suspense fallback={null}>
<Excalidraw
<ExcalidrawComponent
excalidrawAPI={(api) => setExcalidrawAPI(api)}
initialData={{
...excalidrawData,
@@ -163,62 +141,28 @@ export default function ExcalidrawView(props: NodeViewProps) {
</div>
</ReactClearModal>
{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",
)}
/>
<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>
{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>
)}
<Text component="span" size="lg" c="dimmed">
{t("Double-click to edit Excalidraw diagram")}
</Text>
</div>
) : (
<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>
)}
</Card>
</NodeViewWrapper>
);
}
@@ -1,22 +1,29 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import React, { useCallback } from "react";
import React, { useCallback, useRef } 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,
@@ -32,7 +39,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" }),
width: imageAttrs?.width ? parseInt(imageAttrs.width) : null,
src: imageAttrs?.src || null,
};
},
});
@@ -94,17 +101,40 @@ export function ImageMenu({ editor }: EditorMenuProps) {
.run();
}, [editor]);
const onWidthChange = useCallback(
(value: number) => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
.setImageWidth(value)
.run();
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 = "";
},
[editor],
);
const handleDelete = useCallback(() => {
editor.commands.deleteSelection();
}, [editor]);
return (
<BaseBubbleMenu
editor={editor}
@@ -118,13 +148,14 @@ export function ImageMenu({ editor }: EditorMenuProps) {
}}
shouldShow={shouldShow}
>
<ActionIcon.Group className="actionIconGroup">
<div className={classes.toolbar}>
<Tooltip position="top" label={t("Align left")}>
<ActionIcon
onClick={alignImageLeft}
size="lg"
aria-label={t("Align left")}
variant={editorState?.isAlignLeft ? "light" : "default"}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isAlignLeft })}
>
<IconLayoutAlignLeft size={18} />
</ActionIcon>
@@ -135,7 +166,8 @@ export function ImageMenu({ editor }: EditorMenuProps) {
onClick={alignImageCenter}
size="lg"
aria-label={t("Align center")}
variant={editorState?.isAlignCenter ? "light" : "default"}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isAlignCenter })}
>
<IconLayoutAlignCenter size={18} />
</ActionIcon>
@@ -146,16 +178,56 @@ export function ImageMenu({ editor }: EditorMenuProps) {
onClick={alignImageRight}
size="lg"
aria-label={t("Align right")}
variant={editorState?.isAlignRight ? "light" : "default"}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isAlignRight })}
>
<IconLayoutAlignRight size={18} />
</ActionIcon>
</Tooltip>
</ActionIcon.Group>
{editorState?.width && (
<NodeWidthResize onChange={onWidthChange} value={editorState.width} />
)}
<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}
/>
</BaseBubbleMenu>
);
}
@@ -0,0 +1,7 @@
import {
createResizeHandle,
buildResizeClasses,
} from "../common/node-resize-handles";
export const createImageHandle = createResizeHandle;
export const imageResizeClasses = buildResizeClasses("node-image");
@@ -0,0 +1,64 @@
.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,6 +20,8 @@ import {
IconCalendar,
IconAppWindow,
IconSitemap,
IconColumns3,
IconColumns2,
} from "@tabler/icons-react";
import {
CommandProps,
@@ -31,6 +33,8 @@ 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,
@@ -390,6 +394,58 @@ 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="default"
variant="subtle"
size="lg"
aria-label={t("Background color")}
onClick={() => setOpened(!opened)}
@@ -16,6 +16,7 @@ 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 => {
@@ -69,14 +70,16 @@ export const TableCellMenu = React.memo(
}}
shouldShow={shouldShow}
>
<ActionIcon.Group>
<div className={classes.toolbar}>
<TableBackgroundColor editor={editor} />
<TableTextAlignment editor={editor} />
<div className={classes.divider} />
<Tooltip position="top" label={t("Merge cells")}>
<ActionIcon
onClick={mergeCells}
variant="default"
variant="subtle"
size="lg"
aria-label={t("Merge cells")}
>
@@ -87,7 +90,7 @@ export const TableCellMenu = React.memo(
<Tooltip position="top" label={t("Split cell")}>
<ActionIcon
onClick={splitCell}
variant="default"
variant="subtle"
size="lg"
aria-label={t("Split cell")}
>
@@ -95,10 +98,12 @@ export const TableCellMenu = React.memo(
</ActionIcon>
</Tooltip>
<div className={classes.divider} />
<Tooltip position="top" label={t("Delete column")}>
<ActionIcon
onClick={deleteColumn}
variant="default"
variant="subtle"
size="lg"
aria-label={t("Delete column")}
>
@@ -109,7 +114,7 @@ export const TableCellMenu = React.memo(
<Tooltip position="top" label={t("Delete row")}>
<ActionIcon
onClick={deleteRow}
variant="default"
variant="subtle"
size="lg"
aria-label={t("Delete row")}
>
@@ -117,17 +122,19 @@ export const TableCellMenu = React.memo(
</ActionIcon>
</Tooltip>
<div className={classes.divider} />
<Tooltip position="top" label={t("Toggle header cell")}>
<ActionIcon
onClick={toggleHeaderCell}
variant="default"
variant="subtle"
size="lg"
aria-label={t("Toggle header cell")}
>
<IconTableRow size={18} />
</ActionIcon>
</Tooltip>
</ActionIcon.Group>
</div>
</BubbleMenu>
);
}
@@ -18,8 +18,9 @@ import {
IconTrashX,
} from "@tabler/icons-react";
import { BubbleMenu } from "@tiptap/react/menus";
import { isCellSelection } from "@docmost/editor-ext";
import { isCellSelection, isTextSelected } 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 => {
@@ -30,6 +31,7 @@ export const TableMenu = React.memo(
return false;
}
if (isTextSelected(editor)) return false;
return editor.isActive("table") && !isCellSelection(state.selection);
},
[editor]
@@ -118,11 +120,11 @@ export const TableMenu = React.memo(
}}
shouldShow={shouldShow}
>
<ActionIcon.Group>
<div className={classes.toolbar}>
<Tooltip position="top" label={t("Add left column")}>
<ActionIcon
onClick={addColumnLeft}
variant="default"
variant="subtle"
size="lg"
aria-label={t("Add left column")}
>
@@ -133,7 +135,7 @@ export const TableMenu = React.memo(
<Tooltip position="top" label={t("Add right column")}>
<ActionIcon
onClick={addColumnRight}
variant="default"
variant="subtle"
size="lg"
aria-label={t("Add right column")}
>
@@ -144,7 +146,7 @@ export const TableMenu = React.memo(
<Tooltip position="top" label={t("Delete column")}>
<ActionIcon
onClick={deleteColumn}
variant="default"
variant="subtle"
size="lg"
aria-label={t("Delete column")}
>
@@ -152,10 +154,12 @@ export const TableMenu = React.memo(
</ActionIcon>
</Tooltip>
<div className={classes.divider} />
<Tooltip position="top" label={t("Add row above")}>
<ActionIcon
onClick={addRowAbove}
variant="default"
variant="subtle"
size="lg"
aria-label={t("Add row above")}
>
@@ -166,7 +170,7 @@ export const TableMenu = React.memo(
<Tooltip position="top" label={t("Add row below")}>
<ActionIcon
onClick={addRowBelow}
variant="default"
variant="subtle"
size="lg"
aria-label={t("Add row below")}
>
@@ -177,7 +181,7 @@ export const TableMenu = React.memo(
<Tooltip position="top" label={t("Delete row")}>
<ActionIcon
onClick={deleteRow}
variant="default"
variant="subtle"
size="lg"
aria-label={t("Delete row")}
>
@@ -185,10 +189,12 @@ export const TableMenu = React.memo(
</ActionIcon>
</Tooltip>
<div className={classes.divider} />
<Tooltip position="top" label={t("Toggle header row")}>
<ActionIcon
onClick={toggleHeaderRow}
variant="default"
variant="subtle"
size="lg"
aria-label={t("Toggle header row")}
>
@@ -199,7 +205,7 @@ export const TableMenu = React.memo(
<Tooltip position="top" label={t("Toggle header column")}>
<ActionIcon
onClick={toggleHeaderColumn}
variant="default"
variant="subtle"
size="lg"
aria-label={t("Toggle header column")}
>
@@ -207,18 +213,19 @@ export const TableMenu = React.memo(
</ActionIcon>
</Tooltip>
<div className={classes.divider} />
<Tooltip position="top" label={t("Delete table")}>
<ActionIcon
onClick={deleteTable}
variant="default"
variant="subtle"
size="lg"
color="red"
aria-label={t("Delete table")}
>
<IconTrashX size={18} />
</ActionIcon>
</Tooltip>
</ActionIcon.Group>
</div>
</BubbleMenu>
);
}
@@ -88,7 +88,7 @@ export const TableTextAlignment: FC<TableTextAlignmentProps> = ({ editor }) => {
<Popover.Target>
<Tooltip label={t("Text alignment")} withArrow>
<ActionIcon
variant="default"
variant="subtle"
size="lg"
aria-label={t("Text alignment")}
onClick={() => setOpened(!opened)}
@@ -1,19 +1,23 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import React, { useCallback } 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, 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();
@@ -32,7 +36,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" }),
width: videoAttrs?.width ? parseInt(videoAttrs.width) : null,
src: videoAttrs?.src || null,
};
},
});
@@ -70,7 +74,7 @@ export function VideoMenu({ editor }: EditorMenuProps) {
};
}, [editor]);
const alignVideoLeft = useCallback(() => {
const alignLeft = useCallback(() => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
@@ -78,7 +82,7 @@ export function VideoMenu({ editor }: EditorMenuProps) {
.run();
}, [editor]);
const alignVideoCenter = useCallback(() => {
const alignCenter = useCallback(() => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
@@ -86,7 +90,7 @@ export function VideoMenu({ editor }: EditorMenuProps) {
.run();
}, [editor]);
const alignVideoRight = useCallback(() => {
const alignRight = useCallback(() => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
@@ -94,16 +98,18 @@ export function VideoMenu({ editor }: EditorMenuProps) {
.run();
}, [editor]);
const onWidthChange = useCallback(
(value: number) => {
editor
.chain()
.focus(undefined, { scrollIntoView: false })
.setVideoWidth(value)
.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]);
return (
<BaseBubbleMenu
@@ -118,13 +124,14 @@ export function VideoMenu({ editor }: EditorMenuProps) {
}}
shouldShow={shouldShow}
>
<ActionIcon.Group className="actionIconGroup">
<div className={classes.toolbar}>
<Tooltip position="top" label={t("Align left")}>
<ActionIcon
onClick={alignVideoLeft}
onClick={alignLeft}
size="lg"
aria-label={t("Align left")}
variant={editorState?.isAlignLeft ? "light" : "default"}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isAlignLeft })}
>
<IconLayoutAlignLeft size={18} />
</ActionIcon>
@@ -132,10 +139,11 @@ export function VideoMenu({ editor }: EditorMenuProps) {
<Tooltip position="top" label={t("Align center")}>
<ActionIcon
onClick={alignVideoCenter}
onClick={alignCenter}
size="lg"
aria-label={t("Align center")}
variant={editorState?.isAlignCenter ? "light" : "default"}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isAlignCenter })}
>
<IconLayoutAlignCenter size={18} />
</ActionIcon>
@@ -143,19 +151,40 @@ export function VideoMenu({ editor }: EditorMenuProps) {
<Tooltip position="top" label={t("Align right")}>
<ActionIcon
onClick={alignVideoRight}
onClick={alignRight}
size="lg"
aria-label={t("Align right")}
variant={editorState?.isAlignRight ? "light" : "default"}
variant="subtle"
className={clsx({ [classes.active]: editorState?.isAlignRight })}
>
<IconLayoutAlignRight size={18} />
</ActionIcon>
</Tooltip>
</ActionIcon.Group>
{editorState?.width && (
<NodeWidthResize onChange={onWidthChange} value={editorState.width} />
)}
<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>
</BaseBubbleMenu>
);
}
@@ -43,6 +43,8 @@ import {
Highlight,
UniqueID,
SharedStorage,
Columns,
Column,
} from "@docmost/editor-ext";
import {
randomElement,
@@ -52,6 +54,14 @@ 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";
@@ -91,6 +101,7 @@ lowlight.register("fortran", fortran);
lowlight.register("haskell", haskell);
lowlight.register("scala", scala);
// @ts-ignore
export const mainExtensions = [
StarterKit.configure({
heading: false,
@@ -115,7 +126,7 @@ export const mainExtensions = [
filterTransaction: (transaction) => !isChangeOrigin(transaction),
}),
Placeholder.configure({
placeholder: ({ node }) => {
placeholder: ({ editor, node, pos }) => {
if (node.type.name === "heading") {
return i18n.t("Heading {{level}}", { level: node.attrs.level });
}
@@ -123,6 +134,17 @@ 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');
}
},
@@ -200,9 +222,29 @@ 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,
@@ -221,9 +263,29 @@ 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,
@@ -253,6 +315,8 @@ export const mainExtensions = [
};
},
}).configure(),
Columns,
Column,
] as any;
type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[];
@@ -67,6 +67,7 @@ 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;
@@ -416,6 +417,7 @@ export default function PageEditor({
<SubpagesMenu editor={editor} />
<ExcalidrawMenu editor={editor} />
<DrawioMenu editor={editor} />
<ColumnsMenu editor={editor} />
<LinkMenu editor={editor} appendTo={menuContainerRef} />
</div>
)}
@@ -0,0 +1,116 @@
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%;
}
}
+11 -14
View File
@@ -82,13 +82,9 @@
}
blockquote {
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)
);
padding-left: 1rem;
border-left: 3px solid
light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4));
margin: 0;
}
@@ -126,13 +122,14 @@
margin-bottom: 0;
}
&.node-callout {
div[style*="white-space: inherit;"] {
> :first-child {
margin: 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;
}
.selection {
@@ -13,3 +13,4 @@
@import "./mention.css";
@import "./ordered-list.css";
@import "./highlight.css";
@import "./columns.css";
+5 -1
View File
@@ -68,10 +68,14 @@ 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))) {
window.location.href = APP_ROUTE.AUTH.LOGIN;
const redirectTo = window.location.pathname;
const params = new URLSearchParams({ redirect: redirectTo });
window.location.href = `${APP_ROUTE.AUTH.LOGIN}?${params.toString()}`;
}
}
+16
View File
@@ -29,4 +29,20 @@ 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 root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement,
);
const container = document.getElementById("root") as HTMLElement;
const root = (container as any).__reactRoot ??= ReactDOM.createRoot(container);
root.render(
<BrowserRouter>
@@ -35,6 +35,8 @@ 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';
@@ -91,6 +93,8 @@ export const tiptapExtensions = [
Embed,
Mention,
Subpages,
Columns,
Column,
] as any;
export function jsonToHtml(tiptapJson: any) {
@@ -2,6 +2,7 @@ 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;
@@ -40,7 +41,7 @@ export async function prepareFile(
fileName,
fileSize,
fileExtension,
mimeType: file.mimetype,
mimeType: getMimeType(file.filename),
multiPartFile: file,
};
} catch (error) {
+2 -47
View File
@@ -1,51 +1,6 @@
import {
ApiKeys,
Attachments,
AuthAccounts,
AuthProviders,
Backlinks,
Billing,
Comments,
FileTasks,
Groups,
GroupUsers,
Notifications,
PageHistory,
Pages,
Shares,
SpaceMembers,
Spaces,
UserMfa,
Users,
UserTokens,
Watchers,
WorkspaceInvitations,
Workspaces,
} from '@docmost/db/types/db';
import { DB } from '@docmost/db/types/db';
import { PageEmbeddings } from '@docmost/db/types/embeddings.types';
export interface DbInterface {
attachments: Attachments;
authAccounts: AuthAccounts;
authProviders: AuthProviders;
backlinks: Backlinks;
billing: Billing;
comments: Comments;
fileTasks: FileTasks;
groups: Groups;
groupUsers: GroupUsers;
notifications: Notifications;
export interface DbInterface extends DB {
pageEmbeddings: PageEmbeddings;
pageHistory: PageHistory;
pages: Pages;
shares: Shares;
spaceMembers: SpaceMembers;
spaces: Spaces;
userMfa: UserMfa;
users: Users;
userTokens: UserTokens;
watchers: Watchers;
workspaceInvitations: WorkspaceInvitations;
workspaces: Workspaces;
apiKeys: ApiKeys;
}
+2 -1
View File
@@ -96,7 +96,8 @@
"packageManager": "pnpm@10.4.0",
"pnpm": {
"patchedDependencies": {
"react-arborist@3.4.0": "patches/react-arborist@3.4.0.patch"
"react-arborist@3.4.0": "patches/react-arborist@3.4.0.patch",
"@tiptap/core": "patches/@tiptap__core.patch"
},
"overrides": {
"jsdom": "25.0.1",
+1
View File
@@ -25,3 +25,4 @@ export * from "./lib/heading/heading";
export * from "./lib/unique-id";
export * from "./lib/shared-storage";
export * from "./lib/recreate-transform";
export * from "./lib/columns";
+16 -3
View File
@@ -1,8 +1,21 @@
export type CalloutType = "default" | "info" | "success" | "warning" | "danger";
const validCalloutTypes = ["default", "info", "success", "warning", "danger"];
export type CalloutType =
| 'default'
| 'info'
| 'note'
| 'success'
| 'warning'
| 'danger';
const validCalloutTypes = [
'default',
'info',
'note',
'success',
'warning',
'danger',
];
export function getValidCalloutType(value: string): string {
if (value) {
return validCalloutTypes.includes(value) ? value : "info";
return validCalloutTypes.includes(value) ? value : 'info';
}
}
@@ -0,0 +1,127 @@
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 }),
};
},
});
@@ -0,0 +1,196 @@
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 }),
};
},
});
@@ -0,0 +1,4 @@
export { Columns } from "./columns";
export type { ColumnsOptions, ColumnsAttributes, ColumnsLayout, WidthMode } from "./columns";
export { Column } from "./column";
export type { ColumnOptions, ColumnAttributes } from "./column";
+215 -8
View File
@@ -1,15 +1,35 @@
import { Node, mergeAttributes } from "@tiptap/core";
import { Node, mergeAttributes, ResizableNodeView } from "@tiptap/core";
import type { ResizableNodeViewDirection } 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?: string;
width?: number | string;
height?: number;
aspectRatio?: number;
align?: string;
attachmentId?: string;
}
@@ -18,6 +38,8 @@ declare module "@tiptap/core" {
interface Commands<ReturnType> {
drawio: {
setDrawio: (attributes?: DrawioAttributes) => ReturnType;
setDrawioAlign: (align: "left" | "center" | "right") => ReturnType;
setDrawioSize: (width: number, height: number) => ReturnType;
};
}
}
@@ -35,6 +57,7 @@ export const Drawio = Node.create<DrawioOptions>({
return {
HTMLAttributes: {},
view: null,
resize: false,
};
},
@@ -55,12 +78,30 @@ export const Drawio = Node.create<DrawioOptions>({
}),
},
width: {
default: "100%",
parseHTML: (element) => element.getAttribute("data-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;
},
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"),
@@ -68,6 +109,13 @@ 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"),
@@ -99,7 +147,7 @@ export const Drawio = Node.create<DrawioOptions>({
mergeAttributes(
{ "data-type": this.name },
this.options.HTMLAttributes,
HTMLAttributes
HTMLAttributes,
),
[
"img",
@@ -122,13 +170,172 @@ 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() {
// 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;
const resize = this.options.resize;
return ReactNodeViewRenderer(this.options.view);
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;
};
},
});
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";
}
}
+216 -8
View File
@@ -1,15 +1,35 @@
import { Node, mergeAttributes } from "@tiptap/core";
import { Node, mergeAttributes, ResizableNodeView } from "@tiptap/core";
import type { ResizableNodeViewDirection } 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?: string;
width?: number | string;
height?: number;
aspectRatio?: number;
align?: string;
attachmentId?: string;
}
@@ -18,6 +38,8 @@ declare module "@tiptap/core" {
interface Commands<ReturnType> {
excalidraw: {
setExcalidraw: (attributes?: ExcalidrawAttributes) => ReturnType;
setExcalidrawAlign: (align: "left" | "center" | "right") => ReturnType;
setExcalidrawSize: (width: number, height: number) => ReturnType;
};
}
}
@@ -35,8 +57,10 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
return {
HTMLAttributes: {},
view: null,
resize: false,
};
},
addAttributes() {
return {
src: {
@@ -54,12 +78,30 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
}),
},
width: {
default: "100%",
parseHTML: (element) => element.getAttribute("data-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;
},
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"),
@@ -67,6 +109,13 @@ 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"),
@@ -98,7 +147,7 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
mergeAttributes(
{ "data-type": this.name },
this.options.HTMLAttributes,
HTMLAttributes
HTMLAttributes,
),
[
"img",
@@ -121,13 +170,172 @@ 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() {
// 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;
const resize = this.options.resize;
return ReactNodeViewRenderer(this.options.view);
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;
};
},
});
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 > 0) {
if (node.type.name === "heading" && node.content.size > 1) {
const deco = Decoration.widget(
pos + node.nodeSize - 1,
() => {
+230 -11
View File
@@ -1,18 +1,41 @@
import Image from "@tiptap/extension-image";
import { ImageOptions as DefaultImageOptions } from "@tiptap/extension-image";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { mergeAttributes, Range } from "@tiptap/core";
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;
};
};
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;
width?: number | string;
height?: number;
aspectRatio?: number;
placeholder?: {
id: string;
@@ -25,10 +48,11 @@ 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;
};
}
}
@@ -46,6 +70,7 @@ export const TiptapImage = Image.extend<ImageOptions>({
return {
...this.parent?.(),
view: null,
resize: false,
};
},
@@ -59,12 +84,30 @@ export const TiptapImage = Image.extend<ImageOptions>({
}),
},
width: {
default: "100%",
parseHTML: (element) => element.getAttribute("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;
},
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"),
@@ -142,16 +185,192 @@ export const TiptapImage = Image.extend<ImageOptions>({
setImageWidth:
(width) =>
({ commands }) =>
commands.updateAttributes("image", {
width: `${Math.max(0, Math.min(100, width))}%`,
}),
commands.updateAttributes("image", { width }),
setImageSize:
(width, height) =>
({ commands }) =>
commands.updateAttributes("image", { width, height }),
};
},
addNodeView() {
// Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191)
this.editor.isInitialized = true;
const resize = this.options.resize;
return ReactNodeViewRenderer(this.options.view);
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;
};
},
});
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";
}
}
+201 -7
View File
@@ -1,16 +1,35 @@
import { ReactNodeViewRenderer } from "@tiptap/react";
import { Range, Node } from "@tiptap/core";
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;
};
};
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;
width?: number | string;
height?: number;
aspectRatio?: number;
placeholder?: {
id: string;
@@ -27,6 +46,7 @@ declare module "@tiptap/core" {
) => ReturnType;
setVideoAlign: (align: "left" | "center" | "right") => ReturnType;
setVideoWidth: (width: number) => ReturnType;
setVideoSize: (width: number, height: number) => ReturnType;
};
}
}
@@ -44,6 +64,7 @@ export const TiptapVideo = Node.create<VideoOptions>({
return {
view: null,
HTMLAttributes: {},
resize: false,
};
},
@@ -64,12 +85,30 @@ export const TiptapVideo = Node.create<VideoOptions>({
}),
},
width: {
default: "100%",
parseHTML: (element) => element.getAttribute("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;
},
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"),
@@ -136,13 +175,168 @@ 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() {
// 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;
const resize = this.options.resize;
return ReactNodeViewRenderer(this.options.view);
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;
};
},
});
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
@@ -0,0 +1,105 @@
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)
}
+141 -138
View File
@@ -29,6 +29,9 @@ 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
@@ -57,7 +60,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(@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(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)
'@joplin/turndown':
specifier: ^4.0.74
version: 4.0.74
@@ -69,85 +72,85 @@ importers:
version: 1.1.0
'@tiptap/core':
specifier: 3.17.1
version: 3.17.1(@tiptap/pm@3.17.1)
version: 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/extension-code-block':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@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/extension-collaboration':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@tiptap/y-tiptap@3.0.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(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-caret':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@tiptap/y-tiptap@3.0.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(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-color':
specifier: 3.17.1
version: 3.17.1(@tiptap/extension-text-style@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)))
version: 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-document':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
'@tiptap/extension-heading':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
'@tiptap/extension-highlight':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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(@tiptap/pm@3.17.1))(@tiptap/pm@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))
'@tiptap/extension-image':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
'@tiptap/extension-link':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@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/extension-list':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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(@tiptap/pm@3.17.1))(@tiptap/pm@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))
'@tiptap/extension-subscript':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@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/extension-superscript':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@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/extension-table':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@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/extension-text':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
'@tiptap/extension-text-align':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
'@tiptap/extension-text-style':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
'@tiptap/extension-typography':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
'@tiptap/extension-unique-id':
specifier: ^3.17.1
version: 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@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)
'@tiptap/extension-youtube':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))
version: 3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))
'@tiptap/html':
specifier: 3.17.1
version: 3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(happy-dom@20.1.0)
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)
'@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(@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(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/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(@tiptap/pm@3.17.1))(@tiptap/pm@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':
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)
@@ -12332,9 +12335,9 @@ snapshots:
- bufferutil
- utf-8-validate
'@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)':
'@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)':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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)
@@ -14895,191 +14898,191 @@ snapshots:
'@tanstack/query-core': 5.90.17
react: 18.3.1
'@tiptap/core@3.17.1(@tiptap/pm@3.17.1)':
'@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)':
dependencies:
'@tiptap/pm': 3.17.1
'@tiptap/extension-blockquote@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))':
'@tiptap/extension-blockquote@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@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(@tiptap/pm@3.17.1))':
'@tiptap/extension-bold@3.17.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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-bubble-menu@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)':
dependencies:
'@floating-ui/dom': 1.7.4
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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(@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(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))':
dependencies:
'@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-code-block@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)':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@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(@tiptap/pm@3.17.1))':
'@tiptap/extension-code@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/extension-collaboration-caret@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(@tiptap/y-tiptap@3.0.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(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))':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@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@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@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)':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@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-color@3.17.1(@tiptap/extension-text-style@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1)))':
'@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)))':
dependencies:
'@tiptap/extension-text-style': 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-document@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))':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@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(@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(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@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/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)':
'@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)':
dependencies:
'@floating-ui/dom': 1.7.3
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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(@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))':
dependencies:
'@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@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/extension-hard-break@3.19.0(@tiptap/core@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))':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@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))':
'@tiptap/extension-heading@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@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))':
'@tiptap/extension-highlight@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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))':
'@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))':
dependencies:
'@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@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/extension-horizontal-rule@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)':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/pm': 3.17.1
'@tiptap/extension-image@3.17.1(@tiptap/core@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))':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@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))':
'@tiptap/extension-italic@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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-link@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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(@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))':
dependencies:
'@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-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-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))':
dependencies:
'@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)':
'@tiptap/extension-list@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@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))':
'@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))':
dependencies:
'@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-paragraph@3.19.0(@tiptap/core@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))':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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))':
'@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))':
dependencies:
'@tiptap/extensions': 3.19.0(@tiptap/core@3.17.1(@tiptap/pm@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/extension-strike@3.19.0(@tiptap/core@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))':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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)':
'@tiptap/extension-subscript@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@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)':
'@tiptap/extension-superscript@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@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)':
'@tiptap/extension-table@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/pm': 3.17.1
'@tiptap/extension-text-align@3.17.1(@tiptap/core@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))':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@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-text-style@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@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))':
'@tiptap/extension-text@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@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))':
'@tiptap/extension-typography@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@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(@tiptap/pm@3.17.1))':
'@tiptap/extension-underline@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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)':
'@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)':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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(@tiptap/pm@3.17.1))':
'@tiptap/extension-youtube@3.17.1(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@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/extensions@3.19.0(@tiptap/core@3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/pm': 3.17.1
'@tiptap/html@3.17.1(@tiptap/core@3.17.1(@tiptap/pm@3.17.1))(@tiptap/pm@3.17.1)(happy-dom@20.1.0)':
'@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)':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@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
@@ -15104,9 +15107,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(@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(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)':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@3.17.1)
'@tiptap/core': 3.17.1(patch_hash=efe36d923d71b90e115d2d468ea1ddaf04a78c2e43c811a1a4b667989c39ea00)(@tiptap/pm@3.17.1)
'@tiptap/pm': 3.17.1
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
@@ -15116,41 +15119,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(@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)
'@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)
transitivePeerDependencies:
- '@floating-ui/dom'
'@tiptap/starter-kit@3.17.1':
dependencies:
'@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/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/pm': 3.17.1
'@tiptap/suggestion@3.17.1(@tiptap/core@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)':
dependencies:
'@tiptap/core': 3.17.1(@tiptap/pm@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)':