@squiz/formatted-text-editor 1.21.1-alpha.7 → 1.22.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/demo/App.tsx +52 -10
- package/demo/index.scss +11 -10
- package/jest.config.ts +0 -2
- package/lib/Editor/Editor.js +45 -7
- package/lib/Editor/EditorContext.d.ts +15 -0
- package/lib/Editor/EditorContext.js +15 -0
- package/lib/EditorToolbar/FloatingToolbar.js +11 -5
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.d.ts +9 -8
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +91 -23
- package/lib/EditorToolbar/Tools/Image/ImageButton.d.ts +4 -1
- package/lib/EditorToolbar/Tools/Image/ImageButton.js +22 -14
- package/lib/EditorToolbar/Tools/Image/ImageModal.js +9 -5
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.d.ts +14 -5
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +66 -14
- package/lib/EditorToolbar/Tools/Link/LinkButton.js +21 -13
- package/lib/EditorToolbar/Tools/Link/LinkModal.js +12 -5
- package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +1 -8
- package/lib/Extensions/CommandsExtension/CommandsExtension.d.ts +20 -0
- package/lib/Extensions/CommandsExtension/CommandsExtension.js +52 -0
- package/lib/Extensions/Extensions.d.ts +12 -5
- package/lib/Extensions/Extensions.js +42 -20
- package/lib/Extensions/ImageExtension/AssetImageExtension.d.ts +17 -0
- package/lib/Extensions/ImageExtension/AssetImageExtension.js +92 -0
- package/lib/Extensions/ImageExtension/ImageExtension.d.ts +4 -0
- package/lib/Extensions/ImageExtension/ImageExtension.js +11 -0
- package/lib/Extensions/LinkExtension/AssetLinkExtension.d.ts +26 -0
- package/lib/Extensions/LinkExtension/AssetLinkExtension.js +102 -0
- package/lib/Extensions/LinkExtension/LinkExtension.d.ts +19 -12
- package/lib/Extensions/LinkExtension/LinkExtension.js +56 -66
- package/lib/Extensions/LinkExtension/common.d.ts +7 -0
- package/lib/Extensions/LinkExtension/common.js +14 -0
- package/lib/Extensions/PreformattedExtension/PreformattedExtension.d.ts +1 -1
- package/lib/Extensions/PreformattedExtension/PreformattedExtension.js +6 -2
- package/lib/hooks/index.d.ts +1 -0
- package/lib/hooks/index.js +1 -0
- package/lib/hooks/useExpandedSelection.d.ts +23 -0
- package/lib/hooks/useExpandedSelection.js +37 -0
- package/lib/index.css +58 -23
- package/lib/index.d.ts +5 -2
- package/lib/index.js +9 -3
- package/lib/types.d.ts +3 -0
- package/lib/types.js +2 -0
- package/lib/ui/Button/Button.d.ts +2 -1
- package/lib/ui/Button/Button.js +4 -5
- package/lib/ui/Fields/Input/Input.d.ts +1 -0
- package/lib/ui/Fields/Input/Input.js +9 -3
- package/lib/ui/Modal/Modal.js +5 -3
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.d.ts +9 -0
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +174 -0
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.d.ts +9 -0
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +138 -0
- package/lib/utils/resolveMatrixAssetUrl.d.ts +1 -0
- package/lib/utils/resolveMatrixAssetUrl.js +10 -0
- package/lib/utils/undefinedIfEmpty.d.ts +1 -0
- package/lib/utils/undefinedIfEmpty.js +7 -0
- package/package.json +10 -4
- package/src/Editor/Editor.spec.tsx +78 -18
- package/src/Editor/Editor.tsx +28 -9
- package/src/Editor/EditorContext.spec.tsx +26 -0
- package/src/Editor/EditorContext.ts +26 -0
- package/src/Editor/_editor.scss +20 -4
- package/src/EditorToolbar/FloatingToolbar.spec.tsx +26 -7
- package/src/EditorToolbar/FloatingToolbar.tsx +15 -6
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +81 -6
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +167 -47
- package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +250 -2
- package/src/EditorToolbar/Tools/Image/ImageButton.tsx +29 -16
- package/src/EditorToolbar/Tools/Image/ImageModal.spec.tsx +59 -20
- package/src/EditorToolbar/Tools/Image/ImageModal.tsx +12 -10
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +37 -9
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +96 -26
- package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +137 -26
- package/src/EditorToolbar/Tools/Link/LinkButton.tsx +28 -19
- package/src/EditorToolbar/Tools/Link/LinkModal.tsx +13 -6
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +27 -26
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +2 -10
- package/src/EditorToolbar/Tools/Undo/UndoButton.spec.tsx +22 -1
- package/src/EditorToolbar/_floating-toolbar.scss +4 -5
- package/src/EditorToolbar/_toolbar.scss +1 -1
- package/src/Extensions/CommandsExtension/CommandsExtension.ts +54 -0
- package/src/Extensions/Extensions.ts +42 -18
- package/src/Extensions/ImageExtension/AssetImageExtension.spec.ts +76 -0
- package/src/Extensions/ImageExtension/AssetImageExtension.ts +111 -0
- package/src/Extensions/ImageExtension/ImageExtension.ts +17 -1
- package/src/Extensions/LinkExtension/AssetLinkExtension.spec.ts +104 -0
- package/src/Extensions/LinkExtension/AssetLinkExtension.ts +128 -0
- package/src/Extensions/LinkExtension/LinkExtension.spec.ts +68 -0
- package/src/Extensions/LinkExtension/LinkExtension.ts +71 -85
- package/src/Extensions/LinkExtension/common.ts +10 -0
- package/src/Extensions/PreformattedExtension/PreformattedExtension.spec.ts +41 -0
- package/src/Extensions/PreformattedExtension/PreformattedExtension.ts +6 -2
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useExpandedSelection.ts +44 -0
- package/src/index.ts +5 -2
- package/src/types.ts +5 -0
- package/src/ui/Button/Button.tsx +10 -6
- package/src/ui/Button/_button.scss +1 -1
- package/src/ui/Fields/Input/Input.spec.tsx +7 -1
- package/src/ui/Fields/Input/Input.tsx +23 -4
- package/src/ui/Modal/Modal.spec.tsx +15 -0
- package/src/ui/Modal/Modal.tsx +8 -4
- package/src/ui/ToolbarDropdown/_toolbar-dropdown.scss +1 -1
- package/src/ui/_forms.scss +14 -0
- package/src/utils/converters/mocks/squizNodeJson.mock.ts +271 -0
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.spec.ts +480 -0
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +212 -0
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +341 -0
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +159 -0
- package/src/utils/resolveMatrixAssetUrl.spec.ts +26 -0
- package/src/utils/resolveMatrixAssetUrl.ts +7 -0
- package/src/utils/undefinedIfEmpty.spec.ts +12 -0
- package/src/utils/undefinedIfEmpty.ts +3 -0
- package/tailwind.config.cjs +3 -0
- package/tests/renderWithEditor.tsx +28 -15
- package/tsconfig.json +1 -1
- package/lib/FormattedTextEditor.d.ts +0 -2
- package/lib/FormattedTextEditor.js +0 -7
- package/src/Editor/Editor.mock.tsx +0 -43
- package/src/FormattedTextEditor.spec.tsx +0 -10
- package/src/FormattedTextEditor.tsx +0 -3
- /package/tests/{select.tsx → select.ts} +0 -0
@@ -1,8 +1,12 @@
|
|
1
1
|
import '@testing-library/jest-dom';
|
2
|
-
import { screen, fireEvent, waitForElementToBeRemoved, act } from '@testing-library/react';
|
2
|
+
import { screen, fireEvent, waitForElementToBeRemoved, act, waitFor } from '@testing-library/react';
|
3
|
+
import { NodeSelection } from 'prosemirror-state';
|
3
4
|
import React from 'react';
|
4
|
-
import { renderWithEditor } from '../../../../tests';
|
5
|
+
import { renderWithEditor, select } from '../../../../tests';
|
5
6
|
import ImageButton from './ImageButton';
|
7
|
+
import { getImageSize } from 'react-image-size';
|
8
|
+
|
9
|
+
jest.mock('react-image-size');
|
6
10
|
|
7
11
|
describe('ImageButton', () => {
|
8
12
|
const openModal = async () => {
|
@@ -10,6 +14,10 @@ describe('ImageButton', () => {
|
|
10
14
|
await screen.findByRole('button', { name: 'Apply' });
|
11
15
|
};
|
12
16
|
|
17
|
+
beforeEach(() => {
|
18
|
+
(getImageSize as jest.Mock).mockResolvedValue({ width: 2, height: 2 });
|
19
|
+
});
|
20
|
+
|
13
21
|
it('Opens the modal when clicking on the image button', async () => {
|
14
22
|
await renderWithEditor(<ImageButton />);
|
15
23
|
|
@@ -64,6 +72,94 @@ describe('ImageButton', () => {
|
|
64
72
|
});
|
65
73
|
});
|
66
74
|
|
75
|
+
it('Updates the attributes of an existing image', async () => {
|
76
|
+
const { editor, getJsonContent } = await renderWithEditor(<ImageButton />, {
|
77
|
+
content: 'Some <img src="https://httpcats.com/529.jpg" alt="hi" /> nonsense',
|
78
|
+
});
|
79
|
+
|
80
|
+
await act(() => editor.selectText(new NodeSelection(editor.state.doc.resolve(6))));
|
81
|
+
|
82
|
+
await openModal();
|
83
|
+
fireEvent.change(screen.getByLabelText('Source'), { target: { value: 'https://httpcats.com/303.jpg' } });
|
84
|
+
fireEvent.change(screen.getByLabelText('Alternative description'), { target: { value: 'Updated cats!' } });
|
85
|
+
|
86
|
+
// wait for the new image dimensions to be calculated
|
87
|
+
await waitFor(() => expect(screen.getByLabelText('Height')).toHaveValue(2));
|
88
|
+
|
89
|
+
// verify the content matches what was initially set prior to applying
|
90
|
+
expect(getJsonContent()).toEqual({
|
91
|
+
type: 'paragraph',
|
92
|
+
attrs: expect.any(Object),
|
93
|
+
content: [
|
94
|
+
{
|
95
|
+
text: 'Some ',
|
96
|
+
type: 'text',
|
97
|
+
},
|
98
|
+
{
|
99
|
+
type: 'image',
|
100
|
+
attrs: {
|
101
|
+
alt: 'hi',
|
102
|
+
crop: null,
|
103
|
+
height: null,
|
104
|
+
width: null,
|
105
|
+
rotate: null,
|
106
|
+
src: 'https://httpcats.com/529.jpg',
|
107
|
+
title: '',
|
108
|
+
fileName: null,
|
109
|
+
resizable: false,
|
110
|
+
},
|
111
|
+
},
|
112
|
+
{ type: 'text', text: ' nonsense' },
|
113
|
+
],
|
114
|
+
});
|
115
|
+
|
116
|
+
// apply the changes
|
117
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
118
|
+
|
119
|
+
await waitForElementToBeRemoved(() => screen.getByRole('button', { name: 'Apply' }));
|
120
|
+
|
121
|
+
// asset the content has been updated
|
122
|
+
expect(getJsonContent()).toEqual({
|
123
|
+
type: 'paragraph',
|
124
|
+
attrs: expect.any(Object),
|
125
|
+
content: [
|
126
|
+
{
|
127
|
+
text: 'Some ',
|
128
|
+
type: 'text',
|
129
|
+
},
|
130
|
+
{
|
131
|
+
type: 'image',
|
132
|
+
attrs: {
|
133
|
+
alt: 'Updated cats!',
|
134
|
+
crop: null,
|
135
|
+
height: 2,
|
136
|
+
width: 2,
|
137
|
+
rotate: null,
|
138
|
+
src: 'https://httpcats.com/303.jpg',
|
139
|
+
title: '',
|
140
|
+
fileName: null,
|
141
|
+
resizable: false,
|
142
|
+
},
|
143
|
+
},
|
144
|
+
{ type: 'text', text: ' nonsense' },
|
145
|
+
],
|
146
|
+
});
|
147
|
+
});
|
148
|
+
|
149
|
+
it('Removes the image when content is cleared (backspaced)', async () => {
|
150
|
+
const { editor, getJsonContent } = await renderWithEditor(<ImageButton />, {
|
151
|
+
content: '<img src="https://media2.giphy.com/media/3o6ozsIxg5legYvggo/giphy.gif"/>',
|
152
|
+
});
|
153
|
+
|
154
|
+
await act(() => editor.selectText(10));
|
155
|
+
await act(() => editor.backspace(1));
|
156
|
+
|
157
|
+
expect(getJsonContent()).toEqual({
|
158
|
+
type: 'paragraph',
|
159
|
+
attrs: expect.any(Object),
|
160
|
+
});
|
161
|
+
});
|
162
|
+
|
67
163
|
it('Closes the modal when clicking on the cancel button', async () => {
|
68
164
|
await renderWithEditor(<ImageButton />);
|
69
165
|
|
@@ -76,4 +172,156 @@ describe('ImageButton', () => {
|
|
76
172
|
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
77
173
|
expect(modalHeading).not.toBeInTheDocument();
|
78
174
|
});
|
175
|
+
|
176
|
+
it('Adds a new image with no source field', async () => {
|
177
|
+
await renderWithEditor(<ImageButton />, { content: 'Some nonsense content here' });
|
178
|
+
|
179
|
+
// open the modal and add an image.
|
180
|
+
await openModal();
|
181
|
+
fireEvent.change(screen.getByLabelText('Source'), { target: { value: '' } });
|
182
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
183
|
+
expect(await screen.findByText('Source is required')).toBeInTheDocument();
|
184
|
+
});
|
185
|
+
|
186
|
+
it('Adds a new image with data URI in source field', async () => {
|
187
|
+
await renderWithEditor(<ImageButton />);
|
188
|
+
|
189
|
+
const dataUri = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
190
|
+
|
191
|
+
// open the modal and add an image.
|
192
|
+
await openModal();
|
193
|
+
fireEvent.change(screen.getByLabelText('Source'), { target: { value: dataUri } });
|
194
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
195
|
+
expect(await screen.findByText('Must not be a data URI')).toBeInTheDocument();
|
196
|
+
});
|
197
|
+
|
198
|
+
it('Adds a new image with a non-image URL in source field', async () => {
|
199
|
+
(getImageSize as jest.Mock).mockRejectedValue('error: not an image');
|
200
|
+
|
201
|
+
await renderWithEditor(<ImageButton />);
|
202
|
+
|
203
|
+
// open the modal and add an image.
|
204
|
+
await openModal();
|
205
|
+
fireEvent.change(screen.getByLabelText('Source'), { target: { value: 'https://not-an-image.com/not-an-image' } });
|
206
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
207
|
+
expect(await screen.findByText('Must be a valid image URL')).toBeInTheDocument();
|
208
|
+
});
|
209
|
+
|
210
|
+
it('Adds a new image with no alt text', async () => {
|
211
|
+
await renderWithEditor(<ImageButton />, { content: 'Some tacos here' });
|
212
|
+
|
213
|
+
// open the modal and add an image.
|
214
|
+
await openModal();
|
215
|
+
fireEvent.change(screen.getByLabelText('Source'), { target: { value: 'https://httpcats.com/529.jpg' } });
|
216
|
+
fireEvent.change(screen.getByLabelText('Alternative description'), { target: { value: '' } });
|
217
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
218
|
+
expect(await screen.findByText('Alternative description is required')).toBeInTheDocument();
|
219
|
+
});
|
220
|
+
|
221
|
+
it('Adds a new image with no width or height text', async () => {
|
222
|
+
await renderWithEditor(<ImageButton />, { content: 'Some beautiful content here' });
|
223
|
+
|
224
|
+
// open the modal and add an image.
|
225
|
+
await openModal();
|
226
|
+
fireEvent.change(screen.getByLabelText('Width'), { target: { value: '' } });
|
227
|
+
fireEvent.change(screen.getByLabelText('Height'), { target: { value: '' } });
|
228
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
229
|
+
expect(await screen.findByText('Width is required')).toBeInTheDocument();
|
230
|
+
expect(await screen.findByText('Height is required')).toBeInTheDocument();
|
231
|
+
});
|
232
|
+
|
233
|
+
it('Adds a new asset image', async () => {
|
234
|
+
const matrixIdentifier = 'matrix-api-identifier';
|
235
|
+
const matrixDomain = 'https://my-matrix.squiz.net';
|
236
|
+
const { getJsonContent } = await renderWithEditor(<ImageButton />, {
|
237
|
+
content: 'Some nonsense content here',
|
238
|
+
context: {
|
239
|
+
matrix: {
|
240
|
+
matrixIdentifier,
|
241
|
+
matrixDomain,
|
242
|
+
resolveMatrixAsset: () => Promise.resolve({ id: '100', type: 'image' }),
|
243
|
+
},
|
244
|
+
},
|
245
|
+
});
|
246
|
+
|
247
|
+
// open the modal and add an image.
|
248
|
+
await openModal();
|
249
|
+
select(screen.getByLabelText('Type'), 'Asset image');
|
250
|
+
fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: '100' } });
|
251
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
252
|
+
|
253
|
+
await waitForElementToBeRemoved(() => screen.getByRole('button', { name: 'Apply' }));
|
254
|
+
|
255
|
+
expect(getJsonContent()).toEqual({
|
256
|
+
type: 'paragraph',
|
257
|
+
attrs: expect.any(Object),
|
258
|
+
content: [
|
259
|
+
{
|
260
|
+
type: 'assetImage',
|
261
|
+
attrs: { matrixAssetId: '100', matrixIdentifier, matrixDomain },
|
262
|
+
},
|
263
|
+
{ type: 'text', text: 'Some nonsense content here' },
|
264
|
+
],
|
265
|
+
});
|
266
|
+
});
|
267
|
+
|
268
|
+
it('Updates the attributes of an existing asset image', async () => {
|
269
|
+
const matrixIdentifier = 'matrix-api-identifier';
|
270
|
+
const matrixDomain = 'https://my-matrix.squiz.net';
|
271
|
+
const { editor, getJsonContent } = await renderWithEditor(<ImageButton />, {
|
272
|
+
content: 'Some <img src="https://httpcats.com/529.jpg" alt="hi" /> nonsense',
|
273
|
+
context: {
|
274
|
+
matrix: {
|
275
|
+
matrixIdentifier,
|
276
|
+
matrixDomain,
|
277
|
+
resolveMatrixAsset: () => Promise.resolve({ id: '100', type: 'image' }),
|
278
|
+
},
|
279
|
+
},
|
280
|
+
});
|
281
|
+
|
282
|
+
await act(() => editor.selectText(new NodeSelection(editor.state.doc.resolve(6))));
|
283
|
+
|
284
|
+
await openModal();
|
285
|
+
select(screen.getByLabelText('Type'), 'Asset image');
|
286
|
+
fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: '100' } });
|
287
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
288
|
+
|
289
|
+
await waitForElementToBeRemoved(() => screen.getByRole('button', { name: 'Apply' }));
|
290
|
+
|
291
|
+
expect(getJsonContent()).toEqual({
|
292
|
+
type: 'paragraph',
|
293
|
+
attrs: expect.any(Object),
|
294
|
+
content: [
|
295
|
+
{
|
296
|
+
text: 'Some ',
|
297
|
+
type: 'text',
|
298
|
+
},
|
299
|
+
{
|
300
|
+
type: 'assetImage',
|
301
|
+
attrs: { matrixAssetId: '100', matrixIdentifier, matrixDomain },
|
302
|
+
},
|
303
|
+
{ type: 'text', text: ' nonsense' },
|
304
|
+
],
|
305
|
+
});
|
306
|
+
});
|
307
|
+
|
308
|
+
it.each([
|
309
|
+
['Asset ID not provided', '', null, 'Asset ID is required'],
|
310
|
+
['Asset does not exist', '100', null, 'Asset ID is invalid or not an image'],
|
311
|
+
['Asset is not an image', '100', { id: 100, type: 'physical_file' }, 'Asset ID is invalid or not an image'],
|
312
|
+
])(
|
313
|
+
'Shows an error if an invalid asset ID is provided - %s',
|
314
|
+
async (description: string, assetId: string, asset: any, expectedError: string) => {
|
315
|
+
const resolveMatrixAsset = jest.fn(() => Promise.resolve(asset));
|
316
|
+
|
317
|
+
await renderWithEditor(<ImageButton />, { context: { matrix: { resolveMatrixAsset } } });
|
318
|
+
|
319
|
+
await openModal();
|
320
|
+
select(screen.getByLabelText('Type'), 'Asset image');
|
321
|
+
fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: assetId } });
|
322
|
+
await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
|
323
|
+
|
324
|
+
expect(screen.getByText(expectedError)).toBeInTheDocument();
|
325
|
+
},
|
326
|
+
);
|
79
327
|
});
|
@@ -1,29 +1,37 @@
|
|
1
|
-
import React, {
|
1
|
+
import React, { useCallback, useState } from 'react';
|
2
|
+
import { useActive, useCommands, useCurrentSelection, useKeymap } from '@remirror/react';
|
2
3
|
import ImageRoundedIcon from '@mui/icons-material/ImageRounded';
|
3
4
|
import ImageModal from './ImageModal';
|
4
5
|
import { ImageFormData } from './Form/ImageForm';
|
5
6
|
import Button from '../../../ui/Button/Button';
|
6
|
-
import {
|
7
|
+
import { ImageExtension } from '../../../Extensions/ImageExtension/ImageExtension';
|
8
|
+
import { NodeName } from '../../../Extensions/Extensions';
|
9
|
+
import { AssetImageExtension } from '../../../Extensions/ImageExtension/AssetImageExtension';
|
7
10
|
|
8
|
-
|
11
|
+
type ImageButtonProps = {
|
12
|
+
inPopover?: boolean;
|
13
|
+
};
|
14
|
+
|
15
|
+
const ImageButton = ({ inPopover = false }: ImageButtonProps) => {
|
9
16
|
const [showModal, setShowModal] = useState(false);
|
10
|
-
const { insertImage } = useCommands();
|
11
|
-
const active =
|
17
|
+
const { insertImage, insertAssetImage } = useCommands<ImageExtension | AssetImageExtension>();
|
18
|
+
const active = useActive<ImageExtension | AssetImageExtension>();
|
19
|
+
const selection = useCurrentSelection();
|
20
|
+
// if the active selection is not an image, disable the button as it means it will be text
|
21
|
+
const disabled = !selection.empty && !active.image() && !active.assetImage();
|
12
22
|
|
13
23
|
const handleClick = () => {
|
14
24
|
if (!showModal) {
|
15
|
-
|
16
|
-
// update the selected text in state before showing the modal.
|
17
|
-
requestAnimationFrame(() => {
|
18
|
-
setShowModal(true);
|
19
|
-
});
|
25
|
+
setShowModal(true);
|
20
26
|
}
|
21
27
|
};
|
22
28
|
|
23
29
|
const insertImageFromData = (data: ImageFormData) => {
|
24
|
-
const {
|
25
|
-
if (
|
26
|
-
insertImage(
|
30
|
+
const { imageType, image, assetImage } = data;
|
31
|
+
if (imageType === NodeName.Image) {
|
32
|
+
insertImage(image);
|
33
|
+
} else {
|
34
|
+
insertAssetImage(assetImage);
|
27
35
|
}
|
28
36
|
};
|
29
37
|
|
@@ -38,16 +46,21 @@ const ImageButton = () => {
|
|
38
46
|
return true;
|
39
47
|
}, []);
|
40
48
|
|
41
|
-
|
49
|
+
// when Ctrl+l is pressed show the modal, only registered in the toolbar button instance to avoid the key press
|
50
|
+
// being double handled.
|
51
|
+
if (!inPopover) {
|
52
|
+
// disable the shortcut if the button is disabled
|
53
|
+
useKeymap('Mod-l', disabled ? () => false : handleShortcut);
|
54
|
+
}
|
42
55
|
|
43
56
|
return (
|
44
57
|
<>
|
45
58
|
<Button
|
46
59
|
handleOnClick={handleClick}
|
47
|
-
isActive={active}
|
60
|
+
isActive={active.image() || active.assetImage()}
|
48
61
|
icon={<ImageRoundedIcon />}
|
49
62
|
label="Image (cmd+L)"
|
50
|
-
isDisabled={
|
63
|
+
isDisabled={disabled}
|
51
64
|
/>
|
52
65
|
{showModal && <ImageModal onCancel={() => setShowModal(false)} onSubmit={handleSubmit} />}
|
53
66
|
</>
|
@@ -1,13 +1,18 @@
|
|
1
1
|
import '@testing-library/jest-dom';
|
2
|
-
import { screen, fireEvent } from '@testing-library/react';
|
2
|
+
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
3
3
|
import React from 'react';
|
4
4
|
import { renderWithEditor } from '../../../../tests';
|
5
5
|
import ImageModal from './ImageModal';
|
6
|
+
import { getImageSize } from 'react-image-size';
|
6
7
|
|
8
|
+
jest.mock('react-image-size');
|
9
|
+
beforeEach(() => {
|
10
|
+
(getImageSize as jest.Mock).mockResolvedValue({ width: 0, height: 0 });
|
11
|
+
});
|
7
12
|
const mockSubmitFunction = jest.fn();
|
8
13
|
const mockCancelFunction = jest.fn();
|
9
|
-
const setup = () => {
|
10
|
-
const utils = renderWithEditor(<ImageModal onCancel={mockCancelFunction} onSubmit={mockSubmitFunction} />);
|
14
|
+
const setup = async () => {
|
15
|
+
const utils = await renderWithEditor(<ImageModal onCancel={mockCancelFunction} onSubmit={mockSubmitFunction} />);
|
11
16
|
const sourceInput = screen.getByRole('textbox', { name: /source/i }) as HTMLInputElement;
|
12
17
|
const altInput = screen.getByRole('textbox', { name: /alt/i }) as HTMLInputElement;
|
13
18
|
const widthInput = screen.getByRole('spinbutton', { name: /Width/i }) as HTMLInputElement;
|
@@ -29,32 +34,42 @@ describe('ImageModal', () => {
|
|
29
34
|
});
|
30
35
|
|
31
36
|
it('Populates the source field when a source is supplied', async () => {
|
32
|
-
const { sourceInput } = setup();
|
37
|
+
const { sourceInput } = await setup();
|
33
38
|
fireEvent.change(sourceInput, { target: { value: 'https://httpcats.com/302.jpg' } });
|
34
|
-
|
39
|
+
await waitFor(() => {
|
40
|
+
expect(sourceInput.value).toBe('https://httpcats.com/302.jpg');
|
41
|
+
});
|
35
42
|
});
|
36
43
|
|
37
44
|
it('Renders empty width and height fields if the image source is empty', async () => {
|
38
|
-
const { sourceInput, widthInput, heightInput } = setup();
|
45
|
+
const { sourceInput, widthInput, heightInput } = await setup();
|
39
46
|
fireEvent.change(sourceInput, { target: { value: '' } });
|
40
|
-
|
41
|
-
|
47
|
+
await waitFor(() => {
|
48
|
+
expect(widthInput.value).toBe('');
|
49
|
+
expect(heightInput.value).toBe('');
|
50
|
+
});
|
42
51
|
});
|
43
52
|
|
44
53
|
it('Updates the height field with aspect ratio based value from width', async () => {
|
45
|
-
const { widthInput, heightInput } = setup();
|
54
|
+
const { widthInput, heightInput } = await setup();
|
46
55
|
fireEvent.change(widthInput, { target: { value: '300' } });
|
47
|
-
|
48
|
-
|
56
|
+
await waitFor(() => {
|
57
|
+
expect(widthInput.value).toBe('300');
|
58
|
+
expect(heightInput.value).toBe('168.75');
|
59
|
+
});
|
49
60
|
});
|
61
|
+
|
50
62
|
it('Updates the width field with aspect ratio based value from height', async () => {
|
51
|
-
const { widthInput, heightInput } = setup();
|
63
|
+
const { widthInput, heightInput } = await setup();
|
52
64
|
fireEvent.change(heightInput, { target: { value: '100' } });
|
53
|
-
|
54
|
-
|
65
|
+
await waitFor(() => {
|
66
|
+
expect(heightInput.value).toBe('100');
|
67
|
+
expect(widthInput.value).toBe('177.78');
|
68
|
+
});
|
55
69
|
});
|
56
|
-
|
57
|
-
|
70
|
+
|
71
|
+
it('Does not change the width when height is changed and aspect ratio link is off', async () => {
|
72
|
+
const { widthInput, heightInput } = await setup();
|
58
73
|
fireEvent.change(heightInput, { target: { value: '100' } });
|
59
74
|
expect(heightInput.value).toBe('100');
|
60
75
|
expect(widthInput.value).toBe('177.78');
|
@@ -63,8 +78,9 @@ describe('ImageModal', () => {
|
|
63
78
|
expect(heightInput.value).toBe('200');
|
64
79
|
expect(widthInput.value).toBe('177.78');
|
65
80
|
});
|
66
|
-
|
67
|
-
|
81
|
+
|
82
|
+
it('Does not change the height when width is changed and aspect ratio link is off', async () => {
|
83
|
+
const { widthInput, heightInput } = await setup();
|
68
84
|
fireEvent.change(widthInput, { target: { value: '450' } });
|
69
85
|
expect(widthInput.value).toBe('450');
|
70
86
|
expect(heightInput.value).toBe('253.13');
|
@@ -73,11 +89,34 @@ describe('ImageModal', () => {
|
|
73
89
|
expect(widthInput.value).toBe('600');
|
74
90
|
expect(heightInput.value).toBe('253.13');
|
75
91
|
});
|
76
|
-
|
77
|
-
|
92
|
+
|
93
|
+
it('Changes the icon when aspect ratio button is toggled', async () => {
|
94
|
+
await setup();
|
78
95
|
expect(screen.getByTestId('InsertLinkRoundedIcon')).toBeInTheDocument();
|
79
96
|
fireEvent.click(screen.getByRole('button', { name: /constrain properties/i }));
|
80
97
|
expect(screen.queryByTestId('InsertLinkRoundedIcon')).not.toBeInTheDocument();
|
81
98
|
expect(screen.getByTestId('LinkOffIcon')).toBeInTheDocument();
|
82
99
|
});
|
100
|
+
|
101
|
+
it('Returns relevant error message if the width is not higher than 0', async () => {
|
102
|
+
const { widthInput } = await setup();
|
103
|
+
|
104
|
+
fireEvent.change(widthInput, { target: { value: '0' } });
|
105
|
+
fireEvent.click(screen.getByRole('button', { name: /Apply/i }));
|
106
|
+
await waitFor(() => {
|
107
|
+
expect(widthInput.value).toBe('0');
|
108
|
+
});
|
109
|
+
expect(await screen.findByText('Must be higher than 0')).toBeInTheDocument();
|
110
|
+
});
|
111
|
+
|
112
|
+
it('Returns relevant error message if the height is not higher than 0', async () => {
|
113
|
+
const { heightInput } = await setup();
|
114
|
+
|
115
|
+
fireEvent.change(heightInput, { target: { value: '0' } });
|
116
|
+
fireEvent.click(screen.getByRole('button', { name: /Apply/i }));
|
117
|
+
await waitFor(() => {
|
118
|
+
expect(heightInput.value).toBe('0');
|
119
|
+
});
|
120
|
+
expect(await screen.findByText('Must be higher than 0')).toBeInTheDocument();
|
121
|
+
});
|
83
122
|
});
|
@@ -1,9 +1,10 @@
|
|
1
|
-
import { getMarkRanges } from 'remirror';
|
2
1
|
import ImageForm, { ImageFormData } from './Form/ImageForm';
|
3
2
|
import React from 'react';
|
4
|
-
import {
|
3
|
+
import { useCurrentSelection } from '@remirror/react';
|
5
4
|
import FormModal from '../../../ui/Modal/FormModal';
|
6
5
|
import { SubmitHandler } from 'react-hook-form';
|
6
|
+
import { NodeSelection } from 'prosemirror-state';
|
7
|
+
import { NodeName } from '../../../Extensions/Extensions';
|
7
8
|
|
8
9
|
type ImageModalProps = {
|
9
10
|
onCancel: () => void;
|
@@ -11,17 +12,18 @@ type ImageModalProps = {
|
|
11
12
|
};
|
12
13
|
|
13
14
|
const ImageModal = ({ onCancel, onSubmit }: ImageModalProps) => {
|
14
|
-
const
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
15
|
+
const selection = useCurrentSelection() as NodeSelection;
|
16
|
+
const currentImage = selection?.node;
|
17
|
+
const currentImageAttrs = { ...currentImage?.attrs };
|
18
|
+
const formData = {
|
19
|
+
imageType: currentImage?.type.name === NodeName.AssetImage ? NodeName.AssetImage : NodeName.Image,
|
20
|
+
image: currentImage?.type?.name === NodeName.Image ? currentImageAttrs : {},
|
21
|
+
assetImage: currentImage?.type?.name === NodeName.AssetImage ? currentImageAttrs : {},
|
22
|
+
};
|
21
23
|
|
22
24
|
return (
|
23
25
|
<FormModal title="Image" onCancel={onCancel}>
|
24
|
-
<ImageForm data={
|
26
|
+
<ImageForm data={formData} onSubmit={onSubmit} />
|
25
27
|
</FormModal>
|
26
28
|
);
|
27
29
|
};
|
@@ -1,30 +1,58 @@
|
|
1
1
|
import '@testing-library/jest-dom';
|
2
2
|
import { render, screen } from '@testing-library/react';
|
3
3
|
import React from 'react';
|
4
|
-
import LinkForm from './LinkForm';
|
4
|
+
import { LinkForm } from './LinkForm';
|
5
|
+
import { LinkTarget } from '../../../../Extensions/LinkExtension/common';
|
6
|
+
import { MarkName } from '../../../../Extensions/Extensions';
|
5
7
|
|
6
8
|
describe('Link Form', () => {
|
7
9
|
const handleSubmit = jest.fn();
|
8
10
|
const data = {
|
9
|
-
|
10
|
-
target: '_blank',
|
11
|
-
title: 'Link title',
|
11
|
+
linkType: MarkName.Link,
|
12
12
|
text: 'Link text',
|
13
|
+
range: { from: 10, to: 15 },
|
14
|
+
link: {
|
15
|
+
href: 'https://www.squiz.net/link-form',
|
16
|
+
target: LinkTarget.Blank,
|
17
|
+
title: 'Link title',
|
18
|
+
},
|
19
|
+
assetLink: {
|
20
|
+
matrixAssetId: '100',
|
21
|
+
matrixIdentifier: 'matrix-identifier',
|
22
|
+
matrixDomain: 'my-matrix.squiz.net',
|
23
|
+
target: LinkTarget.Blank,
|
24
|
+
},
|
13
25
|
};
|
14
26
|
|
15
|
-
it('Renders the form with
|
27
|
+
it('Renders the form with expected default values when no data is provided', () => {
|
28
|
+
render(<LinkForm onSubmit={handleSubmit} />);
|
29
|
+
|
30
|
+
expect(screen.getByLabelText('Type')).toHaveTextContent('Link to URL');
|
31
|
+
expect(screen.getByLabelText('URL')).toHaveValue('');
|
32
|
+
expect(screen.getByLabelText('Text')).toHaveValue('');
|
33
|
+
expect(screen.getByLabelText('Title')).toHaveValue('');
|
34
|
+
expect(screen.getByLabelText('Target')).toHaveTextContent('Current window');
|
35
|
+
expect(document.querySelectorAll('label')).toHaveLength(5);
|
36
|
+
});
|
37
|
+
|
38
|
+
it('Renders the form with the expected fields for arbitrary links', () => {
|
16
39
|
render(<LinkForm data={data} onSubmit={handleSubmit} />);
|
17
40
|
|
41
|
+
expect(screen.getByLabelText('Type')).toHaveTextContent('Link to URL');
|
18
42
|
expect(screen.getByLabelText('URL')).toHaveValue('https://www.squiz.net/link-form');
|
19
43
|
expect(screen.getByLabelText('Text')).toHaveValue('Link text');
|
20
44
|
expect(screen.getByLabelText('Title')).toHaveValue('Link title');
|
21
45
|
expect(screen.getByLabelText('Target')).toHaveTextContent('New window');
|
46
|
+
expect(document.querySelectorAll('label')).toHaveLength(5);
|
22
47
|
});
|
23
48
|
|
24
|
-
it('Renders the form with the
|
25
|
-
render(<LinkForm data={data} onSubmit={handleSubmit} />);
|
49
|
+
it('Renders the form with the expected fields for asset links', () => {
|
50
|
+
render(<LinkForm data={{ ...data, linkType: MarkName.AssetLink }} onSubmit={handleSubmit} />);
|
26
51
|
|
27
|
-
|
28
|
-
expect(
|
52
|
+
expect(screen.getByLabelText('Type')).toHaveTextContent('Link to asset');
|
53
|
+
expect(screen.getByLabelText('Asset ID')).toHaveValue('100');
|
54
|
+
expect(screen.getByLabelText('Text')).toHaveValue('Link text');
|
55
|
+
expect(screen.getByLabelText('Target')).toHaveTextContent('New window');
|
56
|
+
expect(document.querySelectorAll('label')).toHaveLength(4);
|
29
57
|
});
|
30
58
|
});
|