@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
@@ -73,14 +73,10 @@ const StyledRoot = styled('div', {
73
73
  },
74
74
  },
75
75
  '& blockquote': {
76
- borderLeft: '3px solid var(--gray-3)',
77
- margin: '1.5rem 0',
78
- paddingLeft: '1rem',
79
- },
80
- '& hr': {
81
- border: 'none',
82
- borderTop: '1px solid var(--gray-2)',
83
- margin: '2rem 0',
76
+ background: '#f9f9f9',
77
+ borderLeft: '5px solid #ccc',
78
+ margin: '1.5em 10px',
79
+ padding: '.5em 10px',
84
80
  },
85
81
  '& p': {
86
82
  margin: '0',
@@ -152,6 +148,12 @@ function TiptapContainer(props) {
152
148
  ref,
153
149
  } = props;
154
150
 
151
+ useEffect(() => {
152
+ if (editor && rootRef.current) {
153
+ editor._tiptapContainerEl = rootRef.current;
154
+ }
155
+ }, [editor, rootRef.current]);
156
+
155
157
  useEffect(() => {
156
158
  if (editor && autoFocus) {
157
159
  Promise.resolve().then(() => {
@@ -194,4 +194,26 @@ describe('CharacterPicker', () => {
194
194
  const dialog = container.querySelector('.insert-character-dialog');
195
195
  expect(dialog).toHaveStyle({ position: 'absolute' });
196
196
  });
197
+
198
+ it('adds data-toolbar-for attribute with editor instanceId', () => {
199
+ const editorWithInstanceId = {
200
+ ...mockEditor,
201
+ instanceId: 'editor-123',
202
+ };
203
+ const opts = {
204
+ characters: [['á', 'é']],
205
+ };
206
+ const { container } = render(<CharacterPicker editor={editorWithInstanceId} opts={opts} onClose={jest.fn()} />);
207
+ const dialog = container.querySelector('.insert-character-dialog');
208
+ expect(dialog).toHaveAttribute('data-toolbar-for', 'editor-123');
209
+ });
210
+
211
+ it('renders without instanceId gracefully', () => {
212
+ const opts = {
213
+ characters: [['á', 'é']],
214
+ };
215
+ const { container } = render(<CharacterPicker editor={mockEditor} opts={opts} onClose={jest.fn()} />);
216
+ const dialog = container.querySelector('.insert-character-dialog');
217
+ expect(dialog).toBeInTheDocument();
218
+ });
197
219
  });
@@ -10,19 +10,37 @@ jest.mock('@tiptap/react', () => ({
10
10
  ),
11
11
  }));
12
12
 
13
+ jest.mock('react-dom', () => ({
14
+ ...jest.requireActual('react-dom'),
15
+ createPortal: (node) => node,
16
+ }));
17
+
13
18
  describe('ExplicitConstructedResponse', () => {
14
- const mockEditor = {
15
- state: {
16
- selection: {
17
- from: 0,
18
- to: 1,
19
+ const buildMockEditor = (overrides = {}) => {
20
+ const mockTr = {
21
+ delete: jest.fn(),
22
+ };
23
+ return {
24
+ state: {
25
+ selection: {
26
+ from: 0,
27
+ to: 1,
28
+ },
29
+ tr: mockTr,
19
30
  },
20
- },
21
- commands: {
22
- focus: jest.fn(),
23
- },
31
+ view: {
32
+ dispatch: jest.fn(),
33
+ },
34
+ commands: {
35
+ focus: jest.fn(),
36
+ },
37
+ _tiptapContainerEl: document.createElement('div'),
38
+ ...overrides,
39
+ };
24
40
  };
25
41
 
42
+ let mockEditor;
43
+
26
44
  const mockNode = {
27
45
  attrs: {
28
46
  index: '0',
@@ -46,6 +64,8 @@ describe('ExplicitConstructedResponse', () => {
46
64
 
47
65
  beforeEach(() => {
48
66
  jest.clearAllMocks();
67
+ mockEditor = buildMockEditor();
68
+ defaultProps.editor = mockEditor;
49
69
  });
50
70
 
51
71
  it('renders without crashing', () => {
@@ -97,8 +117,7 @@ describe('ExplicitConstructedResponse', () => {
97
117
  const clickableDiv = container.querySelector('div[style*="border"]');
98
118
  fireEvent.click(clickableDiv);
99
119
  await waitFor(() => {
100
- // After click, toolbar should be shown
101
- expect(container).toBeInTheDocument();
120
+ expect(queryByTestId('toolbar')).toBeInTheDocument();
102
121
  });
103
122
  });
104
123
 
@@ -124,7 +143,7 @@ describe('ExplicitConstructedResponse', () => {
124
143
 
125
144
  it('calls respAreaToolbar with correct params', () => {
126
145
  render(<ExplicitConstructedResponse {...defaultProps} />);
127
- expect(mockOptions.respAreaToolbar).toHaveBeenCalledWith(mockNode, mockEditor, expect.any(Function));
146
+ expect(mockOptions.respAreaToolbar).toHaveBeenCalledWith([mockNode, 5], mockEditor, expect.any(Function));
128
147
  });
129
148
 
130
149
  it('has cursor pointer style', () => {
@@ -158,4 +177,28 @@ describe('ExplicitConstructedResponse', () => {
158
177
  const contentDiv = container.querySelector('div[style*="padding"]');
159
178
  expect(contentDiv).toHaveStyle({ minWidth: '178px' });
160
179
  });
180
+
181
+ it('renders delete control on portaled custom toolbar when container el is set', async () => {
182
+ const { findByLabelText } = render(<ExplicitConstructedResponse {...defaultProps} selected />);
183
+ expect(await findByLabelText('Delete')).toBeInTheDocument();
184
+ });
185
+
186
+ it('does not render portaled delete control when _tiptapContainerEl is missing', async () => {
187
+ const editor = buildMockEditor({ _tiptapContainerEl: undefined });
188
+ const { queryByLabelText, findByTestId } = render(
189
+ <ExplicitConstructedResponse {...defaultProps} editor={editor} selected />,
190
+ );
191
+ expect(await findByTestId('toolbar')).toBeInTheDocument();
192
+ expect(queryByLabelText('Delete')).not.toBeInTheDocument();
193
+ });
194
+
195
+ it('delete clears toolbar flag, removes node range, dispatches, and focuses', async () => {
196
+ mockEditor._toolbarOpened = true;
197
+ const { findByLabelText } = render(<ExplicitConstructedResponse {...defaultProps} selected />);
198
+ fireEvent.mouseDown(await findByLabelText('Delete'));
199
+ expect(mockEditor.state.tr.delete).toHaveBeenCalledWith(5, 6);
200
+ expect(mockEditor.view.dispatch).toHaveBeenCalledWith(mockEditor.state.tr);
201
+ expect(mockEditor._toolbarOpened).toBe(false);
202
+ expect(mockEditor.commands.focus).toHaveBeenCalled();
203
+ });
161
204
  });
@@ -16,18 +16,33 @@ jest.mock('react-dom', () => ({
16
16
  }));
17
17
 
18
18
  describe('InlineDropdown', () => {
19
- const mockEditor = {
20
- state: {
21
- selection: {
22
- from: 0,
23
- to: 1,
19
+ const buildMockEditor = (overrides = {}) => {
20
+ const mockTr = {
21
+ delete: jest.fn(),
22
+ };
23
+ return {
24
+ state: {
25
+ selection: {
26
+ from: 0,
27
+ to: 1,
28
+ },
29
+ tr: mockTr,
24
30
  },
25
- },
26
- view: {
27
- coordsAtPos: jest.fn(() => ({ top: 100, left: 50 })),
28
- },
31
+ view: {
32
+ coordsAtPos: jest.fn(() => ({ top: 100, left: 50 })),
33
+ dispatch: jest.fn(),
34
+ },
35
+ commands: {
36
+ focus: jest.fn(),
37
+ },
38
+ _tiptapContainerEl: document.createElement('div'),
39
+ _toolbarOpened: false,
40
+ ...overrides,
41
+ };
29
42
  };
30
43
 
44
+ let mockEditor;
45
+
31
46
  const mockNode = {
32
47
  attrs: {
33
48
  index: '0',
@@ -51,6 +66,8 @@ describe('InlineDropdown', () => {
51
66
 
52
67
  beforeEach(() => {
53
68
  jest.clearAllMocks();
69
+ mockEditor = buildMockEditor();
70
+ defaultProps.editor = mockEditor;
54
71
  Object.defineProperty(document.body, 'getBoundingClientRect', {
55
72
  value: jest.fn(() => ({ top: 0, left: 0 })),
56
73
  configurable: true,
@@ -73,6 +90,14 @@ describe('InlineDropdown', () => {
73
90
  expect(valueDiv).toBeInTheDocument();
74
91
  });
75
92
 
93
+ it('uses 2px horizontal margin on the value control and no horizontal margin on the wrapper', () => {
94
+ const { container, getByTestId } = render(<InlineDropdown {...defaultProps} />);
95
+ const valueDiv = container.querySelector('div[style*="border"]');
96
+ expect(valueDiv).toHaveStyle({ margin: '0 2px' });
97
+ const wrapper = getByTestId('node-view-wrapper');
98
+ expect(wrapper.getAttribute('style') || '').not.toMatch(/margin/);
99
+ });
100
+
76
101
  it('renders chevron icon', () => {
77
102
  const { container } = render(<InlineDropdown {...defaultProps} />);
78
103
  const chevron = container.querySelector('svg');
@@ -132,7 +157,7 @@ describe('InlineDropdown', () => {
132
157
 
133
158
  it('calls respAreaToolbar with correct params', () => {
134
159
  render(<InlineDropdown {...defaultProps} />);
135
- expect(mockOptions.respAreaToolbar).toHaveBeenCalledWith(mockNode, mockEditor, expect.any(Function));
160
+ expect(mockOptions.respAreaToolbar).toHaveBeenCalledWith([mockNode, 5], mockEditor, expect.any(Function));
136
161
  });
137
162
 
138
163
  it('closes toolbar on outside click', async () => {
@@ -184,4 +209,172 @@ describe('InlineDropdown', () => {
184
209
  }
185
210
  });
186
211
  });
212
+
213
+ it('passes editorCallback to InlineDropdownToolbar', async () => {
214
+ const mockToolbarComponent = jest.fn(({ editorCallback }) => {
215
+ editorCallback?.({ instanceId: 'test-instance' });
216
+ return <div data-testid="inline-dropdown-toolbar">Toolbar</div>;
217
+ });
218
+
219
+ const mockOptionsWithCallback = {
220
+ respAreaToolbar: jest.fn(() => mockToolbarComponent),
221
+ };
222
+
223
+ const { queryByTestId } = render(
224
+ <InlineDropdown {...defaultProps} options={mockOptionsWithCallback} selected={true} />,
225
+ );
226
+
227
+ await waitFor(() => {
228
+ expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
229
+ });
230
+ });
231
+
232
+ it('stores toolbar editor instance in ref when editorCallback is called', async () => {
233
+ let capturedCallback;
234
+ const mockToolbarComponent = ({ editorCallback }) => {
235
+ capturedCallback = editorCallback;
236
+ return <div data-testid="inline-dropdown-toolbar">Toolbar</div>;
237
+ };
238
+
239
+ const mockOptionsWithCallback = {
240
+ respAreaToolbar: jest.fn(() => mockToolbarComponent),
241
+ };
242
+
243
+ const { queryByTestId } = render(
244
+ <InlineDropdown {...defaultProps} options={mockOptionsWithCallback} selected={true} />,
245
+ );
246
+
247
+ await waitFor(() => {
248
+ expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
249
+ });
250
+
251
+ // Verify callback exists
252
+ expect(capturedCallback).toBeDefined();
253
+ });
254
+
255
+ it('handles click outside logic with data-toolbar-for attribute', async () => {
256
+ const editorWithInstanceId = {
257
+ ...mockEditor,
258
+ instanceId: 'editor-123',
259
+ _toolbarOpened: false,
260
+ };
261
+
262
+ // Mock the toolbar callback to set the toolbar editor instance
263
+ let capturedCallback;
264
+ const mockToolbarComponent = ({ editorCallback }) => {
265
+ React.useEffect(() => {
266
+ capturedCallback = editorCallback;
267
+ if (editorCallback) {
268
+ editorCallback({ instanceId: 'editor-123' });
269
+ }
270
+ }, [editorCallback]);
271
+ return <div data-testid="inline-dropdown-toolbar">Toolbar</div>;
272
+ };
273
+
274
+ const mockOptionsWithCallback = {
275
+ respAreaToolbar: jest.fn(() => mockToolbarComponent),
276
+ };
277
+
278
+ const { container, queryByTestId } = render(
279
+ <InlineDropdown
280
+ {...defaultProps}
281
+ editor={editorWithInstanceId}
282
+ options={mockOptionsWithCallback}
283
+ selected={true}
284
+ />,
285
+ );
286
+
287
+ await waitFor(() => {
288
+ expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
289
+ });
290
+
291
+ // Create an element with data-toolbar-for attribute
292
+ const otherToolbar = document.createElement('div');
293
+ otherToolbar.setAttribute('data-toolbar-for', 'editor-456');
294
+ document.body.appendChild(otherToolbar);
295
+
296
+ await waitFor(() => {
297
+ fireEvent.mouseDown(otherToolbar);
298
+ });
299
+
300
+ // Cleanup
301
+ document.body.removeChild(otherToolbar);
302
+ expect(container).toBeInTheDocument();
303
+ });
304
+
305
+ it('does not close when clicking inside same editor toolbar', async () => {
306
+ const editorWithInstanceId = {
307
+ ...mockEditor,
308
+ instanceId: 'editor-123',
309
+ _toolbarOpened: false,
310
+ };
311
+
312
+ let toolbarEditorInstance;
313
+ const mockToolbarComponent = ({ editorCallback }) => {
314
+ React.useEffect(() => {
315
+ if (editorCallback) {
316
+ editorCallback({ instanceId: 'editor-123' });
317
+ toolbarEditorInstance = { instanceId: 'editor-123' };
318
+ }
319
+ }, [editorCallback]);
320
+ return <div data-testid="inline-dropdown-toolbar">Toolbar</div>;
321
+ };
322
+
323
+ const mockOptionsWithCallback = {
324
+ respAreaToolbar: jest.fn(() => mockToolbarComponent),
325
+ };
326
+
327
+ render(
328
+ <InlineDropdown
329
+ {...defaultProps}
330
+ editor={editorWithInstanceId}
331
+ options={mockOptionsWithCallback}
332
+ selected={true}
333
+ />,
334
+ );
335
+
336
+ await waitFor(() => {
337
+ expect(mockOptionsWithCallback.respAreaToolbar).toHaveBeenCalled();
338
+ });
339
+ });
340
+
341
+ it('checks editor._toolbarOpened in click outside handler', async () => {
342
+ const editorWithToolbarOpened = buildMockEditor({ _toolbarOpened: true });
343
+
344
+ const { queryByTestId } = render(
345
+ <InlineDropdown {...defaultProps} editor={editorWithToolbarOpened} selected={true} />,
346
+ );
347
+
348
+ await waitFor(() => {
349
+ expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
350
+ });
351
+
352
+ // When _toolbarOpened is true, clicking outside should not close
353
+ fireEvent.mouseDown(document.body);
354
+
355
+ // Toolbar should still be visible
356
+ expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
357
+ });
358
+
359
+ it('renders delete control on portaled custom toolbar when container el is set', async () => {
360
+ const { findByLabelText } = render(<InlineDropdown {...defaultProps} selected />);
361
+ expect(await findByLabelText('Delete')).toBeInTheDocument();
362
+ });
363
+
364
+ it('does not render portaled delete control when _tiptapContainerEl is missing', async () => {
365
+ const editor = buildMockEditor({ _tiptapContainerEl: undefined });
366
+ const { queryByLabelText, findByTestId } = render(<InlineDropdown {...defaultProps} editor={editor} selected />);
367
+ expect(await findByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
368
+ expect(queryByLabelText('Delete')).not.toBeInTheDocument();
369
+ });
370
+
371
+ it('delete clears toolbar flag, removes node range, dispatches, and focuses', async () => {
372
+ mockEditor._toolbarOpened = true;
373
+ const { findByLabelText } = render(<InlineDropdown {...defaultProps} selected />);
374
+ fireEvent.mouseDown(await findByLabelText('Delete'));
375
+ expect(mockEditor.state.tr.delete).toHaveBeenCalledWith(5, 6);
376
+ expect(mockEditor.view.dispatch).toHaveBeenCalledWith(mockEditor.state.tr);
377
+ expect(mockEditor._toolbarOpened).toBe(false);
378
+ expect(mockEditor.commands.focus).toHaveBeenCalled();
379
+ });
187
380
  });
@@ -6,13 +6,14 @@ describe('InsertImageHandler', () => {
6
6
  nodeSize: 1,
7
7
  };
8
8
 
9
+ const NODE_POS = 5;
10
+
11
+ const nodeInfo = (node = mockNode, pos = NODE_POS) => [node, pos];
12
+
9
13
  const createMockEditor = () => ({
14
+ _insertingImage: true,
10
15
  state: {
11
- doc: {
12
- descendants: jest.fn((callback) => {
13
- callback(mockNode, 5);
14
- }),
15
- },
16
+ doc: {},
16
17
  tr: {
17
18
  setNodeMarkup: jest.fn((pos, type, attrs) => ({ setNodeMarkup: jest.fn() })),
18
19
  delete: jest.fn((from, to) => ({ delete: jest.fn() })),
@@ -31,36 +32,39 @@ describe('InsertImageHandler', () => {
31
32
 
32
33
  it('creates handler instance', () => {
33
34
  const editor = createMockEditor();
34
- const handler = new InsertImageHandler(editor, mockNode, mockOnFinish);
35
+ const info = nodeInfo();
36
+ const handler = new InsertImageHandler(editor, info, mockOnFinish);
35
37
  expect(handler).toBeDefined();
36
38
  expect(handler.editor).toBe(editor);
39
+ expect(handler.nodeInfo).toBe(info);
37
40
  expect(handler.node).toBe(mockNode);
38
41
  expect(handler.onFinish).toBe(mockOnFinish);
39
42
  });
40
43
 
41
- it('stores nodePos from descendants', () => {
44
+ it('stores nodePos from nodeInfo tuple', () => {
42
45
  const editor = createMockEditor();
43
- const handler = new InsertImageHandler(editor, mockNode, mockOnFinish);
44
- expect(handler.nodePos).toBe(5);
46
+ const handler = new InsertImageHandler(editor, nodeInfo(mockNode, 42), mockOnFinish);
47
+ expect(handler.nodePos).toBe(42);
45
48
  });
46
49
 
47
50
  it('stores isPasted parameter', () => {
48
51
  const editor = createMockEditor();
49
- const handler = new InsertImageHandler(editor, mockNode, mockOnFinish, true);
52
+ const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish, true);
50
53
  expect(handler.isPasted).toBe(true);
51
54
  });
52
55
 
53
56
  it('defaults isPasted to false', () => {
54
57
  const editor = createMockEditor();
55
- const handler = new InsertImageHandler(editor, mockNode, mockOnFinish);
58
+ const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
56
59
  expect(handler.isPasted).toBe(false);
57
60
  });
58
61
 
59
62
  it('cancel deletes node and calls onFinish', () => {
60
63
  const editor = createMockEditor();
61
- const handler = new InsertImageHandler(editor, mockNode, mockOnFinish);
64
+ const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
62
65
  handler.cancel();
63
66
  expect(mockOnFinish).toHaveBeenCalledWith(false);
67
+ expect(editor._insertingImage).toBe(false);
64
68
  });
65
69
 
66
70
  it('updateNode dispatches transaction with new attributes', () => {
@@ -68,22 +72,23 @@ describe('InsertImageHandler', () => {
68
72
  const mockNodeAt = jest.fn(() => ({ attrs: { existing: 'value' } }));
69
73
  editor.state.doc.nodeAt = mockNodeAt;
70
74
 
71
- const handler = new InsertImageHandler(editor, mockNode, mockOnFinish);
75
+ const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
72
76
  handler.updateNode({ newAttr: 'newValue' });
73
77
 
74
- expect(mockNodeAt).toHaveBeenCalledWith(5);
78
+ expect(mockNodeAt).toHaveBeenCalledWith(NODE_POS);
75
79
  expect(editor.view.dispatch).toHaveBeenCalled();
76
80
  });
77
81
 
78
82
  it('done calls onFinish with false on error', () => {
79
83
  const editor = createMockEditor();
80
- const handler = new InsertImageHandler(editor, mockNode, mockOnFinish);
84
+ const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
81
85
  const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
82
86
 
83
87
  handler.done('error', null);
84
88
 
85
89
  expect(consoleLogSpy).toHaveBeenCalledWith('error');
86
90
  expect(mockOnFinish).toHaveBeenCalledWith(false);
91
+ expect(editor._insertingImage).toBe(false);
87
92
  consoleLogSpy.mockRestore();
88
93
  });
89
94
 
@@ -92,16 +97,17 @@ describe('InsertImageHandler', () => {
92
97
  const mockNodeAt = jest.fn(() => ({ attrs: {} }));
93
98
  editor.state.doc.nodeAt = mockNodeAt;
94
99
 
95
- const handler = new InsertImageHandler(editor, mockNode, mockOnFinish);
100
+ const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
96
101
  handler.done(null, 'http://example.com/image.jpg');
97
102
 
98
103
  expect(editor.view.dispatch).toHaveBeenCalled();
99
104
  expect(mockOnFinish).toHaveBeenCalledWith(true);
105
+ expect(editor._insertingImage).toBe(false);
100
106
  });
101
107
 
102
108
  it('fileChosen returns early when no file provided', () => {
103
109
  const editor = createMockEditor();
104
- const handler = new InsertImageHandler(editor, mockNode, mockOnFinish);
110
+ const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
105
111
  handler.fileChosen(null);
106
112
  expect(editor.view.dispatch).not.toHaveBeenCalled();
107
113
  });
@@ -111,7 +117,7 @@ describe('InsertImageHandler', () => {
111
117
  const mockNodeAt = jest.fn(() => ({ attrs: {} }));
112
118
  editor.state.doc.nodeAt = mockNodeAt;
113
119
 
114
- const handler = new InsertImageHandler(editor, mockNode, mockOnFinish);
120
+ const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
115
121
  const mockFile = new File(['content'], 'test.jpg', { type: 'image/jpeg' });
116
122
 
117
123
  global.FileReader = jest.fn(function () {
@@ -124,6 +130,7 @@ describe('InsertImageHandler', () => {
124
130
  handler.fileChosen(mockFile);
125
131
 
126
132
  expect(handler.chosenFile).toBe(mockFile);
133
+ expect(editor._insertingImage).toBe(false);
127
134
  });
128
135
 
129
136
  it('progress updates node with percent', () => {
@@ -131,7 +138,7 @@ describe('InsertImageHandler', () => {
131
138
  const mockNodeAt = jest.fn(() => ({ attrs: {} }));
132
139
  editor.state.doc.nodeAt = mockNodeAt;
133
140
 
134
- const handler = new InsertImageHandler(editor, mockNode, mockOnFinish);
141
+ const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
135
142
  handler.progress(50, 500, 1000);
136
143
 
137
144
  expect(editor.view.dispatch).toHaveBeenCalled();
@@ -139,7 +146,7 @@ describe('InsertImageHandler', () => {
139
146
 
140
147
  it('getChosenFile returns the chosen file', () => {
141
148
  const editor = createMockEditor();
142
- const handler = new InsertImageHandler(editor, mockNode, mockOnFinish);
149
+ const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
143
150
  const mockFile = new File(['content'], 'test.jpg', { type: 'image/jpeg' });
144
151
  handler.chosenFile = mockFile;
145
152
 
@@ -148,7 +155,7 @@ describe('InsertImageHandler', () => {
148
155
 
149
156
  it('getChosenFile returns null initially', () => {
150
157
  const editor = createMockEditor();
151
- const handler = new InsertImageHandler(editor, mockNode, mockOnFinish);
158
+ const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
152
159
  expect(handler.getChosenFile()).toBeNull();
153
160
  });
154
161
  });
@@ -214,4 +214,36 @@ describe('StyledMenuBar', () => {
214
214
  toolbar?.dispatchEvent(event);
215
215
  expect(preventDefaultSpy).toHaveBeenCalled();
216
216
  });
217
+
218
+ it('calculates hasTextSelectionInTable correctly when selection is not empty in table', () => {
219
+ // This test verifies the hasTextSelectionInTable state computation
220
+ const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['table', 'bold', 'italic']} />);
221
+ expect(container).toBeInTheDocument();
222
+ });
223
+
224
+ it('hides table manipulation buttons when text is selected in table', () => {
225
+ // When hasTextSelectionInTable is true, table row/column buttons should be hidden
226
+ const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['table']} />);
227
+ // The component should render but table manipulation buttons should be conditional
228
+ expect(container).toBeInTheDocument();
229
+ });
230
+
231
+ it('shows table manipulation buttons when no text is selected in table', () => {
232
+ // When hasTextSelectionInTable is false, table row/column buttons should be visible
233
+ const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['table']} />);
234
+ expect(container).toBeInTheDocument();
235
+ });
236
+
237
+ it('shows text formatting buttons regardless of table state', () => {
238
+ // Bold, italic, etc. should always be visible when their plugin is active
239
+ const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['bold', 'italic', 'underline']} />);
240
+ const buttons = container.querySelectorAll('button');
241
+ expect(buttons.length).toBeGreaterThan(0);
242
+ });
243
+
244
+ it('does not hide text formatting buttons when in table', () => {
245
+ // Verify that the removal of "|| state.isTable" condition works correctly
246
+ const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['table', 'bold', 'italic']} />);
247
+ expect(container).toBeInTheDocument();
248
+ });
217
249
  });
@@ -60,7 +60,7 @@ export default ({ editor, onChange }) => {
60
60
  }
61
61
 
62
62
  const applyAlignment = (event) => {
63
- const alignType = event.target?.closest('div')?.getAttribute('value');
63
+ const alignType = event.currentTarget?.getAttribute('value');
64
64
 
65
65
  if (alignType) {
66
66
  editor.commands.setTextAlign(alignType);
@@ -11,20 +11,11 @@ const log = debug('@pie-lib:editable-html:image:insert-image-handler');
11
11
  * @param {Boolean} isPasted - a boolean that keeps track if the file is pasted
12
12
  */
13
13
  class InsertImageHandler {
14
- constructor(editor, node, onFinish, isPasted = false) {
14
+ constructor(editor, nodeInfo, onFinish, isPasted = false) {
15
15
  this.editor = editor;
16
- this.node = node;
17
-
18
- let nodePos;
19
-
20
- editor.state.doc.descendants((node, pos) => {
21
- if (node === this.node) {
22
- nodePos = pos;
23
- return false;
24
- }
25
- });
26
-
27
- this.nodePos = nodePos;
16
+ this.nodeInfo = nodeInfo;
17
+ this.node = nodeInfo[0];
18
+ this.nodePos = nodeInfo[1];
28
19
  this.onFinish = onFinish;
29
20
  this.isPasted = isPasted;
30
21
  this.chosenFile = null;
@@ -38,6 +29,8 @@ class InsertImageHandler {
38
29
  this.onFinish(false);
39
30
  } catch (err) {
40
31
  //
32
+ } finally {
33
+ this.editor._insertingImage = false;
41
34
  }
42
35
  }
43
36
 
@@ -72,6 +65,8 @@ class InsertImageHandler {
72
65
  this.updateNode({ loaded: true, src, percent: 100 });
73
66
  this.onFinish(true);
74
67
  }
68
+
69
+ this.editor._insertingImage = false;
75
70
  }
76
71
 
77
72
  /**
@@ -86,6 +81,7 @@ class InsertImageHandler {
86
81
 
87
82
  // Save the chosen file to this.chosenFile
88
83
  this.chosenFile = file;
84
+ this.editor._insertingImage = false;
89
85
 
90
86
  log('[fileChosen] file: ', file);
91
87
  const reader = new FileReader();
@@ -33,7 +33,11 @@ const DragDrop = (props) => {
33
33
 
34
34
  // console.log({nodeProps.children})
35
35
  return (
36
- <NodeViewWrapper className="drag-in-the-blank" data-selected={selected}>
36
+ <NodeViewWrapper
37
+ className="drag-in-the-blank"
38
+ data-selected={selected}
39
+ style={{ display: 'inline', whiteSpace: 'normal' }}
40
+ >
37
41
  <span
38
42
  {...attributes}
39
43
  style={{
@@ -52,6 +56,7 @@ const DragDrop = (props) => {
52
56
  pos={pos}
53
57
  value={attributes}
54
58
  duplicates={options.duplicates}
59
+ selected={selected}
55
60
  onChange={(choice) => onValueChange(editor, node, pos, choice)}
56
61
  removeResponse={(choice) => onRemoveResponse(editor, node, choice)}
57
62
  ></DragDropTile>