mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
feat: Inline editor menu (#12611)
* wip * Mobile support * Address review feedback on inline menu - Mark selection-restore transaction as not added to history - Only open desktop inline menu when an anchor is available Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -108,6 +108,9 @@ export const MenuExternalLink = styled.a`
|
||||
|
||||
export const MenuSubTrigger = styled.div<BaseMenuItemProps>`
|
||||
${BaseMenuItemCSS}
|
||||
// Reserve space for the absolutely-positioned disclosure arrow so long
|
||||
// labels truncate before it rather than overlapping.
|
||||
padding-inline-end: 32px;
|
||||
`;
|
||||
|
||||
export const MenuSeparator = styled.hr`
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import * as React from "react";
|
||||
import { RemoveScroll } from "react-remove-scroll";
|
||||
import styled from "styled-components";
|
||||
import EventBoundary from "@shared/components/EventBoundary";
|
||||
import { collapseSelection } from "@shared/editor/commands/collapseSelection";
|
||||
import type { MenuItem } from "@shared/editor/types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Scrollable from "~/components/Scrollable";
|
||||
import { toMenuItems, toMobileMenuItems } from "~/components/Menu/transformer";
|
||||
import * as Components from "~/components/primitives/components/Menu";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerTitle,
|
||||
} from "~/components/primitives/Drawer";
|
||||
import { MenuProvider } from "~/components/primitives/Menu/MenuContext";
|
||||
import type { MenuItem as TMenuItem, MenuItemWithChildren } from "~/types";
|
||||
import useMobile from "~/hooks/useMobile";
|
||||
import { mapMenuItems } from "../menus/mapMenuItems";
|
||||
import { useEditor } from "./EditorContext";
|
||||
import { useInlineMenuAnchor } from "./useInlineMenuAnchor";
|
||||
|
||||
type Props = {
|
||||
items: MenuItem[];
|
||||
/** Whether the document is right-to-left. */
|
||||
rtl: boolean;
|
||||
};
|
||||
|
||||
// The virtual anchor is an invisible zero-size element; the hook positions it
|
||||
// over the selection and Radix anchors the menu to it.
|
||||
const anchorStyle: React.CSSProperties = {
|
||||
position: "fixed",
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a selection-toolbar menu inline — a vertical menu anchored to the
|
||||
* selection with no trigger button — by holding a Radix dropdown `open`
|
||||
* against a virtual anchor positioned over the selection. Radix provides the
|
||||
* positioning, collision handling, submenus, and keyboard navigation. Page
|
||||
* scroll is locked while open (via RemoveScroll, as Radix does for modal
|
||||
* menus) without enabling Radix's modal mode, which conflicts with the menu
|
||||
* being opened by an editor selection rather than a trigger.
|
||||
*/
|
||||
const InlineMenu: React.FC<Props> = ({ items, rtl }) => {
|
||||
const { t } = useTranslation();
|
||||
const { commands, view } = useEditor();
|
||||
const { state } = view;
|
||||
const isMobile = useMobile();
|
||||
const {
|
||||
ref: anchorRef,
|
||||
key: anchorKey,
|
||||
side,
|
||||
align,
|
||||
sideOffset,
|
||||
} = useInlineMenuAnchor(rtl);
|
||||
|
||||
const mapped = React.useMemo(
|
||||
() => mapMenuItems(items, commands, view, state),
|
||||
[items, commands, view, state]
|
||||
);
|
||||
|
||||
const preventFocus = React.useCallback((ev: Event) => {
|
||||
ev.preventDefault();
|
||||
}, []);
|
||||
|
||||
// Dismiss the menu by collapsing the selection so the toolbar stops matching.
|
||||
const handleDismiss = React.useCallback(() => {
|
||||
collapseSelection()(view.state, view.dispatch);
|
||||
}, [view]);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<InlineMenuDrawer
|
||||
items={mapped}
|
||||
ariaLabel={t("Options")}
|
||||
onDismiss={handleDismiss}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuProvider variant="dropdown">
|
||||
<DropdownMenuPrimitive.Root
|
||||
key={anchorKey}
|
||||
open={!!anchorKey}
|
||||
modal={false}
|
||||
>
|
||||
<DropdownMenuPrimitive.Trigger asChild>
|
||||
<div ref={anchorRef} aria-hidden style={anchorStyle} />
|
||||
</DropdownMenuPrimitive.Trigger>
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
side={side}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
collisionPadding={6}
|
||||
aria-label={t("Options")}
|
||||
onCloseAutoFocus={preventFocus}
|
||||
onInteractOutside={handleDismiss}
|
||||
onEscapeKeyDown={handleDismiss}
|
||||
asChild
|
||||
>
|
||||
<RemoveScroll as={Slot} allowPinchZoom>
|
||||
<Components.MenuContent
|
||||
maxHeightVar="--radix-dropdown-menu-content-available-height"
|
||||
transformOriginVar="--radix-dropdown-menu-content-transform-origin"
|
||||
hiddenScrollbars
|
||||
>
|
||||
<EventBoundary>{toMenuItems(mapped)}</EventBoundary>
|
||||
</Components.MenuContent>
|
||||
</RemoveScroll>
|
||||
</DropdownMenuPrimitive.Content>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
</DropdownMenuPrimitive.Root>
|
||||
</MenuProvider>
|
||||
);
|
||||
};
|
||||
|
||||
// Time for the drawer's close animation to play before the selection is
|
||||
// collapsed (which unmounts the menu).
|
||||
const DRAWER_CLOSE_MS = 500;
|
||||
|
||||
type InlineMenuDrawerProps = {
|
||||
items: TMenuItem[];
|
||||
ariaLabel: string;
|
||||
/** Collapse the selection so the toolbar stops rendering the menu. */
|
||||
onDismiss: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mobile presentation of the inline menu: a bottom drawer with submenu drill-in,
|
||||
* matching the other menus. The menu is held open while the selection matches;
|
||||
* closing animates the drawer out before collapsing the selection.
|
||||
*/
|
||||
function InlineMenuDrawer({
|
||||
items,
|
||||
ariaLabel,
|
||||
onDismiss,
|
||||
}: InlineMenuDrawerProps) {
|
||||
const [open, setOpen] = React.useState(true);
|
||||
const [submenuName, setSubmenuName] = React.useState<string>();
|
||||
|
||||
const close = React.useCallback(() => {
|
||||
setOpen(false);
|
||||
setTimeout(() => {
|
||||
setSubmenuName(undefined);
|
||||
onDismiss();
|
||||
}, DRAWER_CLOSE_MS);
|
||||
}, [onDismiss]);
|
||||
|
||||
const handleOpenChange = React.useCallback(
|
||||
(isOpen: boolean) => {
|
||||
if (!isOpen) {
|
||||
close();
|
||||
}
|
||||
},
|
||||
[close]
|
||||
);
|
||||
|
||||
const menuItems = React.useMemo(() => {
|
||||
if (!items.length || !submenuName) {
|
||||
return items;
|
||||
}
|
||||
const submenu = items.find(
|
||||
(item) => item.type === "submenu" && item.title === submenuName
|
||||
) as MenuItemWithChildren | undefined;
|
||||
return submenu?.items ?? items;
|
||||
}, [items, submenuName]);
|
||||
|
||||
const content = toMobileMenuItems(menuItems, close, setSubmenuName);
|
||||
|
||||
return (
|
||||
<Drawer open={open} onOpenChange={handleOpenChange}>
|
||||
<DrawerContent aria-label={ariaLabel} aria-describedby={undefined}>
|
||||
<DrawerTitle hidden>{ariaLabel}</DrawerTitle>
|
||||
<StyledScrollable hiddenScrollbars>{content}</StyledScrollable>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledScrollable = styled(Scrollable)`
|
||||
max-height: 75vh;
|
||||
`;
|
||||
|
||||
export default InlineMenu;
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "@shared/editor/queries/getMarkRange";
|
||||
import { isInCode } from "@shared/editor/queries/isInCode";
|
||||
import { isInNotice } from "@shared/editor/queries/isInNotice";
|
||||
import type { MenuItem } from "@shared/editor/types";
|
||||
import { MenuType, type MenuItem } from "@shared/editor/types";
|
||||
import useBoolean from "~/hooks/useBoolean";
|
||||
import useEventListener from "~/hooks/useEventListener";
|
||||
import useMobile from "~/hooks/useMobile";
|
||||
@@ -24,6 +24,7 @@ import { MediaLinkEditor } from "./MediaLinkEditor";
|
||||
import FloatingToolbar from "./FloatingToolbar";
|
||||
import LinkEditor from "./LinkEditor";
|
||||
import ToolbarMenu from "./ToolbarMenu";
|
||||
import InlineMenu from "./InlineMenu";
|
||||
import { isModKey } from "@shared/utils/keyboard";
|
||||
|
||||
type Props = {
|
||||
@@ -264,6 +265,16 @@ export function SelectionToolbar(props: Props) {
|
||||
setActiveToolbar(null);
|
||||
};
|
||||
|
||||
// Inline menus render as a vertical menu anchored to the selection rather
|
||||
// than as a horizontal toolbar with trigger buttons.
|
||||
if (
|
||||
matched?.variant === MenuType.inline &&
|
||||
activeToolbar === Toolbar.Menu &&
|
||||
items.length
|
||||
) {
|
||||
return <InlineMenu items={items} rtl={rtl} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<FloatingToolbar
|
||||
align={align}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { MenuItem } from "@shared/editor/types";
|
||||
import { hideScrollbars, s } from "@shared/styles";
|
||||
import { TooltipProvider } from "~/components/TooltipContext";
|
||||
import type { MenuItem as TMenuItem } from "~/types";
|
||||
import { mapMenuItems } from "../menus/mapMenuItems";
|
||||
import { useEditor } from "./EditorContext";
|
||||
import { MediaDimension } from "./MediaDimension";
|
||||
import ToolbarButton from "./ToolbarButton";
|
||||
@@ -49,69 +50,11 @@ function ToolbarDropdown(props: ToolbarDropdownProps) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const handleClick = (menuItem: MenuItem) => () => {
|
||||
if (!menuItem.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (commands[menuItem.name]) {
|
||||
closeHistory(view);
|
||||
commands[menuItem.name](
|
||||
typeof menuItem.attrs === "function"
|
||||
? menuItem.attrs(state)
|
||||
: menuItem.attrs
|
||||
);
|
||||
closeHistory(view);
|
||||
} else if (menuItem.onClick) {
|
||||
menuItem.onClick();
|
||||
}
|
||||
};
|
||||
|
||||
const resolveChildren = (
|
||||
children: MenuItem[] | (() => MenuItem[]) | undefined
|
||||
): MenuItem[] | undefined =>
|
||||
typeof children === "function" ? children() : children;
|
||||
|
||||
const mapChildren = (children: MenuItem[]): TMenuItem[] =>
|
||||
children.map((child) => {
|
||||
if (child.name === "separator") {
|
||||
return { type: "separator", visible: child.visible };
|
||||
}
|
||||
if ("content" in child) {
|
||||
return {
|
||||
type: "custom",
|
||||
visible: child.visible,
|
||||
content: child.content,
|
||||
};
|
||||
}
|
||||
const resolvedChildren = resolveChildren(child.children);
|
||||
if (resolvedChildren) {
|
||||
const childWithPreventClose = resolvedChildren.find(
|
||||
(c) => "preventCloseCondition" in c
|
||||
);
|
||||
return {
|
||||
type: "submenu",
|
||||
title: child.label,
|
||||
icon: child.icon,
|
||||
visible: child.visible,
|
||||
preventCloseCondition: childWithPreventClose?.preventCloseCondition,
|
||||
items: mapChildren(resolvedChildren),
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: "button",
|
||||
title: child.label,
|
||||
icon: child.icon,
|
||||
dangerous: child.dangerous,
|
||||
visible: child.visible,
|
||||
selected:
|
||||
child.active !== undefined ? child.active(state) : undefined,
|
||||
onClick: handleClick(child),
|
||||
};
|
||||
});
|
||||
|
||||
const resolvedItemChildren = resolveChildren(item.children);
|
||||
return resolvedItemChildren ? mapChildren(resolvedItemChildren) : [];
|
||||
const resolvedItemChildren =
|
||||
typeof item.children === "function" ? item.children() : item.children;
|
||||
return resolvedItemChildren
|
||||
? mapMenuItems(resolvedItemChildren, commands, view, state)
|
||||
: [];
|
||||
}, [isOpen, commands]);
|
||||
|
||||
const handleCloseAutoFocus = useCallback((ev: Event) => {
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import { selectedRect } from "prosemirror-tables";
|
||||
import * as React from "react";
|
||||
import type { EditorView } from "prosemirror-view";
|
||||
import { ColumnSelection } from "@shared/editor/selection/ColumnSelection";
|
||||
import { RowSelection } from "@shared/editor/selection/RowSelection";
|
||||
import { isTableSelected } from "@shared/editor/queries/table";
|
||||
import { useEditor } from "./EditorContext";
|
||||
|
||||
type Side = "top" | "bottom" | "left" | "right";
|
||||
type Align = "start" | "center" | "end";
|
||||
|
||||
const DEFAULT_SIDE_OFFSET = 4;
|
||||
|
||||
// Column and row menus open next to a grip handle. The grip is modelled as a
|
||||
// strip just outside the cell edge so the two distances are independent:
|
||||
// opening to the outside clears the grip (strip thickness + offset), while
|
||||
// flipping across sits only a small gap (offset) away.
|
||||
const OUTSIDE_CLEARANCE = 20;
|
||||
const FLIP_GAP = 0;
|
||||
const GRIP_INSET = OUTSIDE_CLEARANCE - FLIP_GAP;
|
||||
const GRIP_SIDE_OFFSET = FLIP_GAP;
|
||||
|
||||
type Anchor = {
|
||||
/** Viewport rect to anchor the menu to. */
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
/** Which side of the anchor the menu opens towards. */
|
||||
side: Side;
|
||||
/** How the menu aligns along the anchor edge. */
|
||||
align: Align;
|
||||
/** Distance in pixels between the anchor and the menu. */
|
||||
sideOffset: number;
|
||||
/** Stable identifier for the anchored target, changes when it moves. */
|
||||
key: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the rect and placement to anchor an inline selection menu to, based
|
||||
* on the current table/column/row selection. The menu opens to the "outside"
|
||||
* of the table (above a column, beside a row) to cover the least content, and
|
||||
* is centered on the anchor for minimal pointer movement. Returns null when
|
||||
* there is no supported selection.
|
||||
*
|
||||
* @param view - the editor view.
|
||||
* @param rtl - whether the document is right-to-left.
|
||||
* @returns the anchor, or null.
|
||||
*/
|
||||
function getAnchor(view: EditorView, rtl: boolean): Anchor | null {
|
||||
const { state } = view;
|
||||
const { selection } = state;
|
||||
|
||||
if (isTableSelected(state)) {
|
||||
const rect = selectedRect(state);
|
||||
const bounds = (
|
||||
view.domAtPos(rect.tableStart).node as HTMLElement
|
||||
).getBoundingClientRect();
|
||||
// A horizontal line at the table's top edge so it stays near the top
|
||||
// whether the menu opens above or flips below.
|
||||
return {
|
||||
top: bounds.top,
|
||||
left: bounds.left,
|
||||
width: bounds.width,
|
||||
height: 0,
|
||||
side: "top",
|
||||
align: "start",
|
||||
sideOffset: DEFAULT_SIDE_OFFSET,
|
||||
key: `table-${rect.tableStart}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (selection instanceof ColumnSelection && selection.isColSelection()) {
|
||||
const rect = selectedRect(state);
|
||||
const cell = (
|
||||
view.domAtPos(rect.tableStart).node as HTMLElement
|
||||
).querySelector(`tr > *:nth-child(${rect.left + 1})`);
|
||||
if (cell instanceof HTMLElement) {
|
||||
const bounds = cell.getBoundingClientRect();
|
||||
// A strip just above the column's top edge (the grip), spanning the
|
||||
// column width so the menu centers on the column.
|
||||
return {
|
||||
top: bounds.top - GRIP_INSET,
|
||||
left: bounds.left,
|
||||
width: bounds.width,
|
||||
height: GRIP_INSET,
|
||||
side: "top",
|
||||
align: "center",
|
||||
sideOffset: GRIP_SIDE_OFFSET,
|
||||
key: `col-${rect.tableStart}-${rect.left}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (selection instanceof RowSelection && selection.isRowSelection()) {
|
||||
const rect = selectedRect(state);
|
||||
const cell = (
|
||||
view.domAtPos(rect.tableStart).node as HTMLElement
|
||||
).querySelector(`tr:nth-child(${rect.top + 1}) > *`);
|
||||
if (cell instanceof HTMLElement) {
|
||||
const bounds = cell.getBoundingClientRect();
|
||||
// A strip just outside the row's grip edge (left, or right in RTL),
|
||||
// spanning the row height so the menu centers on the row.
|
||||
return {
|
||||
top: bounds.top,
|
||||
left: rtl ? bounds.right : bounds.left - GRIP_INSET,
|
||||
width: GRIP_INSET,
|
||||
height: bounds.height,
|
||||
side: rtl ? "right" : "left",
|
||||
align: "center",
|
||||
sideOffset: GRIP_SIDE_OFFSET,
|
||||
key: `row-${rect.tableStart}-${rect.top}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Positions an invisible virtual anchor element over the current table, column,
|
||||
* or row selection so a Radix dropdown can anchor an inline menu to it. The
|
||||
* returned `key` changes when the anchored target changes; spread it onto the
|
||||
* menu root so Radix repositions for a new target.
|
||||
*
|
||||
* @param rtl - whether the document is right-to-left.
|
||||
* @returns the anchor ref to attach to the virtual trigger, the target key, and
|
||||
* the side/align the menu should open with.
|
||||
*/
|
||||
export function useInlineMenuAnchor(rtl: boolean) {
|
||||
const { view } = useEditor();
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const anchor = getAnchor(view, rtl);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
const element = ref.current;
|
||||
if (element && anchor) {
|
||||
element.style.top = `${anchor.top}px`;
|
||||
element.style.left = `${anchor.left}px`;
|
||||
element.style.width = `${anchor.width}px`;
|
||||
element.style.height = `${anchor.height}px`;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
ref,
|
||||
key: anchor?.key,
|
||||
side: anchor?.side ?? "top",
|
||||
align: anchor?.align ?? "start",
|
||||
sideOffset: anchor?.sideOffset ?? DEFAULT_SIDE_OFFSET,
|
||||
};
|
||||
}
|
||||
@@ -7,7 +7,10 @@ import Extension from "@shared/editor/lib/Extension";
|
||||
import { isInNotice } from "@shared/editor/queries/isInNotice";
|
||||
import { isMarkActive } from "@shared/editor/queries/isMarkActive";
|
||||
import { isNodeActive } from "@shared/editor/queries/isNodeActive";
|
||||
import type { SelectionToolbarMenuDescriptor } from "@shared/editor/types";
|
||||
import {
|
||||
MenuType,
|
||||
type SelectionToolbarMenuDescriptor,
|
||||
} from "@shared/editor/types";
|
||||
import { SelectionToolbar } from "../components/SelectionToolbar";
|
||||
import getAttachmentMenuItems from "../menus/attachment";
|
||||
import getCodeMenuItems from "../menus/code";
|
||||
@@ -62,16 +65,19 @@ export default class SelectionToolbarExtension extends Extension {
|
||||
},
|
||||
{
|
||||
priority: 90,
|
||||
variant: MenuType.inline,
|
||||
matches: (ctx) => ctx.isTableSelected,
|
||||
getItems: (ctx) => getTableMenuItems(ctx),
|
||||
},
|
||||
{
|
||||
priority: 85,
|
||||
variant: MenuType.inline,
|
||||
matches: (ctx) => ctx.colIndex !== undefined,
|
||||
getItems: (ctx) => getTableColMenuItems(ctx),
|
||||
},
|
||||
{
|
||||
priority: 80,
|
||||
variant: MenuType.inline,
|
||||
matches: (ctx) => ctx.rowIndex !== undefined,
|
||||
getItems: (ctx) => getTableRowMenuItems(ctx),
|
||||
},
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { EditorState } from "prosemirror-state";
|
||||
import type { EditorView } from "prosemirror-view";
|
||||
import { closeHistory } from "@shared/editor/lib/closeHistory";
|
||||
import type { CommandFactory } from "@shared/editor/lib/Extension";
|
||||
import type { MenuItem } from "@shared/editor/types";
|
||||
import type { MenuItem as TMenuItem } from "~/types";
|
||||
|
||||
const resolveChildren = (
|
||||
children: MenuItem[] | (() => MenuItem[]) | undefined
|
||||
): MenuItem[] | undefined =>
|
||||
typeof children === "function" ? children() : children;
|
||||
|
||||
/**
|
||||
* Maps editor `MenuItem`s into the primitive `MenuItem`s consumed by
|
||||
* `toMenuItems`. Shared by the toolbar dropdown and the inline menu so menu
|
||||
* presentation stays consistent. Resolves nested children into submenus and
|
||||
* binds each leaf to its editor command (or `onClick`).
|
||||
*
|
||||
* @param items - the editor menu items to map.
|
||||
* @param commands - the editor command registry.
|
||||
* @param view - the editor view, used to checkpoint history around commands.
|
||||
* @param state - the editor state, used to resolve dynamic attrs and active state.
|
||||
* @returns the mapped primitive menu items.
|
||||
*/
|
||||
export function mapMenuItems(
|
||||
items: MenuItem[],
|
||||
commands: Record<string, CommandFactory>,
|
||||
view: EditorView,
|
||||
state: EditorState
|
||||
): TMenuItem[] {
|
||||
const handleClick = (item: MenuItem) => () => {
|
||||
if (!item.name) {
|
||||
return;
|
||||
}
|
||||
if (commands[item.name]) {
|
||||
closeHistory(view);
|
||||
commands[item.name](
|
||||
typeof item.attrs === "function" ? item.attrs(state) : item.attrs
|
||||
);
|
||||
closeHistory(view);
|
||||
} else if (item.onClick) {
|
||||
item.onClick();
|
||||
}
|
||||
};
|
||||
|
||||
return items.map((item) => {
|
||||
if (item.name === "separator") {
|
||||
return { type: "separator", visible: item.visible };
|
||||
}
|
||||
|
||||
if ("content" in item) {
|
||||
return { type: "custom", visible: item.visible, content: item.content };
|
||||
}
|
||||
|
||||
const resolvedChildren = resolveChildren(item.children);
|
||||
if (resolvedChildren) {
|
||||
const childWithPreventClose = resolvedChildren.find(
|
||||
(child) => "preventCloseCondition" in child
|
||||
);
|
||||
return {
|
||||
type: "submenu",
|
||||
title: item.label,
|
||||
icon: item.icon,
|
||||
visible: item.visible,
|
||||
preventCloseCondition: childWithPreventClose?.preventCloseCondition,
|
||||
items: mapMenuItems(resolvedChildren, commands, view, state),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: "button",
|
||||
title: item.label,
|
||||
icon: item.icon,
|
||||
dangerous: item.dangerous,
|
||||
visible: item.visible,
|
||||
selected: item.active !== undefined ? item.active(state) : undefined,
|
||||
onClick: handleClick(item),
|
||||
};
|
||||
});
|
||||
}
|
||||
+11
-14
@@ -15,9 +15,7 @@ import { TableLayout } from "@shared/editor/types";
|
||||
* @param ctx - the current selection context.
|
||||
* @returns an array of menu items.
|
||||
*/
|
||||
export default function tableMenuItems(
|
||||
ctx: SelectionContext
|
||||
): MenuItem[] {
|
||||
export default function tableMenuItems(ctx: SelectionContext): MenuItem[] {
|
||||
if (ctx.readOnly) {
|
||||
return [];
|
||||
}
|
||||
@@ -30,33 +28,32 @@ export default function tableMenuItems(
|
||||
return [
|
||||
{
|
||||
name: "setTableAttr",
|
||||
tooltip: isFullWidth ? t("Default width") : t("Full width"),
|
||||
label: isFullWidth ? t("Default width") : t("Full width"),
|
||||
icon: <AlignFullWidthIcon />,
|
||||
attrs: isFullWidth ? { layout: null } : { layout: TableLayout.fullWidth },
|
||||
active: () => isFullWidth,
|
||||
},
|
||||
{
|
||||
name: "distributeColumns",
|
||||
tooltip: t("Distribute columns"),
|
||||
label: t("Distribute columns"),
|
||||
icon: <TableColumnsDistributeIcon />,
|
||||
},
|
||||
{
|
||||
name: "separator",
|
||||
},
|
||||
{
|
||||
name: "deleteTable",
|
||||
tooltip: t("Delete table"),
|
||||
icon: <TrashIcon />,
|
||||
name: "exportTable",
|
||||
label: t("Export as CSV"),
|
||||
attrs: { format: "csv", fileName: `${window.document.title}.csv` },
|
||||
icon: <DownloadIcon />,
|
||||
},
|
||||
{
|
||||
name: "separator",
|
||||
},
|
||||
{
|
||||
name: "exportTable",
|
||||
tooltip: t("Export as CSV"),
|
||||
label: "CSV",
|
||||
attrs: { format: "csv", fileName: `${window.document.title}.csv` },
|
||||
icon: <DownloadIcon />,
|
||||
name: "deleteTable",
|
||||
label: t("Delete table"),
|
||||
dangerous: true,
|
||||
icon: <TrashIcon />,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
+122
-118
@@ -5,7 +5,6 @@ import {
|
||||
AlignCenterIcon,
|
||||
InsertLeftIcon,
|
||||
InsertRightIcon,
|
||||
MoreIcon,
|
||||
PaletteIcon,
|
||||
TableHeaderColumnIcon,
|
||||
TableMergeCellsIcon,
|
||||
@@ -24,7 +23,11 @@ import {
|
||||
tableHasRowspan,
|
||||
} from "@shared/editor/queries/table";
|
||||
import { t } from "i18next";
|
||||
import type { MenuItem, NodeAttrMark, SelectionContext } from "@shared/editor/types";
|
||||
import type {
|
||||
MenuItem,
|
||||
NodeAttrMark,
|
||||
SelectionContext,
|
||||
} from "@shared/editor/types";
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "~/components/Icons/ArrowIcon";
|
||||
import CircleIcon from "~/components/Icons/CircleIcon";
|
||||
import CellBackgroundColorPicker from "../components/CellBackgroundColorPicker";
|
||||
@@ -65,9 +68,7 @@ function getColumnColors(state: EditorState, colIndex: number): Set<string> {
|
||||
* @param ctx - the current selection context.
|
||||
* @returns an array of menu items.
|
||||
*/
|
||||
export default function tableColMenuItems(
|
||||
ctx: SelectionContext
|
||||
): MenuItem[] {
|
||||
export default function tableColMenuItems(ctx: SelectionContext): MenuItem[] {
|
||||
if (ctx.readOnly) {
|
||||
return [];
|
||||
}
|
||||
@@ -94,60 +95,65 @@ export default function tableColMenuItems(
|
||||
|
||||
return [
|
||||
{
|
||||
name: "setColumnAttr",
|
||||
tooltip: t("Align left"),
|
||||
icon: <AlignLeftIcon />,
|
||||
attrs: { index, alignment: "left" },
|
||||
active: isNodeActive(schema.nodes.th, {
|
||||
colspan: 1,
|
||||
rowspan: 1,
|
||||
alignment: "left",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "setColumnAttr",
|
||||
tooltip: t("Align center"),
|
||||
label: t("Align"),
|
||||
icon: <AlignCenterIcon />,
|
||||
attrs: { index, alignment: "center" },
|
||||
active: isNodeActive(schema.nodes.th, {
|
||||
colspan: 1,
|
||||
rowspan: 1,
|
||||
alignment: "center",
|
||||
}),
|
||||
children: [
|
||||
{
|
||||
name: "setColumnAttr",
|
||||
label: t("Align left"),
|
||||
icon: <AlignLeftIcon />,
|
||||
attrs: { index, alignment: "left" },
|
||||
active: isNodeActive(schema.nodes.th, {
|
||||
colspan: 1,
|
||||
rowspan: 1,
|
||||
alignment: "left",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "setColumnAttr",
|
||||
label: t("Align center"),
|
||||
icon: <AlignCenterIcon />,
|
||||
attrs: { index, alignment: "center" },
|
||||
active: isNodeActive(schema.nodes.th, {
|
||||
colspan: 1,
|
||||
rowspan: 1,
|
||||
alignment: "center",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "setColumnAttr",
|
||||
label: t("Align right"),
|
||||
icon: <AlignRightIcon />,
|
||||
attrs: { index, alignment: "right" },
|
||||
active: isNodeActive(schema.nodes.th, {
|
||||
colspan: 1,
|
||||
rowspan: 1,
|
||||
alignment: "right",
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "setColumnAttr",
|
||||
tooltip: t("Align right"),
|
||||
icon: <AlignRightIcon />,
|
||||
attrs: { index, alignment: "right" },
|
||||
active: isNodeActive(schema.nodes.th, {
|
||||
colspan: 1,
|
||||
rowspan: 1,
|
||||
alignment: "right",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "separator",
|
||||
},
|
||||
{
|
||||
name: "sortTable",
|
||||
tooltip: t("Sort ascending"),
|
||||
attrs: { index, direction: "asc" },
|
||||
label: t("Sort"),
|
||||
icon: <SortAscendingIcon />,
|
||||
disabled: tableHasRowspan(state),
|
||||
children: [
|
||||
{
|
||||
name: "sortTable",
|
||||
label: t("Sort ascending"),
|
||||
attrs: { index, direction: "asc" },
|
||||
icon: <SortAscendingIcon />,
|
||||
},
|
||||
{
|
||||
name: "sortTable",
|
||||
label: t("Sort descending"),
|
||||
attrs: { index, direction: "desc" },
|
||||
icon: <SortDescendingIcon />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "sortTable",
|
||||
tooltip: t("Sort descending"),
|
||||
attrs: { index, direction: "desc" },
|
||||
icon: <SortDescendingIcon />,
|
||||
disabled: tableHasRowspan(state),
|
||||
},
|
||||
{
|
||||
name: "separator",
|
||||
},
|
||||
{
|
||||
tooltip: t("Background color"),
|
||||
label: t("Background"),
|
||||
icon:
|
||||
colColors.size > 1 ? (
|
||||
<CircleIcon color="rainbow" />
|
||||
@@ -161,7 +167,7 @@ export default function tableColMenuItems(
|
||||
{
|
||||
name: "toggleColumnBackgroundAndCollapseSelection",
|
||||
label: t("None"),
|
||||
icon: <DottedCircleIcon retainColor color="transparent" />,
|
||||
icon: <DottedCircleIcon color="transparent" />,
|
||||
active: () => (hasBackground ? false : true),
|
||||
attrs: { color: null },
|
||||
},
|
||||
@@ -205,71 +211,69 @@ export default function tableColMenuItems(
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <MoreIcon />,
|
||||
children: [
|
||||
{
|
||||
name: "toggleHeaderColumn",
|
||||
label: t("Toggle header"),
|
||||
icon: <TableHeaderColumnIcon />,
|
||||
visible: index === 0,
|
||||
},
|
||||
{
|
||||
name: rtl ? "addColumnAfter" : "addColumnBefore",
|
||||
label: rtl ? t("Insert after") : t("Insert before"),
|
||||
icon: <InsertLeftIcon />,
|
||||
attrs: { index },
|
||||
},
|
||||
{
|
||||
name: rtl ? "addColumnBefore" : "addColumnAfter",
|
||||
label: rtl ? t("Insert before") : t("Insert after"),
|
||||
icon: <InsertRightIcon />,
|
||||
attrs: { index },
|
||||
},
|
||||
{
|
||||
name: "moveTableColumn",
|
||||
label: t("Move left"),
|
||||
icon: <ArrowLeftIcon />,
|
||||
attrs: { from: index, to: index - 1 },
|
||||
visible: index > 0,
|
||||
},
|
||||
{
|
||||
name: "moveTableColumn",
|
||||
label: t("Move right"),
|
||||
icon: <ArrowRightIcon />,
|
||||
attrs: { from: index, to: index + 1 },
|
||||
visible: index < tableMap.map.width - 1,
|
||||
},
|
||||
{
|
||||
name: "separator",
|
||||
},
|
||||
{
|
||||
name: "mergeCells",
|
||||
label: t("Merge cells"),
|
||||
icon: <TableMergeCellsIcon />,
|
||||
visible: isMultipleCellSelection(state),
|
||||
},
|
||||
{
|
||||
name: "splitCell",
|
||||
label: t("Split cell"),
|
||||
icon: <TableSplitCellsIcon />,
|
||||
visible: isMergedCellSelection(state),
|
||||
},
|
||||
{
|
||||
name: "distributeColumns",
|
||||
visible: selectedCols.length > 1,
|
||||
label: t("Distribute columns"),
|
||||
icon: <TableColumnsDistributeIcon />,
|
||||
},
|
||||
{
|
||||
name: "separator",
|
||||
},
|
||||
{
|
||||
name: "deleteColumn",
|
||||
dangerous: true,
|
||||
label: t("Delete"),
|
||||
icon: <TrashIcon />,
|
||||
},
|
||||
],
|
||||
name: "separator",
|
||||
},
|
||||
{
|
||||
name: "toggleHeaderColumn",
|
||||
label: t("Toggle header"),
|
||||
icon: <TableHeaderColumnIcon />,
|
||||
visible: index === 0,
|
||||
},
|
||||
{
|
||||
name: rtl ? "addColumnAfter" : "addColumnBefore",
|
||||
label: rtl ? t("Insert after") : t("Insert before"),
|
||||
icon: <InsertLeftIcon />,
|
||||
attrs: { index },
|
||||
},
|
||||
{
|
||||
name: rtl ? "addColumnBefore" : "addColumnAfter",
|
||||
label: rtl ? t("Insert before") : t("Insert after"),
|
||||
icon: <InsertRightIcon />,
|
||||
attrs: { index },
|
||||
},
|
||||
{
|
||||
name: "moveTableColumn",
|
||||
label: t("Move left"),
|
||||
icon: <ArrowLeftIcon />,
|
||||
attrs: { from: index, to: index - 1 },
|
||||
visible: index > 0,
|
||||
},
|
||||
{
|
||||
name: "moveTableColumn",
|
||||
label: t("Move right"),
|
||||
icon: <ArrowRightIcon />,
|
||||
attrs: { from: index, to: index + 1 },
|
||||
visible: index < tableMap.map.width - 1,
|
||||
},
|
||||
{
|
||||
name: "separator",
|
||||
},
|
||||
{
|
||||
name: "mergeCells",
|
||||
label: t("Merge cells"),
|
||||
icon: <TableMergeCellsIcon />,
|
||||
visible: isMultipleCellSelection(state),
|
||||
},
|
||||
{
|
||||
name: "splitCell",
|
||||
label: t("Split cell"),
|
||||
icon: <TableSplitCellsIcon />,
|
||||
visible: isMergedCellSelection(state),
|
||||
},
|
||||
{
|
||||
name: "distributeColumns",
|
||||
visible: selectedCols.length > 1,
|
||||
label: t("Distribute columns"),
|
||||
icon: <TableColumnsDistributeIcon />,
|
||||
},
|
||||
{
|
||||
name: "separator",
|
||||
},
|
||||
{
|
||||
name: "deleteColumn",
|
||||
dangerous: true,
|
||||
label: t("Delete"),
|
||||
icon: <TrashIcon />,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
TrashIcon,
|
||||
InsertAboveIcon,
|
||||
InsertBelowIcon,
|
||||
MoreIcon,
|
||||
PaletteIcon,
|
||||
TableHeaderRowIcon,
|
||||
TableSplitCellsIcon,
|
||||
@@ -16,7 +15,11 @@ import {
|
||||
isMultipleCellSelection,
|
||||
} from "@shared/editor/queries/table";
|
||||
import { t } from "i18next";
|
||||
import type { MenuItem, NodeAttrMark, SelectionContext } from "@shared/editor/types";
|
||||
import type {
|
||||
MenuItem,
|
||||
NodeAttrMark,
|
||||
SelectionContext,
|
||||
} from "@shared/editor/types";
|
||||
import { ArrowDownIcon, ArrowUpIcon } from "~/components/Icons/ArrowIcon";
|
||||
import CircleIcon from "~/components/Icons/CircleIcon";
|
||||
import CellBackgroundColorPicker from "../components/CellBackgroundColorPicker";
|
||||
@@ -56,9 +59,7 @@ function getRowColors(state: EditorState, rowIndex: number): Set<string> {
|
||||
* @param ctx - the current selection context.
|
||||
* @returns an array of menu items.
|
||||
*/
|
||||
export default function tableRowMenuItems(
|
||||
ctx: SelectionContext
|
||||
): MenuItem[] {
|
||||
export default function tableRowMenuItems(ctx: SelectionContext): MenuItem[] {
|
||||
if (ctx.readOnly) {
|
||||
return [];
|
||||
}
|
||||
@@ -83,7 +84,42 @@ export default function tableRowMenuItems(
|
||||
|
||||
return [
|
||||
{
|
||||
tooltip: t("Background color"),
|
||||
name: "toggleHeaderRow",
|
||||
label: t("Toggle header"),
|
||||
icon: <TableHeaderRowIcon />,
|
||||
visible: index === 0,
|
||||
},
|
||||
{
|
||||
name: "addRowBefore",
|
||||
label: t("Insert before"),
|
||||
icon: <InsertAboveIcon />,
|
||||
attrs: { index },
|
||||
},
|
||||
{
|
||||
name: "addRowAfter",
|
||||
label: t("Insert after"),
|
||||
icon: <InsertBelowIcon />,
|
||||
attrs: { index },
|
||||
},
|
||||
{
|
||||
name: "moveTableRow",
|
||||
label: t("Move up"),
|
||||
icon: <ArrowUpIcon />,
|
||||
attrs: { from: index, to: index - 1 },
|
||||
visible: index > 0,
|
||||
},
|
||||
{
|
||||
name: "moveTableRow",
|
||||
label: t("Move down"),
|
||||
icon: <ArrowDownIcon />,
|
||||
attrs: { from: index, to: index + 1 },
|
||||
visible: index < tableMap.map.height - 1,
|
||||
},
|
||||
{
|
||||
name: "separator",
|
||||
},
|
||||
{
|
||||
label: t("Background"),
|
||||
icon:
|
||||
rowColors.size > 1 ? (
|
||||
<CircleIcon color="rainbow" />
|
||||
@@ -97,7 +133,7 @@ export default function tableRowMenuItems(
|
||||
{
|
||||
name: "toggleRowBackgroundAndCollapseSelection",
|
||||
label: t("None"),
|
||||
icon: <DottedCircleIcon retainColor color="transparent" />,
|
||||
icon: <DottedCircleIcon color="transparent" />,
|
||||
active: () => (hasBackground ? false : true),
|
||||
attrs: { color: null },
|
||||
},
|
||||
@@ -141,65 +177,28 @@ export default function tableRowMenuItems(
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <MoreIcon />,
|
||||
children: [
|
||||
{
|
||||
name: "toggleHeaderRow",
|
||||
label: t("Toggle header"),
|
||||
icon: <TableHeaderRowIcon />,
|
||||
visible: index === 0,
|
||||
},
|
||||
{
|
||||
name: "addRowBefore",
|
||||
label: t("Insert before"),
|
||||
icon: <InsertAboveIcon />,
|
||||
attrs: { index },
|
||||
},
|
||||
{
|
||||
name: "addRowAfter",
|
||||
label: t("Insert after"),
|
||||
icon: <InsertBelowIcon />,
|
||||
attrs: { index },
|
||||
},
|
||||
{
|
||||
name: "moveTableRow",
|
||||
label: t("Move up"),
|
||||
icon: <ArrowUpIcon />,
|
||||
attrs: { from: index, to: index - 1 },
|
||||
visible: index > 0,
|
||||
},
|
||||
{
|
||||
name: "moveTableRow",
|
||||
label: t("Move down"),
|
||||
icon: <ArrowDownIcon />,
|
||||
attrs: { from: index, to: index + 1 },
|
||||
visible: index < tableMap.map.height - 1,
|
||||
},
|
||||
{
|
||||
name: "separator",
|
||||
},
|
||||
{
|
||||
name: "mergeCells",
|
||||
label: t("Merge cells"),
|
||||
icon: <TableMergeCellsIcon />,
|
||||
visible: isMultipleCellSelection(state),
|
||||
},
|
||||
{
|
||||
name: "splitCell",
|
||||
label: t("Split cell"),
|
||||
icon: <TableSplitCellsIcon />,
|
||||
visible: isMergedCellSelection(state),
|
||||
},
|
||||
{
|
||||
name: "separator",
|
||||
},
|
||||
{
|
||||
name: "deleteRow",
|
||||
label: t("Delete"),
|
||||
dangerous: true,
|
||||
icon: <TrashIcon />,
|
||||
},
|
||||
],
|
||||
name: "separator",
|
||||
},
|
||||
{
|
||||
name: "mergeCells",
|
||||
label: t("Merge cells"),
|
||||
icon: <TableMergeCellsIcon />,
|
||||
visible: isMultipleCellSelection(state),
|
||||
},
|
||||
{
|
||||
name: "splitCell",
|
||||
label: t("Split cell"),
|
||||
icon: <TableSplitCellsIcon />,
|
||||
visible: isMergedCellSelection(state),
|
||||
},
|
||||
{
|
||||
name: "separator",
|
||||
},
|
||||
{
|
||||
name: "deleteRow",
|
||||
label: t("Delete"),
|
||||
dangerous: true,
|
||||
icon: <TrashIcon />,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
"@radix-ui/react-one-time-password-field": "^0.1.8",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toolbar": "^1.1.11",
|
||||
@@ -223,6 +224,7 @@
|
||||
"react-i18next": "^12.3.1",
|
||||
"react-merge-refs": "^2.1.1",
|
||||
"react-portal": "^4.3.0",
|
||||
"react-remove-scroll": "^2.7.2",
|
||||
"react-router-dom": "^5.3.4",
|
||||
"react-use-measure": "^2.1.7",
|
||||
"react-virtualized-auto-sizer": "^1.0.26",
|
||||
|
||||
@@ -273,7 +273,19 @@ export default class ExtensionManager {
|
||||
return;
|
||||
}
|
||||
if (extension.focusAfterExecution) {
|
||||
// Focusing a blurred editor (e.g. when the command is run from a
|
||||
// menu that holds focus) can collapse a non-text selection such as
|
||||
// a table cell selection. Restore it so selection-based commands
|
||||
// operate on the intended selection.
|
||||
const { selection } = view.state;
|
||||
view.focus();
|
||||
if (!view.state.selection.eq(selection)) {
|
||||
view.dispatch(
|
||||
view.state.tr
|
||||
.setSelection(selection)
|
||||
.setMeta("addToHistory", false)
|
||||
);
|
||||
}
|
||||
}
|
||||
return callback(attrs)?.(view.state, view.dispatch, view);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { EditorView } from "prosemirror-view";
|
||||
import { DecorationSet, Decoration } from "prosemirror-view";
|
||||
import { isInTable, moveTableColumn, TableMap } from "prosemirror-tables";
|
||||
import { addColumnBefore, selectColumn } from "../commands/table";
|
||||
import { isMobile } from "../../utils/browser";
|
||||
import {
|
||||
getCellAttrs,
|
||||
isValidCellAlignment,
|
||||
@@ -326,7 +327,9 @@ export default class TableHeader extends Node {
|
||||
)
|
||||
);
|
||||
|
||||
if (!isDragging) {
|
||||
// The add-column affordance is too small to tap on mobile, where
|
||||
// columns can be added via the inline menu instead.
|
||||
if (!isDragging && !isMobile()) {
|
||||
if (index === 0) {
|
||||
decorations.push(buildAddColumnDecoration(pos, index));
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Decoration, DecorationSet } from "prosemirror-view";
|
||||
import type { EditorView } from "prosemirror-view";
|
||||
import { Plugin } from "prosemirror-state";
|
||||
import { addRowBefore, selectRow, selectTable } from "../commands/table";
|
||||
import { isMobile } from "../../utils/browser";
|
||||
import {
|
||||
getCellsInRow,
|
||||
getRowsInTable,
|
||||
@@ -339,7 +340,9 @@ export default class TableRow extends Node {
|
||||
)
|
||||
);
|
||||
|
||||
if (!isDragging) {
|
||||
// The add-row affordance is too small to tap on mobile, where
|
||||
// rows can be added via the inline menu instead.
|
||||
if (!isDragging && !isMobile()) {
|
||||
if (index === 0) {
|
||||
decorations.push(buildAddRowDecoration(pos, index));
|
||||
}
|
||||
|
||||
@@ -17,6 +17,14 @@ export enum TableLayout {
|
||||
fullWidth = "full-width",
|
||||
}
|
||||
|
||||
/** How a selection toolbar menu is presented. */
|
||||
export enum MenuType {
|
||||
/** A horizontal strip of buttons; nested options open behind a trigger. */
|
||||
toolbar = "toolbar",
|
||||
/** A vertical menu rendered directly, anchored to the selection. */
|
||||
inline = "inline",
|
||||
}
|
||||
|
||||
type Section = ({ t }: { t: TFunction }) => string;
|
||||
|
||||
export type MenuItem = {
|
||||
@@ -140,6 +148,12 @@ export interface SelectionToolbarMenuDescriptor {
|
||||
priority: number;
|
||||
/** Toolbar alignment when this menu is active. Defaults to "center". */
|
||||
align?: "center" | "start" | "end";
|
||||
/**
|
||||
* How the menu is presented. "toolbar" (default) renders a horizontal strip
|
||||
* of buttons; "inline" renders a vertical menu anchored to the selection
|
||||
* without requiring a trigger button.
|
||||
*/
|
||||
variant?: MenuType;
|
||||
/**
|
||||
* Returns the menu items to display for the current selection.
|
||||
*
|
||||
|
||||
@@ -567,6 +567,7 @@
|
||||
"Replacement": "Replacement",
|
||||
"Replace": "Replace",
|
||||
"Replace all": "Replace all",
|
||||
"Options": "Options",
|
||||
"Go to link": "Go to link",
|
||||
"Open link": "Open link",
|
||||
"Remove link": "Remove link",
|
||||
@@ -647,10 +648,13 @@
|
||||
"Edit image URL": "Edit image URL",
|
||||
"Default width": "Default width",
|
||||
"Distribute columns": "Distribute columns",
|
||||
"Delete table": "Delete table",
|
||||
"Export as CSV": "Export as CSV",
|
||||
"Delete table": "Delete table",
|
||||
"Align": "Align",
|
||||
"Sort": "Sort",
|
||||
"Sort ascending": "Sort ascending",
|
||||
"Sort descending": "Sort descending",
|
||||
"Background": "Background",
|
||||
"Toggle header": "Toggle header",
|
||||
"Insert after": "Insert after",
|
||||
"Insert before": "Insert before",
|
||||
|
||||
@@ -4817,7 +4817,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-slot@npm:1.2.4":
|
||||
"@radix-ui/react-slot@npm:1.2.4, @radix-ui/react-slot@npm:^1.2.3":
|
||||
version: 1.2.4
|
||||
resolution: "@radix-ui/react-slot@npm:1.2.4"
|
||||
dependencies:
|
||||
@@ -15087,6 +15087,7 @@ __metadata:
|
||||
"@radix-ui/react-one-time-password-field": "npm:^0.1.8"
|
||||
"@radix-ui/react-popover": "npm:^1.1.15"
|
||||
"@radix-ui/react-select": "npm:^2.2.6"
|
||||
"@radix-ui/react-slot": "npm:^1.2.3"
|
||||
"@radix-ui/react-switch": "npm:^1.2.6"
|
||||
"@radix-ui/react-tabs": "npm:^1.1.13"
|
||||
"@radix-ui/react-toolbar": "npm:^1.1.11"
|
||||
@@ -15299,6 +15300,7 @@ __metadata:
|
||||
react-merge-refs: "npm:^2.1.1"
|
||||
react-portal: "npm:^4.3.0"
|
||||
react-refresh: "npm:^0.18.0"
|
||||
react-remove-scroll: "npm:^2.7.2"
|
||||
react-router-dom: "npm:^5.3.4"
|
||||
react-use-measure: "npm:^2.1.7"
|
||||
react-virtualized-auto-sizer: "npm:^1.0.26"
|
||||
@@ -16853,7 +16855,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-remove-scroll@npm:^2.6.3":
|
||||
"react-remove-scroll@npm:^2.6.3, react-remove-scroll@npm:^2.7.2":
|
||||
version: 2.7.2
|
||||
resolution: "react-remove-scroll@npm:2.7.2"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user