@phuong-tran-redoc/document-engine-core 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -115
- package/THIRD-PARTY-NOTICES.txt +67 -0
- package/index.d.ts +1 -0
- package/index.esm.js +2238 -0
- package/package.json +21 -5
- package/src/__tests__/helpers/editor-factory.d.ts +0 -13
- package/src/__tests__/helpers/editor-factory.js +0 -25
- package/src/constants/indent.constant.js +0 -1
- package/src/constants/index.js +0 -1
- package/src/constants/table.constant.js +0 -1
- package/src/extensions/clear-content.extension.js +0 -9
- package/src/extensions/indent.extension.js +0 -59
- package/src/extensions/index.js +0 -8
- package/src/extensions/ordered-list.extension.js +0 -47
- package/src/extensions/reset-format.extension.js +0 -9
- package/src/extensions/reset-on-enter.extension.js +0 -37
- package/src/extensions/restricted-editing.extension.js +0 -376
- package/src/extensions/table-resizing.extension.js +0 -164
- package/src/extensions/table-style.extension.js +0 -481
- package/src/extensions/text-case.extension.js +0 -39
- package/src/index.js +0 -9
- package/src/kit/default-extensions.js +0 -86
- package/src/kit/generate-html.js +0 -40
- package/src/kit/index.js +0 -2
- package/src/migrations/doc-migrations.js +0 -24
- package/src/migrations/index.js +0 -3
- package/src/migrations/migrate-doc.js +0 -32
- package/src/migrations/types.js +0 -4
- package/src/models/color.model.js +0 -30
- package/src/models/index.js +0 -1
- package/src/nodes/dynamic-field.node.js +0 -141
- package/src/nodes/heading.node.js +0 -13
- package/src/nodes/image-ref.node.js +0 -138
- package/src/nodes/index.js +0 -4
- package/src/nodes/page-break.node.js +0 -52
- package/src/types/index.js +0 -2
- package/src/types/ordered-list.type.js +0 -2
- package/src/types/text-case.type.js +0 -1
- package/src/types/tiptap-html-server.d.js +0 -10
- package/src/utils/color.util.js +0 -31
- package/src/utils/dom.util.js +0 -33
- package/src/utils/index.js +0 -4
- package/src/utils/table.util.js +0 -95
- package/src/utils/text.util.js +0 -46
- package/src/views/block-handler.js +0 -311
- package/src/views/index.js +0 -1
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { Extension } from '@tiptap/core';
|
|
2
|
-
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
3
|
-
import { tableNodeTypes } from '@tiptap/pm/tables';
|
|
4
|
-
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
|
5
|
-
import { getTableDOMFromView } from '../utils';
|
|
6
|
-
/**
|
|
7
|
-
* Handler for mouse move (dragging)
|
|
8
|
-
*/ function handleMouseMoveEvent(event, dragInfo) {
|
|
9
|
-
if (!dragInfo.isDragging) return;
|
|
10
|
-
event.preventDefault();
|
|
11
|
-
const { startX, tableWidthPx, initialLeftWidth, initialRightWidth, leftColIndex, tablePos, tableNode, view } = dragInfo;
|
|
12
|
-
// 1. Compute the delta (pixels)
|
|
13
|
-
const deltaX = event.clientX - startX;
|
|
14
|
-
// 2. Convert the pixel delta into a percentage delta
|
|
15
|
-
// This is the most important piece of logic
|
|
16
|
-
const deltaPercent = deltaX / tableWidthPx * 100;
|
|
17
|
-
// 3. Compute the new widths
|
|
18
|
-
let newLeftWidth = initialLeftWidth + deltaPercent;
|
|
19
|
-
let newRightWidth = initialRightWidth - deltaPercent;
|
|
20
|
-
// 4. Apply constraints (e.g. min-width 5%)
|
|
21
|
-
const minWidth = 5; // min-width configuration
|
|
22
|
-
if (newLeftWidth < minWidth) {
|
|
23
|
-
const diff = minWidth - newLeftWidth;
|
|
24
|
-
newLeftWidth = minWidth;
|
|
25
|
-
newRightWidth = newRightWidth - diff; // Right column absorbs the shortfall
|
|
26
|
-
} else if (newRightWidth < minWidth) {
|
|
27
|
-
const diff = minWidth - newRightWidth;
|
|
28
|
-
newRightWidth = minWidth;
|
|
29
|
-
newLeftWidth = newLeftWidth - diff; // Left column absorbs the shortfall
|
|
30
|
-
}
|
|
31
|
-
// Make sure no column goes negative (in case of a very fast drag)
|
|
32
|
-
if (newLeftWidth < 0) newLeftWidth = 0;
|
|
33
|
-
if (newRightWidth < 0) newRightWidth = 0;
|
|
34
|
-
// 5. Build the new colwidths array
|
|
35
|
-
const newColWidths = [
|
|
36
|
-
...tableNode?.attrs?.['colwidths']
|
|
37
|
-
];
|
|
38
|
-
newColWidths[leftColIndex] = newLeftWidth;
|
|
39
|
-
newColWidths[leftColIndex + 1] = newRightWidth;
|
|
40
|
-
// 6. Dispatch a transaction to update the state
|
|
41
|
-
if (tableNode && view) {
|
|
42
|
-
const tr = view.state.tr.setNodeMarkup(tablePos, null, {
|
|
43
|
-
...tableNode.attrs,
|
|
44
|
-
colwidths: newColWidths
|
|
45
|
-
});
|
|
46
|
-
// Use dispatchTransaction so Tiptap/ProseMirror updates the UI itself
|
|
47
|
-
view.dispatch(tr);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Handler for mouse up (end of drag)
|
|
52
|
-
*/ function handleMouseUpEvent(event, dragInfo) {
|
|
53
|
-
if (!dragInfo.isDragging) return;
|
|
54
|
-
event.preventDefault();
|
|
55
|
-
// Reset the state
|
|
56
|
-
dragInfo = {
|
|
57
|
-
isDragging: false,
|
|
58
|
-
startX: 0,
|
|
59
|
-
tablePos: -1,
|
|
60
|
-
tableNode: null,
|
|
61
|
-
tableWidthPx: 0,
|
|
62
|
-
leftColIndex: -1,
|
|
63
|
-
initialLeftWidth: 0,
|
|
64
|
-
initialRightWidth: 0,
|
|
65
|
-
view: null
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Extension PercentageColumnResizing
|
|
70
|
-
* * Replaces the default column-resize logic with percentage-based logic.
|
|
71
|
-
*/ export const PercentageColumnResizing = Extension.create({
|
|
72
|
-
name: 'percentageColumnResizing',
|
|
73
|
-
addProseMirrorPlugins () {
|
|
74
|
-
// A temporary object to hold the drag-and-drop state
|
|
75
|
-
// We don't use Plugin State because this state never needs to be "undone"
|
|
76
|
-
let dragInfo = {
|
|
77
|
-
isDragging: false,
|
|
78
|
-
startX: 0,
|
|
79
|
-
tablePos: -1,
|
|
80
|
-
tableNode: null,
|
|
81
|
-
tableWidthPx: 0,
|
|
82
|
-
leftColIndex: -1,
|
|
83
|
-
initialLeftWidth: 0,
|
|
84
|
-
initialRightWidth: 0,
|
|
85
|
-
view: null
|
|
86
|
-
};
|
|
87
|
-
const editorInstance = this.editor;
|
|
88
|
-
return [
|
|
89
|
-
new Plugin({
|
|
90
|
-
key: new PluginKey('percentageColumnResizing'),
|
|
91
|
-
props: {
|
|
92
|
-
/**
|
|
93
|
-
* Draw the resize handles into the DOM
|
|
94
|
-
*/ decorations (state) {
|
|
95
|
-
if (!editorInstance?.isEditable) return;
|
|
96
|
-
const decorations = [];
|
|
97
|
-
const { doc } = state;
|
|
98
|
-
const tableTypes = tableNodeTypes(state.schema);
|
|
99
|
-
// Walk every node in the document
|
|
100
|
-
doc.descendants((node, pos)=>{
|
|
101
|
-
if (node.type.name !== tableTypes.table.name) return;
|
|
102
|
-
const colwidths = node.attrs['colwidths'];
|
|
103
|
-
if (!colwidths || colwidths.length === 0) return;
|
|
104
|
-
let accumulatedWidth = 0;
|
|
105
|
-
// Create a handle for N-1 columns (the last column doesn't need one)
|
|
106
|
-
for(let i = 0; i < colwidths.length - 1; i++){
|
|
107
|
-
accumulatedWidth += colwidths[i] ?? 0;
|
|
108
|
-
// Create a 'div' widget to act as the handle
|
|
109
|
-
const handle = document.createElement('div');
|
|
110
|
-
handle.className = 'pm-col-resizer';
|
|
111
|
-
handle.style.left = `calc(${accumulatedWidth}% - 2px)`;
|
|
112
|
-
// Store the column position so we know which column is being resized
|
|
113
|
-
handle.setAttribute('data-col-index', i.toString());
|
|
114
|
-
handle.setAttribute('data-table-pos', pos.toString());
|
|
115
|
-
// Place the widget right after the <table> tag (pos + 1)
|
|
116
|
-
decorations.push(Decoration.widget(pos + 1, handle));
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
return DecorationSet.create(doc, decorations);
|
|
120
|
-
},
|
|
121
|
-
/**
|
|
122
|
-
* Handle the mousedown event on a handle
|
|
123
|
-
*/ handleDOMEvents: {
|
|
124
|
-
mousedown (view, event) {
|
|
125
|
-
const target = event.target;
|
|
126
|
-
// Only trigger when clicking on a handle
|
|
127
|
-
if (!target.classList.contains('pm-col-resizer')) return false;
|
|
128
|
-
event.preventDefault();
|
|
129
|
-
const colIndex = parseInt(target.getAttribute('data-col-index') ?? '0', 10);
|
|
130
|
-
const tablePos = parseInt(target.getAttribute('data-table-pos') ?? '0', 10);
|
|
131
|
-
const tableNode = view.state.doc.nodeAt(tablePos);
|
|
132
|
-
const colwidths = tableNode?.attrs['colwidths'];
|
|
133
|
-
if (!tableNode || !colwidths) return false;
|
|
134
|
-
// Get table DOM element (handles NodeView wrappers and nested tables)
|
|
135
|
-
const tableDOM = getTableDOMFromView(view, tablePos);
|
|
136
|
-
if (!tableDOM) return false;
|
|
137
|
-
// Store the info needed for dragging
|
|
138
|
-
dragInfo = {
|
|
139
|
-
isDragging: true,
|
|
140
|
-
startX: event.clientX,
|
|
141
|
-
tablePos: tablePos,
|
|
142
|
-
tableNode: tableNode,
|
|
143
|
-
tableWidthPx: tableDOM.offsetWidth,
|
|
144
|
-
leftColIndex: colIndex,
|
|
145
|
-
initialLeftWidth: colwidths[colIndex],
|
|
146
|
-
initialRightWidth: colwidths[colIndex + 1],
|
|
147
|
-
view: view
|
|
148
|
-
};
|
|
149
|
-
const handleMouseMove = (event)=>handleMouseMoveEvent(event, dragInfo);
|
|
150
|
-
const handleMouseUp = (event)=>{
|
|
151
|
-
handleMouseUpEvent(event, dragInfo);
|
|
152
|
-
window.removeEventListener('mousemove', handleMouseMove, true);
|
|
153
|
-
window.removeEventListener('mouseup', handleMouseUp, true);
|
|
154
|
-
};
|
|
155
|
-
window.addEventListener('mousemove', handleMouseMove, true);
|
|
156
|
-
window.addEventListener('mouseup', handleMouseUp, true);
|
|
157
|
-
return true; // Event handled
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
})
|
|
162
|
-
];
|
|
163
|
-
}
|
|
164
|
-
});
|
|
@@ -1,481 +0,0 @@
|
|
|
1
|
-
import { Extension } from '@tiptap/core';
|
|
2
|
-
import { Table, TableCell, TableHeader, TableRow } from '@tiptap/extension-table';
|
|
3
|
-
import { TextSelection } from '@tiptap/pm/state';
|
|
4
|
-
import { addColumnAfter, addColumnBefore, deleteColumn, selectionCell, TableMap } from '@tiptap/pm/tables';
|
|
5
|
-
import { MIN_NEW_COL_WIDTH } from '../constants/table.constant';
|
|
6
|
-
import { getCursorCellInfo } from '../utils';
|
|
7
|
-
import { createTableNodeView } from '../views';
|
|
8
|
-
import { PercentageColumnResizing } from './table-resizing.extension';
|
|
9
|
-
/**
|
|
10
|
-
* Calculate new column widths when adding a column, ensuring no column goes below MIN_NEW_COL_WIDTH
|
|
11
|
-
* @param oldColWidths - Array of current column widths
|
|
12
|
-
* @returns Array of adjusted column widths (without the new column inserted yet)
|
|
13
|
-
*/ function calculateColumnWidthsForNewColumn(oldColWidths) {
|
|
14
|
-
// Find columns that can contribute (above minimum)
|
|
15
|
-
const columnsAboveMin = oldColWidths.filter((w)=>w > MIN_NEW_COL_WIDTH);
|
|
16
|
-
// If not enough space, proportionally reduce all columns
|
|
17
|
-
if (columnsAboveMin.length === 0) {
|
|
18
|
-
const reduction = MIN_NEW_COL_WIDTH / oldColWidths.length;
|
|
19
|
-
return oldColWidths.map((w)=>w - reduction);
|
|
20
|
-
}
|
|
21
|
-
// Calculate available space from columns above minimum
|
|
22
|
-
const availableSpace = columnsAboveMin.reduce((sum, w)=>sum + (w - MIN_NEW_COL_WIDTH), 0);
|
|
23
|
-
// If we have enough space, only tax columns above minimum
|
|
24
|
-
if (availableSpace >= MIN_NEW_COL_WIDTH) {
|
|
25
|
-
return oldColWidths.map((w)=>{
|
|
26
|
-
if (w > MIN_NEW_COL_WIDTH) {
|
|
27
|
-
const contribution = (w - MIN_NEW_COL_WIDTH) / availableSpace * MIN_NEW_COL_WIDTH;
|
|
28
|
-
return Math.max(MIN_NEW_COL_WIDTH, w - contribution);
|
|
29
|
-
}
|
|
30
|
-
return w;
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
// Not enough space even from columns above min, reduce proportionally
|
|
34
|
-
const reduction = MIN_NEW_COL_WIDTH / oldColWidths.length;
|
|
35
|
-
return oldColWidths.map((w)=>w - reduction);
|
|
36
|
-
}
|
|
37
|
-
export const TableDefaultAttributes = {
|
|
38
|
-
border: {
|
|
39
|
-
style: 'double',
|
|
40
|
-
color: '#e5e7eb',
|
|
41
|
-
width: '1px'
|
|
42
|
-
},
|
|
43
|
-
backgroundColor: null,
|
|
44
|
-
colwidths: null,
|
|
45
|
-
cellBorder: {
|
|
46
|
-
style: 'solid',
|
|
47
|
-
color: '#e5e7eb',
|
|
48
|
-
width: '1px'
|
|
49
|
-
},
|
|
50
|
-
cellBackgroundColor: null,
|
|
51
|
-
cellVerticalAlign: 'middle',
|
|
52
|
-
cellTextAlign: null
|
|
53
|
-
};
|
|
54
|
-
// Extend TableCell and TableHeader to support styling attributes
|
|
55
|
-
const commonCellAttributes = {
|
|
56
|
-
backgroundColor: {
|
|
57
|
-
default: null,
|
|
58
|
-
renderHTML: (attrs)=>attrs.backgroundColor ? {
|
|
59
|
-
style: `background-color: ${attrs.backgroundColor};`
|
|
60
|
-
} : {},
|
|
61
|
-
parseHTML: (element)=>element.style.getPropertyValue('background-color') || null
|
|
62
|
-
},
|
|
63
|
-
border: {
|
|
64
|
-
default: TableDefaultAttributes.cellBorder,
|
|
65
|
-
renderHTML: (attrs)=>{
|
|
66
|
-
const border = attrs['border'];
|
|
67
|
-
if (!border) return {};
|
|
68
|
-
const styles = [];
|
|
69
|
-
if (border.style) styles.push(`border-style: ${border.style}`);
|
|
70
|
-
if (border.color) styles.push(`border-color: ${border.color}`);
|
|
71
|
-
if (border.width) styles.push(`border-width: ${border.width}`);
|
|
72
|
-
return styles.length ? {
|
|
73
|
-
style: styles.join('; ') + ';'
|
|
74
|
-
} : {};
|
|
75
|
-
},
|
|
76
|
-
parseHTML: (element)=>{
|
|
77
|
-
const style = element.style.getPropertyValue('border-style');
|
|
78
|
-
const color = element.style.getPropertyValue('border-color');
|
|
79
|
-
const width = element.style.getPropertyValue('border-width');
|
|
80
|
-
if (!style && !color && !width) return null;
|
|
81
|
-
return {
|
|
82
|
-
style: style || null,
|
|
83
|
-
color: color || null,
|
|
84
|
-
width: width || null
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
verticalAlign: {
|
|
89
|
-
default: TableDefaultAttributes.cellVerticalAlign,
|
|
90
|
-
renderHTML: (attrs)=>attrs.verticalAlign ? {
|
|
91
|
-
style: `vertical-align: ${attrs.verticalAlign};`
|
|
92
|
-
} : {},
|
|
93
|
-
parseHTML: (element)=>element.style.getPropertyValue('vertical-align') || null
|
|
94
|
-
},
|
|
95
|
-
textAlign: {
|
|
96
|
-
default: TableDefaultAttributes.cellTextAlign,
|
|
97
|
-
renderHTML: (attrs)=>attrs.textAlign ? {
|
|
98
|
-
style: `text-align: ${attrs.textAlign};`
|
|
99
|
-
} : {},
|
|
100
|
-
parseHTML: (element)=>element.style.getPropertyValue('text-align') || null
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
export const StyledTableCell = TableCell.extend({
|
|
104
|
-
addAttributes () {
|
|
105
|
-
const parent = this.parent?.() ?? {};
|
|
106
|
-
return {
|
|
107
|
-
...parent,
|
|
108
|
-
...commonCellAttributes
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
export const StyledTableHeader = TableHeader.extend({
|
|
113
|
-
addAttributes () {
|
|
114
|
-
const parent = this.parent?.() ?? {};
|
|
115
|
-
return {
|
|
116
|
-
...parent,
|
|
117
|
-
...commonCellAttributes
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
// Extend Table node to support width (%) and alignment (left|center|right)
|
|
122
|
-
export const StyledTable = Table.extend({
|
|
123
|
-
// name: 'styledTable',
|
|
124
|
-
// draggable: true,
|
|
125
|
-
addOptions () {
|
|
126
|
-
return {
|
|
127
|
-
...this.parent?.(),
|
|
128
|
-
enableNodeView: true
|
|
129
|
-
};
|
|
130
|
-
},
|
|
131
|
-
addAttributes () {
|
|
132
|
-
const parent = this.parent?.() ?? {};
|
|
133
|
-
return {
|
|
134
|
-
...parent,
|
|
135
|
-
border: {
|
|
136
|
-
default: null,
|
|
137
|
-
renderHTML: (attrs)=>{
|
|
138
|
-
const border = attrs.border;
|
|
139
|
-
if (!border) return {};
|
|
140
|
-
const styles = [];
|
|
141
|
-
if (border.style) styles.push(`border-style: ${border.style}`);
|
|
142
|
-
if (border.color) styles.push(`border-color: ${border.color}`);
|
|
143
|
-
if (border.width) styles.push(`border-width: ${border.width}`);
|
|
144
|
-
return styles.length ? {
|
|
145
|
-
style: styles.join('; ') + ';'
|
|
146
|
-
} : {};
|
|
147
|
-
},
|
|
148
|
-
parseHTML: (element)=>{
|
|
149
|
-
const style = element.style.getPropertyValue('border-style');
|
|
150
|
-
const color = element.style.getPropertyValue('border-color');
|
|
151
|
-
const width = element.style.getPropertyValue('border-width');
|
|
152
|
-
if (!style && !color && !width) return null;
|
|
153
|
-
return {
|
|
154
|
-
style: style || null,
|
|
155
|
-
color: color || null,
|
|
156
|
-
width: width || null
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
backgroundColor: {
|
|
161
|
-
default: null,
|
|
162
|
-
renderHTML: (attrs)=>attrs.backgroundColor ? {
|
|
163
|
-
style: `background-color: ${attrs.backgroundColor};`
|
|
164
|
-
} : {},
|
|
165
|
-
parseHTML: (element)=>element.style.getPropertyValue('background-color') || null
|
|
166
|
-
},
|
|
167
|
-
colwidths: {
|
|
168
|
-
default: null,
|
|
169
|
-
renderHTML: (attributes)=>{
|
|
170
|
-
if (!attributes['colwidths']) {
|
|
171
|
-
return {}; // Nothing to render
|
|
172
|
-
}
|
|
173
|
-
return {
|
|
174
|
-
'data-colwidths': JSON.stringify(attributes['colwidths'])
|
|
175
|
-
};
|
|
176
|
-
},
|
|
177
|
-
parseHTML: (element)=>{
|
|
178
|
-
// Only get cols from direct child colgroup (not from nested tables)
|
|
179
|
-
const colgroup = element.querySelector(':scope > colgroup');
|
|
180
|
-
if (!colgroup) return null;
|
|
181
|
-
const cols = colgroup.querySelectorAll('col');
|
|
182
|
-
if (cols.length === 0) return null;
|
|
183
|
-
const colwidths = [];
|
|
184
|
-
cols.forEach((col)=>{
|
|
185
|
-
const width = col.getAttribute('style');
|
|
186
|
-
if (width) {
|
|
187
|
-
const match = width.match(/width:\s*(\d+(?:\.\d+)?)%/);
|
|
188
|
-
colwidths.push(match ? parseFloat(match[1]) : null);
|
|
189
|
-
} else {
|
|
190
|
-
colwidths.push(null);
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
return colwidths.length > 0 ? colwidths : null;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
},
|
|
198
|
-
renderHTML ({ node, HTMLAttributes }) {
|
|
199
|
-
const colwidths = node.attrs?.['colwidths'];
|
|
200
|
-
const domNodes = [];
|
|
201
|
-
// Render <colgroup> when colwidths are present
|
|
202
|
-
if (colwidths) {
|
|
203
|
-
const colgroup = [
|
|
204
|
-
'colgroup',
|
|
205
|
-
{}
|
|
206
|
-
];
|
|
207
|
-
colwidths.forEach((width)=>{
|
|
208
|
-
colgroup.push([
|
|
209
|
-
'col',
|
|
210
|
-
{
|
|
211
|
-
style: width ? `width: ${width}%` : null
|
|
212
|
-
}
|
|
213
|
-
]);
|
|
214
|
-
});
|
|
215
|
-
domNodes.push(colgroup);
|
|
216
|
-
}
|
|
217
|
-
// Add <tbody> (0 is the 'content hole')
|
|
218
|
-
domNodes.push([
|
|
219
|
-
'tbody',
|
|
220
|
-
0
|
|
221
|
-
]);
|
|
222
|
-
// Merge the HTML attributes (including global attributes)
|
|
223
|
-
// HTMLAttributes has already been merged by Tiptap via getRenderedAttributes
|
|
224
|
-
return [
|
|
225
|
-
'table',
|
|
226
|
-
HTMLAttributes,
|
|
227
|
-
...domNodes
|
|
228
|
-
];
|
|
229
|
-
},
|
|
230
|
-
addCommands () {
|
|
231
|
-
const parent = this.parent?.() ?? {};
|
|
232
|
-
return {
|
|
233
|
-
...parent,
|
|
234
|
-
// Table
|
|
235
|
-
setTableBorder: (border)=>({ chain, state })=>{
|
|
236
|
-
if (!border) return chain().focus().updateAttributes('table', {
|
|
237
|
-
border: null
|
|
238
|
-
}).run();
|
|
239
|
-
// Get current table border and merge with new values
|
|
240
|
-
// Find the table node
|
|
241
|
-
const cellInfo = getCursorCellInfo(state);
|
|
242
|
-
if (!cellInfo) return false;
|
|
243
|
-
const currentBorder = cellInfo?.tableNode?.attrs?.['border'] || {};
|
|
244
|
-
const mergedBorder = {
|
|
245
|
-
style: border.style !== undefined ? border.style : currentBorder.style || null,
|
|
246
|
-
color: border.color !== undefined ? border.color : currentBorder.color || null,
|
|
247
|
-
width: border.width !== undefined ? border.width : currentBorder.width || null
|
|
248
|
-
};
|
|
249
|
-
return chain().focus().updateAttributes('table', {
|
|
250
|
-
border: mergedBorder
|
|
251
|
-
}).run();
|
|
252
|
-
},
|
|
253
|
-
setTableBackgroundColor: (color)=>({ chain })=>{
|
|
254
|
-
return chain().focus().updateAttributes('table', {
|
|
255
|
-
backgroundColor: color ?? null
|
|
256
|
-
}).run();
|
|
257
|
-
},
|
|
258
|
-
insertTable: ({ rows, cols, withHeaderRow })=>({ tr, dispatch, editor })=>{
|
|
259
|
-
const { schema } = editor.state;
|
|
260
|
-
// --- STEP 1: Compute colWidths ---
|
|
261
|
-
const calculatedColWidths = [];
|
|
262
|
-
const defaultWidth = 100 / cols;
|
|
263
|
-
for(let i = 0; i < cols; i += 1){
|
|
264
|
-
calculatedColWidths.push(defaultWidth);
|
|
265
|
-
}
|
|
266
|
-
// Example: cols = 3 -> calculatedColWidths = [33.33, 33.33, 33.33]
|
|
267
|
-
// --- STEP 2: Build the table content (rows, cells) ---
|
|
268
|
-
// We have to build the child nodes ourselves
|
|
269
|
-
const cellType = schema.nodes['tableCell'];
|
|
270
|
-
const headerType = schema.nodes['tableHeader'];
|
|
271
|
-
const rowType = schema.nodes['tableRow'];
|
|
272
|
-
const tableRows = [];
|
|
273
|
-
// Helper to create a single empty cell
|
|
274
|
-
const createEmptyCell = (type)=>{
|
|
275
|
-
const emptyParagraph = schema.nodes['paragraph'].create();
|
|
276
|
-
return type.create(null, emptyParagraph);
|
|
277
|
-
};
|
|
278
|
-
// Create the rows
|
|
279
|
-
for(let r = 0; r < rows; r += 1){
|
|
280
|
-
const tableCells = [];
|
|
281
|
-
for(let c = 0; c < cols; c += 1){
|
|
282
|
-
let cellNode;
|
|
283
|
-
// If this is the first row and 'withHeaderRow' is set
|
|
284
|
-
if (r === 0 && withHeaderRow) {
|
|
285
|
-
cellNode = createEmptyCell(headerType);
|
|
286
|
-
} else {
|
|
287
|
-
cellNode = createEmptyCell(cellType);
|
|
288
|
-
}
|
|
289
|
-
tableCells.push(cellNode);
|
|
290
|
-
}
|
|
291
|
-
// Add the new row to the rows array
|
|
292
|
-
tableRows.push(rowType.create(null, tableCells));
|
|
293
|
-
}
|
|
294
|
-
// --- STEP 3: Create the main Table node WITH the 'colwidths' ATTRIBUTE ---
|
|
295
|
-
const tableNode = schema.nodes['table'].create({
|
|
296
|
-
// This is the key part: assign our percentage array here
|
|
297
|
-
colwidths: calculatedColWidths
|
|
298
|
-
}, tableRows // Content (the 'tableRow' nodes)
|
|
299
|
-
);
|
|
300
|
-
// --- STEP 4: Dispatch transaction (Original code)---
|
|
301
|
-
if (dispatch) {
|
|
302
|
-
const offset = tr.selection.from + 1;
|
|
303
|
-
tr.replaceSelectionWith(tableNode).scrollIntoView().setSelection(TextSelection.near(tr.doc.resolve(offset)));
|
|
304
|
-
}
|
|
305
|
-
return true;
|
|
306
|
-
},
|
|
307
|
-
// 2. ADDCOLUMNAFTER COMMAND
|
|
308
|
-
addColumnAfter: ()=>({ state, dispatch })=>{
|
|
309
|
-
const $cell = selectionCell(state);
|
|
310
|
-
if (!$cell) return false;
|
|
311
|
-
const table = $cell.node(-1);
|
|
312
|
-
const tablePos = $cell.start(-1) - 1;
|
|
313
|
-
const oldColWidths = table.attrs['colwidths'];
|
|
314
|
-
// Calculate column index
|
|
315
|
-
const map = TableMap.get(table);
|
|
316
|
-
const cellPosInTable = $cell.pos - $cell.start(-1);
|
|
317
|
-
const colIndex = map.colCount(cellPosInTable);
|
|
318
|
-
// Calculate new column widths - ensure no column goes below MIN_NEW_COL_WIDTH
|
|
319
|
-
const newColWidths = calculateColumnWidthsForNewColumn(oldColWidths);
|
|
320
|
-
// Insert new column width
|
|
321
|
-
newColWidths.splice(colIndex + 1, 0, MIN_NEW_COL_WIDTH);
|
|
322
|
-
if (dispatch) {
|
|
323
|
-
// Use the prosemirror-tables function directly to add column
|
|
324
|
-
// This modifies the transaction but doesn't dispatch it
|
|
325
|
-
addColumnAfter(state, (modifiedTr)=>{
|
|
326
|
-
// Add our colwidth changes to the same transaction
|
|
327
|
-
modifiedTr.setNodeMarkup(tablePos, null, {
|
|
328
|
-
...table.attrs,
|
|
329
|
-
colwidths: newColWidths
|
|
330
|
-
});
|
|
331
|
-
// Now dispatch the complete transaction
|
|
332
|
-
dispatch(modifiedTr.scrollIntoView());
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
return true;
|
|
336
|
-
},
|
|
337
|
-
// 3. ADDCOLUMNBEFORE COMMAND
|
|
338
|
-
addColumnBefore: ()=>({ state, dispatch })=>{
|
|
339
|
-
const $cell = selectionCell(state);
|
|
340
|
-
if (!$cell) return false;
|
|
341
|
-
const table = $cell.node(-1);
|
|
342
|
-
const tablePos = $cell.start(-1) - 1;
|
|
343
|
-
const oldColWidths = table.attrs['colwidths'];
|
|
344
|
-
// Calculate column index
|
|
345
|
-
const map = TableMap.get(table);
|
|
346
|
-
const cellPosInTable = $cell.pos - $cell.start(-1);
|
|
347
|
-
const colIndex = map.colCount(cellPosInTable);
|
|
348
|
-
// Calculate new column widths - ensure no column goes below MIN_NEW_COL_WIDTH
|
|
349
|
-
const newColWidths = calculateColumnWidthsForNewColumn(oldColWidths);
|
|
350
|
-
// Insert new column width at current column index (before)
|
|
351
|
-
newColWidths.splice(colIndex, 0, MIN_NEW_COL_WIDTH);
|
|
352
|
-
if (dispatch) {
|
|
353
|
-
addColumnBefore(state, (modifiedTr)=>{
|
|
354
|
-
modifiedTr.setNodeMarkup(tablePos, null, {
|
|
355
|
-
...table.attrs,
|
|
356
|
-
colwidths: newColWidths
|
|
357
|
-
});
|
|
358
|
-
dispatch(modifiedTr.scrollIntoView());
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
return true;
|
|
362
|
-
},
|
|
363
|
-
// 4. DELETECOLUMN COMMAND
|
|
364
|
-
deleteColumn: ()=>({ state, dispatch })=>{
|
|
365
|
-
const $cell = selectionCell(state);
|
|
366
|
-
if (!$cell) return false;
|
|
367
|
-
const table = $cell.node(-1);
|
|
368
|
-
const tablePos = $cell.start(-1) - 1;
|
|
369
|
-
const oldColWidths = table.attrs['colwidths'];
|
|
370
|
-
// Calculate column index
|
|
371
|
-
const map = TableMap.get(table);
|
|
372
|
-
const cellPosInTable = $cell.pos - $cell.start(-1);
|
|
373
|
-
const colIndex = map.colCount(cellPosInTable);
|
|
374
|
-
// Get width of column being removed
|
|
375
|
-
const removedWidth = oldColWidths[colIndex];
|
|
376
|
-
// Create array of remaining columns
|
|
377
|
-
const remainingWidths = oldColWidths.filter((_, i)=>i !== colIndex);
|
|
378
|
-
// Distribute removed width among remaining columns
|
|
379
|
-
const gainPerCol = removedWidth / remainingWidths.length;
|
|
380
|
-
const newColWidths = remainingWidths.map((w)=>w + gainPerCol);
|
|
381
|
-
if (dispatch) {
|
|
382
|
-
deleteColumn(state, (modifiedTr)=>{
|
|
383
|
-
modifiedTr.setNodeMarkup(tablePos, null, {
|
|
384
|
-
...table.attrs,
|
|
385
|
-
colwidths: newColWidths
|
|
386
|
-
});
|
|
387
|
-
dispatch(modifiedTr.scrollIntoView());
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
return true;
|
|
391
|
-
},
|
|
392
|
-
// Cell
|
|
393
|
-
setCellBorder: (border)=>({ chain, state })=>{
|
|
394
|
-
if (!border) return chain().focus().updateAttributes('tableCell', {
|
|
395
|
-
border: null
|
|
396
|
-
}).updateAttributes('tableHeader', {
|
|
397
|
-
border: null
|
|
398
|
-
}).run();
|
|
399
|
-
// Get current border and merge with new values
|
|
400
|
-
const { selection } = state;
|
|
401
|
-
const { $from } = selection;
|
|
402
|
-
const cellPos = $from.node(-1);
|
|
403
|
-
const currentBorder = cellPos?.attrs?.['border'] || {};
|
|
404
|
-
const mergedBorder = {
|
|
405
|
-
style: border.style ? border.style : currentBorder.style || null,
|
|
406
|
-
color: border.color ? border.color : currentBorder.color || null,
|
|
407
|
-
width: border.width ? border.width : currentBorder.width || null
|
|
408
|
-
};
|
|
409
|
-
return chain().focus().updateAttributes('tableCell', {
|
|
410
|
-
border: mergedBorder
|
|
411
|
-
}).updateAttributes('tableHeader', {
|
|
412
|
-
border: mergedBorder
|
|
413
|
-
}).run();
|
|
414
|
-
},
|
|
415
|
-
setCellVerticalAlign: (align)=>({ chain })=>{
|
|
416
|
-
return chain().focus().updateAttributes('tableCell', {
|
|
417
|
-
verticalAlign: align ?? null
|
|
418
|
-
}).updateAttributes('tableHeader', {
|
|
419
|
-
verticalAlign: align ?? null
|
|
420
|
-
}).run();
|
|
421
|
-
},
|
|
422
|
-
setCellTextAlign: (align)=>({ chain })=>{
|
|
423
|
-
return chain().focus().updateAttributes('tableCell', {
|
|
424
|
-
textAlign: align ?? null
|
|
425
|
-
}).updateAttributes('tableHeader', {
|
|
426
|
-
textAlign: align ?? null
|
|
427
|
-
}).run();
|
|
428
|
-
},
|
|
429
|
-
setCellBackgroundColor: (color)=>({ chain })=>{
|
|
430
|
-
// Set all cells to the same background color
|
|
431
|
-
return chain().focus().updateAttributes('tableCell', {
|
|
432
|
-
backgroundColor: color ?? null
|
|
433
|
-
}).updateAttributes('tableHeader', {
|
|
434
|
-
backgroundColor: color ?? null
|
|
435
|
-
}).run();
|
|
436
|
-
}
|
|
437
|
-
};
|
|
438
|
-
},
|
|
439
|
-
addNodeView () {
|
|
440
|
-
const options = this.options;
|
|
441
|
-
if (options.enableNodeView === false) {
|
|
442
|
-
return null;
|
|
443
|
-
}
|
|
444
|
-
// Only use custom NodeView (with handle) when editor is editable
|
|
445
|
-
return (props)=>{
|
|
446
|
-
// Check if editor is editable
|
|
447
|
-
if (!props.editor.isEditable) {
|
|
448
|
-
// Return undefined to use default table rendering (no widget wrapper)
|
|
449
|
-
return undefined;
|
|
450
|
-
}
|
|
451
|
-
// Editor is editable → use custom NodeView with handle
|
|
452
|
-
return createTableNodeView(props);
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
// Convenience export for consumers who want the full set
|
|
457
|
-
export const StyledTableKit = Extension.create({
|
|
458
|
-
name: 'styledTableKit',
|
|
459
|
-
addExtensions () {
|
|
460
|
-
const extensions = [];
|
|
461
|
-
if (this.options.table !== false) {
|
|
462
|
-
extensions.push(StyledTable.configure({
|
|
463
|
-
...this.options.table,
|
|
464
|
-
resizable: false
|
|
465
|
-
}));
|
|
466
|
-
}
|
|
467
|
-
if (this.options.tableRow !== false) {
|
|
468
|
-
extensions.push(TableRow.configure(this.options.tableRow));
|
|
469
|
-
}
|
|
470
|
-
if (this.options.tableHeader !== false) {
|
|
471
|
-
extensions.push(StyledTableHeader.configure(this.options.tableHeader));
|
|
472
|
-
}
|
|
473
|
-
if (this.options.tableCell !== false) {
|
|
474
|
-
extensions.push(StyledTableCell.configure(this.options.tableCell));
|
|
475
|
-
}
|
|
476
|
-
if (this.options.table?.resizable !== false) {
|
|
477
|
-
extensions.push(PercentageColumnResizing);
|
|
478
|
-
}
|
|
479
|
-
return extensions;
|
|
480
|
-
}
|
|
481
|
-
});
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { Extension } from '@tiptap/core';
|
|
2
|
-
import { TextSelection } from '@tiptap/pm/state';
|
|
3
|
-
export const TextCase = Extension.create({
|
|
4
|
-
name: 'textCase',
|
|
5
|
-
addOptions () {
|
|
6
|
-
return {
|
|
7
|
-
type: 'uppercase'
|
|
8
|
-
};
|
|
9
|
-
},
|
|
10
|
-
addCommands () {
|
|
11
|
-
return {
|
|
12
|
-
textCase: (type)=>({ tr, state })=>{
|
|
13
|
-
const { $from, $to, empty } = tr.selection;
|
|
14
|
-
// If selection is empty, return false
|
|
15
|
-
if (empty) return false;
|
|
16
|
-
// If selection is not empty, get the text of the selection
|
|
17
|
-
const text = state.doc.textBetween($from.pos, $to.pos, '\n', '\n');
|
|
18
|
-
// If text is empty, return false
|
|
19
|
-
if (text.length === 0) return false;
|
|
20
|
-
let newText = text;
|
|
21
|
-
// Change the text case
|
|
22
|
-
switch(type){
|
|
23
|
-
case 'uppercase':
|
|
24
|
-
newText = text.toUpperCase();
|
|
25
|
-
break;
|
|
26
|
-
case 'lowercase':
|
|
27
|
-
newText = text.toLowerCase();
|
|
28
|
-
break;
|
|
29
|
-
case 'capitalize':
|
|
30
|
-
newText = text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
|
|
31
|
-
break;
|
|
32
|
-
}
|
|
33
|
-
tr.replaceWith($from.pos, $to.pos, state.schema.text(newText));
|
|
34
|
-
tr.setSelection(TextSelection.create(tr.doc, $from.pos, $from.pos + newText.length));
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
});
|