Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/app/components/tiptap/bubble-menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,14 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
{...bubbleMenuProps}
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!}
isOpen={isNodeSelectorOpen}
setIsOpen={() => {
setIsNodeSelectorOpen(!isNodeSelectorOpen);
setIsLinkSelectorOpen(false);
}}
/>
/>}
<LinkSelector
editor={props.editor!!}
isOpen={isLinkSelectorOpen}
Expand Down
5 changes: 3 additions & 2 deletions apps/app/components/tiptap/bubble-menu/link-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import isValidHttpUrl from "./utils/link-validator";
interface LinkSelectorProps {
editor: Editor;
isOpen: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>;
setIsOpen: Dispatch<SetStateAction<boolean>>
}


Expand Down Expand Up @@ -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"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault(); onLinkSubmit();
e.preventDefault();
onLinkSubmit();
}
}}
>
Expand Down
15 changes: 14 additions & 1 deletion apps/app/components/tiptap/extensions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
import { lowlight } from "lowlight/lib/core";
import SlashCommand from "../slash-command";
import { InputRule } from "@tiptap/core";
import Gapcursor from '@tiptap/extension-gapcursor'

import ts from "highlight.js/lib/languages/typescript";

import "highlight.js/styles/github-dark.css";
import UniqueID from "@tiptap-pro/extension-unique-id";
import UpdatedImage from "./updated-image";
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);

Expand Down Expand Up @@ -55,7 +60,7 @@ export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSub
codeBlock: false,
horizontalRule: false,
dropcursor: {
color: "#DBEAFE",
color: "rgba(var(--color-text-100))",
width: 2,
},
gapcursor: false,
Expand Down Expand Up @@ -86,6 +91,7 @@ export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSub
class: "mb-6 border-t border-custom-border-300",
},
}),
Gapcursor,
TiptapLink.configure({
protocols: ["http", "https"],
validate: (url) => isValidHttpUrl(url),
Expand All @@ -104,6 +110,9 @@ export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSub
if (node.type.name === "heading") {
return `Heading ${node.attrs.level}`;
}
if (node.type.name === "image" || node.type.name === "table") {
return ""
}

return "Press '/' for commands...";
},
Expand Down Expand Up @@ -134,4 +143,8 @@ export const TiptapExtensions = (workspaceSlug: string, setIsSubmitting?: (isSub
html: true,
transformCopiedText: true,
}),
Table,
TableHeader,
CustomTableCell,
TableRow
];
31 changes: 31 additions & 0 deletions apps/app/components/tiptap/extensions/table/table-cell.ts
Original file line number Diff line number Diff line change
@@ -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];
},
});
7 changes: 7 additions & 0 deletions apps/app/components/tiptap/extensions/table/table-header.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { TableHeader as BaseTableHeader } from "@tiptap/extension-table-header";

const TableHeader = BaseTableHeader.extend({
content: "paragraph"
});

export { TableHeader };
9 changes: 9 additions & 0 deletions apps/app/components/tiptap/extensions/table/table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Table as BaseTable } from "@tiptap/extension-table";

const Table = BaseTable.configure({
resizable: true,
cellMinWidth: 100,
allowTableNodeSelection: true
});

export { Table };
2 changes: 2 additions & 0 deletions apps/app/components/tiptap/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { TiptapExtensions } from "./extensions";
import { TiptapEditorProps } from "./props";
import { useImperativeHandle, useRef } from "react";
import { ImageResizer } from "./extensions/image-resize";
import { TableMenu } from "./table-menu";

export interface ITiptapRichTextEditor {
value: string;
Expand Down Expand Up @@ -91,6 +92,7 @@ const Tiptap = (props: ITiptapRichTextEditor) => {
{editor && <EditorBubbleMenu editor={editor} />}
<div className={`${editorContentCustomClassNames}`}>
<EditorContent editor={editor} />
{editor?.isActive("table") && <TableMenu editor={editor} />}
{editor?.isActive("image") && <ImageResizer editor={editor} />}
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/app/components/tiptap/plugins/delete-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const TrackImageDeletionPlugin = () =>
oldState.doc.descendants((oldNode, oldPos) => {
if (oldNode.type.name !== 'image') return;

if (oldPos < 0 || oldPos > newState.doc.content.size) return;
if (!newState.doc.resolve(oldPos).parent) return;
const newNode = newState.doc.nodeAt(oldPos);

Expand All @@ -28,7 +29,6 @@ const TrackImageDeletionPlugin = () =>
nodeExists = true;
}
});

if (!nodeExists) {
removedImages.push(oldNode as ProseMirrorNode);
}
Expand Down
2 changes: 0 additions & 2 deletions apps/app/components/tiptap/plugins/upload-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
if (!file.type.includes("image/")) {
return;
} else if (file.size / 1024 / 1024 > 20) {
return;
}

const id = {};
Expand Down
19 changes: 19 additions & 0 deletions apps/app/components/tiptap/props.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EditorProps } from "@tiptap/pm/view";
import { startImageUpload } from "./plugins/upload-image";
import { findTableAncestor } from "./table-menu";

export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void): EditorProps {
return {
Expand All @@ -18,6 +19,15 @@ export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSu
},
},
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 (
event.clipboardData &&
event.clipboardData.files &&
Expand All @@ -32,6 +42,15 @@ export function TiptapEditorProps(workspaceSlug: string, setIsSubmitting?: (isSu
return false;
},
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 (
!moved &&
event.dataTransfer &&
Expand Down
13 changes: 13 additions & 0 deletions apps/app/components/tiptap/slash-command/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
MinusSquare,
CheckSquare,
ImageIcon,
Table,
} from "lucide-react";
import { startImageUpload } from "../plugins/upload-image";
import { cn } from "../utils";
Expand Down Expand Up @@ -46,6 +47,9 @@ const Command = Extension.create({
return [
Suggestion({
editor: this.editor,
allow({ editor }) {
return !editor.isActive("table");
},
...this.options.suggestion,
}),
];
Expand Down Expand Up @@ -117,6 +121,15 @@ const getSuggestionItems = (workspaceSlug: string, setIsSubmitting?: (isSubmitti
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",
description: "Create a list with numbering.",
Expand Down
96 changes: 96 additions & 0 deletions apps/app/components/tiptap/table-menu/index.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
};
5 changes: 5 additions & 0 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,16 @@
"@tiptap-pro/extension-unique-id": "^2.1.0",
"@tiptap/extension-code-block-lowlight": "^2.0.4",
"@tiptap/extension-color": "^2.0.4",
"@tiptap/extension-gapcursor": "^2.1.7",
"@tiptap/extension-highlight": "^2.0.4",
"@tiptap/extension-horizontal-rule": "^2.0.4",
"@tiptap/extension-image": "^2.0.4",
"@tiptap/extension-link": "^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-list": "^2.0.4",
"@tiptap/extension-text-style": "^2.0.4",
Expand Down
Loading