Compare commits

..

20 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
16 changed files with 1464 additions and 1601 deletions
+1 -1
View File
@@ -59,7 +59,7 @@
},
"devDependencies": {
"@eslint/js": "^9.16.0",
"@tanstack/eslint-plugin-query": "^5.91.4",
"@tanstack/eslint-plugin-query": "^5.62.1",
"@types/blueimp-load-image": "^5.16.0",
"@types/file-saver": "^2.0.7",
"@types/js-cookie": "^3.0.6",
@@ -1,7 +1,7 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import React, { useCallback } from "react";
import { Node as PMNode } from "@tiptap/pm/model";
import { Node as PMNode } from "prosemirror-model";
import {
EditorMenuProps,
ShouldShowProps,
@@ -1,7 +1,7 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import React, { useCallback, useRef, useState } from "react";
import { DOMSerializer, Node as PMNode } from "@tiptap/pm/model";
import React, { useCallback, useState } from "react";
import { Node as PMNode } from "prosemirror-model";
import {
EditorMenuProps,
ShouldShowProps,
@@ -16,8 +16,6 @@ import {
IconLayoutSidebar,
IconLayoutSidebarRight,
IconLayoutAlignCenter,
IconCopy,
IconTrash,
} from "@tabler/icons-react";
import { isTextSelected } from "@docmost/editor-ext";
import type { WidthMode, ColumnsLayout } from "@docmost/editor-ext";
@@ -56,7 +54,8 @@ const threeColumnPresets: LayoutPreset[] = [
label: "Left wide",
icon: IconLayoutSidebarRight,
},
{ layout: "three_right_wide", label: "Right wide", icon: IconLayoutSidebar },
{ layout: "three_right_wide", label: "Right wide", icon: IconLayoutSidebar
},
];
function getPresetsForCount(count: number): LayoutPreset[] {
@@ -68,8 +67,6 @@ function getPresetsForCount(count: number): LayoutPreset[] {
export function ColumnsMenu({ editor }: EditorMenuProps) {
const { t } = useTranslation();
const [isCountOpen, setIsCountOpen] = useState(false);
const [copied, setCopied] = useState(false);
const copyTimerRef = useRef<ReturnType<typeof setTimeout>>();
const nodesWithMenus = [
"callout",
@@ -190,65 +187,6 @@ export function ColumnsMenu({ editor }: EditorMenuProps) {
[editor],
);
const handleCopy = useCallback(() => {
const { state } = editor;
const parent = findParentNode(
(node: PMNode) => node.type.name === "columns",
)(state.selection);
if (!parent) return;
const serializer = DOMSerializer.fromSchema(state.schema);
const dom = serializer.serializeNode(parent.node);
const wrapper = document.createElement("div");
wrapper.appendChild(dom);
const onSuccess = () => {
clearTimeout(copyTimerRef.current);
setCopied(true);
copyTimerRef.current = setTimeout(() => setCopied(false), 1500);
};
if (navigator.clipboard?.write) {
navigator.clipboard
.write([
new ClipboardItem({
"text/html": new Blob([wrapper.innerHTML], { type: "text/html" }),
"text/plain": new Blob([parent.node.textContent], {
type: "text/plain",
}),
}),
])
.then(onSuccess)
.catch(execCommandFallback);
} else {
execCommandFallback();
}
function execCommandFallback() {
wrapper.style.position = "fixed";
wrapper.style.left = "-9999px";
document.body.appendChild(wrapper);
const range = document.createRange();
range.selectNodeContents(wrapper);
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(range);
document.execCommand("copy");
sel?.removeAllRanges();
document.body.removeChild(wrapper);
editor.view.focus();
onSuccess();
}
}, [editor]);
const handleDelete = useCallback(() => {
const parent = findParentNode(
(node: PMNode) => node.type.name === "columns",
)(editor.state.selection);
if (!parent) return;
editor.chain().focus().setNodeSelection(parent.pos).deleteSelection().run();
}, [editor]);
const columnCount = editorState?.columnCount || 2;
const currentLayout = editorState?.layout || "two_equal";
const presets = getPresetsForCount(columnCount);
@@ -321,38 +259,6 @@ export function ColumnsMenu({ editor }: EditorMenuProps) {
</ActionIcon>
</Tooltip>
))}
<div className={classes.divider} />
<Tooltip
position="top"
label={copied ? t("Copied") : t("Copy")}
withinPortal={false}
>
<ActionIcon
onClick={handleCopy}
size="lg"
aria-label={t("Copy")}
variant="subtle"
>
{copied ? (
<IconCheck size={18} color="var(--mantine-color-green-6)" />
) : (
<IconCopy size={18} />
)}
</ActionIcon>
</Tooltip>
<Tooltip position="top" label={t("Delete")} withinPortal={false}>
<ActionIcon
onClick={handleDelete}
size="lg"
aria-label={t("Delete")}
variant="subtle"
>
<IconTrash size={18} />
</ActionIcon>
</Tooltip>
</div>
</BaseBubbleMenu>
);
@@ -1,7 +1,7 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import { useCallback, useRef, useState } from "react";
import { Node as PMNode } from "@tiptap/pm/model";
import { Node as PMNode } from "prosemirror-model";
import {
EditorMenuProps,
ShouldShowProps,
@@ -1,7 +1,7 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import { lazy, Suspense, useCallback, useState } from "react";
import { Node as PMNode } from "@tiptap/pm/model";
import { Node as PMNode } from "prosemirror-model";
import {
EditorMenuProps,
ShouldShowProps,
@@ -1,7 +1,7 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import React, { useCallback, useRef } from "react";
import { Node as PMNode } from "@tiptap/pm/model";
import { Node as PMNode } from "prosemirror-model";
import {
EditorMenuProps,
ShouldShowProps,
@@ -1,7 +1,7 @@
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
import { useCallback } from "react";
import { Node as PMNode } from "@tiptap/pm/model";
import { Node as PMNode } from "prosemirror-model";
import {
EditorMenuProps,
ShouldShowProps,
@@ -1,6 +1,4 @@
import { markInputRule } from "@tiptap/core";
import { StarterKit } from "@tiptap/starter-kit";
import { Code } from "@tiptap/extension-code";
import { TextAlign } from "@tiptap/extension-text-align";
import { TaskList, TaskItem } from "@tiptap/extension-list";
import { Placeholder, CharacterCount } from "@tiptap/extensions";
@@ -115,24 +113,10 @@ export const mainExtensions = [
color: "#70CFF8",
},
codeBlock: false,
code: false,
}),
// Override TipTap's Code extension to fix the inline code input rule.
// The upstream regex /(^|[^`])`([^`]+)`(?!`)$/ captures the character
// before the opening backtick as part of the match, causing markInputRule
// to delete it. Using a lookbehind avoids including it in the match.
Code.configure({
HTMLAttributes: {
spellcheck: false,
},
}).extend({
addInputRules() {
return [
markInputRule({
find: /(?:^|(?<=[^`]))`([^`]+)`(?!`)$/,
type: this.type,
}),
];
code: {
HTMLAttributes: {
spellcheck: false,
},
},
}),
SharedStorage,
@@ -1,17 +1,12 @@
div[data-type="columns"] {
display: flex;
margin: 0.75rem 0;
padding: 0.5em 0;
padding: 0.5em;
}
div[data-type="columns"] > div[data-type="column"] {
flex: 1;
min-width: 0;
padding-right: 1rem;
}
div[data-type="columns"] > div[data-type="column"]:last-child {
padding-right: 0;
}
div[data-type="columns"] > div[data-type="column"] + div[data-type="column"] {
@@ -21,9 +16,6 @@ div[data-type="columns"] > div[data-type="column"] + div[data-type="column"] {
}
div[data-type="columns"]:hover
> div[data-type="column"]
+ div[data-type="column"],
div[data-type="columns"].has-focus
> div[data-type="column"]
+ div[data-type="column"] {
border-left: 1px solid
@@ -74,7 +66,7 @@ div[data-type="columns"][data-layout="three_with_sidebars"]
}
/* Stack columns vertically on small viewports */
@media (max-width: 680px) {
@media (max-width: 820px) {
div[data-type="columns"] {
flex-direction: column;
}
+9 -9
View File
@@ -33,9 +33,9 @@
"@ai-sdk/google": "^3.0.29",
"@ai-sdk/openai": "^3.0.29",
"@ai-sdk/openai-compatible": "^2.0.30",
"@aws-sdk/client-s3": "3.998.0",
"@aws-sdk/lib-storage": "3.998.0",
"@aws-sdk/s3-request-presigner": "3.998.0",
"@aws-sdk/client-s3": "3.982.0",
"@aws-sdk/lib-storage": "3.982.0",
"@aws-sdk/s3-request-presigner": "3.982.0",
"@fastify/cookie": "^11.0.2",
"@fastify/multipart": "^9.4.0",
"@fastify/static": "^9.0.0",
@@ -43,18 +43,18 @@
"@langchain/textsplitters": "1.0.1",
"@nestjs-labs/nestjs-ioredis": "^11.0.4",
"@nestjs/bullmq": "^11.0.4",
"@nestjs/common": "^11.1.14",
"@nestjs/config": "^4.0.3",
"@nestjs/core": "^11.1.14",
"@nestjs/common": "^11.1.11",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.1.13",
"@nestjs/event-emitter": "^3.0.1",
"@nestjs/jwt": "11.0.0",
"@nestjs/mapped-types": "^2.1.0",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-fastify": "^11.1.14",
"@nestjs/platform-socket.io": "^11.1.14",
"@nestjs/platform-fastify": "^11.1.13",
"@nestjs/platform-socket.io": "^11.1.13",
"@nestjs/schedule": "^6.1.0",
"@nestjs/terminus": "^11.0.0",
"@nestjs/websockets": "^11.1.14",
"@nestjs/websockets": "^11.1.13",
"@node-saml/passport-saml": "^5.1.0",
"@react-email/components": "1.0.7",
"@react-email/render": "2.0.4",
@@ -50,7 +50,6 @@ export async function formatImportHtml(opts: {
}
notionFormatter($, $root);
xwikiFormatter($, $root);
defaultHtmlFormatter($, $root);
const backlinks = await rewriteInternalLinksToMentionHtml(
@@ -70,14 +69,6 @@ export async function formatImportHtml(opts: {
};
}
export function xwikiFormatter($: CheerioAPI, $root: Cheerio<any>) {
const $content = $root.find('#xwikicontent');
if ($content.length) {
$root.children().remove();
$root.append($content.contents());
}
}
export function defaultHtmlFormatter($: CheerioAPI, $root: Cheerio<any>) {
$root.find('a[href]').each((_, el) => {
const $el = $(el);
+4 -5
View File
@@ -79,13 +79,13 @@
"yjs": "^13.6.29"
},
"devDependencies": {
"@nx/js": "22.5.2",
"@nx/js": "22.5.0",
"@types/bytes": "^3.1.5",
"@types/turndown": "^5.0.6",
"@types/uuid": "^10.0.0",
"concurrently": "^9.2.1",
"nx": "22.5.2",
"tsx": "^4.21.0"
"concurrently": "^9.1.2",
"nx": "22.5.0",
"tsx": "^4.19.3"
},
"workspaces": {
"packages": [
@@ -113,7 +113,6 @@
"tmp": "0.2.5",
"lodash-es": "4.17.23",
"markdown-it": "14.1.1",
"ajv": "8.18.0",
"@tiptap/core": "3.17.1",
"@tiptap/pm": "3.17.1",
"@tiptap/starter-kit": "3.17.1",
@@ -1,5 +1,5 @@
import { Node, mergeAttributes, findParentNode } from "@tiptap/core";
import { TextSelection } from "@tiptap/pm/state";
import { TextSelection } from "prosemirror-state";
export interface ColumnOptions {
HTMLAttributes: Record<string, any>;
+3 -44
View File
@@ -1,7 +1,6 @@
import { Node, mergeAttributes, findParentNode } from "@tiptap/core";
import { Fragment, Node as PMNode } from "@tiptap/pm/model";
import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
import { Decoration, DecorationSet } from "@tiptap/pm/view";
import { Fragment, Node as PMNode } from "prosemirror-model";
import { TextSelection } from "prosemirror-state";
export type ColumnsLayout =
| "two_equal"
@@ -174,21 +173,7 @@ export const Columns = Node.create<ColumnsOptions>({
}
let mergedContent = columnsNode.child(count - 1).content;
for (let j = count; j < currentCount; j++) {
const col = columnsNode.child(j);
const nonEmpty: PMNode[] = [];
col.content.forEach((child) => {
if (
child.type.name !== "paragraph" ||
child.content.size > 0
) {
nonEmpty.push(child);
}
});
if (nonEmpty.length > 0) {
mergedContent = mergedContent.append(
Fragment.from(nonEmpty),
);
}
mergedContent = mergedContent.append(columnsNode.child(j).content);
}
newChildren.push(columnType.create(null, mergedContent));
}
@@ -199,9 +184,6 @@ export const Columns = Node.create<ColumnsOptions>({
Fragment.from(newChildren),
);
tr.replaceWith(parentPos, parentPos + columnsNode.nodeSize, newNode);
tr.setSelection(
TextSelection.near(tr.doc.resolve(parentPos + 1), 1),
);
return true;
},
@@ -211,27 +193,4 @@ export const Columns = Node.create<ColumnsOptions>({
commands.updateAttributes("columns", { layout }),
};
},
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey("columnsFocus"),
props: {
decorations: (state) => {
const parent = findParentNode(
(node) => node.type.name === "columns",
)(state.selection);
if (!parent) return DecorationSet.empty;
return DecorationSet.create(state.doc, [
Decoration.node(
parent.pos,
parent.pos + parent.node.nodeSize,
{ class: "has-focus" },
),
]);
},
},
}),
];
},
});
@@ -2,8 +2,8 @@ import TiptapHeading, {
HeadingOptions as TiptapHeadingOptions,
} from "@tiptap/extension-heading";
import { mergeAttributes } from "@tiptap/react";
import { Decoration, DecorationSet } from "@tiptap/pm/view";
import { Plugin } from "@tiptap/pm/state";
import { Decoration, DecorationSet } from "prosemirror-view";
import { Plugin } from "prosemirror-state";
import { copyToClipboard } from "../utils";
const copyIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><!-- Icon from Material Symbols Light by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M10.616 16.077H7.077q-1.692 0-2.884-1.192T3 12t1.193-2.885t2.884-1.193h3.539v1H7.077q-1.27 0-2.173.904Q4 10.731 4 12t.904 2.173t2.173.904h3.539zM8.5 12.5v-1h7v1zm4.885 3.577v-1h3.538q1.27 0 2.173-.904Q20 13.269 20 12t-.904-2.173t-2.173-.904h-3.538v-1h3.538q1.692 0 2.885 1.192T21 12t-1.193 2.885t-2.884 1.193z"/></svg>`;
+1429 -1397
View File
File diff suppressed because it is too large Load Diff