vault backup: 2025-04-25 13:28:07

This commit is contained in:
sShemet
2025-04-25 13:28:07 +05:00
parent 0a30b871d8
commit b458ae2756
2 changed files with 340 additions and 20 deletions

View File

@@ -0,0 +1,318 @@
# 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
<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.