@pie-lib/editable-html-tip-tap 1.2.0-next.2 → 1.2.0-next.20

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 (126) hide show
  1. package/CHANGELOG.md +109 -21
  2. package/LICENSE.md +5 -0
  3. package/lib/components/CharacterPicker.js +2 -1
  4. package/lib/components/CharacterPicker.js.map +1 -1
  5. package/lib/components/EditableHtml.js +34 -17
  6. package/lib/components/EditableHtml.js.map +1 -1
  7. package/lib/components/MenuBar.js +77 -45
  8. package/lib/components/MenuBar.js.map +1 -1
  9. package/lib/components/TiptapContainer.js +15 -9
  10. package/lib/components/TiptapContainer.js.map +1 -1
  11. package/lib/components/characters/characterUtils.js +1 -1
  12. package/lib/components/characters/custom-popper.js +1 -1
  13. package/lib/components/common/done-button.js +1 -1
  14. package/lib/components/common/toolbar-buttons.js +1 -1
  15. package/lib/components/icons/CssIcon.js +1 -1
  16. package/lib/components/icons/RespArea.js +1 -1
  17. package/lib/components/icons/TableIcons.js +1 -1
  18. package/lib/components/icons/TextAlign.js +3 -3
  19. package/lib/components/icons/TextAlign.js.map +1 -1
  20. package/lib/components/image/AltDialog.js +1 -1
  21. package/lib/components/image/ImageToolbar.js +1 -1
  22. package/lib/components/image/InsertImageHandler.js +1 -1
  23. package/lib/components/media/MediaDialog.js +1 -1
  24. package/lib/components/media/MediaToolbar.js +1 -1
  25. package/lib/components/media/MediaWrapper.js +1 -1
  26. package/lib/components/respArea/DragInTheBlank/DragInTheBlank.js +7 -2
  27. package/lib/components/respArea/DragInTheBlank/DragInTheBlank.js.map +1 -1
  28. package/lib/components/respArea/DragInTheBlank/choice.js +16 -8
  29. package/lib/components/respArea/DragInTheBlank/choice.js.map +1 -1
  30. package/lib/components/respArea/ExplicitConstructedResponse.js +2 -2
  31. package/lib/components/respArea/ExplicitConstructedResponse.js.map +1 -1
  32. package/lib/components/respArea/InlineDropdown.js +11 -4
  33. package/lib/components/respArea/InlineDropdown.js.map +1 -1
  34. package/lib/components/respArea/MathTemplated.js +130 -0
  35. package/lib/components/respArea/MathTemplated.js.map +1 -0
  36. package/lib/components/respArea/ToolbarIcon.js +1 -1
  37. package/lib/constants.js +1 -1
  38. package/lib/extensions/css.js +1 -1
  39. package/lib/extensions/custom-toolbar-wrapper.js +1 -1
  40. package/lib/extensions/div-node.js +39 -0
  41. package/lib/extensions/div-node.js.map +1 -0
  42. package/lib/extensions/extended-table.js +1 -1
  43. package/lib/extensions/image-component.js +1 -1
  44. package/lib/extensions/image-component.js.map +1 -1
  45. package/lib/extensions/image.js +1 -1
  46. package/lib/extensions/index.js +4 -2
  47. package/lib/extensions/index.js.map +1 -1
  48. package/lib/extensions/math.js +2 -1
  49. package/lib/extensions/math.js.map +1 -1
  50. package/lib/extensions/media.js +15 -12
  51. package/lib/extensions/media.js.map +1 -1
  52. package/lib/extensions/responseArea.js +13 -10
  53. package/lib/extensions/responseArea.js.map +1 -1
  54. package/lib/index.js +1 -1
  55. package/lib/styles/editorContainerStyles.js +6 -5
  56. package/lib/styles/editorContainerStyles.js.map +1 -1
  57. package/lib/theme.js +1 -1
  58. package/lib/utils/helper.js +17 -0
  59. package/lib/utils/helper.js.map +1 -0
  60. package/lib/utils/size.js +1 -1
  61. package/package.json +11 -11
  62. package/src/__tests__/EditableHtml.test.jsx +41 -1
  63. package/src/__tests__/index.test.jsx +4 -1
  64. package/src/components/CharacterPicker.jsx +1 -0
  65. package/src/components/EditableHtml.jsx +40 -16
  66. package/src/components/MenuBar.jsx +66 -25
  67. package/src/components/TiptapContainer.jsx +10 -7
  68. package/src/components/__tests__/CharacterPicker.test.jsx +22 -0
  69. package/src/components/__tests__/ExplicitConstructedResponse.test.jsx +1 -1
  70. package/src/components/__tests__/InlineDropdown.test.jsx +150 -1
  71. package/src/components/__tests__/MenuBar.test.jsx +32 -0
  72. package/src/components/icons/TextAlign.jsx +1 -1
  73. package/src/components/respArea/DragInTheBlank/DragInTheBlank.jsx +6 -1
  74. package/src/components/respArea/DragInTheBlank/choice.jsx +32 -4
  75. package/src/components/respArea/ExplicitConstructedResponse.jsx +1 -1
  76. package/src/components/respArea/InlineDropdown.jsx +12 -2
  77. package/src/components/respArea/MathTemplated.jsx +124 -0
  78. package/src/components/respArea/__tests__/MathTemplated.test.jsx +210 -0
  79. package/src/extensions/__tests__/divNode.test.js +87 -0
  80. package/src/extensions/__tests__/math.test.js +327 -0
  81. package/src/extensions/__tests__/media-node-view.test.jsx +296 -0
  82. package/src/extensions/__tests__/media.test.js +1 -0
  83. package/src/extensions/__tests__/responseArea.test.js +157 -0
  84. package/src/extensions/div-node.js +36 -0
  85. package/src/extensions/index.js +2 -0
  86. package/src/extensions/math.js +1 -0
  87. package/src/extensions/media.js +17 -14
  88. package/src/extensions/responseArea.js +4 -8
  89. package/src/styles/editorContainerStyles.js +5 -4
  90. package/src/utils/helper.js +17 -0
  91. package/lib/__tests__/EditableHtml.test.js +0 -377
  92. package/lib/__tests__/constants.test.js +0 -21
  93. package/lib/__tests__/extensions.test.js +0 -209
  94. package/lib/__tests__/index.test.js +0 -235
  95. package/lib/__tests__/size-utils.test.js +0 -57
  96. package/lib/__tests__/theme.test.js +0 -17
  97. package/lib/components/__tests__/AltDialog.test.js +0 -201
  98. package/lib/components/__tests__/CharacterPicker.test.js +0 -305
  99. package/lib/components/__tests__/CssIcon.test.js +0 -58
  100. package/lib/components/__tests__/DragInTheBlank.test.js +0 -295
  101. package/lib/components/__tests__/ExplicitConstructedResponse.test.js +0 -253
  102. package/lib/components/__tests__/ImageToolbar.test.js +0 -185
  103. package/lib/components/__tests__/InlineDropdown.test.js +0 -287
  104. package/lib/components/__tests__/InsertImageHandler.test.js +0 -162
  105. package/lib/components/__tests__/MediaDialog.test.js +0 -433
  106. package/lib/components/__tests__/MediaToolbar.test.js +0 -126
  107. package/lib/components/__tests__/MediaWrapper.test.js +0 -96
  108. package/lib/components/__tests__/MenuBar.test.js +0 -459
  109. package/lib/components/__tests__/RespArea.test.js +0 -171
  110. package/lib/components/__tests__/TableIcons.test.js +0 -153
  111. package/lib/components/__tests__/TextAlign.test.js +0 -209
  112. package/lib/components/__tests__/TiptapContainer.test.js +0 -196
  113. package/lib/components/__tests__/characterUtils.test.js +0 -178
  114. package/lib/components/__tests__/choice.test.js +0 -213
  115. package/lib/components/__tests__/custom-popper.test.js +0 -108
  116. package/lib/components/__tests__/done-button.test.js +0 -72
  117. package/lib/components/__tests__/toolbar-buttons.test.js +0 -277
  118. package/lib/extensions/__tests__/component.test.js +0 -314
  119. package/lib/extensions/__tests__/css.test.js +0 -214
  120. package/lib/extensions/__tests__/custom-toolbar-wrapper.test.js +0 -175
  121. package/lib/extensions/__tests__/extended-table.test.js +0 -92
  122. package/lib/extensions/__tests__/image-component.test.js +0 -305
  123. package/lib/extensions/__tests__/image.test.js +0 -164
  124. package/lib/extensions/__tests__/media.test.js +0 -292
  125. package/lib/extensions/__tests__/responseArea.test.js +0 -330
  126. package/lib/extensions/component.js +0 -305
@@ -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
 
@@ -65,7 +67,15 @@ const HeadingIcon = () => (
65
67
  </svg>
66
68
  );
67
69
 
68
- function MenuBar({ editor, classes, activePlugins, toolbarOpts: toolOpts, responseAreaProps, onChange }) {
70
+ function MenuBar({
71
+ editor,
72
+ classes,
73
+ activePlugins,
74
+ toolbarOpts: toolOpts,
75
+ responseAreaProps,
76
+ onChange,
77
+ autoWidthToolbar,
78
+ }) {
69
79
  const [showPicker, setShowPicker] = useState(false);
70
80
  const toolbarOpts = toolOpts ?? {};
71
81
 
@@ -83,11 +93,15 @@ function MenuBar({ editor, classes, activePlugins, toolbarOpts: toolOpts, respon
83
93
  const hideDefaultToolbar =
84
94
  ctx.editor?.isActive('math') ||
85
95
  ctx.editor?.isActive('explicit_constructed_response') ||
86
- 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');
87
100
 
88
101
  return {
89
102
  currentNode,
90
103
  hideDefaultToolbar,
104
+ hasTextSelectionInTable,
91
105
  isFocused: ctx.editor?.isFocused,
92
106
  isBold: ctx.editor.isActive('bold') ?? false,
93
107
  canBold: ctx.editor.can().chain().toggleBold().run() ?? false,
@@ -122,7 +136,7 @@ function MenuBar({ editor, classes, activePlugins, toolbarOpts: toolOpts, respon
122
136
  });
123
137
 
124
138
  const hasDoneButton = false;
125
- const autoWidth = false;
139
+ const autoWidth = !!autoWidthToolbar;
126
140
 
127
141
  const names = classNames(classes.toolbar, PIE_TOOLBAR__CLASS, {
128
142
  [classes.toolbarWithNoDone]: !hasDoneButton,
@@ -152,35 +166,35 @@ function MenuBar({ editor, classes, activePlugins, toolbarOpts: toolOpts, respon
152
166
  {
153
167
  icon: <AddRow />,
154
168
  onClick: (editor) => editor.chain().focus().addRowAfter().run(),
155
- hidden: (state) => !state.isTable,
169
+ hidden: (state) => !(state.isTable && !state.hasTextSelectionInTable),
156
170
  isActive: (state) => state.isTable,
157
171
  isDisabled: (state) => !state.canTable,
158
172
  },
159
173
  {
160
174
  icon: <RemoveRow />,
161
175
  onClick: (editor) => editor.chain().focus().deleteRow().run(),
162
- hidden: (state) => !state.isTable,
176
+ hidden: (state) => !(state.isTable && !state.hasTextSelectionInTable),
163
177
  isActive: (state) => state.isTable,
164
178
  isDisabled: (state) => !state.canTable,
165
179
  },
166
180
  {
167
181
  icon: <AddColumn />,
168
182
  onClick: (editor) => editor.chain().focus().addColumnAfter().run(),
169
- hidden: (state) => !state.isTable,
183
+ hidden: (state) => !(state.isTable && !state.hasTextSelectionInTable),
170
184
  isActive: (state) => state.isTable,
171
185
  isDisabled: (state) => !state.canTable,
172
186
  },
173
187
  {
174
188
  icon: <RemoveColumn />,
175
189
  onClick: (editor) => editor.chain().focus().deleteColumn().run(),
176
- hidden: (state) => !state.isTable,
190
+ hidden: (state) => !(state.isTable && !state.hasTextSelectionInTable),
177
191
  isActive: (state) => state.isTable,
178
192
  isDisabled: (state) => !state.canTable,
179
193
  },
180
194
  {
181
195
  icon: <RemoveTable />,
182
196
  onClick: (editor) => editor.chain().focus().deleteTable().run(),
183
- hidden: (state) => !state.isTable,
197
+ hidden: (state) => !(state.isTable && !state.hasTextSelectionInTable),
184
198
  isActive: (state) => state.isTable,
185
199
  isDisabled: (state) => !state.canTable,
186
200
  },
@@ -196,54 +210,54 @@ function MenuBar({ editor, classes, activePlugins, toolbarOpts: toolOpts, respon
196
210
 
197
211
  editor.commands.updateAttributes('table', update);
198
212
  },
199
- hidden: (state) => !state.isTable,
213
+ hidden: (state) => !(state.isTable && !state.hasTextSelectionInTable),
200
214
  isActive: (state) => state.tableHasBorder,
201
215
  isDisabled: (state) => !state.canTable,
202
216
  },
203
217
  {
204
218
  icon: <Bold />,
205
219
  onClick: (editor) => editor.chain().focus().toggleBold().run(),
206
- hidden: (state) => !activePlugins?.includes('bold') || state.isTable,
220
+ hidden: () => !activePlugins?.includes('bold'),
207
221
  isActive: (state) => state.isBold,
208
222
  isDisabled: (state) => !state.canBold,
209
223
  },
210
224
  {
211
225
  icon: <Italic />,
212
226
  onClick: (editor) => editor.chain().focus().toggleItalic().run(),
213
- hidden: (state) => !activePlugins?.includes('italic') || state.isTable,
227
+ hidden: () => !activePlugins?.includes('italic'),
214
228
  isActive: (state) => state.isItalic,
215
229
  isDisabled: (state) => !state.canItalic,
216
230
  },
217
231
  {
218
232
  icon: <Strikethrough />,
219
233
  onClick: (editor) => editor.chain().focus().toggleStrike().run(),
220
- hidden: (state) => !activePlugins?.includes('strikethrough') || state.isTable,
234
+ hidden: () => !activePlugins?.includes('strikethrough'),
221
235
  isActive: (state) => state.isStrike,
222
236
  isDisabled: (state) => !state.canStrike,
223
237
  },
224
238
  {
225
239
  icon: <Code />,
226
240
  onClick: (editor) => editor.chain().focus().toggleCode().run(),
227
- hidden: (state) => !activePlugins?.includes('code') || state.isTable,
241
+ hidden: () => !activePlugins?.includes('code'),
228
242
  isActive: (state) => state.isCode,
229
243
  isDisabled: (state) => !state.canCode,
230
244
  },
231
245
  {
232
246
  icon: <Underline />,
233
247
  onClick: (editor) => editor.chain().focus().toggleUnderline().run(),
234
- hidden: (state) => !activePlugins?.includes('underline') || state.isTable,
248
+ hidden: () => !activePlugins?.includes('underline'),
235
249
  isActive: (state) => state.isUnderline,
236
250
  },
237
251
  {
238
252
  icon: <SubscriptIcon />,
239
253
  onClick: (editor) => editor.chain().focus().toggleSubscript().run(),
240
- hidden: (state) => !activePlugins?.includes('subscript') || state.isTable,
254
+ hidden: () => !activePlugins?.includes('subscript'),
241
255
  isActive: (state) => state.isSubScript,
242
256
  },
243
257
  {
244
258
  icon: <SuperscriptIcon />,
245
259
  onClick: (editor) => editor.chain().focus().toggleSuperscript().run(),
246
- hidden: (state) => !activePlugins?.includes('superscript') || state.isTable,
260
+ hidden: () => !activePlugins?.includes('superscript'),
247
261
  isActive: (state) => state.isSuperScript,
248
262
  },
249
263
  {
@@ -253,22 +267,28 @@ function MenuBar({ editor, classes, activePlugins, toolbarOpts: toolOpts, respon
253
267
  },
254
268
  {
255
269
  icon: <TheatersIcon />,
256
- hidden: (state) => !activePlugins?.includes('video') || state.isTable,
270
+ hidden: () => !activePlugins?.includes('video'),
257
271
  onClick: (editor) => editor.chain().focus().insertMedia({ type: 'video' }).run(),
258
272
  },
259
273
  {
260
274
  icon: <VolumeUpIcon />,
261
- hidden: (state) => !activePlugins?.includes('audio') || state.isTable,
275
+ hidden: () => !activePlugins?.includes('audio'),
262
276
  onClick: (editor) => editor.chain().focus().insertMedia({ type: 'audio', tag: 'audio' }).run(),
263
277
  },
264
278
  {
265
279
  icon: <CSSIcon />,
266
- hidden: (state) => !activePlugins?.includes('css') || state.isTable,
280
+ hidden: () => !activePlugins?.includes('css'),
267
281
  onClick: (editor) => editor.commands.openCSSClassDialog(),
268
282
  },
283
+ {
284
+ icon: <FormatQuote />,
285
+ hidden: () => !activePlugins?.includes('blockquote'),
286
+ onClick: (editor) => editor.chain().focus().toggleBlockquote().run(),
287
+ isActive: (state) => state.isBlockquote,
288
+ },
269
289
  {
270
290
  icon: <HeadingIcon />,
271
- hidden: (state) => !activePlugins?.includes('h3') || state.isTable,
291
+ hidden: () => !activePlugins?.includes('h3'),
272
292
  onClick: (editor) => editor.chain().focus().toggleHeading({ level: 3 }).run(),
273
293
  isActive: (state) => state.isHeading3,
274
294
  },
@@ -289,30 +309,30 @@ function MenuBar({ editor, classes, activePlugins, toolbarOpts: toolOpts, respon
289
309
  },
290
310
  {
291
311
  icon: <TextAlignIcon editor={editor} />,
292
- hidden: (state) => !activePlugins?.includes('text-align') || state.isTable,
312
+ hidden: () => !activePlugins?.includes('text-align'),
293
313
  onClick: () => {},
294
314
  },
295
315
  {
296
316
  icon: <BulletedListIcon />,
297
- hidden: (state) => !activePlugins?.includes('bulleted-list') || state.isTable,
317
+ hidden: () => !activePlugins?.includes('bulleted-list'),
298
318
  onClick: (editor) => editor.chain().focus().toggleBulletList().run(),
299
319
  isActive: (state) => state.isBulletList,
300
320
  },
301
321
  {
302
322
  icon: <NumberedListIcon />,
303
- hidden: (state) => !activePlugins?.includes('numbered-list') || state.isTable,
323
+ hidden: () => !activePlugins?.includes('numbered-list'),
304
324
  onClick: (editor) => editor.chain().focus().toggleOrderedList().run(),
305
325
  isActive: (state) => state.isOrderedList,
306
326
  },
307
327
  {
308
328
  icon: <Undo />,
309
- hidden: (state) => !activePlugins?.includes('undo') || state.isTable,
329
+ hidden: () => !activePlugins?.includes('undo'),
310
330
  onClick: (editor) => editor.chain().focus().undo().run(),
311
331
  isDisabled: (state) => !state.canUndo,
312
332
  },
313
333
  {
314
334
  icon: <Redo />,
315
- hidden: (state) => !activePlugins?.includes('redo') || state.isTable,
335
+ hidden: () => !activePlugins?.includes('redo'),
316
336
  onClick: (editor) => editor.chain().focus().redo().run(),
317
337
  isDisabled: (state) => !state.canRedo,
318
338
  },
@@ -320,8 +340,29 @@ function MenuBar({ editor, classes, activePlugins, toolbarOpts: toolOpts, respon
320
340
  [activePlugins, editor],
321
341
  );
322
342
 
343
+ const isDragInTheBlankSelected =
344
+ editorState.hideDefaultToolbar && editorState.currentNode?.type?.name === 'drag_in_the_blank';
345
+
323
346
  return (
324
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
+ )}
325
366
  {!editorState.hideDefaultToolbar && (
326
367
  <div className={classes.defaultToolbar} tabIndex="1">
327
368
  <div className={classes.buttonsContainer}>
@@ -73,9 +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',
76
+ background: '#f9f9f9',
77
+ borderLeft: '5px solid #ccc',
78
+ margin: '1.5em 10px',
79
+ padding: '.5em 10px',
79
80
  },
80
81
  '& hr': {
81
82
  border: 'none',
@@ -108,13 +109,13 @@ const StyledRoot = styled('div', {
108
109
  }));
109
110
 
110
111
  const StyledEditorHolder = styled('div', {
111
- shouldForwardProp: (prop) => prop !== 'disableScrollbar',
112
- })(({ disableScrollbar }) => ({
112
+ shouldForwardProp: (prop) => !['disableScrollbar', 'highlightShape'].includes(prop),
113
+ })(({ theme, disableScrollbar, highlightShape }) => ({
113
114
  position: 'relative',
114
115
  padding: '0px',
115
116
  overflowY: 'auto',
116
117
  color: color.text(),
117
- backgroundColor: color.background(),
118
+ backgroundColor: highlightShape ? theme.palette.action.selected : color.background(),
118
119
  ...(disableScrollbar && {
119
120
  '&::-webkit-scrollbar': {
120
121
  display: 'none',
@@ -148,6 +149,7 @@ function TiptapContainer(props) {
148
149
  minHeight,
149
150
  height,
150
151
  maxHeight,
152
+ highlightShape,
151
153
  ref,
152
154
  } = props;
153
155
 
@@ -195,7 +197,7 @@ function TiptapContainer(props) {
195
197
  style={{ width: sizeStyle.width, minWidth: sizeStyle.minWidth, maxWidth: sizeStyle.maxWidth }}
196
198
  ref={rootRef}
197
199
  >
198
- <StyledEditorHolder disableScrollbar={disableScrollbar}>
200
+ <StyledEditorHolder disableScrollbar={disableScrollbar} highlightShape={highlightShape}>
199
201
  <StyledChildren noPadding={toolbarOpts && toolbarOpts.noPadding}>{children}</StyledChildren>
200
202
  </StyledEditorHolder>
201
203
 
@@ -206,6 +208,7 @@ function TiptapContainer(props) {
206
208
  toolbarOpts={toolbarOpts}
207
209
  activePlugins={activePlugins}
208
210
  onChange={props.onChange}
211
+ autoWidthToolbar={props.autoWidthToolbar}
209
212
  />
210
213
  )}
211
214
  </StyledRoot>
@@ -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
  });
@@ -124,7 +124,7 @@ describe('ExplicitConstructedResponse', () => {
124
124
 
125
125
  it('calls respAreaToolbar with correct params', () => {
126
126
  render(<ExplicitConstructedResponse {...defaultProps} />);
127
- expect(mockOptions.respAreaToolbar).toHaveBeenCalledWith(mockNode, mockEditor, expect.any(Function));
127
+ expect(mockOptions.respAreaToolbar).toHaveBeenCalledWith([mockNode, 5], mockEditor, expect.any(Function));
128
128
  });
129
129
 
130
130
  it('has cursor pointer style', () => {
@@ -132,7 +132,7 @@ describe('InlineDropdown', () => {
132
132
 
133
133
  it('calls respAreaToolbar with correct params', () => {
134
134
  render(<InlineDropdown {...defaultProps} />);
135
- expect(mockOptions.respAreaToolbar).toHaveBeenCalledWith(mockNode, mockEditor, expect.any(Function));
135
+ expect(mockOptions.respAreaToolbar).toHaveBeenCalledWith([mockNode, 5], mockEditor, expect.any(Function));
136
136
  });
137
137
 
138
138
  it('closes toolbar on outside click', async () => {
@@ -184,4 +184,153 @@ describe('InlineDropdown', () => {
184
184
  }
185
185
  });
186
186
  });
187
+
188
+ it('passes editorCallback to InlineDropdownToolbar', async () => {
189
+ const mockToolbarComponent = jest.fn(({ editorCallback }) => {
190
+ editorCallback?.({ instanceId: 'test-instance' });
191
+ return <div data-testid="inline-dropdown-toolbar">Toolbar</div>;
192
+ });
193
+
194
+ const mockOptionsWithCallback = {
195
+ respAreaToolbar: jest.fn(() => mockToolbarComponent),
196
+ };
197
+
198
+ const { queryByTestId } = render(
199
+ <InlineDropdown {...defaultProps} options={mockOptionsWithCallback} selected={true} />,
200
+ );
201
+
202
+ await waitFor(() => {
203
+ expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
204
+ });
205
+ });
206
+
207
+ it('stores toolbar editor instance in ref when editorCallback is called', async () => {
208
+ let capturedCallback;
209
+ const mockToolbarComponent = ({ editorCallback }) => {
210
+ capturedCallback = editorCallback;
211
+ return <div data-testid="inline-dropdown-toolbar">Toolbar</div>;
212
+ };
213
+
214
+ const mockOptionsWithCallback = {
215
+ respAreaToolbar: jest.fn(() => mockToolbarComponent),
216
+ };
217
+
218
+ const { queryByTestId } = render(
219
+ <InlineDropdown {...defaultProps} options={mockOptionsWithCallback} selected={true} />,
220
+ );
221
+
222
+ await waitFor(() => {
223
+ expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
224
+ });
225
+
226
+ // Verify callback exists
227
+ expect(capturedCallback).toBeDefined();
228
+ });
229
+
230
+ it('handles click outside logic with data-toolbar-for attribute', async () => {
231
+ const editorWithInstanceId = {
232
+ ...mockEditor,
233
+ instanceId: 'editor-123',
234
+ _toolbarOpened: false,
235
+ };
236
+
237
+ // Mock the toolbar callback to set the toolbar editor instance
238
+ let capturedCallback;
239
+ const mockToolbarComponent = ({ editorCallback }) => {
240
+ React.useEffect(() => {
241
+ capturedCallback = editorCallback;
242
+ if (editorCallback) {
243
+ editorCallback({ instanceId: 'editor-123' });
244
+ }
245
+ }, [editorCallback]);
246
+ return <div data-testid="inline-dropdown-toolbar">Toolbar</div>;
247
+ };
248
+
249
+ const mockOptionsWithCallback = {
250
+ respAreaToolbar: jest.fn(() => mockToolbarComponent),
251
+ };
252
+
253
+ const { container, queryByTestId } = render(
254
+ <InlineDropdown
255
+ {...defaultProps}
256
+ editor={editorWithInstanceId}
257
+ options={mockOptionsWithCallback}
258
+ selected={true}
259
+ />,
260
+ );
261
+
262
+ await waitFor(() => {
263
+ expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
264
+ });
265
+
266
+ // Create an element with data-toolbar-for attribute
267
+ const otherToolbar = document.createElement('div');
268
+ otherToolbar.setAttribute('data-toolbar-for', 'editor-456');
269
+ document.body.appendChild(otherToolbar);
270
+
271
+ await waitFor(() => {
272
+ fireEvent.mouseDown(otherToolbar);
273
+ });
274
+
275
+ // Cleanup
276
+ document.body.removeChild(otherToolbar);
277
+ expect(container).toBeInTheDocument();
278
+ });
279
+
280
+ it('does not close when clicking inside same editor toolbar', async () => {
281
+ const editorWithInstanceId = {
282
+ ...mockEditor,
283
+ instanceId: 'editor-123',
284
+ _toolbarOpened: false,
285
+ };
286
+
287
+ let toolbarEditorInstance;
288
+ const mockToolbarComponent = ({ editorCallback }) => {
289
+ React.useEffect(() => {
290
+ if (editorCallback) {
291
+ editorCallback({ instanceId: 'editor-123' });
292
+ toolbarEditorInstance = { instanceId: 'editor-123' };
293
+ }
294
+ }, [editorCallback]);
295
+ return <div data-testid="inline-dropdown-toolbar">Toolbar</div>;
296
+ };
297
+
298
+ const mockOptionsWithCallback = {
299
+ respAreaToolbar: jest.fn(() => mockToolbarComponent),
300
+ };
301
+
302
+ render(
303
+ <InlineDropdown
304
+ {...defaultProps}
305
+ editor={editorWithInstanceId}
306
+ options={mockOptionsWithCallback}
307
+ selected={true}
308
+ />,
309
+ );
310
+
311
+ await waitFor(() => {
312
+ expect(mockOptionsWithCallback.respAreaToolbar).toHaveBeenCalled();
313
+ });
314
+ });
315
+
316
+ it('checks editor._toolbarOpened in click outside handler', async () => {
317
+ const editorWithToolbarOpened = {
318
+ ...mockEditor,
319
+ _toolbarOpened: true,
320
+ };
321
+
322
+ const { queryByTestId } = render(
323
+ <InlineDropdown {...defaultProps} editor={editorWithToolbarOpened} selected={true} />,
324
+ );
325
+
326
+ await waitFor(() => {
327
+ expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
328
+ });
329
+
330
+ // When _toolbarOpened is true, clicking outside should not close
331
+ fireEvent.mouseDown(document.body);
332
+
333
+ // Toolbar should still be visible
334
+ expect(queryByTestId('inline-dropdown-toolbar')).toBeInTheDocument();
335
+ });
187
336
  });
@@ -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);
@@ -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>
@@ -15,7 +15,7 @@ const StyledContent = styled('span')(({ theme }) => ({
15
15
  },
16
16
  }));
17
17
 
18
- export function BlankContent({ n, children, isDragging, isOver, dragItem, value }) {
18
+ export function BlankContent({ n, children, isDragging, isOver, dragItem, value, selected }) {
19
19
  const [hoveredElementSize, setHoveredElementSize] = useState(null);
20
20
  const elementRef = useRef(null);
21
21
 
@@ -56,15 +56,22 @@ export function BlankContent({ n, children, isDragging, isOver, dragItem, value
56
56
  const hasGrip = finalLabel !== '\u00A0';
57
57
  const isPreview = dragItem && isOver;
58
58
 
59
+ const borderStyle = selected
60
+ ? `2px solid ${color.primaryDark()}`
61
+ : isPreview
62
+ ? `1px solid ${color.defaults.BORDER_DARK}`
63
+ : `1px solid ${color.defaults.BORDER_LIGHT}`;
64
+
59
65
  return (
60
66
  <div
61
67
  ref={elementRef}
68
+ className={selected ? 'selected' : undefined}
62
69
  style={{
63
70
  display: 'inline-flex',
64
71
  minWidth: '178px',
65
72
  minHeight: '36px',
66
73
  background: isPreview ? `${color.defaults.BORDER_LIGHT}` : `${color.defaults.WHITE}`,
67
- border: isPreview ? `1px solid ${color.defaults.BORDER_DARK}` : `1px solid ${color.defaults.BORDER_LIGHT}`,
74
+ border: borderStyle,
68
75
  boxSizing: 'border-box',
69
76
  borderRadius: '3px',
70
77
  overflow: 'hidden',
@@ -72,6 +79,7 @@ export function BlankContent({ n, children, isDragging, isOver, dragItem, value
72
79
  padding: '8px 8px 8px 35px',
73
80
  width: hoveredElementSize ? hoveredElementSize.width : undefined,
74
81
  height: hoveredElementSize ? hoveredElementSize.height : undefined,
82
+ touchAction: 'none',
75
83
  }}
76
84
  data-key={n.index}
77
85
  contentEditable={false}
@@ -104,9 +112,21 @@ BlankContent.propTypes = {
104
112
  isOver: PropTypes.bool,
105
113
  dragItem: PropTypes.object,
106
114
  value: PropTypes.object,
115
+ selected: PropTypes.bool,
107
116
  };
108
117
 
109
- function DragDropChoice({ value, disabled, instanceId, children, n, onChange, removeResponse, duplicates, pos }) {
118
+ function DragDropChoice({
119
+ value,
120
+ disabled,
121
+ instanceId,
122
+ children,
123
+ n,
124
+ onChange,
125
+ removeResponse,
126
+ duplicates,
127
+ pos,
128
+ selected,
129
+ }) {
110
130
  const {
111
131
  attributes: dragAttributes,
112
132
  listeners: dragListeners,
@@ -196,7 +216,14 @@ function DragDropChoice({ value, disabled, instanceId, children, n, onChange, re
196
216
  };
197
217
 
198
218
  const dragContent = (
199
- <BlankContent n={n} isDragging={isDragging} isOver={isOver} dragItem={dragItem?.data?.current} value={value}>
219
+ <BlankContent
220
+ n={n}
221
+ isDragging={isDragging}
222
+ isOver={isOver}
223
+ dragItem={dragItem?.data?.current}
224
+ value={value}
225
+ selected={selected}
226
+ >
200
227
  {children}
201
228
  </BlankContent>
202
229
  );
@@ -223,6 +250,7 @@ DragDropChoice.propTypes = {
223
250
  onChange: PropTypes.func.isRequired,
224
251
  removeResponse: PropTypes.func.isRequired,
225
252
  duplicates: PropTypes.bool,
253
+ selected: PropTypes.bool,
226
254
  };
227
255
 
228
256
  export default DragDropChoice;
@@ -9,7 +9,7 @@ const ExplicitConstructedResponse = (props) => {
9
9
  const { respAreaToolbar, error: errorFn } = options;
10
10
  const pos = getPos();
11
11
  const [showToolbar, setShowToolbar] = useState(false);
12
- const EcrToolbar = respAreaToolbar(node, editor, () => {});
12
+ const EcrToolbar = respAreaToolbar([node, pos], editor, () => {});
13
13
  const toolbarRef = useRef(null);
14
14
 
15
15
  let error;