mirror of
https://github.com/docmost/docmost.git
synced 2026-05-08 07:13:06 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0df96d4bb | |||
| 22f33bab7c | |||
| e0a8521566 | |||
| b5803f42da | |||
| 5de1c8e3ed | |||
| ef87210b3d |
@@ -59,7 +59,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.16.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.62.1",
|
||||
"@tanstack/eslint-plugin-query": "^5.91.4",
|
||||
"@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 "prosemirror-model";
|
||||
import { Node as PMNode } from "@tiptap/pm/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, useState } from "react";
|
||||
import { Node as PMNode } from "prosemirror-model";
|
||||
import React, { useCallback, useRef, useState } from "react";
|
||||
import { DOMSerializer, Node as PMNode } from "@tiptap/pm/model";
|
||||
import {
|
||||
EditorMenuProps,
|
||||
ShouldShowProps,
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
IconLayoutSidebar,
|
||||
IconLayoutSidebarRight,
|
||||
IconLayoutAlignCenter,
|
||||
IconCopy,
|
||||
IconTrash,
|
||||
} from "@tabler/icons-react";
|
||||
import { isTextSelected } from "@docmost/editor-ext";
|
||||
import type { WidthMode, ColumnsLayout } from "@docmost/editor-ext";
|
||||
@@ -54,8 +56,7 @@ 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[] {
|
||||
@@ -67,6 +68,8 @@ 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",
|
||||
@@ -187,6 +190,65 @@ 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);
|
||||
@@ -259,6 +321,38 @@ 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 "prosemirror-model";
|
||||
import { Node as PMNode } from "@tiptap/pm/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 "prosemirror-model";
|
||||
import { Node as PMNode } from "@tiptap/pm/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 "prosemirror-model";
|
||||
import { Node as PMNode } from "@tiptap/pm/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 "prosemirror-model";
|
||||
import { Node as PMNode } from "@tiptap/pm/model";
|
||||
import {
|
||||
EditorMenuProps,
|
||||
ShouldShowProps,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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";
|
||||
@@ -113,10 +115,24 @@ export const mainExtensions = [
|
||||
color: "#70CFF8",
|
||||
},
|
||||
codeBlock: false,
|
||||
code: {
|
||||
HTMLAttributes: {
|
||||
spellcheck: 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,
|
||||
}),
|
||||
];
|
||||
},
|
||||
}),
|
||||
SharedStorage,
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
div[data-type="columns"] {
|
||||
display: flex;
|
||||
margin: 0.75rem 0;
|
||||
padding: 0.5em;
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
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"] {
|
||||
@@ -16,6 +21,9 @@ 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
|
||||
@@ -66,7 +74,7 @@ div[data-type="columns"][data-layout="three_with_sidebars"]
|
||||
}
|
||||
|
||||
/* Stack columns vertically on small viewports */
|
||||
@media (max-width: 820px) {
|
||||
@media (max-width: 680px) {
|
||||
div[data-type="columns"] {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -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.982.0",
|
||||
"@aws-sdk/lib-storage": "3.982.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.982.0",
|
||||
"@aws-sdk/client-s3": "3.998.0",
|
||||
"@aws-sdk/lib-storage": "3.998.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.998.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.11",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.1.13",
|
||||
"@nestjs/common": "^11.1.14",
|
||||
"@nestjs/config": "^4.0.3",
|
||||
"@nestjs/core": "^11.1.14",
|
||||
"@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.13",
|
||||
"@nestjs/platform-socket.io": "^11.1.13",
|
||||
"@nestjs/platform-fastify": "^11.1.14",
|
||||
"@nestjs/platform-socket.io": "^11.1.14",
|
||||
"@nestjs/schedule": "^6.1.0",
|
||||
"@nestjs/terminus": "^11.0.0",
|
||||
"@nestjs/websockets": "^11.1.13",
|
||||
"@nestjs/websockets": "^11.1.14",
|
||||
"@node-saml/passport-saml": "^5.1.0",
|
||||
"@react-email/components": "1.0.7",
|
||||
"@react-email/render": "2.0.4",
|
||||
|
||||
@@ -50,6 +50,7 @@ export async function formatImportHtml(opts: {
|
||||
}
|
||||
|
||||
notionFormatter($, $root);
|
||||
xwikiFormatter($, $root);
|
||||
defaultHtmlFormatter($, $root);
|
||||
|
||||
const backlinks = await rewriteInternalLinksToMentionHtml(
|
||||
@@ -69,6 +70,14 @@ 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);
|
||||
|
||||
+5
-4
@@ -79,13 +79,13 @@
|
||||
"yjs": "^13.6.29"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nx/js": "22.5.0",
|
||||
"@nx/js": "22.5.2",
|
||||
"@types/bytes": "^3.1.5",
|
||||
"@types/turndown": "^5.0.6",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"nx": "22.5.0",
|
||||
"tsx": "^4.19.3"
|
||||
"concurrently": "^9.2.1",
|
||||
"nx": "22.5.2",
|
||||
"tsx": "^4.21.0"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
@@ -113,6 +113,7 @@
|
||||
"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 "prosemirror-state";
|
||||
import { TextSelection } from "@tiptap/pm/state";
|
||||
|
||||
export interface ColumnOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Node, mergeAttributes, findParentNode } from "@tiptap/core";
|
||||
import { Fragment, Node as PMNode } from "prosemirror-model";
|
||||
import { TextSelection } from "prosemirror-state";
|
||||
import { Fragment, Node as PMNode } from "@tiptap/pm/model";
|
||||
import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
|
||||
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
||||
|
||||
export type ColumnsLayout =
|
||||
| "two_equal"
|
||||
@@ -173,7 +174,21 @@ export const Columns = Node.create<ColumnsOptions>({
|
||||
}
|
||||
let mergedContent = columnsNode.child(count - 1).content;
|
||||
for (let j = count; j < currentCount; j++) {
|
||||
mergedContent = mergedContent.append(columnsNode.child(j).content);
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
newChildren.push(columnType.create(null, mergedContent));
|
||||
}
|
||||
@@ -184,6 +199,9 @@ 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;
|
||||
},
|
||||
|
||||
@@ -193,4 +211,27 @@ 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 "prosemirror-view";
|
||||
import { Plugin } from "prosemirror-state";
|
||||
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
||||
import { Plugin } from "@tiptap/pm/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>`;
|
||||
|
||||
Generated
+1397
-1429
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user