table
This commit is contained in:
@@ -22,6 +22,10 @@
|
||||
"@tiptap/extension-image": "^3.14.0",
|
||||
"@tiptap/extension-link": "^3.14.0",
|
||||
"@tiptap/extension-list-item": "^3.14.0",
|
||||
"@tiptap/extension-table": "^3.14.0",
|
||||
"@tiptap/extension-table-cell": "^3.14.0",
|
||||
"@tiptap/extension-table-header": "^3.14.0",
|
||||
"@tiptap/extension-table-row": "^3.14.0",
|
||||
"@tiptap/extension-text-style": "^3.14.0",
|
||||
"@tiptap/pm": "^3.14.0",
|
||||
"@tiptap/starter-kit": "^3.14.0",
|
||||
|
||||
83
src/app.css
83
src/app.css
@@ -726,3 +726,86 @@ a.hover-underline-animation:hover::after {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Table styles for TipTap editor */
|
||||
.tiptap-table {
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
margin: 1rem 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tiptap-table td,
|
||||
.tiptap-table th {
|
||||
min-width: 1em;
|
||||
border: 2px solid var(--color-surface2);
|
||||
padding: 0.5rem;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tiptap-table th {
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
background-color: var(--color-surface0);
|
||||
}
|
||||
|
||||
.tiptap-table .selectedCell {
|
||||
background-color: var(--color-surface1);
|
||||
}
|
||||
|
||||
.tiptap-table p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Additional table styles for ProseMirror */
|
||||
.ProseMirror table {
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
margin: 1rem 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ProseMirror table td,
|
||||
.ProseMirror table th {
|
||||
min-width: 3em;
|
||||
border: 2px solid var(--color-text);
|
||||
padding: 0.5rem;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
background-color: var(--color-mantle);
|
||||
}
|
||||
|
||||
.ProseMirror table th {
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
background-color: var(--color-surface0);
|
||||
}
|
||||
|
||||
.ProseMirror table .selectedCell:after {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
content: "";
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--color-blue);
|
||||
opacity: 0.2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ProseMirror table .column-resize-handle {
|
||||
position: absolute;
|
||||
right: -2px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
z-index: 20;
|
||||
background-color: var(--color-blue);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@ import StarterKit from "@tiptap/starter-kit";
|
||||
import Link from "@tiptap/extension-link";
|
||||
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
||||
import Image from "@tiptap/extension-image";
|
||||
import { Table } from "@tiptap/extension-table";
|
||||
import { TableRow } from "@tiptap/extension-table-row";
|
||||
import { TableHeader } from "@tiptap/extension-table-header";
|
||||
import { TableCell } from "@tiptap/extension-table-cell";
|
||||
import { Node } from "@tiptap/core";
|
||||
import { createLowlight, common } from "lowlight";
|
||||
import css from "highlight.js/lib/languages/css";
|
||||
@@ -196,6 +200,12 @@ export default function TextEditor(props: TextEditorProps) {
|
||||
left: 0
|
||||
});
|
||||
|
||||
const [showTableMenu, setShowTableMenu] = createSignal(false);
|
||||
const [tableMenuPosition, setTableMenuPosition] = createSignal({
|
||||
top: 0,
|
||||
left: 0
|
||||
});
|
||||
|
||||
const editor = createTiptapEditor(() => ({
|
||||
element: editorRef,
|
||||
extensions: [
|
||||
@@ -205,7 +215,28 @@ export default function TextEditor(props: TextEditorProps) {
|
||||
openOnClick: true
|
||||
}),
|
||||
Image,
|
||||
IframeEmbed
|
||||
IframeEmbed,
|
||||
Table.configure({
|
||||
resizable: true,
|
||||
HTMLAttributes: {
|
||||
class: "tiptap-table"
|
||||
}
|
||||
}),
|
||||
TableRow.configure({
|
||||
HTMLAttributes: {
|
||||
class: "tiptap-table-row"
|
||||
}
|
||||
}),
|
||||
TableHeader.configure({
|
||||
HTMLAttributes: {
|
||||
class: "tiptap-table-header"
|
||||
}
|
||||
}),
|
||||
TableCell.configure({
|
||||
HTMLAttributes: {
|
||||
class: "tiptap-table-cell"
|
||||
}
|
||||
})
|
||||
],
|
||||
content: props.preSet || `<p><em><b>Hello!</b> World</em></p>`,
|
||||
editorProps: {
|
||||
@@ -319,6 +350,31 @@ export default function TextEditor(props: TextEditorProps) {
|
||||
setShowLanguageSelector(!showLanguageSelector());
|
||||
};
|
||||
|
||||
const insertTable = (rows: number, cols: number) => {
|
||||
const instance = editor();
|
||||
if (!instance) return;
|
||||
|
||||
console.log("Inserting table:", rows, "x", cols);
|
||||
|
||||
instance
|
||||
.chain()
|
||||
.focus()
|
||||
.insertTable({ rows, cols, withHeaderRow: true })
|
||||
.run();
|
||||
|
||||
console.log("Table inserted, isActive:", instance.isActive("table"));
|
||||
setShowTableMenu(false);
|
||||
};
|
||||
|
||||
const showTableInserter = (e: MouseEvent) => {
|
||||
const buttonRect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
setTableMenuPosition({
|
||||
top: buttonRect.bottom + 5,
|
||||
left: buttonRect.left
|
||||
});
|
||||
setShowTableMenu(!showTableMenu());
|
||||
};
|
||||
|
||||
// Close language selector on outside click
|
||||
createEffect(() => {
|
||||
if (showLanguageSelector()) {
|
||||
@@ -340,6 +396,78 @@ export default function TextEditor(props: TextEditorProps) {
|
||||
}
|
||||
});
|
||||
|
||||
// Close table menu on outside click
|
||||
createEffect(() => {
|
||||
if (showTableMenu()) {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (
|
||||
!target.closest(".table-menu") &&
|
||||
!target.closest("[data-table-trigger]")
|
||||
) {
|
||||
setShowTableMenu(false);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
}, 0);
|
||||
|
||||
return () => document.removeEventListener("click", handleClickOutside);
|
||||
}
|
||||
});
|
||||
|
||||
// Table Grid Selector Component
|
||||
const TableGridSelector = () => {
|
||||
const [hoverCell, setHoverCell] = createSignal({ row: 0, col: 0 });
|
||||
const maxRows = 10;
|
||||
const maxCols = 10;
|
||||
|
||||
return (
|
||||
<div class="bg-mantle border-surface2 rounded border p-3 shadow-lg">
|
||||
<div class="text-subtext0 mb-2 text-xs">
|
||||
Insert Table: {hoverCell().row + 1} × {hoverCell().col + 1}
|
||||
</div>
|
||||
<div
|
||||
class="grid gap-1"
|
||||
style={{ "grid-template-columns": `repeat(${maxCols}, 1fr)` }}
|
||||
>
|
||||
<For each={Array.from({ length: maxRows * maxCols })}>
|
||||
{(_, idx) => {
|
||||
const row = Math.floor(idx() / maxCols);
|
||||
const col = idx() % maxCols;
|
||||
|
||||
return (
|
||||
<div
|
||||
class={`border-surface2 h-4 w-4 cursor-pointer border ${
|
||||
row <= hoverCell().row && col <= hoverCell().col
|
||||
? "bg-blue"
|
||||
: "bg-surface0"
|
||||
}`}
|
||||
onMouseEnter={() => setHoverCell({ row, col })}
|
||||
onClick={() => insertTable(row + 1, col + 1)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
<div class="mt-2 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const rows = parseInt(prompt("Number of rows:", "3") || "3");
|
||||
const cols = parseInt(prompt("Number of columns:", "3") || "3");
|
||||
if (rows && cols) insertTable(rows, cols);
|
||||
}}
|
||||
class="hover:bg-surface1 rounded px-2 py-1 text-xs"
|
||||
>
|
||||
Custom Size...
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="border-surface2 text-text w-full rounded-md border px-4 py-2">
|
||||
<Show when={editor()}>
|
||||
@@ -499,6 +627,19 @@ export default function TextEditor(props: TextEditorProps) {
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Table Grid Selector */}
|
||||
<Show when={showTableMenu()}>
|
||||
<div
|
||||
class="table-menu fixed z-50"
|
||||
style={{
|
||||
top: `${tableMenuPosition().top}px`,
|
||||
left: `${tableMenuPosition().left}px`
|
||||
}}
|
||||
>
|
||||
<TableGridSelector />
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="border-surface2 mb-2 flex flex-wrap gap-1 border-b pb-2">
|
||||
<button
|
||||
type="button"
|
||||
@@ -664,6 +805,19 @@ export default function TextEditor(props: TextEditorProps) {
|
||||
>
|
||||
📺 Iframe
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={showTableInserter}
|
||||
data-table-trigger
|
||||
class={`${
|
||||
instance().isActive("table")
|
||||
? "bg-surface2"
|
||||
: "hover:bg-surface1"
|
||||
} rounded px-2 py-1 text-xs`}
|
||||
title="Insert Table"
|
||||
>
|
||||
⊞ Table
|
||||
</button>
|
||||
<div class="border-surface2 mx-1 border-l"></div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -675,6 +829,119 @@ export default function TextEditor(props: TextEditorProps) {
|
||||
>
|
||||
━━ HR
|
||||
</button>
|
||||
|
||||
{/* Table controls - shown when cursor is in a table */}
|
||||
<Show when={instance().isActive("table")}>
|
||||
<div class="border-surface2 mx-1 border-l"></div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
instance().chain().focus().addColumnBefore().run()
|
||||
}
|
||||
class="hover:bg-surface1 rounded px-2 py-1 text-xs"
|
||||
title="Add Column Before"
|
||||
>
|
||||
← Col
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
instance().chain().focus().addColumnAfter().run()
|
||||
}
|
||||
class="hover:bg-surface1 rounded px-2 py-1 text-xs"
|
||||
title="Add Column After"
|
||||
>
|
||||
Col →
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
instance().chain().focus().deleteColumn().run()
|
||||
}
|
||||
class="hover:bg-red bg-opacity-20 rounded px-2 py-1 text-xs"
|
||||
title="Delete Column"
|
||||
>
|
||||
✕ Col
|
||||
</button>
|
||||
|
||||
<div class="border-surface2 mx-1 border-l"></div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
instance().chain().focus().addRowBefore().run()
|
||||
}
|
||||
class="hover:bg-surface1 rounded px-2 py-1 text-xs"
|
||||
title="Add Row Before"
|
||||
>
|
||||
↑ Row
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => instance().chain().focus().addRowAfter().run()}
|
||||
class="hover:bg-surface1 rounded px-2 py-1 text-xs"
|
||||
title="Add Row After"
|
||||
>
|
||||
Row ↓
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => instance().chain().focus().deleteRow().run()}
|
||||
class="hover:bg-red bg-opacity-20 rounded px-2 py-1 text-xs"
|
||||
title="Delete Row"
|
||||
>
|
||||
✕ Row
|
||||
</button>
|
||||
|
||||
<div class="border-surface2 mx-1 border-l"></div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => instance().chain().focus().deleteTable().run()}
|
||||
class="hover:bg-red rounded px-2 py-1 text-xs"
|
||||
title="Delete Table"
|
||||
>
|
||||
✕ Table
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
instance().chain().focus().toggleHeaderRow().run()
|
||||
}
|
||||
class={`${
|
||||
instance().isActive("tableHeader")
|
||||
? "bg-surface2"
|
||||
: "hover:bg-surface1"
|
||||
} rounded px-2 py-1 text-xs`}
|
||||
title="Toggle Header Row"
|
||||
>
|
||||
≡ Header
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => instance().chain().focus().mergeCells().run()}
|
||||
class="hover:bg-surface1 rounded px-2 py-1 text-xs"
|
||||
title="Merge Cells"
|
||||
>
|
||||
⊡ Merge
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => instance().chain().focus().splitCell().run()}
|
||||
class="hover:bg-surface1 rounded px-2 py-1 text-xs"
|
||||
title="Split Cell"
|
||||
>
|
||||
⊞ Split
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user