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.
Files changed (39) hide show
  1. package/README.md +1 -0
  2. package/dist/index.css +1 -1
  3. package/dist/index.d.ts +35 -12
  4. package/dist/index.js +26 -25
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.umd.js +35 -31
  7. package/dist/index.umd.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/__tests__/e2e/table-keyboard-handler.test.ts +218 -0
  10. package/src/__tests__/e2e/table-selection.test.ts +137 -131
  11. package/src/__tests__/unit/table-cell-merge.test.ts +261 -1
  12. package/src/__tests__/unit/table-clipboard.test.ts +62 -44
  13. package/src/__tests__/unit/table-insert.test.ts +39 -2
  14. package/src/__tests__/unit/table-redo-undo.test.ts +69 -0
  15. package/src/__tests__/unit/table-remove.test.ts +4 -2
  16. package/src/__tests__/unit/utils.ts +22 -4
  17. package/src/__tests__/unit/vitest.d.ts +4 -1
  18. package/src/formats/index.ts +6 -0
  19. package/src/formats/overrides/block-embed.ts +54 -0
  20. package/src/formats/overrides/block.ts +10 -4
  21. package/src/formats/overrides/index.ts +1 -0
  22. package/src/formats/table-cell-format.ts +39 -6
  23. package/src/formats/table-cell-inner-format.ts +70 -21
  24. package/src/formats/table-main-format.ts +92 -1
  25. package/src/formats/table-row-format.ts +13 -2
  26. package/src/modules/table-clipboard.ts +30 -35
  27. package/src/modules/table-resize/table-resize-box.ts +2 -2
  28. package/src/modules/table-resize/table-resize-common.ts +1 -1
  29. package/src/modules/table-resize/table-resize-line.ts +1 -1
  30. package/src/modules/table-resize/table-resize-scale.ts +1 -1
  31. package/src/modules/table-scrollbar.ts +5 -5
  32. package/src/modules/table-selection.ts +52 -31
  33. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  34. package/src/style/table-selection.less +1 -0
  35. package/src/table-up.ts +57 -23
  36. package/src/utils/blot-helper.ts +7 -4
  37. package/src/utils/index.ts +1 -1
  38. package/src/utils/{scroll-event-handle.ts → scroll-event-helper.ts} +7 -0
  39. 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
- <col width="100%" data-full="true" />
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
- ${new Array(5).fill(`<col width="20%" data-full="true" />`).join('\n')}
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
- <td rowspan="3" colspan="3">
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(received, expected, options = {}) {
57
- const ignoreAttrs = options?.ignoreAttrs ?? [];
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?: { ignoreAttrs?: string[] }) => R;
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' {
@@ -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 = typeof name === 'string' ? this.scroll.create(name, value) : name;
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
- if (selfParent != null) {
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 || undefined);
66
+ this.parent.insertBefore(replacement, this.next);
61
67
  this.remove();
62
68
  }
63
69
  this.attributes.copy(replacement as TypeParchment.BlockBlot);
@@ -1,2 +1,3 @@
1
1
  export * from './block';
2
+ export * from './block-embed';
2
3
  export * from './scroll';
@@ -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
- if (str.startsWith(style)) {
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
- if (this.children.head && this.children.head.statics.blotName === blotName.tableCellInner && this.domNode.style.cssText) {
96
- (this.children.head.domNode as HTMLElement).dataset.style = this.domNode.style.cssText;
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
- if (str.startsWith(style)) {
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
- const style = cssTextToObject(value.style);
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 afterBlock = this.scroll.create('block');
189
- this.appendChild(this.prev);
190
- this.appendChild(afterBlock);
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
- if (parent !== null && parent.statics.blotName !== blotName.tableCell) {
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
- return this.parent.insertBefore(cellInnerBlot, this.next);
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) {