Files
SergObsidian/WORK & PROJECTS/Mol/Code Chunks/Tiptap resizeTableColumnWidth.md
2025-04-25 13:28:07 +05:00

9.1 KiB

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)

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

// 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

<template>
  <div>
    <editor-content :editor="editor" />
  </div>
</template>

<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import Table from '@tiptap/extension-table'
import TableRow from '@tiptap/extension-table-row'
import TableHeader from '@tiptap/extension-table-header'
import TableCell from '@tiptap/extension-table-cell'
import { ResizableTableExtension } from './ResizableTableExtension'

export default {
  components: {
    EditorContent,
  },

  data() {
    return {
      editor: null,
    }
  },

  mounted() {
    this.editor = new Editor({
      extensions: [
        StarterKit,
        ResizableTableExtension,
        TableRow,
        TableHeader,
        TableCell.configure({
          HTMLAttributes: {
            style: 'position: relative;',
          },
        }),
      ],
      content: `
        <table>
          <tr>
            <th>Header 1</th>
            <th>Header 2</th>
            <th>Header 3</th>
          </tr>
          <tr>
            <td>Cell 1</td>
            <td>Cell 2</td>
            <td>Cell 3</td>
          </tr>
        </table>
      `,
    })
  },

  beforeUnmount() {
    this.editor.destroy()
  },
}
</script>

<style>
.column-resize-handle {
  position: absolute;
  top: 0;
  right: 0;
  width: 5px;
  height: 100%;
  cursor: col-resize;
  background-color: transparent;
  z-index: 10;
}

.column-resize-handle:hover {
  background-color: #adf;
}

table {
  border-collapse: collapse;
  margin: 1rem 0;
  width: 100%;
}

th, td {
  border: 1px solid #ddd;
  padding: 8px;
  position: relative;
}
</style>

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.