mirror of
https://github.com/docmost/docmost.git
synced 2026-08-30 02:25:01 +08:00
feat: base nodeview menu
This commit is contained in:
@@ -1,13 +1,14 @@
|
|||||||
import { NodeViewWrapper, NodeViewProps } from "@tiptap/react";
|
import { NodeViewWrapper, NodeViewProps } from "@tiptap/react";
|
||||||
import { Box, Text } from "@mantine/core";
|
import { ActionIcon, Box, Menu, Text } from "@mantine/core";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { BaseView } from "@/ee/base/components/base-view";
|
import { BaseView } from "@/ee/base/components/base-view";
|
||||||
import { BaseTableSkeleton } from "@/ee/base/components/base-table-skeleton";
|
import { BaseTableSkeleton } from "@/ee/base/components/base-table-skeleton";
|
||||||
import { useBaseQuery } from "@/ee/base/queries/base-query";
|
import { useBaseQuery } from "@/ee/base/queries/base-query";
|
||||||
import { pinOffsetWatcher } from "@docmost/editor-ext";
|
import { pinOffsetWatcher } from "@docmost/editor-ext";
|
||||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||||
import { Feature } from "@/ee/features";
|
import { Feature } from "@/ee/features";
|
||||||
import { IconTable } from "@tabler/icons-react";
|
import { IconDots, IconTable, IconX } from "@tabler/icons-react";
|
||||||
import { usePageQuery } from "@/features/page/queries/page-query";
|
import { usePageQuery } from "@/features/page/queries/page-query";
|
||||||
import classes from "./base-embed.module.css";
|
import classes from "./base-embed.module.css";
|
||||||
|
|
||||||
@@ -49,11 +50,13 @@ function applyExtension(wrapper: HTMLDivElement) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BaseEmbedView({ node, editor }: NodeViewProps) {
|
export function BaseEmbedView({ node, editor, deleteNode }: NodeViewProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const pageId = node.attrs.pageId as string | null;
|
const pageId = node.attrs.pageId as string | null;
|
||||||
const pendingKey = node.attrs.pendingKey as string | null;
|
const pendingKey = node.attrs.pendingKey as string | null;
|
||||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||||
const hasBases = useHasFeature(Feature.BASES);
|
const hasBases = useHasFeature(Feature.BASES);
|
||||||
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
// Suppress the query while the slash command awaits the server-assigned
|
// Suppress the query while the slash command awaits the server-assigned
|
||||||
// pageId; useBaseQuery would otherwise fire with an empty key.
|
// pageId; useBaseQuery would otherwise fire with an empty key.
|
||||||
const { data: base, isLoading, isError } = useBaseQuery(
|
const { data: base, isLoading, isError } = useBaseQuery(
|
||||||
@@ -95,6 +98,8 @@ export function BaseEmbedView({ node, editor }: NodeViewProps) {
|
|||||||
// mounts) is reserved only for the skeleton/loading/table states.
|
// mounts) is reserved only for the skeleton/loading/table states.
|
||||||
const isCompact = !pendingKey && (!pageId || isError);
|
const isCompact = !pendingKey && (!pageId || isError);
|
||||||
|
|
||||||
|
const showControls = editor.isEditable && !pendingKey;
|
||||||
|
|
||||||
let content: React.ReactNode;
|
let content: React.ReactNode;
|
||||||
if (pendingKey) {
|
if (pendingKey) {
|
||||||
// Slash command inserted the embed and is awaiting the server's
|
// Slash command inserted the embed and is awaiting the server's
|
||||||
@@ -133,7 +138,37 @@ export function BaseEmbedView({ node, editor }: NodeViewProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper className={classes.handleGutter}>
|
<NodeViewWrapper
|
||||||
|
className={classes.handleGutter}
|
||||||
|
data-menu-open={menuOpen ? "true" : "false"}
|
||||||
|
>
|
||||||
|
{showControls && (
|
||||||
|
<div
|
||||||
|
className={classes.controls}
|
||||||
|
contentEditable={false}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<Menu position="bottom-end" withinPortal onChange={setMenuOpen}>
|
||||||
|
<Menu.Target>
|
||||||
|
<ActionIcon
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
aria-label={t("Base options")}
|
||||||
|
>
|
||||||
|
<IconDots size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Menu.Target>
|
||||||
|
<Menu.Dropdown>
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<IconX size={14} />}
|
||||||
|
onClick={() => deleteNode()}
|
||||||
|
>
|
||||||
|
{t("Remove from page")}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div data-drag-preview hidden className={classes.dragPreview}>
|
<div data-drag-preview hidden className={classes.dragPreview}>
|
||||||
<IconTable size={16} />
|
<IconTable size={16} />
|
||||||
<span>{page?.title?.trim() || "Untitled base"}</span>
|
<span>{page?.title?.trim() || "Untitled base"}</span>
|
||||||
|
|||||||
@@ -1,8 +1,40 @@
|
|||||||
.handleGutter {
|
.handleGutter {
|
||||||
|
position: relative;
|
||||||
margin-left: -1.5rem;
|
margin-left: -1.5rem;
|
||||||
padding-left: 1.5rem;
|
padding-left: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 4px);
|
||||||
|
right: 0;
|
||||||
|
z-index: 20;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handleGutter:hover .controls,
|
||||||
|
.handleGutter[data-menu-open="true"] .controls {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
.controls {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 48em) {
|
@media (max-width: 48em) {
|
||||||
.handleGutter {
|
.handleGutter {
|
||||||
margin-left: -1rem;
|
margin-left: -1rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user