mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[feat]: Tiptap table integration (#2008)
* added basic table support * fixed table position at bottom * fixed image node deletion logic's regression issue * added compatible styles * enabled slash commands * disabled slash command and bubble menu's node selector for table cells * added dropcursor support to type below the table/image * blocked image uploads for handledrop and paste actions
This commit is contained in:
parent
320608ea73
commit
38b7f4382f
@ -77,14 +77,14 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
|||||||
{...bubbleMenuProps}
|
{...bubbleMenuProps}
|
||||||
className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"
|
className="flex w-fit divide-x divide-custom-border-300 rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"
|
||||||
>
|
>
|
||||||
<NodeSelector
|
{!props.editor.isActive("table") && <NodeSelector
|
||||||
editor={props.editor!}
|
editor={props.editor!}
|
||||||
isOpen={isNodeSelectorOpen}
|
isOpen={isNodeSelectorOpen}
|
||||||
setIsOpen={() => {
|
setIsOpen={() => {
|
||||||
setIsNodeSelectorOpen(!isNodeSelectorOpen);
|
setIsNodeSelectorOpen(!isNodeSelectorOpen);
|
||||||
setIsLinkSelectorOpen(false);
|
setIsLinkSelectorOpen(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>}
|
||||||
<LinkSelector
|
<LinkSelector
|
||||||
editor={props.editor!!}
|
editor={props.editor!!}
|
||||||
isOpen={isLinkSelectorOpen}
|
isOpen={isLinkSelectorOpen}
|
||||||
|
@ -6,7 +6,7 @@ import isValidHttpUrl from "./utils/link-validator";
|
|||||||
interface LinkSelectorProps {
|
interface LinkSelectorProps {
|
||||||
editor: Editor;
|
editor: Editor;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
setIsOpen: Dispatch<SetStateAction<boolean>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -52,7 +52,8 @@ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen
|
|||||||
className="fixed top-full z-[99999] mt-1 flex w-60 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 dow-xl animate-in fade-in slide-in-from-top-1"
|
className="fixed top-full z-[99999] mt-1 flex w-60 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 dow-xl animate-in fade-in slide-in-from-top-1"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault(); onLinkSubmit();
|
e.preventDefault();
|
||||||
|
onLinkSubmit();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -13,6 +13,7 @@ import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
|||||||
import { lowlight } from "lowlight/lib/core";
|
import { lowlight } from "lowlight/lib/core";
|
||||||
import SlashCommand from "../slash-command";
|
import SlashCommand from "../slash-command";
|
||||||
import { InputRule } from "@tiptap/core";
|
import { InputRule } from "@tiptap/core";
|
||||||
|
import Gapcursor from '@tiptap/extension-gapcursor'
|
||||||
|
|
||||||
import ts from "highlight.js/lib/languages/typescript";
|
import ts from "highlight.js/lib/languages/typescript";
|
||||||
|
|
||||||
@ -20,6 +21,10 @@ import "highlight.js/styles/github-dark.css";
|
|||||||
import UniqueID from "@tiptap-pro/extension-unique-id";
|
import UniqueID from "@tiptap-pro/extension-unique-id";
|
||||||
import UpdatedImage from "./updated-image";
|
import UpdatedImage from "./updated-image";
|
||||||
import isValidHttpUrl from "../bubble-menu/utils/link-validator";
|
import isValidHttpUrl from "../bubble-menu/utils/link-validator";
|
||||||
|
import { CustomTableCell } from "./table/table-cell";
|
||||||
|
import { Table } from "./table/table";
|
||||||
|
import { TableHeader } from "./table/table-header";
|
||||||
|
import { TableRow } from "@tiptap/extension-table-row";
|
||||||
|
|
||||||
lowlight.registerLanguage("ts", ts);
|
lowlight.registerLanguage("ts", ts);
|
||||||
|
|
||||||
@ -55,7 +60,7 @@ export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSub
|
|||||||
codeBlock: false,
|
codeBlock: false,
|
||||||
horizontalRule: false,
|
horizontalRule: false,
|
||||||
dropcursor: {
|
dropcursor: {
|
||||||
color: "#DBEAFE",
|
color: "rgba(var(--color-text-100))",
|
||||||
width: 2,
|
width: 2,
|
||||||
},
|
},
|
||||||
gapcursor: false,
|
gapcursor: false,
|
||||||
@ -86,6 +91,7 @@ export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSub
|
|||||||
class: "mb-6 border-t border-custom-border-300",
|
class: "mb-6 border-t border-custom-border-300",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
Gapcursor,
|
||||||
TiptapLink.configure({
|
TiptapLink.configure({
|
||||||
protocols: ["http", "https"],
|
protocols: ["http", "https"],
|
||||||
validate: (url) => isValidHttpUrl(url),
|
validate: (url) => isValidHttpUrl(url),
|
||||||
@ -104,6 +110,9 @@ export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSub
|
|||||||
if (node.type.name === "heading") {
|
if (node.type.name === "heading") {
|
||||||
return `Heading ${node.attrs.level}`;
|
return `Heading ${node.attrs.level}`;
|
||||||
}
|
}
|
||||||
|
if (node.type.name === "image" || node.type.name === "table") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
return "Press '/' for commands...";
|
return "Press '/' for commands...";
|
||||||
},
|
},
|
||||||
@ -134,4 +143,8 @@ export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSub
|
|||||||
html: true,
|
html: true,
|
||||||
transformCopiedText: true,
|
transformCopiedText: true,
|
||||||
}),
|
}),
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
CustomTableCell,
|
||||||
|
TableRow
|
||||||
];
|
];
|
||||||
|
31
apps/app/components/tiptap/extensions/table/table-cell.ts
Normal file
31
apps/app/components/tiptap/extensions/table/table-cell.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { TableCell } from "@tiptap/extension-table-cell";
|
||||||
|
|
||||||
|
export const CustomTableCell = TableCell.extend({
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
...this.parent?.(),
|
||||||
|
isHeader: {
|
||||||
|
default: false,
|
||||||
|
parseHTML: (element) => { isHeader: element.tagName === "TD" },
|
||||||
|
renderHTML: (attributes) => { tag: attributes.isHeader ? "th" : "td" }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
if (HTMLAttributes.isHeader) {
|
||||||
|
return [
|
||||||
|
"th",
|
||||||
|
{
|
||||||
|
...HTMLAttributes,
|
||||||
|
class: `relative ${HTMLAttributes.class}`,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
"span",
|
||||||
|
{ class: "absolute top-0 right-0" },
|
||||||
|
],
|
||||||
|
0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return ["td", HTMLAttributes, 0];
|
||||||
|
},
|
||||||
|
});
|
@ -0,0 +1,7 @@
|
|||||||
|
import { TableHeader as BaseTableHeader } from "@tiptap/extension-table-header";
|
||||||
|
|
||||||
|
const TableHeader = BaseTableHeader.extend({
|
||||||
|
content: "paragraph"
|
||||||
|
});
|
||||||
|
|
||||||
|
export { TableHeader };
|
9
apps/app/components/tiptap/extensions/table/table.ts
Normal file
9
apps/app/components/tiptap/extensions/table/table.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Table as BaseTable } from "@tiptap/extension-table";
|
||||||
|
|
||||||
|
const Table = BaseTable.configure({
|
||||||
|
resizable: true,
|
||||||
|
cellMinWidth: 100,
|
||||||
|
allowTableNodeSelection: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export { Table };
|
@ -5,6 +5,7 @@ import { TiptapExtensions } from "./extensions";
|
|||||||
import { TiptapEditorProps } from "./props";
|
import { TiptapEditorProps } from "./props";
|
||||||
import { useImperativeHandle, useRef, forwardRef } from "react";
|
import { useImperativeHandle, useRef, forwardRef } from "react";
|
||||||
import { ImageResizer } from "./extensions/image-resize";
|
import { ImageResizer } from "./extensions/image-resize";
|
||||||
|
import { TableMenu } from "./table-menu";
|
||||||
|
|
||||||
export interface ITipTapRichTextEditor {
|
export interface ITipTapRichTextEditor {
|
||||||
value: string;
|
value: string;
|
||||||
@ -92,6 +93,7 @@ const Tiptap = (props: ITipTapRichTextEditor) => {
|
|||||||
{editor && <EditorBubbleMenu editor={editor} />}
|
{editor && <EditorBubbleMenu editor={editor} />}
|
||||||
<div className={`${editorContentCustomClassNames}`}>
|
<div className={`${editorContentCustomClassNames}`}>
|
||||||
<EditorContent editor={editor} />
|
<EditorContent editor={editor} />
|
||||||
|
{editor?.isActive("table") && <TableMenu editor={editor} />}
|
||||||
{editor?.isActive("image") && <ImageResizer editor={editor} />}
|
{editor?.isActive("image") && <ImageResizer editor={editor} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,6 +16,7 @@ const TrackImageDeletionPlugin = () =>
|
|||||||
oldState.doc.descendants((oldNode, oldPos) => {
|
oldState.doc.descendants((oldNode, oldPos) => {
|
||||||
if (oldNode.type.name !== 'image') return;
|
if (oldNode.type.name !== 'image') return;
|
||||||
|
|
||||||
|
if (oldPos < 0 || oldPos > newState.doc.content.size) return;
|
||||||
if (!newState.doc.resolve(oldPos).parent) return;
|
if (!newState.doc.resolve(oldPos).parent) return;
|
||||||
const newNode = newState.doc.nodeAt(oldPos);
|
const newNode = newState.doc.nodeAt(oldPos);
|
||||||
|
|
||||||
@ -28,7 +29,6 @@ const TrackImageDeletionPlugin = () =>
|
|||||||
nodeExists = true;
|
nodeExists = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!nodeExists) {
|
if (!nodeExists) {
|
||||||
removedImages.push(oldNode as ProseMirrorNode);
|
removedImages.push(oldNode as ProseMirrorNode);
|
||||||
}
|
}
|
||||||
|
@ -60,8 +60,6 @@ function findPlaceholder(state: EditorState, id: {}) {
|
|||||||
export async function startImageUpload(file: File, view: EditorView, pos: number, workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) {
|
export async function startImageUpload(file: File, view: EditorView, pos: number, workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void) {
|
||||||
if (!file.type.includes("image/")) {
|
if (!file.type.includes("image/")) {
|
||||||
return;
|
return;
|
||||||
} else if (file.size / 1024 / 1024 > 20) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = {};
|
const id = {};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { EditorProps } from "@tiptap/pm/view";
|
import { EditorProps } from "@tiptap/pm/view";
|
||||||
import { startImageUpload } from "./plugins/upload-image";
|
import { startImageUpload } from "./plugins/upload-image";
|
||||||
|
import { findTableAncestor } from "./table-menu";
|
||||||
|
|
||||||
export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void): EditorProps {
|
export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void): EditorProps {
|
||||||
return {
|
return {
|
||||||
@ -18,6 +19,15 @@ export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSu
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
handlePaste: (view, event) => {
|
handlePaste: (view, event) => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const selection: any = window?.getSelection();
|
||||||
|
if (selection.rangeCount !== 0) {
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
if (findTableAncestor(range.startContainer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
event.clipboardData &&
|
event.clipboardData &&
|
||||||
event.clipboardData.files &&
|
event.clipboardData.files &&
|
||||||
@ -32,6 +42,15 @@ export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSu
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
handleDrop: (view, event, _slice, moved) => {
|
handleDrop: (view, event, _slice, moved) => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const selection: any = window?.getSelection();
|
||||||
|
if (selection.rangeCount !== 0) {
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
if (findTableAncestor(range.startContainer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
!moved &&
|
!moved &&
|
||||||
event.dataTransfer &&
|
event.dataTransfer &&
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
MinusSquare,
|
MinusSquare,
|
||||||
CheckSquare,
|
CheckSquare,
|
||||||
ImageIcon,
|
ImageIcon,
|
||||||
|
Table,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { startImageUpload } from "../plugins/upload-image";
|
import { startImageUpload } from "../plugins/upload-image";
|
||||||
import { cn } from "../utils";
|
import { cn } from "../utils";
|
||||||
@ -46,6 +47,9 @@ const Command = Extension.create({
|
|||||||
return [
|
return [
|
||||||
Suggestion({
|
Suggestion({
|
||||||
editor: this.editor,
|
editor: this.editor,
|
||||||
|
allow({ editor }) {
|
||||||
|
return !editor.isActive("table");
|
||||||
|
},
|
||||||
...this.options.suggestion,
|
...this.options.suggestion,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
@ -117,6 +121,15 @@ const getSuggestionItems = (workspaceSlug: string, setIsSubmitting?: (isSubmitti
|
|||||||
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Table",
|
||||||
|
description: "Create a Table",
|
||||||
|
searchTerms: ["table", "cell", "db", "data", "tabular"],
|
||||||
|
icon: <Table size={18} />,
|
||||||
|
command: ({ editor, range }: CommandProps) => {
|
||||||
|
editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Numbered List",
|
title: "Numbered List",
|
||||||
description: "Create a list with numbering.",
|
description: "Create a list with numbering.",
|
||||||
|
96
apps/app/components/tiptap/table-menu/index.tsx
Normal file
96
apps/app/components/tiptap/table-menu/index.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Rows, Columns, ToggleRight } from "lucide-react";
|
||||||
|
import { cn } from "../utils";
|
||||||
|
|
||||||
|
interface TableMenuItem {
|
||||||
|
name: string;
|
||||||
|
command: () => void;
|
||||||
|
icon: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findTableAncestor = (node: Node | null): HTMLTableElement | null => {
|
||||||
|
while (node !== null && node.nodeName !== "TABLE") {
|
||||||
|
node = node.parentNode;
|
||||||
|
}
|
||||||
|
return node as HTMLTableElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TableMenu = ({ editor }: { editor: any }) => {
|
||||||
|
const [tableLocation, setTableLocation] = useState({ bottom: 0, left: 0 });
|
||||||
|
const items: TableMenuItem[] = [
|
||||||
|
{
|
||||||
|
name: "Insert Column right",
|
||||||
|
command: () => editor.chain().focus().addColumnBefore().run(),
|
||||||
|
icon: Columns,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Insert Row below",
|
||||||
|
command: () => editor.chain().focus().addRowAfter().run(),
|
||||||
|
icon: Rows,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Delete Column",
|
||||||
|
command: () => editor.chain().focus().deleteColumn().run(),
|
||||||
|
icon: Columns,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Delete Rows",
|
||||||
|
command: () => editor.chain().focus().deleteRow().run(),
|
||||||
|
icon: Rows,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Toggle Header Row",
|
||||||
|
command: () => editor.chain().focus().toggleHeaderRow().run(),
|
||||||
|
icon: ToggleRight,
|
||||||
|
}
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const handleWindowClick = () => {
|
||||||
|
const selection: any = window?.getSelection();
|
||||||
|
if (selection.rangeCount !== 0) {
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
const tableNode = findTableAncestor(range.startContainer);
|
||||||
|
if (tableNode) {
|
||||||
|
const tableRect = tableNode.getBoundingClientRect();
|
||||||
|
const tableCenter = tableRect.left + tableRect.width / 2;
|
||||||
|
const menuWidth = 45;
|
||||||
|
const menuLeft = tableCenter - menuWidth / 2;
|
||||||
|
const tableBottom = tableRect.bottom;
|
||||||
|
setTableLocation({ bottom: tableBottom, left: menuLeft });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("click", handleWindowClick);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("click", handleWindowClick);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [tableLocation]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="fixed left-1/2 transform -translate-x-1/2 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 shadow-xl"
|
||||||
|
style={{ bottom: `calc(100vh - ${tableLocation.bottom + 45}px)`, left: `${tableLocation.left}px` }}
|
||||||
|
>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={item.command}
|
||||||
|
className="p-2 text-custom-text-200 hover:bg-text-custom-text-100 hover:bg-custom-primary-100/10 active:bg-custom-background-100"
|
||||||
|
title={item.name}
|
||||||
|
>
|
||||||
|
<item.icon
|
||||||
|
className={cn("h-5 w-5 text-lg", {
|
||||||
|
"text-red-600": item.name.includes("Delete"),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
@ -30,11 +30,16 @@
|
|||||||
"@tiptap-pro/extension-unique-id": "^2.1.0",
|
"@tiptap-pro/extension-unique-id": "^2.1.0",
|
||||||
"@tiptap/extension-code-block-lowlight": "^2.0.4",
|
"@tiptap/extension-code-block-lowlight": "^2.0.4",
|
||||||
"@tiptap/extension-color": "^2.0.4",
|
"@tiptap/extension-color": "^2.0.4",
|
||||||
|
"@tiptap/extension-gapcursor": "^2.1.7",
|
||||||
"@tiptap/extension-highlight": "^2.0.4",
|
"@tiptap/extension-highlight": "^2.0.4",
|
||||||
"@tiptap/extension-horizontal-rule": "^2.0.4",
|
"@tiptap/extension-horizontal-rule": "^2.0.4",
|
||||||
"@tiptap/extension-image": "^2.0.4",
|
"@tiptap/extension-image": "^2.0.4",
|
||||||
"@tiptap/extension-link": "^2.0.4",
|
"@tiptap/extension-link": "^2.0.4",
|
||||||
"@tiptap/extension-placeholder": "^2.0.4",
|
"@tiptap/extension-placeholder": "^2.0.4",
|
||||||
|
"@tiptap/extension-table": "^2.1.6",
|
||||||
|
"@tiptap/extension-table-cell": "^2.1.6",
|
||||||
|
"@tiptap/extension-table-header": "^2.1.6",
|
||||||
|
"@tiptap/extension-table-row": "^2.1.6",
|
||||||
"@tiptap/extension-task-item": "^2.0.4",
|
"@tiptap/extension-task-item": "^2.0.4",
|
||||||
"@tiptap/extension-task-list": "^2.0.4",
|
"@tiptap/extension-task-list": "^2.0.4",
|
||||||
"@tiptap/extension-text-style": "^2.0.4",
|
"@tiptap/extension-text-style": "^2.0.4",
|
||||||
|
@ -30,6 +30,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ProseMirror-gapcursor:after {
|
||||||
|
border-top: 1px solid rgb(var(--color-text-100)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Custom TODO list checkboxes – shoutout to this awesome tutorial: https://moderncss.dev/pure-css-custom-checkbox-style/ */
|
/* Custom TODO list checkboxes – shoutout to this awesome tutorial: https://moderncss.dev/pure-css-custom-checkbox-style/ */
|
||||||
|
|
||||||
ul[data-type="taskList"] li > label {
|
ul[data-type="taskList"] li > label {
|
||||||
@ -150,3 +154,78 @@ ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
|||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#tiptap-container {
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
border: 2px solid rgb(var(--color-border-100));
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
min-width: 1em;
|
||||||
|
border: 2px solid rgb(var(--color-border-400));
|
||||||
|
padding: 10px 15px;
|
||||||
|
vertical-align: top;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
background-color: rgb(var(--color-primary-300));
|
||||||
|
}
|
||||||
|
|
||||||
|
td:hover {
|
||||||
|
background-color: rgba(var(--color-primary-300), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedCell:after {
|
||||||
|
z-index: 2;
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
left: 0; right: 0; top: 0; bottom: 0;
|
||||||
|
background-color: rgba(var(--color-primary-300), 0.1);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
right: -2px;
|
||||||
|
top: 0;
|
||||||
|
bottom: -2px;
|
||||||
|
width: 2px;
|
||||||
|
background-color: rgb(var(--color-primary-400));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableWrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-cursor {
|
||||||
|
cursor: ew-resize;
|
||||||
|
cursor: col-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table * p {
|
||||||
|
padding: 0px 1px;
|
||||||
|
margin: 6px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror table * .is-empty::before {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
25
yarn.lock
25
yarn.lock
@ -2282,6 +2282,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.4.tgz#c100a792fd41535ad6382aa8133d0d9c0b2cb2b8"
|
resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.4.tgz#c100a792fd41535ad6382aa8133d0d9c0b2cb2b8"
|
||||||
integrity sha512-VxmKfBQjSSu1mNvHlydA4dJW/zawGKyqmnryiFNcUV9s+/HWLR5i9SiUl4wJM/B8sG8cQxClne5/LrCAeGNYuA==
|
integrity sha512-VxmKfBQjSSu1mNvHlydA4dJW/zawGKyqmnryiFNcUV9s+/HWLR5i9SiUl4wJM/B8sG8cQxClne5/LrCAeGNYuA==
|
||||||
|
|
||||||
|
"@tiptap/extension-gapcursor@^2.1.7":
|
||||||
|
version "2.1.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-2.1.7.tgz#5c0303ba37b4c066f3a3c5835fd0b298f0d3e919"
|
||||||
|
integrity sha512-7eoInzzk1sssoD3RMkwFC86U15Ja4ANve+8wIC+xhN4R3Oe3PY3lFbp1GQxCmaJj8b3rtjNKIQZ2zO0PH58afA==
|
||||||
|
|
||||||
"@tiptap/extension-hard-break@^2.0.4":
|
"@tiptap/extension-hard-break@^2.0.4":
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.0.4.tgz#a4f70fa9a473270f7ec89f20a14b9122af5657bc"
|
resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.0.4.tgz#a4f70fa9a473270f7ec89f20a14b9122af5657bc"
|
||||||
@ -2349,6 +2354,26 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.0.4.tgz#13286dcf8780c55610ed65b24238b8395a5be824"
|
resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-2.0.4.tgz#13286dcf8780c55610ed65b24238b8395a5be824"
|
||||||
integrity sha512-Men7LK6N/Dh3/G4/z2Z9WkDHM2Gxx1XyxYix2ZMf5CnqY37SeDNUnGDqit65pdIN3Y/TQnOZTkKSBilSAtXfJA==
|
integrity sha512-Men7LK6N/Dh3/G4/z2Z9WkDHM2Gxx1XyxYix2ZMf5CnqY37SeDNUnGDqit65pdIN3Y/TQnOZTkKSBilSAtXfJA==
|
||||||
|
|
||||||
|
"@tiptap/extension-table-cell@^2.1.6":
|
||||||
|
version "2.1.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tiptap/extension-table-cell/-/extension-table-cell-2.1.7.tgz#87841144b8368c9611ad46f2134b637e2c33c8bc"
|
||||||
|
integrity sha512-p3e4FNdbKVIjOLHDcXrRtlP6FYPoN6hBUFjq6QZbf5g4+ao2Uq4bQCL+eKbYMxUVERl8g/Qu9X+jG99fVsBDjA==
|
||||||
|
|
||||||
|
"@tiptap/extension-table-header@^2.1.6":
|
||||||
|
version "2.1.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tiptap/extension-table-header/-/extension-table-header-2.1.7.tgz#4757834655e2c4edffa65bc6f6807eb59401e0d8"
|
||||||
|
integrity sha512-rolSUQxFJf/CEj2XBJpeMsLiLHASKrVIzZ2A/AZ9pT6WpFqmECi8r9xyutpJpx21n2Hrk46Y+uGFOKhyvbZ5ug==
|
||||||
|
|
||||||
|
"@tiptap/extension-table-row@^2.1.6":
|
||||||
|
version "2.1.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tiptap/extension-table-row/-/extension-table-row-2.1.7.tgz#f736a61035b271423ef18f65a25f8d1e240263a1"
|
||||||
|
integrity sha512-DBCaEMEuCCoOmr4fdDfp2jnmyWPt672rmCZ5WUuenJ47Cy4Ox2dV+qk5vBZ/yDQcq12WvzLMhdSnAo9pMMMa6Q==
|
||||||
|
|
||||||
|
"@tiptap/extension-table@^2.1.6":
|
||||||
|
version "2.1.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-2.1.7.tgz#c8a83744f60c76ae1e41438b04d5ac9e984afa66"
|
||||||
|
integrity sha512-nlKs35vTQOFW9lfw76S7kJvqVJAfHUlz1muQgWT0gNUlKJYINMXjUIg4Wcx8LTaITCCkp0lMGrLETGRNI+RyxA==
|
||||||
|
|
||||||
"@tiptap/extension-task-item@^2.0.4":
|
"@tiptap/extension-task-item@^2.0.4":
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-task-item/-/extension-task-item-2.0.4.tgz#71f46d35ac629ca10c5c23d4ad170007338a436e"
|
resolved "https://registry.yarnpkg.com/@tiptap/extension-task-item/-/extension-task-item-2.0.4.tgz#71f46d35ac629ca10c5c23d4ad170007338a436e"
|
||||||
|
Loading…
Reference in New Issue
Block a user