@pie-lib/editable-html-tip-tap 1.2.0-next.8 → 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.
- package/CHANGELOG.md +178 -0
- package/lib/components/CharacterPicker.js +1 -0
- package/lib/components/CharacterPicker.js.map +1 -1
- package/lib/components/EditableHtml.js +84 -43
- package/lib/components/EditableHtml.js.map +1 -1
- package/lib/components/MenuBar.js +74 -43
- package/lib/components/MenuBar.js.map +1 -1
- package/lib/components/TiptapContainer.js +9 -8
- package/lib/components/TiptapContainer.js.map +1 -1
- package/lib/components/icons/TextAlign.js +2 -2
- package/lib/components/icons/TextAlign.js.map +1 -1
- package/lib/components/image/InsertImageHandler.js +10 -13
- package/lib/components/image/InsertImageHandler.js.map +1 -1
- package/lib/components/media/MediaDialog.js.map +1 -1
- package/lib/components/respArea/DragInTheBlank/DragInTheBlank.js +6 -1
- package/lib/components/respArea/DragInTheBlank/DragInTheBlank.js.map +1 -1
- package/lib/components/respArea/DragInTheBlank/choice.js +15 -7
- package/lib/components/respArea/DragInTheBlank/choice.js.map +1 -1
- package/lib/components/respArea/ExplicitConstructedResponse.js +29 -11
- package/lib/components/respArea/ExplicitConstructedResponse.js.map +1 -1
- package/lib/components/respArea/InlineDropdown.js +35 -6
- package/lib/components/respArea/InlineDropdown.js.map +1 -1
- package/lib/components/respArea/MathTemplated.js +130 -0
- package/lib/components/respArea/MathTemplated.js.map +1 -0
- package/lib/extensions/custom-toolbar-wrapper.js +3 -2
- package/lib/extensions/custom-toolbar-wrapper.js.map +1 -1
- package/lib/extensions/div-node.js +83 -0
- package/lib/extensions/div-node.js.map +1 -0
- package/lib/extensions/ensure-empty-root-div.js +48 -0
- package/lib/extensions/ensure-empty-root-div.js.map +1 -0
- package/lib/extensions/ensure-list-item-content-is-div.js +64 -0
- package/lib/extensions/ensure-list-item-content-is-div.js.map +1 -0
- package/lib/extensions/extended-list-item.js +15 -0
- package/lib/extensions/extended-list-item.js.map +1 -0
- package/lib/extensions/extended-table-cell.js +22 -0
- package/lib/extensions/extended-table-cell.js.map +1 -0
- package/lib/extensions/extended-table.js +50 -1
- package/lib/extensions/extended-table.js.map +1 -1
- package/lib/extensions/image-component.js +102 -51
- package/lib/extensions/image-component.js.map +1 -1
- package/lib/extensions/image.js +51 -2
- package/lib/extensions/image.js.map +1 -1
- package/lib/extensions/math.js +50 -9
- package/lib/extensions/math.js.map +1 -1
- package/lib/extensions/media.js +3 -1
- package/lib/extensions/media.js.map +1 -1
- package/lib/extensions/responseArea.js +22 -13
- package/lib/extensions/responseArea.js.map +1 -1
- package/lib/styles/editorContainerStyles.js +5 -4
- package/lib/styles/editorContainerStyles.js.map +1 -1
- package/lib/utils/helper.js +17 -0
- package/lib/utils/helper.js.map +1 -0
- package/package.json +8 -8
- package/src/__tests__/EditableHtml.test.jsx +93 -7
- package/src/__tests__/index.test.jsx +11 -3
- package/src/components/CharacterPicker.jsx +1 -0
- package/src/components/EditableHtml.jsx +93 -41
- package/src/components/MenuBar.jsx +57 -24
- package/src/components/TiptapContainer.jsx +10 -8
- package/src/components/__tests__/CharacterPicker.test.jsx +22 -0
- package/src/components/__tests__/ExplicitConstructedResponse.test.jsx +55 -12
- package/src/components/__tests__/InlineDropdown.test.jsx +203 -10
- package/src/components/__tests__/InsertImageHandler.test.js +28 -21
- package/src/components/__tests__/MenuBar.test.jsx +32 -0
- package/src/components/icons/TextAlign.jsx +1 -1
- package/src/components/image/InsertImageHandler.js +9 -13
- package/src/components/respArea/DragInTheBlank/DragInTheBlank.jsx +6 -1
- package/src/components/respArea/DragInTheBlank/choice.jsx +32 -4
- package/src/components/respArea/ExplicitConstructedResponse.jsx +33 -10
- package/src/components/respArea/InlineDropdown.jsx +45 -10
- package/src/components/respArea/MathTemplated.jsx +124 -0
- package/src/components/respArea/__tests__/MathTemplated.test.jsx +210 -0
- package/src/extensions/__tests__/divNode.test.js +87 -0
- package/src/extensions/__tests__/ensure-empty-root-div.test.js +57 -0
- package/src/extensions/__tests__/ensure-list-item-content-is-div.test.js +44 -0
- package/src/extensions/__tests__/extended-list-item.test.js +13 -0
- package/src/extensions/__tests__/extended-table-cell.test.js +22 -0
- package/src/extensions/__tests__/extended-table.test.js +98 -1
- package/src/extensions/__tests__/image-component.test.jsx +105 -9
- package/src/extensions/__tests__/image.test.js +109 -8
- package/src/extensions/__tests__/math.test.js +348 -0
- package/src/extensions/__tests__/media-node-view.test.jsx +10 -8
- package/src/extensions/__tests__/responseArea.test.js +291 -0
- package/src/extensions/custom-toolbar-wrapper.jsx +2 -2
- package/src/extensions/div-node.js +86 -0
- package/src/extensions/ensure-empty-root-div.js +47 -0
- package/src/extensions/ensure-list-item-content-is-div.js +62 -0
- package/src/extensions/extended-list-item.js +10 -0
- package/src/extensions/extended-table-cell.js +19 -0
- package/src/extensions/extended-table.js +37 -1
- package/src/extensions/image-component.jsx +114 -69
- package/src/extensions/image.js +56 -1
- package/src/extensions/math.js +62 -10
- package/src/extensions/media.js +1 -1
- package/src/extensions/responseArea.js +15 -12
- package/src/styles/editorContainerStyles.js +5 -4
- package/src/utils/helper.js +17 -0
- /package/src/components/media/{MediaDialog.js → MediaDialog.jsx} +0 -0
|
@@ -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: (
|
|
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: (
|
|
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: (
|
|
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: (
|
|
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: (
|
|
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: (
|
|
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: (
|
|
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: (
|
|
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: (
|
|
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: (
|
|
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: (
|
|
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: (
|
|
312
|
+
hidden: () => !activePlugins?.includes('text-align'),
|
|
301
313
|
onClick: () => {},
|
|
302
314
|
},
|
|
303
315
|
{
|
|
304
316
|
icon: <BulletedListIcon />,
|
|
305
|
-
hidden: (
|
|
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: (
|
|
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: (
|
|
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: (
|
|
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}>
|
|
@@ -73,14 +73,10 @@ const StyledRoot = styled('div', {
|
|
|
73
73
|
},
|
|
74
74
|
},
|
|
75
75
|
'& blockquote': {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
});
|