mirror of
https://github.com/docmost/docmost.git
synced 2026-05-21 01:04:39 +08:00
enhance columns
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
|
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus";
|
||||||
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
|
import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react";
|
||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useRef, useState } from "react";
|
||||||
import { Node as PMNode } from "prosemirror-model";
|
import { DOMSerializer, Node as PMNode } from "prosemirror-model";
|
||||||
import {
|
import {
|
||||||
EditorMenuProps,
|
EditorMenuProps,
|
||||||
ShouldShowProps,
|
ShouldShowProps,
|
||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
IconLayoutSidebar,
|
IconLayoutSidebar,
|
||||||
IconLayoutSidebarRight,
|
IconLayoutSidebarRight,
|
||||||
IconLayoutAlignCenter,
|
IconLayoutAlignCenter,
|
||||||
|
IconCopy,
|
||||||
|
IconTrash,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { isTextSelected } from "@docmost/editor-ext";
|
import { isTextSelected } from "@docmost/editor-ext";
|
||||||
import type { WidthMode, ColumnsLayout } from "@docmost/editor-ext";
|
import type { WidthMode, ColumnsLayout } from "@docmost/editor-ext";
|
||||||
@@ -67,6 +69,8 @@ function getPresetsForCount(count: number): LayoutPreset[] {
|
|||||||
export function ColumnsMenu({ editor }: EditorMenuProps) {
|
export function ColumnsMenu({ editor }: EditorMenuProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isCountOpen, setIsCountOpen] = useState(false);
|
const [isCountOpen, setIsCountOpen] = useState(false);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const copyTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
const nodesWithMenus = [
|
const nodesWithMenus = [
|
||||||
"callout",
|
"callout",
|
||||||
@@ -187,6 +191,68 @@ export function ColumnsMenu({ editor }: EditorMenuProps) {
|
|||||||
[editor],
|
[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 { state } = editor;
|
||||||
|
const parent = findParentNode(
|
||||||
|
(node: PMNode) => node.type.name === "columns",
|
||||||
|
)(state.selection);
|
||||||
|
if (!parent) return;
|
||||||
|
const { tr } = state;
|
||||||
|
tr.delete(parent.pos, parent.pos + parent.node.nodeSize);
|
||||||
|
editor.view.dispatch(tr);
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
const columnCount = editorState?.columnCount || 2;
|
const columnCount = editorState?.columnCount || 2;
|
||||||
const currentLayout = editorState?.layout || "two_equal";
|
const currentLayout = editorState?.layout || "two_equal";
|
||||||
const presets = getPresetsForCount(columnCount);
|
const presets = getPresetsForCount(columnCount);
|
||||||
@@ -259,6 +325,34 @@ export function ColumnsMenu({ editor }: EditorMenuProps) {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</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>
|
</div>
|
||||||
</BaseBubbleMenu>
|
</BaseBubbleMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
div[data-type="columns"] {
|
div[data-type="columns"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0.75rem 0;
|
margin: 0.75rem 0;
|
||||||
padding: 0.5em;
|
padding: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
div[data-type="columns"] > div[data-type="column"] {
|
div[data-type="columns"] > div[data-type="column"] {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
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"] {
|
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="columns"]:hover
|
||||||
|
> div[data-type="column"]
|
||||||
|
+ div[data-type="column"],
|
||||||
|
div[data-type="columns"].has-focus
|
||||||
> div[data-type="column"]
|
> div[data-type="column"]
|
||||||
+ div[data-type="column"] {
|
+ div[data-type="column"] {
|
||||||
border-left: 1px solid
|
border-left: 1px solid
|
||||||
@@ -66,7 +74,7 @@ div[data-type="columns"][data-layout="three_with_sidebars"]
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Stack columns vertically on small viewports */
|
/* Stack columns vertically on small viewports */
|
||||||
@media (max-width: 820px) {
|
@media (max-width: 680px) {
|
||||||
div[data-type="columns"] {
|
div[data-type="columns"] {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Node, mergeAttributes, findParentNode } from "@tiptap/core";
|
import { Node, mergeAttributes, findParentNode } from "@tiptap/core";
|
||||||
import { Fragment, Node as PMNode } from "prosemirror-model";
|
import { Fragment, Node as PMNode } from "prosemirror-model";
|
||||||
import { TextSelection } from "prosemirror-state";
|
import { Plugin, PluginKey, TextSelection } from "prosemirror-state";
|
||||||
|
import { Decoration, DecorationSet } from "prosemirror-view";
|
||||||
|
|
||||||
export type ColumnsLayout =
|
export type ColumnsLayout =
|
||||||
| "two_equal"
|
| "two_equal"
|
||||||
@@ -173,7 +174,21 @@ export const Columns = Node.create<ColumnsOptions>({
|
|||||||
}
|
}
|
||||||
let mergedContent = columnsNode.child(count - 1).content;
|
let mergedContent = columnsNode.child(count - 1).content;
|
||||||
for (let j = count; j < currentCount; j++) {
|
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));
|
newChildren.push(columnType.create(null, mergedContent));
|
||||||
}
|
}
|
||||||
@@ -184,6 +199,9 @@ export const Columns = Node.create<ColumnsOptions>({
|
|||||||
Fragment.from(newChildren),
|
Fragment.from(newChildren),
|
||||||
);
|
);
|
||||||
tr.replaceWith(parentPos, parentPos + columnsNode.nodeSize, newNode);
|
tr.replaceWith(parentPos, parentPos + columnsNode.nodeSize, newNode);
|
||||||
|
tr.setSelection(
|
||||||
|
TextSelection.near(tr.doc.resolve(parentPos + 1), 1),
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -193,4 +211,27 @@ export const Columns = Node.create<ColumnsOptions>({
|
|||||||
commands.updateAttributes("columns", { layout }),
|
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" },
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user