@pie-lib/editable-html-tip-tap 1.2.0-next.9 → 2.0.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 (94) hide show
  1. package/CHANGELOG.md +172 -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
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { render, waitFor } from '@testing-library/react';
3
+ import { useEditor } from '@tiptap/react';
3
4
  import { EditableHtml } from '../components/EditableHtml';
4
5
 
5
6
  // Mock TipTap dependencies
@@ -24,7 +25,9 @@ jest.mock('@tiptap/react', () => ({
24
25
 
25
26
  jest.mock('@tiptap/starter-kit', () => ({
26
27
  __esModule: true,
27
- default: {},
28
+ default: {
29
+ configure: jest.fn(() => ({})),
30
+ },
28
31
  }));
29
32
 
30
33
  jest.mock('@tiptap/extension-text-style', () => ({
@@ -68,12 +71,9 @@ jest.mock('@tiptap/extension-table-row', () => ({
68
71
  TableRow: {},
69
72
  }));
70
73
 
71
- jest.mock('@tiptap/extension-table-cell', () => ({
72
- TableCell: {},
73
- }));
74
-
75
- jest.mock('@tiptap/extension-table-header', () => ({
76
- TableHeader: {},
74
+ jest.mock('../extensions/extended-table-cell', () => ({
75
+ ExtendedTableCell: {},
76
+ ExtendedTableHeader: {},
77
77
  }));
78
78
 
79
79
  jest.mock('../extensions/extended-table', () => ({
@@ -81,6 +81,18 @@ jest.mock('../extensions/extended-table', () => ({
81
81
  default: {},
82
82
  }));
83
83
 
84
+ jest.mock('../extensions/ensure-empty-root-div', () => ({
85
+ EnsureEmptyRootIsDiv: {},
86
+ }));
87
+
88
+ jest.mock('../extensions/extended-list-item', () => ({
89
+ ExtendedListItem: {},
90
+ }));
91
+
92
+ jest.mock('../extensions/ensure-list-item-content-is-div', () => ({
93
+ EnsureListItemContentIsDiv: {},
94
+ }));
95
+
84
96
  jest.mock('../extensions/responseArea', () => ({
85
97
  ExplicitConstructedResponseNode: {
86
98
  configure: jest.fn(() => ({})),
@@ -266,4 +278,75 @@ describe('EditableHtml', () => {
266
278
  const { container } = render(<EditableHtml {...defaultProps} disableImageAlignmentButtons={true} />);
267
279
  expect(container).toBeInTheDocument();
268
280
  });
281
+
282
+ it('calls editorRef callback when editor is initialized', async () => {
283
+ const editorRef = jest.fn();
284
+ render(<EditableHtml {...defaultProps} editorRef={editorRef} />);
285
+
286
+ await waitFor(() => {
287
+ expect(editorRef).toHaveBeenCalled();
288
+ });
289
+ });
290
+
291
+ it('calls editorRef with the editor instance', async () => {
292
+ const editorRef = jest.fn();
293
+ render(<EditableHtml {...defaultProps} editorRef={editorRef} />);
294
+
295
+ await waitFor(() => {
296
+ expect(editorRef).toHaveBeenCalled();
297
+ // Verify it was called with an object that has editor-like properties
298
+ const callArg = editorRef.mock.calls[0][0];
299
+ expect(callArg).toHaveProperty('getHTML');
300
+ expect(callArg).toHaveProperty('commands');
301
+ });
302
+ });
303
+
304
+ it('handles editorRef being undefined', () => {
305
+ const { container } = render(<EditableHtml {...defaultProps} editorRef={undefined} />);
306
+ expect(container).toBeInTheDocument();
307
+ });
308
+
309
+ it('applies flex display to StyledEditorContent', async () => {
310
+ const { getByTestId } = render(<EditableHtml {...defaultProps} />);
311
+ await waitFor(() => {
312
+ const editorContent = getByTestId('editor-content');
313
+ expect(editorContent).toBeInTheDocument();
314
+ });
315
+ });
316
+
317
+ it('does not run blur onChange/onDone while an image insert flow is active', async () => {
318
+ jest.useFakeTimers();
319
+ const onChange = jest.fn();
320
+ const onDone = jest.fn();
321
+
322
+ render(
323
+ <EditableHtml
324
+ {...defaultProps}
325
+ markup="<p>Hello World</p>"
326
+ onChange={onChange}
327
+ onDone={onDone}
328
+ toolbarOpts={{ ...defaultProps.toolbarOpts, doneOn: 'blur' }}
329
+ />,
330
+ );
331
+
332
+ await waitFor(() => {
333
+ expect(useEditor).toHaveBeenCalled();
334
+ });
335
+
336
+ const editorConfig = useEditor.mock.calls[useEditor.mock.calls.length - 1][0];
337
+ const blurEditor = {
338
+ getHTML: jest.fn(() => '<p>changed</p>'),
339
+ _insertingImage: true,
340
+ _toolbarOpened: false,
341
+ isActive: jest.fn(() => false),
342
+ };
343
+
344
+ editorConfig.onBlur({ editor: blurEditor });
345
+ jest.advanceTimersByTime(200);
346
+
347
+ expect(onChange).not.toHaveBeenCalled();
348
+ expect(onDone).not.toHaveBeenCalled();
349
+
350
+ jest.useRealTimers();
351
+ });
269
352
  });
@@ -13,7 +13,10 @@ jest.mock('@tiptap/react', () => ({
13
13
  useEditorState: jest.fn(() => ({ isFocused: false })),
14
14
  }));
15
15
 
16
- jest.mock('@tiptap/starter-kit', () => ({ __esModule: true, default: {} }));
16
+ jest.mock('@tiptap/starter-kit', () => ({
17
+ __esModule: true,
18
+ default: { configure: jest.fn(() => ({})) },
19
+ }));
17
20
  jest.mock('@tiptap/extension-text-style', () => ({ TextStyleKit: {} }));
18
21
  jest.mock('@tiptap/extension-character-count', () => ({
19
22
  CharacterCount: { configure: jest.fn(() => ({})) },
@@ -27,9 +30,14 @@ jest.mock('@tiptap/extension-text-align', () => ({
27
30
  jest.mock('@tiptap/extension-image', () => ({ __esModule: true, default: {} }));
28
31
  jest.mock('@tiptap/extension-table', () => ({ __esModule: true, default: {} }));
29
32
  jest.mock('@tiptap/extension-table-row', () => ({ TableRow: {} }));
30
- jest.mock('@tiptap/extension-table-cell', () => ({ TableCell: {} }));
31
- jest.mock('@tiptap/extension-table-header', () => ({ TableHeader: {} }));
33
+ jest.mock('../extensions/extended-table-cell', () => ({
34
+ ExtendedTableCell: {},
35
+ ExtendedTableHeader: {},
36
+ }));
32
37
  jest.mock('../extensions/extended-table', () => ({ __esModule: true, default: {} }));
38
+ jest.mock('../extensions/ensure-empty-root-div', () => ({ EnsureEmptyRootIsDiv: {} }));
39
+ jest.mock('../extensions/extended-list-item', () => ({ ExtendedListItem: {} }));
40
+ jest.mock('../extensions/ensure-list-item-content-is-div', () => ({ EnsureListItemContentIsDiv: {} }));
33
41
  jest.mock('../extensions/responseArea', () => ({
34
42
  ExplicitConstructedResponseNode: { configure: jest.fn(() => ({})) },
35
43
  DragInTheBlankNode: { configure: jest.fn(() => ({})) },
@@ -121,6 +121,7 @@ export function CharacterPicker({ editor, opts, onClose }) {
121
121
  <div
122
122
  ref={containerRef}
123
123
  className="insert-character-dialog"
124
+ data-toolbar-for={editor.instanceId}
124
125
  style={{
125
126
  visibility: position.top === 0 && position.left === 0 ? 'hidden' : 'initial',
126
127
  position: 'absolute',
@@ -1,5 +1,7 @@
1
- import React, { useEffect, useMemo, useState } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import debounce from 'lodash-es/debounce';
2
3
  import { EditorContent, useEditor, useEditorState } from '@tiptap/react';
4
+ import { styled } from '@mui/material/styles';
3
5
  import StarterKit from '@tiptap/starter-kit';
4
6
  import { TextStyleKit } from '@tiptap/extension-text-style';
5
7
  import { CharacterCount } from '@tiptap/extension-character-count';
@@ -8,13 +10,14 @@ import SubScript from '@tiptap/extension-subscript';
8
10
  import TextAlign from '@tiptap/extension-text-align';
9
11
  import Image from '@tiptap/extension-image';
10
12
  import Placeholder from '@tiptap/extension-placeholder';
11
- import { styled } from '@mui/material/styles';
12
- import debounce from 'lodash-es/debounce';
13
+ import { normalizeInitialMarkup } from '../utils/helper';
13
14
 
14
15
  import ExtendedTable from '../extensions/extended-table';
16
+ import { ExtendedTableCell, ExtendedTableHeader } from '../extensions/extended-table-cell';
17
+ import { DivNode } from '../extensions/div-node';
18
+ import { EnsureEmptyRootIsDiv } from '../extensions/ensure-empty-root-div';
19
+ import { EnsureListItemContentIsDiv } from '../extensions/ensure-list-item-content-is-div';
15
20
  import { TableRow } from '@tiptap/extension-table-row';
16
- import { TableCell } from '@tiptap/extension-table-cell';
17
- import { TableHeader } from '@tiptap/extension-table-header';
18
21
  import {
19
22
  DragInTheBlankNode,
20
23
  ExplicitConstructedResponseNode,
@@ -26,6 +29,7 @@ import { MathNode } from '../extensions/math';
26
29
  import { ImageUploadNode } from '../extensions/image';
27
30
  import { Media } from '../extensions/media';
28
31
  import { CSSMark } from '../extensions/css';
32
+ import { ExtendedListItem } from '../extensions/extended-list-item';
29
33
 
30
34
  import EditorContainer from './TiptapContainer';
31
35
  import { valueToSize } from '../utils/size';
@@ -97,6 +101,19 @@ export const EditableHtml = (props) => {
97
101
  const [scheduled, setScheduled] = useState(false);
98
102
  const { toolbarOpts } = props;
99
103
 
104
+ const removePendingImage = useCallback(
105
+ (imagePos) => {
106
+ setPendingImages((prev) => {
107
+ const next = prev.filter((img) => img.pos !== imagePos);
108
+ if (next.length === 0) {
109
+ setScheduled(false);
110
+ }
111
+ return next;
112
+ });
113
+ },
114
+ [setPendingImages],
115
+ );
116
+
100
117
  const toolbarOptsToUse = {
101
118
  ...defaultToolbarOpts,
102
119
  ...toolbarOpts,
@@ -135,11 +152,24 @@ export const EditableHtml = (props) => {
135
152
  }, [props]);
136
153
 
137
154
  const extensions = [
155
+ TextAlign.configure({
156
+ types: ['heading', 'paragraph', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'td', 'th'],
157
+ alignments: ['left', 'right', 'center', 'justify'],
158
+ }),
138
159
  TextStyleKit,
139
160
  CharacterCount.configure({
140
161
  limit: props.charactersLimit || 1000000,
141
162
  }),
142
- StarterKit,
163
+ StarterKit.configure({
164
+ trailingNode: {
165
+ node: 'paragraph',
166
+ notAfter: ['paragraph', 'div'],
167
+ },
168
+ }),
169
+ ExtendedListItem,
170
+ DivNode,
171
+ EnsureEmptyRootIsDiv,
172
+ EnsureListItemContentIsDiv,
143
173
  Placeholder.configure({
144
174
  placeholder: props.placeholder,
145
175
  // show placeholder even when editor is focused
@@ -149,8 +179,8 @@ export const EditableHtml = (props) => {
149
179
  }),
150
180
  ExtendedTable,
151
181
  TableRow,
152
- TableHeader,
153
- TableCell,
182
+ ExtendedTableHeader,
183
+ ExtendedTableCell,
154
184
  ResponseAreaExtension.configure(props.responseAreaProps),
155
185
  ExplicitConstructedResponseNode.configure(props.responseAreaProps),
156
186
  DragInTheBlankNode.configure(props.responseAreaProps),
@@ -158,19 +188,16 @@ export const EditableHtml = (props) => {
158
188
  MathTemplatedNode.configure(props.responseAreaProps),
159
189
  MathNode.configure({
160
190
  toolbarOpts: toolbarOptsToUse,
191
+ math: props.pluginProps?.math || {},
161
192
  }),
162
193
  SubScript,
163
194
  SuperScript,
164
- TextAlign.configure({
165
- types: ['heading', 'paragraph'],
166
- alignments: ['left', 'right', 'center'],
167
- }),
168
195
  Image,
169
196
  ImageUploadNode.configure({
170
197
  toolbarOpts: toolbarOptsToUse,
171
198
  imageHandling: {
172
199
  disableImageAlignmentButtons: props.disableImageAlignmentButtons,
173
- onDone: () => props.onDone?.(editor.getHTML()),
200
+ onDone: (editor) => props.onDone?.(editor.getHTML()),
174
201
  onDelete:
175
202
  props.imageSupport &&
176
203
  props.imageSupport.delete &&
@@ -178,19 +205,14 @@ export const EditableHtml = (props) => {
178
205
  const { src } = node.attrs;
179
206
 
180
207
  props.imageSupport.delete(src, (e) => {
181
- const newPendingImages = pendingImages.filter((img) => img.key !== node.key);
182
- const newState = {
183
- pendingImages: newPendingImages,
184
- scheduled: scheduled && newPendingImages.length === 0 ? false : scheduled,
185
- };
186
-
187
- setPendingImages(newState.pendingImages);
188
- setScheduled(newState.scheduled);
208
+ removePendingImage(node.pos);
189
209
  });
190
210
  }),
191
211
  insertImageRequested:
192
212
  props.imageSupport &&
193
- ((addedImage, getHandler) => {
213
+ ((editor, imageInfo, getHandler) => {
214
+ const [addedImage, pos] = imageInfo;
215
+
194
216
  const onFinish = (result) => {
195
217
  let cb;
196
218
 
@@ -199,29 +221,39 @@ export const EditableHtml = (props) => {
199
221
  cb = props.onChange;
200
222
  }
201
223
 
202
- const newPendingImages = pendingImages.filter((img) => img.key !== addedImage.key);
203
- const newState = {
204
- pendingImages: newPendingImages,
205
- };
206
-
207
- if (newPendingImages.length === 0) {
208
- newState.scheduled = false;
209
- }
210
-
211
- setPendingImages(newState.pendingImages);
212
- setScheduled(newState.scheduled);
224
+ removePendingImage(pos);
213
225
  cb?.(editor.getHTML());
214
226
  };
227
+
215
228
  const callback = () => {
216
229
  /**
217
230
  * The handler is the object through which the outer context
218
231
  * communicates file upload events like: fileChosen, cancel, progress
219
232
  */
220
233
  const handler = getHandler(onFinish);
234
+
235
+ // If the user closes the file picker without choosing a file, the window regains
236
+ // focus while _insertingImage is still true — drop the stale pending entry.
237
+ const focusHandler = debounce(() => {
238
+ const detach = () => window.removeEventListener('focus', focusHandler);
239
+
240
+ if (!editor._insertingImage) {
241
+ detach();
242
+ return;
243
+ }
244
+
245
+ removePendingImage(pos);
246
+ editor._insertingImage = false;
247
+ detach();
248
+ }, 500);
249
+
250
+ window.addEventListener('focus', focusHandler);
251
+
221
252
  props.imageSupport.add(handler);
222
253
  };
223
254
 
224
- setPendingImages([...pendingImages, addedImage]);
255
+ editor._insertingImage = true;
256
+ setPendingImages((prev) => [...prev, addedImage]);
225
257
  callback();
226
258
  }),
227
259
  maxImageWidth: props.maxImageWidth,
@@ -252,7 +284,7 @@ export const EditableHtml = (props) => {
252
284
  },
253
285
  },
254
286
  editable: !props.disabled,
255
- content: props.markup,
287
+ content: normalizeInitialMarkup(props.markup),
256
288
  onUpdate: ({ editor, transaction }) => {
257
289
  if (transaction.isDone) {
258
290
  props.onChange?.(editor.getHTML());
@@ -260,6 +292,7 @@ export const EditableHtml = (props) => {
260
292
  },
261
293
  onBlur: debounce(({ editor }) => {
262
294
  const otherToolbarOpened =
295
+ editor._insertingImage ||
263
296
  editor._toolbarOpened ||
264
297
  editor.isActive('inline_dropdown') ||
265
298
  editor.isActive('explicit_constructed_response');
@@ -280,6 +313,12 @@ export const EditableHtml = (props) => {
280
313
  [props.charactersLimit],
281
314
  );
282
315
 
316
+ useEffect(() => {
317
+ if (props.editorRef) {
318
+ props.editorRef(editor);
319
+ }
320
+ }, [props.editorRef, editor]);
321
+
283
322
  useEffect(() => {
284
323
  editor?.setEditable(!props.disabled);
285
324
  }, [props.disabled, editor]);
@@ -288,9 +327,10 @@ export const EditableHtml = (props) => {
288
327
  if (!editor) {
289
328
  return;
290
329
  }
330
+ const nextMarkup = normalizeInitialMarkup(props.markup);
291
331
 
292
- if (props.markup !== editor.getHTML()) {
293
- editor.commands.setContent(props.markup, false); // false = don’t emit update
332
+ if (nextMarkup !== editor.getHTML()) {
333
+ editor.commands.setContent(nextMarkup, false);
294
334
  }
295
335
  }, [props.markup, editor]);
296
336
 
@@ -349,26 +389,36 @@ export const EditableHtml = (props) => {
349
389
  const StyledEditorContent = styled(EditorContent, {
350
390
  shouldForwardProp: (prop) => !['showParagraph', 'separateParagraph'].includes(prop),
351
391
  })(({ showParagraph, separateParagraph }) => ({
392
+ display: 'flex',
352
393
  outline: 'none !important',
353
394
  '& .ProseMirror': {
395
+ flex: 1,
354
396
  padding: '5px',
355
397
  maxHeight: '500px',
356
398
  outline: 'none !important',
357
399
  position: 'initial',
358
- '& > p': {
400
+
401
+ // reset default margins for all block paragraphs/divs in the editor
402
+ '& > p, & > div': {
359
403
  margin: '0',
360
404
  },
361
405
 
362
- '& p.is-editor-empty:first-child::before': {
406
+ // Out of flow so the caret stays at the start of the block; in-flow ::before pushes the caret after the hint text.
407
+ '& p.is-editor-empty, & div.is-editor-empty': {
408
+ position: 'relative',
409
+ },
410
+ '& p.is-editor-empty::before, & div.is-editor-empty::before': {
363
411
  content: 'attr(data-placeholder)',
364
- display: 'block',
412
+ position: 'absolute',
413
+ left: 0,
414
+ top: 0,
365
415
  color: '#9CA3AF',
366
416
  pointerEvents: 'none',
367
417
  whiteSpace: 'pre-wrap',
368
418
  },
369
419
 
370
420
  ...(showParagraph && {
371
- '& > p:has(+ p)::after': {
421
+ '& > p:has(+ p)::after, & > div:has(+ div)::after': {
372
422
  display: 'block',
373
423
  content: '"¶"',
374
424
  fontSize: '1em',
@@ -15,9 +15,11 @@ import Functions from '@mui/icons-material/Functions';
15
15
  import ImageIcon from '@mui/icons-material/Image';
16
16
  import Redo from '@mui/icons-material/Redo';
17
17
  import Undo from '@mui/icons-material/Undo';
18
+ import FormatQuote from '@mui/icons-material/FormatQuote';
18
19
  import TheatersIcon from '@mui/icons-material/Theaters';
19
20
  import VolumeUpIcon from '@mui/icons-material/VolumeUp';
20
21
  import BorderAll from '@mui/icons-material/BorderAll';
22
+ import Delete from '@mui/icons-material/Delete';
21
23
 
22
24
  import { useEditorState } from '@tiptap/react';
23
25
 
@@ -91,11 +93,15 @@ function MenuBar({
91
93
  const hideDefaultToolbar =
92
94
  ctx.editor?.isActive('math') ||
93
95
  ctx.editor?.isActive('explicit_constructed_response') ||
94
- ctx.editor?.isActive('imageUploadNode');
96
+ ctx.editor?.isActive('imageUploadNode') ||
97
+ ctx.editor?.isActive('drag_in_the_blank');
98
+
99
+ const hasTextSelectionInTable = selection && selection.empty === false && ctx.editor.isActive('table');
95
100
 
96
101
  return {
97
102
  currentNode,
98
103
  hideDefaultToolbar,
104
+ hasTextSelectionInTable,
99
105
  isFocused: ctx.editor?.isFocused,
100
106
  isBold: ctx.editor.isActive('bold') ?? false,
101
107
  canBold: ctx.editor.can().chain().toggleBold().run() ?? false,
@@ -136,7 +142,7 @@ function MenuBar({
136
142
  [classes.toolbarWithNoDone]: !hasDoneButton,
137
143
  [classes.toolbarTop]: toolbarOpts.position === 'top',
138
144
  [classes.toolbarRight]: toolbarOpts.alignment === 'right',
139
- [classes.focused]: toolbarOpts.alwaysVisible || (editorState.isFocused && !editor._toolbarOpened),
145
+ [classes.focused]: toolbarOpts.alwaysVisible || (editorState.isFocused && !editor._toolbarOpened && !editorState.hideDefaultToolbar),
140
146
  [classes.autoWidth]: autoWidth,
141
147
  [classes.fullWidth]: !autoWidth,
142
148
  [classes.hidden]: toolbarOpts.isHidden === true,
@@ -160,35 +166,35 @@ function MenuBar({
160
166
  {
161
167
  icon: <AddRow />,
162
168
  onClick: (editor) => editor.chain().focus().addRowAfter().run(),
163
- hidden: (state) => !state.isTable,
169
+ hidden: (state) => !(state.isTable && !state.hasTextSelectionInTable),
164
170
  isActive: (state) => state.isTable,
165
171
  isDisabled: (state) => !state.canTable,
166
172
  },
167
173
  {
168
174
  icon: <RemoveRow />,
169
175
  onClick: (editor) => editor.chain().focus().deleteRow().run(),
170
- hidden: (state) => !state.isTable,
176
+ hidden: (state) => !(state.isTable && !state.hasTextSelectionInTable),
171
177
  isActive: (state) => state.isTable,
172
178
  isDisabled: (state) => !state.canTable,
173
179
  },
174
180
  {
175
181
  icon: <AddColumn />,
176
182
  onClick: (editor) => editor.chain().focus().addColumnAfter().run(),
177
- hidden: (state) => !state.isTable,
183
+ hidden: (state) => !(state.isTable && !state.hasTextSelectionInTable),
178
184
  isActive: (state) => state.isTable,
179
185
  isDisabled: (state) => !state.canTable,
180
186
  },
181
187
  {
182
188
  icon: <RemoveColumn />,
183
189
  onClick: (editor) => editor.chain().focus().deleteColumn().run(),
184
- hidden: (state) => !state.isTable,
190
+ hidden: (state) => !(state.isTable && !state.hasTextSelectionInTable),
185
191
  isActive: (state) => state.isTable,
186
192
  isDisabled: (state) => !state.canTable,
187
193
  },
188
194
  {
189
195
  icon: <RemoveTable />,
190
196
  onClick: (editor) => editor.chain().focus().deleteTable().run(),
191
- hidden: (state) => !state.isTable,
197
+ hidden: (state) => !(state.isTable && !state.hasTextSelectionInTable),
192
198
  isActive: (state) => state.isTable,
193
199
  isDisabled: (state) => !state.canTable,
194
200
  },
@@ -204,54 +210,54 @@ function MenuBar({
204
210
 
205
211
  editor.commands.updateAttributes('table', update);
206
212
  },
207
- hidden: (state) => !state.isTable,
213
+ hidden: (state) => !(state.isTable && !state.hasTextSelectionInTable),
208
214
  isActive: (state) => state.tableHasBorder,
209
215
  isDisabled: (state) => !state.canTable,
210
216
  },
211
217
  {
212
218
  icon: <Bold />,
213
219
  onClick: (editor) => editor.chain().focus().toggleBold().run(),
214
- hidden: (state) => !activePlugins?.includes('bold') || state.isTable,
220
+ hidden: () => !activePlugins?.includes('bold'),
215
221
  isActive: (state) => state.isBold,
216
222
  isDisabled: (state) => !state.canBold,
217
223
  },
218
224
  {
219
225
  icon: <Italic />,
220
226
  onClick: (editor) => editor.chain().focus().toggleItalic().run(),
221
- hidden: (state) => !activePlugins?.includes('italic') || state.isTable,
227
+ hidden: () => !activePlugins?.includes('italic'),
222
228
  isActive: (state) => state.isItalic,
223
229
  isDisabled: (state) => !state.canItalic,
224
230
  },
225
231
  {
226
232
  icon: <Strikethrough />,
227
233
  onClick: (editor) => editor.chain().focus().toggleStrike().run(),
228
- hidden: (state) => !activePlugins?.includes('strikethrough') || state.isTable,
234
+ hidden: () => !activePlugins?.includes('strikethrough'),
229
235
  isActive: (state) => state.isStrike,
230
236
  isDisabled: (state) => !state.canStrike,
231
237
  },
232
238
  {
233
239
  icon: <Code />,
234
240
  onClick: (editor) => editor.chain().focus().toggleCode().run(),
235
- hidden: (state) => !activePlugins?.includes('code') || state.isTable,
241
+ hidden: () => !activePlugins?.includes('code'),
236
242
  isActive: (state) => state.isCode,
237
243
  isDisabled: (state) => !state.canCode,
238
244
  },
239
245
  {
240
246
  icon: <Underline />,
241
247
  onClick: (editor) => editor.chain().focus().toggleUnderline().run(),
242
- hidden: (state) => !activePlugins?.includes('underline') || state.isTable,
248
+ hidden: () => !activePlugins?.includes('underline'),
243
249
  isActive: (state) => state.isUnderline,
244
250
  },
245
251
  {
246
252
  icon: <SubscriptIcon />,
247
253
  onClick: (editor) => editor.chain().focus().toggleSubscript().run(),
248
- hidden: (state) => !activePlugins?.includes('subscript') || state.isTable,
254
+ hidden: () => !activePlugins?.includes('subscript'),
249
255
  isActive: (state) => state.isSubScript,
250
256
  },
251
257
  {
252
258
  icon: <SuperscriptIcon />,
253
259
  onClick: (editor) => editor.chain().focus().toggleSuperscript().run(),
254
- hidden: (state) => !activePlugins?.includes('superscript') || state.isTable,
260
+ hidden: () => !activePlugins?.includes('superscript'),
255
261
  isActive: (state) => state.isSuperScript,
256
262
  },
257
263
  {
@@ -261,22 +267,28 @@ function MenuBar({
261
267
  },
262
268
  {
263
269
  icon: <TheatersIcon />,
264
- hidden: (state) => !activePlugins?.includes('video') || state.isTable,
270
+ hidden: () => !activePlugins?.includes('video'),
265
271
  onClick: (editor) => editor.chain().focus().insertMedia({ type: 'video' }).run(),
266
272
  },
267
273
  {
268
274
  icon: <VolumeUpIcon />,
269
- hidden: (state) => !activePlugins?.includes('audio') || state.isTable,
275
+ hidden: () => !activePlugins?.includes('audio'),
270
276
  onClick: (editor) => editor.chain().focus().insertMedia({ type: 'audio', tag: 'audio' }).run(),
271
277
  },
272
278
  {
273
279
  icon: <CSSIcon />,
274
- hidden: (state) => !activePlugins?.includes('css') || state.isTable,
280
+ hidden: () => !activePlugins?.includes('css'),
275
281
  onClick: (editor) => editor.commands.openCSSClassDialog(),
276
282
  },
283
+ {
284
+ icon: <FormatQuote />,
285
+ hidden: () => !activePlugins?.includes('blockquote'),
286
+ onClick: (editor) => editor.chain().focus().toggleBlockquote().run(),
287
+ isActive: (state) => state.isBlockquote,
288
+ },
277
289
  {
278
290
  icon: <HeadingIcon />,
279
- hidden: (state) => !activePlugins?.includes('h3') || state.isTable,
291
+ hidden: () => !activePlugins?.includes('h3'),
280
292
  onClick: (editor) => editor.chain().focus().toggleHeading({ level: 3 }).run(),
281
293
  isActive: (state) => state.isHeading3,
282
294
  },
@@ -297,30 +309,30 @@ function MenuBar({
297
309
  },
298
310
  {
299
311
  icon: <TextAlignIcon editor={editor} />,
300
- hidden: (state) => !activePlugins?.includes('text-align') || state.isTable,
312
+ hidden: () => !activePlugins?.includes('text-align'),
301
313
  onClick: () => {},
302
314
  },
303
315
  {
304
316
  icon: <BulletedListIcon />,
305
- hidden: (state) => !activePlugins?.includes('bulleted-list') || state.isTable,
317
+ hidden: () => !activePlugins?.includes('bulleted-list'),
306
318
  onClick: (editor) => editor.chain().focus().toggleBulletList().run(),
307
319
  isActive: (state) => state.isBulletList,
308
320
  },
309
321
  {
310
322
  icon: <NumberedListIcon />,
311
- hidden: (state) => !activePlugins?.includes('numbered-list') || state.isTable,
323
+ hidden: () => !activePlugins?.includes('numbered-list'),
312
324
  onClick: (editor) => editor.chain().focus().toggleOrderedList().run(),
313
325
  isActive: (state) => state.isOrderedList,
314
326
  },
315
327
  {
316
328
  icon: <Undo />,
317
- hidden: (state) => !activePlugins?.includes('undo') || state.isTable,
329
+ hidden: () => !activePlugins?.includes('undo'),
318
330
  onClick: (editor) => editor.chain().focus().undo().run(),
319
331
  isDisabled: (state) => !state.canUndo,
320
332
  },
321
333
  {
322
334
  icon: <Redo />,
323
- hidden: (state) => !activePlugins?.includes('redo') || state.isTable,
335
+ hidden: () => !activePlugins?.includes('redo'),
324
336
  onClick: (editor) => editor.chain().focus().redo().run(),
325
337
  isDisabled: (state) => !state.canRedo,
326
338
  },
@@ -328,8 +340,29 @@ function MenuBar({
328
340
  [activePlugins, editor],
329
341
  );
330
342
 
343
+ const isDragInTheBlankSelected =
344
+ editorState.hideDefaultToolbar && editorState.currentNode?.type?.name === 'drag_in_the_blank';
345
+
331
346
  return (
332
347
  <div className={names} style={{ ...customStyles }} onMouseDown={handleMouseDown}>
348
+ {isDragInTheBlankSelected && (
349
+ <div className={classes.defaultToolbar} tabIndex="1">
350
+ <div className={classes.buttonsContainer}>
351
+ <button
352
+ type="button"
353
+ className={classes.button}
354
+ onClick={(e) => {
355
+ e.preventDefault();
356
+ editor.chain().focus().deleteSelection().run();
357
+ onChange?.(editor.getHTML());
358
+ }}
359
+ aria-label="Delete response area"
360
+ >
361
+ <Delete />
362
+ </button>
363
+ </div>
364
+ </div>
365
+ )}
333
366
  {!editorState.hideDefaultToolbar && (
334
367
  <div className={classes.defaultToolbar} tabIndex="1">
335
368
  <div className={classes.buttonsContainer}>