Table control improvements
This commit is contained in:
@@ -0,0 +1,187 @@
|
|||||||
|
import { type Component, createSignal, createEffect, For } from "solid-js";
|
||||||
|
import {
|
||||||
|
Trash2,
|
||||||
|
Type
|
||||||
|
} from "lucide-solid";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { Editor } from "@tiptap/core";
|
||||||
|
|
||||||
|
// Custom Icons
|
||||||
|
const AddRowAbove = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="3" y="8" width="12" height="7" rx="1.5" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<path d="M9 2V6M7 4H11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const AddRowBelow = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="3" y="3" width="12" height="7" rx="1.5" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<path d="M9 12V16M7 14H11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const AddColLeft = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="8" y="3" width="7" height="12" rx="1.5" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<path d="M2 9H6M4 7V11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const AddColRight = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="3" y="3" width="7" height="12" rx="1.5" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<path d="M12 9H16M14 7V11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const DeleteRow = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="3" y="6.5" width="12" height="5" rx="1" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<path d="M7.5 7.5L10.5 10.5M10.5 7.5L7.5 10.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const DeleteCol = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="6.5" y="3" width="5" height="12" rx="1" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<path d="M7.5 7.5L10.5 10.5M10.5 7.5L7.5 10.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const MergeCells = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="3" y="3" width="12" height="12" rx="1.5" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<path d="M9 5V13M5 9H13" stroke="currentColor" stroke-width="1" stroke-dasharray="2 2" />
|
||||||
|
<path d="M5.5 9L7.5 9M12.5 9L10.5 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||||
|
<path d="M7.5 9L6.5 8M7.5 9L6.5 10M10.5 9L11.5 8M10.5 9L11.5 10" stroke="currentColor" stroke-width="1" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const SplitCell = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="3" y="3" width="12" height="12" rx="1.5" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<path d="M9 5V13" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<path d="M7 9L5 9M11 9L13 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||||
|
<path d="M5 9L6 8M5 9L6 10M13 9L12 8M13 9L12 10" stroke="currentColor" stroke-width="1" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface TableAction {
|
||||||
|
label: string;
|
||||||
|
icon: Component;
|
||||||
|
command: () => void;
|
||||||
|
iconClass?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableGroup {
|
||||||
|
name: string;
|
||||||
|
actions: TableAction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableBubbleMenuProps {
|
||||||
|
editor: Editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TableBubbleMenu: Component<TableBubbleMenuProps> = (props) => {
|
||||||
|
const [isVisible, setIsVisible] = createSignal(false);
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const updateVisibility = () => {
|
||||||
|
const editor = props.editor;
|
||||||
|
if (!editor) return;
|
||||||
|
setIsVisible(editor.isActive("table"));
|
||||||
|
};
|
||||||
|
|
||||||
|
props.editor.on("selectionUpdate", updateVisibility);
|
||||||
|
props.editor.on("transaction", updateVisibility);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
props.editor.off("selectionUpdate", updateVisibility);
|
||||||
|
props.editor.off("transaction", updateVisibility);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const groups: TableGroup[] = [
|
||||||
|
{
|
||||||
|
name: "Rows",
|
||||||
|
actions: [
|
||||||
|
{ label: "Insert Row Above", icon: AddRowAbove, command: () => props.editor.chain().focus().addRowBefore().run() },
|
||||||
|
{ label: "Insert Row Below", icon: AddRowBelow, command: () => props.editor.chain().focus().addRowAfter().run() },
|
||||||
|
{ label: "Delete Row", icon: DeleteRow, iconClass: "text-destructive", command: () => props.editor.chain().focus().deleteRow().run() },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Cols",
|
||||||
|
actions: [
|
||||||
|
{ label: "Insert Column Left", icon: AddColLeft, command: () => props.editor.chain().focus().addColumnBefore().run() },
|
||||||
|
{ label: "Insert Column Right", icon: AddColRight, command: () => props.editor.chain().focus().addColumnAfter().run() },
|
||||||
|
{ label: "Delete Column", icon: DeleteCol, iconClass: "text-destructive", command: () => props.editor.chain().focus().deleteColumn().run() },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Cells",
|
||||||
|
actions: [
|
||||||
|
{ label: "Merge Cells", icon: MergeCells, command: () => props.editor.chain().focus().mergeCells().run() },
|
||||||
|
{ label: "Split Cell", icon: SplitCell, command: () => props.editor.chain().focus().splitCell().run() },
|
||||||
|
{ label: "Header Row", icon: Type, command: () => props.editor.chain().focus().toggleHeaderRow().run() },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Table",
|
||||||
|
actions: [
|
||||||
|
{ label: "Delete Table", icon: Trash2, iconClass: "text-destructive", command: () => props.editor.chain().focus().deleteTable().run() },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={cn(
|
||||||
|
"sticky top-0 z-[150] w-full bg-background/80 backdrop-blur-md border-b border-border transition-all duration-300 ease-in-out overflow-hidden flex flex-col",
|
||||||
|
isVisible() ? "h-10 opacity-100 mb-2" : "h-0 opacity-0 pointer-events-none"
|
||||||
|
)}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center h-full px-2 max-w-4xl mx-auto w-full">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<For each={groups}>
|
||||||
|
{(group, groupIdx) => (
|
||||||
|
<>
|
||||||
|
<div class="flex items-center gap-0 px-0.5">
|
||||||
|
<For each={group.actions}>
|
||||||
|
{(action) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
action.command();
|
||||||
|
}}
|
||||||
|
class={cn(
|
||||||
|
"p-0.5 rounded-md hover:bg-muted transition-colors text-muted-foreground hover:text-foreground shrink-0 group relative",
|
||||||
|
action.iconClass
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<action.icon />
|
||||||
|
<span class="absolute top-full left-1/2 -translate-x-1/2 mt-2 px-2 py-1 text-[10px] font-bold bg-foreground text-background rounded opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-[200] shadow-lg">
|
||||||
|
{action.label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
{groupIdx() < groups.length - 1 && (
|
||||||
|
<div class="w-[1px] h-4 bg-border/50 mx-1" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -35,6 +35,7 @@ import { Video } from "@/lib/extensions/video";
|
|||||||
import { VideoUpload } from "@/lib/extensions/video-upload";
|
import { VideoUpload } from "@/lib/extensions/video-upload";
|
||||||
import { FileAttachment } from "@/lib/extensions/file-attachment";
|
import { FileAttachment } from "@/lib/extensions/file-attachment";
|
||||||
import { FileUpload } from "@/lib/extensions/file-upload";
|
import { FileUpload } from "@/lib/extensions/file-upload";
|
||||||
|
import { TableBubbleMenu } from "./TableBubbleMenu";
|
||||||
|
|
||||||
interface TaskEditorProps {
|
interface TaskEditorProps {
|
||||||
content?: string;
|
content?: string;
|
||||||
@@ -335,6 +336,9 @@ export const TaskEditor: Component<TaskEditorProps> = (props) => {
|
|||||||
Click to insert newline above
|
Click to insert newline above
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<Show when={editor()}>
|
||||||
|
{(e) => <TableBubbleMenu editor={e()} />}
|
||||||
|
</Show>
|
||||||
<div
|
<div
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
class="w-full flex-1 cursor-text"
|
class="w-full flex-1 cursor-text"
|
||||||
|
|||||||
Reference in New Issue
Block a user