# Creating a Tiptap Plugin for Resizable Table Columns in Vue 3 To create a Tiptap plugin that allows users to resize table columns and persists the width information in the node's JSON, you'll need to: 1. Extend the Table extension 2. Add resize handles 3. Handle mouse events for resizing 4. Store column widths in node attributes Here's a comprehensive solution: ## 1. First, install required dependencies (if not already installed) ```bash npm install @tiptap/vue-3 @tiptap/pm @tiptap/starter-kit @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-header @tiptap/extension-table-cell ``` ## 2. Create a custom Table extension with resizable columns ```javascript // ResizableTableExtension.js import { Table } from '@tiptap/extension-table' import { Plugin, PluginKey } from '@tiptap/pm/state' export const ResizableTableExtension = Table.extend({ addAttributes() { return { ...this.parent?.(), colWidths: { default: null, parseHTML: element => element.getAttribute('data-col-widths'), renderHTML: attributes => { if (!attributes.colWidths) { return {} } return { 'data-col-widths': attributes.colWidths.join(',') } } } } }, addProseMirrorPlugins() { const parentPlugins = this.parent?.() || [] return [ ...parentPlugins, resizableColumnsPlugin({ handleWidth: 5, cellMinWidth: 25, onColumnResize: (colIndex, width, colWidths, node) => { const transaction = this.editor.state.tr.setNodeMarkup( this.editor.state.selection.$anchor.posAtIndex(0), undefined, { ...node.attrs, colWidths: colWidths } ) this.editor.view.dispatch(transaction) } }) ] } }) function resizableColumnsPlugin(options) { const pluginKey = new PluginKey('resizableColumns') let resizing = false let startX, startWidth, colIndex, table, colWidths return new Plugin({ key: pluginKey, props: { handleDOMEvents: { mousedown: (view, event) => { const target = event.target if (target.classList.contains('column-resize-handle')) { event.preventDefault() startResizing(view, target, event.clientX) return true } }, mousemove: (view, event) => { if (!resizing) return const tableRect = table.getBoundingClientRect() const totalWidth = tableRect.width const newWidth = startWidth + (event.clientX - startX) const percentageWidth = (newWidth / totalWidth) * 100 // Update the column widths array const newColWidths = [...colWidths] newColWidths[colIndex] = Math.max(options.cellMinWidth, percentageWidth) // Apply the new widths to the table columns applyColumnWidths(table, newColWidths) if (options.onColumnResize) { const node = view.state.doc.nodeAt(view.state.selection.$anchor.posAtIndex(0)) options.onColumnResize(colIndex, percentageWidth, newColWidths, node) } return true }, mouseup: () => { if (resizing) { resizing = false document.body.style.cursor = '' return true } } } }, view: (view) => { return { update: (view) => { // Add resize handles to columns const tables = view.dom.querySelectorAll('table') tables.forEach(table => { const rows = table.querySelectorAll('tr') if (rows.length === 0) return // Remove existing handles const existingHandles = table.querySelectorAll('.column-resize-handle') existingHandles.forEach(handle => handle.remove()) // Get column widths from node attributes or calculate equal distribution const node = view.state.doc.nodeAt(view.state.selection.$anchor.posAtIndex(0)) const attrs = node?.attrs || {} colWidths = attrs.colWidths ? attrs.colWidths.split(',').map(Number) : Array(rows[0].cells.length).fill(100 / rows[0].cells.length) // Apply initial column widths applyColumnWidths(table, colWidths) // Add resize handles const firstRow = rows[0] Array.from(firstRow.cells).forEach((cell, index) => { const handle = document.createElement('div') handle.className = 'column-resize-handle' handle.style.position = 'absolute' handle.style.top = '0' handle.style.right = '0' handle.style.width = `${options.handleWidth}px` handle.style.height = '100%' handle.style.cursor = 'col-resize' handle.style.backgroundColor = 'transparent' handle.style.zIndex = '10' cell.style.position = 'relative' cell.appendChild(handle) }) }) } } } }) function startResizing(view, handle, clientX) { resizing = true startX = clientX const cell = handle.parentElement const row = cell.parentElement table = row.closest('table') colIndex = Array.from(row.cells).indexOf(cell) // Get current column widths const node = view.state.doc.nodeAt(view.state.selection.$anchor.posAtIndex(0)) const attrs = node?.attrs || {} colWidths = attrs.colWidths ? attrs.colWidths.split(',').map(Number) : Array(row.cells.length).fill(100 / row.cells.length) const tableRect = table.getBoundingClientRect() const totalWidth = tableRect.width startWidth = (colWidths[colIndex] / 100) * totalWidth document.body.style.cursor = 'col-resize' } function applyColumnWidths(table, widths) { const rows = table.querySelectorAll('tr') if (rows.length === 0) return rows.forEach(row => { Array.from(row.cells).forEach((cell, index) => { cell.style.width = `${widths[index]}%` }) }) } } ``` ## 3. Use the extension in your Vue 3 component ```vue ``` ## Key Features of This Implementation: 1. **Column Width Storage**: Column widths are stored in the table node's attributes as a comma-separated string of percentages. 2. **Resize Handles**: Adds invisible resize handles to each column that become active on hover. 3. **Persistence**: When you save the editor content to JSON, the column widths are preserved in the node attributes. 4. **Initialization**: If no widths are specified, columns are initially equally distributed. 5. **Minimum Width**: Enforces a minimum column width to prevent columns from becoming too narrow. ## Usage Notes: - The plugin adds resize handles to the right side of each column header - Users can drag these handles to resize columns - The widths are stored as percentages to maintain responsiveness - The data is saved in the table node's attributes and will be included when you call `editor.getJSON()` ## Customization Options: You can adjust these parameters in the plugin options: - `handleWidth`: Width of the resize handle in pixels - `cellMinWidth`: Minimum column width in percentage - `onColumnResize`: Callback function when a column is resized This implementation provides a complete solution for resizable table columns in Tiptap with Vue 3, with proper persistence of column widths in the document JSON.