@phuong-tran-redoc/document-engine-core 0.0.12
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/LICENSE.md +19 -0
- package/README.md +224 -0
- package/package.json +30 -0
- package/src/constants/indent.constant.d.ts +1 -0
- package/src/constants/indent.constant.js +11 -0
- package/src/constants/index.d.ts +1 -0
- package/src/constants/index.js +18 -0
- package/src/constants/table.constant.d.ts +1 -0
- package/src/constants/table.constant.js +11 -0
- package/src/extensions/clear-content.extension.d.ts +9 -0
- package/src/extensions/clear-content.extension.js +19 -0
- package/src/extensions/indent.extension.d.ts +16 -0
- package/src/extensions/indent.extension.js +69 -0
- package/src/extensions/index.d.ts +8 -0
- package/src/extensions/index.js +25 -0
- package/src/extensions/ordered-list.extension.d.ts +13 -0
- package/src/extensions/ordered-list.extension.js +48 -0
- package/src/extensions/reset-format.extension.d.ts +9 -0
- package/src/extensions/reset-format.extension.js +19 -0
- package/src/extensions/reset-on-enter.extension.d.ts +2 -0
- package/src/extensions/reset-on-enter.extension.js +42 -0
- package/src/extensions/restricted-editing.extension.d.ts +17 -0
- package/src/extensions/restricted-editing.extension.js +384 -0
- package/src/extensions/table-resizing.extension.d.ts +6 -0
- package/src/extensions/table-resizing.extension.js +171 -0
- package/src/extensions/table-style.extension.d.ts +41 -0
- package/src/extensions/table-style.extension.js +499 -0
- package/src/extensions/text-case.extension.d.ts +13 -0
- package/src/extensions/text-case.extension.js +49 -0
- package/src/index.d.ts +7 -0
- package/src/index.js +24 -0
- package/src/models/color.model.d.ts +14 -0
- package/src/models/color.model.js +40 -0
- package/src/models/index.d.ts +1 -0
- package/src/models/index.js +18 -0
- package/src/nodes/dynamic-field.node.d.ts +49 -0
- package/src/nodes/dynamic-field.node.js +99 -0
- package/src/nodes/heading.node.d.ts +8 -0
- package/src/nodes/heading.node.js +23 -0
- package/src/nodes/index.d.ts +3 -0
- package/src/nodes/index.js +20 -0
- package/src/nodes/page-break.node.d.ts +9 -0
- package/src/nodes/page-break.node.js +50 -0
- package/src/types/index.d.ts +2 -0
- package/src/types/index.js +19 -0
- package/src/types/ordered-list.type.d.ts +1 -0
- package/src/types/ordered-list.type.js +4 -0
- package/src/types/text-case.type.d.ts +1 -0
- package/src/types/text-case.type.js +4 -0
- package/src/utils/color.util.d.ts +14 -0
- package/src/utils/color.util.js +41 -0
- package/src/utils/dom.util.d.ts +2 -0
- package/src/utils/dom.util.js +43 -0
- package/src/utils/index.d.ts +4 -0
- package/src/utils/index.js +21 -0
- package/src/utils/table.util.d.ts +36 -0
- package/src/utils/table.util.js +103 -0
- package/src/utils/text.util.d.ts +9 -0
- package/src/utils/text.util.js +59 -0
- package/src/views/block-handler.d.ts +44 -0
- package/src/views/block-handler.js +310 -0
- package/src/views/index.d.ts +1 -0
- package/src/views/index.js +18 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: all[name]
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
EditableRegion: function() {
|
|
13
|
+
return EditableRegion;
|
|
14
|
+
},
|
|
15
|
+
RestrictedEditing: function() {
|
|
16
|
+
return RestrictedEditing;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
const _core = require("@tiptap/core");
|
|
20
|
+
const _state = require("@tiptap/pm/state");
|
|
21
|
+
const EditableRegion = _core.Node.create({
|
|
22
|
+
name: 'editableRegion',
|
|
23
|
+
group: 'inline',
|
|
24
|
+
inline: true,
|
|
25
|
+
content: 'text*',
|
|
26
|
+
selectable: false,
|
|
27
|
+
parseHTML () {
|
|
28
|
+
return [
|
|
29
|
+
{
|
|
30
|
+
tag: 'span[data-editable-region]'
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
},
|
|
34
|
+
renderHTML ({ HTMLAttributes }) {
|
|
35
|
+
return [
|
|
36
|
+
'span',
|
|
37
|
+
(0, _core.mergeAttributes)(HTMLAttributes, {
|
|
38
|
+
'data-editable-region': 'true',
|
|
39
|
+
class: 'editable-region'
|
|
40
|
+
}),
|
|
41
|
+
0
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const RestrictedEditing = _core.Extension.create({
|
|
46
|
+
name: 'restrictedEditing',
|
|
47
|
+
addOptions () {
|
|
48
|
+
return {
|
|
49
|
+
editableRegionNodeName: 'editableRegion',
|
|
50
|
+
disabledNodeTypes: [
|
|
51
|
+
'listItem',
|
|
52
|
+
'blockquote'
|
|
53
|
+
],
|
|
54
|
+
initialMode: 'restricted'
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
addStorage () {
|
|
58
|
+
return {
|
|
59
|
+
mode: this.options.initialMode
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
addCommands () {
|
|
63
|
+
return {
|
|
64
|
+
wrapSelectionInEditableRegion: ()=>({ state, dispatch })=>{
|
|
65
|
+
// 0. Always return false if the editable region node is not registered
|
|
66
|
+
const type = state.schema.nodes[this.options.editableRegionNodeName];
|
|
67
|
+
if (!type) return false;
|
|
68
|
+
const { from, to, empty } = state.selection;
|
|
69
|
+
// 1. Insert an empty editable region with a zero-width space
|
|
70
|
+
if (empty) {
|
|
71
|
+
const node = type.create(null, state.schema.text('\u200b'));
|
|
72
|
+
const tr = state.tr.replaceSelectionWith(node, false);
|
|
73
|
+
tr.setMeta('restrictedEditing', {
|
|
74
|
+
allow: true
|
|
75
|
+
});
|
|
76
|
+
dispatch?.(tr.scrollIntoView());
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
// Disallow wrapping when selection contains non-text inline nodes
|
|
80
|
+
let containsNonTextInline = false;
|
|
81
|
+
state.doc.nodesBetween(from, to, (node)=>{
|
|
82
|
+
if (node.isInline && !node.isText) {
|
|
83
|
+
containsNonTextInline = true;
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
});
|
|
88
|
+
if (containsNonTextInline) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const multiLine = state.doc.textBetween(from, to, '\n', '\n').includes('\n');
|
|
92
|
+
let tr = state.tr;
|
|
93
|
+
if (multiLine) {
|
|
94
|
+
// Wrap each textblock segment independently
|
|
95
|
+
const ranges = [];
|
|
96
|
+
state.doc.nodesBetween(from, to, (node, pos)=>{
|
|
97
|
+
if (node.isTextblock) {
|
|
98
|
+
const s = Math.max(from, pos + 1);
|
|
99
|
+
const e = Math.min(to, pos + node.nodeSize - 1);
|
|
100
|
+
if (e > s) ranges.push({
|
|
101
|
+
s,
|
|
102
|
+
e
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
});
|
|
107
|
+
// Apply replacements from end to start to keep positions stable
|
|
108
|
+
for(let i = ranges.length - 1; i >= 0; i--){
|
|
109
|
+
const { s, e } = ranges[i];
|
|
110
|
+
const text = state.doc.textBetween(s, e, '\n', '\n');
|
|
111
|
+
if (text.length === 0) continue;
|
|
112
|
+
const node = type.create(null, state.schema.text(text));
|
|
113
|
+
tr = tr.replaceRangeWith(s, e, node);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
const text = state.doc.textBetween(from, to, '\n', '\n');
|
|
117
|
+
if (text.length === 0) return false;
|
|
118
|
+
const node = type.create(null, state.schema.text(text));
|
|
119
|
+
tr = tr.replaceRangeWith(from, to, node);
|
|
120
|
+
}
|
|
121
|
+
tr.setMeta('restrictedEditing', {
|
|
122
|
+
allow: true
|
|
123
|
+
});
|
|
124
|
+
dispatch?.(tr.scrollIntoView());
|
|
125
|
+
return true;
|
|
126
|
+
},
|
|
127
|
+
removeEditableRegion: ()=>({ state, dispatch })=>{
|
|
128
|
+
const type = state.schema.nodes[this.options.editableRegionNodeName];
|
|
129
|
+
if (!type) return false;
|
|
130
|
+
const $pos = state.selection.$head;
|
|
131
|
+
for(let depth = $pos.depth; depth >= 0; depth--){
|
|
132
|
+
const node = $pos.node(depth);
|
|
133
|
+
if (node.type === type) {
|
|
134
|
+
const from = $pos.before(depth);
|
|
135
|
+
const to = $pos.after(depth);
|
|
136
|
+
const tr = state.tr.replaceWith(from, to, node.content);
|
|
137
|
+
tr.setMeta('restrictedEditing', {
|
|
138
|
+
allow: true
|
|
139
|
+
});
|
|
140
|
+
dispatch?.(tr.scrollIntoView());
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
},
|
|
146
|
+
toggleEditableRegion: ()=>({ state, dispatch, commands })=>{
|
|
147
|
+
const type = state.schema.nodes[this.options.editableRegionNodeName];
|
|
148
|
+
if (!type) return false;
|
|
149
|
+
const { from, to, empty } = state.selection;
|
|
150
|
+
const findRegionAtPos = (pos)=>{
|
|
151
|
+
const $pos = state.doc.resolve(pos);
|
|
152
|
+
for(let depth = $pos.depth; depth >= 0; depth--){
|
|
153
|
+
const node = $pos.node(depth);
|
|
154
|
+
if (node.type === type) {
|
|
155
|
+
const nodeFrom = $pos.before(depth);
|
|
156
|
+
const nodeTo = $pos.after(depth);
|
|
157
|
+
return {
|
|
158
|
+
from: nodeFrom,
|
|
159
|
+
to: nodeTo,
|
|
160
|
+
node
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
};
|
|
166
|
+
// Cursor case: inside a region -> unwrap that region; else insert empty region
|
|
167
|
+
if (empty) {
|
|
168
|
+
const region = findRegionAtPos(from);
|
|
169
|
+
if (region) {
|
|
170
|
+
const tr = state.tr.replaceWith(region.from, region.to, region.node.content);
|
|
171
|
+
tr.setMeta('restrictedEditing', {
|
|
172
|
+
allow: true
|
|
173
|
+
});
|
|
174
|
+
dispatch?.(tr.scrollIntoView());
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
const node = type.create(null, state.schema.text('\u200b'));
|
|
178
|
+
const tr = state.tr.replaceSelectionWith(node, false);
|
|
179
|
+
tr.setMeta('restrictedEditing', {
|
|
180
|
+
allow: true
|
|
181
|
+
});
|
|
182
|
+
dispatch?.(tr.scrollIntoView());
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
// Range case: unwrap only if the entire selection is inside the SAME region; otherwise
|
|
186
|
+
// if selection is completely outside all regions, wrap it; else ignore
|
|
187
|
+
const regionStart = findRegionAtPos(from);
|
|
188
|
+
const regionEnd = findRegionAtPos(Math.max(from, to - 1));
|
|
189
|
+
if (regionStart && regionEnd && regionStart.from === regionEnd.from && regionStart.to === regionEnd.to) {
|
|
190
|
+
const tr = state.tr.replaceWith(regionStart.from, regionStart.to, regionStart.node.content);
|
|
191
|
+
tr.setMeta('restrictedEditing', {
|
|
192
|
+
allow: true
|
|
193
|
+
});
|
|
194
|
+
dispatch?.(tr.scrollIntoView());
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
// If neither endpoint is in a region, ensure there is no region intersecting the range
|
|
198
|
+
if (!regionStart && !regionEnd) {
|
|
199
|
+
let hasRegionInside = false;
|
|
200
|
+
state.doc.nodesBetween(from, to, (node)=>{
|
|
201
|
+
if (node.type === type) {
|
|
202
|
+
hasRegionInside = true;
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
return undefined;
|
|
206
|
+
});
|
|
207
|
+
if (!hasRegionInside) {
|
|
208
|
+
return commands.wrapSelectionInEditableRegion();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
addKeyboardShortcuts () {
|
|
216
|
+
return {
|
|
217
|
+
Enter: ()=>{
|
|
218
|
+
// Get the current selection
|
|
219
|
+
const selection = this.editor.state.selection;
|
|
220
|
+
const $from = selection.$from;
|
|
221
|
+
if (!selection.empty) return false;
|
|
222
|
+
let editableRegionInfo = null;
|
|
223
|
+
for(let d = $from.depth; d > 0; d--){
|
|
224
|
+
const node = $from.node(d);
|
|
225
|
+
if (node.type.name === 'editableRegion') {
|
|
226
|
+
editableRegionInfo = {
|
|
227
|
+
node: node,
|
|
228
|
+
depth: d,
|
|
229
|
+
// Position of the entire node (before the opening tag)
|
|
230
|
+
posBefore: $from.before(d),
|
|
231
|
+
// Position of the entire node (after the closing tag)
|
|
232
|
+
posAfter: $from.after(d),
|
|
233
|
+
// Position of the start of the content inside the node
|
|
234
|
+
contentStartPos: $from.start(d),
|
|
235
|
+
// Position of the end of the content inside the node
|
|
236
|
+
contentEndPos: $from.end(d)
|
|
237
|
+
};
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (!editableRegionInfo) return false;
|
|
242
|
+
const atStart = $from.pos === editableRegionInfo.contentStartPos;
|
|
243
|
+
const atEnd = $from.pos === editableRegionInfo.contentEndPos;
|
|
244
|
+
// If cursor is at the start of the editable region, add a new paragraph before the editable region
|
|
245
|
+
if (atStart) {
|
|
246
|
+
return this.editor.chain().insertContentAt(editableRegionInfo.posBefore, '<p></p>').focus(editableRegionInfo.posBefore - 2).run();
|
|
247
|
+
}
|
|
248
|
+
// If cursor is at the end of the editable region, add a new paragraph after the editable region
|
|
249
|
+
if (atEnd) {
|
|
250
|
+
return this.editor.chain().insertContentAt(editableRegionInfo.posAfter, '<p></p>').focus(editableRegionInfo.posAfter + 1).run();
|
|
251
|
+
}
|
|
252
|
+
// If cursor is in the middle of the editable region, return false
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
},
|
|
257
|
+
addProseMirrorPlugins () {
|
|
258
|
+
const key = new _state.PluginKey('restricted-editing');
|
|
259
|
+
const editor = this.editor;
|
|
260
|
+
const editableRegionNodeName = this.options.editableRegionNodeName;
|
|
261
|
+
const disabledNodeTypes = this.options.disabledNodeTypes ?? [];
|
|
262
|
+
const getEditableRegionBounds = (doc, schema, pos)=>{
|
|
263
|
+
const $pos = doc.resolve(pos);
|
|
264
|
+
const type = schema.nodes[editableRegionNodeName];
|
|
265
|
+
for(let depth = $pos.depth; depth >= 0; depth--){
|
|
266
|
+
if ($pos.node(depth).type === type) {
|
|
267
|
+
return {
|
|
268
|
+
depth,
|
|
269
|
+
from: $pos.start(depth),
|
|
270
|
+
to: $pos.end(depth)
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
};
|
|
276
|
+
const rangesFromTransaction = (tr)=>{
|
|
277
|
+
const ranges = [];
|
|
278
|
+
for(let i = 0; i < tr.steps.length; i++){
|
|
279
|
+
const map = tr.mapping.maps[i];
|
|
280
|
+
const forward = tr.mapping.slice(i + 1);
|
|
281
|
+
map.forEach((oldStart, oldEnd, newStart, newEnd)=>{
|
|
282
|
+
const finalNewFrom = forward.map(newStart, 1);
|
|
283
|
+
const finalNewTo = forward.map(newEnd, -1);
|
|
284
|
+
ranges.push({
|
|
285
|
+
oldFrom: oldStart,
|
|
286
|
+
oldTo: oldEnd,
|
|
287
|
+
newFrom: finalNewFrom,
|
|
288
|
+
newTo: finalNewTo
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return ranges;
|
|
293
|
+
};
|
|
294
|
+
return [
|
|
295
|
+
new _state.Plugin({
|
|
296
|
+
key,
|
|
297
|
+
filterTransaction: (tr, state)=>{
|
|
298
|
+
// In standard mode, do not restrict edits
|
|
299
|
+
if (this.storage.mode === 'standard') return true;
|
|
300
|
+
if (!tr.docChanged) return true;
|
|
301
|
+
if (tr.getMeta('restrictedEditing')?.allow) return true;
|
|
302
|
+
// Allow while the document is still the default empty state
|
|
303
|
+
if (editor.isEmpty) return true;
|
|
304
|
+
// In restricted mode: disable mark/style/attr changes from other extensions entirely
|
|
305
|
+
const disallowedStepTypes = new Set([
|
|
306
|
+
'addMark',
|
|
307
|
+
'removeMark',
|
|
308
|
+
'setNodeMarkup'
|
|
309
|
+
]);
|
|
310
|
+
for (const step of tr.steps){
|
|
311
|
+
const t = step?.toJSON?.().stepType;
|
|
312
|
+
if (t && disallowedStepTypes.has(t)) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (disabledNodeTypes.some((type)=>editor.isActive(type))) {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
const ranges = rangesFromTransaction(tr);
|
|
320
|
+
if (ranges.length === 0) return true;
|
|
321
|
+
for (const range of ranges){
|
|
322
|
+
// Pre-change validation (against state.doc)
|
|
323
|
+
const preStart = getEditableRegionBounds(state.doc, state.schema, range.oldFrom);
|
|
324
|
+
const preEnd = getEditableRegionBounds(state.doc, state.schema, range.oldTo);
|
|
325
|
+
if (!preStart || !preEnd) return false;
|
|
326
|
+
if (preStart.from !== preEnd.from || preStart.to !== preEnd.to) return false;
|
|
327
|
+
// Post-change validation (against final tr.doc)
|
|
328
|
+
const postStart = getEditableRegionBounds(tr.doc, state.schema, range.newFrom);
|
|
329
|
+
const postEnd = getEditableRegionBounds(tr.doc, state.schema, range.newTo);
|
|
330
|
+
if (!postStart || !postEnd) return false;
|
|
331
|
+
if (postStart.from !== postEnd.from || postStart.to !== postEnd.to) return false;
|
|
332
|
+
}
|
|
333
|
+
return true;
|
|
334
|
+
},
|
|
335
|
+
appendTransaction: (_transactions, _oldState, newState)=>{
|
|
336
|
+
const type = newState.schema.nodes[editableRegionNodeName];
|
|
337
|
+
if (!type) return null;
|
|
338
|
+
const isRestricted = this.storage.mode === 'restricted';
|
|
339
|
+
const edits = [];
|
|
340
|
+
newState.doc.descendants((node, pos)=>{
|
|
341
|
+
if (node.type === type) {
|
|
342
|
+
const contentText = node.textContent ?? '';
|
|
343
|
+
if (contentText.length === 0) {
|
|
344
|
+
if (isRestricted) {
|
|
345
|
+
// Ensure a single space exists inside empty region
|
|
346
|
+
const innerFrom = pos + 1;
|
|
347
|
+
const innerTo = pos + node.nodeSize - 1;
|
|
348
|
+
edits.push({
|
|
349
|
+
kind: 'fill',
|
|
350
|
+
from: innerFrom,
|
|
351
|
+
to: innerTo
|
|
352
|
+
});
|
|
353
|
+
} else {
|
|
354
|
+
// Remove empty region
|
|
355
|
+
edits.push({
|
|
356
|
+
kind: 'delete',
|
|
357
|
+
from: pos,
|
|
358
|
+
to: pos + node.nodeSize
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
if (edits.length === 0) return null;
|
|
365
|
+
// Apply in descending order of from
|
|
366
|
+
edits.sort((a, b)=>b.from - a.from);
|
|
367
|
+
const tr = newState.tr;
|
|
368
|
+
for (const e of edits){
|
|
369
|
+
if (e.kind === 'delete') {
|
|
370
|
+
tr.delete(e.from, e.to);
|
|
371
|
+
} else {
|
|
372
|
+
const textNode = newState.schema.text(' ');
|
|
373
|
+
tr.replaceWith(e.from, e.to, textNode);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
tr.setMeta('restrictedEditing', {
|
|
377
|
+
allow: true
|
|
378
|
+
});
|
|
379
|
+
return tr;
|
|
380
|
+
}
|
|
381
|
+
})
|
|
382
|
+
];
|
|
383
|
+
}
|
|
384
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "PercentageColumnResizing", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return PercentageColumnResizing;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _core = require("@tiptap/core");
|
|
12
|
+
const _state = require("@tiptap/pm/state");
|
|
13
|
+
const _tables = require("@tiptap/pm/tables");
|
|
14
|
+
const _view = require("@tiptap/pm/view");
|
|
15
|
+
const _utils = require("../utils");
|
|
16
|
+
/**
|
|
17
|
+
* Hàm xử lý khi di chuột (kéo)
|
|
18
|
+
*/ function handleMouseMoveEvent(event, dragInfo) {
|
|
19
|
+
if (!dragInfo.isDragging) return;
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
const { startX, tableWidthPx, initialLeftWidth, initialRightWidth, leftColIndex, tablePos, tableNode, view } = dragInfo;
|
|
22
|
+
// 1. Tính toán delta (pixel)
|
|
23
|
+
const deltaX = event.clientX - startX;
|
|
24
|
+
// 2. Chuyển delta pixel sang delta percentage
|
|
25
|
+
// Đây là logic quan trọng nhất
|
|
26
|
+
const deltaPercent = deltaX / tableWidthPx * 100;
|
|
27
|
+
// 3. Tính toán width mới
|
|
28
|
+
let newLeftWidth = initialLeftWidth + deltaPercent;
|
|
29
|
+
let newRightWidth = initialRightWidth - deltaPercent;
|
|
30
|
+
// 4. Áp dụng ràng buộc (ví dụ: min-width 5%)
|
|
31
|
+
const minWidth = 5; // Cấu hình min-width
|
|
32
|
+
if (newLeftWidth < minWidth) {
|
|
33
|
+
const diff = minWidth - newLeftWidth;
|
|
34
|
+
newLeftWidth = minWidth;
|
|
35
|
+
newRightWidth = newRightWidth - diff; // Cột phải bù phần bị thiếu
|
|
36
|
+
} else if (newRightWidth < minWidth) {
|
|
37
|
+
const diff = minWidth - newRightWidth;
|
|
38
|
+
newRightWidth = minWidth;
|
|
39
|
+
newLeftWidth = newLeftWidth - diff; // Cột trái bù phần bị thiếu
|
|
40
|
+
}
|
|
41
|
+
// Đảm bảo không có cột nào bị âm (nếu kéo quá nhanh)
|
|
42
|
+
if (newLeftWidth < 0) newLeftWidth = 0;
|
|
43
|
+
if (newRightWidth < 0) newRightWidth = 0;
|
|
44
|
+
// 5. Tạo mảng colwidths mới
|
|
45
|
+
const newColWidths = [
|
|
46
|
+
...tableNode?.attrs?.['colwidths']
|
|
47
|
+
];
|
|
48
|
+
newColWidths[leftColIndex] = newLeftWidth;
|
|
49
|
+
newColWidths[leftColIndex + 1] = newRightWidth;
|
|
50
|
+
// 6. Dispatch transaction để cập nhật state
|
|
51
|
+
if (tableNode && view) {
|
|
52
|
+
const tr = view.state.tr.setNodeMarkup(tablePos, null, {
|
|
53
|
+
...tableNode.attrs,
|
|
54
|
+
colwidths: newColWidths
|
|
55
|
+
});
|
|
56
|
+
// Dùng dispatchTransaction để Tiptap/ProseMirror tự cập nhật UI
|
|
57
|
+
view.dispatch(tr);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Hàm xử lý khi nhả chuột (kết thúc kéo)
|
|
62
|
+
*/ function handleMouseUpEvent(event, dragInfo) {
|
|
63
|
+
if (!dragInfo.isDragging) return;
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
// Reset trạng thái
|
|
66
|
+
dragInfo = {
|
|
67
|
+
isDragging: false,
|
|
68
|
+
startX: 0,
|
|
69
|
+
tablePos: -1,
|
|
70
|
+
tableNode: null,
|
|
71
|
+
tableWidthPx: 0,
|
|
72
|
+
leftColIndex: -1,
|
|
73
|
+
initialLeftWidth: 0,
|
|
74
|
+
initialRightWidth: 0,
|
|
75
|
+
view: null
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const PercentageColumnResizing = _core.Extension.create({
|
|
79
|
+
name: 'percentageColumnResizing',
|
|
80
|
+
addProseMirrorPlugins () {
|
|
81
|
+
// Một đối tượng tạm thời để lưu trữ trạng thái kéo-thả
|
|
82
|
+
// Chúng ta không dùng Plugin State vì nó không cần thiết phải "undo"
|
|
83
|
+
let dragInfo = {
|
|
84
|
+
isDragging: false,
|
|
85
|
+
startX: 0,
|
|
86
|
+
tablePos: -1,
|
|
87
|
+
tableNode: null,
|
|
88
|
+
tableWidthPx: 0,
|
|
89
|
+
leftColIndex: -1,
|
|
90
|
+
initialLeftWidth: 0,
|
|
91
|
+
initialRightWidth: 0,
|
|
92
|
+
view: null
|
|
93
|
+
};
|
|
94
|
+
const editorInstance = this.editor;
|
|
95
|
+
return [
|
|
96
|
+
new _state.Plugin({
|
|
97
|
+
key: new _state.PluginKey('percentageColumnResizing'),
|
|
98
|
+
props: {
|
|
99
|
+
/**
|
|
100
|
+
* Vẽ các resize handles vào DOM
|
|
101
|
+
*/ decorations (state) {
|
|
102
|
+
if (!editorInstance?.isEditable) return;
|
|
103
|
+
const decorations = [];
|
|
104
|
+
const { doc } = state;
|
|
105
|
+
const tableTypes = (0, _tables.tableNodeTypes)(state.schema);
|
|
106
|
+
// Duyệt qua tất cả node trong document
|
|
107
|
+
doc.descendants((node, pos)=>{
|
|
108
|
+
if (node.type.name !== tableTypes.table.name) return;
|
|
109
|
+
const colwidths = node.attrs['colwidths'];
|
|
110
|
+
if (!colwidths || colwidths.length === 0) return;
|
|
111
|
+
let accumulatedWidth = 0;
|
|
112
|
+
// Tạo handle cho N-1 cột (không cần cho cột cuối)
|
|
113
|
+
for(let i = 0; i < colwidths.length - 1; i++){
|
|
114
|
+
accumulatedWidth += colwidths[i] ?? 0;
|
|
115
|
+
// Tạo một widget 'div' để làm handle
|
|
116
|
+
const handle = document.createElement('div');
|
|
117
|
+
handle.className = 'pm-col-resizer';
|
|
118
|
+
handle.style.left = `calc(${accumulatedWidth}% - 2px)`;
|
|
119
|
+
// Lưu vị trí cột để biết đang resize cột nào
|
|
120
|
+
handle.setAttribute('data-col-index', i.toString());
|
|
121
|
+
handle.setAttribute('data-table-pos', pos.toString());
|
|
122
|
+
// Đặt widget ngay sau thẻ <table> (pos + 1)
|
|
123
|
+
decorations.push(_view.Decoration.widget(pos + 1, handle));
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
return _view.DecorationSet.create(doc, decorations);
|
|
127
|
+
},
|
|
128
|
+
/**
|
|
129
|
+
* Xử lý sự kiện mousedown trên handle
|
|
130
|
+
*/ handleDOMEvents: {
|
|
131
|
+
mousedown (view, event) {
|
|
132
|
+
const target = event.target;
|
|
133
|
+
// Chỉ kích hoạt khi click vào handle
|
|
134
|
+
if (!target.classList.contains('pm-col-resizer')) return false;
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
const colIndex = parseInt(target.getAttribute('data-col-index') ?? '0', 10);
|
|
137
|
+
const tablePos = parseInt(target.getAttribute('data-table-pos') ?? '0', 10);
|
|
138
|
+
const tableNode = view.state.doc.nodeAt(tablePos);
|
|
139
|
+
const colwidths = tableNode?.attrs['colwidths'];
|
|
140
|
+
if (!tableNode || !colwidths) return false;
|
|
141
|
+
// Get table DOM element (handles NodeView wrappers and nested tables)
|
|
142
|
+
const tableDOM = (0, _utils.getTableDOMFromView)(view, tablePos);
|
|
143
|
+
if (!tableDOM) return false;
|
|
144
|
+
// Lưu thông tin cần thiết cho việc kéo
|
|
145
|
+
dragInfo = {
|
|
146
|
+
isDragging: true,
|
|
147
|
+
startX: event.clientX,
|
|
148
|
+
tablePos: tablePos,
|
|
149
|
+
tableNode: tableNode,
|
|
150
|
+
tableWidthPx: tableDOM.offsetWidth,
|
|
151
|
+
leftColIndex: colIndex,
|
|
152
|
+
initialLeftWidth: colwidths[colIndex],
|
|
153
|
+
initialRightWidth: colwidths[colIndex + 1],
|
|
154
|
+
view: view
|
|
155
|
+
};
|
|
156
|
+
const handleMouseMove = (event)=>handleMouseMoveEvent(event, dragInfo);
|
|
157
|
+
const handleMouseUp = (event)=>{
|
|
158
|
+
handleMouseUpEvent(event, dragInfo);
|
|
159
|
+
window.removeEventListener('mousemove', handleMouseMove, true);
|
|
160
|
+
window.removeEventListener('mouseup', handleMouseUp, true);
|
|
161
|
+
};
|
|
162
|
+
window.addEventListener('mousemove', handleMouseMove, true);
|
|
163
|
+
window.addEventListener('mouseup', handleMouseUp, true);
|
|
164
|
+
return true; // Đã xử lý event
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Extension, Node } from '@tiptap/core';
|
|
2
|
+
import { TableKitOptions, TableOptions } from '@tiptap/extension-table';
|
|
3
|
+
export declare const MIN_NEW_COL_WIDTH = 5;
|
|
4
|
+
export declare const TableDefaultAttributes: {
|
|
5
|
+
border: {
|
|
6
|
+
style: string;
|
|
7
|
+
color: string;
|
|
8
|
+
width: string;
|
|
9
|
+
};
|
|
10
|
+
backgroundColor: null;
|
|
11
|
+
colwidths: null;
|
|
12
|
+
cellBorder: {
|
|
13
|
+
style: string;
|
|
14
|
+
color: string;
|
|
15
|
+
width: string;
|
|
16
|
+
};
|
|
17
|
+
cellBackgroundColor: null;
|
|
18
|
+
cellVerticalAlign: string;
|
|
19
|
+
cellTextAlign: null;
|
|
20
|
+
};
|
|
21
|
+
export type TableBorder = {
|
|
22
|
+
style?: string | null;
|
|
23
|
+
color?: string | null;
|
|
24
|
+
width?: string | null;
|
|
25
|
+
} | null | undefined;
|
|
26
|
+
export declare const StyledTableCell: Node<import("@tiptap/extension-table").TableCellOptions, any>;
|
|
27
|
+
export declare const StyledTableHeader: Node<import("@tiptap/extension-table").TableHeaderOptions, any>;
|
|
28
|
+
declare module '@tiptap/core' {
|
|
29
|
+
interface Commands<ReturnType> {
|
|
30
|
+
tableStyles: {
|
|
31
|
+
setCellBorder: (border: TableBorder) => ReturnType;
|
|
32
|
+
setCellBackgroundColor: (color: string | null) => ReturnType;
|
|
33
|
+
setCellVerticalAlign: (align: 'top' | 'middle' | 'bottom' | null) => ReturnType;
|
|
34
|
+
setCellTextAlign: (align: 'left' | 'center' | 'right' | 'justify' | null) => ReturnType;
|
|
35
|
+
setTableBorder: (border: TableBorder) => ReturnType;
|
|
36
|
+
setTableBackgroundColor: (color: string | null) => ReturnType;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export declare const StyledTable: Node<TableOptions, any>;
|
|
41
|
+
export declare const StyledTableKit: Extension<TableKitOptions, any>;
|