diff --git a/packages/editor/core/package.json b/packages/editor/core/package.json
index b6f4df8e591..83f59a1f4a9 100644
--- a/packages/editor/core/package.json
+++ b/packages/editor/core/package.json
@@ -41,6 +41,8 @@
"@tiptap/extension-task-list": "^2.1.7",
"@tiptap/extension-text-style": "^2.1.11",
"@tiptap/extension-underline": "^2.1.7",
+ "@tiptap/prosemirror-tables": "^1.1.4",
+ "jsx-dom-cjs": "^8.0.3",
"@tiptap/pm": "^2.1.7",
"@tiptap/react": "^2.1.7",
"@tiptap/starter-kit": "^2.1.10",
diff --git a/packages/editor/core/src/index.ts b/packages/editor/core/src/index.ts
index 590b17172f5..9c1c292b274 100644
--- a/packages/editor/core/src/index.ts
+++ b/packages/editor/core/src/index.ts
@@ -2,8 +2,11 @@
// import "./styles/tailwind.css";
// import "./styles/editor.css";
+export { isCellSelection } from "./ui/extensions/table/table/utilities/is-cell-selection";
+
// utils
export * from "./lib/utils";
+export * from "./ui/extensions/table/table";
export { startImageUpload } from "./ui/plugins/upload-image";
// components
diff --git a/packages/editor/core/src/ui/components/editor-content.tsx b/packages/editor/core/src/ui/components/editor-content.tsx
index 972184b08cd..d0531da0181 100644
--- a/packages/editor/core/src/ui/components/editor-content.tsx
+++ b/packages/editor/core/src/ui/components/editor-content.tsx
@@ -1,7 +1,6 @@
import { Editor, EditorContent } from "@tiptap/react";
import { ReactNode } from "react";
import { ImageResizer } from "../extensions/image/image-resize";
-import { TableMenu } from "../menus/table-menu";
interface EditorContentProps {
editor: Editor | null;
@@ -10,10 +9,8 @@ interface EditorContentProps {
}
export const EditorContentWrapper = ({ editor, editorContentCustomClassNames = '', children }: EditorContentProps) => (
-
- {/* @ts-ignore */}
+
- {editor?.isEditable &&
}
{(editor?.isActive("image") && editor?.isEditable) &&
}
{children}
diff --git a/packages/editor/core/src/ui/extensions/index.tsx b/packages/editor/core/src/ui/extensions/index.tsx
index 9bf5f0d9b48..a7621ab20cb 100644
--- a/packages/editor/core/src/ui/extensions/index.tsx
+++ b/packages/editor/core/src/ui/extensions/index.tsx
@@ -8,10 +8,10 @@ import TaskList from "@tiptap/extension-task-list";
import { Markdown } from "tiptap-markdown";
import Gapcursor from "@tiptap/extension-gapcursor";
-import { CustomTableCell } from "./table/table-cell";
-import { Table } from "./table";
-import { TableHeader } from "./table/table-header";
-import { TableRow } from "@tiptap/extension-table-row";
+import TableHeader from "./table/table-header/table-header";
+import Table from "./table/table";
+import TableCell from "./table/table-cell/table-cell";
+import TableRow from "./table/table-row/table-row";
import ImageExtension from "./image";
@@ -95,7 +95,7 @@ export const CoreEditorExtensions = (
}),
Table,
TableHeader,
- CustomTableCell,
+ TableCell,
TableRow,
Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, false),
];
diff --git a/packages/editor/core/src/ui/extensions/table/index.ts b/packages/editor/core/src/ui/extensions/table/index.ts
deleted file mode 100644
index 9b727bb51bd..00000000000
--- a/packages/editor/core/src/ui/extensions/table/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { Table as BaseTable } from "@tiptap/extension-table";
-
-const Table = BaseTable.configure({
- resizable: true,
- cellMinWidth: 100,
- allowTableNodeSelection: true,
-});
-
-export { Table };
diff --git a/packages/editor/core/src/ui/extensions/table/table-cell.ts b/packages/editor/core/src/ui/extensions/table/table-cell.ts
deleted file mode 100644
index 643cb8c64a7..00000000000
--- a/packages/editor/core/src/ui/extensions/table/table-cell.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-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];
- },
-});
diff --git a/packages/editor/core/src/ui/extensions/table/table-cell/index.ts b/packages/editor/core/src/ui/extensions/table/table-cell/index.ts
new file mode 100644
index 00000000000..b39fe7104e5
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/table/table-cell/index.ts
@@ -0,0 +1 @@
+export { default as default } from "./table-cell"
diff --git a/packages/editor/core/src/ui/extensions/table/table-cell/table-cell.ts b/packages/editor/core/src/ui/extensions/table/table-cell/table-cell.ts
new file mode 100644
index 00000000000..ac43875dac8
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/table/table-cell/table-cell.ts
@@ -0,0 +1,58 @@
+import { mergeAttributes, Node } from "@tiptap/core"
+
+export interface TableCellOptions {
+ HTMLAttributes: Record
+}
+
+export default Node.create({
+ name: "tableCell",
+
+ addOptions() {
+ return {
+ HTMLAttributes: {}
+ }
+ },
+
+ content: "paragraph+",
+
+ addAttributes() {
+ return {
+ colspan: {
+ default: 1
+ },
+ rowspan: {
+ default: 1
+ },
+ colwidth: {
+ default: null,
+ parseHTML: (element) => {
+ const colwidth = element.getAttribute("colwidth")
+ const value = colwidth ? [parseInt(colwidth, 10)] : null
+
+ return value
+ }
+ },
+ background: {
+ default: "none"
+ }
+ }
+ },
+
+ tableRole: "cell",
+
+ isolating: true,
+
+ parseHTML() {
+ return [{ tag: "td" }]
+ },
+
+ renderHTML({ node, HTMLAttributes }) {
+ return [
+ "td",
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
+ style: `background-color: ${node.attrs.background}`
+ }),
+ 0
+ ]
+ }
+})
diff --git a/packages/editor/core/src/ui/extensions/table/table-header.ts b/packages/editor/core/src/ui/extensions/table/table-header.ts
deleted file mode 100644
index f23aa93ef55..00000000000
--- a/packages/editor/core/src/ui/extensions/table/table-header.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { TableHeader as BaseTableHeader } from "@tiptap/extension-table-header";
-
-const TableHeader = BaseTableHeader.extend({
- content: "paragraph",
-});
-
-export { TableHeader };
diff --git a/packages/editor/core/src/ui/extensions/table/table-header/index.ts b/packages/editor/core/src/ui/extensions/table/table-header/index.ts
new file mode 100644
index 00000000000..57137dedd43
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/table/table-header/index.ts
@@ -0,0 +1 @@
+export { default as default } from "./table-header"
diff --git a/packages/editor/core/src/ui/extensions/table/table-header/table-header.ts b/packages/editor/core/src/ui/extensions/table/table-header/table-header.ts
new file mode 100644
index 00000000000..712ca65f073
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/table/table-header/table-header.ts
@@ -0,0 +1,57 @@
+import { mergeAttributes, Node } from "@tiptap/core"
+
+export interface TableHeaderOptions {
+ HTMLAttributes: Record
+}
+export default Node.create({
+ name: "tableHeader",
+
+ addOptions() {
+ return {
+ HTMLAttributes: {}
+ }
+ },
+
+ content: "paragraph+",
+
+ addAttributes() {
+ return {
+ colspan: {
+ default: 1
+ },
+ rowspan: {
+ default: 1
+ },
+ colwidth: {
+ default: null,
+ parseHTML: (element) => {
+ const colwidth = element.getAttribute("colwidth")
+ const value = colwidth ? [parseInt(colwidth, 10)] : null
+
+ return value
+ }
+ },
+ background: {
+ default: "rgb(var(--color-primary-100))"
+ }
+ }
+ },
+
+ tableRole: "header_cell",
+
+ isolating: true,
+
+ parseHTML() {
+ return [{ tag: "th" }]
+ },
+
+ renderHTML({ node, HTMLAttributes }) {
+ return [
+ "th",
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
+ style: `background-color: ${node.attrs.background}`
+ }),
+ 0
+ ]
+ }
+})
diff --git a/packages/editor/core/src/ui/extensions/table/table-row/index.ts b/packages/editor/core/src/ui/extensions/table/table-row/index.ts
new file mode 100644
index 00000000000..9ecc2c0ae57
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/table/table-row/index.ts
@@ -0,0 +1 @@
+export { default as default } from "./table-row"
diff --git a/packages/editor/core/src/ui/extensions/table/table-row/table-row.ts b/packages/editor/core/src/ui/extensions/table/table-row/table-row.ts
new file mode 100644
index 00000000000..e922e7fa197
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/table/table-row/table-row.ts
@@ -0,0 +1,31 @@
+import { mergeAttributes, Node } from "@tiptap/core"
+
+export interface TableRowOptions {
+ HTMLAttributes: Record
+}
+
+export default Node.create({
+ name: "tableRow",
+
+ addOptions() {
+ return {
+ HTMLAttributes: {}
+ }
+ },
+
+ content: "(tableCell | tableHeader)*",
+
+ tableRole: "row",
+
+ parseHTML() {
+ return [{ tag: "tr" }]
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return [
+ "tr",
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
+ 0
+ ]
+ }
+})
diff --git a/packages/editor/core/src/ui/extensions/table/table/icons.ts b/packages/editor/core/src/ui/extensions/table/table/icons.ts
new file mode 100644
index 00000000000..d3159d4aa8c
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/table/table/icons.ts
@@ -0,0 +1,55 @@
+const icons = {
+ colorPicker: ``,
+ deleteColumn: ``,
+ deleteRow: ``,
+ insertLeftTableIcon: `
+`,
+ insertRightTableIcon: `
+`,
+ insertTopTableIcon: `
+`,
+ insertBottomTableIcon:`
+`,
+};
+
+export default icons;
diff --git a/packages/editor/core/src/ui/extensions/table/table/index.ts b/packages/editor/core/src/ui/extensions/table/table/index.ts
new file mode 100644
index 00000000000..5dbd0f38a45
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/table/table/index.ts
@@ -0,0 +1 @@
+export { default as default } from "./table"
diff --git a/packages/editor/core/src/ui/extensions/table/table/table-controls.ts b/packages/editor/core/src/ui/extensions/table/table/table-controls.ts
new file mode 100644
index 00000000000..f5ec958a463
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/table/table/table-controls.ts
@@ -0,0 +1,117 @@
+import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
+import { findParentNode } from "@tiptap/core";
+import { DecorationSet, Decoration } from "@tiptap/pm/view";
+
+const key = new PluginKey("tableControls");
+
+export function tableControls() {
+ return new Plugin({
+ key,
+ state: {
+ init() {
+ return new TableControlsState();
+ },
+ apply(tr, prev) {
+ return prev.apply(tr);
+ },
+ },
+ props: {
+ handleDOMEvents: {
+ mousemove: (view, event) => {
+ const pluginState = key.getState(view.state);
+
+ if (
+ !(event.target as HTMLElement).closest(".tableWrapper") &&
+ pluginState.values.hoveredTable
+ ) {
+ return view.dispatch(
+ view.state.tr.setMeta(key, {
+ setHoveredTable: null,
+ setHoveredCell: null,
+ }),
+ );
+ }
+
+ const pos = view.posAtCoords({
+ left: event.clientX,
+ top: event.clientY,
+ });
+
+ if (!pos) return;
+
+ const table = findParentNode((node) => node.type.name === "table")(
+ TextSelection.create(view.state.doc, pos.pos),
+ );
+ const cell = findParentNode(
+ (node) =>
+ node.type.name === "tableCell" ||
+ node.type.name === "tableHeader",
+ )(TextSelection.create(view.state.doc, pos.pos));
+
+ if (!table || !cell) return;
+
+ if (pluginState.values.hoveredCell?.pos !== cell.pos) {
+ return view.dispatch(
+ view.state.tr.setMeta(key, {
+ setHoveredTable: table,
+ setHoveredCell: cell,
+ }),
+ );
+ }
+ },
+ },
+ decorations: (state) => {
+ const pluginState = key.getState(state);
+ if (!pluginState) {
+ return null;
+ }
+
+ const { hoveredTable, hoveredCell } = pluginState.values;
+ const docSize = state.doc.content.size;
+ if (hoveredTable && hoveredCell && hoveredTable.pos < docSize && hoveredCell.pos < docSize) {
+ const decorations = [
+ Decoration.node(
+ hoveredTable.pos,
+ hoveredTable.pos + hoveredTable.node.nodeSize,
+ {},
+ {
+ hoveredTable,
+ hoveredCell,
+ },
+ ),
+ ];
+
+ return DecorationSet.create(state.doc, decorations);
+ }
+
+ return null;
+ },
+ },
+ });
+}
+
+class TableControlsState {
+ values;
+
+ constructor(props = {}) {
+ this.values = {
+ hoveredTable: null,
+ hoveredCell: null,
+ ...props,
+ };
+ }
+
+ apply(tr: any) {
+ const actions = tr.getMeta(key);
+
+ if (actions?.setHoveredTable !== undefined) {
+ this.values.hoveredTable = actions.setHoveredTable;
+ }
+
+ if (actions?.setHoveredCell !== undefined) {
+ this.values.hoveredCell = actions.setHoveredCell;
+ }
+
+ return this;
+ }
+}
diff --git a/packages/editor/core/src/ui/extensions/table/table/table-view.tsx b/packages/editor/core/src/ui/extensions/table/table/table-view.tsx
new file mode 100644
index 00000000000..6e3f9318e98
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/table/table/table-view.tsx
@@ -0,0 +1,530 @@
+import { h } from "jsx-dom-cjs";
+import { Node as ProseMirrorNode } from "@tiptap/pm/model";
+import { Decoration, NodeView } from "@tiptap/pm/view";
+import tippy, { Instance, Props } from "tippy.js";
+
+import { Editor } from "@tiptap/core";
+import {
+ CellSelection,
+ TableMap,
+ updateColumnsOnResize,
+} from "@tiptap/prosemirror-tables";
+
+import icons from "./icons";
+
+export function updateColumns(
+ node: ProseMirrorNode,
+ colgroup: HTMLElement,
+ table: HTMLElement,
+ cellMinWidth: number,
+ overrideCol?: number,
+ overrideValue?: any,
+) {
+ let totalWidth = 0;
+ let fixedWidth = true;
+ let nextDOM = colgroup.firstChild as HTMLElement;
+ const row = node.firstChild;
+
+ if (!row) return;
+
+ for (let i = 0, col = 0; i < row.childCount; i += 1) {
+ const { colspan, colwidth } = row.child(i).attrs;
+
+ for (let j = 0; j < colspan; j += 1, col += 1) {
+ const hasWidth =
+ overrideCol === col ? overrideValue : colwidth && colwidth[j];
+ const cssWidth = hasWidth ? `${hasWidth}px` : "";
+
+ totalWidth += hasWidth || cellMinWidth;
+
+ if (!hasWidth) {
+ fixedWidth = false;
+ }
+
+ if (!nextDOM) {
+ colgroup.appendChild(document.createElement("col")).style.width =
+ cssWidth;
+ } else {
+ if (nextDOM.style.width !== cssWidth) {
+ nextDOM.style.width = cssWidth;
+ }
+
+ nextDOM = nextDOM.nextSibling as HTMLElement;
+ }
+ }
+ }
+
+ while (nextDOM) {
+ const after = nextDOM.nextSibling;
+
+ nextDOM.parentNode?.removeChild(nextDOM);
+ nextDOM = after as HTMLElement;
+ }
+
+ if (fixedWidth) {
+ table.style.width = `${totalWidth}px`;
+ table.style.minWidth = "";
+ } else {
+ table.style.width = "";
+ table.style.minWidth = `${totalWidth}px`;
+ }
+}
+
+const defaultTippyOptions: Partial = {
+ allowHTML: true,
+ arrow: false,
+ trigger: "click",
+ animation: "scale-subtle",
+ theme: "light-border no-padding",
+ interactive: true,
+ hideOnClick: true,
+ placement: "right",
+};
+
+function setCellsBackgroundColor(editor: Editor, backgroundColor) {
+ return editor
+ .chain()
+ .focus()
+ .updateAttributes("tableCell", {
+ background: backgroundColor,
+ })
+ .updateAttributes("tableHeader", {
+ background: backgroundColor,
+ })
+ .run();
+}
+
+const columnsToolboxItems = [
+ {
+ label: "Add Column Before",
+ icon: icons.insertLeftTableIcon,
+ action: ({ editor }: { editor: Editor }) =>
+ editor.chain().focus().addColumnBefore().run(),
+ },
+ {
+ label: "Add Column After",
+ icon: icons.insertRightTableIcon,
+ action: ({ editor }: { editor: Editor }) =>
+ editor.chain().focus().addColumnAfter().run(),
+ },
+ {
+ label: "Pick Column Color",
+ icon: icons.colorPicker,
+ action: ({
+ editor,
+ triggerButton,
+ controlsContainer,
+ }: {
+ editor: Editor;
+ triggerButton: HTMLElement;
+ controlsContainer;
+ }) => {
+ createColorPickerToolbox({
+ triggerButton,
+ tippyOptions: {
+ appendTo: controlsContainer,
+ },
+ onSelectColor: (color) => setCellsBackgroundColor(editor, color),
+ });
+ },
+ },
+ {
+ label: "Delete Column",
+ icon: icons.deleteColumn,
+ action: ({ editor }: { editor: Editor }) =>
+ editor.chain().focus().deleteColumn().run(),
+ },
+];
+
+const rowsToolboxItems = [
+ {
+ label: "Add Row Above",
+ icon: icons.insertTopTableIcon,
+ action: ({ editor }: { editor: Editor }) =>
+ editor.chain().focus().addRowBefore().run(),
+ },
+ {
+ label: "Add Row Below",
+ icon: icons.insertBottomTableIcon,
+ action: ({ editor }: { editor: Editor }) =>
+ editor.chain().focus().addRowAfter().run(),
+ },
+ {
+ label: "Pick Row Color",
+ icon: icons.colorPicker,
+ action: ({
+ editor,
+ triggerButton,
+ controlsContainer,
+ }: {
+ editor: Editor;
+ triggerButton: HTMLButtonElement;
+ controlsContainer:
+ | Element
+ | "parent"
+ | ((ref: Element) => Element)
+ | undefined;
+ }) => {
+ createColorPickerToolbox({
+ triggerButton,
+ tippyOptions: {
+ appendTo: controlsContainer,
+ },
+ onSelectColor: (color) => setCellsBackgroundColor(editor, color),
+ });
+ },
+ },
+ {
+ label: "Delete Row",
+ icon: icons.deleteRow,
+ action: ({ editor }: { editor: Editor }) =>
+ editor.chain().focus().deleteRow().run(),
+ },
+];
+
+function createToolbox({
+ triggerButton,
+ items,
+ tippyOptions,
+ onClickItem,
+}: {
+ triggerButton: HTMLElement;
+ items: { icon: string; label: string }[];
+ tippyOptions: any;
+ onClickItem: any;
+}): Instance {
+ const toolbox = tippy(triggerButton, {
+ content: h(
+ "div",
+ { className: "tableToolbox" },
+ items.map((item) =>
+ h(
+ "div",
+ {
+ className: "toolboxItem",
+ onClick() {
+ onClickItem(item);
+ },
+ },
+ [
+ h("div", {
+ className: "iconContainer",
+ innerHTML: item.icon,
+ }),
+ h("div", { className: "label" }, item.label),
+ ],
+ ),
+ ),
+ ),
+ ...tippyOptions,
+ });
+
+ return Array.isArray(toolbox) ? toolbox[0] : toolbox;
+}
+
+function createColorPickerToolbox({
+ triggerButton,
+ tippyOptions,
+ onSelectColor = () => {},
+}: {
+ triggerButton: HTMLElement;
+ tippyOptions: Partial;
+ onSelectColor?: (color: string) => void;
+}) {
+ const items = {
+ Default: "rgb(var(--color-primary-100))",
+ Orange: "#FFE5D1",
+ Grey: "#F1F1F1",
+ Yellow: "#FEF3C7",
+ Green: "#DCFCE7",
+ Red: "#FFDDDD",
+ Blue: "#D9E4FF",
+ Pink: "#FFE8FA",
+ Purple: "#E8DAFB",
+ };
+
+ const colorPicker = tippy(triggerButton, {
+ ...defaultTippyOptions,
+ content: h(
+ "div",
+ { className: "tableColorPickerToolbox" },
+ Object.entries(items).map(([key, value]) =>
+ h(
+ "div",
+ {
+ className: "toolboxItem",
+ onClick: () => {
+ onSelectColor(value);
+ colorPicker.hide();
+ },
+ },
+ [
+ h("div", {
+ className: "colorContainer",
+ style: {
+ backgroundColor: value,
+ },
+ }),
+ h(
+ "div",
+ {
+ className: "label",
+ },
+ key,
+ ),
+ ],
+ ),
+ ),
+ ),
+ onHidden: (instance) => {
+ instance.destroy();
+ },
+ showOnCreate: true,
+ ...tippyOptions,
+ });
+
+ return colorPicker;
+}
+
+export class TableView implements NodeView {
+ node: ProseMirrorNode;
+ cellMinWidth: number;
+ decorations: Decoration[];
+ editor: Editor;
+ getPos: () => number;
+ hoveredCell;
+ map: TableMap;
+ root: HTMLElement;
+ table: HTMLElement;
+ colgroup: HTMLElement;
+ tbody: HTMLElement;
+ rowsControl?: HTMLElement;
+ columnsControl?: HTMLElement;
+ columnsToolbox?: Instance;
+ rowsToolbox?: Instance;
+ controls?: HTMLElement;
+
+ get dom() {
+ return this.root;
+ }
+
+ get contentDOM() {
+ return this.tbody;
+ }
+
+ constructor(
+ node: ProseMirrorNode,
+ cellMinWidth: number,
+ decorations: Decoration[],
+ editor: Editor,
+ getPos: () => number,
+ ) {
+ this.node = node;
+ this.cellMinWidth = cellMinWidth;
+ this.decorations = decorations;
+ this.editor = editor;
+ this.getPos = getPos;
+ this.hoveredCell = null;
+ this.map = TableMap.get(node);
+
+ if (editor.isEditable) {
+ this.rowsControl = h(
+ "div",
+ { className: "rowsControl" },
+ h("button", {
+ onClick: () => this.selectRow(),
+ }),
+ );
+
+ this.columnsControl = h(
+ "div",
+ { className: "columnsControl" },
+ h("button", {
+ onClick: () => this.selectColumn(),
+ }),
+ );
+
+ this.controls = h(
+ "div",
+ { className: "tableControls", contentEditable: "false" },
+ this.rowsControl,
+ this.columnsControl,
+ );
+
+ this.columnsToolbox = createToolbox({
+ triggerButton: this.columnsControl.querySelector("button"),
+ items: columnsToolboxItems,
+ tippyOptions: {
+ ...defaultTippyOptions,
+ appendTo: this.controls,
+ },
+ onClickItem: (item) => {
+ item.action({
+ editor: this.editor,
+ triggerButton: this.columnsControl?.firstElementChild,
+ controlsContainer: this.controls,
+ });
+ this.columnsToolbox?.hide();
+ },
+ });
+
+ this.rowsToolbox = createToolbox({
+ triggerButton: this.rowsControl.firstElementChild,
+ items: rowsToolboxItems,
+ tippyOptions: {
+ ...defaultTippyOptions,
+ appendTo: this.controls,
+ },
+ onClickItem: (item) => {
+ item.action({
+ editor: this.editor,
+ triggerButton: this.rowsControl?.firstElementChild,
+ controlsContainer: this.controls,
+ });
+ this.rowsToolbox?.hide();
+ },
+ });
+ }
+
+ // Table
+
+ this.colgroup = h(
+ "colgroup",
+ null,
+ Array.from({ length: this.map.width }, () => 1).map(() => h("col")),
+ );
+ this.tbody = h("tbody");
+ this.table = h("table", null, this.colgroup, this.tbody);
+
+ this.root = h(
+ "div",
+ {
+ className: "tableWrapper controls--disabled",
+ },
+ this.controls,
+ this.table,
+ );
+
+ this.render();
+ }
+
+ update(node: ProseMirrorNode, decorations) {
+ if (node.type !== this.node.type) {
+ return false;
+ }
+
+ this.node = node;
+ this.decorations = decorations;
+ this.map = TableMap.get(this.node);
+
+ if (this.editor.isEditable) {
+ this.updateControls();
+ }
+
+ this.render();
+
+ return true;
+ }
+
+ render() {
+ if (this.colgroup.children.length !== this.map.width) {
+ const cols = Array.from({ length: this.map.width }, () => 1).map(() =>
+ h("col"),
+ );
+ this.colgroup.replaceChildren(...cols);
+ }
+
+ updateColumnsOnResize(
+ this.node,
+ this.colgroup,
+ this.table,
+ this.cellMinWidth,
+ );
+ }
+
+ ignoreMutation() {
+ return true;
+ }
+
+ updateControls() {
+ const { hoveredTable: table, hoveredCell: cell } = Object.values(
+ this.decorations,
+ ).reduce(
+ (acc, curr) => {
+ if (curr.spec.hoveredCell !== undefined) {
+ acc["hoveredCell"] = curr.spec.hoveredCell;
+ }
+
+ if (curr.spec.hoveredTable !== undefined) {
+ acc["hoveredTable"] = curr.spec.hoveredTable;
+ }
+ return acc;
+ },
+ {} as Record,
+ ) as any;
+
+ if (table === undefined || cell === undefined) {
+ return this.root.classList.add("controls--disabled");
+ }
+
+ this.root.classList.remove("controls--disabled");
+ this.hoveredCell = cell;
+
+ const cellDom = this.editor.view.nodeDOM(cell.pos) as HTMLElement;
+
+ const tableRect = this.table.getBoundingClientRect();
+ const cellRect = cellDom.getBoundingClientRect();
+
+ this.columnsControl.style.left = `${
+ cellRect.left - tableRect.left - this.table.parentElement!.scrollLeft
+ }px`;
+ this.columnsControl.style.width = `${cellRect.width}px`;
+
+ this.rowsControl.style.top = `${cellRect.top - tableRect.top}px`;
+ this.rowsControl.style.height = `${cellRect.height}px`;
+ }
+
+ selectColumn() {
+ if (!this.hoveredCell) return;
+
+ const colIndex = this.map.colCount(
+ this.hoveredCell.pos - (this.getPos() + 1),
+ );
+ const anchorCellPos = this.hoveredCell.pos;
+ const headCellPos =
+ this.map.map[colIndex + this.map.width * (this.map.height - 1)] +
+ (this.getPos() + 1);
+
+ const cellSelection = CellSelection.create(
+ this.editor.view.state.doc,
+ anchorCellPos,
+ headCellPos,
+ );
+ this.editor.view.dispatch(
+ // @ts-ignore
+ this.editor.state.tr.setSelection(cellSelection),
+ );
+ }
+
+ selectRow() {
+ if (!this.hoveredCell) return;
+
+ const anchorCellPos = this.hoveredCell.pos;
+ const anchorCellIndex = this.map.map.indexOf(
+ anchorCellPos - (this.getPos() + 1),
+ );
+ const headCellPos =
+ this.map.map[anchorCellIndex + (this.map.width - 1)] +
+ (this.getPos() + 1);
+
+ const cellSelection = CellSelection.create(
+ this.editor.state.doc,
+ anchorCellPos,
+ headCellPos,
+ );
+ this.editor.view.dispatch(
+ // @ts-ignore
+ this.editor.view.state.tr.setSelection(cellSelection),
+ );
+ }
+}
diff --git a/packages/editor/core/src/ui/extensions/table/table/table.ts b/packages/editor/core/src/ui/extensions/table/table/table.ts
new file mode 100644
index 00000000000..eab3cad92e0
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/table/table/table.ts
@@ -0,0 +1,298 @@
+import { TextSelection } from "@tiptap/pm/state"
+
+import { callOrReturn, getExtensionField, mergeAttributes, Node, ParentConfig } from "@tiptap/core"
+import {
+ addColumnAfter,
+ addColumnBefore,
+ addRowAfter,
+ addRowBefore,
+ CellSelection,
+ columnResizing,
+ deleteColumn,
+ deleteRow,
+ deleteTable,
+ fixTables,
+ goToNextCell,
+ mergeCells,
+ setCellAttr,
+ splitCell,
+ tableEditing,
+ toggleHeader,
+ toggleHeaderCell
+} from "@tiptap/prosemirror-tables"
+
+import { tableControls } from "./table-controls"
+import { TableView } from "./table-view"
+import { createTable } from "./utilities/create-table"
+import { deleteTableWhenAllCellsSelected } from "./utilities/delete-table-when-all-cells-selected"
+
+export interface TableOptions {
+ HTMLAttributes: Record
+ resizable: boolean
+ handleWidth: number
+ cellMinWidth: number
+ lastColumnResizable: boolean
+ allowTableNodeSelection: boolean
+}
+
+declare module "@tiptap/core" {
+ interface Commands {
+ table: {
+ insertTable: (options?: {
+ rows?: number
+ cols?: number
+ withHeaderRow?: boolean
+ }) => ReturnType
+ addColumnBefore: () => ReturnType
+ addColumnAfter: () => ReturnType
+ deleteColumn: () => ReturnType
+ addRowBefore: () => ReturnType
+ addRowAfter: () => ReturnType
+ deleteRow: () => ReturnType
+ deleteTable: () => ReturnType
+ mergeCells: () => ReturnType
+ splitCell: () => ReturnType
+ toggleHeaderColumn: () => ReturnType
+ toggleHeaderRow: () => ReturnType
+ toggleHeaderCell: () => ReturnType
+ mergeOrSplit: () => ReturnType
+ setCellAttribute: (name: string, value: any) => ReturnType
+ goToNextCell: () => ReturnType
+ goToPreviousCell: () => ReturnType
+ fixTables: () => ReturnType
+ setCellSelection: (position: {
+ anchorCell: number
+ headCell?: number
+ }) => ReturnType
+ }
+ }
+
+ interface NodeConfig {
+ tableRole?:
+ | string
+ | ((this: {
+ name: string
+ options: Options
+ storage: Storage
+ parent: ParentConfig>["tableRole"]
+ }) => string)
+ }
+}
+
+export default Node.create({
+ name: "table",
+
+ addOptions() {
+ return {
+ HTMLAttributes: {},
+ resizable: true,
+ handleWidth: 5,
+ cellMinWidth: 100,
+ lastColumnResizable: true,
+ allowTableNodeSelection: true
+ }
+ },
+
+ content: "tableRow+",
+
+ tableRole: "table",
+
+ isolating: true,
+
+ group: "block",
+
+ allowGapCursor: false,
+
+ parseHTML() {
+ return [{ tag: "table" }]
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return [
+ "table",
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
+ ["tbody", 0]
+ ]
+ },
+
+ addCommands() {
+ return {
+ insertTable:
+ ({ rows = 3, cols = 3, withHeaderRow = true} = {}) =>
+ ({ tr, dispatch, editor }) => {
+ const node = createTable(
+ editor.schema,
+ rows,
+ cols,
+ withHeaderRow
+ )
+
+ if (dispatch) {
+ const offset = tr.selection.anchor + 1
+
+ tr.replaceSelectionWith(node)
+ .scrollIntoView()
+ .setSelection(
+ TextSelection.near(tr.doc.resolve(offset))
+ )
+ }
+
+ return true
+ },
+ addColumnBefore:
+ () =>
+ ({ state, dispatch }) => addColumnBefore(state, dispatch),
+ addColumnAfter:
+ () =>
+ ({ state, dispatch }) => addColumnAfter(state, dispatch),
+ deleteColumn:
+ () =>
+ ({ state, dispatch }) => deleteColumn(state, dispatch),
+ addRowBefore:
+ () =>
+ ({ state, dispatch }) => addRowBefore(state, dispatch),
+ addRowAfter:
+ () =>
+ ({ state, dispatch }) => addRowAfter(state, dispatch),
+ deleteRow:
+ () =>
+ ({ state, dispatch }) => deleteRow(state, dispatch),
+ deleteTable:
+ () =>
+ ({ state, dispatch }) => deleteTable(state, dispatch),
+ mergeCells:
+ () =>
+ ({ state, dispatch }) => mergeCells(state, dispatch),
+ splitCell:
+ () =>
+ ({ state, dispatch }) => splitCell(state, dispatch),
+ toggleHeaderColumn:
+ () =>
+ ({ state, dispatch }) => toggleHeader("column")(state, dispatch),
+ toggleHeaderRow:
+ () =>
+ ({ state, dispatch }) => toggleHeader("row")(state, dispatch),
+ toggleHeaderCell:
+ () =>
+ ({ state, dispatch }) => toggleHeaderCell(state, dispatch),
+ mergeOrSplit:
+ () =>
+ ({ state, dispatch }) => {
+ if (mergeCells(state, dispatch)) {
+ return true
+ }
+
+ return splitCell(state, dispatch)
+ },
+ setCellAttribute:
+ (name, value) =>
+ ({ state, dispatch }) => setCellAttr(name, value)(state, dispatch),
+ goToNextCell:
+ () =>
+ ({ state, dispatch }) => goToNextCell(1)(state, dispatch),
+ goToPreviousCell:
+ () =>
+ ({ state, dispatch }) => goToNextCell(-1)(state, dispatch),
+ fixTables:
+ () =>
+ ({ state, dispatch }) => {
+ if (dispatch) {
+ fixTables(state)
+ }
+
+ return true
+ },
+ setCellSelection:
+ (position) =>
+ ({ tr, dispatch }) => {
+ if (dispatch) {
+ const selection = CellSelection.create(
+ tr.doc,
+ position.anchorCell,
+ position.headCell
+ )
+
+ // @ts-ignore
+ tr.setSelection(selection)
+ }
+
+ return true
+ }
+ }
+ },
+
+ addKeyboardShortcuts() {
+ return {
+ Tab: () => {
+ if (this.editor.commands.goToNextCell()) {
+ return true
+ }
+
+ if (!this.editor.can().addRowAfter()) {
+ return false
+ }
+
+ return this.editor.chain().addRowAfter().goToNextCell().run()
+ },
+ "Shift-Tab": () => this.editor.commands.goToPreviousCell(),
+ Backspace: deleteTableWhenAllCellsSelected,
+ "Mod-Backspace": deleteTableWhenAllCellsSelected,
+ Delete: deleteTableWhenAllCellsSelected,
+ "Mod-Delete": deleteTableWhenAllCellsSelected
+ }
+ },
+
+ addNodeView() {
+ return ({ editor, getPos, node, decorations }) => {
+ const { cellMinWidth } = this.options
+
+ return new TableView(
+ node,
+ cellMinWidth,
+ decorations,
+ editor,
+ getPos as () => number
+ )
+ }
+ },
+
+ addProseMirrorPlugins() {
+ const isResizable = this.options.resizable && this.editor.isEditable
+
+ const plugins = [
+ tableEditing({
+ allowTableNodeSelection: this.options.allowTableNodeSelection
+ }),
+ tableControls()
+ ]
+
+ if (isResizable) {
+ plugins.unshift(
+ columnResizing({
+ handleWidth: this.options.handleWidth,
+ cellMinWidth: this.options.cellMinWidth,
+ // View: TableView,
+
+ // @ts-ignore
+ lastColumnResizable: this.options.lastColumnResizable
+ })
+ )
+ }
+
+ return plugins
+ },
+
+ extendNodeSchema(extension) {
+ const context = {
+ name: extension.name,
+ options: extension.options,
+ storage: extension.storage
+ }
+
+ return {
+ tableRole: callOrReturn(
+ getExtensionField(extension, "tableRole", context)
+ )
+ }
+ }
+})
diff --git a/packages/editor/core/src/ui/extensions/table/table/utilities/create-cell.ts b/packages/editor/core/src/ui/extensions/table/table/utilities/create-cell.ts
new file mode 100644
index 00000000000..a3d7f2da814
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/table/table/utilities/create-cell.ts
@@ -0,0 +1,12 @@
+import { Fragment, Node as ProsemirrorNode, NodeType } from "prosemirror-model"
+
+export function createCell(
+ cellType: NodeType,
+ cellContent?: Fragment | ProsemirrorNode | Array
+): ProsemirrorNode | null | undefined {
+ if (cellContent) {
+ return cellType.createChecked(null, cellContent)
+ }
+
+ return cellType.createAndFill()
+}
diff --git a/packages/editor/core/src/ui/extensions/table/table/utilities/create-table.ts b/packages/editor/core/src/ui/extensions/table/table/utilities/create-table.ts
new file mode 100644
index 00000000000..75bf7cb41db
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/table/table/utilities/create-table.ts
@@ -0,0 +1,45 @@
+import { Fragment, Node as ProsemirrorNode, Schema } from "@tiptap/pm/model"
+
+import { createCell } from "./create-cell"
+import { getTableNodeTypes } from "./get-table-node-types"
+
+export function createTable(
+ schema: Schema,
+ rowsCount: number,
+ colsCount: number,
+ withHeaderRow: boolean,
+ cellContent?: Fragment | ProsemirrorNode | Array
+): ProsemirrorNode {
+ const types = getTableNodeTypes(schema)
+ const headerCells: ProsemirrorNode[] = []
+ const cells: ProsemirrorNode[] = []
+
+ for (let index = 0; index < colsCount; index += 1) {
+ const cell = createCell(types.cell, cellContent)
+
+ if (cell) {
+ cells.push(cell)
+ }
+
+ if (withHeaderRow) {
+ const headerCell = createCell(types.header_cell, cellContent)
+
+ if (headerCell) {
+ headerCells.push(headerCell)
+ }
+ }
+ }
+
+ const rows: ProsemirrorNode[] = []
+
+ for (let index = 0; index < rowsCount; index += 1) {
+ rows.push(
+ types.row.createChecked(
+ null,
+ withHeaderRow && index === 0 ? headerCells : cells
+ )
+ )
+ }
+
+ return types.table.createChecked(null, rows)
+}
diff --git a/packages/editor/core/src/ui/extensions/table/table/utilities/delete-table-when-all-cells-selected.ts b/packages/editor/core/src/ui/extensions/table/table/utilities/delete-table-when-all-cells-selected.ts
new file mode 100644
index 00000000000..dcb20b3239f
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/table/table/utilities/delete-table-when-all-cells-selected.ts
@@ -0,0 +1,39 @@
+import { findParentNodeClosestToPos, KeyboardShortcutCommand } from "@tiptap/core"
+
+import { isCellSelection } from "./is-cell-selection"
+
+export const deleteTableWhenAllCellsSelected: KeyboardShortcutCommand = ({
+ editor
+}) => {
+ const { selection } = editor.state
+
+ if (!isCellSelection(selection)) {
+ return false
+ }
+
+ let cellCount = 0
+ const table = findParentNodeClosestToPos(
+ selection.ranges[0].$from,
+ (node) => node.type.name === "table"
+ )
+
+ table?.node.descendants((node) => {
+ if (node.type.name === "table") {
+ return false
+ }
+
+ if (["tableCell", "tableHeader"].includes(node.type.name)) {
+ cellCount += 1
+ }
+ })
+
+ const allCellsSelected = cellCount === selection.ranges.length
+
+ if (!allCellsSelected) {
+ return false
+ }
+
+ editor.commands.deleteTable()
+
+ return true
+}
diff --git a/packages/editor/core/src/ui/extensions/table/table/utilities/get-table-node-types.ts b/packages/editor/core/src/ui/extensions/table/table/utilities/get-table-node-types.ts
new file mode 100644
index 00000000000..293878cb0a4
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/table/table/utilities/get-table-node-types.ts
@@ -0,0 +1,21 @@
+import { NodeType, Schema } from "prosemirror-model"
+
+export function getTableNodeTypes(schema: Schema): { [key: string]: NodeType } {
+ if (schema.cached.tableNodeTypes) {
+ return schema.cached.tableNodeTypes
+ }
+
+ const roles: { [key: string]: NodeType } = {}
+
+ Object.keys(schema.nodes).forEach((type) => {
+ const nodeType = schema.nodes[type]
+
+ if (nodeType.spec.tableRole) {
+ roles[nodeType.spec.tableRole] = nodeType
+ }
+ })
+
+ schema.cached.tableNodeTypes = roles
+
+ return roles
+}
diff --git a/packages/editor/core/src/ui/extensions/table/table/utilities/is-cell-selection.ts b/packages/editor/core/src/ui/extensions/table/table/utilities/is-cell-selection.ts
new file mode 100644
index 00000000000..3c36bf055e2
--- /dev/null
+++ b/packages/editor/core/src/ui/extensions/table/table/utilities/is-cell-selection.ts
@@ -0,0 +1,5 @@
+import { CellSelection } from "@tiptap/prosemirror-tables"
+
+export function isCellSelection(value: unknown): value is CellSelection {
+ return value instanceof CellSelection
+}
diff --git a/packages/editor/core/src/ui/mentions/custom.tsx b/packages/editor/core/src/ui/mentions/custom.tsx
index 6ffb97af1a7..c3bfa370310 100644
--- a/packages/editor/core/src/ui/mentions/custom.tsx
+++ b/packages/editor/core/src/ui/mentions/custom.tsx
@@ -9,7 +9,6 @@ export interface CustomMentionOptions extends MentionOptions {
}
export const CustomMention = Mention.extend({
-
addAttributes() {
return {
id: {
@@ -54,6 +53,3 @@ export const CustomMention = Mention.extend({
return ['mention-component', mergeAttributes(HTMLAttributes)]
},
})
-
-
-
diff --git a/packages/editor/core/src/ui/menus/table-menu/index.tsx b/packages/editor/core/src/ui/menus/table-menu/index.tsx
deleted file mode 100644
index c115196db74..00000000000
--- a/packages/editor/core/src/ui/menus/table-menu/index.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-import { useState, useEffect } from "react";
-import { Rows, Columns, ToggleRight } from "lucide-react";
-import InsertLeftTableIcon from "./InsertLeftTableIcon";
-import InsertRightTableIcon from "./InsertRightTableIcon";
-import InsertTopTableIcon from "./InsertTopTableIcon";
-import InsertBottomTableIcon from "./InsertBottomTableIcon";
-import { cn, findTableAncestor } from "../../../lib/utils";
-import { Tooltip } from "./tooltip";
-
-interface TableMenuItem {
- command: () => void;
- icon: any;
- key: string;
- name: string;
-}
-
-
-
-export const TableMenu = ({ editor }: { editor: any }) => {
- const [tableLocation, setTableLocation] = useState({ bottom: 0, left: 0 });
- const isOpen = editor?.isActive("table");
-
- const items: TableMenuItem[] = [
- {
- command: () => editor.chain().focus().addColumnBefore().run(),
- icon: InsertLeftTableIcon,
- key: "insert-column-left",
- name: "Insert 1 column left",
- },
- {
- command: () => editor.chain().focus().addColumnAfter().run(),
- icon: InsertRightTableIcon,
- key: "insert-column-right",
- name: "Insert 1 column right",
- },
- {
- command: () => editor.chain().focus().addRowBefore().run(),
- icon: InsertTopTableIcon,
- key: "insert-row-above",
- name: "Insert 1 row above",
- },
- {
- command: () => editor.chain().focus().addRowAfter().run(),
- icon: InsertBottomTableIcon,
- key: "insert-row-below",
- name: "Insert 1 row below",
- },
- {
- command: () => editor.chain().focus().deleteColumn().run(),
- icon: Columns,
- key: "delete-column",
- name: "Delete column",
- },
- {
- command: () => editor.chain().focus().deleteRow().run(),
- icon: Rows,
- key: "delete-row",
- name: "Delete row",
- },
- {
- command: () => editor.chain().focus().toggleHeaderRow().run(),
- icon: ToggleRight,
- key: "toggle-header-row",
- name: "Toggle header row",
- },
- ];
-
- useEffect(() => {
- if (!window) return;
-
- 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, editor]);
-
- return (
-
- {items.map((item, index) => (
-
-
-
- ))}
-
- );
-};
diff --git a/packages/editor/core/src/ui/read-only/extensions.tsx b/packages/editor/core/src/ui/read-only/extensions.tsx
index 6b326f951c3..8901d34c517 100644
--- a/packages/editor/core/src/ui/read-only/extensions.tsx
+++ b/packages/editor/core/src/ui/read-only/extensions.tsx
@@ -8,10 +8,10 @@ import TaskList from "@tiptap/extension-task-list";
import { Markdown } from "tiptap-markdown";
import Gapcursor from "@tiptap/extension-gapcursor";
-import { CustomTableCell } from "../extensions/table/table-cell";
-import { Table } from "../extensions/table";
-import { TableHeader } from "../extensions/table/table-header";
-import { TableRow } from "@tiptap/extension-table-row";
+import TableHeader from "../extensions/table/table-header/table-header";
+import Table from "../extensions/table/table";
+import TableCell from "../extensions/table/table-cell/table-cell";
+import TableRow from "../extensions/table/table-row/table-row";
import ReadOnlyImageExtension from "../extensions/image/read-only-image";
import { isValidHttpUrl } from "../../lib/utils";
@@ -91,7 +91,7 @@ export const CoreReadOnlyEditorExtensions = (
}),
Table,
TableHeader,
- CustomTableCell,
+ TableCell,
TableRow,
Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, true),
];
diff --git a/packages/editor/rich-text-editor/src/ui/extensions/slash-command.tsx b/packages/editor/rich-text-editor/src/ui/extensions/slash-command.tsx
index e00585dd82a..bab13304a54 100644
--- a/packages/editor/rich-text-editor/src/ui/extensions/slash-command.tsx
+++ b/packages/editor/rich-text-editor/src/ui/extensions/slash-command.tsx
@@ -1,4 +1,11 @@
-import { useState, useEffect, useCallback, ReactNode, useRef, useLayoutEffect } from "react";
+import {
+ useState,
+ useEffect,
+ useCallback,
+ ReactNode,
+ useRef,
+ useLayoutEffect,
+} from "react";
import { Editor, Range, Extension } from "@tiptap/core";
import Suggestion from "@tiptap/suggestion";
import { ReactRenderer } from "@tiptap/react";
@@ -18,7 +25,18 @@ import {
Table,
} from "lucide-react";
import { UploadImage } from "../";
-import { cn, insertTableCommand, toggleBlockquote, toggleBulletList, toggleOrderedList, toggleTaskList, insertImageCommand, toggleHeadingOne, toggleHeadingTwo, toggleHeadingThree } from "@plane/editor-core";
+import {
+ cn,
+ insertTableCommand,
+ toggleBlockquote,
+ toggleBulletList,
+ toggleOrderedList,
+ toggleTaskList,
+ insertImageCommand,
+ toggleHeadingOne,
+ toggleHeadingTwo,
+ toggleHeadingThree,
+} from "@plane/editor-core";
interface CommandItemProps {
title: string;
@@ -37,7 +55,15 @@ const Command = Extension.create({
return {
suggestion: {
char: "/",
- command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => {
+ command: ({
+ editor,
+ range,
+ props,
+ }: {
+ editor: Editor;
+ range: Range;
+ props: any;
+ }) => {
props.command({ editor, range });
},
},
@@ -59,127 +85,135 @@ const Command = Extension.create({
const getSuggestionItems =
(
uploadFile: UploadImage,
- setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
+ setIsSubmitting?: (
+ isSubmitting: "submitting" | "submitted" | "saved",
+ ) => void,
) =>
- ({ query }: { query: string }) =>
- [
- {
- title: "Text",
- description: "Just start typing with plain text.",
- searchTerms: ["p", "paragraph"],
- icon: ,
- command: ({ editor, range }: CommandProps) => {
- editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
- },
- },
- {
- title: "Heading 1",
- description: "Big section heading.",
- searchTerms: ["title", "big", "large"],
- icon: ,
- command: ({ editor, range }: CommandProps) => {
- toggleHeadingOne(editor, range);
- },
+ ({ query }: { query: string }) =>
+ [
+ {
+ title: "Text",
+ description: "Just start typing with plain text.",
+ searchTerms: ["p", "paragraph"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor
+ .chain()
+ .focus()
+ .deleteRange(range)
+ .toggleNode("paragraph", "paragraph")
+ .run();
},
- {
- title: "Heading 2",
- description: "Medium section heading.",
- searchTerms: ["subtitle", "medium"],
- icon: ,
- command: ({ editor, range }: CommandProps) => {
- toggleHeadingTwo(editor, range);
- },
- },
- {
- title: "Heading 3",
- description: "Small section heading.",
- searchTerms: ["subtitle", "small"],
- icon: ,
- command: ({ editor, range }: CommandProps) => {
- toggleHeadingThree(editor, range);
- },
+ },
+ {
+ title: "Heading 1",
+ description: "Big section heading.",
+ searchTerms: ["title", "big", "large"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ toggleHeadingOne(editor, range);
},
- {
- title: "To-do List",
- description: "Track tasks with a to-do list.",
- searchTerms: ["todo", "task", "list", "check", "checkbox"],
- icon: ,
- command: ({ editor, range }: CommandProps) => {
- toggleTaskList(editor, range)
- },
+ },
+ {
+ title: "Heading 2",
+ description: "Medium section heading.",
+ searchTerms: ["subtitle", "medium"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ toggleHeadingTwo(editor, range);
},
- {
- title: "Bullet List",
- description: "Create a simple bullet list.",
- searchTerms: ["unordered", "point"],
- icon:
,
- command: ({ editor, range }: CommandProps) => {
- toggleBulletList(editor, range);
- },
+ },
+ {
+ title: "Heading 3",
+ description: "Small section heading.",
+ searchTerms: ["subtitle", "small"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ toggleHeadingThree(editor, range);
},
- {
- title: "Divider",
- description: "Visually divide blocks",
- searchTerms: ["line", "divider", "horizontal", "rule", "separate"],
- icon: ,
- command: ({ editor, range }: CommandProps) => {
- editor.chain().focus().deleteRange(range).setHorizontalRule().run();
- },
+ },
+ {
+ title: "To-do List",
+ description: "Track tasks with a to-do list.",
+ searchTerms: ["todo", "task", "list", "check", "checkbox"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ toggleTaskList(editor, range);
},
- {
- title: "Table",
- description: "Create a Table",
- searchTerms: ["table", "cell", "db", "data", "tabular"],
- icon: ,
- command: ({ editor, range }: CommandProps) => {
- insertTableCommand(editor, range);
- },
+ },
+ {
+ title: "Bullet List",
+ description: "Create a simple bullet list.",
+ searchTerms: ["unordered", "point"],
+ icon:
,
+ command: ({ editor, range }: CommandProps) => {
+ toggleBulletList(editor, range);
},
- {
- title: "Numbered List",
- description: "Create a list with numbering.",
- searchTerms: ["ordered"],
- icon: ,
- command: ({ editor, range }: CommandProps) => {
- toggleOrderedList(editor, range)
- },
+ },
+ {
+ title: "Divider",
+ description: "Visually divide blocks",
+ searchTerms: ["line", "divider", "horizontal", "rule", "separate"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ editor.chain().focus().deleteRange(range).setHorizontalRule().run();
},
- {
- title: "Quote",
- description: "Capture a quote.",
- searchTerms: ["blockquote"],
- icon: ,
- command: ({ editor, range }: CommandProps) =>
- toggleBlockquote(editor, range)
+ },
+ {
+ title: "Table",
+ description: "Create a Table",
+ searchTerms: ["table", "cell", "db", "data", "tabular"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ insertTableCommand(editor, range);
},
- {
- title: "Code",
- description: "Capture a code snippet.",
- searchTerms: ["codeblock"],
- icon: ,
- command: ({ editor, range }: CommandProps) =>
- editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
+ },
+ {
+ title: "Numbered List",
+ description: "Create a list with numbering.",
+ searchTerms: ["ordered"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ toggleOrderedList(editor, range);
},
- {
- title: "Image",
- description: "Upload an image from your computer.",
- searchTerms: ["photo", "picture", "media"],
- icon: ,
- command: ({ editor, range }: CommandProps) => {
- insertImageCommand(editor, uploadFile, setIsSubmitting, range);
- },
+ },
+ {
+ title: "Quote",
+ description: "Capture a quote.",
+ searchTerms: ["blockquote"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) =>
+ toggleBlockquote(editor, range),
+ },
+ {
+ title: "Code",
+ description: "Capture a code snippet.",
+ searchTerms: ["codeblock"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) =>
+ editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
+ },
+ {
+ title: "Image",
+ description: "Upload an image from your computer.",
+ searchTerms: ["photo", "picture", "media"],
+ icon: ,
+ command: ({ editor, range }: CommandProps) => {
+ insertImageCommand(editor, uploadFile, setIsSubmitting, range);
},
- ].filter((item) => {
- if (typeof query === "string" && query.length > 0) {
- const search = query.toLowerCase();
- return (
- item.title.toLowerCase().includes(search) ||
- item.description.toLowerCase().includes(search) ||
- (item.searchTerms && item.searchTerms.some((term: string) => term.includes(search)))
- );
- }
- return true;
- });
+ },
+ ].filter((item) => {
+ if (typeof query === "string" && query.length > 0) {
+ const search = query.toLowerCase();
+ return (
+ item.title.toLowerCase().includes(search) ||
+ item.description.toLowerCase().includes(search) ||
+ (item.searchTerms &&
+ item.searchTerms.some((term: string) => term.includes(search)))
+ );
+ }
+ return true;
+ });
export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
const containerHeight = container.offsetHeight;
@@ -213,7 +247,7 @@ const CommandList = ({
command(item);
}
},
- [command, items]
+ [command, items],
);
useEffect(() => {
@@ -266,11 +300,17 @@ const CommandList = ({