@pie-lib/editable-html-tip-tap 1.2.0-next.9 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +176 -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/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 +12 -7
- 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 +90 -7
- package/src/__tests__/index.test.jsx +11 -3
- package/src/components/CharacterPicker.jsx +1 -0
- package/src/components/EditableHtml.jsx +91 -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/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 +13 -11
- package/src/styles/editorContainerStyles.js +5 -4
- package/src/utils/helper.js +17 -0
- /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('
|
|
72
|
-
|
|
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', () => ({
|
|
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('
|
|
31
|
-
|
|
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 {
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
((
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
293
|
-
editor.commands.setContent(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: (
|
|
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}>
|