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
2 changes: 1 addition & 1 deletion packages/editor/src/core/extensions/side-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ const SideMenu = (options: SideMenuPluginProps) => {
}
}

if (node.matches(".table-wrapper")) {
if (node.matches("table")) {
rect.top += 8;
rect.left -= 8;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { findParentNode, type Editor } from "@tiptap/core";
import { Plugin, PluginKey } from "@tiptap/pm/state";
import { CellSelection, TableMap } from "@tiptap/pm/tables";
import { Decoration, DecorationSet } from "@tiptap/pm/view";
// local imports
import { getCellBorderClasses } from "./utils";

type TableCellSelectionOutlinePluginState = {
decorations?: DecorationSet;
};

const TABLE_SELECTION_OUTLINE_PLUGIN_KEY = new PluginKey("table-cell-selection-outline");

export const TableCellSelectionOutlinePlugin = (editor: Editor): Plugin<TableCellSelectionOutlinePluginState> =>
new Plugin<TableCellSelectionOutlinePluginState>({
key: TABLE_SELECTION_OUTLINE_PLUGIN_KEY,
state: {
init: () => ({}),
apply(tr, prev, oldState, newState) {
if (!editor.isEditable) return {};
const table = findParentNode((node) => node.type.spec.tableRole === "table")(newState.selection);
const hasDocChanged = tr.docChanged || !newState.selection.eq(oldState.selection);
if (!table || !hasDocChanged) {
return table === undefined ? {} : prev;
}

const { selection } = newState;
if (!(selection instanceof CellSelection)) return {};

const decorations: Decoration[] = [];
const tableMap = TableMap.get(table.node);
const selectedCells: number[] = [];

// First, collect all selected cell positions
selection.forEachCell((_node, pos) => {
const start = pos - table.pos - 1;
selectedCells.push(start);
});

// Then, add decorations with appropriate border classes
selection.forEachCell((node, pos) => {
const start = pos - table.pos - 1;
const classes = getCellBorderClasses(start, selectedCells, tableMap);

decorations.push(Decoration.node(pos, pos + node.nodeSize, { class: classes.join(" ") }));
});

return {
decorations: DecorationSet.create(newState.doc, decorations),
};
},
},
props: {
decorations(state) {
return TABLE_SELECTION_OUTLINE_PLUGIN_KEY.getState(state).decorations;
},
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { TableMap } from "@tiptap/pm/tables";

/**
* Calculates the positions of cells adjacent to a given cell in a table
* @param cellStart - The start position of the current cell in the document
* @param tableMap - ProseMirror's table mapping structure containing cell positions and dimensions
* @returns Object with positions of adjacent cells (undefined if cell doesn't exist at table edge)
*/
const getAdjacentCellPositions = (
cellStart: number,
tableMap: TableMap
): { top?: number; bottom?: number; left?: number; right?: number } => {
// Extract table dimensions
// width -> number of columns in the table
// height -> number of rows in the table
const { width, height } = tableMap;

// Find the index of our cell in the flat tableMap.map array
// tableMap.map contains start positions of all cells in row-by-row order
const cellIndex = tableMap.map.indexOf(cellStart);

// Safety check: if cell position not found in table map, return empty object
if (cellIndex === -1) return {};

// Convert flat array index to 2D grid coordinates
// row = which row the cell is in (0-based from top)
// col = which column the cell is in (0-based from left)
const row = Math.floor(cellIndex / width); // Integer division gives row number
const col = cellIndex % width; // Remainder gives column number

return {
// Top cell: same column, one row up
// Check if we're not in the first row (row > 0) before calculating
top: row > 0 ? tableMap.map[(row - 1) * width + col] : undefined,

// Bottom cell: same column, one row down
// Check if we're not in the last row (row < height - 1) before calculating
bottom: row < height - 1 ? tableMap.map[(row + 1) * width + col] : undefined,

// Left cell: same row, one column left
// Check if we're not in the first column (col > 0) before calculating
left: col > 0 ? tableMap.map[row * width + (col - 1)] : undefined,

// Right cell: same row, one column right
// Check if we're not in the last column (col < width - 1) before calculating
right: col < width - 1 ? tableMap.map[row * width + (col + 1)] : undefined,
};
};

export const getCellBorderClasses = (cellStart: number, selectedCells: number[], tableMap: TableMap): string[] => {
const adjacent = getAdjacentCellPositions(cellStart, tableMap);
const classes: string[] = [];

// Add border-right if right cell is not selected or doesn't exist
if (adjacent.right === undefined || !selectedCells.includes(adjacent.right)) {
classes.push("selectedCell-border-right");
}

// Add border-left if left cell is not selected or doesn't exist
if (adjacent.left === undefined || !selectedCells.includes(adjacent.left)) {
classes.push("selectedCell-border-left");
}

// Add border-top if top cell is not selected or doesn't exist
if (adjacent.top === undefined || !selectedCells.includes(adjacent.top)) {
classes.push("selectedCell-border-top");
}

// Add border-bottom if bottom cell is not selected or doesn't exist
if (adjacent.bottom === undefined || !selectedCells.includes(adjacent.bottom)) {
classes.push("selectedCell-border-bottom");
}

return classes;
};
10 changes: 9 additions & 1 deletion packages/editor/src/core/extensions/table/table-cell.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { mergeAttributes, Node } from "@tiptap/core";
// constants
import { CORE_EXTENSIONS } from "@/constants/extension";
// local imports
import { TableCellSelectionOutlinePlugin } from "./plugins/table-selection-outline/plugin";
import { DEFAULT_COLUMN_WIDTH } from "./table";

export interface TableCellOptions {
HTMLAttributes: Record<string, any>;
}
Expand All @@ -25,7 +29,7 @@ export const TableCell = Node.create<TableCellOptions>({
default: 1,
},
colwidth: {
default: null,
default: [DEFAULT_COLUMN_WIDTH],
parseHTML: (element) => {
const colwidth = element.getAttribute("colwidth");
const value = colwidth ? [parseInt(colwidth, 10)] : null;
Expand All @@ -46,6 +50,10 @@ export const TableCell = Node.create<TableCellOptions>({

isolating: true,

addProseMirrorPlugins() {
return [TableCellSelectionOutlinePlugin(this.editor)];
},

parseHTML() {
return [{ tag: "td" }];
},
Expand Down
5 changes: 4 additions & 1 deletion packages/editor/src/core/extensions/table/table-header.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { mergeAttributes, Node } from "@tiptap/core";
// constants
import { CORE_EXTENSIONS } from "@/constants/extension";
// local imports
import { DEFAULT_COLUMN_WIDTH } from "./table";

export interface TableHeaderOptions {
HTMLAttributes: Record<string, any>;
}
Expand All @@ -25,7 +28,7 @@ export const TableHeader = Node.create<TableHeaderOptions>({
default: 1,
},
colwidth: {
default: null,
default: [DEFAULT_COLUMN_WIDTH],
parseHTML: (element) => {
const colwidth = element.getAttribute("colwidth");
const value = colwidth ? [parseInt(colwidth, 10)] : null;
Expand Down
2 changes: 2 additions & 0 deletions packages/editor/src/core/extensions/table/table/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { Table } from "./table";

export const DEFAULT_COLUMN_WIDTH = 150;
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ export class TableView implements NodeView {
this.root = h(
"div",
{
className: "table-wrapper horizontal-scrollbar scrollbar-md controls--disabled",
className: "table-wrapper editor-full-width-block horizontal-scrollbar scrollbar-sm controls--disabled",
},
this.controls,
this.table
Expand Down
20 changes: 11 additions & 9 deletions packages/editor/src/core/extensions/table/table/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { createTable } from "./utilities/create-table";
import { deleteTableWhenAllCellsSelected } from "./utilities/delete-table-when-all-cells-selected";
import { insertLineAboveTableAction } from "./utilities/insert-line-above-table-action";
import { insertLineBelowTableAction } from "./utilities/insert-line-below-table-action";
import { DEFAULT_COLUMN_WIDTH } from ".";

export interface TableOptions {
HTMLAttributes: Record<string, any>;
Expand All @@ -42,12 +43,7 @@ export interface TableOptions {
declare module "@tiptap/core" {
interface Commands<ReturnType> {
[CORE_EXTENSIONS.TABLE]: {
insertTable: (options?: {
rows?: number;
cols?: number;
withHeaderRow?: boolean;
columnWidth?: number;
}) => ReturnType;
insertTable: (options?: { rows?: number; cols?: number; withHeaderRow?: boolean }) => ReturnType;
addColumnBefore: () => ReturnType;
addColumnAfter: () => ReturnType;
deleteColumn: () => ReturnType;
Expand Down Expand Up @@ -81,7 +77,7 @@ declare module "@tiptap/core" {
}
}

export const Table = Node.create({
export const Table = Node.create<TableOptions>({
name: CORE_EXTENSIONS.TABLE,

addOptions() {
Expand Down Expand Up @@ -116,9 +112,15 @@ export const Table = Node.create({
addCommands() {
return {
insertTable:
({ rows = 3, cols = 3, withHeaderRow = false, columnWidth = 150 } = {}) =>
({ rows = 3, cols = 3, withHeaderRow = false } = {}) =>
({ tr, dispatch, editor }) => {
const node = createTable(editor.schema, rows, cols, withHeaderRow, undefined, columnWidth);
const node = createTable({
schema: editor.schema,
rowsCount: rows,
colsCount: cols,
withHeaderRow,
columnWidth: DEFAULT_COLUMN_WIDTH,
});
if (dispatch) {
const offset = tr.selection.anchor + 1;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ import { Fragment, Node as ProsemirrorNode, Schema } from "@tiptap/pm/model";
import { createCell } from "@/extensions/table/table/utilities/create-cell";
import { getTableNodeTypes } from "@/extensions/table/table/utilities/get-table-node-types";

export function createTable(
schema: Schema,
rowsCount: number,
colsCount: number,
withHeaderRow: boolean,
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>,
columnWidth: number = 100
): ProsemirrorNode {
type Props = {
schema: Schema;
rowsCount: number;
colsCount: number;
withHeaderRow: boolean;
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>;
columnWidth: number;
};

export const createTable = (props: Props): ProsemirrorNode => {
const { schema, rowsCount, colsCount, withHeaderRow, cellContent, columnWidth } = props;

const types = getTableNodeTypes(schema);
const headerCells: ProsemirrorNode[] = [];
const cells: ProsemirrorNode[] = [];
Expand Down Expand Up @@ -38,4 +42,4 @@ export function createTable(
}

return types.table.createChecked(null, rows);
}
};
5 changes: 2 additions & 3 deletions packages/editor/src/core/helpers/editor-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,8 @@ export const insertTableCommand = (editor: Editor, range?: Range) => {
}
}
}
if (range)
editor.chain().focus().deleteRange(range).clearNodes().insertTable({ rows: 3, cols: 3, columnWidth: 150 }).run();
else editor.chain().focus().clearNodes().insertTable({ rows: 3, cols: 3, columnWidth: 150 }).run();
if (range) editor.chain().focus().deleteRange(range).clearNodes().insertTable({ rows: 3, cols: 3 }).run();
else editor.chain().focus().clearNodes().insertTable({ rows: 3, cols: 3 }).run();
};

export const insertImage = ({
Expand Down
6 changes: 3 additions & 3 deletions packages/editor/src/core/plugins/drag-handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const generalSelectors = [
"blockquote",
"h1.editor-heading-block, h2.editor-heading-block, h3.editor-heading-block, h4.editor-heading-block, h5.editor-heading-block, h6.editor-heading-block",
"[data-type=horizontalRule]",
".table-wrapper",
"table",
".issue-embed",
".image-component",
".image-upload-component",
Expand Down Expand Up @@ -90,7 +90,7 @@ export const nodeDOMAtCoords = (coords: { x: number; y: number }) => {

for (const elem of elements) {
// Check for table wrapper first
if (elem.matches(".table-wrapper")) {
if (elem.matches("table")) {
return elem;
}

Expand All @@ -99,7 +99,7 @@ export const nodeDOMAtCoords = (coords: { x: number; y: number }) => {
}

// Skip table cells
if (elem.closest(".table-wrapper")) {
if (elem.closest("table")) {
continue;
}

Expand Down
5 changes: 3 additions & 2 deletions packages/editor/src/styles/drag-drop.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
}
/* end ai handle */

.ProseMirror:not(.dragging) .ProseMirror-selectednode:not(.node-imageComponent):not(.node-image) {
.ProseMirror:not(.dragging) .ProseMirror-selectednode:not(.node-imageComponent):not(.node-image):not(.table-wrapper) {
position: relative;
cursor: grab;
outline: none !important;
Expand All @@ -61,7 +61,8 @@
}

&.node-imageComponent,
&.node-image {
&.node-image,
&.table-wrapper {
--horizontal-offset: 0px;

&::after {
Expand Down
Loading