@squiz/formatted-text-editor 1.21.1-alpha.35 → 1.21.1-alpha.39
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 +9 -2
- package/lib/Editor/EditorContext.d.ts +6 -1
- package/lib/Editor/EditorContext.js +1 -1
- package/lib/EditorToolbar/FloatingToolbar.js +1 -1
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.d.ts +9 -8
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +71 -43
- package/lib/EditorToolbar/Tools/Image/ImageButton.js +13 -15
- package/lib/EditorToolbar/Tools/Image/ImageModal.js +7 -1
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +1 -1
- package/lib/Extensions/Extensions.d.ts +5 -0
- package/lib/Extensions/Extensions.js +12 -1
- package/lib/Extensions/ImageExtension/AssetImageExtension.d.ts +17 -0
- package/lib/Extensions/ImageExtension/AssetImageExtension.js +92 -0
- package/lib/Extensions/ImageExtension/ImageExtension.d.ts +1 -4
- package/lib/Extensions/ImageExtension/ImageExtension.js +4 -78
- package/lib/Extensions/LinkExtension/AssetLinkExtension.js +3 -3
- package/lib/Extensions/LinkExtension/LinkExtension.d.ts +1 -3
- package/lib/Extensions/LinkExtension/LinkExtension.js +1 -9
- package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +12 -3
- package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +14 -5
- package/lib/utils/resolveMatrixAssetUrl.d.ts +1 -0
- package/lib/utils/resolveMatrixAssetUrl.js +10 -0
- package/package.json +3 -3
- package/src/Editor/EditorContext.spec.tsx +3 -3
- package/src/Editor/EditorContext.ts +9 -2
- package/src/EditorToolbar/FloatingToolbar.spec.tsx +24 -4
- package/src/EditorToolbar/FloatingToolbar.tsx +1 -1
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +26 -5
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +145 -96
- package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +128 -7
- package/src/EditorToolbar/Tools/Image/ImageButton.tsx +15 -17
- package/src/EditorToolbar/Tools/Image/ImageModal.tsx +7 -1
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +1 -1
- package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +17 -5
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +1 -0
- package/src/Extensions/Extensions.ts +11 -0
- package/src/Extensions/ImageExtension/AssetImageExtension.spec.ts +76 -0
- package/src/Extensions/ImageExtension/AssetImageExtension.ts +111 -0
- package/src/Extensions/ImageExtension/ImageExtension.ts +6 -99
- package/src/Extensions/LinkExtension/AssetLinkExtension.spec.ts +1 -1
- package/src/Extensions/LinkExtension/AssetLinkExtension.ts +3 -3
- package/src/Extensions/LinkExtension/LinkExtension.ts +2 -22
- package/src/utils/converters/mocks/squizNodeJson.mock.ts +19 -0
- package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +13 -3
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +13 -1
- package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +13 -5
- package/src/utils/resolveMatrixAssetUrl.spec.ts +26 -0
- package/src/utils/resolveMatrixAssetUrl.ts +7 -0
@@ -2,14 +2,11 @@ import '@testing-library/jest-dom';
|
|
2
2
|
import { screen, fireEvent, waitForElementToBeRemoved, act } from '@testing-library/react';
|
3
3
|
import { NodeSelection } from 'prosemirror-state';
|
4
4
|
import React from 'react';
|
5
|
-
import { renderWithEditor } from '../../../../tests';
|
5
|
+
import { renderWithEditor, select } from '../../../../tests';
|
6
6
|
import ImageButton from './ImageButton';
|
7
7
|
import { getImageSize } from 'react-image-size';
|
8
8
|
|
9
9
|
jest.mock('react-image-size');
|
10
|
-
beforeEach(() => {
|
11
|
-
(getImageSize as jest.Mock).mockResolvedValue({ width: 2, height: 2 });
|
12
|
-
});
|
13
10
|
|
14
11
|
describe('ImageButton', () => {
|
15
12
|
const openModal = async () => {
|
@@ -17,6 +14,10 @@ describe('ImageButton', () => {
|
|
17
14
|
await screen.findByRole('button', { name: 'Apply' });
|
18
15
|
};
|
19
16
|
|
17
|
+
beforeEach(() => {
|
18
|
+
(getImageSize as jest.Mock).mockResolvedValue({ width: 2, height: 2 });
|
19
|
+
});
|
20
|
+
|
20
21
|
it('Opens the modal when clicking on the image button', async () => {
|
21
22
|
await renderWithEditor(<ImageButton />);
|
22
23
|
|
@@ -98,8 +99,8 @@ describe('ImageButton', () => {
|
|
98
99
|
attrs: {
|
99
100
|
alt: 'Updated cats!',
|
100
101
|
crop: null,
|
101
|
-
height:
|
102
|
-
width:
|
102
|
+
height: 2,
|
103
|
+
width: 2,
|
103
104
|
rotate: null,
|
104
105
|
src: 'https://httpcats.com/303.jpg',
|
105
106
|
title: '',
|
@@ -138,7 +139,8 @@ describe('ImageButton', () => {
|
|
138
139
|
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
139
140
|
expect(modalHeading).not.toBeInTheDocument();
|
140
141
|
});
|
141
|
-
|
142
|
+
|
143
|
+
it('Adds a new image with no source field', async () => {
|
142
144
|
await renderWithEditor(<ImageButton />, { content: 'Some nonsense content here' });
|
143
145
|
|
144
146
|
// open the modal and add an image.
|
@@ -148,6 +150,30 @@ describe('ImageButton', () => {
|
|
148
150
|
expect(await screen.findByText('Source is required')).toBeInTheDocument();
|
149
151
|
});
|
150
152
|
|
153
|
+
it('Adds a new image with data URI in source field', async () => {
|
154
|
+
await renderWithEditor(<ImageButton />);
|
155
|
+
|
156
|
+
const dataUri = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
157
|
+
|
158
|
+
// open the modal and add an image.
|
159
|
+
await openModal();
|
160
|
+
fireEvent.change(screen.getByLabelText('Source'), { target: { value: dataUri } });
|
161
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
162
|
+
expect(await screen.findByText('Must not be a data URI')).toBeInTheDocument();
|
163
|
+
});
|
164
|
+
|
165
|
+
it('Adds a new image with a non-image URL in source field', async () => {
|
166
|
+
(getImageSize as jest.Mock).mockRejectedValue('error: not an image');
|
167
|
+
|
168
|
+
await renderWithEditor(<ImageButton />);
|
169
|
+
|
170
|
+
// open the modal and add an image.
|
171
|
+
await openModal();
|
172
|
+
fireEvent.change(screen.getByLabelText('Source'), { target: { value: 'https://not-an-image.com/not-an-image' } });
|
173
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
174
|
+
expect(await screen.findByText('Must be a valid image URL')).toBeInTheDocument();
|
175
|
+
});
|
176
|
+
|
151
177
|
it('Adds a new image with no alt text', async () => {
|
152
178
|
await renderWithEditor(<ImageButton />, { content: 'Some tacos here' });
|
153
179
|
|
@@ -170,4 +196,99 @@ describe('ImageButton', () => {
|
|
170
196
|
expect(await screen.findByText('Width is required')).toBeInTheDocument();
|
171
197
|
expect(await screen.findByText('Height is required')).toBeInTheDocument();
|
172
198
|
});
|
199
|
+
|
200
|
+
it('Adds a new asset image', async () => {
|
201
|
+
const matrixIdentifier = 'matrix-api-identifier';
|
202
|
+
const matrixDomain = 'https://my-matrix.squiz.net';
|
203
|
+
const { getJsonContent } = await renderWithEditor(<ImageButton />, {
|
204
|
+
content: 'Some nonsense content here',
|
205
|
+
context: {
|
206
|
+
matrix: {
|
207
|
+
matrixIdentifier,
|
208
|
+
matrixDomain,
|
209
|
+
resolveMatrixAsset: () => Promise.resolve({ id: '100', type: 'image' }),
|
210
|
+
},
|
211
|
+
},
|
212
|
+
});
|
213
|
+
|
214
|
+
// open the modal and add an image.
|
215
|
+
await openModal();
|
216
|
+
select(screen.getByLabelText('Type'), 'Asset image');
|
217
|
+
fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: '100' } });
|
218
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
219
|
+
|
220
|
+
await waitForElementToBeRemoved(() => screen.getByRole('button', { name: 'Apply' }));
|
221
|
+
|
222
|
+
expect(getJsonContent()).toEqual({
|
223
|
+
type: 'paragraph',
|
224
|
+
attrs: expect.any(Object),
|
225
|
+
content: [
|
226
|
+
{
|
227
|
+
type: 'assetImage',
|
228
|
+
attrs: { matrixAssetId: '100', matrixIdentifier, matrixDomain },
|
229
|
+
},
|
230
|
+
{ type: 'text', text: 'Some nonsense content here' },
|
231
|
+
],
|
232
|
+
});
|
233
|
+
});
|
234
|
+
|
235
|
+
it('Updates the attributes of an existing asset image', async () => {
|
236
|
+
const matrixIdentifier = 'matrix-api-identifier';
|
237
|
+
const matrixDomain = 'https://my-matrix.squiz.net';
|
238
|
+
const { editor, getJsonContent } = await renderWithEditor(<ImageButton />, {
|
239
|
+
content: 'Some <img src="https://httpcats.com/529.jpg" alt="hi" /> nonsense',
|
240
|
+
context: {
|
241
|
+
matrix: {
|
242
|
+
matrixIdentifier,
|
243
|
+
matrixDomain,
|
244
|
+
resolveMatrixAsset: () => Promise.resolve({ id: '100', type: 'image' }),
|
245
|
+
},
|
246
|
+
},
|
247
|
+
});
|
248
|
+
|
249
|
+
await act(() => editor.selectText(new NodeSelection(editor.state.doc.resolve(6))));
|
250
|
+
|
251
|
+
await openModal();
|
252
|
+
select(screen.getByLabelText('Type'), 'Asset image');
|
253
|
+
fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: '100' } });
|
254
|
+
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
255
|
+
|
256
|
+
await waitForElementToBeRemoved(() => screen.getByRole('button', { name: 'Apply' }));
|
257
|
+
|
258
|
+
expect(getJsonContent()).toEqual({
|
259
|
+
type: 'paragraph',
|
260
|
+
attrs: expect.any(Object),
|
261
|
+
content: [
|
262
|
+
{
|
263
|
+
text: 'Some ',
|
264
|
+
type: 'text',
|
265
|
+
},
|
266
|
+
{
|
267
|
+
type: 'assetImage',
|
268
|
+
attrs: { matrixAssetId: '100', matrixIdentifier, matrixDomain },
|
269
|
+
},
|
270
|
+
{ type: 'text', text: ' nonsense' },
|
271
|
+
],
|
272
|
+
});
|
273
|
+
});
|
274
|
+
|
275
|
+
it.each([
|
276
|
+
['Asset ID not provided', '', null, 'Asset ID is required'],
|
277
|
+
['Asset does not exist', '100', null, 'Asset ID is invalid or not an image'],
|
278
|
+
['Asset is not an image', '100', { id: 100, type: 'physical_file' }, 'Asset ID is invalid or not an image'],
|
279
|
+
])(
|
280
|
+
'Shows an error if an invalid asset ID is provided - %s',
|
281
|
+
async (description: string, assetId: string, asset: any, expectedError: string) => {
|
282
|
+
const resolveMatrixAsset = jest.fn(() => Promise.resolve(asset));
|
283
|
+
|
284
|
+
await renderWithEditor(<ImageButton />, { context: { matrix: { resolveMatrixAsset } } });
|
285
|
+
|
286
|
+
await openModal();
|
287
|
+
select(screen.getByLabelText('Type'), 'Asset image');
|
288
|
+
fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: assetId } });
|
289
|
+
await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
|
290
|
+
|
291
|
+
expect(screen.getByText(expectedError)).toBeInTheDocument();
|
292
|
+
},
|
293
|
+
);
|
173
294
|
});
|
@@ -1,11 +1,12 @@
|
|
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 { useCommands, useKeymap, useActive, usePositioner } from '@remirror/react';
|
7
7
|
import { ImageExtension } from '../../../Extensions/ImageExtension/ImageExtension';
|
8
|
-
import {
|
8
|
+
import { NodeName } from '../../../Extensions/Extensions';
|
9
|
+
import { AssetImageExtension } from '../../../Extensions/ImageExtension/AssetImageExtension';
|
9
10
|
|
10
11
|
type ImageButtonProps = {
|
11
12
|
inPopover?: boolean;
|
@@ -13,27 +14,24 @@ type ImageButtonProps = {
|
|
13
14
|
|
14
15
|
const ImageButton = ({ inPopover = false }: ImageButtonProps) => {
|
15
16
|
const [showModal, setShowModal] = useState(false);
|
16
|
-
const { insertImage } = useCommands();
|
17
|
-
const active = useActive<ImageExtension>();
|
18
|
-
const
|
19
|
-
const { data } = usePositioner<Partial<ToolbarPositionerRange>>(positioner, []);
|
17
|
+
const { insertImage, insertAssetImage } = useCommands<ImageExtension | AssetImageExtension>();
|
18
|
+
const active = useActive<ImageExtension | AssetImageExtension>();
|
19
|
+
const selection = useCurrentSelection();
|
20
20
|
// if the active selection is not an image, disable the button as it means it will be text
|
21
|
-
const disabled =
|
21
|
+
const disabled = !selection.empty && !active.image() && !active.assetImage();
|
22
22
|
|
23
23
|
const handleClick = () => {
|
24
24
|
if (!showModal) {
|
25
|
-
|
26
|
-
// update the selected text in state before showing the modal.
|
27
|
-
requestAnimationFrame(() => {
|
28
|
-
setShowModal(true);
|
29
|
-
});
|
25
|
+
setShowModal(true);
|
30
26
|
}
|
31
27
|
};
|
32
28
|
|
33
29
|
const insertImageFromData = (data: ImageFormData) => {
|
34
|
-
const {
|
35
|
-
if (
|
36
|
-
insertImage(
|
30
|
+
const { imageType, image, assetImage } = data;
|
31
|
+
if (imageType === NodeName.Image) {
|
32
|
+
insertImage(image);
|
33
|
+
} else {
|
34
|
+
insertAssetImage(assetImage);
|
37
35
|
}
|
38
36
|
};
|
39
37
|
|
@@ -59,7 +57,7 @@ const ImageButton = ({ inPopover = false }: ImageButtonProps) => {
|
|
59
57
|
<>
|
60
58
|
<Button
|
61
59
|
handleOnClick={handleClick}
|
62
|
-
isActive={active.image()}
|
60
|
+
isActive={active.image() || active.assetImage()}
|
63
61
|
icon={<ImageRoundedIcon />}
|
64
62
|
label="Image (cmd+L)"
|
65
63
|
isDisabled={disabled}
|
@@ -4,6 +4,7 @@ import { useCurrentSelection } from '@remirror/react';
|
|
4
4
|
import FormModal from '../../../ui/Modal/FormModal';
|
5
5
|
import { SubmitHandler } from 'react-hook-form';
|
6
6
|
import { NodeSelection } from 'prosemirror-state';
|
7
|
+
import { NodeName } from '../../../Extensions/Extensions';
|
7
8
|
|
8
9
|
type ImageModalProps = {
|
9
10
|
onCancel: () => void;
|
@@ -13,10 +14,15 @@ type ImageModalProps = {
|
|
13
14
|
const ImageModal = ({ onCancel, onSubmit }: ImageModalProps) => {
|
14
15
|
const selection = useCurrentSelection() as NodeSelection;
|
15
16
|
const currentImage = selection?.node;
|
17
|
+
const formData = {
|
18
|
+
imageType: currentImage?.type.name === NodeName.AssetImage ? NodeName.AssetImage : NodeName.Image,
|
19
|
+
image: currentImage?.type?.name === NodeName.Image ? currentImage?.attrs : {},
|
20
|
+
assetImage: currentImage?.type?.name === NodeName.AssetImage ? currentImage?.attrs : {},
|
21
|
+
};
|
16
22
|
|
17
23
|
return (
|
18
24
|
<FormModal title="Image" onCancel={onCancel}>
|
19
|
-
<ImageForm data={
|
25
|
+
<ImageForm data={formData} onSubmit={onSubmit} />
|
20
26
|
</FormModal>
|
21
27
|
);
|
22
28
|
};
|
@@ -91,7 +91,7 @@ export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
|
|
91
91
|
{...register('assetLink.matrixAssetId', {
|
92
92
|
validate: {
|
93
93
|
isValidAsset: async (assetId: string | undefined) => {
|
94
|
-
if (assetId && !(await context.matrix.
|
94
|
+
if (assetId && !(await context.matrix.resolveMatrixAsset(assetId))) {
|
95
95
|
return 'Invalid asset ID';
|
96
96
|
}
|
97
97
|
},
|
@@ -261,7 +261,13 @@ describe('LinkButton', () => {
|
|
261
261
|
const matrixIdentifier = 'matrix-api-identifier';
|
262
262
|
const matrixDomain = 'https://my-matrix.squiz.net';
|
263
263
|
const { getJsonContent } = await renderWithEditor(<LinkButton />, {
|
264
|
-
context: {
|
264
|
+
context: {
|
265
|
+
matrix: {
|
266
|
+
matrixIdentifier,
|
267
|
+
matrixDomain,
|
268
|
+
resolveMatrixAsset: () => Promise.resolve({ id: '100', type: 'physical_file' }),
|
269
|
+
},
|
270
|
+
},
|
265
271
|
});
|
266
272
|
|
267
273
|
await openModal();
|
@@ -297,7 +303,13 @@ describe('LinkButton', () => {
|
|
297
303
|
content:
|
298
304
|
'<a href="https://www.example.org/my-link">Sample link</a> with ' +
|
299
305
|
'<a href="https://www.example.org/another-link">another link</a>',
|
300
|
-
context: {
|
306
|
+
context: {
|
307
|
+
matrix: {
|
308
|
+
matrixIdentifier,
|
309
|
+
matrixDomain,
|
310
|
+
resolveMatrixAsset: () => Promise.resolve({ id: '100', type: 'physical_file' }),
|
311
|
+
},
|
312
|
+
},
|
301
313
|
});
|
302
314
|
|
303
315
|
await act(() => editor.selectText(5));
|
@@ -340,9 +352,9 @@ describe('LinkButton', () => {
|
|
340
352
|
});
|
341
353
|
|
342
354
|
it('Shows an error if an invalid asset ID is provided', async () => {
|
343
|
-
const
|
355
|
+
const resolveMatrixAsset = jest.fn(() => Promise.resolve(null));
|
344
356
|
|
345
|
-
await renderWithEditor(<LinkButton />, { context: { matrix: {
|
357
|
+
await renderWithEditor(<LinkButton />, { context: { matrix: { resolveMatrixAsset } } });
|
346
358
|
|
347
359
|
await openModal();
|
348
360
|
select(screen.getByLabelText('Type'), 'Link to asset');
|
@@ -350,6 +362,6 @@ describe('LinkButton', () => {
|
|
350
362
|
await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
|
351
363
|
|
352
364
|
expect(screen.getByText('Invalid asset ID')).toBeInTheDocument();
|
353
|
-
expect(
|
365
|
+
expect(resolveMatrixAsset).toHaveBeenCalledWith('invalid-asset-id');
|
354
366
|
});
|
355
367
|
});
|
@@ -7,6 +7,7 @@ import RemoveLinkButton from './RemoveLinkButton';
|
|
7
7
|
describe('RemoveLinkButton', () => {
|
8
8
|
it('Removes a link', async () => {
|
9
9
|
const { editor, getJsonContent } = await renderWithEditor(<RemoveLinkButton />, {
|
10
|
+
context: { matrix: { matrixDomain: 'my-matrix.squiz.net' } },
|
10
11
|
content: {
|
11
12
|
type: 'doc',
|
12
13
|
content: [
|
@@ -14,6 +14,13 @@ import { LinkExtension } from './LinkExtension/LinkExtension';
|
|
14
14
|
import { ImageExtension } from './ImageExtension/ImageExtension';
|
15
15
|
import { CommandsExtension } from './CommandsExtension/CommandsExtension';
|
16
16
|
import { EditorContextOptions } from '../Editor/EditorContext';
|
17
|
+
import { AssetImageExtension } from './ImageExtension/AssetImageExtension';
|
18
|
+
|
19
|
+
export enum NodeName {
|
20
|
+
Image = 'image',
|
21
|
+
AssetImage = 'assetImage',
|
22
|
+
Text = 'text',
|
23
|
+
}
|
17
24
|
|
18
25
|
export enum MarkName {
|
19
26
|
Link = 'link',
|
@@ -34,6 +41,10 @@ export const createExtensions = (context: EditorContextOptions) => {
|
|
34
41
|
new HistoryExtension(),
|
35
42
|
new ImageExtension(),
|
36
43
|
new ImageExtension({ preferPastedTextContent: false }),
|
44
|
+
new AssetImageExtension({
|
45
|
+
matrixIdentifier: context.matrix.matrixIdentifier,
|
46
|
+
matrixDomain: context.matrix.matrixDomain,
|
47
|
+
}),
|
37
48
|
new LinkExtension(),
|
38
49
|
new AssetLinkExtension({
|
39
50
|
matrixIdentifier: context.matrix.matrixIdentifier,
|
@@ -0,0 +1,76 @@
|
|
1
|
+
import { renderWithEditor } from '../../../tests';
|
2
|
+
|
3
|
+
describe('AssetImageExtension', () => {
|
4
|
+
it('Parses HTML content representing an asset image', async () => {
|
5
|
+
const { getJsonContent } = await renderWithEditor(null, {
|
6
|
+
content: `<img
|
7
|
+
src="https://my-matrix.squiz.net/?a=this-is-actually-ignored"
|
8
|
+
data-matrix-asset-id="123"
|
9
|
+
data-matrix-identifier="matrix-api-identifier"
|
10
|
+
data-matrix-domain="https://matrix-domain.squiz.net" />`,
|
11
|
+
});
|
12
|
+
|
13
|
+
expect(getJsonContent()).toEqual({
|
14
|
+
type: 'paragraph',
|
15
|
+
attrs: expect.any(Object),
|
16
|
+
content: [
|
17
|
+
{
|
18
|
+
type: 'assetImage',
|
19
|
+
attrs: {
|
20
|
+
matrixAssetId: '123',
|
21
|
+
matrixDomain: 'https://matrix-domain.squiz.net',
|
22
|
+
matrixIdentifier: 'matrix-api-identifier',
|
23
|
+
},
|
24
|
+
},
|
25
|
+
],
|
26
|
+
});
|
27
|
+
});
|
28
|
+
|
29
|
+
it('Resolves to a regular image if HTML content is missing some of the expected attributes', async () => {
|
30
|
+
const { getJsonContent } = await renderWithEditor(null, {
|
31
|
+
content: '<img src="https://my-matrix.squiz.net/?a=123" data-matrix-asset-id="123" />',
|
32
|
+
});
|
33
|
+
|
34
|
+
expect(getJsonContent()).toEqual({
|
35
|
+
type: 'paragraph',
|
36
|
+
attrs: expect.any(Object),
|
37
|
+
content: [
|
38
|
+
{
|
39
|
+
type: 'image',
|
40
|
+
attrs: expect.objectContaining({
|
41
|
+
src: 'https://my-matrix.squiz.net/?a=123',
|
42
|
+
}),
|
43
|
+
},
|
44
|
+
],
|
45
|
+
});
|
46
|
+
});
|
47
|
+
|
48
|
+
it('Outputs expected HTML', async () => {
|
49
|
+
const { getHtmlContent } = await renderWithEditor(null, {
|
50
|
+
content: {
|
51
|
+
type: 'paragraph',
|
52
|
+
content: [
|
53
|
+
{
|
54
|
+
type: 'assetImage',
|
55
|
+
attrs: {
|
56
|
+
matrixAssetId: '123',
|
57
|
+
matrixDomain: 'https://matrix-domain.squiz.net',
|
58
|
+
matrixIdentifier: 'matrix-api-identifier',
|
59
|
+
},
|
60
|
+
},
|
61
|
+
],
|
62
|
+
},
|
63
|
+
});
|
64
|
+
|
65
|
+
expect(getHtmlContent()).toEqual(
|
66
|
+
'<img ' +
|
67
|
+
'src="https://matrix-domain.squiz.net/_nocache?a=123" ' +
|
68
|
+
'data-matrix-asset-id="123" ' +
|
69
|
+
'data-matrix-identifier="matrix-api-identifier" ' +
|
70
|
+
'data-matrix-domain="https://matrix-domain.squiz.net" ' +
|
71
|
+
'draggable="true">' +
|
72
|
+
'<img class="ProseMirror-separator" alt="">' +
|
73
|
+
'<br class="ProseMirror-trailingBreak">',
|
74
|
+
);
|
75
|
+
});
|
76
|
+
});
|
@@ -0,0 +1,111 @@
|
|
1
|
+
import {
|
2
|
+
ApplySchemaAttributes,
|
3
|
+
command,
|
4
|
+
extension,
|
5
|
+
ExtensionPriority,
|
6
|
+
ExtensionTag,
|
7
|
+
isElementDomNode,
|
8
|
+
NodeExtension,
|
9
|
+
NodeExtensionSpec,
|
10
|
+
NodeSpecOverride,
|
11
|
+
omitExtraAttributes,
|
12
|
+
CommandFunction,
|
13
|
+
} from '@remirror/core';
|
14
|
+
import { getTextSelection } from 'remirror';
|
15
|
+
import { resolveMatrixAssetUrl } from '../../utils/resolveMatrixAssetUrl';
|
16
|
+
import { NodeName } from '../Extensions';
|
17
|
+
|
18
|
+
export type AssetImageOptions = {
|
19
|
+
matrixIdentifier?: string;
|
20
|
+
matrixDomain?: string;
|
21
|
+
};
|
22
|
+
|
23
|
+
export type AssetImageAttributes = {
|
24
|
+
matrixAssetId: string;
|
25
|
+
matrixIdentifier: string;
|
26
|
+
matrixDomain: string;
|
27
|
+
};
|
28
|
+
|
29
|
+
@extension<AssetImageOptions>({
|
30
|
+
defaultOptions: {
|
31
|
+
matrixIdentifier: '',
|
32
|
+
matrixDomain: '',
|
33
|
+
},
|
34
|
+
defaultPriority: ExtensionPriority.High,
|
35
|
+
})
|
36
|
+
export class AssetImageExtension extends NodeExtension<AssetImageOptions> {
|
37
|
+
get name() {
|
38
|
+
return NodeName.AssetImage as const;
|
39
|
+
}
|
40
|
+
|
41
|
+
createTags() {
|
42
|
+
return [ExtensionTag.InlineNode, ExtensionTag.Media];
|
43
|
+
}
|
44
|
+
|
45
|
+
createNodeSpec(extra: ApplySchemaAttributes, override: NodeSpecOverride): NodeExtensionSpec {
|
46
|
+
return {
|
47
|
+
inline: true,
|
48
|
+
draggable: true,
|
49
|
+
selectable: true,
|
50
|
+
...override,
|
51
|
+
attrs: {
|
52
|
+
...extra.defaults(),
|
53
|
+
matrixAssetId: {},
|
54
|
+
matrixIdentifier: { default: this.options.matrixIdentifier },
|
55
|
+
matrixDomain: { default: this.options.matrixDomain },
|
56
|
+
},
|
57
|
+
parseDOM: [
|
58
|
+
{
|
59
|
+
tag: 'img[data-matrix-asset-id]',
|
60
|
+
getAttrs: (node) => {
|
61
|
+
if (!isElementDomNode(node)) {
|
62
|
+
return false;
|
63
|
+
}
|
64
|
+
|
65
|
+
const matrixAssetId = node.getAttribute('data-matrix-asset-id');
|
66
|
+
const matrixIdentifier = node.getAttribute('data-matrix-identifier');
|
67
|
+
const matrixDomain = node.getAttribute('data-matrix-domain');
|
68
|
+
|
69
|
+
if (!matrixAssetId || !matrixIdentifier || !matrixDomain) {
|
70
|
+
return false;
|
71
|
+
}
|
72
|
+
|
73
|
+
return {
|
74
|
+
...extra.parse(node),
|
75
|
+
matrixAssetId,
|
76
|
+
matrixIdentifier,
|
77
|
+
matrixDomain,
|
78
|
+
};
|
79
|
+
},
|
80
|
+
},
|
81
|
+
],
|
82
|
+
toDOM: (node) => {
|
83
|
+
const { matrixAssetId, matrixIdentifier, matrixDomain, ...rest } = omitExtraAttributes(node.attrs, extra);
|
84
|
+
|
85
|
+
return [
|
86
|
+
'img',
|
87
|
+
{
|
88
|
+
...extra.dom(node),
|
89
|
+
...rest,
|
90
|
+
src: resolveMatrixAssetUrl(String(matrixAssetId), String(matrixDomain)),
|
91
|
+
'data-matrix-asset-id': matrixAssetId,
|
92
|
+
'data-matrix-identifier': matrixIdentifier,
|
93
|
+
'data-matrix-domain': matrixDomain,
|
94
|
+
},
|
95
|
+
];
|
96
|
+
},
|
97
|
+
};
|
98
|
+
}
|
99
|
+
|
100
|
+
@command()
|
101
|
+
insertAssetImage(attrs: AssetImageAttributes): CommandFunction {
|
102
|
+
return ({ tr, dispatch }) => {
|
103
|
+
const { from, to } = getTextSelection(tr.selection, tr.doc);
|
104
|
+
const node = this.type.create(attrs);
|
105
|
+
|
106
|
+
dispatch?.(tr.replaceRangeWith(from, to, node));
|
107
|
+
|
108
|
+
return true;
|
109
|
+
};
|
110
|
+
}
|
111
|
+
}
|
@@ -1,112 +1,19 @@
|
|
1
1
|
import { ImageExtension as RemirrorImageExtension } from 'remirror/extensions';
|
2
2
|
import { PasteRule } from 'prosemirror-paste-rules';
|
3
|
-
import {
|
4
|
-
isElementDomNode,
|
5
|
-
omitExtraAttributes,
|
6
|
-
ApplySchemaAttributes,
|
7
|
-
NodeSpecOverride,
|
8
|
-
NodeExtensionSpec,
|
9
|
-
getTextSelection,
|
10
|
-
PrimitiveSelection,
|
11
|
-
} from '@remirror/core';
|
12
|
-
import { ImageAttributes } from '@remirror/extension-image/dist-types/image-extension';
|
13
|
-
import { CommandFunction } from '@remirror/pm';
|
14
|
-
|
15
|
-
/**
|
16
|
-
* Get the width and the height of the image.
|
17
|
-
*/
|
18
|
-
function getDimensions(element: HTMLElement) {
|
19
|
-
let { width, height } = element.style;
|
20
|
-
width = width || element.getAttribute('width') || '';
|
21
|
-
height = height || element.getAttribute('height') || '';
|
22
|
-
|
23
|
-
return { width, height };
|
24
|
-
}
|
25
|
-
|
26
|
-
/**
|
27
|
-
* Retrieve attributes from the dom for the image extension.
|
28
|
-
*/
|
29
|
-
function getImageAttributes({ element, parse }: { element: HTMLElement; parse: ApplySchemaAttributes['parse'] }) {
|
30
|
-
const { width, height } = getDimensions(element);
|
31
|
-
|
32
|
-
return {
|
33
|
-
...parse(element),
|
34
|
-
alt: element.getAttribute('alt') ?? '',
|
35
|
-
height: Number.parseInt(height || '0', 10) || null,
|
36
|
-
src: element.getAttribute('src') ?? null,
|
37
|
-
title: element.getAttribute('title') ?? '',
|
38
|
-
width: Number.parseInt(width || '0', 10) || null,
|
39
|
-
fileName: element.getAttribute('data-file-name') ?? null,
|
40
|
-
};
|
41
|
-
}
|
3
|
+
import { ApplySchemaAttributes, NodeSpecOverride, NodeExtensionSpec } from '@remirror/core';
|
42
4
|
|
43
5
|
export class ImageExtension extends RemirrorImageExtension {
|
44
6
|
createPasteRules(): PasteRule[] {
|
45
|
-
|
46
|
-
|
47
|
-
type: 'file',
|
48
|
-
regexp: /image/i,
|
49
|
-
fileHandler: (): boolean => {
|
50
|
-
return false;
|
51
|
-
},
|
52
|
-
},
|
53
|
-
];
|
7
|
+
// override super behaviour of handling file uploads.
|
8
|
+
return [];
|
54
9
|
}
|
55
10
|
|
56
11
|
createNodeSpec(extra: ApplySchemaAttributes, override: NodeSpecOverride): NodeExtensionSpec {
|
57
|
-
const
|
12
|
+
const spec = super.createNodeSpec(extra, override);
|
13
|
+
|
58
14
|
return {
|
59
|
-
|
60
|
-
draggable: true,
|
15
|
+
...spec,
|
61
16
|
selectable: true,
|
62
|
-
...override,
|
63
|
-
attrs: {
|
64
|
-
...extra.defaults(),
|
65
|
-
alt: { default: '' },
|
66
|
-
crop: { default: null },
|
67
|
-
height: { default: null },
|
68
|
-
width: { default: null },
|
69
|
-
rotate: { default: null },
|
70
|
-
src: { default: null },
|
71
|
-
title: { default: '' },
|
72
|
-
fileName: { default: null },
|
73
|
-
|
74
|
-
resizable: { default: false },
|
75
|
-
},
|
76
|
-
parseDOM: [
|
77
|
-
{
|
78
|
-
tag: 'img[src]',
|
79
|
-
getAttrs: (element) => {
|
80
|
-
if (isElementDomNode(element)) {
|
81
|
-
const attrs = getImageAttributes({ element, parse: extra.parse });
|
82
|
-
|
83
|
-
if (preferPastedTextContent && attrs.src?.startsWith('file:///')) {
|
84
|
-
return false;
|
85
|
-
}
|
86
|
-
|
87
|
-
return attrs;
|
88
|
-
}
|
89
|
-
|
90
|
-
return {};
|
91
|
-
},
|
92
|
-
},
|
93
|
-
...(override.parseDOM ?? []),
|
94
|
-
],
|
95
|
-
toDOM: (node) => {
|
96
|
-
const attrs = omitExtraAttributes(node.attrs, extra);
|
97
|
-
return ['img', { ...extra.dom(node), ...attrs }];
|
98
|
-
},
|
99
|
-
};
|
100
|
-
}
|
101
|
-
|
102
|
-
insertImage(attributes: ImageAttributes, selection?: PrimitiveSelection): CommandFunction {
|
103
|
-
return ({ tr, dispatch }) => {
|
104
|
-
const { from, to } = getTextSelection(selection ?? tr.selection, tr.doc);
|
105
|
-
const node = this.type.create(attributes);
|
106
|
-
|
107
|
-
dispatch?.(tr.replaceRangeWith(from, to, node));
|
108
|
-
|
109
|
-
return true;
|
110
17
|
};
|
111
18
|
}
|
112
19
|
}
|
@@ -92,7 +92,7 @@ describe('AssetLinkExtension', () => {
|
|
92
92
|
|
93
93
|
expect(getHtmlContent()).toEqual(
|
94
94
|
'<a rel="noopener noreferrer nofollow" ' +
|
95
|
-
'href="
|
95
|
+
'href="https://matrix-domain.squiz.net/_nocache?a=123" ' +
|
96
96
|
'target="_blank" ' +
|
97
97
|
'data-matrix-asset-id="123" ' +
|
98
98
|
'data-matrix-identifier="matrix-api-identifier" ' +
|