mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 09:12:40 +02:00
feat: introduce fixed toolbar and insert button for enhanced editor functionality
This commit is contained in:
parent
73147c69a3
commit
1450e22f54
8 changed files with 359 additions and 87 deletions
105
surfsense_web/components/ui/fixed-toolbar-buttons.tsx
Normal file
105
surfsense_web/components/ui/fixed-toolbar-buttons.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
BoldIcon,
|
||||
Code2Icon,
|
||||
HighlighterIcon,
|
||||
ItalicIcon,
|
||||
RedoIcon,
|
||||
StrikethroughIcon,
|
||||
UnderlineIcon,
|
||||
UndoIcon,
|
||||
} from 'lucide-react';
|
||||
import { KEYS } from 'platejs';
|
||||
import { useEditorReadOnly, useEditorRef } from 'platejs/react';
|
||||
|
||||
import { InsertToolbarButton } from './insert-toolbar-button';
|
||||
import { LinkToolbarButton } from './link-toolbar-button';
|
||||
import { MarkToolbarButton } from './mark-toolbar-button';
|
||||
import { MoreToolbarButton } from './more-toolbar-button';
|
||||
import { ToolbarButton, ToolbarGroup } from './toolbar';
|
||||
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
|
||||
|
||||
export function FixedToolbarButtons() {
|
||||
const readOnly = useEditorReadOnly();
|
||||
const editor = useEditorRef();
|
||||
|
||||
if (readOnly) return null;
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-wrap">
|
||||
<ToolbarGroup>
|
||||
<ToolbarButton
|
||||
tooltip="Undo (⌘+Z)"
|
||||
onClick={() => {
|
||||
editor.undo();
|
||||
editor.tf.focus();
|
||||
}}
|
||||
>
|
||||
<UndoIcon />
|
||||
</ToolbarButton>
|
||||
|
||||
<ToolbarButton
|
||||
tooltip="Redo (⌘+⇧+Z)"
|
||||
onClick={() => {
|
||||
editor.redo();
|
||||
editor.tf.focus();
|
||||
}}
|
||||
>
|
||||
<RedoIcon />
|
||||
</ToolbarButton>
|
||||
</ToolbarGroup>
|
||||
|
||||
<ToolbarGroup>
|
||||
<InsertToolbarButton />
|
||||
<TurnIntoToolbarButton />
|
||||
</ToolbarGroup>
|
||||
|
||||
<ToolbarGroup>
|
||||
<MarkToolbarButton nodeType={KEYS.bold} tooltip="Bold (⌘+B)">
|
||||
<BoldIcon />
|
||||
</MarkToolbarButton>
|
||||
|
||||
<MarkToolbarButton nodeType={KEYS.italic} tooltip="Italic (⌘+I)">
|
||||
<ItalicIcon />
|
||||
</MarkToolbarButton>
|
||||
|
||||
<MarkToolbarButton
|
||||
nodeType={KEYS.underline}
|
||||
tooltip="Underline (⌘+U)"
|
||||
>
|
||||
<UnderlineIcon />
|
||||
</MarkToolbarButton>
|
||||
|
||||
<MarkToolbarButton
|
||||
nodeType={KEYS.strikethrough}
|
||||
tooltip="Strikethrough (⌘+⇧+M)"
|
||||
>
|
||||
<StrikethroughIcon />
|
||||
</MarkToolbarButton>
|
||||
|
||||
<MarkToolbarButton nodeType={KEYS.code} tooltip="Code (⌘+E)">
|
||||
<Code2Icon />
|
||||
</MarkToolbarButton>
|
||||
|
||||
<MarkToolbarButton
|
||||
nodeType={KEYS.highlight}
|
||||
tooltip="Highlight (⌘+⇧+H)"
|
||||
>
|
||||
<HighlighterIcon />
|
||||
</MarkToolbarButton>
|
||||
</ToolbarGroup>
|
||||
|
||||
<ToolbarGroup>
|
||||
<LinkToolbarButton />
|
||||
</ToolbarGroup>
|
||||
|
||||
<ToolbarGroup>
|
||||
<MoreToolbarButton />
|
||||
</ToolbarGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
26
surfsense_web/components/ui/fixed-toolbar.tsx
Normal file
26
surfsense_web/components/ui/fixed-toolbar.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { Toolbar } from './toolbar';
|
||||
|
||||
export function FixedToolbar({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Toolbar>) {
|
||||
return (
|
||||
<Toolbar
|
||||
className={cn(
|
||||
'scrollbar-hide sticky top-0 left-0 z-50 w-full justify-between overflow-x-auto rounded-t-lg border-b bg-background/95 p-1 backdrop-blur supports-backdrop-filter:bg-background/60',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Toolbar>
|
||||
);
|
||||
}
|
||||
|
||||
205
surfsense_web/components/ui/insert-toolbar-button.tsx
Normal file
205
surfsense_web/components/ui/insert-toolbar-button.tsx
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';
|
||||
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
FileCodeIcon,
|
||||
Heading1Icon,
|
||||
Heading2Icon,
|
||||
Heading3Icon,
|
||||
InfoIcon,
|
||||
ListIcon,
|
||||
ListOrderedIcon,
|
||||
MinusIcon,
|
||||
PilcrowIcon,
|
||||
PlusIcon,
|
||||
QuoteIcon,
|
||||
RadicalIcon,
|
||||
SquareIcon,
|
||||
TableIcon,
|
||||
} from 'lucide-react';
|
||||
import { KEYS } from 'platejs';
|
||||
import { type PlateEditor, useEditorRef } from 'platejs/react';
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
insertBlock,
|
||||
insertInlineElement,
|
||||
} from '@/components/editor/transforms';
|
||||
|
||||
import { ToolbarButton, ToolbarMenuGroup } from './toolbar';
|
||||
|
||||
type Group = {
|
||||
group: string;
|
||||
items: Item[];
|
||||
};
|
||||
|
||||
type Item = {
|
||||
icon: React.ReactNode;
|
||||
value: string;
|
||||
onSelect: (editor: PlateEditor, value: string) => void;
|
||||
focusEditor?: boolean;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
const groups: Group[] = [
|
||||
{
|
||||
group: 'Basic blocks',
|
||||
items: [
|
||||
{
|
||||
icon: <PilcrowIcon />,
|
||||
label: 'Paragraph',
|
||||
value: KEYS.p,
|
||||
},
|
||||
{
|
||||
icon: <Heading1Icon />,
|
||||
label: 'Heading 1',
|
||||
value: 'h1',
|
||||
},
|
||||
{
|
||||
icon: <Heading2Icon />,
|
||||
label: 'Heading 2',
|
||||
value: 'h2',
|
||||
},
|
||||
{
|
||||
icon: <Heading3Icon />,
|
||||
label: 'Heading 3',
|
||||
value: 'h3',
|
||||
},
|
||||
{
|
||||
icon: <TableIcon />,
|
||||
label: 'Table',
|
||||
value: KEYS.table,
|
||||
},
|
||||
{
|
||||
icon: <FileCodeIcon />,
|
||||
label: 'Code block',
|
||||
value: KEYS.codeBlock,
|
||||
},
|
||||
{
|
||||
icon: <QuoteIcon />,
|
||||
label: 'Quote',
|
||||
value: KEYS.blockquote,
|
||||
},
|
||||
{
|
||||
icon: <MinusIcon />,
|
||||
label: 'Divider',
|
||||
value: KEYS.hr,
|
||||
},
|
||||
].map((item) => ({
|
||||
...item,
|
||||
onSelect: (editor: PlateEditor, value: string) => {
|
||||
insertBlock(editor, value);
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
group: 'Lists',
|
||||
items: [
|
||||
{
|
||||
icon: <ListIcon />,
|
||||
label: 'Bulleted list',
|
||||
value: KEYS.ul,
|
||||
},
|
||||
{
|
||||
icon: <ListOrderedIcon />,
|
||||
label: 'Numbered list',
|
||||
value: KEYS.ol,
|
||||
},
|
||||
{
|
||||
icon: <SquareIcon />,
|
||||
label: 'To-do list',
|
||||
value: KEYS.listTodo,
|
||||
},
|
||||
{
|
||||
icon: <ChevronRightIcon />,
|
||||
label: 'Toggle list',
|
||||
value: KEYS.toggle,
|
||||
},
|
||||
].map((item) => ({
|
||||
...item,
|
||||
onSelect: (editor: PlateEditor, value: string) => {
|
||||
insertBlock(editor, value);
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
group: 'Advanced',
|
||||
items: [
|
||||
{
|
||||
icon: <InfoIcon />,
|
||||
label: 'Callout',
|
||||
value: KEYS.callout,
|
||||
},
|
||||
{
|
||||
focusEditor: false,
|
||||
icon: <RadicalIcon />,
|
||||
label: 'Equation',
|
||||
value: KEYS.equation,
|
||||
},
|
||||
].map((item) => ({
|
||||
...item,
|
||||
onSelect: (editor: PlateEditor, value: string) => {
|
||||
if (item.value === KEYS.equation) {
|
||||
insertInlineElement(editor, value);
|
||||
} else {
|
||||
insertBlock(editor, value);
|
||||
}
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
export function InsertToolbarButton(props: DropdownMenuProps) {
|
||||
const editor = useEditorRef();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen} modal={false} {...props}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ToolbarButton pressed={open} tooltip="Insert" isDropdown>
|
||||
<PlusIcon />
|
||||
</ToolbarButton>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
className="flex max-h-[500px] min-w-0 flex-col overflow-y-auto"
|
||||
align="start"
|
||||
>
|
||||
{groups.map(({ group, items }) => (
|
||||
<React.Fragment key={group}>
|
||||
<ToolbarMenuGroup>
|
||||
{items.map(({ icon, label, value, onSelect, focusEditor }) => (
|
||||
<DropdownMenuItem
|
||||
key={value}
|
||||
onSelect={() => {
|
||||
onSelect(editor, value);
|
||||
if (focusEditor !== false) {
|
||||
editor.tf.focus();
|
||||
}
|
||||
setOpen(false);
|
||||
}}
|
||||
className="group"
|
||||
>
|
||||
<div className="flex items-center text-sm text-muted-foreground focus:text-accent-foreground group-aria-selected:text-accent-foreground">
|
||||
{icon}
|
||||
<span className="ml-2">{label || value}</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</ToolbarMenuGroup>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -9,7 +9,6 @@ import {
|
|||
BlockSelectionPlugin,
|
||||
useBlockSelected,
|
||||
} from '@platejs/selection/react';
|
||||
import { setCellBackground } from '@platejs/table';
|
||||
import {
|
||||
TablePlugin,
|
||||
TableProvider,
|
||||
|
|
@ -27,10 +26,8 @@ import {
|
|||
ArrowRight,
|
||||
ArrowUp,
|
||||
CombineIcon,
|
||||
EraserIcon,
|
||||
Grid2X2Icon,
|
||||
GripVertical,
|
||||
PaintBucketIcon,
|
||||
SquareSplitHorizontalIcon,
|
||||
Trash2Icon,
|
||||
XIcon,
|
||||
|
|
@ -66,7 +63,6 @@ import {
|
|||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
|
|
@ -74,10 +70,6 @@ import { Popover, PopoverContent } from '@/components/ui/popover';
|
|||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { blockSelectionVariants } from './block-selection';
|
||||
import {
|
||||
ColorDropdownMenuItems,
|
||||
DEFAULT_COLORS,
|
||||
} from './font-color-toolbar-button';
|
||||
import { ResizeHandle } from './resize-handle';
|
||||
import {
|
||||
BorderAllIcon,
|
||||
|
|
@ -91,8 +83,8 @@ import {
|
|||
Toolbar,
|
||||
ToolbarButton,
|
||||
ToolbarGroup,
|
||||
ToolbarMenuGroup,
|
||||
} from './toolbar';
|
||||
|
||||
export const TableElement = withHOC(
|
||||
TableProvider,
|
||||
function TableElement({
|
||||
|
|
@ -181,9 +173,6 @@ function TableFloatingToolbar({
|
|||
contentEditable={false}
|
||||
>
|
||||
<ToolbarGroup>
|
||||
<ColorDropdownMenu tooltip="Background color">
|
||||
<PaintBucketIcon />
|
||||
</ColorDropdownMenu>
|
||||
{canMerge && (
|
||||
<ToolbarButton
|
||||
onClick={() => tf.table.merge()}
|
||||
|
|
@ -370,59 +359,6 @@ function TableBordersDropdownMenuContent(
|
|||
);
|
||||
}
|
||||
|
||||
function ColorDropdownMenu({
|
||||
children,
|
||||
tooltip,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
tooltip: string;
|
||||
}) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const editor = useEditorRef();
|
||||
const selectedCells = usePluginOption(TablePlugin, 'selectedCells');
|
||||
|
||||
const onUpdateColor = React.useCallback(
|
||||
(color: string) => {
|
||||
setOpen(false);
|
||||
setCellBackground(editor, { color, selectedCells: selectedCells ?? [] });
|
||||
},
|
||||
[selectedCells, editor]
|
||||
);
|
||||
|
||||
const onClearColor = React.useCallback(() => {
|
||||
setOpen(false);
|
||||
setCellBackground(editor, {
|
||||
color: null,
|
||||
selectedCells: selectedCells ?? [],
|
||||
});
|
||||
}, [selectedCells, editor]);
|
||||
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ToolbarButton tooltip={tooltip}>{children}</ToolbarButton>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="start">
|
||||
<ToolbarMenuGroup label="Colors">
|
||||
<ColorDropdownMenuItems
|
||||
className="px-2"
|
||||
colors={DEFAULT_COLORS}
|
||||
updateColor={onUpdateColor}
|
||||
/>
|
||||
</ToolbarMenuGroup>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem className="p-2" onClick={onClearColor}>
|
||||
<EraserIcon />
|
||||
<span>Clear</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableRowElement({
|
||||
children,
|
||||
...props
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue