@pie-lib/editable-html-tip-tap 1.2.0-next.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +172 -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
|
@@ -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
|
});
|
|
@@ -6,13 +6,14 @@ describe('InsertImageHandler', () => {
|
|
|
6
6
|
nodeSize: 1,
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
+
const NODE_POS = 5;
|
|
10
|
+
|
|
11
|
+
const nodeInfo = (node = mockNode, pos = NODE_POS) => [node, pos];
|
|
12
|
+
|
|
9
13
|
const createMockEditor = () => ({
|
|
14
|
+
_insertingImage: true,
|
|
10
15
|
state: {
|
|
11
|
-
doc: {
|
|
12
|
-
descendants: jest.fn((callback) => {
|
|
13
|
-
callback(mockNode, 5);
|
|
14
|
-
}),
|
|
15
|
-
},
|
|
16
|
+
doc: {},
|
|
16
17
|
tr: {
|
|
17
18
|
setNodeMarkup: jest.fn((pos, type, attrs) => ({ setNodeMarkup: jest.fn() })),
|
|
18
19
|
delete: jest.fn((from, to) => ({ delete: jest.fn() })),
|
|
@@ -31,36 +32,39 @@ describe('InsertImageHandler', () => {
|
|
|
31
32
|
|
|
32
33
|
it('creates handler instance', () => {
|
|
33
34
|
const editor = createMockEditor();
|
|
34
|
-
const
|
|
35
|
+
const info = nodeInfo();
|
|
36
|
+
const handler = new InsertImageHandler(editor, info, mockOnFinish);
|
|
35
37
|
expect(handler).toBeDefined();
|
|
36
38
|
expect(handler.editor).toBe(editor);
|
|
39
|
+
expect(handler.nodeInfo).toBe(info);
|
|
37
40
|
expect(handler.node).toBe(mockNode);
|
|
38
41
|
expect(handler.onFinish).toBe(mockOnFinish);
|
|
39
42
|
});
|
|
40
43
|
|
|
41
|
-
it('stores nodePos from
|
|
44
|
+
it('stores nodePos from nodeInfo tuple', () => {
|
|
42
45
|
const editor = createMockEditor();
|
|
43
|
-
const handler = new InsertImageHandler(editor, mockNode, mockOnFinish);
|
|
44
|
-
expect(handler.nodePos).toBe(
|
|
46
|
+
const handler = new InsertImageHandler(editor, nodeInfo(mockNode, 42), mockOnFinish);
|
|
47
|
+
expect(handler.nodePos).toBe(42);
|
|
45
48
|
});
|
|
46
49
|
|
|
47
50
|
it('stores isPasted parameter', () => {
|
|
48
51
|
const editor = createMockEditor();
|
|
49
|
-
const handler = new InsertImageHandler(editor,
|
|
52
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish, true);
|
|
50
53
|
expect(handler.isPasted).toBe(true);
|
|
51
54
|
});
|
|
52
55
|
|
|
53
56
|
it('defaults isPasted to false', () => {
|
|
54
57
|
const editor = createMockEditor();
|
|
55
|
-
const handler = new InsertImageHandler(editor,
|
|
58
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
|
|
56
59
|
expect(handler.isPasted).toBe(false);
|
|
57
60
|
});
|
|
58
61
|
|
|
59
62
|
it('cancel deletes node and calls onFinish', () => {
|
|
60
63
|
const editor = createMockEditor();
|
|
61
|
-
const handler = new InsertImageHandler(editor,
|
|
64
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
|
|
62
65
|
handler.cancel();
|
|
63
66
|
expect(mockOnFinish).toHaveBeenCalledWith(false);
|
|
67
|
+
expect(editor._insertingImage).toBe(false);
|
|
64
68
|
});
|
|
65
69
|
|
|
66
70
|
it('updateNode dispatches transaction with new attributes', () => {
|
|
@@ -68,22 +72,23 @@ describe('InsertImageHandler', () => {
|
|
|
68
72
|
const mockNodeAt = jest.fn(() => ({ attrs: { existing: 'value' } }));
|
|
69
73
|
editor.state.doc.nodeAt = mockNodeAt;
|
|
70
74
|
|
|
71
|
-
const handler = new InsertImageHandler(editor,
|
|
75
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
|
|
72
76
|
handler.updateNode({ newAttr: 'newValue' });
|
|
73
77
|
|
|
74
|
-
expect(mockNodeAt).toHaveBeenCalledWith(
|
|
78
|
+
expect(mockNodeAt).toHaveBeenCalledWith(NODE_POS);
|
|
75
79
|
expect(editor.view.dispatch).toHaveBeenCalled();
|
|
76
80
|
});
|
|
77
81
|
|
|
78
82
|
it('done calls onFinish with false on error', () => {
|
|
79
83
|
const editor = createMockEditor();
|
|
80
|
-
const handler = new InsertImageHandler(editor,
|
|
84
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
|
|
81
85
|
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
82
86
|
|
|
83
87
|
handler.done('error', null);
|
|
84
88
|
|
|
85
89
|
expect(consoleLogSpy).toHaveBeenCalledWith('error');
|
|
86
90
|
expect(mockOnFinish).toHaveBeenCalledWith(false);
|
|
91
|
+
expect(editor._insertingImage).toBe(false);
|
|
87
92
|
consoleLogSpy.mockRestore();
|
|
88
93
|
});
|
|
89
94
|
|
|
@@ -92,16 +97,17 @@ describe('InsertImageHandler', () => {
|
|
|
92
97
|
const mockNodeAt = jest.fn(() => ({ attrs: {} }));
|
|
93
98
|
editor.state.doc.nodeAt = mockNodeAt;
|
|
94
99
|
|
|
95
|
-
const handler = new InsertImageHandler(editor,
|
|
100
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
|
|
96
101
|
handler.done(null, 'http://example.com/image.jpg');
|
|
97
102
|
|
|
98
103
|
expect(editor.view.dispatch).toHaveBeenCalled();
|
|
99
104
|
expect(mockOnFinish).toHaveBeenCalledWith(true);
|
|
105
|
+
expect(editor._insertingImage).toBe(false);
|
|
100
106
|
});
|
|
101
107
|
|
|
102
108
|
it('fileChosen returns early when no file provided', () => {
|
|
103
109
|
const editor = createMockEditor();
|
|
104
|
-
const handler = new InsertImageHandler(editor,
|
|
110
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
|
|
105
111
|
handler.fileChosen(null);
|
|
106
112
|
expect(editor.view.dispatch).not.toHaveBeenCalled();
|
|
107
113
|
});
|
|
@@ -111,7 +117,7 @@ describe('InsertImageHandler', () => {
|
|
|
111
117
|
const mockNodeAt = jest.fn(() => ({ attrs: {} }));
|
|
112
118
|
editor.state.doc.nodeAt = mockNodeAt;
|
|
113
119
|
|
|
114
|
-
const handler = new InsertImageHandler(editor,
|
|
120
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
|
|
115
121
|
const mockFile = new File(['content'], 'test.jpg', { type: 'image/jpeg' });
|
|
116
122
|
|
|
117
123
|
global.FileReader = jest.fn(function () {
|
|
@@ -124,6 +130,7 @@ describe('InsertImageHandler', () => {
|
|
|
124
130
|
handler.fileChosen(mockFile);
|
|
125
131
|
|
|
126
132
|
expect(handler.chosenFile).toBe(mockFile);
|
|
133
|
+
expect(editor._insertingImage).toBe(false);
|
|
127
134
|
});
|
|
128
135
|
|
|
129
136
|
it('progress updates node with percent', () => {
|
|
@@ -131,7 +138,7 @@ describe('InsertImageHandler', () => {
|
|
|
131
138
|
const mockNodeAt = jest.fn(() => ({ attrs: {} }));
|
|
132
139
|
editor.state.doc.nodeAt = mockNodeAt;
|
|
133
140
|
|
|
134
|
-
const handler = new InsertImageHandler(editor,
|
|
141
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
|
|
135
142
|
handler.progress(50, 500, 1000);
|
|
136
143
|
|
|
137
144
|
expect(editor.view.dispatch).toHaveBeenCalled();
|
|
@@ -139,7 +146,7 @@ describe('InsertImageHandler', () => {
|
|
|
139
146
|
|
|
140
147
|
it('getChosenFile returns the chosen file', () => {
|
|
141
148
|
const editor = createMockEditor();
|
|
142
|
-
const handler = new InsertImageHandler(editor,
|
|
149
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
|
|
143
150
|
const mockFile = new File(['content'], 'test.jpg', { type: 'image/jpeg' });
|
|
144
151
|
handler.chosenFile = mockFile;
|
|
145
152
|
|
|
@@ -148,7 +155,7 @@ describe('InsertImageHandler', () => {
|
|
|
148
155
|
|
|
149
156
|
it('getChosenFile returns null initially', () => {
|
|
150
157
|
const editor = createMockEditor();
|
|
151
|
-
const handler = new InsertImageHandler(editor,
|
|
158
|
+
const handler = new InsertImageHandler(editor, nodeInfo(), mockOnFinish);
|
|
152
159
|
expect(handler.getChosenFile()).toBeNull();
|
|
153
160
|
});
|
|
154
161
|
});
|
|
@@ -214,4 +214,36 @@ describe('StyledMenuBar', () => {
|
|
|
214
214
|
toolbar?.dispatchEvent(event);
|
|
215
215
|
expect(preventDefaultSpy).toHaveBeenCalled();
|
|
216
216
|
});
|
|
217
|
+
|
|
218
|
+
it('calculates hasTextSelectionInTable correctly when selection is not empty in table', () => {
|
|
219
|
+
// This test verifies the hasTextSelectionInTable state computation
|
|
220
|
+
const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['table', 'bold', 'italic']} />);
|
|
221
|
+
expect(container).toBeInTheDocument();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('hides table manipulation buttons when text is selected in table', () => {
|
|
225
|
+
// When hasTextSelectionInTable is true, table row/column buttons should be hidden
|
|
226
|
+
const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['table']} />);
|
|
227
|
+
// The component should render but table manipulation buttons should be conditional
|
|
228
|
+
expect(container).toBeInTheDocument();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('shows table manipulation buttons when no text is selected in table', () => {
|
|
232
|
+
// When hasTextSelectionInTable is false, table row/column buttons should be visible
|
|
233
|
+
const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['table']} />);
|
|
234
|
+
expect(container).toBeInTheDocument();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('shows text formatting buttons regardless of table state', () => {
|
|
238
|
+
// Bold, italic, etc. should always be visible when their plugin is active
|
|
239
|
+
const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['bold', 'italic', 'underline']} />);
|
|
240
|
+
const buttons = container.querySelectorAll('button');
|
|
241
|
+
expect(buttons.length).toBeGreaterThan(0);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('does not hide text formatting buttons when in table', () => {
|
|
245
|
+
// Verify that the removal of "|| state.isTable" condition works correctly
|
|
246
|
+
const { container } = render(<StyledMenuBar {...defaultProps} activePlugins={['table', 'bold', 'italic']} />);
|
|
247
|
+
expect(container).toBeInTheDocument();
|
|
248
|
+
});
|
|
217
249
|
});
|
|
@@ -60,7 +60,7 @@ export default ({ editor, onChange }) => {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
const applyAlignment = (event) => {
|
|
63
|
-
const alignType = event.
|
|
63
|
+
const alignType = event.currentTarget?.getAttribute('value');
|
|
64
64
|
|
|
65
65
|
if (alignType) {
|
|
66
66
|
editor.commands.setTextAlign(alignType);
|
|
@@ -11,20 +11,11 @@ const log = debug('@pie-lib:editable-html:image:insert-image-handler');
|
|
|
11
11
|
* @param {Boolean} isPasted - a boolean that keeps track if the file is pasted
|
|
12
12
|
*/
|
|
13
13
|
class InsertImageHandler {
|
|
14
|
-
constructor(editor,
|
|
14
|
+
constructor(editor, nodeInfo, onFinish, isPasted = false) {
|
|
15
15
|
this.editor = editor;
|
|
16
|
-
this.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
editor.state.doc.descendants((node, pos) => {
|
|
21
|
-
if (node === this.node) {
|
|
22
|
-
nodePos = pos;
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
this.nodePos = nodePos;
|
|
16
|
+
this.nodeInfo = nodeInfo;
|
|
17
|
+
this.node = nodeInfo[0];
|
|
18
|
+
this.nodePos = nodeInfo[1];
|
|
28
19
|
this.onFinish = onFinish;
|
|
29
20
|
this.isPasted = isPasted;
|
|
30
21
|
this.chosenFile = null;
|
|
@@ -38,6 +29,8 @@ class InsertImageHandler {
|
|
|
38
29
|
this.onFinish(false);
|
|
39
30
|
} catch (err) {
|
|
40
31
|
//
|
|
32
|
+
} finally {
|
|
33
|
+
this.editor._insertingImage = false;
|
|
41
34
|
}
|
|
42
35
|
}
|
|
43
36
|
|
|
@@ -72,6 +65,8 @@ class InsertImageHandler {
|
|
|
72
65
|
this.updateNode({ loaded: true, src, percent: 100 });
|
|
73
66
|
this.onFinish(true);
|
|
74
67
|
}
|
|
68
|
+
|
|
69
|
+
this.editor._insertingImage = false;
|
|
75
70
|
}
|
|
76
71
|
|
|
77
72
|
/**
|
|
@@ -86,6 +81,7 @@ class InsertImageHandler {
|
|
|
86
81
|
|
|
87
82
|
// Save the chosen file to this.chosenFile
|
|
88
83
|
this.chosenFile = file;
|
|
84
|
+
this.editor._insertingImage = false;
|
|
89
85
|
|
|
90
86
|
log('[fileChosen] file: ', file);
|
|
91
87
|
const reader = new FileReader();
|
|
@@ -33,7 +33,11 @@ const DragDrop = (props) => {
|
|
|
33
33
|
|
|
34
34
|
// console.log({nodeProps.children})
|
|
35
35
|
return (
|
|
36
|
-
<NodeViewWrapper
|
|
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>
|