@pie-lib/editable-html-tip-tap 1.2.0-next.9 → 2.0.1

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 (94) hide show
  1. package/CHANGELOG.md +176 -0
  2. package/lib/components/CharacterPicker.js +1 -0
  3. package/lib/components/CharacterPicker.js.map +1 -1
  4. package/lib/components/EditableHtml.js +84 -43
  5. package/lib/components/EditableHtml.js.map +1 -1
  6. package/lib/components/MenuBar.js +74 -43
  7. package/lib/components/MenuBar.js.map +1 -1
  8. package/lib/components/TiptapContainer.js +9 -8
  9. package/lib/components/TiptapContainer.js.map +1 -1
  10. package/lib/components/icons/TextAlign.js +2 -2
  11. package/lib/components/icons/TextAlign.js.map +1 -1
  12. package/lib/components/image/InsertImageHandler.js +10 -13
  13. package/lib/components/image/InsertImageHandler.js.map +1 -1
  14. package/lib/components/media/MediaDialog.js.map +1 -1
  15. package/lib/components/respArea/DragInTheBlank/DragInTheBlank.js +6 -1
  16. package/lib/components/respArea/DragInTheBlank/DragInTheBlank.js.map +1 -1
  17. package/lib/components/respArea/DragInTheBlank/choice.js +15 -7
  18. package/lib/components/respArea/DragInTheBlank/choice.js.map +1 -1
  19. package/lib/components/respArea/ExplicitConstructedResponse.js +29 -11
  20. package/lib/components/respArea/ExplicitConstructedResponse.js.map +1 -1
  21. package/lib/components/respArea/InlineDropdown.js +35 -6
  22. package/lib/components/respArea/InlineDropdown.js.map +1 -1
  23. package/lib/extensions/custom-toolbar-wrapper.js +3 -2
  24. package/lib/extensions/custom-toolbar-wrapper.js.map +1 -1
  25. package/lib/extensions/div-node.js +83 -0
  26. package/lib/extensions/div-node.js.map +1 -0
  27. package/lib/extensions/ensure-empty-root-div.js +48 -0
  28. package/lib/extensions/ensure-empty-root-div.js.map +1 -0
  29. package/lib/extensions/ensure-list-item-content-is-div.js +64 -0
  30. package/lib/extensions/ensure-list-item-content-is-div.js.map +1 -0
  31. package/lib/extensions/extended-list-item.js +15 -0
  32. package/lib/extensions/extended-list-item.js.map +1 -0
  33. package/lib/extensions/extended-table-cell.js +22 -0
  34. package/lib/extensions/extended-table-cell.js.map +1 -0
  35. package/lib/extensions/extended-table.js +50 -1
  36. package/lib/extensions/extended-table.js.map +1 -1
  37. package/lib/extensions/image-component.js +102 -51
  38. package/lib/extensions/image-component.js.map +1 -1
  39. package/lib/extensions/image.js +51 -2
  40. package/lib/extensions/image.js.map +1 -1
  41. package/lib/extensions/math.js +50 -9
  42. package/lib/extensions/math.js.map +1 -1
  43. package/lib/extensions/media.js +3 -1
  44. package/lib/extensions/media.js.map +1 -1
  45. package/lib/extensions/responseArea.js +12 -7
  46. package/lib/extensions/responseArea.js.map +1 -1
  47. package/lib/styles/editorContainerStyles.js +5 -4
  48. package/lib/styles/editorContainerStyles.js.map +1 -1
  49. package/lib/utils/helper.js +17 -0
  50. package/lib/utils/helper.js.map +1 -0
  51. package/package.json +8 -8
  52. package/src/__tests__/EditableHtml.test.jsx +90 -7
  53. package/src/__tests__/index.test.jsx +11 -3
  54. package/src/components/CharacterPicker.jsx +1 -0
  55. package/src/components/EditableHtml.jsx +91 -41
  56. package/src/components/MenuBar.jsx +57 -24
  57. package/src/components/TiptapContainer.jsx +10 -8
  58. package/src/components/__tests__/CharacterPicker.test.jsx +22 -0
  59. package/src/components/__tests__/ExplicitConstructedResponse.test.jsx +55 -12
  60. package/src/components/__tests__/InlineDropdown.test.jsx +203 -10
  61. package/src/components/__tests__/InsertImageHandler.test.js +28 -21
  62. package/src/components/__tests__/MenuBar.test.jsx +32 -0
  63. package/src/components/icons/TextAlign.jsx +1 -1
  64. package/src/components/image/InsertImageHandler.js +9 -13
  65. package/src/components/respArea/DragInTheBlank/DragInTheBlank.jsx +6 -1
  66. package/src/components/respArea/DragInTheBlank/choice.jsx +32 -4
  67. package/src/components/respArea/ExplicitConstructedResponse.jsx +33 -10
  68. package/src/components/respArea/InlineDropdown.jsx +45 -10
  69. package/src/extensions/__tests__/divNode.test.js +87 -0
  70. package/src/extensions/__tests__/ensure-empty-root-div.test.js +57 -0
  71. package/src/extensions/__tests__/ensure-list-item-content-is-div.test.js +44 -0
  72. package/src/extensions/__tests__/extended-list-item.test.js +13 -0
  73. package/src/extensions/__tests__/extended-table-cell.test.js +22 -0
  74. package/src/extensions/__tests__/extended-table.test.js +98 -1
  75. package/src/extensions/__tests__/image-component.test.jsx +105 -9
  76. package/src/extensions/__tests__/image.test.js +109 -8
  77. package/src/extensions/__tests__/math.test.js +348 -0
  78. package/src/extensions/__tests__/media-node-view.test.jsx +10 -8
  79. package/src/extensions/__tests__/responseArea.test.js +291 -0
  80. package/src/extensions/custom-toolbar-wrapper.jsx +2 -2
  81. package/src/extensions/div-node.js +86 -0
  82. package/src/extensions/ensure-empty-root-div.js +47 -0
  83. package/src/extensions/ensure-list-item-content-is-div.js +62 -0
  84. package/src/extensions/extended-list-item.js +10 -0
  85. package/src/extensions/extended-table-cell.js +19 -0
  86. package/src/extensions/extended-table.js +37 -1
  87. package/src/extensions/image-component.jsx +114 -69
  88. package/src/extensions/image.js +56 -1
  89. package/src/extensions/math.js +62 -10
  90. package/src/extensions/media.js +1 -1
  91. package/src/extensions/responseArea.js +13 -11
  92. package/src/styles/editorContainerStyles.js +5 -4
  93. package/src/utils/helper.js +17 -0
  94. /package/src/components/media/{MediaDialog.js → MediaDialog.jsx} +0 -0
@@ -100,7 +100,7 @@ describe('MediaNodeView Component', () => {
100
100
  updateAttributes={mockUpdateAttributes}
101
101
  deleteNode={mockDeleteNode}
102
102
  options={{}}
103
- />
103
+ />,
104
104
  );
105
105
 
106
106
  // Verify the component rendered successfully
@@ -130,7 +130,7 @@ describe('MediaNodeView Component', () => {
130
130
  updateAttributes={mockUpdateAttributes}
131
131
  deleteNode={mockDeleteNode}
132
132
  options={{}}
133
- />
133
+ />,
134
134
  );
135
135
 
136
136
  // Verify the component rendered successfully
@@ -159,7 +159,7 @@ describe('MediaNodeView Component', () => {
159
159
  updateAttributes={mockUpdateAttributes}
160
160
  deleteNode={mockDeleteNode}
161
161
  options={{}}
162
- />
162
+ />,
163
163
  );
164
164
 
165
165
  // Empty string is falsy, so dialog WOULD open with current implementation
@@ -185,7 +185,7 @@ describe('MediaNodeView Component', () => {
185
185
  updateAttributes={mockUpdateAttributes}
186
186
  deleteNode={mockDeleteNode}
187
187
  options={{}}
188
- />
188
+ />,
189
189
  );
190
190
 
191
191
  expect(getByTestId('edit-button')).toBeTruthy();
@@ -210,7 +210,7 @@ describe('MediaNodeView Component', () => {
210
210
  updateAttributes={mockUpdateAttributes}
211
211
  deleteNode={mockDeleteNode}
212
212
  options={{}}
213
- />
213
+ />,
214
214
  );
215
215
 
216
216
  const audio = container.querySelector('audio');
@@ -237,12 +237,14 @@ describe('MediaNodeView Component', () => {
237
237
  updateAttributes={mockUpdateAttributes}
238
238
  deleteNode={mockDeleteNode}
239
239
  options={{}}
240
- />
240
+ />,
241
241
  );
242
242
 
243
243
  const iframe = container.querySelector('iframe');
244
244
  expect(iframe).toBeTruthy();
245
245
  expect(iframe.getAttribute('src')).toBe('https://www.youtube.com/embed/dQw4w9WgXcQ');
246
+ expect(iframe.getAttribute('width')).toBe('640');
247
+ expect(iframe.getAttribute('height')).toBe('480');
246
248
  });
247
249
 
248
250
  it('should render MediaToolbar', () => {
@@ -261,7 +263,7 @@ describe('MediaNodeView Component', () => {
261
263
  updateAttributes={mockUpdateAttributes}
262
264
  deleteNode={mockDeleteNode}
263
265
  options={{}}
264
- />
266
+ />,
265
267
  );
266
268
 
267
269
  expect(getByTestId('media-toolbar')).toBeTruthy();
@@ -285,7 +287,7 @@ describe('MediaNodeView Component', () => {
285
287
  updateAttributes={mockUpdateAttributes}
286
288
  deleteNode={mockDeleteNode}
287
289
  options={{}}
288
- />
290
+ />,
289
291
  );
290
292
 
291
293
  fireEvent.click(getByTestId('remove-button'));
@@ -101,6 +101,297 @@ describe('ResponseAreaExtension', () => {
101
101
  expect(commands).toHaveProperty('insertResponseArea');
102
102
  expect(typeof commands.insertResponseArea).toBe('function');
103
103
  });
104
+
105
+ it('returns refreshResponseArea command', () => {
106
+ const commands = ResponseAreaExtension.addCommands();
107
+
108
+ expect(commands).toHaveProperty('refreshResponseArea');
109
+ expect(typeof commands.refreshResponseArea).toBe('function');
110
+ });
111
+
112
+ it('refreshResponseArea handles node with attrs safely', () => {
113
+ const context = {
114
+ options: {
115
+ type: 'explicit-constructed-response',
116
+ maxResponseAreas: 5,
117
+ },
118
+ };
119
+
120
+ const commands = ResponseAreaExtension.addCommands.call(context);
121
+ const refreshCommand = commands.refreshResponseArea();
122
+
123
+ // Mock transaction and state
124
+ const mockNode = {
125
+ attrs: {
126
+ index: '0',
127
+ value: 'test',
128
+ },
129
+ };
130
+
131
+ const mockTr = {
132
+ setNodeMarkup: jest.fn(),
133
+ setSelection: jest.fn(),
134
+ };
135
+
136
+ const mockState = {
137
+ selection: {
138
+ from: 0,
139
+ $from: {
140
+ nodeAfter: mockNode,
141
+ },
142
+ },
143
+ tr: mockTr,
144
+ };
145
+
146
+ const mockCommands = {
147
+ focus: jest.fn(),
148
+ };
149
+
150
+ const mockDispatch = jest.fn();
151
+
152
+ refreshCommand({
153
+ tr: mockTr,
154
+ state: mockState,
155
+ commands: mockCommands,
156
+ dispatch: mockDispatch,
157
+ });
158
+
159
+ expect(mockTr.setNodeMarkup).toHaveBeenCalled();
160
+ });
161
+
162
+ it('refreshResponseArea handles node without attrs safely (optional chaining)', () => {
163
+ const context = {
164
+ options: {
165
+ type: 'explicit-constructed-response',
166
+ maxResponseAreas: 5,
167
+ },
168
+ };
169
+
170
+ const commands = ResponseAreaExtension.addCommands.call(context);
171
+ const refreshCommand = commands.refreshResponseArea();
172
+
173
+ // Mock transaction and state with node that has no attrs
174
+ const mockNode = null;
175
+
176
+ const mockTr = {
177
+ setNodeMarkup: jest.fn(),
178
+ setSelection: jest.fn(),
179
+ };
180
+
181
+ const mockState = {
182
+ selection: {
183
+ from: 0,
184
+ $from: {
185
+ nodeAfter: mockNode,
186
+ },
187
+ },
188
+ tr: mockTr,
189
+ };
190
+
191
+ const mockCommands = {
192
+ focus: jest.fn(),
193
+ };
194
+
195
+ const mockDispatch = jest.fn();
196
+
197
+ // This should not throw an error due to optional chaining on node?.attrs
198
+ expect(() => {
199
+ refreshCommand({
200
+ tr: mockTr,
201
+ state: mockState,
202
+ commands: mockCommands,
203
+ dispatch: mockDispatch,
204
+ });
205
+ }).not.toThrow();
206
+ });
207
+
208
+ it('refreshResponseArea updates timestamp in node attributes', () => {
209
+ const context = {
210
+ options: {
211
+ type: 'explicit-constructed-response',
212
+ maxResponseAreas: 5,
213
+ },
214
+ };
215
+
216
+ const commands = ResponseAreaExtension.addCommands.call(context);
217
+ const refreshCommand = commands.refreshResponseArea();
218
+
219
+ const mockNode = {
220
+ attrs: {
221
+ index: '0',
222
+ value: 'test',
223
+ updated: '1234567890',
224
+ },
225
+ };
226
+
227
+ const mockTr = {
228
+ setNodeMarkup: jest.fn((pos, type, attrs) => {
229
+ // Verify that updated timestamp is being set
230
+ expect(attrs.updated).toBeDefined();
231
+ expect(attrs.updated).not.toBe('1234567890');
232
+ }),
233
+ setSelection: jest.fn(),
234
+ };
235
+
236
+ const mockState = {
237
+ selection: {
238
+ from: 0,
239
+ $from: {
240
+ nodeAfter: mockNode,
241
+ },
242
+ },
243
+ tr: mockTr,
244
+ };
245
+
246
+ const mockCommands = {
247
+ focus: jest.fn(),
248
+ };
249
+
250
+ const mockDispatch = jest.fn();
251
+
252
+ refreshCommand({
253
+ tr: mockTr,
254
+ state: mockState,
255
+ commands: mockCommands,
256
+ dispatch: mockDispatch,
257
+ });
258
+
259
+ expect(mockTr.setNodeMarkup).toHaveBeenCalled();
260
+ });
261
+
262
+ describe('insertResponseArea', () => {
263
+ beforeEach(() => {
264
+ jest.resetModules();
265
+ });
266
+
267
+ const buildInsertCommand = () => {
268
+ const { ResponseAreaExtension } = require('../responseArea');
269
+ const context = {
270
+ options: {
271
+ type: 'inline-dropdown',
272
+ maxResponseAreas: 5,
273
+ },
274
+ };
275
+ const commands = ResponseAreaExtension.addCommands.call(context);
276
+ return commands.insertResponseArea('inline-dropdown');
277
+ };
278
+
279
+ const createDoc = (existingCount, typeName = 'inline_dropdown') => ({
280
+ descendants: jest.fn((callback) => {
281
+ for (let i = 0; i < existingCount; i += 1) {
282
+ callback({ type: { name: typeName } }, i);
283
+ }
284
+ }),
285
+ content: { size: 50 },
286
+ });
287
+
288
+ it('assigns index 1 and id 1 on the first insert', () => {
289
+ const insert = buildInsertCommand();
290
+ const mockInlineNode = { nodeSize: 1 };
291
+ const create = jest.fn(() => mockInlineNode);
292
+ const mockDoc = createDoc(0);
293
+ const mockTr = {
294
+ insert: jest.fn(),
295
+ doc: mockDoc,
296
+ setSelection: jest.fn(),
297
+ };
298
+ const state = {
299
+ schema: {
300
+ nodes: {
301
+ inline_dropdown: { create },
302
+ },
303
+ },
304
+ doc: mockDoc,
305
+ selection: { from: 5 },
306
+ };
307
+ const mockDispatch = jest.fn();
308
+ const mockCommands = { focus: jest.fn() };
309
+
310
+ const result = insert({
311
+ tr: mockTr,
312
+ state,
313
+ dispatch: mockDispatch,
314
+ commands: mockCommands,
315
+ });
316
+
317
+ expect(result).toBe(true);
318
+ expect(create).toHaveBeenCalledWith({
319
+ index: '1',
320
+ id: '1',
321
+ value: '',
322
+ });
323
+ expect(mockTr.insert).toHaveBeenCalledWith(5, mockInlineNode);
324
+ expect(mockDispatch).toHaveBeenCalled();
325
+ });
326
+
327
+ it('assigns consecutive indices on repeated inserts', () => {
328
+ const insert = buildInsertCommand();
329
+ const mockInlineNode = { nodeSize: 1 };
330
+ const create = jest.fn(() => mockInlineNode);
331
+ const mockDoc = createDoc(0);
332
+
333
+ const runOnce = () => {
334
+ const mockTr = {
335
+ insert: jest.fn(),
336
+ doc: mockDoc,
337
+ setSelection: jest.fn(),
338
+ };
339
+ const state = {
340
+ schema: {
341
+ nodes: {
342
+ inline_dropdown: { create },
343
+ },
344
+ },
345
+ doc: mockDoc,
346
+ selection: { from: 1 },
347
+ };
348
+ insert({
349
+ tr: mockTr,
350
+ state,
351
+ dispatch: jest.fn(),
352
+ commands: { focus: jest.fn() },
353
+ });
354
+ };
355
+
356
+ runOnce();
357
+ runOnce();
358
+
359
+ expect(create.mock.calls[0][0]).toEqual({ index: '1', id: '1', value: '' });
360
+ expect(create.mock.calls[1][0]).toEqual({ index: '2', id: '2', value: '' });
361
+ });
362
+
363
+ it('returns false when maxResponseAreas is reached', () => {
364
+ const insert = buildInsertCommand();
365
+ const mockInlineNode = { nodeSize: 1 };
366
+ const create = jest.fn(() => mockInlineNode);
367
+ const mockDoc = createDoc(5);
368
+ const mockTr = {
369
+ insert: jest.fn(),
370
+ doc: mockDoc,
371
+ setSelection: jest.fn(),
372
+ };
373
+ const state = {
374
+ schema: {
375
+ nodes: {
376
+ inline_dropdown: { create },
377
+ },
378
+ },
379
+ doc: mockDoc,
380
+ selection: { from: 5 },
381
+ };
382
+
383
+ const result = insert({
384
+ tr: mockTr,
385
+ state,
386
+ dispatch: jest.fn(),
387
+ commands: { focus: jest.fn() },
388
+ });
389
+
390
+ expect(result).toBe(false);
391
+ expect(create).not.toHaveBeenCalled();
392
+ expect(mockTr.insert).not.toHaveBeenCalled();
393
+ });
394
+ });
104
395
  });
105
396
  });
106
397
 
@@ -48,7 +48,7 @@ const SharedContainer = styled('div')({
48
48
  });
49
49
 
50
50
  function CustomToolbarWrapper(props) {
51
- const { children, deletable, toolbarOpts, autoWidth, isFocused, doneButtonRef, onDelete, showDone, onDone } = props;
51
+ const { children, deletable, toolbarOpts, autoWidth, isFocused, doneButtonRef, onDelete, showDone, onDone, style } = props;
52
52
  const customStyles = toolbarOpts.minWidth !== undefined ? { minWidth: toolbarOpts.minWidth } : {};
53
53
 
54
54
  return (
@@ -59,7 +59,7 @@ function CustomToolbarWrapper(props) {
59
59
  isFocused={toolbarOpts.alwaysVisible || isFocused}
60
60
  autoWidth={autoWidth}
61
61
  isHidden={toolbarOpts.isHidden === true}
62
- style={{ ...customStyles }}
62
+ style={{ ...customStyles, ...style }}
63
63
  >
64
64
  {children}
65
65
 
@@ -0,0 +1,86 @@
1
+ // DivNode.ts
2
+ import { Node } from '@tiptap/core';
3
+
4
+ export const DivNode = Node.create({
5
+ name: 'div',
6
+ group: 'block',
7
+ content: 'inline*',
8
+
9
+ parseHTML() {
10
+ return [{ tag: 'div' }];
11
+ },
12
+
13
+ renderHTML({ HTMLAttributes }) {
14
+ return ['div', HTMLAttributes, 0];
15
+ },
16
+
17
+ addKeyboardShortcuts() {
18
+ const isInsideListItem = ($from) => {
19
+ for (let depth = $from.depth; depth >= 0; depth -= 1) {
20
+ if ($from.node(depth).type.name === 'listItem') {
21
+ return true;
22
+ }
23
+ }
24
+ return false;
25
+ };
26
+
27
+ return {
28
+ Enter: () => {
29
+ const { state } = this.editor;
30
+ const { $from } = state.selection;
31
+
32
+ if ($from.parent.type.name !== 'div') {
33
+ return false;
34
+ }
35
+ if (isInsideListItem($from)) {
36
+ return false;
37
+ }
38
+
39
+ return this.editor
40
+ .chain()
41
+ .focus()
42
+ .setNode('paragraph') // current div becomes <p>
43
+ .splitBlock() // create another <p>
44
+ .run();
45
+ },
46
+
47
+ // When the cursor is in a div and the user presses Backspace,
48
+ // ProseMirror's default handler may try to join/delete the block node
49
+ // once it becomes empty. That triggers the Enter shortcut above
50
+ // (div → p conversion + split), making it look like a new line is
51
+ // inserted instead of deleting.
52
+ // We handle two cases explicitly:
53
+ // 1. The div already IS empty → swallow the event (nothing to delete).
54
+ // 2. The div has exactly ONE character left → delete just that character
55
+ // using a precise transaction, then stop. This prevents ProseMirror
56
+ // from following up with a block-join that triggers the Enter handler.
57
+ Backspace: () => {
58
+ const { state } = this.editor;
59
+ const { $from, empty: selectionEmpty } = state.selection;
60
+
61
+ if ($from.parent.type.name !== 'div') {
62
+ return false;
63
+ }
64
+
65
+ if (!selectionEmpty) {
66
+ return false;
67
+ }
68
+
69
+ const parentText = $from.parent.textContent;
70
+
71
+ if (parentText.length === 0) {
72
+ return state.doc.childCount === 1 ? true : false;
73
+ }
74
+
75
+ if (parentText.length === 1 && $from.parentOffset === 1) {
76
+ const { tr } = state;
77
+ tr.delete($from.pos - 1, $from.pos);
78
+ this.editor.view.dispatch(tr);
79
+ return true;
80
+ }
81
+
82
+ return false;
83
+ },
84
+ };
85
+ },
86
+ });
@@ -0,0 +1,47 @@
1
+ import { Extension } from '@tiptap/core';
2
+ import { Plugin, PluginKey } from '@tiptap/pm/state';
3
+
4
+ /**
5
+ * After ProseMirror repairs an empty document, it often inserts a `paragraph`.
6
+ * We want a lone empty top-level block to be `div` (DivNode) so typing stays `<div>A</div>`.
7
+ */
8
+ export const EnsureEmptyRootIsDiv = Extension.create({
9
+ name: 'ensureEmptyRootIsDiv',
10
+
11
+ addProseMirrorPlugins() {
12
+ const key = new PluginKey(this.name);
13
+
14
+ return [
15
+ new Plugin({
16
+ key,
17
+ appendTransaction(transactions, _oldState, newState) {
18
+ if (!transactions.some((tr) => tr.docChanged)) {
19
+ return null;
20
+ }
21
+
22
+ const { doc } = newState;
23
+ if (doc.childCount !== 1) {
24
+ return null;
25
+ }
26
+
27
+ const first = doc.firstChild;
28
+ if (first.type.name !== 'paragraph' || first.content.size > 0) {
29
+ return null;
30
+ }
31
+
32
+ const divType = newState.schema.nodes.div;
33
+ if (!divType) {
34
+ return null;
35
+ }
36
+
37
+ // Top-level content positions are 0 .. doc.content.size. The first block spans
38
+ // [0, first.nodeSize). Using start=1 replaces the wrong slice and can leave the
39
+ // document inconsistent after a full delete (Cmd+A, Backspace).
40
+ const start = 0;
41
+ const end = first.nodeSize;
42
+ return newState.tr.replaceWith(start, end, divType.create());
43
+ },
44
+ }),
45
+ ];
46
+ },
47
+ });
@@ -0,0 +1,62 @@
1
+ import { Extension } from '@tiptap/core';
2
+ import { Plugin, PluginKey } from '@tiptap/pm/state';
3
+
4
+ /**
5
+ * Some list operations preserve/create `paragraph` children in `listItem`.
6
+ * Normalize direct `listItem > paragraph` children into `div` so list content
7
+ * stays consistent with DivNode-based editing.
8
+ */
9
+ export const EnsureListItemContentIsDiv = Extension.create({
10
+ name: 'ensureListItemContentIsDiv',
11
+
12
+ addProseMirrorPlugins() {
13
+ const key = new PluginKey(this.name);
14
+
15
+ return [
16
+ new Plugin({
17
+ key,
18
+ appendTransaction(transactions, _oldState, newState) {
19
+ if (!transactions.some((tr) => tr.docChanged)) {
20
+ return null;
21
+ }
22
+
23
+ const { doc, schema } = newState;
24
+ const divType = schema.nodes.div;
25
+ if (!divType) {
26
+ return null;
27
+ }
28
+
29
+ const positionsToConvert = [];
30
+
31
+ doc.descendants((node, pos) => {
32
+ if (node.type.name !== 'listItem') {
33
+ return;
34
+ }
35
+
36
+ let childOffset = 1;
37
+ node.forEach((child) => {
38
+ if (child.type.name === 'paragraph') {
39
+ positionsToConvert.push({
40
+ pos: pos + childOffset,
41
+ attrs: child.attrs,
42
+ });
43
+ }
44
+ childOffset += child.nodeSize;
45
+ });
46
+ });
47
+
48
+ if (positionsToConvert.length === 0) {
49
+ return null;
50
+ }
51
+
52
+ const tr = newState.tr;
53
+ positionsToConvert
54
+ .sort((a, b) => b.pos - a.pos)
55
+ .forEach(({ pos, attrs }) => tr.setNodeMarkup(pos, divType, attrs));
56
+
57
+ return tr;
58
+ },
59
+ }),
60
+ ];
61
+ },
62
+ });
@@ -0,0 +1,10 @@
1
+ import { ListItem } from '@tiptap/extension-list-item';
2
+
3
+ /**
4
+ * Default list items use `paragraph block*`, so empty/new items become `<p>`.
5
+ * Prefer `div` first to keep consistency with DivNode at root and table cells.
6
+ */
7
+ export const ExtendedListItem = ListItem.extend({
8
+ content:
9
+ '(div | paragraph | heading | bulletList | orderedList | blockquote | codeBlock | horizontalRule | image | imageUploadNode)+',
10
+ });
@@ -0,0 +1,19 @@
1
+ import { TableCell } from '@tiptap/extension-table-cell';
2
+ import { TableHeader } from '@tiptap/extension-table-header';
3
+
4
+ /**
5
+ * Default table cells use ProseMirror `createAndFill()`, which prefers the first
6
+ * block type allowed by the content expression. Stock cells use `block+`, so
7
+ * `paragraph` wins. Listing `div` first matches the editor default for plain text
8
+ * (see DivNode) while still allowing other blocks (lists, headings, images, …).
9
+ */
10
+ const TABLE_CELL_BLOCK_CONTENT =
11
+ '(div | paragraph | heading | bulletList | orderedList | blockquote | codeBlock | horizontalRule | image | imageUploadNode)+';
12
+
13
+ export const ExtendedTableCell = TableCell.extend({
14
+ content: TABLE_CELL_BLOCK_CONTENT,
15
+ });
16
+
17
+ export const ExtendedTableHeader = TableHeader.extend({
18
+ content: TABLE_CELL_BLOCK_CONTENT,
19
+ });
@@ -1,6 +1,41 @@
1
- import { Table } from '@tiptap/extension-table';
1
+ import { Table, TableView } from '@tiptap/extension-table';
2
+
3
+ const applyPresentationToTableElement = (table, attrs) => {
4
+ const border = attrs?.border != null && attrs.border !== '' ? attrs.border : '1';
5
+
6
+ table.setAttribute('border', border);
7
+ table.style.setProperty('color', 'var(--pie-text, black)');
8
+ table.style.setProperty('background-color', 'var(--pie-background, rgba(255, 255, 255))');
9
+ };
10
+
11
+ class ExtendedTableView extends TableView {
12
+ constructor(node, cellMinWidth, view) {
13
+ super(node, cellMinWidth, view);
14
+ applyPresentationToTableElement(this.table, node.attrs);
15
+ }
16
+
17
+ update(node) {
18
+ const ok = super.update(node);
19
+
20
+ if (ok) {
21
+ applyPresentationToTableElement(this.table, node.attrs);
22
+ }
23
+
24
+ return ok;
25
+ }
26
+ }
2
27
 
3
28
  const ExtendedTable = Table.extend({
29
+ addOptions() {
30
+ return {
31
+ ...this.parent?.(),
32
+ // ProseMirror's default table DOM does not prune extra <col> nodes when a column
33
+ // is removed; TableView (via column resizing) does.
34
+ resizable: true,
35
+ handleWidth: 0,
36
+ View: ExtendedTableView,
37
+ };
38
+ },
4
39
  addAttributes() {
5
40
  return {
6
41
  border: { default: '1' },
@@ -21,4 +56,5 @@ const ExtendedTable = Table.extend({
21
56
  },
22
57
  });
23
58
 
59
+ export { applyPresentationToTableElement, ExtendedTableView };
24
60
  export default ExtendedTable;