@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.
Files changed (48) hide show
  1. package/demo/App.tsx +9 -2
  2. package/lib/Editor/EditorContext.d.ts +6 -1
  3. package/lib/Editor/EditorContext.js +1 -1
  4. package/lib/EditorToolbar/FloatingToolbar.js +1 -1
  5. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.d.ts +9 -8
  6. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +71 -43
  7. package/lib/EditorToolbar/Tools/Image/ImageButton.js +13 -15
  8. package/lib/EditorToolbar/Tools/Image/ImageModal.js +7 -1
  9. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +1 -1
  10. package/lib/Extensions/Extensions.d.ts +5 -0
  11. package/lib/Extensions/Extensions.js +12 -1
  12. package/lib/Extensions/ImageExtension/AssetImageExtension.d.ts +17 -0
  13. package/lib/Extensions/ImageExtension/AssetImageExtension.js +92 -0
  14. package/lib/Extensions/ImageExtension/ImageExtension.d.ts +1 -4
  15. package/lib/Extensions/ImageExtension/ImageExtension.js +4 -78
  16. package/lib/Extensions/LinkExtension/AssetLinkExtension.js +3 -3
  17. package/lib/Extensions/LinkExtension/LinkExtension.d.ts +1 -3
  18. package/lib/Extensions/LinkExtension/LinkExtension.js +1 -9
  19. package/lib/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.js +12 -3
  20. package/lib/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.js +14 -5
  21. package/lib/utils/resolveMatrixAssetUrl.d.ts +1 -0
  22. package/lib/utils/resolveMatrixAssetUrl.js +10 -0
  23. package/package.json +3 -3
  24. package/src/Editor/EditorContext.spec.tsx +3 -3
  25. package/src/Editor/EditorContext.ts +9 -2
  26. package/src/EditorToolbar/FloatingToolbar.spec.tsx +24 -4
  27. package/src/EditorToolbar/FloatingToolbar.tsx +1 -1
  28. package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +26 -5
  29. package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +145 -96
  30. package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +128 -7
  31. package/src/EditorToolbar/Tools/Image/ImageButton.tsx +15 -17
  32. package/src/EditorToolbar/Tools/Image/ImageModal.tsx +7 -1
  33. package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +1 -1
  34. package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +17 -5
  35. package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +1 -0
  36. package/src/Extensions/Extensions.ts +11 -0
  37. package/src/Extensions/ImageExtension/AssetImageExtension.spec.ts +76 -0
  38. package/src/Extensions/ImageExtension/AssetImageExtension.ts +111 -0
  39. package/src/Extensions/ImageExtension/ImageExtension.ts +6 -99
  40. package/src/Extensions/LinkExtension/AssetLinkExtension.spec.ts +1 -1
  41. package/src/Extensions/LinkExtension/AssetLinkExtension.ts +3 -3
  42. package/src/Extensions/LinkExtension/LinkExtension.ts +2 -22
  43. package/src/utils/converters/mocks/squizNodeJson.mock.ts +19 -0
  44. package/src/utils/converters/remirrorNodeToSquizNode/remirrorNodeToSquizNode.ts +13 -3
  45. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.spec.ts +13 -1
  46. package/src/utils/converters/squizNodeToRemirrorNode/squizNodeToRemirrorNode.ts +13 -5
  47. package/src/utils/resolveMatrixAssetUrl.spec.ts +26 -0
  48. 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: null,
102
- width: null,
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
- it('Adds a new image with Source field', async () => {
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, { useState, useCallback, useMemo } from '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 { createToolbarPositioner, ToolbarPositionerRange } from '../../../utils/createToolbarPositioner';
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 positioner = useMemo(() => createToolbarPositioner({ types: ['link'] }), []);
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 = data.isSelectionInView && !active.image() ? true : false;
21
+ const disabled = !selection.empty && !active.image() && !active.assetImage();
22
22
 
23
23
  const handleClick = () => {
24
24
  if (!showModal) {
25
- // form element are uncontrolled, let the event loop run to
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 { src, alt, width, height } = data;
35
- if (src) {
36
- insertImage({ src, alt, width, height });
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={{ ...currentImage?.attrs, src: currentImage?.attrs.src }} onSubmit={onSubmit} />
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.isValidMatrixAssetId(assetId))) {
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: { matrix: { matrixIdentifier, matrixDomain } },
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: { matrix: { matrixIdentifier, matrixDomain } },
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 isValidMatrixAssetId = jest.fn(() => Promise.resolve(false));
355
+ const resolveMatrixAsset = jest.fn(() => Promise.resolve(null));
344
356
 
345
- await renderWithEditor(<LinkButton />, { context: { matrix: { isValidMatrixAssetId } } });
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(isValidMatrixAssetId).toHaveBeenCalledWith('invalid-asset-id');
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
- return [
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 { preferPastedTextContent } = this.options;
12
+ const spec = super.createNodeSpec(extra, override);
13
+
58
14
  return {
59
- inline: true,
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="/?a=123" ' +
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" ' +