From 18ba1c0f2251e78bfb6fedd2f4298a9dd7d142bf Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Sat, 12 Jul 2025 16:57:18 +0530 Subject: [PATCH 1/2] chore: smooth insert handles --- .../table/plugins/insert-handlers/utils.ts | 144 +++++++++++------- 1 file changed, 88 insertions(+), 56 deletions(-) diff --git a/packages/editor/src/core/extensions/table/plugins/insert-handlers/utils.ts b/packages/editor/src/core/extensions/table/plugins/insert-handlers/utils.ts index 1306e7919a1..e98b95f4020 100644 --- a/packages/editor/src/core/extensions/table/plugins/insert-handlers/utils.ts +++ b/packages/editor/src/core/extensions/table/plugins/insert-handlers/utils.ts @@ -31,21 +31,27 @@ export const createColumnInsertButton = (editor: Editor, tableInfo: TableInfo): 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 + let columnsAdded = 0; + let originalColumnCount = 0; // Track original column count at drag start + const DRAG_THRESHOLD = 5; + const ACTION_THRESHOLD = 150; const onMouseDown = (e: MouseEvent) => { - if (e.button !== 0) return; // Only left mouse button + if (e.button !== 0) return; e.preventDefault(); e.stopPropagation(); mouseDownX = e.clientX; - lastActionX = e.clientX; isDragging = false; dragStarted = false; + // Initialize with existing column count + const currentTableInfo = getCurrentTableInfo(editor, tableInfo); + const tableMapData = TableMap.get(currentTableInfo.tableNode); + originalColumnCount = tableMapData.width; + columnsAdded = originalColumnCount; // Current total columns + document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); }; @@ -54,35 +60,47 @@ export const createColumnInsertButton = (editor: Editor, tableInfo: TableInfo): 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); + // Calculate target columns based on displacement from start position + let targetColumns = originalColumnCount; // Start with original count + + if (deltaX > 0) { + // Moving right - add columns based on distance + let columnsToAdd = 0; + while (columnsToAdd * ACTION_THRESHOLD + ACTION_THRESHOLD / 2 <= deltaX) { + columnsToAdd++; + } + targetColumns = originalColumnCount + columnsToAdd; + } else if (deltaX < 0) { + // Moving left - remove columns based on distance + const leftDistance = Math.abs(deltaX); + let columnsToRemove = 0; + while (columnsToRemove * ACTION_THRESHOLD + ACTION_THRESHOLD / 2 <= leftDistance) { + columnsToRemove++; + } + targetColumns = Math.max(1, originalColumnCount - columnsToRemove); // Keep at least 1 column + } - // 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; + // Add columns if needed + while (columnsAdded < targetColumns) { + insertColumnAfterLast(editor, tableInfo); + columnsAdded++; + } - // 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 - } + // Remove columns if needed + while (columnsAdded > targetColumns) { + const deleted = removeLastColumn(editor, tableInfo); + if (deleted) { + columnsAdded--; + } else { + break; } } } @@ -93,22 +111,20 @@ export const createColumnInsertButton = (editor: Editor, tableInfo: TableInfo): 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); + columnsAdded++; } isDragging = false; dragStarted = false; + // Don't reset columnsAdded and originalColumnCount here - they'll be reset on next drag }; button.addEventListener("mousedown", onMouseDown); - - // Prevent context menu and text selection button.addEventListener("contextmenu", (e) => e.preventDefault()); button.addEventListener("selectstart", (e) => e.preventDefault()); @@ -129,21 +145,27 @@ export const createRowInsertButton = (editor: Editor, tableInfo: TableInfo): HTM 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 + let rowsAdded = 0; + let originalRowCount = 0; // Track original row count at drag start + const DRAG_THRESHOLD = 5; + const ACTION_THRESHOLD = 40; const onMouseDown = (e: MouseEvent) => { - if (e.button !== 0) return; // Only left mouse button + if (e.button !== 0) return; e.preventDefault(); e.stopPropagation(); mouseDownY = e.clientY; - lastActionY = e.clientY; isDragging = false; dragStarted = false; + // Initialize with existing row count + const currentTableInfo = getCurrentTableInfo(editor, tableInfo); + const tableMapData = TableMap.get(currentTableInfo.tableNode); + originalRowCount = tableMapData.height; + rowsAdded = originalRowCount; // Current total rows + document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); }; @@ -152,35 +174,47 @@ export const createRowInsertButton = (editor: Editor, tableInfo: TableInfo): HTM 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); + // Calculate target rows based on displacement from start position + let targetRows = originalRowCount; // Start with original count + + if (deltaY > 0) { + // Moving down - add rows based on distance + let rowsToAdd = 0; + while (rowsToAdd * ACTION_THRESHOLD + ACTION_THRESHOLD / 2 <= deltaY) { + rowsToAdd++; + } + targetRows = originalRowCount + rowsToAdd; + } else if (deltaY < 0) { + // Moving up - remove rows based on distance + const upDistance = Math.abs(deltaY); + let rowsToRemove = 0; + while (rowsToRemove * ACTION_THRESHOLD + ACTION_THRESHOLD / 2 <= upDistance) { + rowsToRemove++; + } + targetRows = Math.max(1, originalRowCount - rowsToRemove); // Keep at least 1 row + } - // 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; + // Add rows if needed + while (rowsAdded < targetRows) { + insertRowAfterLast(editor, tableInfo); + rowsAdded++; + } - // 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 - } + // Remove rows if needed + while (rowsAdded > targetRows) { + const deleted = removeLastRow(editor, tableInfo); + if (deleted) { + rowsAdded--; + } else { + break; } } } @@ -191,22 +225,20 @@ export const createRowInsertButton = (editor: Editor, tableInfo: TableInfo): HTM 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); + rowsAdded++; } isDragging = false; dragStarted = false; + // Don't reset rowsAdded and originalRowCount here - they'll be reset on next drag }; button.addEventListener("mousedown", onMouseDown); - - // Prevent context menu and text selection button.addEventListener("contextmenu", (e) => e.preventDefault()); button.addEventListener("selectstart", (e) => e.preventDefault()); From 3379e21b97725864e19a2c84873d6a7636f711a6 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Sat, 12 Jul 2025 17:06:38 +0530 Subject: [PATCH 2/2] refactor: drag logic --- .../table/plugins/insert-handlers/utils.ts | 589 ++++++++---------- 1 file changed, 271 insertions(+), 318 deletions(-) diff --git a/packages/editor/src/core/extensions/table/plugins/insert-handlers/utils.ts b/packages/editor/src/core/extensions/table/plugins/insert-handlers/utils.ts index e98b95f4020..5e9eb97b612 100644 --- a/packages/editor/src/core/extensions/table/plugins/insert-handlers/utils.ts +++ b/packages/editor/src/core/extensions/table/plugins/insert-handlers/utils.ts @@ -17,138 +17,251 @@ export type TableInfo = { 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"; +// 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 icon = document.createElement("span"); - icon.innerHTML = addSvg; - button.appendChild(icon); + 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, + }; - let mouseDownX = 0; - let isDragging = false; - let dragStarted = false; - let columnsAdded = 0; - let originalColumnCount = 0; // Track original column count at drag start - const DRAG_THRESHOLD = 5; - const ACTION_THRESHOLD = 150; + const newTr = addColumn(tr, rect, lastColumnIndex); + editor.view.dispatch(newTr); +}; - const onMouseDown = (e: MouseEvent) => { - if (e.button !== 0) return; +const removeLastColumn = (editor: Editor, tableInfo: TableInfo): boolean => { + const currentTableInfo = getCurrentTableInfo(editor, tableInfo); + const { tableNode, tablePos } = currentTableInfo; + const tableMapData = TableMap.get(tableNode); - e.preventDefault(); - e.stopPropagation(); + if (tableMapData.width <= 1) { + return false; + } - mouseDownX = e.clientX; - isDragging = false; - dragStarted = false; + const lastColumnIndex = tableMapData.width - 1; - // Initialize with existing column count - const currentTableInfo = getCurrentTableInfo(editor, tableInfo); - const tableMapData = TableMap.get(currentTableInfo.tableNode); - originalColumnCount = tableMapData.width; - columnsAdded = originalColumnCount; // Current total columns + if (!isColumnEmpty(currentTableInfo, lastColumnIndex)) { + return false; + } - document.addEventListener("mousemove", onMouseMove); - document.addEventListener("mouseup", onMouseUp); + 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 onMouseMove = (e: MouseEvent) => { - const deltaX = e.clientX - mouseDownX; - const distance = Math.abs(deltaX); + removeColumn(tr, rect, lastColumnIndex); + editor.view.dispatch(tr); + return true; +}; - if (!isDragging && distance > DRAG_THRESHOLD) { - isDragging = true; - dragStarted = true; - button.classList.add("dragging"); - document.body.style.userSelect = "none"; - } +const insertRowAfterLast = (editor: Editor, tableInfo: TableInfo) => { + const currentTableInfo = getCurrentTableInfo(editor, tableInfo); + const { tableNode, tablePos } = currentTableInfo; + const tableMapData = TableMap.get(tableNode); + const lastRowIndex = tableMapData.height; - if (isDragging) { - // Calculate target columns based on displacement from start position - let targetColumns = originalColumnCount; // Start with original count + 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, + }; - if (deltaX > 0) { - // Moving right - add columns based on distance - let columnsToAdd = 0; - while (columnsToAdd * ACTION_THRESHOLD + ACTION_THRESHOLD / 2 <= deltaX) { - columnsToAdd++; - } - targetColumns = originalColumnCount + columnsToAdd; - } else if (deltaX < 0) { - // Moving left - remove columns based on distance - const leftDistance = Math.abs(deltaX); - let columnsToRemove = 0; - while (columnsToRemove * ACTION_THRESHOLD + ACTION_THRESHOLD / 2 <= leftDistance) { - columnsToRemove++; - } - targetColumns = Math.max(1, originalColumnCount - columnsToRemove); // Keep at least 1 column - } + const newTr = addRow(tr, rect, lastRowIndex); + editor.view.dispatch(newTr); +}; - // Add columns if needed - while (columnsAdded < targetColumns) { - insertColumnAfterLast(editor, tableInfo); - columnsAdded++; - } +const removeLastRow = (editor: Editor, tableInfo: TableInfo): boolean => { + const currentTableInfo = getCurrentTableInfo(editor, tableInfo); + const { tableNode, tablePos } = currentTableInfo; + const tableMapData = TableMap.get(tableNode); - // Remove columns if needed - while (columnsAdded > targetColumns) { - const deleted = removeLastColumn(editor, tableInfo); - if (deleted) { - columnsAdded--; - } else { - break; - } + if (tableMapData.height <= 1) { + return false; + } + + const lastRowIndex = tableMapData.height - 1; + + 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; +}; + +// Helper functions +const isCellEmpty = (cell: ProseMirrorNode | null | undefined): boolean => { + if (!cell || cell.content.size === 0) { + return true; + } + + 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; } - }; + }); - const onMouseUp = () => { - document.removeEventListener("mousemove", onMouseMove); - document.removeEventListener("mouseup", onMouseUp); + return !hasContent; +}; - if (isDragging) { - button.classList.remove("dragging"); - document.body.style.cursor = ""; - document.body.style.userSelect = ""; - } else if (!dragStarted) { - insertColumnAfterLast(editor, tableInfo); - columnsAdded++; +const isColumnEmpty = (tableInfo: TableInfo, columnIndex: number): boolean => { + const { tableNode } = tableInfo; + const tableMapData = TableMap.get(tableNode); + + 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; +}; - isDragging = false; - dragStarted = false; - // Don't reset columnsAdded and originalColumnCount here - they'll be reset on next drag - }; +const isRowEmpty = (tableInfo: TableInfo, rowIndex: number): boolean => { + const { tableNode } = tableInfo; + const tableMapData = TableMap.get(tableNode); - button.addEventListener("mousedown", onMouseDown); - button.addEventListener("contextmenu", (e) => e.preventDefault()); - button.addEventListener("selectstart", (e) => e.preventDefault()); + for (let col = 0; col < tableMapData.width; col++) { + const cellIndex = rowIndex * tableMapData.width + col; + const cellPos = tableMapData.map[cellIndex]; + const cell = tableNode.nodeAt(cellPos); - return button; + if (!isCellEmpty(cell)) { + return false; + } + } + return true; +}; + +type InsertDirection = "column" | "row"; + +interface InsertButtonConfig { + direction: InsertDirection; + className: string; + title: string; + ariaLabel: string; + dragThreshold: number; + actionThreshold: number; + coordinate: "clientX" | "clientY"; +} + +interface DragState { + mouseDownPosition: number; + isDragging: boolean; + dragStarted: boolean; + itemsAdded: number; + originalItemCount: number; +} + +interface InsertHandlers { + insertItem: (editor: Editor, tableInfo: TableInfo) => void; + removeItem: (editor: Editor, tableInfo: TableInfo) => boolean; + getItemCount: (tableNode: ProseMirrorNode) => number; + isEmpty: (tableInfo: TableInfo, itemIndex: number) => boolean; +} + +// Column handlers +const columnHandlers: InsertHandlers = { + insertItem: insertColumnAfterLast, + removeItem: removeLastColumn, + getItemCount: (tableNode) => TableMap.get(tableNode).width, + isEmpty: isColumnEmpty, +}; + +// Row handlers +const rowHandlers: InsertHandlers = { + insertItem: insertRowAfterLast, + removeItem: removeLastRow, + getItemCount: (tableNode) => TableMap.get(tableNode).height, + isEmpty: isRowEmpty, }; -export const createRowInsertButton = (editor: Editor, tableInfo: TableInfo): HTMLElement => { +// Configuration for different button types +const BUTTON_CONFIGS: Record = { + column: { + direction: "column", + className: "table-column-insert-button", + title: "Insert columns", + ariaLabel: "Insert columns", + dragThreshold: 5, + actionThreshold: 150, + coordinate: "clientX", + }, + row: { + direction: "row", + className: "table-row-insert-button", + title: "Insert rows", + ariaLabel: "Insert rows", + dragThreshold: 5, + actionThreshold: 40, + coordinate: "clientY", + }, +}; + +const createInsertButton = ( + editor: Editor, + tableInfo: TableInfo, + config: InsertButtonConfig, + handlers: InsertHandlers +): HTMLElement => { const button = document.createElement("button"); button.type = "button"; - button.className = "table-row-insert-button"; - button.title = "Insert rows"; - button.ariaLabel = "Insert rows"; + button.className = config.className; + button.title = config.title; + button.ariaLabel = config.ariaLabel; const icon = document.createElement("span"); icon.innerHTML = addSvg; button.appendChild(icon); - let mouseDownY = 0; - let isDragging = false; - let dragStarted = false; - let rowsAdded = 0; - let originalRowCount = 0; // Track original row count at drag start - const DRAG_THRESHOLD = 5; - const ACTION_THRESHOLD = 40; + const dragState: DragState = { + mouseDownPosition: 0, + isDragging: false, + dragStarted: false, + itemsAdded: 0, + originalItemCount: 0, + }; const onMouseDown = (e: MouseEvent) => { if (e.button !== 0) return; @@ -156,63 +269,44 @@ export const createRowInsertButton = (editor: Editor, tableInfo: TableInfo): HTM e.preventDefault(); e.stopPropagation(); - mouseDownY = e.clientY; - isDragging = false; - dragStarted = false; + dragState.mouseDownPosition = e[config.coordinate]; + dragState.isDragging = false; + dragState.dragStarted = false; - // Initialize with existing row count + // Initialize with existing item count const currentTableInfo = getCurrentTableInfo(editor, tableInfo); - const tableMapData = TableMap.get(currentTableInfo.tableNode); - originalRowCount = tableMapData.height; - rowsAdded = originalRowCount; // Current total rows + dragState.originalItemCount = handlers.getItemCount(currentTableInfo.tableNode); + dragState.itemsAdded = dragState.originalItemCount; document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); }; const onMouseMove = (e: MouseEvent) => { - const deltaY = e.clientY - mouseDownY; - const distance = Math.abs(deltaY); + const delta = e[config.coordinate] - dragState.mouseDownPosition; + const distance = Math.abs(delta); - if (!isDragging && distance > DRAG_THRESHOLD) { - isDragging = true; - dragStarted = true; + if (!dragState.isDragging && distance > config.dragThreshold) { + dragState.isDragging = true; + dragState.dragStarted = true; button.classList.add("dragging"); document.body.style.userSelect = "none"; } - if (isDragging) { - // Calculate target rows based on displacement from start position - let targetRows = originalRowCount; // Start with original count + if (dragState.isDragging) { + const targetItems = calculateTargetItems(delta, dragState.originalItemCount, config.actionThreshold); - if (deltaY > 0) { - // Moving down - add rows based on distance - let rowsToAdd = 0; - while (rowsToAdd * ACTION_THRESHOLD + ACTION_THRESHOLD / 2 <= deltaY) { - rowsToAdd++; - } - targetRows = originalRowCount + rowsToAdd; - } else if (deltaY < 0) { - // Moving up - remove rows based on distance - const upDistance = Math.abs(deltaY); - let rowsToRemove = 0; - while (rowsToRemove * ACTION_THRESHOLD + ACTION_THRESHOLD / 2 <= upDistance) { - rowsToRemove++; - } - targetRows = Math.max(1, originalRowCount - rowsToRemove); // Keep at least 1 row + // Add items if needed + while (dragState.itemsAdded < targetItems) { + handlers.insertItem(editor, tableInfo); + dragState.itemsAdded++; } - // Add rows if needed - while (rowsAdded < targetRows) { - insertRowAfterLast(editor, tableInfo); - rowsAdded++; - } - - // Remove rows if needed - while (rowsAdded > targetRows) { - const deleted = removeLastRow(editor, tableInfo); + // Remove items if needed + while (dragState.itemsAdded > targetItems) { + const deleted = handlers.removeItem(editor, tableInfo); if (deleted) { - rowsAdded--; + dragState.itemsAdded--; } else { break; } @@ -224,18 +318,17 @@ export const createRowInsertButton = (editor: Editor, tableInfo: TableInfo): HTM document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); - if (isDragging) { + if (dragState.isDragging) { button.classList.remove("dragging"); document.body.style.cursor = ""; document.body.style.userSelect = ""; - } else if (!dragStarted) { - insertRowAfterLast(editor, tableInfo); - rowsAdded++; + } else if (!dragState.dragStarted) { + handlers.insertItem(editor, tableInfo); + dragState.itemsAdded++; } - isDragging = false; - dragStarted = false; - // Don't reset rowsAdded and originalRowCount here - they'll be reset on next drag + dragState.isDragging = false; + dragState.dragStarted = false; }; button.addEventListener("mousedown", onMouseDown); @@ -245,22 +338,48 @@ export const createRowInsertButton = (editor: Editor, tableInfo: TableInfo): HTM return button; }; +const calculateTargetItems = (delta: number, originalCount: number, actionThreshold: number): number => { + let targetItems = originalCount; + + if (delta > 0) { + // Moving in positive direction - add items + let itemsToAdd = 0; + while (itemsToAdd * actionThreshold + actionThreshold / 2 <= delta) { + itemsToAdd++; + } + targetItems = originalCount + itemsToAdd; + } else if (delta < 0) { + // Moving in negative direction - remove items + const distance = Math.abs(delta); + let itemsToRemove = 0; + while (itemsToRemove * actionThreshold + actionThreshold / 2 <= distance) { + itemsToRemove++; + } + targetItems = Math.max(1, originalCount - itemsToRemove); + } + + return targetItems; +}; + +export const createColumnInsertButton = (editor: Editor, tableInfo: TableInfo): HTMLElement => + createInsertButton(editor, tableInfo, BUTTON_CONFIGS.column, columnHandlers); + +export const createRowInsertButton = (editor: Editor, tableInfo: TableInfo): HTMLElement => + createInsertButton(editor, tableInfo, BUTTON_CONFIGS.row, rowHandlers); + 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; } @@ -272,7 +391,7 @@ export const findAllTables = (editor: Editor): TableInfo[] => { if (domTable === tableElement) { tablePos = pos; tableNode = node; - return false; // Stop iteration + return false; } } }); @@ -290,173 +409,7 @@ export const findAllTables = (editor: Editor): TableInfo[] => { }; 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; -};