quill-table-up 2.3.1 → 2.4.0
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 +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +35 -12
- package/dist/index.js +26 -25
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +35 -31
- package/dist/index.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/e2e/table-keyboard-handler.test.ts +218 -0
- package/src/__tests__/e2e/table-selection.test.ts +137 -131
- package/src/__tests__/unit/table-cell-merge.test.ts +261 -1
- package/src/__tests__/unit/table-clipboard.test.ts +62 -44
- package/src/__tests__/unit/table-insert.test.ts +39 -2
- package/src/__tests__/unit/table-redo-undo.test.ts +69 -0
- package/src/__tests__/unit/table-remove.test.ts +4 -2
- package/src/__tests__/unit/utils.ts +22 -4
- package/src/__tests__/unit/vitest.d.ts +4 -1
- package/src/formats/index.ts +6 -0
- package/src/formats/overrides/block-embed.ts +54 -0
- package/src/formats/overrides/block.ts +10 -4
- package/src/formats/overrides/index.ts +1 -0
- package/src/formats/table-cell-format.ts +39 -6
- package/src/formats/table-cell-inner-format.ts +70 -21
- package/src/formats/table-main-format.ts +92 -1
- package/src/formats/table-row-format.ts +13 -2
- package/src/modules/table-clipboard.ts +30 -35
- package/src/modules/table-resize/table-resize-box.ts +2 -2
- package/src/modules/table-resize/table-resize-common.ts +1 -1
- package/src/modules/table-resize/table-resize-line.ts +1 -1
- package/src/modules/table-resize/table-resize-scale.ts +1 -1
- package/src/modules/table-scrollbar.ts +5 -5
- package/src/modules/table-selection.ts +52 -31
- package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/src/style/table-selection.less +1 -0
- package/src/table-up.ts +57 -23
- package/src/utils/blot-helper.ts +7 -4
- package/src/utils/index.ts +1 -1
- package/src/utils/{scroll-event-handle.ts → scroll-event-helper.ts} +7 -0
- package/src/utils/types.ts +2 -0
|
@@ -217,14 +217,13 @@ describe('insert block embed blot', () => {
|
|
|
217
217
|
<div>
|
|
218
218
|
<table cellpadding="0" cellspacing="0" data-full="true">
|
|
219
219
|
<colgroup data-full="true">
|
|
220
|
-
|
|
220
|
+
<col width="100%" data-full="true" />
|
|
221
221
|
</colgroup>
|
|
222
222
|
<tbody>
|
|
223
223
|
<tr>
|
|
224
224
|
<td rowspan="1" colspan="1">
|
|
225
225
|
<div>
|
|
226
226
|
<iframe src="https://quilljs.com/" frameborder="0" allowfullscreen="true"></iframe>
|
|
227
|
-
<p><br></p>
|
|
228
227
|
</div>
|
|
229
228
|
</td>
|
|
230
229
|
</tr>
|
|
@@ -236,6 +235,44 @@ describe('insert block embed blot', () => {
|
|
|
236
235
|
{ ignoreAttrs: ['class', 'style', 'data-table-id', 'data-row-id', 'data-col-id', 'data-rowspan', 'data-colspan', 'contenteditable'] },
|
|
237
236
|
);
|
|
238
237
|
});
|
|
238
|
+
|
|
239
|
+
it('BlockEmbed should at correct position in cell', async () => {
|
|
240
|
+
const quill = createQuillWithTableModule(`<p><br></p>`);
|
|
241
|
+
quill.setContents([
|
|
242
|
+
{ insert: '\n' },
|
|
243
|
+
{ insert: { 'table-up-col': { tableId: '2l7117zsa6r', colId: 'd962746f4w8', full: false, width: 100 } } },
|
|
244
|
+
{ insert: '1' },
|
|
245
|
+
{ attributes: { 'table-up-cell-inner': { tableId: '2l7117zsa6r', rowId: 'a9r52q9z4l', colId: 'd962746f4w8', rowspan: 1, colspan: 1 } }, insert: '\n' },
|
|
246
|
+
{ insert: { video: 'http://localhost:5500/docs/index.html' } },
|
|
247
|
+
{ insert: 'bbb' },
|
|
248
|
+
{ attributes: { 'table-up-cell-inner': { tableId: '2l7117zsa6r', rowId: 'a9r52q9z4l', colId: 'd962746f4w8', rowspan: 1, colspan: 1 } }, insert: '\n' },
|
|
249
|
+
{ insert: '\n' },
|
|
250
|
+
]);
|
|
251
|
+
await vi.runAllTimersAsync();
|
|
252
|
+
expect(quill.root).toEqualHTML(
|
|
253
|
+
`
|
|
254
|
+
<p><br></p>
|
|
255
|
+
<div>
|
|
256
|
+
<table cellpadding="0" cellspacing="0">
|
|
257
|
+
${createTaleColHTML(1, { full: false, width: 100 })}
|
|
258
|
+
<tbody>
|
|
259
|
+
<tr>
|
|
260
|
+
<td rowspan="1" colspan="1">
|
|
261
|
+
<div>
|
|
262
|
+
<p>1</p>
|
|
263
|
+
<iframe frameborder="0" allowfullscreen="true" src="http://localhost:5500/docs/index.html"></iframe>
|
|
264
|
+
<p>bbb</p>
|
|
265
|
+
</div>
|
|
266
|
+
</td
|
|
267
|
+
</tr>
|
|
268
|
+
</tbody>
|
|
269
|
+
</table>
|
|
270
|
+
</div>
|
|
271
|
+
<p><br></p>
|
|
272
|
+
`,
|
|
273
|
+
{ ignoreAttrs: ['class', 'style', 'data-table-id', 'data-row-id', 'data-col-id', 'data-rowspan', 'data-colspan', 'contenteditable'] },
|
|
274
|
+
);
|
|
275
|
+
});
|
|
239
276
|
});
|
|
240
277
|
|
|
241
278
|
describe('set contents', () => {
|
|
@@ -1188,6 +1188,75 @@ describe('undo cell attribute', () => {
|
|
|
1188
1188
|
{ ignoreAttrs: ['class', 'style', 'data-table-id', 'data-row-id', 'data-col-id', 'data-rowspan', 'data-colspan', 'contenteditable'] },
|
|
1189
1189
|
);
|
|
1190
1190
|
});
|
|
1191
|
+
|
|
1192
|
+
it('undo table style with Container in cell', async () => {
|
|
1193
|
+
const quill = await createTable(3, 3);
|
|
1194
|
+
quill.setContents([
|
|
1195
|
+
{ insert: '\n' },
|
|
1196
|
+
{ insert: { 'table-up-col': { tableId: 'wm7stgtxmn', colId: 'vm3doxdsmq', full: false, width: 100 } } },
|
|
1197
|
+
{ insert: { 'table-up-col': { tableId: 'wm7stgtxmn', colId: 'k4ele1u8u4n', full: false, width: 100 } } },
|
|
1198
|
+
{ insert: '1' },
|
|
1199
|
+
{ attributes: { 'list': 'unchecked', 'table-up-cell-inner': { tableId: 'wm7stgtxmn', rowId: 'xn7r03opjc', colId: 'vm3doxdsmq', rowspan: 1, colspan: 1 } }, insert: '\n\n' },
|
|
1200
|
+
{ attributes: { 'table-up-cell-inner': { tableId: 'wm7stgtxmn', rowId: 'xn7r03opjc', colId: 'k4ele1u8u4n', rowspan: 1, colspan: 1 } }, insert: '\n' },
|
|
1201
|
+
{ attributes: { 'table-up-cell-inner': { tableId: 'wm7stgtxmn', rowId: 'nqpe206omjn', colId: 'vm3doxdsmq', rowspan: 1, colspan: 1 } }, insert: '\n' },
|
|
1202
|
+
{ attributes: { 'table-up-cell-inner': { tableId: 'wm7stgtxmn', rowId: 'nqpe206omjn', colId: 'k4ele1u8u4n', rowspan: 1, colspan: 1 } }, insert: '\n' },
|
|
1203
|
+
{ insert: '\n' },
|
|
1204
|
+
]);
|
|
1205
|
+
await vi.runAllTimersAsync();
|
|
1206
|
+
|
|
1207
|
+
const tableModule = quill.getModule(TableUp.moduleName) as TableUp;
|
|
1208
|
+
const tds = quill.scroll.descendants(TableCellInnerFormat, 0);
|
|
1209
|
+
tableModule.setCellAttrs([tds[0]], 'background-color', 'rgb(0, 163, 245)', true);
|
|
1210
|
+
await vi.runAllTimersAsync();
|
|
1211
|
+
expectDelta(
|
|
1212
|
+
new Delta([
|
|
1213
|
+
{ insert: '\n' },
|
|
1214
|
+
{ insert: { 'table-up-col': { tableId: 'wm7stgtxmn', colId: 'vm3doxdsmq', full: false, width: 100 } } },
|
|
1215
|
+
{ insert: { 'table-up-col': { tableId: 'wm7stgtxmn', colId: 'k4ele1u8u4n', full: false, width: 100 } } },
|
|
1216
|
+
{ insert: '1' },
|
|
1217
|
+
{ attributes: { 'list': 'unchecked', 'table-up-cell-inner': { tableId: 'wm7stgtxmn', rowId: 'xn7r03opjc', colId: 'vm3doxdsmq', rowspan: 1, colspan: 1, style: 'background-color: rgb(0, 163, 245);' } }, insert: '\n\n' },
|
|
1218
|
+
{ attributes: { 'table-up-cell-inner': { tableId: 'wm7stgtxmn', rowId: 'xn7r03opjc', colId: 'k4ele1u8u4n', rowspan: 1, colspan: 1 } }, insert: '\n' },
|
|
1219
|
+
{ attributes: { 'table-up-cell-inner': { tableId: 'wm7stgtxmn', rowId: 'nqpe206omjn', colId: 'vm3doxdsmq', rowspan: 1, colspan: 1 } }, insert: '\n' },
|
|
1220
|
+
{ attributes: { 'table-up-cell-inner': { tableId: 'wm7stgtxmn', rowId: 'nqpe206omjn', colId: 'k4ele1u8u4n', rowspan: 1, colspan: 1 } }, insert: '\n' },
|
|
1221
|
+
{ insert: '\n' },
|
|
1222
|
+
]),
|
|
1223
|
+
quill.getContents(),
|
|
1224
|
+
);
|
|
1225
|
+
|
|
1226
|
+
quill.history.undo();
|
|
1227
|
+
await vi.runAllTimersAsync();
|
|
1228
|
+
expectDelta(
|
|
1229
|
+
new Delta([
|
|
1230
|
+
{ insert: '\n' },
|
|
1231
|
+
{ insert: { 'table-up-col': { tableId: 'wm7stgtxmn', colId: 'vm3doxdsmq', full: false, width: 100 } } },
|
|
1232
|
+
{ insert: { 'table-up-col': { tableId: 'wm7stgtxmn', colId: 'k4ele1u8u4n', full: false, width: 100 } } },
|
|
1233
|
+
{ insert: '1' },
|
|
1234
|
+
{ attributes: { 'list': 'unchecked', 'table-up-cell-inner': { tableId: 'wm7stgtxmn', rowId: 'xn7r03opjc', colId: 'vm3doxdsmq', rowspan: 1, colspan: 1 } }, insert: '\n\n' },
|
|
1235
|
+
{ attributes: { 'table-up-cell-inner': { tableId: 'wm7stgtxmn', rowId: 'xn7r03opjc', colId: 'k4ele1u8u4n', rowspan: 1, colspan: 1 } }, insert: '\n' },
|
|
1236
|
+
{ attributes: { 'table-up-cell-inner': { tableId: 'wm7stgtxmn', rowId: 'nqpe206omjn', colId: 'vm3doxdsmq', rowspan: 1, colspan: 1 } }, insert: '\n' },
|
|
1237
|
+
{ attributes: { 'table-up-cell-inner': { tableId: 'wm7stgtxmn', rowId: 'nqpe206omjn', colId: 'k4ele1u8u4n', rowspan: 1, colspan: 1 } }, insert: '\n' },
|
|
1238
|
+
{ insert: '\n' },
|
|
1239
|
+
]),
|
|
1240
|
+
quill.getContents(),
|
|
1241
|
+
);
|
|
1242
|
+
|
|
1243
|
+
quill.history.redo();
|
|
1244
|
+
await vi.runAllTimersAsync();
|
|
1245
|
+
expectDelta(
|
|
1246
|
+
new Delta([
|
|
1247
|
+
{ insert: '\n' },
|
|
1248
|
+
{ insert: { 'table-up-col': { tableId: 'wm7stgtxmn', colId: 'vm3doxdsmq', full: false, width: 100 } } },
|
|
1249
|
+
{ insert: { 'table-up-col': { tableId: 'wm7stgtxmn', colId: 'k4ele1u8u4n', full: false, width: 100 } } },
|
|
1250
|
+
{ insert: '1' },
|
|
1251
|
+
{ attributes: { 'list': 'unchecked', 'table-up-cell-inner': { tableId: 'wm7stgtxmn', rowId: 'xn7r03opjc', colId: 'vm3doxdsmq', rowspan: 1, colspan: 1, style: 'background-color: rgb(0, 163, 245);' } }, insert: '\n\n' },
|
|
1252
|
+
{ attributes: { 'table-up-cell-inner': { tableId: 'wm7stgtxmn', rowId: 'xn7r03opjc', colId: 'k4ele1u8u4n', rowspan: 1, colspan: 1 } }, insert: '\n' },
|
|
1253
|
+
{ attributes: { 'table-up-cell-inner': { tableId: 'wm7stgtxmn', rowId: 'nqpe206omjn', colId: 'vm3doxdsmq', rowspan: 1, colspan: 1 } }, insert: '\n' },
|
|
1254
|
+
{ attributes: { 'table-up-cell-inner': { tableId: 'wm7stgtxmn', rowId: 'nqpe206omjn', colId: 'k4ele1u8u4n', rowspan: 1, colspan: 1 } }, insert: '\n' },
|
|
1255
|
+
{ insert: '\n' },
|
|
1256
|
+
]),
|
|
1257
|
+
quill.getContents(),
|
|
1258
|
+
);
|
|
1259
|
+
});
|
|
1191
1260
|
});
|
|
1192
1261
|
|
|
1193
1262
|
describe('table caption', () => {
|
|
@@ -241,11 +241,13 @@ describe('unusual delete', () => {
|
|
|
241
241
|
<div>
|
|
242
242
|
<table cellpadding="0" cellspacing="0" data-full="true">
|
|
243
243
|
<colgroup data-full="true">
|
|
244
|
-
|
|
244
|
+
<col width="60%" data-full="true" />
|
|
245
|
+
<col width="20%" data-full="true" />
|
|
246
|
+
<col width="20%" data-full="true" />
|
|
245
247
|
</colgroup>
|
|
246
248
|
<tbody>
|
|
247
249
|
<tr>
|
|
248
|
-
|
|
250
|
+
<td rowspan="3" colspan="1">
|
|
249
251
|
<div>
|
|
250
252
|
<p><br></p>
|
|
251
253
|
<p><br></p>
|
|
@@ -53,16 +53,34 @@ export function createQuillWithTableModule(html: string, tableOptions: Partial<T
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
expect.extend({
|
|
56
|
-
toEqualHTML(
|
|
57
|
-
|
|
56
|
+
toEqualHTML(
|
|
57
|
+
received,
|
|
58
|
+
expected,
|
|
59
|
+
{
|
|
60
|
+
ignoreAttrs = [],
|
|
61
|
+
replaceAttrs = {},
|
|
62
|
+
}: {
|
|
63
|
+
ignoreAttrs?: string[];
|
|
64
|
+
replaceAttrs?: Record<string, (attrValue: string) => string>;
|
|
65
|
+
} = {},
|
|
66
|
+
) {
|
|
58
67
|
const receivedDOM = document.createElement('div');
|
|
59
68
|
const expectedDOM = document.createElement('div');
|
|
60
69
|
receivedDOM.innerHTML = normalizeHTML(
|
|
61
70
|
typeof received === 'string' ? received : received.innerHTML,
|
|
62
71
|
);
|
|
63
72
|
expectedDOM.innerHTML = normalizeHTML(expected);
|
|
64
|
-
const doms = [receivedDOM, expectedDOM];
|
|
65
73
|
|
|
74
|
+
for (const [attr, handler] of Object.entries(replaceAttrs)) {
|
|
75
|
+
for (const node of Array.from(receivedDOM.querySelectorAll(`[${attr}]`))) {
|
|
76
|
+
const attrValue = node.getAttribute(attr);
|
|
77
|
+
if (attrValue) {
|
|
78
|
+
node.setAttribute(attr, handler(attrValue));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const doms = [receivedDOM, expectedDOM];
|
|
66
84
|
for (const dom of doms) {
|
|
67
85
|
for (const node of Array.from(dom.querySelectorAll('.ql-ui'))) {
|
|
68
86
|
node.remove();
|
|
@@ -86,7 +104,7 @@ expect.extend({
|
|
|
86
104
|
`HTMLs don't match.\n${this.utils.diff(
|
|
87
105
|
this.utils.stringify(receivedDOM),
|
|
88
106
|
this.utils.stringify(expectedDOM),
|
|
89
|
-
)}`,
|
|
107
|
+
)}\n`,
|
|
90
108
|
};
|
|
91
109
|
},
|
|
92
110
|
});
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
import type { Assertion, AsymmetricMatchersContaining } from 'vitest';
|
|
3
3
|
|
|
4
4
|
interface CustomMatchers<R = unknown> {
|
|
5
|
-
toEqualHTML: (html: string, options?: {
|
|
5
|
+
toEqualHTML: (html: string, options?: {
|
|
6
|
+
ignoreAttrs?: string[];
|
|
7
|
+
replaceAttrs?: Record<string, (attrValue: string) => string>;
|
|
8
|
+
}) => R;
|
|
6
9
|
}
|
|
7
10
|
|
|
8
11
|
declare module 'vitest' {
|
package/src/formats/index.ts
CHANGED
|
@@ -16,6 +16,12 @@ export * from './table-wrapper-format';
|
|
|
16
16
|
|
|
17
17
|
export function getTableMainRect(tableMainBlot: TableMainFormat) {
|
|
18
18
|
const [tableBodyBlot] = findChildBlot(tableMainBlot, TableBodyFormat);
|
|
19
|
+
if (!tableBodyBlot) {
|
|
20
|
+
return {
|
|
21
|
+
body: null,
|
|
22
|
+
rect: null,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
19
25
|
return {
|
|
20
26
|
body: tableBodyBlot,
|
|
21
27
|
rect: tableBodyBlot.domNode.getBoundingClientRect(),
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Parchment as TypeParchment } from 'quill';
|
|
2
|
+
import type { BlockEmbed as TypeBlockEmbed } from 'quill/blots/block';
|
|
3
|
+
import Quill from 'quill';
|
|
4
|
+
import { blotName } from '../../utils';
|
|
5
|
+
|
|
6
|
+
const BlockEmbed = Quill.import('blots/block/embed') as typeof TypeBlockEmbed;
|
|
7
|
+
|
|
8
|
+
export class BlockEmbedOverride extends BlockEmbed {
|
|
9
|
+
delta() {
|
|
10
|
+
// if BlockEmbed is the last line of the tableCellInner. need to add value in delta
|
|
11
|
+
const delta = super.delta();
|
|
12
|
+
const formats = bubbleFormats(this);
|
|
13
|
+
if (formats[blotName.tableCellInner]) {
|
|
14
|
+
delta.insert('\n', { [blotName.tableCellInner]: formats[blotName.tableCellInner] });
|
|
15
|
+
}
|
|
16
|
+
return delta;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
length() {
|
|
20
|
+
// because BlockEmbed is the last line of the tableCellInner. need add value in delta, also need add 1 to length
|
|
21
|
+
const formats = bubbleFormats(this);
|
|
22
|
+
if (formats[blotName.tableCellInner] && (!this.next || this.next.length() <= 1)) {
|
|
23
|
+
return super.length() + 1;
|
|
24
|
+
}
|
|
25
|
+
return super.length();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// copy from `quill/blots/block`
|
|
30
|
+
function bubbleFormats(
|
|
31
|
+
blot: TypeParchment.Blot | null,
|
|
32
|
+
formats: Record<string, unknown> = {},
|
|
33
|
+
filter = true,
|
|
34
|
+
): Record<string, unknown> {
|
|
35
|
+
if (blot == null) return formats;
|
|
36
|
+
if ('formats' in blot && typeof blot.formats === 'function') {
|
|
37
|
+
formats = {
|
|
38
|
+
...formats,
|
|
39
|
+
...blot.formats(),
|
|
40
|
+
};
|
|
41
|
+
if (filter) {
|
|
42
|
+
// exclude syntax highlighting from deltas and getFormat()
|
|
43
|
+
delete formats['code-token'];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (
|
|
47
|
+
blot.parent == null
|
|
48
|
+
|| blot.parent.statics.blotName === 'scroll'
|
|
49
|
+
|| blot.parent.statics.scope !== blot.statics.scope
|
|
50
|
+
) {
|
|
51
|
+
return formats;
|
|
52
|
+
}
|
|
53
|
+
return bubbleFormats(blot.parent, formats, filter);
|
|
54
|
+
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { Parchment as TypeParchment } from 'quill';
|
|
2
2
|
import type TypeBlock from 'quill/blots/block';
|
|
3
|
+
import type TypeContainer from 'quill/blots/container';
|
|
3
4
|
import Quill from 'quill';
|
|
4
|
-
import { blotName, findParentBlot } from '../../utils';
|
|
5
|
+
import { blotName, findParentBlot, isString } from '../../utils';
|
|
5
6
|
|
|
6
7
|
const Parchment = Quill.import('parchment');
|
|
7
8
|
const Block = Quill.import('blots/block') as typeof TypeBlock;
|
|
9
|
+
const Container = Quill.import('blots/container') as typeof TypeContainer;
|
|
8
10
|
|
|
9
11
|
export class BlockOverride extends Block {
|
|
10
12
|
replaceWith(name: string | TypeParchment.Blot, value?: any): TypeParchment.Blot {
|
|
11
|
-
const replacement =
|
|
13
|
+
const replacement = isString(name) ? this.scroll.create(name, value) : name;
|
|
12
14
|
if (replacement instanceof Parchment.ParentBlot) {
|
|
13
15
|
// replace block to TableCellInner length is 0 when setContents
|
|
14
16
|
// that will set text direct in TableCellInner but not in block
|
|
@@ -45,7 +47,11 @@ export class BlockOverride extends Block {
|
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
else {
|
|
48
|
-
|
|
50
|
+
// `list` will wrap Container. tableCellInner should be insert outside it
|
|
51
|
+
if (selfParent instanceof Container) {
|
|
52
|
+
selfParent.parent.insertBefore(replacement, selfParent);
|
|
53
|
+
}
|
|
54
|
+
else if (selfParent != null) {
|
|
49
55
|
selfParent.insertBefore(replacement, this.next);
|
|
50
56
|
}
|
|
51
57
|
replacement.appendChild(this);
|
|
@@ -57,7 +63,7 @@ export class BlockOverride extends Block {
|
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
if (this.parent != null) {
|
|
60
|
-
this.parent.insertBefore(replacement, this.next
|
|
66
|
+
this.parent.insertBefore(replacement, this.next);
|
|
61
67
|
this.remove();
|
|
62
68
|
}
|
|
63
69
|
this.attributes.copy(replacement as TypeParchment.BlockBlot);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TableCellValue } from '../utils';
|
|
2
|
-
import { blotName, findParentBlot } from '../utils';
|
|
2
|
+
import { blotName, findParentBlot, toCamelCase } from '../utils';
|
|
3
3
|
import { ContainerFormat } from './container-format';
|
|
4
4
|
import { TableCellInnerFormat } from './table-cell-inner-format';
|
|
5
5
|
import { TableRowFormat } from './table-row-format';
|
|
@@ -9,14 +9,16 @@ export class TableCellFormat extends ContainerFormat {
|
|
|
9
9
|
static blotName = blotName.tableCell;
|
|
10
10
|
static tagName = 'td';
|
|
11
11
|
static className = 'ql-table-cell';
|
|
12
|
-
static allowDataAttrs = new Set(['table-id', 'row-id', 'col-id']);
|
|
12
|
+
static allowDataAttrs = new Set(['table-id', 'row-id', 'col-id', 'empty-row']);
|
|
13
13
|
static allowAttrs = new Set(['rowspan', 'colspan']);
|
|
14
14
|
|
|
15
15
|
// keep `isAllowStyle` and `allowStyle` same with TableCellInnerFormat
|
|
16
16
|
static allowStyle = new Set(['background-color', 'border', 'height']);
|
|
17
17
|
static isAllowStyle(str: string): boolean {
|
|
18
|
+
const cssAttrName = toCamelCase(str);
|
|
18
19
|
for (const style of this.allowStyle) {
|
|
19
|
-
|
|
20
|
+
// cause `cssTextToObject` will transform css string to camel case style name
|
|
21
|
+
if (cssAttrName.startsWith(toCamelCase(style))) {
|
|
20
22
|
return true;
|
|
21
23
|
}
|
|
22
24
|
}
|
|
@@ -31,6 +33,7 @@ export class TableCellFormat extends ContainerFormat {
|
|
|
31
33
|
rowspan,
|
|
32
34
|
colspan,
|
|
33
35
|
style,
|
|
36
|
+
emptyRow,
|
|
34
37
|
} = value;
|
|
35
38
|
const node = super.create() as HTMLElement;
|
|
36
39
|
node.dataset.tableId = tableId;
|
|
@@ -39,11 +42,15 @@ export class TableCellFormat extends ContainerFormat {
|
|
|
39
42
|
node.setAttribute('rowspan', String(getValidCellspan(rowspan)));
|
|
40
43
|
node.setAttribute('colspan', String(getValidCellspan(colspan)));
|
|
41
44
|
style && (node.style.cssText = style);
|
|
45
|
+
try {
|
|
46
|
+
emptyRow && (node.dataset.emptyRow = JSON.stringify(emptyRow));
|
|
47
|
+
}
|
|
48
|
+
catch {}
|
|
42
49
|
return node;
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
static formats(domNode: HTMLElement) {
|
|
46
|
-
const { tableId, rowId, colId } = domNode.dataset;
|
|
53
|
+
const { tableId, rowId, colId, emptyRow } = domNode.dataset;
|
|
47
54
|
const rowspan = Number(domNode.getAttribute('rowspan'));
|
|
48
55
|
const colspan = Number(domNode.getAttribute('colspan'));
|
|
49
56
|
const value: Record<string, any> = {
|
|
@@ -67,6 +74,11 @@ export class TableCellFormat extends ContainerFormat {
|
|
|
67
74
|
value.style = entries.map(([key, value]) => `${key}: ${value}`).join(';');
|
|
68
75
|
}
|
|
69
76
|
|
|
77
|
+
try {
|
|
78
|
+
emptyRow && (value.emptyRow = JSON.parse(emptyRow));
|
|
79
|
+
}
|
|
80
|
+
catch {}
|
|
81
|
+
|
|
70
82
|
return value;
|
|
71
83
|
}
|
|
72
84
|
|
|
@@ -92,8 +104,15 @@ export class TableCellFormat extends ContainerFormat {
|
|
|
92
104
|
}
|
|
93
105
|
}
|
|
94
106
|
|
|
95
|
-
|
|
96
|
-
|
|
107
|
+
const headChild = this.children.head;
|
|
108
|
+
if (
|
|
109
|
+
this.domNode.style.cssText
|
|
110
|
+
&& headChild
|
|
111
|
+
&& headChild.statics.blotName === blotName.tableCellInner
|
|
112
|
+
// only update if data not match. avoid optimize circular updates
|
|
113
|
+
&& this.domNode.style.cssText !== (headChild.domNode as HTMLElement).dataset.style
|
|
114
|
+
) {
|
|
115
|
+
(headChild!.domNode as HTMLElement).dataset.style = this.domNode.style.cssText;
|
|
97
116
|
}
|
|
98
117
|
}
|
|
99
118
|
|
|
@@ -193,6 +212,15 @@ export class TableCellFormat extends ContainerFormat {
|
|
|
193
212
|
return Number(this.domNode.getAttribute('colspan'));
|
|
194
213
|
}
|
|
195
214
|
|
|
215
|
+
get emptyRow(): string[] {
|
|
216
|
+
try {
|
|
217
|
+
return JSON.parse(this.domNode.dataset.emptyRow!);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
196
224
|
getColumnIndex() {
|
|
197
225
|
const table = findParentBlot(this, blotName.tableMain);
|
|
198
226
|
return table.getColIds().indexOf(this.colId);
|
|
@@ -221,6 +249,11 @@ export class TableCellFormat extends ContainerFormat {
|
|
|
221
249
|
if (parent !== null && parent.statics.blotName !== blotName.tableRow) {
|
|
222
250
|
this.wrap(blotName.tableRow, { tableId, rowId });
|
|
223
251
|
}
|
|
252
|
+
if (this.emptyRow.length > 0) {
|
|
253
|
+
for (const rowId of this.emptyRow) {
|
|
254
|
+
this.parent.parent.insertBefore(this.scroll.create(blotName.tableRow, { tableId: this.tableId, rowId }), this.parent.next);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
224
257
|
|
|
225
258
|
super.optimize(context);
|
|
226
259
|
}
|
|
@@ -4,7 +4,7 @@ import type TypeScroll from 'quill/blots/scroll';
|
|
|
4
4
|
import type { TableCellValue } from '../utils';
|
|
5
5
|
import type { TableCellFormat } from './table-cell-format';
|
|
6
6
|
import Quill from 'quill';
|
|
7
|
-
import { blotName, cssTextToObject, findParentBlot, findParentBlots } from '../utils';
|
|
7
|
+
import { blotName, cssTextToObject, findParentBlot, findParentBlots, toCamelCase } from '../utils';
|
|
8
8
|
import { ContainerFormat } from './container-format';
|
|
9
9
|
import { getValidCellspan } from './utils';
|
|
10
10
|
|
|
@@ -15,14 +15,16 @@ export class TableCellInnerFormat extends ContainerFormat {
|
|
|
15
15
|
static blotName = blotName.tableCellInner;
|
|
16
16
|
static tagName = 'div';
|
|
17
17
|
static className = 'ql-table-cell-inner';
|
|
18
|
-
static allowDataAttrs: Set<string> = new Set(['table-id', 'row-id', 'col-id', 'rowspan', 'colspan']);
|
|
18
|
+
static allowDataAttrs: Set<string> = new Set(['table-id', 'row-id', 'col-id', 'rowspan', 'colspan', 'empty-row']);
|
|
19
19
|
static defaultChild: TypeParchment.BlotConstructor = Block;
|
|
20
20
|
declare parent: TableCellFormat;
|
|
21
21
|
// keep `isAllowStyle` and `allowStyle` same with TableCellFormat
|
|
22
22
|
static allowStyle = new Set(['background-color', 'border', 'height']);
|
|
23
23
|
static isAllowStyle(str: string): boolean {
|
|
24
|
+
const cssAttrName = toCamelCase(str);
|
|
24
25
|
for (const style of this.allowStyle) {
|
|
25
|
-
|
|
26
|
+
// cause `cssTextToObject` will transform css string to camel case style name
|
|
27
|
+
if (cssAttrName.startsWith(toCamelCase(style))) {
|
|
26
28
|
return true;
|
|
27
29
|
}
|
|
28
30
|
}
|
|
@@ -37,6 +39,7 @@ export class TableCellInnerFormat extends ContainerFormat {
|
|
|
37
39
|
rowspan,
|
|
38
40
|
colspan,
|
|
39
41
|
style,
|
|
42
|
+
emptyRow,
|
|
40
43
|
} = value;
|
|
41
44
|
const node = super.create() as HTMLElement;
|
|
42
45
|
node.dataset.tableId = tableId;
|
|
@@ -45,11 +48,15 @@ export class TableCellInnerFormat extends ContainerFormat {
|
|
|
45
48
|
node.dataset.rowspan = String(getValidCellspan(rowspan));
|
|
46
49
|
node.dataset.colspan = String(getValidCellspan(colspan));
|
|
47
50
|
style && (node.dataset.style = style);
|
|
51
|
+
try {
|
|
52
|
+
emptyRow && (node.dataset.emptyRow = JSON.stringify(emptyRow));
|
|
53
|
+
}
|
|
54
|
+
catch {}
|
|
48
55
|
return node;
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
static formats(domNode: HTMLElement) {
|
|
52
|
-
const { tableId, rowId, colId, rowspan, colspan, style } = domNode.dataset;
|
|
59
|
+
const { tableId, rowId, colId, rowspan, colspan, style, emptyRow } = domNode.dataset;
|
|
53
60
|
const value: Record<string, any> = {
|
|
54
61
|
tableId: String(tableId),
|
|
55
62
|
rowId: String(rowId),
|
|
@@ -58,6 +65,10 @@ export class TableCellInnerFormat extends ContainerFormat {
|
|
|
58
65
|
colspan: Number(getValidCellspan(colspan)),
|
|
59
66
|
};
|
|
60
67
|
style && (value.style = style);
|
|
68
|
+
try {
|
|
69
|
+
emptyRow && (value.emptyRow = JSON.parse(emptyRow));
|
|
70
|
+
}
|
|
71
|
+
catch {}
|
|
61
72
|
return value;
|
|
62
73
|
}
|
|
63
74
|
|
|
@@ -129,11 +140,44 @@ export class TableCellInnerFormat extends ContainerFormat {
|
|
|
129
140
|
this.setFormatValue('colspan', value);
|
|
130
141
|
}
|
|
131
142
|
|
|
143
|
+
get emptyRow(): string[] {
|
|
144
|
+
try {
|
|
145
|
+
return JSON.parse(this.domNode.dataset.emptyRow!);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
set emptyRow(value: string[]) {
|
|
153
|
+
// if value same as currentEmptyRow, do nothing
|
|
154
|
+
if (this.emptyRow.toString() === value.toString()) return;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
if (value.length > 0) {
|
|
158
|
+
this.setFormatValue('empty-row', JSON.stringify(value), false);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
this.setFormatValue('empty-row', null, false);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
this.setFormatValue('empty-row', null, false);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
132
169
|
getColumnIndex() {
|
|
133
170
|
const table = findParentBlot(this, blotName.tableMain);
|
|
134
171
|
return table.getColIds().indexOf(this.colId);
|
|
135
172
|
}
|
|
136
173
|
|
|
174
|
+
setStyleByString(styleStr: string) {
|
|
175
|
+
const style = cssTextToObject(styleStr);
|
|
176
|
+
for (const [name, value] of Object.entries(style)) {
|
|
177
|
+
this.setFormatValue(name, value, true);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
137
181
|
formatAt(index: number, length: number, name: string, value: any) {
|
|
138
182
|
if (this.children.length === 0) {
|
|
139
183
|
this.appendChild(this.scroll.create(this.statics.defaultChild.blotName));
|
|
@@ -143,10 +187,7 @@ export class TableCellInnerFormat extends ContainerFormat {
|
|
|
143
187
|
super.formatAt(index, length, name, value);
|
|
144
188
|
// set style for `td`
|
|
145
189
|
if (value && value.style) {
|
|
146
|
-
|
|
147
|
-
for (const [name, value] of Object.entries(style)) {
|
|
148
|
-
this.setFormatValue(name, value, true);
|
|
149
|
-
}
|
|
190
|
+
this.setStyleByString(value.style);
|
|
150
191
|
}
|
|
151
192
|
}
|
|
152
193
|
|
|
@@ -185,11 +226,15 @@ export class TableCellInnerFormat extends ContainerFormat {
|
|
|
185
226
|
const blotValue = this.statics.formats(this.domNode);
|
|
186
227
|
// handle BlockEmbed to insert tableCellInner when setContents
|
|
187
228
|
if (this.prev && this.prev instanceof BlockEmbed) {
|
|
188
|
-
const
|
|
189
|
-
this.
|
|
190
|
-
this.
|
|
229
|
+
const prev = this.prev;
|
|
230
|
+
this.insertBefore(prev, this.children.head);
|
|
231
|
+
if (this.length() <= 1) {
|
|
232
|
+
const afterBlock = this.scroll.create('block');
|
|
233
|
+
this.insertBefore(afterBlock, prev.next);
|
|
234
|
+
}
|
|
191
235
|
}
|
|
192
|
-
|
|
236
|
+
const parentIsTableCell = parent !== null && parent.statics.blotName !== blotName.tableCell;
|
|
237
|
+
if (parentIsTableCell) {
|
|
193
238
|
this.wrap(blotName.tableCell, blotValue);
|
|
194
239
|
// when insert delta like: [ { attributes: { 'table-up-cell-inner': { ... } }, insert: '\n' }, { attributes: { 'table-up-cell-inner': { ... } }, insert: '\n' }, ...]
|
|
195
240
|
// that delta will create dom like: <td><div></div></td>... . that means TableCellInner will be an empty cell without 'block'
|
|
@@ -213,6 +258,16 @@ export class TableCellInnerFormat extends ContainerFormat {
|
|
|
213
258
|
// if cellInner doesn't have child then remove it. not insert a block
|
|
214
259
|
this.remove();
|
|
215
260
|
}
|
|
261
|
+
else {
|
|
262
|
+
// update delta data
|
|
263
|
+
if (
|
|
264
|
+
this.domNode.dataset.style
|
|
265
|
+
&& parentIsTableCell
|
|
266
|
+
&& parent.domNode.style.cssText !== this.domNode.dataset.style
|
|
267
|
+
) {
|
|
268
|
+
this.setStyleByString(this.domNode.dataset.style);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
216
271
|
}
|
|
217
272
|
|
|
218
273
|
insertBefore(blot: TypeParchment.Blot, ref?: TypeParchment.Blot | null) {
|
|
@@ -220,7 +275,7 @@ export class TableCellInnerFormat extends ContainerFormat {
|
|
|
220
275
|
const cellInnerBlot = blot as TableCellInnerFormat;
|
|
221
276
|
const cellInnerBlotValue = this.statics.formats(cellInnerBlot.domNode);
|
|
222
277
|
const selfValue = this.statics.formats(this.domNode);
|
|
223
|
-
const isSame = Object.entries(selfValue).every(([key, value]) => value === cellInnerBlotValue[key]);
|
|
278
|
+
const isSame = Object.entries(selfValue).every(([key, value]) => String(value) === String(cellInnerBlotValue[key]));
|
|
224
279
|
|
|
225
280
|
if (!isSame) {
|
|
226
281
|
const [selfRow, selfCell] = findParentBlots(this, [blotName.tableRow, blotName.tableCell] as const);
|
|
@@ -234,13 +289,6 @@ export class TableCellInnerFormat extends ContainerFormat {
|
|
|
234
289
|
newCellInner.appendChild(block);
|
|
235
290
|
});
|
|
236
291
|
selfRow.insertBefore(newCellInner.wrap(blotName.tableCell, selfValue), selfCell.next);
|
|
237
|
-
|
|
238
|
-
if (this.children.length === 0) {
|
|
239
|
-
this.remove();
|
|
240
|
-
if (this.parent.children.length === 0) {
|
|
241
|
-
this.parent.remove();
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
292
|
}
|
|
245
293
|
}
|
|
246
294
|
// different rowId. split current row. move lines which after ref to next row
|
|
@@ -265,7 +313,8 @@ export class TableCellInnerFormat extends ContainerFormat {
|
|
|
265
313
|
);
|
|
266
314
|
}
|
|
267
315
|
else {
|
|
268
|
-
|
|
316
|
+
const next = this.split(ref ? ref.offset() : 0);
|
|
317
|
+
return this.parent.insertBefore(cellInnerBlot, next);
|
|
269
318
|
}
|
|
270
319
|
}
|
|
271
320
|
else if (blot.statics.blotName === blotName.tableCol) {
|