@squiz/formatted-text-editor 1.33.1-alpha.2 → 1.33.1-alpha.4
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 +4 -24
- package/demo/AppContext.tsx +28 -0
- package/demo/index.scss +0 -2
- package/demo/main.tsx +2 -0
- package/demo/resources.json +28 -0
- package/demo/sources.json +23 -0
- package/lib/Editor/EditorContext.d.ts +0 -7
- package/lib/Editor/EditorContext.js +0 -2
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +16 -32
- package/lib/EditorToolbar/Tools/Image/ImageModal.js +3 -2
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +18 -58
- package/lib/EditorToolbar/Tools/Link/LinkModal.js +3 -2
- package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +1 -1
- package/lib/Extensions/Extensions.js +0 -2
- package/lib/Extensions/ImageExtension/AssetImageExtension.d.ts +0 -1
- package/lib/Extensions/ImageExtension/AssetImageExtension.js +1 -2
- package/lib/Extensions/LinkExtension/AssetLinkExtension.d.ts +0 -1
- package/lib/Extensions/LinkExtension/AssetLinkExtension.js +2 -3
- package/lib/Extensions/LinkExtension/LinkExtension.js +1 -1
- package/lib/index.css +84 -4
- package/lib/types.d.ts +3 -3
- package/lib/ui/Fields/Checkbox/Checkbox.d.ts +8 -0
- package/lib/ui/Fields/Checkbox/Checkbox.js +47 -0
- package/lib/ui/Fields/Input/Input.d.ts +2 -4
- package/lib/ui/Fields/Input/Input.js +3 -9
- package/lib/ui/Fields/InputContainer/InputContainer.d.ts +9 -0
- package/lib/ui/Fields/InputContainer/InputContainer.js +16 -0
- package/lib/ui/Fields/MatrixAsset/MatrixAsset.d.ts +17 -0
- package/lib/ui/Fields/MatrixAsset/MatrixAsset.js +29 -0
- package/lib/ui/Modal/Modal.d.ts +1 -0
- package/lib/ui/Modal/Modal.js +3 -2
- package/lib/ui/Tabs/Tabs.d.ts +10 -0
- package/lib/ui/Tabs/Tabs.js +46 -0
- package/lib/utils/validation.d.ts +2 -1
- package/lib/utils/validation.js +8 -2
- package/package.json +4 -3
- package/src/Editor/Editor.spec.tsx +1 -1
- package/src/Editor/EditorContext.spec.tsx +11 -13
- package/src/Editor/EditorContext.ts +0 -11
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +29 -12
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +37 -53
- package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +76 -49
- package/src/EditorToolbar/Tools/Image/ImageModal.spec.tsx +1 -0
- package/src/EditorToolbar/Tools/Image/ImageModal.tsx +3 -2
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +22 -13
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +35 -57
- package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +52 -36
- package/src/EditorToolbar/Tools/Link/LinkModal.tsx +3 -2
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +47 -4
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +3 -2
- package/src/Extensions/Extensions.ts +0 -2
- package/src/Extensions/ImageExtension/AssetImageExtension.ts +1 -3
- package/src/Extensions/LinkExtension/AssetLinkExtension.ts +2 -4
- package/src/Extensions/LinkExtension/LinkExtension.ts +1 -1
- package/src/index.scss +1 -0
- package/src/types.ts +7 -5
- package/src/ui/Fields/Checkbox/Checkbox.spec.tsx +50 -0
- package/src/ui/Fields/Checkbox/Checkbox.tsx +49 -0
- package/src/ui/Fields/Checkbox/_checkbox.scss +26 -0
- package/src/ui/Fields/Input/Input.tsx +4 -18
- package/src/ui/Fields/InputContainer/InputContainer.spec.tsx +18 -0
- package/src/ui/Fields/InputContainer/InputContainer.tsx +29 -0
- package/src/ui/Fields/MatrixAsset/MatrixAsset.spec.tsx +103 -0
- package/src/ui/Fields/MatrixAsset/MatrixAsset.tsx +55 -0
- package/src/ui/Modal/FormModal.spec.tsx +2 -1
- package/src/ui/Modal/Modal.spec.tsx +15 -7
- package/src/ui/Modal/Modal.tsx +4 -2
- package/src/ui/Tabs/Tabs.spec.tsx +44 -0
- package/src/ui/Tabs/Tabs.tsx +41 -0
- package/src/ui/_forms.scss +4 -2
- package/src/utils/validation.spec.ts +22 -0
- package/src/utils/validation.ts +9 -1
- package/tests/index.ts +2 -0
- package/tests/mockResourceBrowserContext.tsx +63 -0
- package/tests/renderWithContext.tsx +18 -0
- package/tests/renderWithEditor.tsx +18 -21
- package/vite.config.ts +8 -0
- package/lib/ui/Fields/Select/Select.d.ts +0 -12
- package/lib/ui/Fields/Select/Select.js +0 -53
@@ -1,18 +1,18 @@
|
|
1
|
-
import React, { ReactElement,
|
2
|
-
import {
|
3
|
-
import { SubmitHandler, useForm } from 'react-hook-form';
|
1
|
+
import React, { ReactElement, useState } from 'react';
|
2
|
+
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
4
3
|
import { getImageSize } from 'react-image-size';
|
4
|
+
import clsx from 'clsx';
|
5
|
+
import { Input } from '../../../../ui/Fields/Input/Input';
|
5
6
|
import { ImageAttributes } from '@remirror/extension-image/dist-types/image-extension';
|
6
7
|
import Button from '../../../../ui/Button/Button';
|
7
8
|
import LinkOffIcon from '@mui/icons-material/LinkOff';
|
8
9
|
import InsertLinkRoundedIcon from '@mui/icons-material/InsertLinkRounded';
|
9
|
-
import clsx from 'clsx';
|
10
10
|
import { NodeName } from '../../../../Extensions/Extensions';
|
11
11
|
import { AssetImageAttributes } from '../../../../Extensions/ImageExtension/AssetImageExtension';
|
12
12
|
import { DeepPartial } from '../../../../types';
|
13
|
-
import {
|
14
|
-
import {
|
15
|
-
import {
|
13
|
+
import { noEmptySpacesValidation, regexDataURI, hasProperties } from '../../../../utils/validation';
|
14
|
+
import { TabOptions, Tabs } from '../../../../ui/Tabs/Tabs';
|
15
|
+
import { MatrixAsset } from '../../../../ui/Fields/MatrixAsset/MatrixAsset';
|
16
16
|
|
17
17
|
export type ImageFormData = {
|
18
18
|
imageType: NodeName;
|
@@ -20,9 +20,9 @@ export type ImageFormData = {
|
|
20
20
|
assetImage: AssetImageAttributes;
|
21
21
|
};
|
22
22
|
|
23
|
-
const imageTypeOptions:
|
24
|
-
[NodeName.
|
25
|
-
[NodeName.
|
23
|
+
const imageTypeOptions: TabOptions = {
|
24
|
+
[NodeName.AssetImage]: { label: 'From source' },
|
25
|
+
[NodeName.Image]: { label: 'From URL' },
|
26
26
|
};
|
27
27
|
|
28
28
|
export type FormProps = {
|
@@ -33,6 +33,7 @@ export type Dimensions = 'image.width' | 'image.height';
|
|
33
33
|
|
34
34
|
const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
|
35
35
|
const {
|
36
|
+
control,
|
36
37
|
register,
|
37
38
|
handleSubmit,
|
38
39
|
setValue,
|
@@ -41,8 +42,7 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
|
|
41
42
|
} = useForm<ImageFormData>({
|
42
43
|
defaultValues: data,
|
43
44
|
});
|
44
|
-
const imageType = watch('imageType') || NodeName.
|
45
|
-
const context = useContext(EditorContext);
|
45
|
+
const imageType = watch('imageType') || NodeName.AssetImage;
|
46
46
|
const [aspectRatioFromWidth, setAspectRatioFromWidth] = useState(9 / 16);
|
47
47
|
const [aspectRatioFromHeight, setAspectRatioFromHeight] = useState(16 / 9);
|
48
48
|
const [aspectRatioLocked, setAspectRatioLocked] = useState(true);
|
@@ -88,10 +88,8 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
|
|
88
88
|
|
89
89
|
return (
|
90
90
|
<form className="squiz-fte-form" onSubmit={handleSubmit(onSubmit)}>
|
91
|
-
<div className="squiz-fte-form-group mb-
|
92
|
-
<
|
93
|
-
name="imageType"
|
94
|
-
label="Type"
|
91
|
+
<div className="squiz-fte-form-group mb-4">
|
92
|
+
<Tabs
|
95
93
|
value={imageType}
|
96
94
|
options={imageTypeOptions}
|
97
95
|
onChange={(value) => setValue('imageType', value as NodeName)}
|
@@ -121,19 +119,6 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
|
|
121
119
|
})}
|
122
120
|
/>
|
123
121
|
</div>
|
124
|
-
<div className="squiz-fte-form-group mb-2">
|
125
|
-
<Input
|
126
|
-
label="Alternative description"
|
127
|
-
required
|
128
|
-
error={errors?.image?.alt?.message}
|
129
|
-
{...register('image.alt', {
|
130
|
-
required: 'Alternative description is required',
|
131
|
-
validate: {
|
132
|
-
noEmptySpaces: noEmptySpacesValidation,
|
133
|
-
},
|
134
|
-
})}
|
135
|
-
/>
|
136
|
-
</div>
|
137
122
|
<div className="flex flex-row">
|
138
123
|
<div className="squiz-fte-form-group mb-2">
|
139
124
|
<Input
|
@@ -146,7 +131,7 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
|
|
146
131
|
required: 'Width is required',
|
147
132
|
validate: {
|
148
133
|
isValidWidth: (value) => {
|
149
|
-
if (value && !(value > 0)) {
|
134
|
+
if (value && !((value as number) > 0)) {
|
150
135
|
return 'Must be higher than 0';
|
151
136
|
}
|
152
137
|
},
|
@@ -175,7 +160,7 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
|
|
175
160
|
required: 'Height is required',
|
176
161
|
validate: {
|
177
162
|
isValidHeight: (value) => {
|
178
|
-
if (value && !(value > 0)) {
|
163
|
+
if (value && !((value as number) > 0)) {
|
179
164
|
return 'Must be higher than 0';
|
180
165
|
}
|
181
166
|
},
|
@@ -184,32 +169,31 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
|
|
184
169
|
/>
|
185
170
|
</div>
|
186
171
|
</div>
|
187
|
-
</>
|
188
|
-
)}
|
189
|
-
{imageType === NodeName.AssetImage && (
|
190
|
-
<>
|
191
172
|
<div className="squiz-fte-form-group mb-2">
|
192
|
-
<Input
|
193
|
-
label="Asset ID"
|
194
|
-
required
|
195
|
-
error={errors?.assetImage?.matrixAssetId?.message}
|
196
|
-
{...register('assetImage.matrixAssetId', {
|
197
|
-
required: 'Asset ID is required',
|
198
|
-
validate: {
|
199
|
-
isImage: async (assetId) => {
|
200
|
-
const asset = await context.matrix.resolveMatrixAsset(assetId);
|
201
|
-
|
202
|
-
if (asset?.type !== 'image') {
|
203
|
-
return 'Asset ID is invalid or not an image';
|
204
|
-
}
|
205
|
-
},
|
206
|
-
noEmptySpaces: noEmptySpacesValidation,
|
207
|
-
},
|
208
|
-
})}
|
209
|
-
/>
|
173
|
+
<Input label="Alternative description" error={errors?.image?.alt?.message} {...register('image.alt')} />
|
210
174
|
</div>
|
211
175
|
</>
|
212
176
|
)}
|
177
|
+
{imageType === NodeName.AssetImage && (
|
178
|
+
<div className="squiz-fte-form-group mb-2">
|
179
|
+
<Controller
|
180
|
+
control={control}
|
181
|
+
name="assetImage"
|
182
|
+
rules={{
|
183
|
+
validate: hasProperties('An image must be selected', ['matrixIdentifier', 'matrixAssetId']),
|
184
|
+
}}
|
185
|
+
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
186
|
+
<MatrixAsset
|
187
|
+
modalTitle="Insert image"
|
188
|
+
allowedTypes={['image']}
|
189
|
+
value={value}
|
190
|
+
onChange={onChange}
|
191
|
+
error={error?.message}
|
192
|
+
/>
|
193
|
+
)}
|
194
|
+
/>
|
195
|
+
</div>
|
196
|
+
)}
|
213
197
|
</form>
|
214
198
|
);
|
215
199
|
};
|
@@ -2,7 +2,7 @@ import '@testing-library/jest-dom';
|
|
2
2
|
import { screen, fireEvent, waitForElementToBeRemoved, act, waitFor } from '@testing-library/react';
|
3
3
|
import { NodeSelection } from 'prosemirror-state';
|
4
4
|
import React from 'react';
|
5
|
-
import { renderWithEditor,
|
5
|
+
import { renderWithEditor, mockResourceBrowserContext } from '../../../../tests';
|
6
6
|
import ImageButton from './ImageButton';
|
7
7
|
import { getImageSize } from 'react-image-size';
|
8
8
|
|
@@ -12,6 +12,7 @@ describe('ImageButton', () => {
|
|
12
12
|
const openModal = async () => {
|
13
13
|
fireEvent.click(screen.getByRole('button', { name: 'Image (cmd+L)' }));
|
14
14
|
await screen.findByRole('button', { name: 'Apply' });
|
15
|
+
fireEvent.click(screen.getByRole('button', { name: 'From URL' }));
|
15
16
|
};
|
16
17
|
|
17
18
|
beforeEach(() => {
|
@@ -28,14 +29,13 @@ describe('ImageButton', () => {
|
|
28
29
|
});
|
29
30
|
|
30
31
|
it('Opens the modal when clicking the keyboard shortcut', async () => {
|
31
|
-
const {
|
32
|
+
const { elements } = await renderWithEditor(<ImageButton />);
|
32
33
|
|
33
34
|
// press the keyboard shortcut.
|
34
35
|
fireEvent.keyDown(elements.editor, { key: 'l', ctrlKey: true });
|
35
36
|
|
36
37
|
// verify the modal opens
|
37
|
-
await
|
38
|
-
expect(await screen.findByLabelText('Source')).toHaveValue('');
|
38
|
+
expect(await screen.findByRole('button', { name: 'Choose image' })).toBeInTheDocument();
|
39
39
|
});
|
40
40
|
|
41
41
|
it('Adds a new image', async () => {
|
@@ -215,7 +215,10 @@ describe('ImageButton', () => {
|
|
215
215
|
fireEvent.change(screen.getByLabelText('Source'), { target: { value: 'https://httpcats.com/529.jpg' } });
|
216
216
|
fireEvent.change(screen.getByLabelText('Alternative description'), { target: { value: '' } });
|
217
217
|
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
218
|
-
|
218
|
+
|
219
|
+
await waitFor(() => {
|
220
|
+
expect(document.querySelector('img[src="https://httpcats.com/529.jpg"]')).toBeInTheDocument();
|
221
|
+
});
|
219
222
|
});
|
220
223
|
|
221
224
|
it('Adds a new image with no width or height text', async () => {
|
@@ -233,21 +236,40 @@ describe('ImageButton', () => {
|
|
233
236
|
it('Adds a new asset image', async () => {
|
234
237
|
const matrixIdentifier = 'matrix-api-identifier';
|
235
238
|
const matrixDomain = 'https://my-matrix.squiz.net';
|
236
|
-
const {
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
239
|
+
const { MockResourceBrowserContext, selectResource } = mockResourceBrowserContext({
|
240
|
+
sources: [{ id: matrixIdentifier }],
|
241
|
+
resources: [
|
242
|
+
{
|
243
|
+
id: 'image-resource-id',
|
244
|
+
name: 'My image resource',
|
245
|
+
type: {
|
246
|
+
code: 'image',
|
247
|
+
name: 'Image',
|
248
|
+
},
|
243
249
|
},
|
244
|
-
|
250
|
+
],
|
245
251
|
});
|
246
252
|
|
253
|
+
const { getJsonContent } = await renderWithEditor(
|
254
|
+
<MockResourceBrowserContext>
|
255
|
+
<ImageButton />
|
256
|
+
</MockResourceBrowserContext>,
|
257
|
+
{
|
258
|
+
content: 'Some nonsense content here',
|
259
|
+
context: {
|
260
|
+
editor: {
|
261
|
+
matrix: {
|
262
|
+
matrixDomain,
|
263
|
+
},
|
264
|
+
},
|
265
|
+
},
|
266
|
+
},
|
267
|
+
);
|
268
|
+
|
247
269
|
// open the modal and add an image.
|
248
270
|
await openModal();
|
249
|
-
|
250
|
-
|
271
|
+
fireEvent.click(screen.getByRole('button', { name: 'From source' }));
|
272
|
+
await selectResource(screen.getByRole('button', { name: 'Choose image' }), 'My image resource');
|
251
273
|
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
252
274
|
|
253
275
|
await waitForElementToBeRemoved(() => screen.getByRole('button', { name: 'Apply' }));
|
@@ -258,7 +280,7 @@ describe('ImageButton', () => {
|
|
258
280
|
content: [
|
259
281
|
{
|
260
282
|
type: 'assetImage',
|
261
|
-
attrs: { matrixAssetId: '
|
283
|
+
attrs: { matrixAssetId: 'image-resource-id', matrixIdentifier, matrixDomain },
|
262
284
|
},
|
263
285
|
{ type: 'text', text: 'Some nonsense content here' },
|
264
286
|
],
|
@@ -268,22 +290,41 @@ describe('ImageButton', () => {
|
|
268
290
|
it('Updates the attributes of an existing asset image', async () => {
|
269
291
|
const matrixIdentifier = 'matrix-api-identifier';
|
270
292
|
const matrixDomain = 'https://my-matrix.squiz.net';
|
271
|
-
const {
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
293
|
+
const { MockResourceBrowserContext, selectResource } = mockResourceBrowserContext({
|
294
|
+
sources: [{ id: matrixIdentifier }],
|
295
|
+
resources: [
|
296
|
+
{
|
297
|
+
id: 'image-resource-id',
|
298
|
+
name: 'My image resource',
|
299
|
+
type: {
|
300
|
+
code: 'image',
|
301
|
+
name: 'Image',
|
302
|
+
},
|
278
303
|
},
|
279
|
-
|
304
|
+
],
|
280
305
|
});
|
281
306
|
|
307
|
+
const { editor, getJsonContent } = await renderWithEditor(
|
308
|
+
<MockResourceBrowserContext>
|
309
|
+
<ImageButton />
|
310
|
+
</MockResourceBrowserContext>,
|
311
|
+
{
|
312
|
+
content: 'Some <img src="https://httpcats.com/529.jpg" alt="hi" /> nonsense',
|
313
|
+
context: {
|
314
|
+
editor: {
|
315
|
+
matrix: {
|
316
|
+
matrixDomain,
|
317
|
+
},
|
318
|
+
},
|
319
|
+
},
|
320
|
+
},
|
321
|
+
);
|
322
|
+
|
282
323
|
await act(() => editor.selectText(new NodeSelection(editor.state.doc.resolve(6))));
|
283
324
|
|
284
325
|
await openModal();
|
285
|
-
|
286
|
-
|
326
|
+
fireEvent.click(screen.getByRole('button', { name: 'From source' }));
|
327
|
+
await selectResource(screen.getByRole('button', { name: 'Choose image' }), 'My image resource');
|
287
328
|
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
288
329
|
|
289
330
|
await waitForElementToBeRemoved(() => screen.getByRole('button', { name: 'Apply' }));
|
@@ -298,32 +339,21 @@ describe('ImageButton', () => {
|
|
298
339
|
},
|
299
340
|
{
|
300
341
|
type: 'assetImage',
|
301
|
-
attrs: { matrixAssetId: '
|
342
|
+
attrs: { matrixAssetId: 'image-resource-id', matrixIdentifier, matrixDomain },
|
302
343
|
},
|
303
344
|
{ type: 'text', text: ' nonsense' },
|
304
345
|
],
|
305
346
|
});
|
306
347
|
});
|
307
348
|
|
308
|
-
it
|
309
|
-
|
310
|
-
|
311
|
-
|
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' })));
|
349
|
+
it('Shows an error if a resource is not selected', async () => {
|
350
|
+
await renderWithEditor(<ImageButton />);
|
351
|
+
await openModal();
|
352
|
+
fireEvent.click(screen.getByRole('button', { name: 'From source' }));
|
353
|
+
await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
|
323
354
|
|
324
|
-
|
325
|
-
|
326
|
-
);
|
355
|
+
expect(screen.getByText('An image must be selected')).toBeInTheDocument();
|
356
|
+
});
|
327
357
|
|
328
358
|
it('Shows an error if the field value is just an empty space', async () => {
|
329
359
|
await renderWithEditor(<ImageButton />);
|
@@ -332,11 +362,8 @@ describe('ImageButton', () => {
|
|
332
362
|
await act(async () => {
|
333
363
|
fireEvent.change(screen.getByLabelText('Source'), { target: { value: ' ' } });
|
334
364
|
});
|
335
|
-
await act(async () => {
|
336
|
-
fireEvent.change(screen.getByLabelText('Alternative description'), { target: { value: ' ' } });
|
337
|
-
});
|
338
365
|
await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
|
339
366
|
|
340
|
-
expect(screen.getAllByText('Empty space is not allowed')).toHaveLength(
|
367
|
+
expect(screen.getAllByText('Empty space is not allowed')).toHaveLength(1);
|
341
368
|
});
|
342
369
|
});
|
@@ -13,6 +13,7 @@ const mockSubmitFunction = jest.fn();
|
|
13
13
|
const mockCancelFunction = jest.fn();
|
14
14
|
const setup = async () => {
|
15
15
|
const utils = await renderWithEditor(<ImageModal onCancel={mockCancelFunction} onSubmit={mockSubmitFunction} />);
|
16
|
+
fireEvent.click(screen.getByRole('button', { name: 'From URL' }));
|
16
17
|
const sourceInput = screen.getByRole('textbox', { name: /source/i }) as HTMLInputElement;
|
17
18
|
const altInput = screen.getByRole('textbox', { name: /alt/i }) as HTMLInputElement;
|
18
19
|
const widthInput = screen.getByRole('spinbutton', { name: /Width/i }) as HTMLInputElement;
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import ImageForm, { ImageFormData } from './Form/ImageForm';
|
2
2
|
import React from 'react';
|
3
3
|
import { useCurrentSelection } from '@remirror/react';
|
4
|
+
import ImageRoundedIcon from '@mui/icons-material/ImageRounded';
|
4
5
|
import FormModal from '../../../ui/Modal/FormModal';
|
5
6
|
import { SubmitHandler } from 'react-hook-form';
|
6
7
|
import { NodeSelection } from 'prosemirror-state';
|
@@ -16,13 +17,13 @@ const ImageModal = ({ onCancel, onSubmit }: ImageModalProps) => {
|
|
16
17
|
const currentImage = selection?.node;
|
17
18
|
const currentImageAttrs = { ...currentImage?.attrs };
|
18
19
|
const formData = {
|
19
|
-
imageType: currentImage?.type.name === NodeName.
|
20
|
+
imageType: currentImage?.type.name === NodeName.Image ? NodeName.Image : NodeName.AssetImage,
|
20
21
|
image: currentImage?.type?.name === NodeName.Image ? currentImageAttrs : {},
|
21
22
|
assetImage: currentImage?.type?.name === NodeName.AssetImage ? currentImageAttrs : {},
|
22
23
|
};
|
23
24
|
|
24
25
|
return (
|
25
|
-
<FormModal title="Image" onCancel={onCancel}>
|
26
|
+
<FormModal title="Image" icon={<ImageRoundedIcon />} onCancel={onCancel}>
|
26
27
|
<ImageForm data={formData} onSubmit={onSubmit} />
|
27
28
|
</FormModal>
|
28
29
|
);
|
@@ -4,6 +4,7 @@ import React from 'react';
|
|
4
4
|
import { LinkForm } from './LinkForm';
|
5
5
|
import { LinkTarget } from '../../../../Extensions/LinkExtension/common';
|
6
6
|
import { MarkName } from '../../../../Extensions/Extensions';
|
7
|
+
import { mockResourceBrowserContext } from '../../../../../tests';
|
7
8
|
|
8
9
|
describe('Link Form', () => {
|
9
10
|
const handleSubmit = jest.fn();
|
@@ -27,32 +28,40 @@ describe('Link Form', () => {
|
|
27
28
|
it('Renders the form with expected default values when no data is provided', () => {
|
28
29
|
render(<LinkForm onSubmit={handleSubmit} />);
|
29
30
|
|
30
|
-
expect(
|
31
|
-
expect(screen.
|
31
|
+
expect(document.querySelector('div[data-headlessui-state="selected"]')).toHaveTextContent('From source');
|
32
|
+
expect(screen.queryByRole('button', { name: 'Choose asset' })).toBeInTheDocument();
|
32
33
|
expect(screen.getByLabelText('Text')).toHaveValue('');
|
33
|
-
expect(
|
34
|
-
expect(
|
35
|
-
expect(document.querySelectorAll('label')).toHaveLength(5);
|
34
|
+
expect(document.querySelector('div.squiz-fte-checkbox')).toHaveTextContent('Open link in new window');
|
35
|
+
expect(document.querySelectorAll('label')).toHaveLength(1);
|
36
36
|
});
|
37
37
|
|
38
38
|
it('Renders the form with the expected fields for arbitrary links', () => {
|
39
39
|
render(<LinkForm data={data} onSubmit={handleSubmit} />);
|
40
40
|
|
41
|
-
expect(
|
41
|
+
expect(document.querySelector('div[data-headlessui-state="selected"]')).toHaveTextContent('From URL');
|
42
42
|
expect(screen.getByLabelText('URL')).toHaveValue('https://www.squiz.net/link-form');
|
43
43
|
expect(screen.getByLabelText('Text')).toHaveValue('Link text');
|
44
44
|
expect(screen.getByLabelText('Title')).toHaveValue('Link title');
|
45
|
-
expect(
|
46
|
-
expect(document.querySelectorAll('label')).toHaveLength(
|
45
|
+
expect(document.querySelector('div.squiz-fte-checkbox')).toHaveTextContent('Open link in new window');
|
46
|
+
expect(document.querySelectorAll('label')).toHaveLength(3);
|
47
47
|
});
|
48
48
|
|
49
49
|
it('Renders the form with the expected fields for asset links', () => {
|
50
|
-
|
50
|
+
const { MockResourceBrowserContext } = mockResourceBrowserContext({
|
51
|
+
sources: [{ id: 'my-source-id' }],
|
52
|
+
resources: [{ id: '100', name: 'My selected resource' }],
|
53
|
+
});
|
51
54
|
|
52
|
-
|
53
|
-
|
55
|
+
render(
|
56
|
+
<MockResourceBrowserContext>
|
57
|
+
<LinkForm data={{ ...data, linkType: MarkName.AssetLink }} onSubmit={handleSubmit} />
|
58
|
+
</MockResourceBrowserContext>,
|
59
|
+
);
|
60
|
+
|
61
|
+
expect(document.querySelector('div[data-headlessui-state="selected"]')).toHaveTextContent('From source');
|
62
|
+
expect(screen.getByText('My selected resource')).toBeInTheDocument();
|
54
63
|
expect(screen.getByLabelText('Text')).toHaveValue('Link text');
|
55
|
-
expect(
|
56
|
-
expect(document.querySelectorAll('label')).toHaveLength(
|
64
|
+
expect(document.querySelector('div.squiz-fte-checkbox')).toHaveTextContent('Open link in new window');
|
65
|
+
expect(document.querySelectorAll('label')).toHaveLength(1);
|
57
66
|
});
|
58
67
|
});
|
@@ -1,16 +1,17 @@
|
|
1
|
-
import React, { ReactElement
|
1
|
+
import React, { ReactElement } from 'react';
|
2
2
|
import clsx from 'clsx';
|
3
|
-
import { SubmitHandler, useForm } from 'react-hook-form';
|
3
|
+
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
|
4
4
|
import { FromToProps } from 'remirror';
|
5
5
|
import { Input } from '../../../../ui/Fields/Input/Input';
|
6
|
-
import {
|
6
|
+
import { Checkbox } from '../../../../ui/Fields/Checkbox/Checkbox';
|
7
7
|
import { UpdateLinkProps } from '../../../../Extensions/LinkExtension/LinkExtension';
|
8
8
|
import { UpdateAssetLinkProps } from '../../../../Extensions/LinkExtension/AssetLinkExtension';
|
9
9
|
import { LinkTarget } from '../../../../Extensions/LinkExtension/common';
|
10
|
-
import { EditorContext } from '../../../../Editor/EditorContext';
|
11
10
|
import { MarkName } from '../../../../Extensions/Extensions';
|
12
11
|
import { DeepPartial } from '../../../../types';
|
13
|
-
import { noEmptySpacesValidation } from '../../../../utils/validation';
|
12
|
+
import { hasProperties, noEmptySpacesValidation } from '../../../../utils/validation';
|
13
|
+
import { TabOptions, Tabs } from '../../../../ui/Tabs/Tabs';
|
14
|
+
import { MatrixAsset } from '../../../../ui/Fields/MatrixAsset/MatrixAsset';
|
14
15
|
|
15
16
|
export type LinkFormData = {
|
16
17
|
linkType: MarkName;
|
@@ -25,19 +26,14 @@ export type FormProps = {
|
|
25
26
|
onSubmit: SubmitHandler<LinkFormData>;
|
26
27
|
};
|
27
28
|
|
28
|
-
const linkTypeOptions:
|
29
|
-
[MarkName.
|
30
|
-
[MarkName.
|
31
|
-
};
|
32
|
-
|
33
|
-
const targetOptions: SelectOptions = {
|
34
|
-
[LinkTarget.Self]: { label: 'Current window' },
|
35
|
-
[LinkTarget.Blank]: { label: 'New window' },
|
29
|
+
const linkTypeOptions: TabOptions = {
|
30
|
+
[MarkName.AssetLink]: { label: 'From source' },
|
31
|
+
[MarkName.Link]: { label: 'From URL' },
|
36
32
|
};
|
37
33
|
|
38
34
|
export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
|
39
|
-
const context = useContext(EditorContext);
|
40
35
|
const {
|
36
|
+
control,
|
41
37
|
register,
|
42
38
|
handleSubmit,
|
43
39
|
setValue,
|
@@ -46,14 +42,12 @@ export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
|
|
46
42
|
} = useForm<LinkFormData>({
|
47
43
|
defaultValues: data,
|
48
44
|
});
|
49
|
-
const linkType = watch('linkType') || MarkName.
|
45
|
+
const linkType = watch('linkType') || MarkName.AssetLink;
|
50
46
|
|
51
47
|
return (
|
52
48
|
<form className="squiz-fte-form" onSubmit={handleSubmit(onSubmit)}>
|
53
|
-
<div className="squiz-fte-form-group mb-
|
54
|
-
<
|
55
|
-
name="linkType"
|
56
|
-
label="Type"
|
49
|
+
<div className="squiz-fte-form-group mb-4">
|
50
|
+
<Tabs
|
57
51
|
value={linkType}
|
58
52
|
options={linkTypeOptions}
|
59
53
|
onChange={(value) => setValue('linkType', value as MarkName)}
|
@@ -90,25 +84,15 @@ export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
|
|
90
84
|
/>
|
91
85
|
</div>
|
92
86
|
<div className={clsx('squiz-fte-form-group mb-2')}>
|
93
|
-
<Input
|
94
|
-
label="Title"
|
95
|
-
required
|
96
|
-
error={errors?.link?.title?.message}
|
97
|
-
{...register('link.title', {
|
98
|
-
required: 'Title is required',
|
99
|
-
validate: {
|
100
|
-
noEmptySpaces: noEmptySpacesValidation,
|
101
|
-
},
|
102
|
-
})}
|
103
|
-
/>
|
87
|
+
<Input label="Title" error={errors?.link?.title?.message} {...register('link.title')} />
|
104
88
|
</div>
|
105
|
-
<div className={clsx('squiz-fte-form-group mb-
|
106
|
-
<
|
107
|
-
|
108
|
-
label="Target"
|
109
|
-
value={data?.link?.target || '_self'}
|
110
|
-
options={targetOptions}
|
89
|
+
<div className={clsx('squiz-fte-form-group mb-2')}>
|
90
|
+
<Checkbox
|
91
|
+
label="Open link in new window"
|
111
92
|
onChange={(value) => setValue('link.target', value as LinkTarget)}
|
93
|
+
defaultChecked={data?.link?.target === LinkTarget.Blank}
|
94
|
+
unchecked={LinkTarget.Self}
|
95
|
+
checked={LinkTarget.Blank}
|
112
96
|
/>
|
113
97
|
</div>
|
114
98
|
</>
|
@@ -117,21 +101,15 @@ export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
|
|
117
101
|
{linkType === MarkName.AssetLink && (
|
118
102
|
<>
|
119
103
|
<div className={clsx('squiz-fte-form-group mb-2')}>
|
120
|
-
<
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
return 'Invalid asset ID';
|
130
|
-
}
|
131
|
-
},
|
132
|
-
noEmptySpaces: noEmptySpacesValidation,
|
133
|
-
},
|
134
|
-
})}
|
104
|
+
<Controller
|
105
|
+
control={control}
|
106
|
+
name="assetLink"
|
107
|
+
rules={{
|
108
|
+
validate: hasProperties('An asset must be selected', ['matrixIdentifier', 'matrixAssetId']),
|
109
|
+
}}
|
110
|
+
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
111
|
+
<MatrixAsset modalTitle="Insert link" value={value} onChange={onChange} error={error?.message} />
|
112
|
+
)}
|
135
113
|
/>
|
136
114
|
</div>
|
137
115
|
<div className={clsx('squiz-fte-form-group mb-2')}>
|
@@ -147,13 +125,13 @@ export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
|
|
147
125
|
})}
|
148
126
|
/>
|
149
127
|
</div>
|
150
|
-
<div className={clsx('squiz-fte-form-group mb-
|
151
|
-
<
|
152
|
-
|
153
|
-
label="Target"
|
154
|
-
value={data?.assetLink?.target || '_self'}
|
155
|
-
options={targetOptions}
|
128
|
+
<div className={clsx('squiz-fte-form-group mb-2')}>
|
129
|
+
<Checkbox
|
130
|
+
label="Open link in new window"
|
156
131
|
onChange={(value) => setValue('assetLink.target', value as LinkTarget)}
|
132
|
+
defaultChecked={data?.assetLink?.target === '_blank'}
|
133
|
+
unchecked={'_self' as LinkTarget}
|
134
|
+
checked={'_blank' as LinkTarget}
|
157
135
|
/>
|
158
136
|
</div>
|
159
137
|
</>
|