Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45ddfc4559 | |||
| ace33f6093 | |||
| 9d1d3755d0 | |||
| 50e342638f | |||
| 756cba5096 | |||
| 369be93a77 | |||
| a8487c2ee3 | |||
| e32807d908 | |||
| 13c7ac8f9c | |||
| 167e53f74c | |||
| ec7cbd474d | |||
| a0d6fa0f1e | |||
| fdd4f80576 | |||
| a414ae23f5 |
@@ -131,7 +131,7 @@ const SideMenu = (options: SideMenuPluginProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (node.matches(".table-wrapper")) {
|
||||
if (node.matches("table")) {
|
||||
rect.top += 8;
|
||||
rect.left -= 8;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { type Editor } from "@tiptap/core";
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
// local imports
|
||||
import { createColumnInsertButton, createRowInsertButton, findAllTables, TableInfo } from "./utils";
|
||||
|
||||
const TABLE_INSERT_PLUGIN_KEY = new PluginKey("table-insert");
|
||||
|
||||
export const TableInsertPlugin = (editor: Editor): Plugin => {
|
||||
const tableMap = new Map<HTMLElement, TableInfo>();
|
||||
|
||||
const setupTable = (tableInfo: TableInfo) => {
|
||||
const { tableElement } = tableInfo;
|
||||
|
||||
// Create and add column button if it doesn't exist
|
||||
if (!tableInfo.columnButtonElement) {
|
||||
const columnButton = createColumnInsertButton(editor, tableInfo);
|
||||
tableElement.appendChild(columnButton);
|
||||
tableInfo.columnButtonElement = columnButton;
|
||||
}
|
||||
|
||||
// Create and add row button if it doesn't exist
|
||||
if (!tableInfo.rowButtonElement) {
|
||||
const rowButton = createRowInsertButton(editor, tableInfo);
|
||||
tableElement.appendChild(rowButton);
|
||||
tableInfo.rowButtonElement = rowButton;
|
||||
}
|
||||
|
||||
tableMap.set(tableElement, tableInfo);
|
||||
};
|
||||
|
||||
const cleanupTable = (tableElement: HTMLElement) => {
|
||||
const tableInfo = tableMap.get(tableElement);
|
||||
tableInfo?.columnButtonElement?.remove();
|
||||
tableInfo?.rowButtonElement?.remove();
|
||||
tableMap.delete(tableElement);
|
||||
};
|
||||
|
||||
const updateAllTables = () => {
|
||||
if (!editor.isEditable) {
|
||||
// Clean up all tables if editor is not editable
|
||||
tableMap.forEach((_, tableElement) => {
|
||||
cleanupTable(tableElement);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTables = findAllTables(editor);
|
||||
const currentTableElements = new Set(currentTables.map((t) => t.tableElement));
|
||||
|
||||
// Remove buttons from tables that no longer exist
|
||||
tableMap.forEach((_, tableElement) => {
|
||||
if (!currentTableElements.has(tableElement)) {
|
||||
cleanupTable(tableElement);
|
||||
}
|
||||
});
|
||||
|
||||
// Add buttons to new tables
|
||||
currentTables.forEach((tableInfo) => {
|
||||
if (!tableMap.has(tableInfo.tableElement)) {
|
||||
setupTable(tableInfo);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return new Plugin({
|
||||
key: TABLE_INSERT_PLUGIN_KEY,
|
||||
view() {
|
||||
setTimeout(updateAllTables, 0);
|
||||
|
||||
return {
|
||||
update(view, prevState) {
|
||||
// Update when document changes
|
||||
if (!prevState.doc.eq(view.state.doc)) {
|
||||
updateAllTables();
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
// Clean up all tables
|
||||
tableMap.forEach((_, tableElement) => {
|
||||
cleanupTable(tableElement);
|
||||
});
|
||||
tableMap.clear();
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,430 @@
|
||||
import type { Editor } from "@tiptap/core";
|
||||
import type { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||
import { addColumn, removeColumn, addRow, removeRow, TableMap } from "@tiptap/pm/tables";
|
||||
|
||||
const addSvg = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8.5 7.49988V3.49988C8.5 3.22374 8.27614 2.99988 8 2.99988C7.72386 2.99988 7.5 3.22374 7.5 3.49988L7.5 7.49988L3.5 7.49988C3.22386 7.49988 3 7.72374 3 7.99988C3 8.27602 3.22386 8.49988 3.5 8.49988H7.5L7.5 12.4999C7.5 12.776 7.72386 12.9999 8 12.9999C8.27614 12.9999 8.5 12.776 8.5 12.4999L8.5 8.49988L12.5 8.49988C12.7761 8.49988 13 8.27602 13 7.99988C13 7.72374 12.7761 7.49988 12.5 7.49988L8.5 7.49988Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>`;
|
||||
|
||||
export type TableInfo = {
|
||||
tableElement: HTMLElement;
|
||||
tableNode: ProseMirrorNode;
|
||||
tablePos: number;
|
||||
columnButtonElement?: HTMLElement;
|
||||
rowButtonElement?: HTMLElement;
|
||||
};
|
||||
|
||||
export const createColumnInsertButton = (editor: Editor, tableInfo: TableInfo): HTMLElement => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "table-column-insert-button";
|
||||
button.title = "Insert columns";
|
||||
button.ariaLabel = "Insert columns";
|
||||
|
||||
const icon = document.createElement("span");
|
||||
icon.innerHTML = addSvg;
|
||||
button.appendChild(icon);
|
||||
|
||||
let mouseDownX = 0;
|
||||
let isDragging = false;
|
||||
let dragStarted = false;
|
||||
let lastActionX = 0;
|
||||
const DRAG_THRESHOLD = 5; // pixels to start drag
|
||||
const ACTION_THRESHOLD = 150; // pixels total distance to trigger action
|
||||
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
if (e.button !== 0) return; // Only left mouse button
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
mouseDownX = e.clientX;
|
||||
lastActionX = e.clientX;
|
||||
isDragging = false;
|
||||
dragStarted = false;
|
||||
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
};
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
const deltaX = e.clientX - mouseDownX;
|
||||
const distance = Math.abs(deltaX);
|
||||
|
||||
// Start dragging if moved more than threshold
|
||||
if (!isDragging && distance > DRAG_THRESHOLD) {
|
||||
isDragging = true;
|
||||
dragStarted = true;
|
||||
|
||||
// Visual feedback
|
||||
button.classList.add("dragging");
|
||||
document.body.style.userSelect = "none";
|
||||
}
|
||||
|
||||
if (isDragging) {
|
||||
const totalDistance = Math.abs(e.clientX - lastActionX);
|
||||
|
||||
// Only trigger action when total distance reaches threshold
|
||||
if (totalDistance >= ACTION_THRESHOLD) {
|
||||
// Determine direction based on current movement relative to last action point
|
||||
const directionFromLastAction = e.clientX - lastActionX;
|
||||
|
||||
// Right direction - add columns
|
||||
if (directionFromLastAction > 0) {
|
||||
insertColumnAfterLast(editor, tableInfo);
|
||||
lastActionX = e.clientX; // Reset action point
|
||||
}
|
||||
// Left direction - delete empty columns
|
||||
else if (directionFromLastAction < 0) {
|
||||
const deleted = removeLastColumn(editor, tableInfo);
|
||||
if (deleted) {
|
||||
lastActionX = e.clientX; // Reset action point
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
document.removeEventListener("mousemove", onMouseMove);
|
||||
document.removeEventListener("mouseup", onMouseUp);
|
||||
|
||||
if (isDragging) {
|
||||
// Clean up drag state
|
||||
button.classList.remove("dragging");
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
} else if (!dragStarted) {
|
||||
// Handle as click if no dragging occurred
|
||||
insertColumnAfterLast(editor, tableInfo);
|
||||
}
|
||||
|
||||
isDragging = false;
|
||||
dragStarted = false;
|
||||
};
|
||||
|
||||
button.addEventListener("mousedown", onMouseDown);
|
||||
|
||||
// Prevent context menu and text selection
|
||||
button.addEventListener("contextmenu", (e) => e.preventDefault());
|
||||
button.addEventListener("selectstart", (e) => e.preventDefault());
|
||||
|
||||
return button;
|
||||
};
|
||||
|
||||
export const createRowInsertButton = (editor: Editor, tableInfo: TableInfo): HTMLElement => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "table-row-insert-button";
|
||||
button.title = "Insert rows";
|
||||
button.ariaLabel = "Insert rows";
|
||||
|
||||
const icon = document.createElement("span");
|
||||
icon.innerHTML = addSvg;
|
||||
button.appendChild(icon);
|
||||
|
||||
let mouseDownY = 0;
|
||||
let isDragging = false;
|
||||
let dragStarted = false;
|
||||
let lastActionY = 0;
|
||||
const DRAG_THRESHOLD = 5; // pixels to start drag
|
||||
const ACTION_THRESHOLD = 40; // pixels total distance to trigger action
|
||||
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
if (e.button !== 0) return; // Only left mouse button
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
mouseDownY = e.clientY;
|
||||
lastActionY = e.clientY;
|
||||
isDragging = false;
|
||||
dragStarted = false;
|
||||
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
};
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
const deltaY = e.clientY - mouseDownY;
|
||||
const distance = Math.abs(deltaY);
|
||||
|
||||
// Start dragging if moved more than threshold
|
||||
if (!isDragging && distance > DRAG_THRESHOLD) {
|
||||
isDragging = true;
|
||||
dragStarted = true;
|
||||
|
||||
// Visual feedback
|
||||
button.classList.add("dragging");
|
||||
document.body.style.userSelect = "none";
|
||||
}
|
||||
|
||||
if (isDragging) {
|
||||
const totalDistance = Math.abs(e.clientY - lastActionY);
|
||||
|
||||
// Only trigger action when total distance reaches threshold
|
||||
if (totalDistance >= ACTION_THRESHOLD) {
|
||||
// Determine direction based on current movement relative to last action point
|
||||
const directionFromLastAction = e.clientY - lastActionY;
|
||||
|
||||
// Down direction - add rows
|
||||
if (directionFromLastAction > 0) {
|
||||
insertRowAfterLast(editor, tableInfo);
|
||||
lastActionY = e.clientY; // Reset action point
|
||||
}
|
||||
// Up direction - delete empty rows
|
||||
else if (directionFromLastAction < 0) {
|
||||
const deleted = removeLastRow(editor, tableInfo);
|
||||
if (deleted) {
|
||||
lastActionY = e.clientY; // Reset action point
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
document.removeEventListener("mousemove", onMouseMove);
|
||||
document.removeEventListener("mouseup", onMouseUp);
|
||||
|
||||
if (isDragging) {
|
||||
// Clean up drag state
|
||||
button.classList.remove("dragging");
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
} else if (!dragStarted) {
|
||||
// Handle as click if no dragging occurred
|
||||
insertRowAfterLast(editor, tableInfo);
|
||||
}
|
||||
|
||||
isDragging = false;
|
||||
dragStarted = false;
|
||||
};
|
||||
|
||||
button.addEventListener("mousedown", onMouseDown);
|
||||
|
||||
// Prevent context menu and text selection
|
||||
button.addEventListener("contextmenu", (e) => e.preventDefault());
|
||||
button.addEventListener("selectstart", (e) => e.preventDefault());
|
||||
|
||||
return button;
|
||||
};
|
||||
|
||||
export const findAllTables = (editor: Editor): TableInfo[] => {
|
||||
const tables: TableInfo[] = [];
|
||||
const tableElements = editor.view.dom.querySelectorAll("table");
|
||||
|
||||
tableElements.forEach((tableElement) => {
|
||||
// Find the table's ProseMirror position
|
||||
let tablePos = -1;
|
||||
let tableNode: ProseMirrorNode | null = null;
|
||||
|
||||
// Walk through the document to find matching table nodes
|
||||
editor.state.doc.descendants((node, pos) => {
|
||||
if (node.type.spec.tableRole === "table") {
|
||||
const domAtPos = editor.view.domAtPos(pos + 1);
|
||||
let domTable = domAtPos.node;
|
||||
|
||||
// Navigate to find the table element
|
||||
while (domTable && domTable.parentNode && domTable.nodeType !== Node.ELEMENT_NODE) {
|
||||
domTable = domTable.parentNode;
|
||||
}
|
||||
|
||||
while (domTable && domTable.parentNode && (domTable as HTMLElement).tagName !== "TABLE") {
|
||||
domTable = domTable.parentNode;
|
||||
}
|
||||
|
||||
if (domTable === tableElement) {
|
||||
tablePos = pos;
|
||||
tableNode = node;
|
||||
return false; // Stop iteration
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (tablePos !== -1 && tableNode) {
|
||||
tables.push({
|
||||
tableElement,
|
||||
tableNode,
|
||||
tablePos,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return tables;
|
||||
};
|
||||
|
||||
const getCurrentTableInfo = (editor: Editor, tableInfo: TableInfo): TableInfo => {
|
||||
// Refresh table info to get latest state
|
||||
const tables = findAllTables(editor);
|
||||
const updated = tables.find((t) => t.tableElement === tableInfo.tableElement);
|
||||
return updated || tableInfo;
|
||||
};
|
||||
|
||||
// Column functions
|
||||
const insertColumnAfterLast = (editor: Editor, tableInfo: TableInfo) => {
|
||||
const currentTableInfo = getCurrentTableInfo(editor, tableInfo);
|
||||
const { tableNode, tablePos } = currentTableInfo;
|
||||
const tableMapData = TableMap.get(tableNode);
|
||||
const lastColumnIndex = tableMapData.width;
|
||||
|
||||
const tr = editor.state.tr;
|
||||
const rect = {
|
||||
map: tableMapData,
|
||||
tableStart: tablePos,
|
||||
table: tableNode,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: tableMapData.height - 1,
|
||||
right: tableMapData.width - 1,
|
||||
};
|
||||
|
||||
const newTr = addColumn(tr, rect, lastColumnIndex);
|
||||
editor.view.dispatch(newTr);
|
||||
};
|
||||
|
||||
const removeLastColumn = (editor: Editor, tableInfo: TableInfo): boolean => {
|
||||
const currentTableInfo = getCurrentTableInfo(editor, tableInfo);
|
||||
const { tableNode, tablePos } = currentTableInfo;
|
||||
const tableMapData = TableMap.get(tableNode);
|
||||
|
||||
// Don't delete if only one column left
|
||||
if (tableMapData.width <= 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lastColumnIndex = tableMapData.width - 1;
|
||||
|
||||
// Check if last column is empty
|
||||
if (!isColumnEmpty(currentTableInfo, lastColumnIndex)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tr = editor.state.tr;
|
||||
const rect = {
|
||||
map: tableMapData,
|
||||
tableStart: tablePos,
|
||||
table: tableNode,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: tableMapData.height - 1,
|
||||
right: tableMapData.width - 1,
|
||||
};
|
||||
|
||||
removeColumn(tr, rect, lastColumnIndex);
|
||||
editor.view.dispatch(tr);
|
||||
return true;
|
||||
};
|
||||
|
||||
// Helper function to check if a single cell is empty
|
||||
const isCellEmpty = (cell: ProseMirrorNode | null | undefined): boolean => {
|
||||
if (!cell || cell.content.size === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if cell has any non-empty content
|
||||
let hasContent = false;
|
||||
cell.content.forEach((node) => {
|
||||
if (node.type.name === "paragraph") {
|
||||
if (node.content.size > 0) {
|
||||
hasContent = true;
|
||||
}
|
||||
} else if (node.content.size > 0 || node.isText) {
|
||||
hasContent = true;
|
||||
}
|
||||
});
|
||||
|
||||
return !hasContent;
|
||||
};
|
||||
|
||||
const isColumnEmpty = (tableInfo: TableInfo, columnIndex: number): boolean => {
|
||||
const { tableNode } = tableInfo;
|
||||
const tableMapData = TableMap.get(tableNode);
|
||||
|
||||
// Check each cell in the column
|
||||
for (let row = 0; row < tableMapData.height; row++) {
|
||||
const cellIndex = row * tableMapData.width + columnIndex;
|
||||
const cellPos = tableMapData.map[cellIndex];
|
||||
const cell = tableNode.nodeAt(cellPos);
|
||||
|
||||
if (!isCellEmpty(cell)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Row functions
|
||||
const insertRowAfterLast = (editor: Editor, tableInfo: TableInfo) => {
|
||||
const currentTableInfo = getCurrentTableInfo(editor, tableInfo);
|
||||
const { tableNode, tablePos } = currentTableInfo;
|
||||
const tableMapData = TableMap.get(tableNode);
|
||||
const lastRowIndex = tableMapData.height;
|
||||
|
||||
const tr = editor.state.tr;
|
||||
const rect = {
|
||||
map: tableMapData,
|
||||
tableStart: tablePos,
|
||||
table: tableNode,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: tableMapData.height - 1,
|
||||
right: tableMapData.width - 1,
|
||||
};
|
||||
|
||||
const newTr = addRow(tr, rect, lastRowIndex);
|
||||
editor.view.dispatch(newTr);
|
||||
};
|
||||
|
||||
const removeLastRow = (editor: Editor, tableInfo: TableInfo): boolean => {
|
||||
const currentTableInfo = getCurrentTableInfo(editor, tableInfo);
|
||||
const { tableNode, tablePos } = currentTableInfo;
|
||||
const tableMapData = TableMap.get(tableNode);
|
||||
|
||||
// Don't delete if only one row left
|
||||
if (tableMapData.height <= 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lastRowIndex = tableMapData.height - 1;
|
||||
|
||||
// Check if last row is empty
|
||||
if (!isRowEmpty(currentTableInfo, lastRowIndex)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tr = editor.state.tr;
|
||||
const rect = {
|
||||
map: tableMapData,
|
||||
tableStart: tablePos,
|
||||
table: tableNode,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: tableMapData.height - 1,
|
||||
right: tableMapData.width - 1,
|
||||
};
|
||||
|
||||
removeRow(tr, rect, lastRowIndex);
|
||||
editor.view.dispatch(tr);
|
||||
return true;
|
||||
};
|
||||
|
||||
const isRowEmpty = (tableInfo: TableInfo, rowIndex: number): boolean => {
|
||||
const { tableNode } = tableInfo;
|
||||
const tableMapData = TableMap.get(tableNode);
|
||||
|
||||
// Check each cell in the row
|
||||
for (let col = 0; col < tableMapData.width; col++) {
|
||||
const cellIndex = rowIndex * tableMapData.width + col;
|
||||
const cellPos = tableMapData.map[cellIndex];
|
||||
const cell = tableNode.nodeAt(cellPos);
|
||||
|
||||
if (!isCellEmpty(cell)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -1,6 +1,10 @@
|
||||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
// local imports
|
||||
import { TableCellSelectionOutlinePlugin } from "./plugins/selection-outline/plugin";
|
||||
import { DEFAULT_COLUMN_WIDTH } from "./table";
|
||||
|
||||
export interface TableCellOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -46,6 +50,10 @@ export const TableCell = Node.create<TableCellOptions>({
|
||||
|
||||
isolating: true,
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [TableCellSelectionOutlinePlugin(this.editor)];
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: "td" }];
|
||||
},
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export { Table } from "./table";
|
||||
|
||||
export const DEFAULT_COLUMN_WIDTH = 150;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,12 +23,14 @@ import { Decoration } from "@tiptap/pm/view";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
// local imports
|
||||
import { TableInsertPlugin } from "../plugins/insert-handlers/plugin";
|
||||
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";
|
||||
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>;
|
||||
@@ -42,12 +44,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;
|
||||
@@ -81,7 +78,7 @@ declare module "@tiptap/core" {
|
||||
}
|
||||
}
|
||||
|
||||
export const Table = Node.create({
|
||||
export const Table = Node.create<TableOptions>({
|
||||
name: CORE_EXTENSIONS.TABLE,
|
||||
|
||||
addOptions() {
|
||||
@@ -116,9 +113,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;
|
||||
|
||||
@@ -264,6 +267,7 @@ export const Table = Node.create({
|
||||
allowTableNodeSelection: this.options.allowTableNodeSelection,
|
||||
}),
|
||||
tableControls(),
|
||||
TableInsertPlugin(this.editor),
|
||||
];
|
||||
|
||||
if (isResizable) {
|
||||
|
||||
@@ -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[] = [];
|
||||
@@ -38,4 +42,4 @@ export function createTable(
|
||||
}
|
||||
|
||||
return types.table.createChecked(null, rows);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
@@ -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",
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -61,7 +61,8 @@
|
||||
}
|
||||
|
||||
&.node-imageComponent,
|
||||
&.node-image {
|
||||
&.node-image,
|
||||
&.table-wrapper {
|
||||
--horizontal-offset: 0px;
|
||||
|
||||
&::after {
|
||||
|
||||
@@ -1,115 +1,155 @@
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}
|
||||
padding-bottom: 30px;
|
||||
|
||||
.table-wrapper table {
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
margin: 0.5rem 0 1rem 0;
|
||||
border: 1px solid rgba(var(--color-border-200));
|
||||
width: 100%;
|
||||
}
|
||||
table {
|
||||
position: relative;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
margin: 0.5rem 0 0 0;
|
||||
border: 1px solid rgba(var(--color-border-200));
|
||||
width: 100%;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
.table-wrapper table td,
|
||||
.table-wrapper table th {
|
||||
min-width: 1em;
|
||||
border: 1px solid rgba(var(--color-border-200));
|
||||
padding: 7px 10px;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
> * {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.table-wrapper table {
|
||||
th {
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
tr[background="none"],
|
||||
tr:not([background]) {
|
||||
td,
|
||||
th {
|
||||
background-color: rgba(var(--color-background-90));
|
||||
min-width: 1em;
|
||||
border: 1px solid rgba(var(--color-border-300));
|
||||
padding: 7px 10px;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
> * {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Selected cell outline */
|
||||
&.selectedCell {
|
||||
user-select: none;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
height: calc(100% + 2px);
|
||||
width: calc(100% + 2px);
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
&.selectedCell-border-top::after {
|
||||
border-top-color: rgba(var(--color-primary-100));
|
||||
}
|
||||
|
||||
&.selectedCell-border-left::after {
|
||||
border-left-color: rgba(var(--color-primary-100));
|
||||
}
|
||||
|
||||
&.selectedCell-border-bottom::after {
|
||||
border-bottom-color: rgba(var(--color-primary-100));
|
||||
}
|
||||
|
||||
&.selectedCell-border-right::after {
|
||||
border-right-color: rgba(var(--color-primary-100));
|
||||
}
|
||||
}
|
||||
/* End selected cell outline */
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.ProseMirror-selectednode) {
|
||||
table {
|
||||
tr[background="none"],
|
||||
tr:not([background]) {
|
||||
th {
|
||||
background-color: rgba(var(--color-background-90));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Selected status */
|
||||
&.ProseMirror-selectednode {
|
||||
table {
|
||||
background-color: rgba(var(--color-primary-100), 0.2);
|
||||
}
|
||||
}
|
||||
/* End selected status */
|
||||
}
|
||||
|
||||
.table-wrapper table .selectedCell {
|
||||
outline: 0.5px solid rgba(var(--color-primary-100));
|
||||
}
|
||||
|
||||
/* table dropdown */
|
||||
/* Column resizer */
|
||||
.table-wrapper table .column-resize-handle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
right: -1px;
|
||||
top: -1px;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
height: calc(100% + 2px);
|
||||
z-index: 5;
|
||||
background-color: rgba(var(--color-primary-100));
|
||||
pointer-events: none;
|
||||
}
|
||||
/* End column resizer */
|
||||
|
||||
.table-wrapper .table-controls {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.table-wrapper .table-controls .columns-control,
|
||||
.table-wrapper .table-controls .rows-control {
|
||||
transition: opacity ease-in 100ms;
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.columns-control,
|
||||
.rows-control {
|
||||
transition: opacity ease-in 100ms;
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-wrapper .table-controls .columns-control {
|
||||
height: 20px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.columns-control {
|
||||
height: 20px;
|
||||
transform: translateY(-50%);
|
||||
|
||||
.table-wrapper .table-controls .columns-control .columns-control-div {
|
||||
color: white;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6 12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21 12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E");
|
||||
width: 30px;
|
||||
height: 15px;
|
||||
}
|
||||
.columns-control-div {
|
||||
color: white;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6 12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21 12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E");
|
||||
width: 30px;
|
||||
height: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-wrapper .table-controls .rows-control {
|
||||
width: 20px;
|
||||
transform: translateX(-50%);
|
||||
left: -8px;
|
||||
}
|
||||
.rows-control {
|
||||
width: 20px;
|
||||
transform: translateX(-50%);
|
||||
left: -8px;
|
||||
|
||||
.table-wrapper .table-controls .rows-control .rows-control-div {
|
||||
color: white;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M12 3c-.825 0-1.5.675-1.5 1.5S11.175 6 12 6s1.5-.675 1.5-1.5S12.825 3 12 3zm0 15c-.825 0-1.5.675-1.5 1.5S11.175 21 12 21s1.5-.675 1.5-1.5S12.825 18 12 18zm0-7.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E");
|
||||
height: 30px;
|
||||
width: 15px;
|
||||
}
|
||||
.rows-control-div {
|
||||
color: white;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M12 3c-.825 0-1.5.675-1.5 1.5S11.175 6 12 6s1.5-.675 1.5-1.5S12.825 3 12 3zm0 15c-.825 0-1.5.675-1.5 1.5S11.175 21 12 21s1.5-.675 1.5-1.5S12.825 18 12 18zm0-7.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E");
|
||||
height: 30px;
|
||||
width: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-wrapper .table-controls .rows-control-div,
|
||||
.table-wrapper .table-controls .columns-control-div {
|
||||
background-color: rgba(var(--color-background-80));
|
||||
border: 0.5px solid rgba(var(--color-border-200));
|
||||
border-radius: 4px;
|
||||
background-size: 1.25rem;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
transition:
|
||||
transform ease-out 100ms,
|
||||
background-color ease-out 100ms;
|
||||
outline: none;
|
||||
box-shadow: rgba(var(--color-shadow-2xs));
|
||||
cursor: pointer;
|
||||
.columns-control-div,
|
||||
.rows-control-div {
|
||||
background-color: rgba(var(--color-background-80));
|
||||
border: 0.5px solid rgba(var(--color-border-200));
|
||||
border-radius: 4px;
|
||||
background-size: 1.25rem;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
transition:
|
||||
transform ease-out 100ms,
|
||||
background-color ease-out 100ms;
|
||||
outline: none;
|
||||
box-shadow: rgba(var(--color-shadow-2xs));
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.resize-cursor .table-wrapper .table-controls .rows-control,
|
||||
@@ -119,3 +159,65 @@
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Insert buttons */
|
||||
.table-wrapper {
|
||||
.table-column-insert-button,
|
||||
.table-row-insert-button {
|
||||
position: absolute;
|
||||
background-color: rgba(var(--color-background-90));
|
||||
color: rgba(var(--color-text-300));
|
||||
border: 1px solid rgba(var(--color-border-200));
|
||||
border-radius: 4px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
outline: none;
|
||||
z-index: 1000;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--color-background-80));
|
||||
color: rgba(var(--color-text-100));
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
background-color: rgba(var(--color-primary-100), 0.2);
|
||||
color: rgba(var(--color-text-100));
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-column-insert-button {
|
||||
top: 0;
|
||||
right: -20px;
|
||||
width: 20px;
|
||||
height: 100%;
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
.table-row-insert-button {
|
||||
bottom: -20px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
transform: translateY(50%);
|
||||
}
|
||||
|
||||
/* Show buttons on table hover */
|
||||
&:hover {
|
||||
.table-column-insert-button,
|
||||
.table-row-insert-button {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* End insert buttons */
|
||||
|
||||
@@ -179,9 +179,18 @@
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
max-width: var(--editor-content-width);
|
||||
margin: 0 auto;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
& > *:not(.editor-full-width-block) {
|
||||
max-width: var(--editor-content-width);
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
& > .editor-full-width-block {
|
||||
max-width: 100%;
|
||||
padding-inline-start: calc((100% - var(--editor-content-width)) / 2);
|
||||
padding-inline-end: var(--wide-content-margin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user